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

292 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-05-31 20:00 +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://gitlab.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 

22 

23# pylint: disable=unused-import 

24from tsfpga.test.conftest import fixture_tmp_path # noqa: F401 

25from tsfpga.vivado.generics import StringGenericValue 

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

27 

28 

29def test_casting_to_string(): 

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

31 assert ( 

32 str(project) 

33 == """\ 

34my_project 

35Type: VivadoProject 

36Top level: my_project_top 

37Generics: - 

38""" 

39 ) 

40 

41 project = VivadoProject( 

42 name="my_project", 

43 modules=[], 

44 part="", 

45 top="apa", 

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

47 ) 

48 assert ( 

49 str(project) 

50 == """\ 

51my_project 

52Type: VivadoProject 

53Top level: apa 

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

55""" 

56 ) 

57 

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

59 assert ( 

60 str(project) 

61 == """\ 

62my_project 

63Type: VivadoProject 

64Top level: my_project_top 

65Generics: - 

66Arguments: apa=123, hest=456 

67""" 

68 ) 

69 

70 

71def test_modules_list_should_be_copied(): 

72 modules = [1] 

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

74 

75 modules.append(2) 

76 assert len(proj.modules) == 1 

77 

78 

79def test_static_generics_dictionary_should_be_copied(): 

80 generics = dict(apa=3) 

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

82 

83 generics["apa"] = False 

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

85 

86 

87def test_constraints_list_should_be_copied(): 

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

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

90 

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

92 assert len(proj.constraints) == 1 

93 

94 

95def test_bad_constraint_type_should_raise_error(): 

96 # Correct type should not give error 

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

98 

99 # Bad type should raise exception 

100 with pytest.raises(TypeError) as exception_info: 

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

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

103 

104 

105def test_bad_tcl_sources_type_should_raise_error(): 

106 # Correct type should not give error 

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

108 

109 # Bad type should raise exception 

110 with pytest.raises(TypeError) as exception_info: 

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

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

113 

114 

115def test_bad_build_step_hooks_type_should_raise_error(): 

116 # Correct type should not give error 

117 VivadoProject( 

118 name="name", 

119 modules=[], 

120 part="part", 

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

122 ) 

123 

124 # Bad type should raise exception 

125 with pytest.raises(TypeError) as exception_info: 

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

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

128 

129 

130def test_create_should_raise_exception_if_project_path_already_exists(tmp_path): 

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

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

133 with pytest.raises(ValueError) as exception_info: 

134 proj.create(tmp_path / "projects") 

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

136 

137 

138def test_build_should_raise_exception_if_project_does_not_exists(tmp_path): 

139 create_directory(tmp_path / "projects") 

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

141 with pytest.raises(ValueError) as exception_info: 

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

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

144 

145 

146def test_build_with_impl_run_should_raise_exception_if_no_output_path_is_given(): 

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

148 with pytest.raises(ValueError) as exception_info: 

149 proj.build("None") 

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

151 

152 

153def test_top_name(): 

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

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

156 

157 

158def test_project_file_name_is_same_as_project_name(): 

159 project_path = Path("projects/apa") 

160 assert ( 

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

162 == project_path / "apa.xpr" 

163 ) 

164 

165 

166def test_project_create(tmp_path): 

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

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

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

170 

171 

172def test_project_create_should_raise_exception_if_project_path_already_exists(tmp_path): 

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

174 with pytest.raises(ValueError) as exception_info: 

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

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

177 

178 

179def test_copy_and_combine_dict_with_both_arguments_none(): 

180 assert copy_and_combine_dicts(None, None) is None 

181 

182 

183def test_copy_and_combine_dict_with_first_argument_valid(): 

184 dict_first = dict(first=1) 

185 

186 result = copy_and_combine_dicts(dict_first, None) 

187 assert result == dict(first=1) 

188 assert dict_first == dict(first=1) 

189 

190 dict_first["first_dummy"] = True 

191 assert result == dict(first=1) 

