Coverage for tsfpga/yosys/test/test_project.py: 100%

356 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-09 00:40 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project, a project platform for modern FPGA development. 

5# https://tsfpga.com 

6# https://github.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9import gc 

10import sys 

11import threading 

12import time 

13from pathlib import Path 

14from unittest.mock import patch 

15 

16import pytest 

17 

18from tsfpga.module import get_modules 

19from tsfpga.system_utils import create_file 

20from tsfpga.vivado.build_result_checker import EqualTo, Ffs, TotalLuts 

21from tsfpga.vivado.generics import BitVectorGenericValue, StringGenericValue 

22from tsfpga.yosys.project import ( 

23 YosysIntelNetlistBuild, 

24 YosysMicrochipNetlistBuild, 

25 YosysNetlistBuild, 

26 YosysXilinxNetlistBuild, 

27 _get_ghdl_generic_value, 

28 _get_verilog_parameter_value, 

29 _suppress_stdout, 

30) 

31 

32# ruff: noqa: ARG002 

33 

34 

35def _create_module(modules_path, module_name="apa", top_name=None): 

36 top_name = f"{module_name}_top" if top_name is None else top_name 

37 create_file( 

38 modules_path / module_name / "src" / f"{top_name}.vhd", 

39 f"""\ 

40library ieee; 

41use ieee.std_logic_1164.all; 

42 

43entity {top_name} is 

44 port ( 

45 clk : in std_logic 

46 ); 

47end entity; 

48 

49architecture a of {top_name} is 

50begin 

51end architecture; 

52""", 

53 ) 

54 return get_modules(modules_folder=modules_path) 

55 

56 

57def test_casting_to_string(): 

58 project = YosysNetlistBuild(name="my_project", modules=[]) 

59 assert ( 

60 str(project) 

61 == """\ 

62my_project 

63Type: YosysNetlistBuild 

64Top level: my_project_top 

65Generics: - 

66""" 

67 ) 

68 

69 project = YosysNetlistBuild( 

70 name="my_project", 

71 modules=[], 

72 top="apa", 

73 generics={"hest": True, "zebra": 3, "foo": StringGenericValue("test")}, 

74 ) 

75 assert ( 

76 str(project) 

77 == """\ 

78my_project 

79Type: YosysNetlistBuild 

80Top level: apa 

81Generics: hest=True, zebra=3, foo=test 

82""" 

83 ) 

84 

85 project = YosysNetlistBuild(name="my_project", modules=[], apa=123, hest=456) 

86 assert ( 

87 str(project) 

88 == """\ 

89my_project 

90Type: YosysNetlistBuild 

91Top level: my_project_top 

92Generics: - 

93Arguments: apa=123, hest=456 

94""" 

95 ) 

96 

97 

98def test_modules_list_should_be_copied(): 

99 modules = [1] 

100 proj = YosysNetlistBuild(name="name", modules=modules) 

101 

102 modules.append(2) 

103 assert len(proj.modules) == 1 

104 

105 

106def test_static_generics_dictionary_should_be_copied(): 

107 generics = {"apa": 3} 

108 proj = YosysNetlistBuild(name="name", modules=[], generics=generics) 

109 

110 generics["apa"] = False 

111 assert proj.static_generics["apa"] == 3 

112 

113 

114def test_build_result_checkers_list_should_be_copied(): 

115 checkers = [TotalLuts(EqualTo(3))] 

116 proj = YosysNetlistBuild(name="name", modules=[], build_result_checkers=checkers) 

117 

118 checkers.append(Ffs(EqualTo(4))) 

119 assert len(proj.build_result_checkers) == 1 

120 

121 

122def test_vhdl_entities_list_should_be_copied(): 

123 vhdl_entities = ["apa"] 

124 proj = YosysNetlistBuild(name="name", modules=[], vhdl_entities=vhdl_entities) 

125 

126 vhdl_entities.append("hest") 

127 assert len(proj.vhdl_entities) == 1 

128 

129 

130def test_top_name(): 

131 assert YosysNetlistBuild(name="apa", modules=[]).top == "apa_top" 

132 assert YosysNetlistBuild(name="apa", modules=[], top="hest").top == "hest" 

133 

134 

135def test_project_file_name_is_same_as_project_name(): 

136 project = YosysNetlistBuild(name="apa", modules=[]) 

137 assert project.project_file(Path("/hest")) == Path("/hest/apa.ys") 

