Coverage for tsfpga/vivado/test/test_tcl.py: 100%
163 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-17 20:51 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-17 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# --------------------------------------------------------------------------------------------------
9from collections import OrderedDict
10from pathlib import Path
11from unittest.mock import MagicMock
13import pytest
15from tsfpga.build_step_tcl_hook import BuildStepTclHook
16from tsfpga.constraint import Constraint
17from tsfpga.ip_core_file import IpCoreFile
18from tsfpga.module import BaseModule, get_modules
19from tsfpga.system_utils import create_file
20from tsfpga.vivado.common import to_tcl_path
21from tsfpga.vivado.generics import BitVectorGenericValue, StringGenericValue
22from tsfpga.vivado.tcl import VivadoTcl
25def test_set_create_run_index():
26 tcl = VivadoTcl(name="").create(project_folder=Path(), modules=[], part="", top="", run_index=2)
27 assert '\ncurrent_run [get_runs "synth_2"]\n' in tcl
30def test_static_generics():
31 # Use OrderedDict here in test so that order will be preserved and we can test for equality.
32 # In real world case a normal dict can be used.
33 generics = OrderedDict(
34 enable=True,
35 disable=False,
36 integer=123,
37 slv=BitVectorGenericValue("0101"),
38 string=StringGenericValue("apa"),
39 )
41 tcl = VivadoTcl(name="").create(
42 project_folder=Path(), modules=[], part="", top="", run_index=1, generics=generics
43 )
44 expected = (
45 '\nset_property "generic" '
46 "{enable=1'b1 disable=1'b0 integer=123 slv=4'b0101 string=\"apa\"} "
47 "[current_fileset]\n"
48 )
49 assert expected in tcl
52def test_build_step_hooks(tmp_path):
53 project_folder = tmp_path / "dummy_project_folder"
55 dummy1 = BuildStepTclHook(Path("dummy.tcl"), "STEPS.SYNTH_DESIGN.TCL.PRE")
56 dummy2 = BuildStepTclHook(Path("files.tcl"), "STEPS.ROUTE_DESIGN.TCL.PRE")
58 tcl = VivadoTcl(name="").create(
59 project_folder=Path(),
60 modules=[],
61 part="part",
62 top="",
63 run_index=1,
64 build_step_hooks={
65 dummy1.hook_step: (project_folder / "synth.tcl", [dummy1]),
66 dummy2.hook_step: (project_folder / "impl.tcl", [dummy2]),
67 },
68 )
70 assert (
71 'foreach run [get_runs "synth_*"] {\n set_property "STEPS.SYNTH_DESIGN.TCL.PRE" {'
72 + to_tcl_path(project_folder / "synth.tcl")
73 + "} ${run}\n"
74 ) in tcl
75 assert (
76 'foreach run [get_runs "impl_*"] {\n set_property "STEPS.ROUTE_DESIGN.TCL.PRE" {'
77 + to_tcl_path(project_folder / "impl.tcl")
78 + "} ${run}\n"
79 ) in tcl
82def test_ip_cache_location(tmp_path):
83 tcl = VivadoTcl(name="").create(
84 project_folder=Path(), modules=[], part="part", top="", run_index=1
85 )
86 assert "config_ip_cache" not in tcl
88 tcl = VivadoTcl(name="").create(
89 project_folder=Path(), modules=[], part="part", top="", run_index=1, ip_cache_path=tmp_path
90 )
91 assert f"\nconfig_ip_cache -use_cache_location {{{to_tcl_path(tmp_path)}}}\n" in tcl
94def test_multiple_threads_is_capped_by_vivado_limits():
95 num_threads = 128
96 tcl = VivadoTcl(name="").build(
97 project_file=Path(), output_path=Path(), num_threads=num_threads, run_index=1
98 )
99 assert 'set_param "general.maxThreads" 32' in tcl
100 assert 'set_param "synth.maxThreads" 8' in tcl
101 assert "launch_runs ${run} -jobs 128" in tcl
102 assert "launch_runs ${run} -jobs 128" in tcl
105def test_set_build_run_index():
106 tcl = VivadoTcl(name="").build(
107 project_file=Path(), output_path=Path(), num_threads=0, run_index=1
108 )
109 assert "impl_1" in tcl
110 assert "synth_1" in tcl
111 assert "impl_2" not in tcl
112 assert "synth_2" not in tcl
114 tcl = VivadoTcl(name="").build(
115 project_file=Path(), output_path=Path(), num_threads=0, run_index=2
116 )
117 assert "impl_2" in tcl
118 assert "synth_2" in tcl
119 assert "impl_1" not in tcl
120 assert "synth_1" not in tcl
123def test_runtime_generics():
124 tcl = VivadoTcl(name="").build(
125 project_file=Path(),
126 output_path=Path(),
127 num_threads=0,
128 run_index=0,
129 generics={"dummy": True},
130 )
131 expected = '\nset_property "generic" {dummy=1\'b1} [current_fileset]\n'
132 assert expected in tcl
135def test_build_with_synth_only():
136 tcl = VivadoTcl(name="").build(
137 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, synth_only=False
138 )
139 assert "synth_" in tcl
140 assert "impl_" in tcl
142 tcl = VivadoTcl(name="").build(
143 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, synth_only=True
144 )
145 assert "synth_" in tcl
146 assert "impl_" not in tcl
149def test_build_with_from_impl():
150 tcl = VivadoTcl(name="").build(
151 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, from_impl=False
152 )
153 assert "synth_" in tcl
154 assert "impl_" in tcl
156 tcl = VivadoTcl(name="").build(
157 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, from_impl=True
158 )
159 assert "synth_" not in tcl
160 assert "impl_" in tcl
163def test_module_getters_are_called_with_correct_arguments():
164 modules = [MagicMock(spec=BaseModule)]
165 VivadoTcl(name="").create(
166 project_folder=Path(),
167 modules=modules,
168 part="",
169 top="",
170 run_index=1,
171 other_arguments={"apa": 123, "hest": 456},
172 )
174 modules[0].get_synthesis_files.assert_called_once_with(apa=123, hest=456)
175 modules[0].get_scoped_constraints.assert_called_once_with(apa=123, hest=456)
176 modules[0].get_ip_core_files.assert_called_once_with(apa=123, hest=456)
179@pytest.fixture
180def vivado_tcl_test(tmp_path):
181 class VivadoTclTest:
182 def __init__(self):
183 self.modules_folder = tmp_path / "modules"
185 # A library with some synth files and some test files
186 self.a_vhd = to_tcl_path(create_file(self.modules_folder / "apa" / "a.vhd"))
187 self.b_vhd = to_tcl_path(create_file(self.modules_folder / "apa" / "b.vhd"))
188 self.tb_a_vhd = to_tcl_path(
189 create_file(self.modules_folder / "apa" / "test" / "tb_a.vhd")
190 )
191 self.a_xdc = to_tcl_path(
192 create_file(self.modules_folder / "apa" / "scoped_constraints" / "a.xdc")
193 )
195 self.c_v = to_tcl_path(create_file(self.modules_folder / "apa" / "c.v"))
196 self.b_tcl = to_tcl_path(
197 create_file(self.modules_folder / "apa" / "scoped_constraints" / "b.tcl")
198 )
200 self.c_tcl = to_tcl_path(
201 create_file(self.modules_folder / "apa" / "ip_cores" / "c.tcl")
202 )
204 # A library with only test files
205 self.d_vhd = to_tcl_path(create_file(self.modules_folder / "zebra" / "test" / "d.vhd"))
207 self.modules = get_modules(self.modules_folder)
209 self.tcl = VivadoTcl(name="name")
211 return VivadoTclTest()
214def test_source_file_list_is_correctly_formatted(vivado_tcl_test):
215 tcl = vivado_tcl_test.tcl.create(
216 project_folder=Path(), modules=vivado_tcl_test.modules, part="", top="", run_index=1
217 )
219 # Order of files is not really deterministic
220 expected_1 = (
221 '\nread_vhdl -library "apa" -vhdl2008 '
222 f"{{{{{vivado_tcl_test.b_vhd}}} {{{vivado_tcl_test.a_vhd}}}}}\n"
223 )
224 expected_2 = (
225 '\nread_vhdl -library "apa" -vhdl2008 '
226 f"{{{{{vivado_tcl_test.a_vhd}}} {{{vivado_tcl_test.b_vhd}}}}}\n"
227 )
228 assert expected_1 in tcl or expected_2 in tcl
230 expected = f"\nread_verilog {{{vivado_tcl_test.c_v}}}\n"
231 assert expected in tcl
234def test_only_synthesis_files_added_to_create_project_tcl(vivado_tcl_test):
235 tcl = vivado_tcl_test.tcl.create(
236 project_folder=Path(), modules=vivado_tcl_test.modules, part="", top="", run_index=1
237 )
238 assert vivado_tcl_test.a_vhd in tcl
239 assert vivado_tcl_test.c_v in tcl
240 assert vivado_tcl_test.tb_a_vhd not in tcl
241 assert "tb_a.vhd" not in tcl
244def test_constraints(vivado_tcl_test):
245 constraint_folder = vivado_tcl_test.modules[0].path.parent.resolve() / "z" / "tcl"
247 tcl = vivado_tcl_test.tcl.create(
248 project_folder=Path(),
249 modules=vivado_tcl_test.modules,
250 part="part",
251 top="",
252 run_index=1,
253 constraints=[
254 Constraint(constraint_folder / "x.xdc"),
255 Constraint(constraint_folder / "y.tcl", used_in_synthesis=False),
256 Constraint(
257 constraint_folder / "z.xdc", used_in_implementation=False, processing_order="early"
258 ),
259 ],
260 )
262 # Scoped constraints from the modules.
263 assert f'\nread_xdc -ref "a" {{{vivado_tcl_test.a_xdc}}}\n' in tcl
264 assert f'\nread_xdc -ref "b" -unmanaged {{{vivado_tcl_test.b_tcl}}}\n' in tcl
266 # Regular constraints
267 constraint_file = to_tcl_path(constraint_folder / "x.xdc")
268 assert f"\nread_xdc {{{constraint_file}}}\n" in tcl
269 assert f'"PROCESSING_ORDER" "NORMAL" [get_files {{{constraint_file}}}' in tcl
270 assert f"false [get_files {{{constraint_file}}}" not in tcl
272 constraint_file = to_tcl_path(constraint_folder / "y.tcl")
273 assert f"\nread_xdc -unmanaged {{{constraint_file}}}\n" in tcl
274 assert f'"PROCESSING_ORDER" "NORMAL" [get_files {{{constraint_file}}}' in tcl
275 assert f'"USED_IN_SYNTHESIS" false [get_files {{{constraint_file}}}' in tcl
276 assert f'"USED_IN_IMPLEMENTATION" false [get_files {{{constraint_file}}}' not in tcl
278 constraint_file = to_tcl_path(constraint_folder / "z.xdc")
279 assert f"\nread_xdc {{{constraint_file}}}\n" in tcl
280 assert f'"PROCESSING_ORDER" "EARLY" [get_files {{{constraint_file}}}' in tcl
281 assert f'"USED_IN_SYNTHESIS" false [get_files {{{constraint_file}}}' not in tcl
282 assert f'"USED_IN_IMPLEMENTATION" false [get_files {{{constraint_file}}}' in tcl
285def test_ip_core_files(vivado_tcl_test):
286 ip_core_file_path = vivado_tcl_test.modules_folder.parent / "my_name.tcl"
287 module = MagicMock(spec=BaseModule)
288 module.get_ip_core_files.return_value = [
289 IpCoreFile(path=ip_core_file_path, apa="hest", zebra=123)
290 ]
292 vivado_tcl_test.modules.append(module)
294 tcl = vivado_tcl_test.tcl.create(
295 project_folder=Path(), modules=vivado_tcl_test.modules, part="part", top="", run_index=1
296 )
298 assert (
299 f"""
300proc create_ip_core_c {{}} {{
301 source -notrace {{{vivado_tcl_test.c_tcl}}}
302}}
303create_ip_core_c
304"""
305 in tcl
306 )
308 assert (
309 f"""
310proc create_ip_core_my_name {{}} {{
311 set apa "hest"
312 set zebra "123"
313 source -notrace {{{to_tcl_path(ip_core_file_path)}}}
314}}
315create_ip_core_my_name
316"""
317 in tcl
318 )
321def test_create_with_ip_cores_only(vivado_tcl_test):
322 tcl = vivado_tcl_test.tcl.create(
323 project_folder=Path(),
324 modules=vivado_tcl_test.modules,
325 part="part",
326 top="",
327 run_index=1,
328 ip_cores_only=True,
329 )
330 assert vivado_tcl_test.c_tcl in tcl
331 assert vivado_tcl_test.a_vhd not in tcl
334def test_empty_library_not_in_create_project_tcl(vivado_tcl_test):
335 tcl = vivado_tcl_test.tcl.create(
336 project_folder=Path(), modules=vivado_tcl_test.modules, part="part", top="", run_index=1
337 )
338 assert "zebra" not in tcl
341def test_multiple_tcl_sources(vivado_tcl_test):
342 extra_tcl_sources = [Path("dummy.tcl"), Path("files.tcl")]
343 tcl = vivado_tcl_test.tcl.create(
344 project_folder=Path(),
345 modules=vivado_tcl_test.modules,
346 part="part",
347 top="",
348 run_index=1,
349 tcl_sources=extra_tcl_sources,
350 )
352 for filename in extra_tcl_sources:
353 assert f"\nsource -notrace {{{to_tcl_path(filename)}}}\n" in tcl
356def test_io_buffer_setting(vivado_tcl_test):
357 tcl = vivado_tcl_test.tcl.create(
358 project_folder=Path(),
359 modules=vivado_tcl_test.modules,
360 part="part",
361 top="",
362 run_index=1,
363 disable_io_buffers=True,
364 )
366 no_io_buffers_tcl = (
367 '\nset_property -name "STEPS.SYNTH_DESIGN.ARGS.MORE OPTIONS" '
368 '-value "-no_iobuf" -objects [get_runs "synth_1"]\n'
369 )
370 assert no_io_buffers_tcl in tcl
372 tcl = vivado_tcl_test.tcl.create(
373 project_folder=Path(),
374 modules=vivado_tcl_test.modules,
375 part="part",
376 top="",
377 run_index=1,
378 disable_io_buffers=False,
379 )
381 assert no_io_buffers_tcl not in tcl
384def test_analyze_synthesis_settings_on_and_off(vivado_tcl_test):
385 tcl = vivado_tcl_test.tcl.build(
386 project_file=Path(),
387 output_path=Path(),
388 num_threads=1,
389 run_index=1,
390 open_and_analyze_synthesized_design=True,
391 )
392 assert "open_run" in tcl
393 assert "report_clock_interaction" in tcl
395 tcl = vivado_tcl_test.tcl.build(
396 project_file=Path(),
397 output_path=Path(),
398 num_threads=1,
399 run_index=1,
400 open_and_analyze_synthesized_design=False,
401 )
402 # When disabled, the run should not even be opened, which saves time
403 assert "open_run" not in tcl
404 assert "report_clock_interaction" not in tcl
407def test_impl_explore(vivado_tcl_test):
408 num_runs = 4
410 tcl = vivado_tcl_test.tcl.build(
411 project_file=Path(),
412 output_path=Path(),
413 num_threads=num_runs,
414 run_index=1,
415 impl_explore=True,
416 )
418 assert (
419 f'launch_runs -jobs {num_runs} [get_runs "impl_explore_*"] -to_step "write_bitstream"'
420 in tcl
421 )
422 assert (
423 'wait_on_runs -quiet -exit_condition ANY_ONE_MET_TIMING [get_runs "impl_explore_*"]' in tcl
424 )
425 assert 'reset_runs [get_runs -filter {STATUS == "Queued..."}]' in tcl
426 assert (
427 'wait_on_runs -quiet [get_runs -filter {STATUS != "Not started"} "impl_explore_*"]' in tcl
428 )
429 assert 'foreach run [get_runs -filter {PROGRESS == "100%"} "impl_explore_*"]' in tcl