192 

193 

194def test_copy_and_combine_dict_with_second_argument_valid(): 

195 dict_second = dict(second=2) 

196 

197 result = copy_and_combine_dicts(None, dict_second) 

198 assert result == dict(second=2) 

199 assert dict_second == dict(second=2) 

200 

201 dict_second["second_dummy"] = True 

202 assert result == dict(second=2) 

203 

204 

205def test_copy_and_combine_dict_with_both_arguments_valid(): 

206 dict_first = dict(first=1) 

207 dict_second = dict(second=2) 

208 

209 result = copy_and_combine_dicts(dict_first, dict_second) 

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

211 assert dict_first == dict(first=1) 

212 assert dict_second == dict(second=2) 

213 

214 dict_first["first_dummy"] = True 

215 dict_second["second_dummy"] = True 

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

217 

218 

219def test_copy_and_combine_dict_with_both_arguments_valid_and_same_key(): 

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

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

222 

223 result = copy_and_combine_dicts(dict_first, dict_second) 

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

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

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

227 

228 dict_first["first_dummy"] = True 

229 dict_second["second_dummy"] = True 

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

231 

232 

233# pylint: disable=too-many-instance-attributes 

234@pytest.mark.usefixtures("fixture_tmp_path") 

235class TestVivadoProject(unittest.TestCase): 

236 tmp_path = None 

237 

238 def setUp(self): 

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

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

241 self.ip_cache_path = MagicMock() 

242 self.build_time_generics = dict(enable=True) 

243 self.num_threads = 4 

244 self.run_index = 3 

245 self.synth_only = False 

246 self.from_impl = False 

247 

248 self.mocked_run_vivado_tcl = None 

249 

250 def _create(self, project, **other_arguments): 

251 with patch( 

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

253 ) as self.mocked_run_vivado_tcl: 

254 return project.create( 

255 project_path=self.project_path, ip_cache_path=self.ip_cache_path, **other_arguments 

256 ) 

257 

258 def test_default_pre_create_hook_should_pass(self): 

259 class CustomVivadoProject(VivadoProject): 

260 pass 

261 

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

263 self._create(project) 

264 self.mocked_run_vivado_tcl.assert_called_once() 

265 

266 def test_project_pre_create_hook_returning_false_should_fail_and_not_call_vivado_run(self): 

267 class CustomVivadoProject(VivadoProject): 

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

269 return False 

270 

271 assert not self._create(CustomVivadoProject(name="apa", modules=[], part="")) 

272 self.mocked_run_vivado_tcl.assert_not_called() 

273 

274 def test_create_should_call_pre_create_with_correct_parameters(self): 

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

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

277 self._create(project, zebra=789) 

278 mocked_pre_create.assert_called_once_with( 

279 project_path=self.project_path, 

280 ip_cache_path=self.ip_cache_path, 

281 part="", 

282 generics=dict(apa=123), 

283 hest=456, 

284 zebra=789, 

285 ) 

286 self.mocked_run_vivado_tcl.assert_called_once() 

287 

288 def _build(self, project): 

289 with patch( 

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

291 ) as self.mocked_run_vivado_tcl, patch( 

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

293 ) as _, patch( 

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

295 ) as _: 

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

297 return project.build( 

298 project_path=self.project_path, 

299 output_path=self.output_path, 

300 run_index=self.run_index, 

301 generics=self.build_time_generics, 

302 synth_only=self.synth_only, 

303 num_threads=self.num_threads, 

304 other_parameter="hest", 

305 ) 

306 

307 def test_build_module_pre_build_hook_and_create_regs_are_called(self): 

308 project = VivadoProject( 

309 name="apa", 

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

311 part="", 

312 apa=123, 

313 ) 

314 build_result = self._build(project) 

315 assert build_result.success 

316 

317 for module in project.modules: 

