Coverage for tsfpga/vivado/test/test_project.py: 100%
293 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-21 20:51 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-21 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# --------------------------------------------------------------------------------------------------
9import unittest
10from pathlib import Path
11from unittest.mock import MagicMock, patch
13import pytest
15from tsfpga.build_step_tcl_hook import BuildStepTclHook
16from tsfpga.constraint import Constraint
17from tsfpga.module import BaseModule
18from tsfpga.system_utils import create_directory, create_file
19from tsfpga.vivado.generics import StringGenericValue
20from tsfpga.vivado.project import VivadoNetlistProject, VivadoProject, copy_and_combine_dicts
22# ruff: noqa: ARG002
25def test_casting_to_string():
26 project = VivadoProject(name="my_project", modules=[], part="")
27 assert (
28 str(project)
29 == """\
30my_project
31Type: VivadoProject
32Top level: my_project_top
33Generics: -
34"""
35 )
37 project = VivadoProject(
38 name="my_project",
39 modules=[],
40 part="",
41 top="apa",
42 generics={"hest": True, "zebra": 3, "foo": StringGenericValue("/home/test.vhd")},
43 )
44 assert (
45 str(project)
46 == """\
47my_project
48Type: VivadoProject
49Top level: apa
50Generics: hest=True, zebra=3, foo=/home/test.vhd
51"""
52 )
54 project = VivadoProject(name="my_project", modules=[], part="", apa=123, hest=456)
55 assert (
56 str(project)
57 == """\
58my_project
59Type: VivadoProject
60Top level: my_project_top
61Generics: -
62Arguments: apa=123, hest=456
63"""
64 )
67def test_modules_list_should_be_copied():
68 modules = [1]
69 proj = VivadoProject(name="name", modules=modules, part="part")
71 modules.append(2)
72 assert len(proj.modules) == 1
75def test_static_generics_dictionary_should_be_copied():
76 generics = {"apa": 3}
77 proj = VivadoProject(name="name", modules=[], part="part", generics=generics)
79 generics["apa"] = False
80 assert proj.static_generics["apa"] == 3
83def test_constraints_list_should_be_copied():
84 constraints = [Constraint(file="1")]
85 proj = VivadoProject(name="name", modules=[], part="part", constraints=constraints)
87 constraints.append(Constraint(file="2"))
88 assert len(proj.constraints) == 1
91def test_bad_constraint_type_should_raise_error():
92 # Correct type should not give error
93 VivadoProject(name="name", modules=[], part="part", constraints=[Constraint(file=None)])
95 # Bad type should raise exception
96 with pytest.raises(TypeError) as exception_info:
97 VivadoProject(name="name", modules=[], part="part", constraints=["file.vhd"])
98 assert str(exception_info.value) == 'Got bad type for "constraints" element: file.vhd'
101def test_bad_tcl_sources_type_should_raise_error():
102 # Correct type should not give error
103 VivadoProject(name="name", modules=[], part="part", tcl_sources=[Path()])
105 # Bad type should raise exception
106 with pytest.raises(TypeError) as exception_info:
107 VivadoProject(name="name", modules=[], part="part", tcl_sources=["file.tcl"])
108 assert str(exception_info.value) == 'Got bad type for "tcl_sources" element: file.tcl'
111def test_bad_build_step_hooks_type_should_raise_error():
112 # Correct type should not give error
113 VivadoProject(
114 name="name",
115 modules=[],
116 part="part",
117 build_step_hooks=[BuildStepTclHook(tcl_file="", hook_step="")],
118 )
120 # Bad type should raise exception
121 with pytest.raises(TypeError) as exception_info:
122 VivadoProject(name="name", modules=[], part="part", build_step_hooks=["file.tcl"])
123 assert str(exception_info.value) == 'Got bad type for "build_step_hooks" element: file.tcl'
126def test_create_should_raise_exception_if_project_path_already_exists(tmp_path):
127 create_directory(tmp_path / "projects" / "name")
128 proj = VivadoProject(name="name", modules=[], part="part")
129 with pytest.raises(ValueError) as exception_info:
130 proj.create(tmp_path / "projects")
131 assert str(exception_info.value).startswith("Folder already exists")
134def test_build_should_raise_exception_if_project_does_not_exists(tmp_path):
135 create_directory(tmp_path / "projects")
136 proj = VivadoProject(name="name", modules=[], part="part")
137 with pytest.raises(ValueError) as exception_info:
138 proj.build(tmp_path / "projects", synth_only=True)
139 assert str(exception_info.value).startswith("Project file does not exist")
142def test_build_with_impl_run_should_raise_exception_if_no_output_path_is_given():
143 proj = VivadoProject(name="name", modules=[], part="part")
144 with pytest.raises(ValueError) as exception_info:
145 proj.build("None")
146 assert str(exception_info.value).startswith("Must specify output_path")
149def test_top_name():
150 assert VivadoProject(name="apa", modules=[], part="").top == "apa_top"
151 assert VivadoProject(name="apa", modules=[], part="", top="hest").top == "hest"
154def test_project_file_name_is_same_as_project_name():
155 project_path = Path("projects/apa")
156 assert (
157 VivadoProject(name="apa", modules=[], part="").project_file(project_path)
158 == project_path / "apa.xpr"
159 )
162def test_project_create(tmp_path):
163 with patch("tsfpga.vivado.project.run_vivado_tcl", autospec=True) as _:
164 assert VivadoProject(name="apa", modules=[], part="").create(tmp_path / "projects" / "apa")
165 assert (tmp_path / "projects" / "apa" / "create_vivado_project.tcl").exists()
168def test_project_create_should_raise_exception_if_project_path_already_exists(tmp_path):
169 project_path = create_directory(tmp_path / "projects" / "apa")
170 with pytest.raises(ValueError) as exception_info:
171 VivadoProject(name="apa", modules=[], part="").create(project_path)
172 assert str(exception_info.value) == f"Folder already exists: {project_path}"
175def test_copy_and_combine_dict_with_both_arguments_none():
176 assert copy_and_combine_dicts(None, None) == {}
179def test_copy_and_combine_dict_with_first_argument_valid():
180 dict_first = {"first": 1}
182 result = copy_and_combine_dicts(dict_first, None)
183 assert result == {"first": 1}
184 assert dict_first == {"first": 1}
186 dict_first["first_dummy"] = True
187 assert result == {"first": 1}
190def test_copy_and_combine_dict_with_second_argument_valid():
191 dict_second = {"second": 2}
193 result = copy_and_combine_dicts(None, dict_second)
194 assert result == {"second": 2}
195 assert dict_second == {"second": 2}
197 dict_second["second_dummy"] = True
198 assert result == {"second": 2}
201def test_copy_and_combine_dict_with_both_arguments_valid():
202 dict_first = {"first": 1}
203 dict_second = {"second": 2}
205 result = copy_and_combine_dicts(dict_first, dict_second)
206 assert result == {"first": 1, "second": 2}
207 assert dict_first == {"first": 1}
208 assert dict_second == {"second": 2}
210 dict_first["first_dummy"] = True
211 dict_second["second_dummy"] = True
212 assert result == {"first": 1, "second": 2}
215def test_copy_and_combine_dict_with_both_arguments_valid_and_same_key():
216 dict_first = {"first": 1, "common": 3}
217 dict_second = {"second": 2, "common": 4}
219 result = copy_and_combine_dicts(dict_first, dict_second)
220 assert result == {"first": 1, "second": 2, "common": 4}
221 assert dict_first == {"first": 1, "common": 3}
222 assert dict_second == {"second": 2, "common": 4}
224 dict_first["first_dummy"] = True
225 dict_second["second_dummy"] = True
226 assert result == {"first": 1, "second": 2, "common": 4}
229@pytest.fixture
230def vivado_project_test(tmp_path):
231 class VivadoProjectTest:
232 def __init__(self):
233 self.project_path = tmp_path / "projects" / "apa" / "project"
234 self.output_path = tmp_path / "projects" / "apa"
235 self.ip_cache_path = MagicMock()
236 self.build_time_generics = {"enable": True}
237 self.num_threads = 4
238 self.run_index = 3
239 self.synth_only = False
240 self.from_impl = False
242 self.mocked_run_vivado_tcl = None
244 def create(self, project, **other_arguments):
245 with patch(
246 "tsfpga.vivado.project.run_vivado_tcl", autospec=True
247 ) as self.mocked_run_vivado_tcl:
248 return project.create(
249 project_path=self.project_path,
250 ip_cache_path=self.ip_cache_path,
251 **other_arguments,
252 )
254 def build(self, project):
255 with (
256 patch(
257 "tsfpga.vivado.project.run_vivado_tcl", autospec=True
258 ) as self.mocked_run_vivado_tcl,
259 patch("tsfpga.vivado.project.VivadoProject._get_size", autospec=True) as _,
260 patch("tsfpga.vivado.project.shutil.copy2", autospec=True) as _,
261 ):
262 create_file(self.project_path / "apa.xpr")
263 return project.build(
264 project_path=self.project_path,
265 output_path=self.output_path,
266 run_index=self.run_index,
267 generics=self.build_time_generics,
268 synth_only=self.synth_only,
269 num_threads=self.num_threads,
270 other_parameter="hest",
271 )
273 return VivadoProjectTest()
276def test_default_pre_create_hook_should_pass(vivado_project_test):
277 class CustomVivadoProject(VivadoProject):
278 pass
280 project = CustomVivadoProject(name="apa", modules=[], part="")
281 vivado_project_test.create(project)
282 vivado_project_test.mocked_run_vivado_tcl.assert_called_once()
285def test_project_pre_create_hook_returning_false_should_fail_and_not_call_vivado_run(
286 vivado_project_test,
287):
288 class CustomVivadoProject(VivadoProject):
289 def pre_create(self, **kwargs):
290 return False
292 assert not vivado_project_test.create(CustomVivadoProject(name="apa", modules=[], part=""))
293 vivado_project_test.mocked_run_vivado_tcl.assert_not_called()
296def test_create_should_call_pre_create_with_correct_parameters(vivado_project_test):
297 project = VivadoProject(name="apa", modules=[], part="", generics={"apa": 123}, hest=456)
298 with patch("tsfpga.vivado.project.VivadoProject.pre_create") as mocked_pre_create:
299 vivado_project_test.create(project, zebra=789)
300 mocked_pre_create.assert_called_once_with(
301 project_path=vivado_project_test.project_path,
302 ip_cache_path=vivado_project_test.ip_cache_path,
303 part="",
304 generics={"apa": 123},
305 hest=456,
306 zebra=789,
307 )
308 vivado_project_test.mocked_run_vivado_tcl.assert_called_once()
311def test_build_module_pre_build_hook_and_create_regs_are_called(vivado_project_test):
312 project = VivadoProject(
313 name="apa",
314 modules=[MagicMock(spec=BaseModule), MagicMock(spec=BaseModule)],
315 part="",
316 apa=123,
317 )
318 build_result = vivado_project_test.build(project)
319 assert build_result.success
321 for module in project.modules:
322 module.pre_build.assert_called_once_with(
323 project=project,
324 other_parameter="hest",
325 apa=123,
326 project_path=vivado_project_test.project_path,
327 output_path=vivado_project_test.output_path,
328 run_index=vivado_project_test.run_index,
329 generics=vivado_project_test.build_time_generics,
330 synth_only=vivado_project_test.synth_only,
331 from_impl=vivado_project_test.from_impl,
332 num_threads=vivado_project_test.num_threads,
333 )
334 module.create_register_synthesis_files.assert_called_once()
335 module.create_register_simulation_files.assert_not_called()
338def test_default_pre_and_post_build_hooks_should_pass(vivado_project_test):
339 class CustomVivadoProject(VivadoProject):
340 pass
342 build_result = vivado_project_test.build(CustomVivadoProject(name="apa", modules=[], part=""))
343 assert build_result.success
344 vivado_project_test.mocked_run_vivado_tcl.assert_called_once()
347def test_project_pre_build_hook_returning_false_should_fail_and_not_call_vivado_run(
348 vivado_project_test,
349):
350 class CustomVivadoProject(VivadoProject):
351 def pre_build(self, **kwargs):
352 return False
354 build_result = vivado_project_test.build(CustomVivadoProject(name="apa", modules=[], part=""))
355 assert not build_result.success
356 vivado_project_test.mocked_run_vivado_tcl.assert_not_called()
359def test_project_post_build_hook_returning_false_should_fail(vivado_project_test):
360 class CustomVivadoProject(VivadoProject):
361 def post_build(self, **kwargs):
362 return False
364 build_result = vivado_project_test.build(CustomVivadoProject(name="apa", modules=[], part=""))
365 assert not build_result.success
366 vivado_project_test.mocked_run_vivado_tcl.assert_called_once()
369def test_project_build_hooks_should_be_called_with_correct_parameters(vivado_project_test):
370 project = VivadoProject(
371 name="apa", modules=[], part="", generics={"static_generic": 2}, apa=123
372 )
373 with (
374 patch("tsfpga.vivado.project.VivadoProject.pre_build") as mocked_pre_build,
375 patch("tsfpga.vivado.project.VivadoProject.post_build") as mocked_post_build,
376 ):
377 vivado_project_test.build(project)
379 arguments = {
380 "project_path": vivado_project_test.project_path,
381 "output_path": vivado_project_test.output_path,
382 "run_index": vivado_project_test.run_index,
383 "generics": copy_and_combine_dicts(
384 {"static_generic": 2}, vivado_project_test.build_time_generics
385 ),
386 "synth_only": vivado_project_test.synth_only,
387 "from_impl": vivado_project_test.from_impl,
388 "num_threads": vivado_project_test.num_threads,
389 "other_parameter": "hest",
390 "apa": 123,
391 }
392 mocked_pre_build.assert_called_once_with(**arguments)
394 arguments.update(build_result=unittest.mock.ANY)
395 mocked_post_build.assert_called_once_with(**arguments)
398def test_module_pre_build_hook_returning_false_should_fail_and_not_call_vivado(vivado_project_test):
399 module = MagicMock(spec=BaseModule)
400 module.name = "whatever"
401 project = VivadoProject(name="apa", modules=[module], part="")
403 project.modules[0].pre_build.return_value = True
404 build_result = vivado_project_test.build(project)
405 assert build_result.success
406 vivado_project_test.mocked_run_vivado_tcl.assert_called_once()
408 project.modules[0].pre_build.return_value = False
409 build_result = vivado_project_test.build(project)
410 assert not build_result.success
411 vivado_project_test.mocked_run_vivado_tcl.assert_not_called()
414@patch("tsfpga.vivado.project.VivadoTcl", autospec=True)
415def test_different_generic_combinations(mocked_vivado_tcl, vivado_project_test):
416 mocked_vivado_tcl.return_value.build.return_value = ""
418 # No generics
419 vivado_project_test.build_time_generics = None
420 build_result = vivado_project_test.build(VivadoProject(name="apa", modules=[], part=""))
421 assert build_result.success
422 # Note: In python 3.8 we can use call_args.kwargs straight away
423 _, kwargs = mocked_vivado_tcl.return_value.build.call_args
424 assert kwargs["generics"] == {}
426 # Only build time generics
427 vivado_project_test.build_time_generics = {"runtime": "value"}
428 build_result = vivado_project_test.build(VivadoProject(name="apa", modules=[], part=""))
429 assert build_result.success
430 _, kwargs = mocked_vivado_tcl.return_value.build.call_args
431 assert kwargs["generics"] == {"runtime": "value"}
433 # Static and build time generics
434 vivado_project_test.build_time_generics = {"runtime": "value"}
435 build_result = vivado_project_test.build(
436 VivadoProject(name="apa", modules=[], part="", generics={"static": "a value"})
437 )
438 assert build_result.success
439 _, kwargs = mocked_vivado_tcl.return_value.build.call_args
440 assert kwargs["generics"] == {"runtime": "value", "static": "a value"}
442 # Same key in both static and build time generic. Should prefer build time.
443 vivado_project_test.build_time_generics = {"static_and_runtime": "build value"}
444 build_result = vivado_project_test.build(
445 VivadoProject(
446 name="apa", modules=[], part="", generics={"static_and_runtime": "static value"}
447 )
448 )
449 assert build_result.success
450 _, kwargs = mocked_vivado_tcl.return_value.build.call_args
451 assert kwargs["generics"] == {"static_and_runtime": "build value"}
453 # Only static generics
454 vivado_project_test.build_time_generics = None
455 build_result = vivado_project_test.build(
456 VivadoProject(name="apa", modules=[], part="", generics={"runtime": "a value"})
457 )
458 assert build_result.success
459 _, kwargs = mocked_vivado_tcl.return_value.build.call_args
460 assert kwargs["generics"] == {"runtime": "a value"}
463def test_build_time_generics_are_copied(vivado_project_test):
464 vivado_project_test.build_time_generics = {"runtime": "value"}
465 with patch("tsfpga.vivado.project.VivadoTcl", autospec=True) as mocked_vivado_tcl:
466 mocked_vivado_tcl.return_value.build.return_value = ""
467 build_result = vivado_project_test.build(
468 VivadoProject(name="apa", modules=[], part="", generics={"static": "a value"})
469 )
470 assert build_result.success
471 assert vivado_project_test.build_time_generics == {"runtime": "value"}
474def test_modules_are_deep_copied_before_pre_create_hook(vivado_project_test):
475 class CustomVivadoProject(VivadoProject):
476 def pre_create(self, **kwargs):
477 self.modules[0].registers = "Some other value"
478 return True
480 module = MagicMock(spec=BaseModule)
481 module.registers = "Some value"
483 project = CustomVivadoProject(name="apa", modules=[module], part="")
484 assert vivado_project_test.create(project)
486 assert module.registers == "Some value"
489def test_modules_are_deep_copied_before_pre_build_hook(vivado_project_test):
490 class CustomVivadoProject(VivadoProject):
491 def pre_build(self, **kwargs):
492 self.modules[0].registers = "Some other value"
493 return True
495 module = MagicMock(spec=BaseModule)
496 module.registers = "Some value"
498 project = CustomVivadoProject(name="apa", modules=[module], part="")
499 assert vivado_project_test.build(project).success
501 assert module.registers == "Some value"
504def test_get_size_is_called_correctly(vivado_project_test):
505 project = VivadoProject(name="apa", modules=[], part="")
507 def _build_with_size(synth_only):
508 """
509 The project.build() call is very similar to _build() method in this class, but it mocks
510 the _get_size() method in a different way.
511 """
512 with (
513 patch(
514 "tsfpga.vivado.project.run_vivado_tcl", autospec=True
515 ) as vivado_project_test.mocked_run_vivado_tcl,
516 patch(
517 "tsfpga.vivado.project.HierarchicalUtilizationParser.get_size", autospec=True
518 ) as mocked_get_size,
519 patch("tsfpga.vivado.project.shutil.copy2", autospec=True) as _,
520 ):
521 # Only the first return value will be used if we are in synth_only
522 mocked_get_size.side_effect = ["synth_size", "impl_size"]
524 build_result = project.build(
525 project_path=vivado_project_test.project_path,
526 output_path=vivado_project_test.output_path,
527 run_index=vivado_project_test.run_index,
528 synth_only=synth_only,
529 )
531 assert build_result.synthesis_size == "synth_size"
533 if synth_only:
534 mocked_get_size.assert_called_once_with("synth_file")
535 assert build_result.implementation_size is None
536 else:
537 assert mocked_get_size.call_count == 2
538 mocked_get_size.assert_any_call("synth_file")
539 mocked_get_size.assert_any_call("impl_file")
541 assert build_result.implementation_size == "impl_size"
543 create_file(vivado_project_test.project_path / "apa.xpr")
545 create_file(
546 vivado_project_test.project_path / "apa.runs" / "synth_3" / "hierarchical_utilization.rpt",
547 contents="synth_file",
548 )
549 create_file(
550 vivado_project_test.project_path / "apa.runs" / "impl_3" / "hierarchical_utilization.rpt",
551 contents="impl_file",
552 )
554 _build_with_size(synth_only=True)
555 _build_with_size(synth_only=False)
558def test_netlist_build_should_set_logic_level_distribution(vivado_project_test):
559 def _build_with_logic_level_distribution(project):
560 """
561 The project.build() call is very similar to _build() method in this class, except it
562 also mocks the _get_logic_level_distribution() method.
563 """
564 with (
565 patch(
566 "tsfpga.vivado.project.run_vivado_tcl", autospec=True
567 ) as vivado_project_test.mocked_run_vivado_tcl,
568 patch("tsfpga.vivado.project.VivadoProject._get_size", autospec=True) as _,
569 patch("tsfpga.vivado.project.shutil.copy2", autospec=True) as _,
570 patch(
571 "tsfpga.vivado.project.LogicLevelDistributionParser.get_table", autospec=True
572 ) as mocked_get_table,
573 ):
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)