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
« 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# --------------------------------------------------------------------------------------------------
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_register_synthesis_files.assert_called_once()
340 module.create_register_simulation_files.assert_not_called()
343def test_default_pre_and_post_build_hooks_should_pass(vivado_project_test):
344 class CustomVivadoProject(VivadoProject):
345 pass
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()
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
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()
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
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()
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)
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)
398 arguments.update(build_result=unittest.mock.ANY)
399 mocked_post_build.assert_called_once_with(**arguments)
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="")
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()
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()
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 = ""
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"] == {}
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")
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")
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")
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")
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")
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
484 module = MagicMock(spec=BaseModule)
485 module.registers = "Some value"
487 project = CustomVivadoProject(name="apa", modules=[module], part="")
488 assert vivado_project_test.create(project)
490 assert module.registers == "Some value"
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
499 module = MagicMock(spec=BaseModule)
500 module.registers = "Some value"
502 project = CustomVivadoProject(name="apa", modules=[module], part="")
503 assert vivado_project_test.build(project).success
505 assert module.registers == "Some value"
508def test_get_size_is_called_correctly(vivado_project_test):
509 project = VivadoProject(name="apa", modules=[], part="")
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"]
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 )
533 assert build_result.synthesis_size == "synth_size"
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")
543 assert build_result.implementation_size == "impl_size"
545 create_file(vivado_project_test.project_path / "apa.xpr")
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 )
556 _build_with_size(synth_only=True)
557 _build_with_size(synth_only=False)
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"
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 )
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
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 )
600 project = VivadoNetlistProject(name="apa", modules=[], part="")
601 _build_with_logic_level_distribution(project=project)
603 project = VivadoProject(name="apa", modules=[], part="")
604 _build_with_logic_level_distribution(project=project)