318 module.pre_build.assert_called_once_with( 

319 project=project, 

320 other_parameter="hest", 

321 apa=123, 

322 project_path=self.project_path, 

323 output_path=self.output_path, 

324 run_index=self.run_index, 

325 generics=self.build_time_generics, 

326 synth_only=self.synth_only, 

327 from_impl=self.from_impl, 

328 num_threads=self.num_threads, 

329 ) 

330 module.create_regs_vhdl_package.assert_called_once() 

331 

332 def test_default_pre_and_post_build_hooks_should_pass(self): 

333 class CustomVivadoProject(VivadoProject): 

334 pass 

335 

336 build_result = self._build(CustomVivadoProject(name="apa", modules=[], part="")) 

337 assert build_result.success 

338 self.mocked_run_vivado_tcl.assert_called_once() 

339 

340 def test_project_pre_build_hook_returning_false_should_fail_and_not_call_vivado_run(self): 

341 class CustomVivadoProject(VivadoProject): 

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

343 return False 

344 

345 build_result = self._build(CustomVivadoProject(name="apa", modules=[], part="")) 

346 assert not build_result.success 

347 self.mocked_run_vivado_tcl.assert_not_called() 

348 

349 def test_project_post_build_hook_returning_false_should_fail(self): 

350 class CustomVivadoProject(VivadoProject): 

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

352 return False 

353 

354 build_result = self._build(CustomVivadoProject(name="apa", modules=[], part="")) 

355 assert not build_result.success 

356 self.mocked_run_vivado_tcl.assert_called_once() 

357 

358 def test_project_build_hooks_should_be_called_with_correct_parameters(self): 

359 project = VivadoProject( 

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

361 ) 

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

363 "tsfpga.vivado.project.VivadoProject.post_build" 

364 ) as mocked_post_build: 

365 self._build(project) 

366 

367 arguments = dict( 

368 project_path=self.project_path, 

369 output_path=self.output_path, 

370 run_index=self.run_index, 

371 generics=copy_and_combine_dicts(dict(static_generic=2), self.build_time_generics), 

372 synth_only=self.synth_only, 

373 from_impl=self.from_impl, 

374 num_threads=self.num_threads, 

375 other_parameter="hest", 

376 apa=123, 

377 ) 

378 mocked_pre_build.assert_called_once_with(**arguments) 

379 

380 # Could be improved by actually checking the build_result object. 

381 # See https://gitlab.com/tsfpga/tsfpga/-/issues/39 

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

383 mocked_post_build.assert_called_once_with(**arguments) 

384 

385 def test_module_pre_build_hook_returning_false_should_fail_and_not_call_vivado(self): 

386 module = MagicMock(spec=BaseModule) 

387 module.name = "whatever" 

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

389 

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

391 build_result = self._build(project) 

392 assert build_result.success 

393 self.mocked_run_vivado_tcl.assert_called_once() 

394 

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

396 build_result = self._build(project) 

397 assert not build_result.success 

398 self.mocked_run_vivado_tcl.assert_not_called() 

399 

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

401 def test_different_generic_combinations(self, mocked_vivado_tcl): 

402 mocked_vivado_tcl.return_value.build.return_value = "" 

403 

404 # No generics 

405 self.build_time_generics = None 

406 build_result = self._build(VivadoProject(name="apa", modules=[], part="")) 

407 assert build_result.success 

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

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

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

411 

412 # Only build time generics 

413 self.build_time_generics = dict(runtime="value") 

414 build_result = self._build(VivadoProject(name="apa", modules=[], part="")) 

415 assert build_result.success 

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

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

418 

419 # Static and build time generics 

420 self.build_time_generics = dict(runtime="value") 

421 build_result = self._build( 

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

423 ) 

424 assert build_result.success 

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

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

427 

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

429 self.build_time_generics = dict(static_and_runtime="build value") 

430 build_result = self._build( 

431 VivadoProject( 

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

433 ) 

434 ) 

435 assert build_result.success 

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

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

438 

439 # Only static generics 

440 self.build_time_generics = None 

441 build_result = self._build( 

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

443 ) 