138 

139 

140def test_xilinx_netlist_build_synth_command(): 

141 project = YosysXilinxNetlistBuild(name="apa", modules=[]) 

142 assert project._get_synth_command() == "synth_xilinx -top apa_top -flatten" # noqa: SLF001 

143 

144 project = YosysXilinxNetlistBuild(name="apa", modules=[], family="xc7") 

145 assert ( 

146 project._get_synth_command() # noqa: SLF001 

147 == "synth_xilinx -family xc7 -top apa_top -flatten" 

148 ) 

149 

150 

151def test_intel_netlist_build_synth_command(): 

152 project = YosysIntelNetlistBuild(name="apa", modules=[]) 

153 # 'synth_intel' does not accept a '-flatten' flag, unlike 'synth_xilinx'. 

154 assert project._get_synth_command() == "synth_intel -top apa_top" # noqa: SLF001 

155 

156 project = YosysIntelNetlistBuild(name="apa", modules=[], family="cycloneiv") 

157 assert ( 

158 project._get_synth_command() # noqa: SLF001 

159 == "synth_intel -family cycloneiv -top apa_top" 

160 ) 

161 

162 

163def test_microchip_netlist_build_synth_command(): 

164 project = YosysMicrochipNetlistBuild(name="apa", modules=[]) 

165 assert project._get_synth_command() == "synth_microchip -top apa_top" # noqa: SLF001 

166 

167 project = YosysMicrochipNetlistBuild(name="apa", modules=[], family="polarfire") 

168 assert ( 

169 project._get_synth_command() # noqa: SLF001 

170 == "synth_microchip -family polarfire -top apa_top" 

171 ) 

172 

173 project = YosysMicrochipNetlistBuild(name="apa", modules=[], discard_ffinit=True) 

174 assert ( 

175 project._get_synth_command() # noqa: SLF001 

176 == "synth_microchip -discard-ffinit -top apa_top" 

177 ) 

178 

179 

180def test_synth_command_and_arguments_can_be_set_when_subclassing(): 

181 class YosysIntelAlmNetlistBuild(YosysNetlistBuild): 

182 _synth_command = "synth_intel_alm" 

183 _synth_arguments = ("-noiopad",) 

184 

185 project = YosysIntelAlmNetlistBuild(name="apa", modules=[]) 

186 assert ( 

187 project._get_synth_command() # noqa: SLF001 

188 == "synth_intel_alm -noiopad -top apa_top -flatten" 

189 ) 

190 

191 # Arguments set by a subclass are kept when the parent class adds its own. 

192 class YosysXilinxNoBramNetlistBuild(YosysXilinxNetlistBuild): 

193 _synth_arguments = ("-nobram",) 

194 

195 project = YosysXilinxNoBramNetlistBuild(name="apa", modules=[], family="xc7") 

196 assert ( 

197 project._get_synth_command() # noqa: SLF001 

198 == "synth_xilinx -nobram -family xc7 -top apa_top -flatten" 

199 ) 

200 

201 

202def test_synth_arguments_are_not_shared_between_objects(): 

203 YosysXilinxNetlistBuild(name="apa", modules=[], family="xc7") 

204 

205 # Adding the family argument to the first object must not affect the class, or any 

206 # object created after it. 

207 assert YosysXilinxNetlistBuild._synth_arguments == () # noqa: SLF001 

208 project = YosysXilinxNetlistBuild(name="hest", modules=[]) 

209 assert project._get_synth_command() == "synth_xilinx -top hest_top -flatten" # noqa: SLF001 

210 

211 

212def test_get_ghdl_generic_value(): 

213 assert _get_ghdl_generic_value(True) == "true" 

214 assert _get_ghdl_generic_value(False) == "false" 

215 assert _get_ghdl_generic_value(3) == "3" 

216 assert _get_ghdl_generic_value(3.5) == "3.5" 

217 assert _get_ghdl_generic_value(BitVectorGenericValue("1010")) == "1010" 

218 assert _get_ghdl_generic_value(StringGenericValue("hest")) == "hest" 

219 

220 with pytest.raises(TypeError): 

221 _get_ghdl_generic_value("apa") 

222 

223 

224def test_get_read_verilog_command_returns_none_if_no_verilog_files(tmp_path): 

225 modules = _create_module(tmp_path) 

226 project = YosysNetlistBuild(name="apa", modules=modules) 

227 

