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

293 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-07 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 

9# Standard libraries 

10import unittest 

11from pathlib import Path 

12from unittest.mock import MagicMock, patch 

13 

14# Third party libraries 

15import pytest 

16 

17# First party libraries 

18from tsfpga.build_step_tcl_hook import BuildStepTclHook 

19from tsfpga.constraint import Constraint 

20from tsfpga.module import BaseModule 

21from tsfpga.system_utils import create_directory, create_file 

22from tsfpga.vivado.generics import StringGenericValue 

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

24 

25 

26def test_casting_to_string(): 

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

28 assert ( 

29 str(project) 

30 == """\ 

31my_project 

32Type: VivadoProject 

33Top level: my_project_top 

34Generics: - 

35""" 

36 ) 

37 

38 project = VivadoProject( 

39 name="my_project", 

40 modules=[], 

41 part="", 

42 top="apa", 

43 generics=dict(hest=True, zebra=3, foo=StringGenericValue("/home/test.vhd")), 

44 ) 

45 assert ( 

46 str(project) 

47 == """\ 

48my_project 

49Type: VivadoProject 

50Top level: apa 

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

52""" 

53 ) 

54 

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

56 assert ( 

57 str(project) 

58 == """\ 

59my_project 

60Type: VivadoProject 

61Top level: my_project_top 

62Generics: - 

63Arguments: apa=123, hest=456 

64""" 

65 ) 

66 

67 

68def test_modules_list_should_be_copied(): 

69 modules = [1] 

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

71 

72 modules.append(2) 

73 assert len(proj.modules) == 1 

74 

75 

76def test_static_generics_dictionary_should_be_copied(): 

77 generics = dict(apa=3) 

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

79 

80 generics["apa"] = False 

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

82 

83 

84def test_constraints_list_should_be_copied(): 

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

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

87 

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

89 assert len(proj.constraints) == 1 

90 

91 

92def test_bad_constraint_type_should_raise_error(): 

93 # Correct type should not give error 

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

95 

96 # Bad type should raise exception 

97 with pytest.raises(TypeError) as exception_info: 

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

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

100 

101 

102def test_bad_tcl_sources_type_should_raise_error(): 

103 # Correct type should not give error 

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

105 

106 # Bad type should raise exception 

107 with pytest.raises(TypeError) as exception_info: 

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

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

110 

111 

112def test_bad_build_step_hooks_type_should_raise_error(): 

113 # Correct type should not give error 

114 VivadoProject( 

115 name="name", 

116 modules=[], 

117 part="part", 

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

119 ) 

120 

121 # Bad type should raise exception 

122 with pytest.raises(TypeError) as exception_info: 

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

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

125 

126 

127def test_create_should_raise_exception_if_project_path_already_exists(tmp_path): 

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

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

130 with pytest.raises(ValueError) as exception_info: 

131 proj.create(tmp_path / "projects") 

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

133 

134 

135def test_build_should_raise_exception_if_project_does_not_exists(tmp_path): 

136 create_directory(tmp_path / "projects") 

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

138 with pytest.raises(ValueError) as exception_info: 

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

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

141 

142 

143def test_build_with_impl_run_should_raise_exception_if_no_output_path_is_given(): 

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

145 with pytest.raises(ValueError) as exception_info: 

146 proj.build("None") 

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

148 

149 

150def test_top_name(): 

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

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

153 

154 

155def test_project_file_name_is_same_as_project_name(): 

156 project_path = Path("projects/apa") 

157 assert ( 

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

159 == project_path / "apa.xpr" 

160 ) 

161 

162 

163def test_project_create(tmp_path): 

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

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

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

167 

168 

169def test_project_create_should_raise_exception_if_project_path_already_exists(tmp_path): 

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

171 with pytest.raises(ValueError) as exception_info: 

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

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

174 

175 

176def test_copy_and_combine_dict_with_both_arguments_none(): 

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

178 

179 

180def test_copy_and_combine_dict_with_first_argument_valid(): 

181 dict_first = dict(first=1) 

182 

183 result = copy_and_combine_dicts(dict_first, None) 

184 assert result == dict(first=1) 

185 assert dict_first == dict(first=1) 

186 

187 dict_first["first_dummy"] = True 

188 assert result == dict(first=1) 

189 

190 

191def test_copy_and_combine_dict_with_second_argument_valid(): 

192 dict_second = dict(second=2) 

193 

194 result = copy_and_combine_dicts(None, dict_second) 

195 assert result == dict(second=2) 

