Coverage for tsfpga/vivado/test/test_tcl.py: 100%
171 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-01 20:51 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-01 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.test.test_utils import file_contains_string
21from tsfpga.vivado.common import to_tcl_path
22from tsfpga.vivado.generics import BitVectorGenericValue, StringGenericValue
23from tsfpga.vivado.tcl import VivadoTcl
26def test_set_create_run_index():
27 tcl = VivadoTcl(name="").create(project_folder=Path(), modules=[], part="", top="", run_index=2)
28 assert '\ncurrent_run [get_runs "synth_2"]\n' in tcl
31def test_static_generics():
32 # Use OrderedDict here in test so that order will be preserved and we can test for equality.
33 # In real world case a normal dict can be used.
34 generics = OrderedDict(
35 enable=True,
36 disable=False,
37 integer=123,
38 slv=BitVectorGenericValue("0101"),
39 string=StringGenericValue("apa"),
40 )
42 tcl = VivadoTcl(name="").create(
43 project_folder=Path(), modules=[], part="", top="", run_index=1, generics=generics
44 )
45 expected = (
46 '\nset_property "generic" '
47 "{enable=1'b1 disable=1'b0 integer=123 slv=4'b0101 string=\"apa\"} "
48 "[current_fileset]\n"
49 )
50 assert expected in tcl
53def test_build_step_hooks():
54 dummy = BuildStepTclHook(Path("dummy.tcl"), "STEPS.SYNTH_DESIGN.TCL.PRE")
55 files = BuildStepTclHook(Path("files.tcl"), "STEPS.ROUTE_DESIGN.TCL.PRE")
56 tcl = VivadoTcl(name="").create(
57 project_folder=Path(),
58 modules=[],
59 part="part",
60 top="",
61 run_index=1,
62 build_step_hooks=[dummy, files],
63 )
65 assert (
66 f"\n "
67 f'set_property "STEPS.SYNTH_DESIGN.TCL.PRE" { {to_tcl_path(dummy.tcl_file)}} ${ run} \n'
68 ) in tcl
69 assert (
70 f"\n "
71 f'set_property "STEPS.ROUTE_DESIGN.TCL.PRE" { {to_tcl_path(files.tcl_file)}} ${ run} \n'
72 ) in tcl
75def test_build_step_hooks_with_same_hook_step(tmp_path):
76 dummy = BuildStepTclHook(Path("dummy.tcl"), "STEPS.SYNTH_DESIGN.TCL.PRE")
77 files = BuildStepTclHook(Path("files.tcl"), "STEPS.SYNTH_DESIGN.TCL.PRE")
78 tcl = VivadoTcl(name="").create(
79 project_folder=tmp_path / "dummy_project_folder",
80 modules=[],
81 part="part",
82 top="",
83 run_index=1,
84 build_step_hooks=[dummy, files],
85 )
87 hook_file = tmp_path / "dummy_project_folder" / "hook_STEPS_SYNTH_DESIGN_TCL_PRE.tcl"
89 assert file_contains_string(hook_file, f"source { {to_tcl_path(dummy.tcl_file)}} ")
90 assert file_contains_string(hook_file, f"source { {to_tcl_path(files.tcl_file)}} ")
92 assert (
93 f'\n set_property "STEPS.SYNTH_DESIGN.TCL.PRE" { {to_tcl_path(hook_file)}} ${ run} \n'
94 in tcl
95 )
98def test_ip_cache_location(tmp_path):
99 tcl = VivadoTcl(name="").create(
100 project_folder=Path(), modules=[], part="part", top="", run_index=1
101 )
102 assert "config_ip_cache" not in tcl
104 tcl = VivadoTcl(name="").create(
105 project_folder=Path(), modules=[], part="part", top="", run_index=1, ip_cache_path=tmp_path
106 )
107 assert f"\nconfig_ip_cache -use_cache_location { {to_tcl_path(tmp_path)}} \n" in tcl
110def test_multiple_threads_is_capped_by_vivado_limits():
111 num_threads = 128
112 tcl = VivadoTcl(name="").build(
113 project_file=Path(), output_path=Path(), num_threads=num_threads, run_index=1
114 )
115 assert 'set_param "general.maxThreads" 32' in tcl
116 assert 'set_param "synth.maxThreads" 8' in tcl
117 assert "launch_runs ${run} -jobs 128" in tcl
118 assert "launch_runs ${run} -jobs 128" in tcl
121def test_set_build_run_index():
122 tcl = VivadoTcl(name="").build(
123 project_file=Path(), output_path=Path(), num_threads=0, run_index=1
124 )
125 assert "impl_1" in tcl
126 assert "synth_1" in tcl
127 assert "impl_2" not in tcl
128 assert "synth_2" not in tcl
130 tcl = VivadoTcl(name="").build(
131 project_file=Path(), output_path=Path(), num_threads=0, run_index=2
132 )
133 assert "impl_2" in tcl
134 assert "synth_2" in tcl
135 assert "impl_1" not in tcl
136 assert "synth_1" not in tcl
139def test_runtime_generics():
140 tcl = VivadoTcl(name="").build(
141 project_file=Path(),
142 output_path=Path(),
143 num_threads=0,
144 run_index=0,
145 generics={"dummy": True},
146 )
147 expected = '\nset_property "generic" {dummy=1\'b1} [current_fileset]\n'
148 assert expected in tcl
151def test_build_with_synth_only():
152 tcl = VivadoTcl(name="").build(
153 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, synth_only=False
154 )
155 assert "synth_" in tcl
156 assert "impl_" in tcl
158 tcl = VivadoTcl(name="").build(
159 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, synth_only=True
160 )
161 assert "synth_" in tcl
162 assert "impl_" not in tcl
165def test_build_with_from_impl():
166 tcl = VivadoTcl(name="").build(
167 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, from_impl=False
168 )
169 assert "synth_" in tcl
170 assert "impl_" in tcl
172 tcl = VivadoTcl(name="").build(
173 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, from_impl=True
174 )
175 assert "synth_" not in tcl
176 assert "impl_" in tcl
179def test_module_getters_are_called_with_correct_arguments():
180 modules = [MagicMock(spec=BaseModule)]
181 VivadoTcl(name="").create(
182 project_folder=Path(),
183 modules=modules,
184 part="",
185 top="",
186 run_index=1,
187 other_arguments={"apa": 123, "hest": 456},
188 )
190 modules[0].get_synthesis_files.assert_called_once_with(apa=123, hest=456)
191 modules[0].get_scoped_constraints.assert_called_once_with(apa=123, hest=456)
192 modules[0].get_ip_core_files.assert_called_once_with(apa=123, hest=456)
195@pytest.fixture
196def vivado_tcl_test(tmp_path):
197 class VivadoTclTest:
198 def __init__(self):
199 self.modules_folder = tmp_path / "modules"
201 # A library with some synth files and some test files
202 self.a_vhd = to_tcl_path(create_file(self.modules_folder / "apa" / "a.vhd"))
203 self.b_vhd = to_tcl_path(create_file(self.modules_folder / "apa" / "b.vhd"))
204 self.tb_a_vhd = to_tcl_path(
205 create_file(self.modules_folder / "apa" / "test" / "tb_a.vhd")
206 )
207 self.a_xdc = to_tcl_path(
208 create_file(self.modules_folder / "apa" / "scoped_constraints" / "a.xdc")
209 )
211 self.c_v = to_tcl_path(create_file(self.modules_folder / "apa" / "c.v"))
212 self.b_tcl = to_tcl_path(
213 create_file(self.modules_folder / "apa" / "scoped_constraints" / "b.tcl")
214 )
216 self.c_tcl = to_tcl_path(
217 create_file(self.modules_folder / "apa" / "ip_cores" / "c.tcl")
218 )
220 # A library with only test files
221 self.d_vhd = to_tcl_path(create_file(self.modules_folder / "zebra" / "test" / "d.vhd"))
223 self.modules = get_modules(self.modules_folder)
225 self.tcl = VivadoTcl(name="name")
227 return VivadoTclTest()
230def test_source_file_list_is_correctly_formatted(vivado_tcl_test):
231 tcl = vivado_tcl_test.tcl.create(
232 project_folder=Path(), modules=vivado_tcl_test.modules, part="", top="", run_index=1
233 )
235 # Order of files is not really deterministic
236 expected_1 = (
237 '\nread_vhdl -library "apa" -vhdl2008 '
238 f"{ { {vivado_tcl_test.b_vhd}} { {vivado_tcl_test.a_vhd}} } \n"
239 )
240 expected_2 = (
241 '\nread_vhdl -library "apa" -vhdl2008 '
242 f"{ { {vivado_tcl_test.a_vhd}} { {vivado_tcl_test.b_vhd}} } \n"
243 )
244 assert expected_1 in tcl or expected_2 in tcl
246 expected = f"\nread_verilog { {vivado_tcl_test.c_v}} \n"
247 assert expected in tcl
250def test_only_synthesis_files_added_to_create_project_tcl(vivado_tcl_test):
251 tcl = vivado_tcl_test.tcl.create(
252 project_folder=Path(), modules=vivado_tcl_test.modules, part="", top="", run_index=1
253 )
254 assert vivado_tcl_test.a_vhd in tcl
255 assert vivado_tcl_test.c_v in tcl
256 assert vivado_tcl_test.tb_a_vhd not in tcl
257 assert "tb_a.vhd" not in tcl
260def test_constraints(vivado_tcl_test):
261 constraint_folder = vivado_tcl_test.modules[0].path.parent.resolve() / "z" / "tcl"
263 tcl = vivado_tcl_test.tcl.create(
264 project_folder=Path(),
265 modules=vivado_tcl_test.modules,
266 part="part",
267 top="",
268 run_index=1,
269 constraints=[
270 Constraint(constraint_folder / "x.xdc"),
271 Constraint(constraint_folder / "y.tcl", used_in_synthesis=False),
272 Constraint(
273 constraint_folder / "z.xdc", used_in_implementation=False, processing_order="early"
274 ),
275 ],
276 )
278 # Scoped constraints from the modules.
279 assert f'\nread_xdc -ref "a" { {vivado_tcl_test.a_xdc}} \n' in tcl
280 assert f'\nread_xdc -ref "b" -unmanaged { {vivado_tcl_test.b_tcl}} \n' in tcl
282 # Regular constraints
283 constraint_file = to_tcl_path(constraint_folder / "x.xdc")
284 assert f"\nread_xdc { {constraint_file}} \n" in tcl
285 assert f'"PROCESSING_ORDER" "NORMAL" [get_files { {constraint_file}} ' in tcl
286 assert f"false [get_files { {constraint_file}} " not in tcl
288 constraint_file = to_tcl_path(constraint_folder / "y.tcl")
289 assert f"\nread_xdc -unmanaged { {constraint_file}} \n" in tcl
290 assert f'"PROCESSING_ORDER" "NORMAL" [get_files { {constraint_file}} ' in tcl
291 assert f'"USED_IN_SYNTHESIS" false [get_files { {constraint_file}} ' in tcl
292 assert f'"USED_IN_IMPLEMENTATION" false [get_files { {constraint_file}} ' not in tcl
294 constraint_file = to_tcl_path(constraint_folder / "z.xdc")
295 assert f"\nread_xdc { {constraint_file}} \n" in tcl
296 assert f'"PROCESSING_ORDER" "EARLY" [get_files { {constraint_file}} ' in tcl
297 assert f'"USED_IN_SYNTHESIS" false [get_files { {constraint_file}} ' not in tcl
298 assert f'"USED_IN_IMPLEMENTATION" false [get_files { {constraint_file}} ' in tcl
301def test_ip_core_files(vivado_tcl_test):
302 ip_core_file_path = vivado_tcl_test.modules_folder.parent / "my_name.tcl"
303 module = MagicMock(spec=BaseModule)
304 module.get_ip_core_files.return_value = [
305 IpCoreFile(path=ip_core_file_path, apa="hest", zebra=123)
306 ]
308 vivado_tcl_test.modules.append(module)
310 tcl = vivado_tcl_test.tcl.create(
311 project_folder=Path(), modules=vivado_tcl_test.modules, part="part", top="", run_index=1
312 )
314 assert (
315 f"""
316proc create_ip_core_c { } {
317 source -notrace { {vivado_tcl_test.c_tcl}}
318}
319create_ip_core_c
320"""
321 in tcl
322 )
324 assert (
325 f"""
326proc create_ip_core_my_name { } {
327 set apa "hest"
328 set zebra "123"
329 source -notrace { {to_tcl_path(ip_core_file_path)}}
330}
331create_ip_core_my_name
332"""
333 in tcl
334 )
337def test_create_with_ip_cores_only(vivado_tcl_test):
338 tcl = vivado_tcl_test.tcl.create(
339 project_folder=Path(),
340 modules=vivado_tcl_test.modules,
341 part="part",
342 top="",
343 run_index=1,
344 ip_cores_only=True,
345 )
346 assert vivado_tcl_test.c_tcl in tcl
347 assert vivado_tcl_test.a_vhd not in tcl
350def test_empty_library_not_in_create_project_tcl(vivado_tcl_test):
351 tcl = vivado_tcl_test.tcl.create(
352 project_folder=Path(), modules=vivado_tcl_test.modules, part="part", top="", run_index=1
353 )
354 assert "zebra" not in tcl
357def test_multiple_tcl_sources(vivado_tcl_test):
358 extra_tcl_sources = [Path("dummy.tcl"), Path("files.tcl")]
359 tcl = vivado_tcl_test.tcl.create(
360 project_folder=Path(),
361 modules=vivado_tcl_test.modules,
362 part="part",
363 top="",
364 run_index=1,
365 tcl_sources=extra_tcl_sources,
366 )
368 for filename in extra_tcl_sources:
369 assert f"\nsource -notrace { {to_tcl_path(filename)}} \n" in tcl
372def test_io_buffer_setting(vivado_tcl_test):
373 tcl = vivado_tcl_test.tcl.create(
374 project_folder=Path(),
375 modules=vivado_tcl_test.modules,
376 part="part",
377 top="",
378 run_index=1,
379 disable_io_buffers=True,
380 )
382 no_io_buffers_tcl = (
383 '\nset_property -name "STEPS.SYNTH_DESIGN.ARGS.MORE OPTIONS" '
384 '-value "-no_iobuf" -objects [get_runs "synth_1"]\n'
385 )
386 assert no_io_buffers_tcl in tcl
388 tcl = vivado_tcl_test.tcl.create(
389 project_folder=Path(),
390 modules=vivado_tcl_test.modules,
391 part="part",
392 top="",
393 run_index=1,
394 disable_io_buffers=False,
395 )
397 assert no_io_buffers_tcl not in tcl
400def test_analyze_synthesis_settings_on_and_off(vivado_tcl_test):
401 tcl = vivado_tcl_test.tcl.build(
402 project_file=Path(),
403 output_path=Path(),
404 num_threads=1,
405 run_index=1,
406 analyze_synthesis_timing=True,
407 )
408 assert "open_run" in tcl
409 assert "report_clock_interaction" in tcl
411 tcl = vivado_tcl_test.tcl.build(
412 project_file=Path(),
413 output_path=Path(),
414 num_threads=1,
415 run_index=1,
416 analyze_synthesis_timing=False,
417 )
418 # When disabled, the run should not even be opened, which saves time
419 assert "open_run" not in tcl
420 assert "report_clock_interaction" not in tcl
423def test_impl_explore(vivado_tcl_test):
424 num_runs = 4
426 tcl = vivado_tcl_test.tcl.build(
427 project_file=Path(),
428 output_path=Path(),
429 num_threads=num_runs,
430 run_index=1,
431 impl_explore=True,
432 )
434 assert (
435 f'launch_runs -jobs {num_runs} [get_runs "impl_explore_*"] -to_step "write_bitstream"'
436 in tcl
437 )
438 assert (
439 'wait_on_runs -quiet -exit_condition ANY_ONE_MET_TIMING [get_runs "impl_explore_*"]' in tcl
440 )
441 assert 'reset_runs [get_runs -filter {STATUS == "Queued..."}]' in tcl
442 assert (
443 'wait_on_runs -quiet [get_runs -filter {STATUS != "Not started"} "impl_explore_*"]' in tcl
444 )
445 assert 'foreach run [get_runs -filter {PROGRESS == "100%"} "impl_explore_*"]' in tcl