228 assert project._get_read_verilog_command() is None # noqa: SLF001 

229 

230 

231def test_get_read_verilog_command_with_verilog_and_systemverilog_files(tmp_path): 

232 src_path = tmp_path / "apa" / "src" 

233 create_file(src_path / "counter.v", "module counter (); endmodule\n") 

234 create_file(src_path / "adder.sv", "module adder (); endmodule\n") 

235 create_file(src_path / "counter_defines.vh", "`define WIDTH 8\n") 

236 

237 modules = _create_module(tmp_path) 

238 project = YosysNetlistBuild(name="apa", modules=modules) 

239 

240 command = project._get_read_verilog_command() # noqa: SLF001 

241 

242 assert command is not None 

243 assert command.startswith("read_verilog -sv ") 

244 # Include directories must NOT be quoted, Yosys would treat the quotes as part of the path. 

245 assert f"-I{src_path.resolve().as_posix()} " in command 

246 assert '-I"' not in command 

247 assert (src_path / "counter.v").resolve().as_posix() in command 

248 assert (src_path / "adder.sv").resolve().as_posix() in command 

249 # Header files are 'include'd by the source files, not passed as source arguments themselves. 

250 assert "counter_defines.vh" not in command 

251 

252 

253@pytest.fixture 

254def yosys_project_test(tmp_path): 

255 class YosysProjectTest: 

256 def __init__(self): 

257 self.modules_path = tmp_path / "modules" 

258 self.project_path = tmp_path / "projects" / "apa" / "project" 

259 self.output_path = tmp_path / "projects" / "apa" 

260 self.build_time_generics = {"enable": True} 

261 

262 self.mocked_run_ghdl = None 

263 self.mocked_run_yosys = None 

264 

265 def create(self, project, **other_arguments): 

266 with patch("tsfpga.yosys.project.run_ghdl", autospec=True) as self.mocked_run_ghdl: 

267 self.mocked_run_ghdl.return_value = True 

268 return project.create(project_path=self.project_path, **other_arguments) 

269 

270 def build(self, project, ghdl_side_effect=None, **other_arguments): 

271 # Note that 'build' analyzes the sources as well, so that any changes made by the 

272 # pre-build hooks are picked up. Hence GHDL is mocked here too. 

273 with ( 

274 patch("tsfpga.yosys.project.run_ghdl", autospec=True) as self.mocked_run_ghdl, 

275 patch("tsfpga.yosys.project.run_yosys", autospec=True) as self.mocked_run_yosys, 

276 patch( 

277 "tsfpga.yosys.project.YosysNetlistBuild._get_size", autospec=True 

278 ) as mocked_get_size, 

279 ): 

280 if ghdl_side_effect is None: 

281 self.mocked_run_ghdl.return_value = True 

282 else: 

283 self.mocked_run_ghdl.side_effect = ghdl_side_effect 

284 self.mocked_run_yosys.return_value = True 

285 mocked_get_size.return_value = {"Total LUTs": 0, "FFs": 0} 

286 

287 return project.build( 

288 project_path=self.project_path, 

289 output_path=self.output_path, 

290 generics=self.build_time_generics, 

291 **other_arguments, 

292 ) 

293 

294 return YosysProjectTest() 

295 

296 

297def test_default_pre_create_hook_should_pass(yosys_project_test): 

298 class CustomYosysNetlistBuild(YosysNetlistBuild): 

299 pass 

300 

301 modules = _create_module(yosys_project_test.modules_path) 

302 project = CustomYosysNetlistBuild(name="apa", modules=modules) 

303 assert yosys_project_test.create(project) 

304 yosys_project_test.mocked_run_ghdl.assert_called_once() 

305 

306 

307def test_create_with_vhdl_extension_top_level_file(yosys_project_test): 

308 # Regression test: the top-level file lookup used to hardcode the '.vhd' file ending, 

309 # which meant it failed to find a top-level file using e.g. the '.vhdl' file ending. 

310 create_file( 

311 yosys_project_test.modules_path / "apa" / "src" / "apa_top.vhdl", 

312 """\ 

313library ieee; 

314use ieee.std_logic_1164.all; 

315 

316entity apa_top is 

317 port ( 

318 clk : in std_logic 

319 ); 

320end entity; 

321 

322architecture a of apa_top is 

323begin 

324end architecture; 

325""", 

326 ) 

327 modules = get_modules(modules_folder=yosys_project_test.modules_path) 