196 assert dict_second == dict(second=2) 

197 

198 dict_second["second_dummy"] = True 

199 assert result == dict(second=2) 

200 

201 

202def test_copy_and_combine_dict_with_both_arguments_valid(): 

203 dict_first = dict(first=1) 

204 dict_second = dict(second=2) 

205 

206 result = copy_and_combine_dicts(dict_first, dict_second) 

207 assert result == dict(first=1, second=2) 

208 assert dict_first == dict(first=1) 

209 assert dict_second == dict(second=2) 

210 

211 dict_first["first_dummy"] = True 

212 dict_second["second_dummy"] = True 

213 assert result == dict(first=1, second=2) 

214 

215 

216def test_copy_and_combine_dict_with_both_arguments_valid_and_same_key(): 

217 dict_first = dict(first=1, common=3) 

218 dict_second = dict(second=2, common=4) 

219 

220 result = copy_and_combine_dicts(dict_first, dict_second) 

221 assert result == dict(first=1, second=2, common=4) 

222 assert dict_first == dict(first=1, common=3) 

223 assert dict_second == dict(second=2, common=4) 

224 

225 dict_first["first_dummy"] = True 

226 dict_second["second_dummy"] = True 

227 assert result == dict(first=1, second=2, common=4) 

228 

229 

230@pytest.fixture 

231def vivado_project_test(tmp_path): 

232 class VivadoProjectTest: # pylint: disable=too-many-instance-attributes 

233 def __init__(self): 

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

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

236 self.ip_cache_path = MagicMock() 

237 self.build_time_generics = dict(enable=True) 

238 self.num_threads = 4 

239 self.run_index = 3 

240 self.synth_only = False 

241 self.from_impl = False 

242 

243 self.mocked_run_vivado_tcl = None 

244 

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

246 with patch( 

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

248 ) as self.mocked_run_vivado_tcl: 

249 return project.create( 

250 project_path=self.project_path, 

251 ip_cache_path=self.ip_cache_path, 

252 **other_arguments, 

253 ) 

254 

255 def build(self, project): 

256 with patch( 

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

258 ) as self.mocked_run_vivado_tcl, patch( 

259 "tsfpga.vivado.project.VivadoProject._get_size", autospec=True 

260 ) as _, patch( 

261 "tsfpga.vivado.project.shutil.copy2", autospec=True 

262 ) as _: 

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

264 return project.build( 

265 project_path=self.project_path, 

266 output_path=self.output_path, 

267 run_index=self.run_index, 

268 generics=self.build_time_generics, 

269 synth_only=self.synth_only, 

270 num_threads=self.num_threads, 

271 other_parameter="hest", 

272 ) 

273 

274 return VivadoProjectTest() 

275 

276 

277# False positive for pytest fixtures 

278# pylint: disable=redefined-outer-name 

279 

280 

281def test_default_pre_create_hook_should_pass(vivado_project_test): 

282 class CustomVivadoProject(VivadoProject): 

283 pass 

284 

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

286 vivado_project_test.create(project) 

287 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

288 

289 

290def test_project_pre_create_hook_returning_false_should_fail_and_not_call_vivado_run( 

291 vivado_project_test, 

292): 

293 class CustomVivadoProject(VivadoProject): 

294 def pre_create(self, **kwargs): # pylint: disable=unused-argument 

295 return False 

296 

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

298 vivado_project_test.mocked_run_vivado_tcl.assert_not_called() 

299 

300 

301def test_create_should_call_pre_create_with_correct_parameters(vivado_project_test): 

302 project = VivadoProject(name="apa", modules=[], part="", generics=dict(apa=123), hest=456) 

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

304 vivado_project_test.create(project, zebra=789) 

305 mocked_pre_create.assert_called_once_with( 

306 project_path=vivado_project_test.project_path, 

307 ip_cache_path=vivado_project_test.ip_cache_path, 

308 part="", 

309 generics=dict(apa=123), 

310 hest=456, 

311 zebra=789, 

312 ) 

313 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

314 

315 

316def test_build_module_pre_build_hook_and_create_regs_are_called(vivado_project_test): 

317 project = VivadoProject( 

318 name="apa", 

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

320 part="", 

321 apa=123, 

322 ) 

323 build_result = vivado_project_test.build(project) 

324 assert build_result.success 

325 

326 for module in project.modules: 

