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
« 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# --------------------------------------------------------------------------------------------------
9# Standard libraries
10import unittest
11from pathlib import Path
12from unittest.mock import MagicMock, patch
14# Third party libraries
15import pytest
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
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
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 )
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 )
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 )
71def test_modules_list_should_be_copied():
72 modules = [1]
73 proj = VivadoProject(name="name", modules=modules, part="part")
75 modules.append(2)
76 assert len(proj.modules) == 1
79def test_static_generics_dictionary_should_be_copied():
80 generics = dict(apa=3)
81 proj = VivadoProject(name="name", modules=[], part="part", generics=generics)
83 generics["apa"] = False
84 assert proj.static_generics["apa"] == 3
87def test_constraints_list_should_be_copied():
88 constraints = [Constraint(file="1")]
89 proj = VivadoProject(name="name", modules=[], part="part", constraints=constraints)
91 constraints.append(Constraint(file="2"))
92 assert len(proj.constraints) == 1
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)])
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'
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()])
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'
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 )
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'
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")
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")
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")
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"
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 )
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()
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}"
179def test_copy_and_combine_dict_with_both_arguments_none():
180 assert copy_and_combine_dicts(None, None) is None
183def test_copy_and_combine_dict_with_first_argument_valid():
184 dict_first = dict(first=1)
186 result = copy_and_combine_dicts(dict_first, None)
187 assert result == dict(first=1)
188 assert dict_first == dict(first=1)
190 dict_first["first_dummy"] = True
191 assert result == dict(first=1)
194def test_copy_and_combine_dict_with_second_argument_valid():
195 dict_second = dict(second=2)
197 result = copy_and_combine_dicts(None, dict_second)
198 assert result == dict(second=2)
199 assert dict_second == dict(second=2)
201 dict_second["second_dummy"] = True
202 assert result == dict(second=2)
205def test_copy_and_combine_dict_with_both_arguments_valid():
206 dict_first = dict(first=1)
207 dict_second = dict(second=2)
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)
214 dict_first["first_dummy"] = True
215 dict_second["second_dummy"] = True
216 assert result == dict(first=1, second=2)
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)
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)
228 dict_first["first_dummy"] = True
229 dict_second["second_dummy"] = True
230 assert result == dict(first=1, second=2, common=4)
233# pylint: disable=too-many-instance-attributes
234@pytest.mark.usefixtures("fixture_tmp_path")
235class TestVivadoProject(unittest.TestCase):
236 tmp_path = None
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
248 self.mocked_run_vivado_tcl = None
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 )
258 def test_default_pre_create_hook_should_pass(self):
259 class CustomVivadoProject(VivadoProject):
260 pass
262 project = CustomVivadoProject(name="apa", modules=[], part="")
263 self._create(project)
264 self.mocked_run_vivado_tcl.assert_called_once()
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
271 assert not self._create(CustomVivadoProject(name="apa", modules=[], part=""))
272 self.mocked_run_vivado_tcl.assert_not_called()
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()
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 )
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
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()
332 def test_default_pre_and_post_build_hooks_should_pass(self):
333 class CustomVivadoProject(VivadoProject):
334 pass
336 build_result = self._build(CustomVivadoProject(name="apa", modules=[], part=""))
337 assert build_result.success
338 self.mocked_run_vivado_tcl.assert_called_once()
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
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()
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
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()
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)
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)
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)
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="")
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()
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()
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 = ""
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"] == {}
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")
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")
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")
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")
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 = ""
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")
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
465 module = MagicMock(spec=BaseModule)
466 module.registers = "Some value"
468 project = CustomVivadoProject(name="apa", modules=[module], part="")
469 assert self._create(project)
471 assert module.registers == "Some value"
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
479 module = MagicMock(spec=BaseModule)
480 module.registers = "Some value"
482 project = CustomVivadoProject(name="apa", modules=[module], part="")
483 assert self._build(project).success
485 assert module.registers == "Some value"
487 def test_get_size_is_called_correctly(self):
488 project = VivadoProject(name="apa", modules=[], part="")
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"]
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 )
512 assert build_result.synthesis_size == "synth_size"
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")
522 assert build_result.implementation_size == "impl_size"
524 create_file(self.project_path / "apa.xpr")
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 )
535 _build_with_size(synth_only=True)
536 _build_with_size(synth_only=False)
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"
555 build_result = project.build(
556 project_path=self.project_path,
557 output_path=self.output_path,
558 run_index=self.run_index,
559 )
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
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 )
575 project = VivadoNetlistProject(name="apa", modules=[], part="")
576 _build_with_logic_level_distribution(project=project)
578 project = VivadoProject(name="apa", modules=[], part="")
579 _build_with_logic_level_distribution(project=project)