328 project = YosysNetlistBuild(name="apa", modules=modules) 

329 

330 assert yosys_project_test.create(project) 

331 yosys_project_test.mocked_run_ghdl.assert_called_once() 

332 

333 

334def test_project_pre_create_hook_returning_false_should_fail_and_not_call_ghdl( 

335 yosys_project_test, 

336): 

337 class CustomYosysNetlistBuild(YosysNetlistBuild): 

338 def pre_create(self, **kwargs): 

339 return False 

340 

341 modules = _create_module(yosys_project_test.modules_path) 

342 project = CustomYosysNetlistBuild(name="apa", modules=modules) 

343 assert not yosys_project_test.create(project) 

344 yosys_project_test.mocked_run_ghdl.assert_not_called() 

345 

346 

347def test_create_should_call_pre_create_with_correct_parameters(yosys_project_test): 

348 modules = _create_module(yosys_project_test.modules_path) 

349 project = YosysNetlistBuild(name="apa", modules=modules, generics={"apa": 123}, hest=456) 

350 with patch("tsfpga.yosys.project.YosysNetlistBuild.pre_create") as mocked_pre_create: 

351 yosys_project_test.create(project, zebra=789) 

352 mocked_pre_create.assert_called_once_with( 

353 project_path=yosys_project_test.project_path, 

354 hest=456, 

355 zebra=789, 

356 ) 

357 yosys_project_test.mocked_run_ghdl.assert_called_once() 

358 

359 

360def test_default_pre_and_post_build_hooks_should_pass(yosys_project_test): 

361 modules = _create_module(yosys_project_test.modules_path) 

362 project = YosysNetlistBuild(name="apa", modules=modules) 

363 yosys_project_test.create(project) 

364 

365 build_result = yosys_project_test.build(project) 

366 assert build_result.success 

367 yosys_project_test.mocked_run_yosys.assert_called_once() 

368 

369 

370def test_project_pre_build_hook_returning_false_should_fail_and_not_call_yosys( 

371 yosys_project_test, 

372): 

373 class CustomYosysNetlistBuild(YosysNetlistBuild): 

374 def pre_build(self, **kwargs): 

375 return False 

376 

377 modules = _create_module(yosys_project_test.modules_path) 

378 project = CustomYosysNetlistBuild(name="apa", modules=modules) 

379 yosys_project_test.create(project) 

380 

381 build_result = yosys_project_test.build(project) 

382 assert not build_result.success 

383 yosys_project_test.mocked_run_yosys.assert_not_called() 

384 

385 

386def test_project_post_build_hook_returning_false_should_fail(yosys_project_test): 

387 class CustomYosysNetlistBuild(YosysNetlistBuild): 

388 def post_build(self, **kwargs): 

389 return False 

390 

391 modules = _create_module(yosys_project_test.modules_path) 

392 project = CustomYosysNetlistBuild(name="apa", modules=modules) 

393 yosys_project_test.create(project) 

394 

395 build_result = yosys_project_test.build(project) 

396 assert not build_result.success 

397 yosys_project_test.mocked_run_yosys.assert_called_once() 

398 

399 

400def test_module_pre_build_hook_and_create_regs_are_called(yosys_project_test): 

401 # Patched on the class, since 'YosysNetlistBuild' deep-copies the module list before 

402 # 'pre_build'/'build' is called. 

403 with ( 

404 patch("tsfpga.module.BaseModule.pre_build", autospec=True) as mocked_pre_build, 

405 patch( 

406 "tsfpga.module.BaseModule.create_register_synthesis_files", autospec=True 

407 ) as mocked_create_register_synthesis_files, 

408 ): 

409 mocked_pre_build.return_value = True 

410 

411 modules = _create_module(yosys_project_test.modules_path) 

412 project = YosysNetlistBuild(name="apa", modules=modules) 

413 yosys_project_test.create(project) 

414 

415 build_result = yosys_project_test.build(project) 

416 assert build_result.success 

417 

418 mocked_pre_build.assert_called_once() 

419 # Note: 'BaseModule.get_synthesis_files' also triggers register file generation as a side 

420 # effect, and is called a few times while resolving compile order in 'create()'. So this 

421 # will be called more than once, which is expected and not a problem since it is idempotent. 

422 mocked_create_register_synthesis_files.assert_called() 

423 

424 

425def test_build_should_raise_exception_if_project_does_not_exist(yosys_project_test): 