327 module.pre_build.assert_called_once_with( 

328 project=project, 

329 other_parameter="hest", 

330 apa=123, 

331 project_path=vivado_project_test.project_path, 

332 output_path=vivado_project_test.output_path, 

333 run_index=vivado_project_test.run_index, 

334 generics=vivado_project_test.build_time_generics, 

335 synth_only=vivado_project_test.synth_only, 

336 from_impl=vivado_project_test.from_impl, 

337 num_threads=vivado_project_test.num_threads, 

338 ) 

339 module.create_register_synthesis_files.assert_called_once() 

340 module.create_register_simulation_files.assert_not_called() 

341 

342 

343def test_default_pre_and_post_build_hooks_should_pass(vivado_project_test): 

344 class CustomVivadoProject(VivadoProject): 

345 pass 

346 

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

348 assert build_result.success 

349 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

350 

351 

352def test_project_pre_build_hook_returning_false_should_fail_and_not_call_vivado_run( 

353 vivado_project_test, 

354): 

355 class CustomVivadoProject(VivadoProject): 

356 def pre_build(self, **kwargs): # pylint: disable=unused-argument 

357 return False 

358 

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

360 assert not build_result.success 

361 vivado_project_test.mocked_run_vivado_tcl.assert_not_called() 

362 

363 

364def test_project_post_build_hook_returning_false_should_fail(vivado_project_test): 

365 class CustomVivadoProject(VivadoProject): 

366 def post_build(self, **kwargs): # pylint: disable=unused-argument 

367 return False 

368 

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

370 assert not build_result.success 

371 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

372 

373 

374def test_project_build_hooks_should_be_called_with_correct_parameters(vivado_project_test): 

375 project = VivadoProject( 

376 name="apa", modules=[], part="", generics=dict(static_generic=2), apa=123 

377 ) 

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

379 "tsfpga.vivado.project.VivadoProject.post_build" 

380 ) as mocked_post_build: 

381 vivado_project_test.build(project) 

382 

383 arguments = dict( 

384 project_path=vivado_project_test.project_path, 

385 output_path=vivado_project_test.output_path, 

386 run_index=vivado_project_test.run_index, 

387 generics=copy_and_combine_dicts( 

388 dict(static_generic=2), vivado_project_test.build_time_generics 

389 ), 

390 synth_only=vivado_project_test.synth_only, 

391 from_impl=vivado_project_test.from_impl, 

392 num_threads=vivado_project_test.num_threads, 

393 other_parameter="hest", 

394 apa=123, 

395 ) 

396 mocked_pre_build.assert_called_once_with(**arguments) 

397 

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

399 mocked_post_build.assert_called_once_with(**arguments) 

400 

401 

402def test_module_pre_build_hook_returning_false_should_fail_and_not_call_vivado(vivado_project_test): 

403 module = MagicMock(spec=BaseModule) 

404 module.name = "whatever" 

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

406 

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

408 build_result = vivado_project_test.build(project) 

409 assert build_result.success 

410 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

411 

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

413 build_result = vivado_project_test.build(project) 

414 assert not build_result.success 

415 vivado_project_test.mocked_run_vivado_tcl.assert_not_called() 

416 

417 

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

419def test_different_generic_combinations(mocked_vivado_tcl, vivado_project_test): 

420 mocked_vivado_tcl.return_value.build.return_value = "" 

421 

422 # No generics 

423 vivado_project_test.build_time_generics = None 

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

425 assert build_result.success 

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

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

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

429 

430 # Only build time generics 

431 vivado_project_test.build_time_generics = dict(runtime="value") 

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

433 assert build_result.success 

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

435 assert kwargs["generics"] == dict(runtime="value") 

436 

437 # Static and build time generics 

438 vivado_project_test.build_time_generics = dict(runtime="value") 

439 build_result = vivado_project_test.build( 

440 VivadoProject(name="apa", modules=[], part="", generics=dict(static="a value")) 

441 ) 

442 assert build_result.success 

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

444 assert kwargs["generics"] == dict(runtime="value", static="a value") 

445 

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

447 vivado_project_test.build_time_generics = dict(static_and_runtime="build value") 

448 build_result = vivado_project_test.build( 

449 VivadoProject( 

450 name="apa", modules=[], part="", generics=dict(static_and_runtime="static value") 

451 ) 

452 ) 

453 assert build_result.success 

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

455 assert kwargs["generics"] == dict(static_and_runtime="build value") 

456 

457 # Only static generics 

458 vivado_project_test.build_time_generics = None 

459 build_result = vivado_project_test.build( 

460 VivadoProject(name="apa", modules=[], part="", generics=dict(runtime="a value")) 

461 ) 

