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

292 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-10 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_regs_vhdl_package.assert_called_once() 

340 

341 

342def test_default_pre_and_post_build_hooks_should_pass(vivado_project_test): 

343 class CustomVivadoProject(VivadoProject): 

344 pass 

345 

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

347 assert build_result.success 

348 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

349 

350 

351def test_project_pre_build_hook_returning_false_should_fail_and_not_call_vivado_run( 

352 vivado_project_test, 

353): 

354 class CustomVivadoProject(VivadoProject): 

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

356 return False 

357 

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

359 assert not build_result.success 

360 vivado_project_test.mocked_run_vivado_tcl.assert_not_called() 

361 

362 

363def test_project_post_build_hook_returning_false_should_fail(vivado_project_test): 

364 class CustomVivadoProject(VivadoProject): 

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

366 return False 

367 

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

369 assert not build_result.success 

370 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

371 

372 

373def test_project_build_hooks_should_be_called_with_correct_parameters(vivado_project_test): 

374 project = VivadoProject( 

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

376 ) 

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

378 "tsfpga.vivado.project.VivadoProject.post_build" 

379 ) as mocked_post_build: 

380 vivado_project_test.build(project) 

381 

382 arguments = dict( 

383 project_path=vivado_project_test.project_path, 

384 output_path=vivado_project_test.output_path, 

385 run_index=vivado_project_test.run_index, 

386 generics=copy_and_combine_dicts( 

387 dict(static_generic=2), vivado_project_test.build_time_generics 

388 ), 

389 synth_only=vivado_project_test.synth_only, 

390 from_impl=vivado_project_test.from_impl, 

391 num_threads=vivado_project_test.num_threads, 

392 other_parameter="hest", 

393 apa=123, 

394 ) 

395 mocked_pre_build.assert_called_once_with(**arguments) 

396 

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

398 mocked_post_build.assert_called_once_with(**arguments) 

399 

400 

401def test_module_pre_build_hook_returning_false_should_fail_and_not_call_vivado(vivado_project_test): 

402 module = MagicMock(spec=BaseModule) 

403 module.name = "whatever" 

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

405 

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

407 build_result = vivado_project_test.build(project) 

408 assert build_result.success 

409 vivado_project_test.mocked_run_vivado_tcl.assert_called_once() 

410 

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

412 build_result = vivado_project_test.build(project) 

413 assert not build_result.success 

414 vivado_project_test.mocked_run_vivado_tcl.assert_not_called() 

415 

416 

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

418def test_different_generic_combinations(mocked_vivado_tcl, vivado_project_test): 

419 mocked_vivado_tcl.return_value.build.return_value = "" 

420 

421 # No generics 

422 vivado_project_test.build_time_generics = None 

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

424 assert build_result.success 

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

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

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

428 

429 # Only build time generics 

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

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

432 assert build_result.success 

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

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

435 

436 # Static and build time generics 

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

438 build_result = vivado_project_test.build( 

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

440 ) 

441 assert build_result.success 

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

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

444 

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

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

447 build_result = vivado_project_test.build( 

448 VivadoProject( 

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

450 ) 

451 ) 

452 assert build_result.success 

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

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

455 

456 # Only static generics 

457 vivado_project_test.build_time_generics = None 

458 build_result = vivado_project_test.build( 

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

460 ) 

461 assert build_result.success 

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

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

464 

465 

466def test_build_time_generics_are_copied(vivado_project_test): 

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

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

469 mocked_vivado_tcl.return_value.build.return_value = "" 

470 build_result = vivado_project_test.build( 

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

472 ) 

473 assert build_result.success 

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

475 

476 

477def test_modules_are_deep_copied_before_pre_create_hook(vivado_project_test): 

478 class CustomVivadoProject(VivadoProject): 

479 def pre_create(self, **kwargs): 

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

481 return True 

482 

483 module = MagicMock(spec=BaseModule) 

484 module.registers = "Some value" 

485 

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

487 assert vivado_project_test.create(project) 

488 

489 assert module.registers == "Some value" 

490 

491 

492def test_modules_are_deep_copied_before_pre_build_hook(vivado_project_test): 

493 class CustomVivadoProject(VivadoProject): 

494 def pre_build(self, **kwargs): 

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

496 return True 

497 

498 module = MagicMock(spec=BaseModule) 

499 module.registers = "Some value" 

500 

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

502 assert vivado_project_test.build(project).success 

503 

504 assert module.registers == "Some value" 

505 

506 

507def test_get_size_is_called_correctly(vivado_project_test): 

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

509 

510 def _build_with_size(synth_only): 

511 """ 

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

513 the _get_size() method in a different way. 

514 """ 

515 with patch( 

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

517 ) as vivado_project_test.mocked_run_vivado_tcl, patch( 

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

519 ) as mocked_get_size, patch( 

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

521 ) as _: 

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

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

524 

525 build_result = project.build( 

526 project_path=vivado_project_test.project_path, 

527 output_path=vivado_project_test.output_path, 

528 run_index=vivado_project_test.run_index, 

529 synth_only=synth_only, 

530 ) 

531 

532 assert build_result.synthesis_size == "synth_size" 

533 

534 if synth_only: 

535 mocked_get_size.assert_called_once_with("synth_file") 

536 assert build_result.implementation_size is None 

537 else: 

538 assert mocked_get_size.call_count == 2 

539 mocked_get_size.assert_any_call("synth_file") 

540 mocked_get_size.assert_any_call("impl_file") 

541 

542 assert build_result.implementation_size == "impl_size" 

543 

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

545 

546 create_file( 

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

548 contents="synth_file", 

549 ) 

550 create_file( 

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

552 contents="impl_file", 

553 ) 

554 

555 _build_with_size(synth_only=True) 

556 _build_with_size(synth_only=False) 

557 

558 

559def test_netlist_build_should_set_logic_level_distribution(vivado_project_test): 

560 def _build_with_logic_level_distribution(project): 

561 """ 

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

563 also mocks the _get_logic_level_distribution() method. 

564 """ 

565 with patch( 

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

567 ) as vivado_project_test.mocked_run_vivado_tcl, patch( 

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

569 ) as _, patch( 

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

571 ) as _, patch( 

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

573 ) as mocked_get_table: 

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)