426 modules = _create_module(yosys_project_test.modules_path) 

427 project = YosysNetlistBuild(name="apa", modules=modules) 

428 

429 with pytest.raises(ValueError, match='Project "apa" does not exist'): 

430 yosys_project_test.build(project) 

431 

432 

433def test_build_result_checkers_are_run_and_can_fail_build(yosys_project_test): 

434 modules = _create_module(yosys_project_test.modules_path) 

435 project = YosysNetlistBuild( 

436 name="apa", modules=modules, build_result_checkers=[TotalLuts(EqualTo(0))] 

437 ) 

438 yosys_project_test.create(project) 

439 

440 with ( 

441 patch("tsfpga.yosys.project.YosysNetlistBuild._get_size", autospec=True) as mocked_get_size, 

442 patch("tsfpga.yosys.project.run_ghdl", autospec=True) as mocked_run_ghdl, 

443 patch("tsfpga.yosys.project.run_yosys", autospec=True) as mocked_run_yosys, 

444 ): 

445 mocked_get_size.return_value = {"Total LUTs": 3} 

446 mocked_run_ghdl.return_value = True 

447 mocked_run_yosys.return_value = True 

448 (yosys_project_test.project_path / "ghdl").mkdir(parents=True, exist_ok=True) 

449 build_result = project.build( 

450 project_path=yosys_project_test.project_path, 

451 output_path=yosys_project_test.output_path, 

452 ) 

453 

454 assert not build_result.success 

455 assert build_result.synthesis_size == {"Total LUTs": 3} 

456 

457 

458def test_build_time_generics_are_copied(yosys_project_test): 

459 modules = _create_module(yosys_project_test.modules_path) 

460 project = YosysNetlistBuild(name="apa", modules=modules) 

461 

462 generics = {"apa": 3} 

463 yosys_project_test.build_time_generics = generics 

464 yosys_project_test.create(project) 

465 yosys_project_test.build(project) 

466 

467 generics["apa"] = False 

468 _, kwargs = yosys_project_test.mocked_run_yosys.call_args 

469 # Generics are baked into the script file content, not passed as an explicit argument. 

470 script_content = kwargs["script_file"].read_text() 

471 assert "-gapa=3" in script_content 

472 

473 

474def test_different_generic_combinations(yosys_project_test): 

475 modules = _create_module(yosys_project_test.modules_path) 

476 

477 # Only build time generics 

478 project = YosysNetlistBuild(name="apa", modules=modules) 

479 yosys_project_test.build_time_generics = {"runtime": 1} 

480 yosys_project_test.create(project) 

481 yosys_project_test.build(project) 

482 _, kwargs = yosys_project_test.mocked_run_yosys.call_args 

483 assert "-gruntime=1" in kwargs["script_file"].read_text() 

484 

485 # Static and build time generics 

486 project = YosysNetlistBuild(name="apa", modules=modules, generics={"static": 2}) 

487 yosys_project_test.build_time_generics = {"runtime": 1} 

488 yosys_project_test.create(project) 

489 yosys_project_test.build(project) 

490 _, kwargs = yosys_project_test.mocked_run_yosys.call_args 

491 script_content = kwargs["script_file"].read_text() 

492 assert "-gruntime=1" in script_content 

493 assert "-gstatic=2" in script_content 

494 

495 # Same key in both. Should prefer build time. 

496 project = YosysNetlistBuild(name="apa", modules=modules, generics={"common": 2}) 

497 yosys_project_test.build_time_generics = {"common": 5} 

498 yosys_project_test.create(project) 

499 yosys_project_test.build(project) 

500 _, kwargs = yosys_project_test.mocked_run_yosys.call_args 

501 assert "-gcommon=5" in kwargs["script_file"].read_text() 

502 

503 

504def test_non_vhdl_top_with_vhdl_entities(yosys_project_test): 

505 modules = _create_module(yosys_project_test.modules_path, module_name="apa", top_name="sub_a") 

506 modules = _create_module(yosys_project_test.modules_path, module_name="apa", top_name="sub_b") 

507 

508 project = YosysNetlistBuild( 

509 name="apa", modules=modules, top="verilog_top", vhdl_entities=["sub_a", "sub_b"] 

510 ) 

511 

512 yosys_project_test.build_time_generics = {} 

513 yosys_project_test.create(project) 

514 # One call per analyzed VHDL source file ('sub_a.vhd' and 'sub_b.vhd'). 

