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

293 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-21 20:51 +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 unittest 

10from pathlib import Path 

11from unittest.mock import MagicMock, patch 

12 

13import pytest 

14 

15from tsfpga.build_step_tcl_hook import BuildStepTclHook 

16from tsfpga.constraint import Constraint 

17from tsfpga.module import BaseModule 

18from tsfpga.system_utils import create_directory, create_file 

19from tsfpga.vivado.generics import StringGenericValue 

20from tsfpga.vivado.project import VivadoNetlistProject, VivadoProject, copy_and_combine_dicts 

21 

22# ruff: noqa: ARG002 

23 

24 

25def test_casting_to_string(): 

26 project = VivadoProject(name="my_project", modules=[], part="") 

27 assert ( 

28 str(project) 

29 == """\ 

30my_project 

31Type: VivadoProject 

32Top level: my_project_top 

33Generics: - 

34""" 

35 ) 

36 

37 project = VivadoProject( 

38 name="my_project", 

39 modules=[], 

40 part="", 

41 top="apa", 

42 generics={"hest": True, "zebra": 3, "foo": StringGenericValue("/home/test.vhd")}, 

43 ) 

44 assert ( 

45 str(project) 

46 == """\ 

47my_project 

48Type: VivadoProject 

49Top level: apa 

50Generics: hest=True, zebra=3, foo=/home/test.vhd 

51""" 

52 ) 

53 

54 project = VivadoProject(name="my_project", modules=[], part="", apa=123, hest=456) 

55 assert ( 

56 str(project) 

57 == """\ 

58my_project 

59Type: VivadoProject 

60Top level: my_project_top 

61Generics: - 

62Arguments: apa=123, hest=456 

63""" 

64 ) 

65 

66 

67def test_modules_list_should_be_copied(): 

68 modules = [1] 

69 proj = VivadoProject(name="name", modules=modules, part="part") 

70 

71 modules.append(2) 

72 assert len(proj.modules) == 1 

73 

74 

75def test_static_generics_dictionary_should_be_copied(): 

76 generics = {"apa": 3} 

77 proj = VivadoProject(name="name", modules=[], part="part", generics=generics) 

78 

79 generics["apa"] = False 

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

81 

82 

83def test_constraints_list_should_be_copied(): 

84 constraints = [Constraint(file="1")] 

85 proj = VivadoProject(name="name", modules=[], part="part", constraints=constraints) 

86 

87 constraints.append(Constraint(file="2")) 

88 assert len(proj.constraints) == 1 

89 

90 

91def test_bad_constraint_type_should_raise_error(): 

92 # Correct type should not give error 

93 VivadoProject(name="name", modules=[], part="part", constraints=[Constraint(file=None)]) 

94 

95 # Bad type should raise exception 

96 with pytest.raises(TypeError) as exception_info: 

97 VivadoProject(name="name", modules=[], part="part", constraints=["file.vhd"]) 

98 assert str(exception_info.value) == 'Got bad type for "constraints" element: file.vhd' 

99 

100 

101def test_bad_tcl_sources_type_should_raise_error(): 

102 # Correct type should not give error 

103 VivadoProject(name="name", modules=[], part="part", tcl_sources=[Path()]) 

104 

105 # Bad type should raise exception 

106 with pytest.raises(TypeError) as exception_info: 

107 VivadoProject(name="name", modules=[], part="part", tcl_sources=["file.tcl"]) 

108 assert str(exception_info.value) == 'Got bad type for "tcl_sources" element: file.tcl' 

109 

110 

111def test_bad_build_step_hooks_type_should_raise_error(): 

112 # Correct type should not give error 

113 VivadoProject( 

114 name="name", 

115 modules=[], 

116 part="part", 

117 build_step_hooks=[BuildStepTclHook(tcl_file="", hook_step="")], 

118 ) 

119 

120 # Bad type should raise exception 

121 with pytest.raises(TypeError) as exception_info: 

122 VivadoProject(name="name", modules=[], part="part", build_step_hooks=["file.tcl"]) 