444 assert build_result.success 

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

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

447 

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

449 def test_build_time_generics_are_copied(self, mocked_vivado_tcl): 

450 mocked_vivado_tcl.return_value.build.return_value = "" 

451 

452 self.build_time_generics = dict(runtime="value") 

453 build_result = self._build( 

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

455 ) 

456 assert build_result.success 

457 assert self.build_time_generics == dict(runtime="value") 

458 

459 def test_modules_are_deep_copied_before_pre_create_hook(self): 

460 class CustomVivadoProject(VivadoProject): 

461 def pre_create(self, **kwargs): 

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

463 return True 

464 

465 module = MagicMock(spec=BaseModule) 

466 module.registers = "Some value" 

467 

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

469 assert self._create(project) 

470 

471 assert module.registers == "Some value" 

472 

473 def test_modules_are_deep_copied_before_pre_build_hook(self): 

474 class CustomVivadoProject(VivadoProject): 

475 def pre_build(self, **kwargs): 

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

477 return True 

478 

479 module = MagicMock(spec=BaseModule) 

480 module.registers = "Some value" 

481 

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

483 assert self._build(project).success 

484 

485 assert module.registers == "Some value" 

486 

487 def test_get_size_is_called_correctly(self): 

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

489 

490 def _build_with_size(synth_only): 

491 """ 

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

493 the _get_size() method in a different way. 

494 """ 

495 with patch( 

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

497 ) as self.mocked_run_vivado_tcl, patch( 

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

499 ) as mocked_get_size, patch( 

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

501 ) as _: 

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

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

504 

505 build_result = project.build( 

506 project_path=self.project_path, 

507 output_path=self.output_path, 

508 run_index=self.run_index, 

509 synth_only=synth_only, 

510 ) 

511 

512 assert build_result.synthesis_size == "synth_size" 

513 

514 if synth_only: 

515 mocked_get_size.assert_called_once_with("synth_file") 

516 assert build_result.implementation_size is None 

517 else: 

518 assert mocked_get_size.call_count == 2 

519 mocked_get_size.assert_any_call("synth_file") 

520 mocked_get_size.assert_any_call("impl_file") 

521 

522 assert build_result.implementation_size == "impl_size" 

523 

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

525 

526 create_file( 

527 self.project_path / "apa.runs" / "synth_3" / "hierarchical_utilization.rpt", 

528 contents="synth_file", 

529 ) 

530 create_file( 

531 self.project_path / "apa.runs" / "impl_3" / "hierarchical_utilization.rpt", 

532 contents="impl_file", 

533 ) 

534 

535 _build_with_size(synth_only=True) 

536 _build_with_size(synth_only=False) 

537 

538 def test_netlist_build_should_set_logic_level_distribution(self): 

539 def _build_with_logic_level_distribution(project): 

540 """ 

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

542 also mocks the _get_logic_level_distribution() method. 

543 """ 

544 with patch( 

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

546 ) as self.mocked_run_vivado_tcl, patch( 

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

548 ) as _, patch( 

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

550 ) as _, patch( 

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

552 ) as mocked_get_table: 

553 mocked_get_table.return_value = "logic_table" 

554 

555 build_result = project.build( 

556 project_path=self.project_path, 

557 output_path=self.output_path, 

558 run_index=self.run_index, 

559 ) 

560 

561 if project.is_netlist_build: 

562 mocked_get_table.assert_called_once_with("logic_file") 

563 assert build_result.logic_level_distribution == "logic_table" 

564 else: 

565 mocked_get_table.assert_not_called() 

566 assert build_result.logic_level_distribution is None 

567 assert build_result.maximum_logic_level is None 

568 

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

570 create_file( 

571 self.project_path / "apa.runs" / "synth_3" / "logical_level_distribution.rpt", 

572 contents="logic_file", 

573 ) 

574 

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

576 _build_with_logic_level_distribution(project=project) 

577 

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

579 _build_with_logic_level_distribution(project=project)