515 assert yosys_project_test.mocked_run_ghdl.call_count == 2 

516 

517 build_result = yosys_project_test.build(project) 

518 assert build_result.success 

519 

520 _, kwargs = yosys_project_test.mocked_run_yosys.call_args 

521 script_content = kwargs["script_file"].read_text() 

522 

523 assert script_content.count("ghdl --std=08") == 2 

524 assert script_content.splitlines()[0].endswith(" sub_a") 

525 assert script_content.splitlines()[1].endswith(" sub_b") 

526 assert "-top verilog_top" in script_content 

527 

528 

529def test_non_vhdl_top_with_vhdl_entity_not_found_should_raise_exception(yosys_project_test): 

530 modules = _create_module(yosys_project_test.modules_path) 

531 project = YosysNetlistBuild( 

532 name="apa", modules=modules, top="verilog_top", vhdl_entities=["non_existent"] 

533 ) 

534 

535 yosys_project_test.build_time_generics = {} 

536 yosys_project_test.create(project) 

537 

538 with pytest.raises( 

539 ValueError, match='Could not find a VHDL source file for entity "non_existent"' 

540 ): 

541 yosys_project_test.build(project) 

542 

543 

544def test_non_vhdl_top_with_generics_sets_verilog_parameters(yosys_project_test): 

545 modules = _create_module(yosys_project_test.modules_path) 

546 project = YosysNetlistBuild( 

547 name="apa", modules=modules, top="verilog_top", generics={"width": 16} 

548 ) 

549 

550 yosys_project_test.create(project) 

551 

552 yosys_project_test.build_time_generics = {"enable": True} 

553 build_result = yosys_project_test.build(project) 

554 assert build_result.success 

555 

556 _, kwargs = yosys_project_test.mocked_run_yosys.call_args 

557 script_content = kwargs["script_file"].read_text() 

558 

559 # Static and build-time generics both end up as Verilog parameters on the top level. 

560 assert "hierarchy -top verilog_top " in script_content 

561 assert "-chparam width 16" in script_content 

562 assert "-chparam enable 1" in script_content 

563 # The parameters must be set before synthesis. 

564 assert script_content.index("hierarchy -top") < script_content.index( 

565 "-top verilog_top -flatten" 

566 ) 

567 

568 

569def test_vhdl_top_with_generics_does_not_set_verilog_parameters(yosys_project_test): 

570 modules = _create_module(yosys_project_test.modules_path) 

571 project = YosysNetlistBuild(name="apa", modules=modules, generics={"width": 16}) 

572 

573 yosys_project_test.create(project) 

574 yosys_project_test.build(project) 

575 

576 _, kwargs = yosys_project_test.mocked_run_yosys.call_args 

577 script_content = kwargs["script_file"].read_text() 

578 

579 # Generics go to GHDL for a VHDL top level, not to a Yosys 'hierarchy' command. 

580 assert "hierarchy" not in script_content 

581 assert "-gwidth=16" in script_content 

582 

583 

584def test_non_vhdl_top_with_generics_and_vhdl_entities(yosys_project_test): 

585 """ 

586 Project generics target the project top level. When that is a Verilog module they are applied 

587 as parameters there, and must not be passed to the VHDL entities listed in 'vhdl_entities', 

588 which generally do not declare them. 

589 """ 

590 _create_module(yosys_project_test.modules_path, module_name="apa", top_name="sub_a") 

591 modules = _create_module(yosys_project_test.modules_path, module_name="apa", top_name="sub_b") 

592 

593 project = YosysNetlistBuild( 

594 name="apa", 

595 modules=modules, 

596 top="verilog_top", 

597 vhdl_entities=["sub_a", "sub_b"], 

598 generics={"width": 16}, 

599 ) 

600 

601 yosys_project_test.create(project) 

602 

603 yosys_project_test.build_time_generics = {} 

604 assert yosys_project_test.build(project).success 

605 

606 _, kwargs = yosys_project_test.mocked_run_yosys.call_args 

607 script_content = kwargs["script_file"].read_text() 

608 

609 assert "-chparam width 16" in script_content 

610 # The GHDL commands for the listed entities must carry no generic arguments. 

611 assert "-gwidth" not in script_content 

612 

613 

614def test_pre_build_changes_to_source_are_analyzed(yosys_project_test): 