123 assert str(exception_info.value) == 'Got bad type for "build_step_hooks" element: file.tcl' 

124 

125 

126def test_create_should_raise_exception_if_project_path_already_exists(tmp_path): 

127 create_directory(tmp_path / "projects" / "name") 

128 proj = VivadoProject(name="name", modules=[], part="part") 

129 with pytest.raises(ValueError) as exception_info: 

130 proj.create(tmp_path / "projects") 

131 assert str(exception_info.value).startswith("Folder already exists") 

132 

133 

134def test_build_should_raise_exception_if_project_does_not_exists(tmp_path): 

135 create_directory(tmp_path / "projects") 

136 proj = VivadoProject(name="name", modules=[], part="part") 

137 with pytest.raises(ValueError) as exception_info: 

138 proj.build(tmp_path / "projects", synth_only=True) 

139 assert str(exception_info.value).startswith("Project file does not exist") 

140 

141 

142def test_build_with_impl_run_should_raise_exception_if_no_output_path_is_given(): 

143 proj = VivadoProject(name="name", modules=[], part="part") 

144 with pytest.raises(ValueError) as exception_info: 

145 proj.build("None") 

146 assert str(exception_info.value).startswith("Must specify output_path") 

147 

148 

149def test_top_name(): 

150 assert VivadoProject(name="apa", modules=[], part="").top == "apa_top" 

151 assert VivadoProject(name="apa", modules=[], part="", top="hest").top == "hest" 

152 

153 

154def test_project_file_name_is_same_as_project_name(): 

155 project_path = Path("projects/apa") 

156 assert ( 

157 VivadoProject(name="apa", modules=[], part="").project_file(project_path) 

158 == project_path / "apa.xpr" 

159 ) 

160 

161 

162def test_project_create(tmp_path): 

163 with patch("tsfpga.vivado.project.run_vivado_tcl", autospec=True) as _: 

164 assert VivadoProject(name="apa", modules=[], part="").create(tmp_path / "projects" / "apa") 

165 assert (tmp_path / "projects" / "apa" / "create_vivado_project.tcl").exists() 

166 

167 

168def test_project_create_should_raise_exception_if_project_path_already_exists(tmp_path): 

169 project_path = create_directory(tmp_path / "projects" / "apa") 

170 with pytest.raises(ValueError) as exception_info: 

171 VivadoProject(name="apa", modules=[], part="").create(project_path) 

172 assert str(exception_info.value) == f"Folder already exists: {project_path}" 

173 

174 

175def test_copy_and_combine_dict_with_both_arguments_none(): 

176 assert copy_and_combine_dicts(None, None) == {} 

177 

178 

179def test_copy_and_combine_dict_with_first_argument_valid(): 

180 dict_first = {"first": 1} 

181 

182 result = copy_and_combine_dicts(dict_first, None) 

183 assert result == {"first": 1} 

184 assert dict_first == {"first": 1} 

185 

186 dict_first["first_dummy"] = True 

187 assert result == {"first": 1} 

188 

189 

190def test_copy_and_combine_dict_with_second_argument_valid(): 

191 dict_second = {"second": 2} 

192 

193 result = copy_and_combine_dicts(None, dict_second) 

194 assert result == {"second": 2} 

195 assert dict_second == {"second": 2} 

196 

197 dict_second["second_dummy"] = True 

198 assert result == {"second": 2} 

199 

200 

201def test_copy_and_combine_dict_with_both_arguments_valid(): 

202 dict_first = {"first": 1} 

203 dict_second = {"second": 2} 

204 

205 result = copy_and_combine_dicts(dict_first, dict_second) 

206 assert result == {"first": 1, "second": 2} 

207 assert dict_first == {"first": 1} 

208 assert dict_second == {"second": 2} 

209 

210 dict_first["first_dummy"] = True 

211 dict_second["second_dummy"] = True 

212 assert result == {"first": 1, "second": 2} 

213 

214 

215def test_copy_and_combine_dict_with_both_arguments_valid_and_same_key(): 