462 assert build_result.success 

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

464 assert kwargs["generics"] == dict(runtime="a value") 

465 

466 

467def test_build_time_generics_are_copied(vivado_project_test): 

468 vivado_project_test.build_time_generics = dict(runtime="value") 

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

470 mocked_vivado_tcl.return_value.build.return_value = "" 

471 build_result = vivado_project_test.build( 

472 VivadoProject(name="apa", modules=[], part="", generics=dict(static="a value")) 

473 ) 

474 assert build_result.success 

475 assert vivado_project_test.build_time_generics == dict(runtime="value") 

476 

477 

478def test_modules_are_deep_copied_before_pre_create_hook(vivado_project_test): 

479 class CustomVivadoProject(VivadoProject): 

480 def pre_create(self, **kwargs): 

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

482 return True 

483 

484 module = MagicMock(spec=BaseModule) 

485 module.registers = "Some value" 

486 

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

488 assert vivado_project_test.create(project) 

489 

490 assert module.registers == "Some value" 

491 

492 

493def test_modules_are_deep_copied_before_pre_build_hook(vivado_project_test): 

494 class CustomVivadoProject(VivadoProject): 

495 def pre_build(self, **kwargs): 

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

497 return True 

498 

499 module = MagicMock(spec=BaseModule) 

500 module.registers = "Some value" 

501 

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

503 assert vivado_project_test.build(project).success 

504 

505 assert module.registers == "Some value" 

506 

507 

508def test_get_size_is_called_correctly(vivado_project_test): 

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

510 

511 def _build_with_size(synth_only): 

512 """ 

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

514 the _get_size() method in a different way. 

515 """ 

516 with patch( 

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

518 ) as vivado_project_test.mocked_run_vivado_tcl, patch( 

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

520 ) as mocked_get_size, patch( 

521 "tsfpga.vivado.project.shutil.copy2", autospec=True 

522 ) as _: 

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

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

525 

526 build_result = project.build( 

527 project_path=vivado_project_test.project_path, 

528 output_path=vivado_project_test.output_path, 

529 run_index=vivado_project_test.run_index, 

530 synth_only=synth_only, 

531 ) 

532 

533 assert build_result.synthesis_size == "synth_size" 

534 

535 if synth_only: 

536 mocked_get_size.assert_called_once_with("synth_file") 

537 assert build_result.implementation_size is None 

538 else: 

539 assert mocked_get_size.call_count == 2 

540 mocked_get_size.assert_any_call("synth_file") 

541 mocked_get_size.assert_any_call("impl_file") 

542 

543 assert build_result.implementation_size == "impl_size" 

544 

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

546 

547 create_file( 

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

549 contents="synth_file", 

550 ) 

551 create_file( 

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

553 contents="impl_file", 

554 ) 

555 

556 _build_with_size(synth_only=True) 

557 _build_with_size(synth_only=False) 

558 

559 

560def test_netlist_build_should_set_logic_level_distribution(vivado_project_test): 

561 def _build_with_logic_level_distribution(project): 

562 """ 

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

564 also mocks the _get_logic_level_distribution() method. 

565 """ 

566 with patch( 

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

568 ) as vivado_project_test.mocked_run_vivado_tcl, patch( 

569 "tsfpga.vivado.project.VivadoProject._get_size", autospec=True 

570 ) as _, patch( 

571 "tsfpga.vivado.project.shutil.copy2", autospec=True 

572 ) as _, patch( 

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

574 ) as mocked_get_table: 

575 mocked_get_table.return_value = "logic_table" 

576 

577 build_result = project.build( 

578 project_path=vivado_project_test.project_path, 

579 output_path=vivado_project_test.output_path, 

580 run_index=vivado_project_test.run_index, 

581 ) 

582 

583 if project.is_netlist_build: 

584 mocked_get_table.assert_called_once_with("logic_file") 

585 assert build_result.logic_level_distribution == "logic_table" 

586 else: 

587 mocked_get_table.assert_not_called() 

588 assert build_result.logic_level_distribution is None 

589 assert build_result.maximum_logic_level is None 

590 

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

592 create_file( 

593 vivado_project_test.project_path 

594 / "apa.runs" 

595 / "synth_3" 

596 / "logical_level_distribution.rpt", 

597 contents="logic_file", 

598 ) 

599 

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

601 _build_with_logic_level_distribution(project=project) 

602 

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

604 _build_with_logic_level_distribution(project=project)