615 """ 

616 A 'pre_build' hook may rewrite synthesis sources based on the build-time generics, which is 

617 an explicitly supported use case. Those changes must be analyzed before synthesis, not left 

618 behind in the work library that 'create' produced. 

619 """ 

620 modules_path = yosys_project_test.modules_path 

621 modules = _create_module(modules_path) 

622 

623 create_file( 

624 modules_path / "apa" / "module_apa.py", 

625 """\ 

626from tsfpga.module import BaseModule 

627 

628 

629class Module(BaseModule): 

630 def pre_build(self, **kwargs): 

631 width = kwargs["generics"]["width"] 

632 (self.path / "src" / "apa_top.vhd").write_text(f"-- width is {width}\\n") 

633 return True 

634""", 

635 ) 

636 modules = get_modules(modules_folder=modules_path) 

637 

638 project = YosysNetlistBuild(name="apa", modules=modules, top="apa_top") 

639 yosys_project_test.create(project) 

640 

641 analyzed_contents = [] 

642 

643 def record_analyzed(ghdl_path, arguments, cwd): # noqa: ARG001 

644 analyzed_contents.append(Path(arguments[-1]).read_text()) 

645 return True 

646 

647 for width in [8, 16]: 

648 analyzed_contents.clear() 

649 yosys_project_test.build_time_generics = {"width": width} 

650 yosys_project_test.build(project, ghdl_side_effect=record_analyzed) 

651 

652 # The file that the hook just rewrote is what got analyzed. 

653 assert any(f"width is {width}" in contents for contents in analyzed_contents), ( 

654 f"Stale source analyzed for width={width}: {analyzed_contents}" 

655 ) 

656 

657 

658def test_suppress_stdout_is_serialized(): 

659 """ 

660 Netlist builds are created from VUnit test-runner worker threads. Two threads inside the 

661 global 'sys.stdout' swap at the same time interleave their save/restore, and the last one 

662 out installs an already-closed file as 'sys.stdout'. So it must be mutually exclusive. 

663 """ 

664 original_stdout = sys.stdout 

665 num_inside = 0 

666 max_num_inside = 0 

667 counter_lock = threading.Lock() 

668 

669 def worker(): 

670 nonlocal num_inside, max_num_inside 

671 for _ in range(20): 

672 with _suppress_stdout(): 

673 with counter_lock: 

674 num_inside += 1 

675 max_num_inside = max(max_num_inside, num_inside) 

676 

677 # Widen the window, so that a missing lock is detected reliably. 

678 time.sleep(0.001) 

679 

680 with counter_lock: 

681 num_inside -= 1 

682 

683 threads = [threading.Thread(target=worker) for _ in range(4)] 

684 for thread in threads: 

685 thread.start() 

686 for thread in threads: 

687 thread.join() 

688 

689 assert max_num_inside == 1, "Global 'sys.stdout' swap was not serialized" 

690 assert sys.stdout is original_stdout 

691 assert not sys.stdout.closed 

692 

693 

694def test_get_verilog_parameter_value(): 

695 assert _get_verilog_parameter_value(True) == "1" 

696 assert _get_verilog_parameter_value(False) == "0" 

697 assert _get_verilog_parameter_value(3) == "3" 

698 assert _get_verilog_parameter_value(3.5) == "3.5" 

699 assert _get_verilog_parameter_value(BitVectorGenericValue("1010")) == "4'b1010" 

700 

701 # Yosys can not set string parameters. 

702 with pytest.raises(TypeError, match="can not set string parameters"): 

703 _get_verilog_parameter_value(StringGenericValue("hest")) 

704 

705 

706def test_vunit_output_directory_is_removed_with_the_project(yosys_project_test): 

707 """ 

708 The VUnit project is only used to resolve compile order, and its output is throwaway. It 

709 used to be created with 'mkdtemp', which never removes anything, so every netlist build 

710 left a directory behind in the system temp directory forever. 

711 """ 

712 project = YosysNetlistBuild(name="apa", modules=_create_module(yosys_project_test.modules_path)) 

713 

714 project._get_vunit_project() # noqa: SLF001 

715 output_directory = Path(project._vunit_output_dir.name) # noqa: SLF001 

716 assert output_directory.exists() 

717 

718 # Dropping the last reference must take the directory with it. 'gc.collect()' rather than 

719 # relying on refcounting alone, since the VUnit object graph can contain cycles. 

720 del project 

721 gc.collect() 

722 

723 assert not output_directory.exists()