216 dict_first = {"first": 1, "common": 3} 

217 dict_second = {"second": 2, "common": 4} 

218 

219 result = copy_and_combine_dicts(dict_first, dict_second) 

220 assert result == {"first": 1, "second": 2, "common": 4} 

221 assert dict_first == {"first": 1, "common": 3} 

222 assert dict_second == {"second": 2, "common": 4} 

223 

224 dict_first["first_dummy"] = True 

225 dict_second["second_dummy"] = True 

226 assert result == {"first": 1, "second": 2, "common": 4} 

227 

228 

229@pytest.fixture 

230def vivado_project_test(tmp_path): 

231 class VivadoProjectTest: 

232 def __init__(self): 

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

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

235 self.ip_cache_path = MagicMock() 

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

237 self.num_threads = 4 

238 self.run_index = 3 

239 self.synth_only = False 

240 self.from_impl = False 

241 

242 self.mocked_run_vivado_tcl = None 

243 

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

245 with patch( 

246 "tsfpga.vivado.project.run_vivado_tcl", autospec=True 

247 ) as self.mocked_run_vivado_tcl: 

248 return project.create( 

249 project_path=self.project_path, 

250 ip_cache_path=self.ip_cache_path, 

251 **other_arguments, 

252 ) 

253 

254 def build(self, project): 

255 with ( 

256 patch( 

257 "tsfpga.vivado.project.run_vivado_tcl", autospec=True 

258 ) as self.mocked_run_vivado_tcl, 

259 patch("tsfpga.vivado.project.VivadoProject._get_size", autospec=True) as _, 

260 patch("tsfpga.vivado.project.shutil.copy2", autospec=True) as _, 

261 ): 

262 create_file(self.project_path / "apa.xpr") 

263 return project.build( 

264 project_path=self.project_path, 

265 output_path=self.output_path, 

266 run_index=self.run_index, 

267 generics=self.build_time_generics, 

268 synth_only=self.synth_only, 

269 num_threads=self.num_threads, 

270 other_parameter="hest", 

271 ) 

272 

273 return VivadoProjectTest() 

274 

275 

276def test_default_pre_create_hook_should_pass(vivado_project_test): 

277 class CustomVivadoProject(VivadoProject): 

278 pass 

279 

280 project = CustomVivadoProject(name="apa", modules=[], part="") 

281 vivado_project_test.create(project) 

282 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

283 

284 

285def test_project_pre_create_hook_returning_false_should_fail_and_not_call_vivado_run( 

286 vivado_project_test, 

287): 

288 class CustomVivadoProject(VivadoProject): 

289 def pre_create(self, **kwargs): 

290 return False 

291 

292 assert not vivado_project_test.create(CustomVivadoProject(name="apa", modules=[], part="")) 

293 vivado_project_test.mocked_run_vivado_tcl.assert_not_called() 

294 

295 

296def test_create_should_call_pre_create_with_correct_parameters(vivado_project_test): 

297 project = VivadoProject(name="apa", modules=[], part="", generics={"apa": 123}, hest=456) 

298 with patch("tsfpga.vivado.project.VivadoProject.pre_create") as mocked_pre_create: 

299 vivado_project_test.create(project, zebra=789) 

300 mocked_pre_create.assert_called_once_with( 

301 project_path=vivado_project_test.project_path, 

302 ip_cache_path=vivado_project_test.ip_cache_path, 

303 part="", 

304 generics={"apa": 123}, 

305 hest=456, 

306 zebra=789, 

307 ) 

308 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

309 

310 

311def test_build_module_pre_build_hook_and_create_regs_are_called(vivado_project_test): 

312 project = VivadoProject( 

313 name="apa", 

314 modules=[MagicMock(spec=BaseModule), MagicMock(spec=BaseModule)], 

315 part="", 

316 apa=123, 

317 ) 

318 build_result = vivado_project_test.build(project) 

319 assert build_result.success 

320 

321 for module in project.modules: 

