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
« 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# --------------------------------------------------------------------------------------------------
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
22from tsfpga.vivado.generics import StringGenericValue
23from tsfpga.vivado.project import VivadoNetlistProject, VivadoProject, copy_and_combine_dicts
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 )
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 )
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 )
68def test_modules_list_should_be_copied():
69 modules = [1]
70 proj = VivadoProject(name="name", modules=modules, part="part")
72 modules.append(2)
73 assert len(proj.modules) == 1
76def test_static_generics_dictionary_should_be_copied():
77 generics = dict(apa=3)
78 proj = VivadoProject(name="name", modules=[], part="part", generics=generics)
80 generics["apa"] = False
81 assert proj.static_generics["apa"] == 3
84def test_constraints_list_should_be_copied():
85 constraints = [Constraint(file="1")]
86 proj = VivadoProject(name="name", modules=[], part="part", constraints=constraints)
88 constraints.append(Constraint(file="2"))
89 assert len(proj.constraints) == 1
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)])
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'
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()])
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'
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 )
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'
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")
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")
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")
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"
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 )
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()
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}"
176def test_copy_and_combine_dict_with_both_arguments_none():
177 assert copy_and_combine_dicts(None, None) == {}
180def test_copy_and_combine_dict_with_first_argument_valid():
181 dict_first = dict(first=1)
183 result = copy_and_combine_dicts(dict_first, None)
184 assert result == dict(first=1)
185 assert dict_first == dict(first=1)
187 dict_first["first_dummy"] = True
188 assert result == dict(first=1)
191def test_copy_and_combine_dict_with_second_argument_valid():
192 dict_second = dict(second=2)
194 result = copy_and_combine_dicts(None, dict_second)
195 assert result == dict(second=2)
196 assert dict_second == dict(second=2)
198 dict_second["second_dummy"] = True
199 assert result == dict(second=2)
202def test_copy_and_combine_dict_with_both_arguments_valid():
203 dict_first = dict(first=1)
204 dict_second = dict(second=2)
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)
211 dict_first["first_dummy"] = True
212 dict_second["second_dummy"] = True
213 assert result == dict(first=1, second=2)
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)
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)
225 dict_first["first_dummy"] = True
226 dict_second["second_dummy"] = True
227 assert result == dict(first=1, second=2, common=4)
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
243 self.mocked_run_vivado_tcl = None
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 )
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 )
274 return VivadoProjectTest()
277# False positive for pytest fixtures
278# pylint: disable=redefined-outer-name
281def test_default_pre_create_hook_should_pass(vivado_project_test):
282 class CustomVivadoProject(VivadoProject):
283 pass
285 project = CustomVivadoProject(name="apa", modules=[], part="")
286 vivado_project_test.create(project)
287 vivado_project_test.mocked_run_vivado_tcl.assert_called_once()
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
297 assert not vivado_project_test.create(CustomVivadoProject(name="apa", modules=[], part=""))
298 vivado_project_test.mocked_run_vivado_tcl.assert_not_called()
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()
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
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()
342def test_default_pre_and_post_build_hooks_should_pass(vivado_project_test):
343 class CustomVivadoProject(VivadoProject):
344 pass
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()
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
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()
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
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()
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)
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)
397 arguments.update(build_result=unittest.mock.ANY)
398 mocked_post_build.assert_called_once_with(**arguments)
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="")
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()
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()
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 = ""
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"] == {}
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")
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")
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")
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")
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")
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
483 module = MagicMock(spec=BaseModule)
484 module.registers = "Some value"
486 project = CustomVivadoProject(name="apa", modules=[module], part="")
487 assert vivado_project_test.create(project)
489 assert module.registers == "Some value"
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
498 module = MagicMock(spec=BaseModule)
499 module.registers = "Some value"
501 project = CustomVivadoProject(name="apa", modules=[module], part="")
502 assert vivado_project_test.build(project).success
504 assert module.registers == "Some value"
507def test_get_size_is_called_correctly(vivado_project_test):
508 project = VivadoProject(name="apa", modules=[], part="")
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"]
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 )
532 assert build_result.synthesis_size == "synth_size"
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")
542 assert build_result.implementation_size == "impl_size"
544 create_file(vivado_project_test.project_path / "apa.xpr")
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 )
555 _build_with_size(synth_only=True)
556 _build_with_size(synth_only=False)
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"
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 )
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
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 )
599 project = VivadoNetlistProject(name="apa", modules=[], part="")
600 _build_with_logic_level_distribution(project=project)
602 project = VivadoProject(name="apa", modules=[], part="")
603 _build_with_logic_level_distribution(project=project)