322 module.pre_build.assert_called_once_with( 

323 project=project, 

324 other_parameter="hest", 

325 apa=123, 

326 project_path=vivado_project_test.project_path, 

327 output_path=vivado_project_test.output_path, 

328 run_index=vivado_project_test.run_index, 

329 generics=vivado_project_test.build_time_generics, 

330 synth_only=vivado_project_test.synth_only, 

331 from_impl=vivado_project_test.from_impl, 

332 num_threads=vivado_project_test.num_threads, 

333 ) 

334 module.create_register_synthesis_files.assert_called_once() 

335 module.create_register_simulation_files.assert_not_called() 

336 

337 

338def test_default_pre_and_post_build_hooks_should_pass(vivado_project_test): 

339 class CustomVivadoProject(VivadoProject): 

340 pass 

341 

342 build_result = vivado_project_test.build(CustomVivadoProject(name="apa", modules=[], part="")) 

343 assert build_result.success 

344 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

345 

346 

347def test_project_pre_build_hook_returning_false_should_fail_and_not_call_vivado_run( 

348 vivado_project_test, 

349): 

350 class CustomVivadoProject(VivadoProject): 

351 def pre_build(self, **kwargs): 

352 return False 

353 

354 build_result = vivado_project_test.build(CustomVivadoProject(name="apa", modules=[], part="")) 

355 assert not build_result.success 

356 vivado_project_test.mocked_run_vivado_tcl.assert_not_called() 

357 

358 

359def test_project_post_build_hook_returning_false_should_fail(vivado_project_test): 

360 class CustomVivadoProject(VivadoProject): 

361 def post_build(self, **kwargs): 

362 return False 

363 

364 build_result = vivado_project_test.build(CustomVivadoProject(name="apa", modules=[], part="")) 

365 assert not build_result.success 

366 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

367 

368 

369def test_project_build_hooks_should_be_called_with_correct_parameters(vivado_project_test): 

370 project = VivadoProject( 

371 name="apa", modules=[], part="", generics={"static_generic": 2}, apa=123 

372 ) 

373 with ( 

374 patch("tsfpga.vivado.project.VivadoProject.pre_build") as mocked_pre_build, 

375 patch("tsfpga.vivado.project.VivadoProject.post_build") as mocked_post_build, 

376 ): 

377 vivado_project_test.build(project) 

378 

379 arguments = { 

380 "project_path": vivado_project_test.project_path, 

381 "output_path": vivado_project_test.output_path, 

382 "run_index": vivado_project_test.run_index, 

383 "generics": copy_and_combine_dicts( 

384 {"static_generic": 2}, vivado_project_test.build_time_generics 

385 ), 

386 "synth_only": vivado_project_test.synth_only, 

387 "from_impl": vivado_project_test.from_impl, 

388 "num_threads": vivado_project_test.num_threads, 

389 "other_parameter": "hest", 

390 "apa": 123, 

391 } 

392 mocked_pre_build.assert_called_once_with(**arguments) 

393 

394 arguments.update(build_result=unittest.mock.ANY) 

395 mocked_post_build.assert_called_once_with(**arguments) 

396 

397 

398def test_module_pre_build_hook_returning_false_should_fail_and_not_call_vivado(vivado_project_test): 

399 module = MagicMock(spec=BaseModule) 

400 module.name = "whatever" 

401 project = VivadoProject(name="apa", modules=[module], part="") 

402 

403 project.modules[0].pre_build.return_value = True 

404 build_result = vivado_project_test.build(project) 

405 assert build_result.success 

406 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

407 

408 project.modules[0].pre_build.return_value = False 

409 build_result = vivado_project_test.build(project) 

410 assert not build_result.success 

411 vivado_project_test.mocked_run_vivado_tcl.assert_not_called() 

412 

413 

414@patch("tsfpga.vivado.project.VivadoTcl", autospec=True) 

415def test_different_generic_combinations(mocked_vivado_tcl, vivado_project_test): 

416 mocked_vivado_tcl.return_value.build.return_value = "" 

417 

418 # No generics 

419 vivado_project_test.build_time_generics = None 

420 build_result = vivado_project_test.build(VivadoProject(name="apa", modules=[], part="")) 

421 assert build_result.success 

422 # Note: In python 3.8 we can use call_args.kwargs straight away 

423 _, kwargs = mocked_vivado_tcl.return_value.build.call_args 

424 assert kwargs["generics"] == {} 

425 

426 # Only build time generics 

427 vivado_project_test.build_time_generics = {"runtime": "value"} 

428 build_result = vivado_project_test.build(VivadoProject(name="apa", modules=[], part="")) 

429 assert build_result.success 

430 _, kwargs = mocked_vivado_tcl.return_value.build.call_args 

431 assert kwargs["generics"] == {"runtime": "value"} 

432 

433 # Static and build time generics 

434 vivado_project_test.build_time_generics = {"runtime": "value"} 

435 build_result = vivado_project_test.build( 

436 VivadoProject(name="apa", modules=[], part="", generics={"static": "a value"}) 

437 ) 

438 assert build_result.success 

439 _, kwargs = mocked_vivado_tcl.return_value.build.call_args 

440 assert kwargs["generics"] == {"runtime": "value", "static": "a value"} 

441 

442 # Same key in both static and build time generic. Should prefer build time. 

443 vivado_project_test.build_time_generics = {"static_and_runtime": "build value"} 

444 build_result = vivado_project_test.build( 

445 VivadoProject( 

446 name="apa", modules=[], part="", generics={"static_and_runtime": "static value"} 

447 ) 

448 ) 

449 assert build_result.success 

450 _, kwargs = mocked_vivado_tcl.return_value.build.call_args 

451 assert kwargs["generics"] == {"static_and_runtime": "build value"} 

452 

453 # Only static generics 

454 vivado_project_test.build_time_generics = None 

455 build_result = vivado_project_test.build( 

456 VivadoProject(name="apa", modules=[], part="", generics={"runtime": "a value"}) 

457 ) 

458 assert build_result.success 

459 _, kwargs = mocked_vivado_tcl.return_value.build.call_args 

460 assert kwargs["generics"] == {"runtime": "a value"} 

461 

462 

463def test_build_time_generics_are_copied(vivado_project_test): 

464 vivado_project_test.build_time_generics = {"runtime": "value"} 

465 with patch("tsfpga.vivado.project.VivadoTcl", autospec=True) as mocked_vivado_tcl: 

466 mocked_vivado_tcl.return_value.build.return_value = "" 

467 build_result = vivado_project_test.build( 

468 VivadoProject(name="apa", modules=[], part="", generics={"static": "a value"}) 

469 ) 

470 assert build_result.success 

471 assert vivado_project_test.build_time_generics == {"runtime": "value"} 

472 

473 

474def test_modules_are_deep_copied_before_pre_create_hook(vivado_project_test): 

475 class CustomVivadoProject(VivadoProject): 

476 def pre_create(self, **kwargs): 

477 self.modules[0].registers = "Some other value" 

478 return True 

479 

480 module = MagicMock(spec=BaseModule) 

481 module.registers = "Some value" 

482 

483 project = CustomVivadoProject(name="apa", modules=[module], part="") 

484 assert vivado_project_test.create(project) 

485 

486 assert module.registers == "Some value" 

487 

488 

489def test_modules_are_deep_copied_before_pre_build_hook(vivado_project_test): 

490 class CustomVivadoProject(VivadoProject): 

491 def pre_build(self, **kwargs): 

492 self.modules[0].registers = "Some other value" 

493 return True 

494 

495 module = MagicMock(spec=BaseModule) 

496 module.registers = "Some value" 

497 

498 project = CustomVivadoProject(name="apa", modules=[module], part="") 

499 assert vivado_project_test.build(project).success 

500 

501 assert module.registers == "Some value" 

502 

503 

504def test_get_size_is_called_correctly(vivado_project_test): 

505 project = VivadoProject(name="apa", modules=[], part="") 

506 

507 def _build_with_size(synth_only): 

508 """ 

509 The project.build() call is very similar to _build() method in this class, but it mocks 

510 the _get_size() method in a different way. 

511 """ 

512 with ( 

513 patch( 

514 "tsfpga.vivado.project.run_vivado_tcl", autospec=True 

515 ) as vivado_project_test.mocked_run_vivado_tcl, 

516 patch( 

517 "tsfpga.vivado.project.HierarchicalUtilizationParser.get_size", autospec=True 

518 ) as mocked_get_size, 

519 patch("tsfpga.vivado.project.shutil.copy2", autospec=True) as _, 

520 ): 

521 # Only the first return value will be used if we are in synth_only 

522 mocked_get_size.side_effect = ["synth_size", "impl_size"] 

523 

524 build_result = project.build( 

525 project_path=vivado_project_test.project_path, 

526 output_path=vivado_project_test.output_path, 

527 run_index=vivado_project_test.run_index, 

528 synth_only=synth_only, 

529 ) 

530 

531 assert build_result.synthesis_size == "synth_size" 

532 

533 if synth_only: 

534 mocked_get_size.assert_called_once_with("synth_file") 

535 assert build_result.implementation_size is None 

536 else: 

537 assert mocked_get_size.call_count == 2 

538 mocked_get_size.assert_any_call("synth_file") 

539 mocked_get_size.assert_any_call("impl_file") 

540 

541 assert build_result.implementation_size == "impl_size" 

542 

543 create_file(vivado_project_test.project_path / "apa.xpr") 

544 

545 create_file( 

546 vivado_project_test.project_path / "apa.runs" / "synth_3" / "hierarchical_utilization.rpt", 

547 contents="synth_file", 

548 ) 

549 create_file( 

550 vivado_project_test.project_path / "apa.runs" / "impl_3" / "hierarchical_utilization.rpt", 

551 contents="impl_file", 

552 ) 

553 

554 _build_with_size(synth_only=True) 

555 _build_with_size(synth_only=False) 

556 

557 

558def test_netlist_build_should_set_logic_level_distribution(vivado_project_test): 

559 def _build_with_logic_level_distribution(project): 

560 """ 

561 The project.build() call is very similar to _build() method in this class, except it 

562 also mocks the _get_logic_level_distribution() method. 

563 """ 

564 with ( 

565 patch( 

566 "tsfpga.vivado.project.run_vivado_tcl", autospec=True 

567 ) as vivado_project_test.mocked_run_vivado_tcl, 

568 patch("tsfpga.vivado.project.VivadoProject._get_size", autospec=True) as _, 

569 patch("tsfpga.vivado.project.shutil.copy2", autospec=True) as _, 

570 patch( 

571 "tsfpga.vivado.project.LogicLevelDistributionParser.get_table", autospec=True 

572 ) as mocked_get_table, 

573 ): 

574 mocked_get_table.return_value = "logic_table" 

575 

576 build_result = project.build( 

577 project_path=vivado_project_test.project_path, 

578 output_path=vivado_project_test.output_path, 

579 run_index=vivado_project_test.run_index, 

580 ) 

581 

582 if project.is_netlist_build: 

583 mocked_get_table.assert_called_once_with("logic_file") 

584 assert build_result.logic_level_distribution == "logic_table" 

585 else: 

586 mocked_get_table.assert_not_called() 

587 assert build_result.logic_level_distribution is None 

588 assert build_result.maximum_logic_level is None 

589 

590 create_file(vivado_project_test.project_path / "apa.xpr") 

591 create_file( 

592 vivado_project_test.project_path 

593 / "apa.runs" 

594 / "synth_3" 

595 / "logical_level_distribution.rpt", 

596 contents="logic_file", 

597 ) 

598 

599 project = VivadoNetlistProject(name="apa", modules=[], part="") 

600 _build_with_logic_level_distribution(project=project) 

601 

602 project = VivadoProject(name="apa", modules=[], part="") 

603 _build_with_logic_level_distribution(project=project)