Coverage for tsfpga/vivado/test/test_tcl.py: 100%
157 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# --------------------------------------------------------------------------------------------------
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.ip_core_file import IpCoreFile
17from tsfpga.module import BaseModule, get_modules
18from tsfpga.system_utils import create_file
19from tsfpga.test.test_utils import file_contains_string
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():
53 dummy = BuildStepTclHook(Path("dummy.tcl"), "STEPS.SYNTH_DESIGN.TCL.PRE")
54 files = BuildStepTclHook(Path("files.tcl"), "STEPS.ROUTE_DESIGN.TCL.PRE")
55 tcl = VivadoTcl(name="").create(
56 project_folder=Path(),
57 modules=[],
58 part="part",
59 top="",
60 run_index=1,
61 build_step_hooks=[dummy, files],
62 )
64 assert (
65 f"\n "
66 f'set_property "STEPS.SYNTH_DESIGN.TCL.PRE" { {to_tcl_path(dummy.tcl_file)}} ${ run} \n'
67 ) in tcl
68 assert (
69 f"\n "
70 f'set_property "STEPS.ROUTE_DESIGN.TCL.PRE" { {to_tcl_path(files.tcl_file)}} ${ run} \n'
71 ) in tcl
74def test_build_step_hooks_with_same_hook_step(tmp_path):
75 dummy = BuildStepTclHook(Path("dummy.tcl"), "STEPS.SYNTH_DESIGN.TCL.PRE")
76 files = BuildStepTclHook(Path("files.tcl"), "STEPS.SYNTH_DESIGN.TCL.PRE")
77 tcl = VivadoTcl(name="").create(
78 project_folder=tmp_path / "dummy_project_folder",
79 modules=[],
80 part="part",
81 top="",
82 run_index=1,
83 build_step_hooks=[dummy, files],
84 )
86 hook_file = tmp_path / "dummy_project_folder" / "hook_STEPS_SYNTH_DESIGN_TCL_PRE.tcl"
88 assert file_contains_string(hook_file, f"source { {to_tcl_path(dummy.tcl_file)}} ")
89 assert file_contains_string(hook_file, f"source { {to_tcl_path(files.tcl_file)}} ")
91 assert (
92 f'\n set_property "STEPS.SYNTH_DESIGN.TCL.PRE" { {to_tcl_path(hook_file)}} ${ run} \n'
93 in tcl
94 )
97def test_ip_cache_location(tmp_path):
98 tcl = VivadoTcl(name="").create(
99 project_folder=Path(), modules=[], part="part", top="", run_index=1
100 )
101 assert "config_ip_cache" not in tcl
103 tcl = VivadoTcl(name="").create(
104 project_folder=Path(), modules=[], part="part", top="", run_index=1, ip_cache_path=tmp_path
105 )
106 assert f"\nconfig_ip_cache -use_cache_location { {to_tcl_path(tmp_path)}} \n" in tcl
109def test_multiple_threads_is_capped_by_vivado_limits():
110 num_threads = 128
111 tcl = VivadoTcl(name="").build(
112 project_file=Path(), output_path=Path(), num_threads=num_threads, run_index=1
113 )
114 assert 'set_param "general.maxThreads" 32' in tcl
115 assert 'set_param "synth.maxThreads" 8' in tcl
116 assert "launch_runs ${run} -jobs 128" in tcl
117 assert "launch_runs ${run} -jobs 128" in tcl
120def test_set_build_run_index():
121 tcl = VivadoTcl(name="").build(
122 project_file=Path(), output_path=Path(), num_threads=0, run_index=1
123 )
124 assert "impl_1" in tcl
125 assert "synth_1" in tcl
126 assert "impl_2" not in tcl
127 assert "synth_2" not in tcl
129 tcl = VivadoTcl(name="").build(
130 project_file=Path(), output_path=Path(), num_threads=0, run_index=2
131 )
132 assert "impl_2" in tcl
133 assert "synth_2" in tcl
134 assert "impl_1" not in tcl
135 assert "synth_1" not in tcl
138def test_runtime_generics():
139 tcl = VivadoTcl(name="").build(
140 project_file=Path(),
141 output_path=Path(),
142 num_threads=0,
143 run_index=0,
144 generics={"dummy": True},
145 )
146 expected = '\nset_property "generic" {dummy=1\'b1} [current_fileset]\n'
147 assert expected in tcl
150def test_build_with_synth_only():
151 tcl = VivadoTcl(name="").build(
152 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, synth_only=False
153 )
154 assert "synth_" in tcl
155 assert "impl_" in tcl
157 tcl = VivadoTcl(name="").build(
158 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, synth_only=True
159 )
160 assert "synth_" in tcl
161 assert "impl_" not in tcl
164def test_build_with_from_impl():
165 tcl = VivadoTcl(name="").build(
166 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, from_impl=False
167 )
168 assert "synth_" in tcl
169 assert "impl_" in tcl
171 tcl = VivadoTcl(name="").build(
172 project_file=Path(), output_path=Path(), num_threads=0, run_index=0, from_impl=True
173 )
174 assert "synth_" not in tcl
175 assert "impl_" in tcl
178def test_module_getters_are_called_with_correct_arguments():
179 modules = [MagicMock(spec=BaseModule)]
180 VivadoTcl(name="").create(
181 project_folder=Path(),
182 modules=modules,
183 part="",
184 top="",
185 run_index=1,
186 other_arguments={"apa": 123, "hest": 456},
187 )
189 modules[0].get_synthesis_files.assert_called_once_with(apa=123, hest=456)
190 modules[0].get_scoped_constraints.assert_called_once_with(apa=123, hest=456)
191 modules[0].get_ip_core_files.assert_called_once_with(apa=123, hest=456)
194@pytest.fixture
195def vivado_tcl_test(tmp_path):
196 class VivadoTclTest:
197 def __init__(self):
198 self.modules_folder = tmp_path / "modules"
200 # A library with some synth files and some test files
201 self.a_vhd = to_tcl_path(create_file(self.modules_folder / "apa" / "a.vhd"))
202 self.b_vhd = to_tcl_path(create_file(self.modules_folder / "apa" / "b.vhd"))
203 self.tb_a_vhd = to_tcl_path(
204 create_file(self.modules_folder / "apa" / "test" / "tb_a.vhd")
205 )
206 self.a_xdc = to_tcl_path(
207 create_file(self.modules_folder / "apa" / "scoped_constraints" / "a.xdc")
208 )
210 self.c_v = to_tcl_path(create_file(self.modules_folder / "apa" / "c.v"))
211 self.b_tcl = to_tcl_path(
212 create_file(self.modules_folder / "apa" / "scoped_constraints" / "b.tcl")
213 )
215 self.c_tcl = to_tcl_path(
216 create_file(self.modules_folder / "apa" / "ip_cores" / "c.tcl")
217 )
219 # A library with only test files
220 self.d_vhd = to_tcl_path(create_file(self.modules_folder / "zebra" / "test" / "d.vhd"))
222 self.modules = get_modules(self.modules_folder)
224 self.tcl = VivadoTcl(name="name")
226 return VivadoTclTest()
229def test_source_file_list_is_correctly_formatted(vivado_tcl_test):
230 tcl = vivado_tcl_test.tcl.create(
231 project_folder=Path(), modules=vivado_tcl_test.modules, part="", top="", run_index=1
232 )
234 # Order of files is not really deterministic
235 expected_1 = (
236 '\nread_vhdl -library "apa" -vhdl2008 '
237 f"{ { {vivado_tcl_test.b_vhd}} { {vivado_tcl_test.a_vhd}} } \n"
238 )
239 expected_2 = (
240 '\nread_vhdl -library "apa" -vhdl2008 '
241 f"{ { {vivado_tcl_test.a_vhd}} { {vivado_tcl_test.b_vhd}} } \n"
242 )
243 assert expected_1 in tcl or expected_2 in tcl
245 expected = f"\nread_verilog { {vivado_tcl_test.c_v}} \n"
246 assert expected in tcl
249def test_only_synthesis_files_added_to_create_project_tcl(vivado_tcl_test):
250 tcl = vivado_tcl_test.tcl.create(
251 project_folder=Path(), modules=vivado_tcl_test.modules, part="", top="", run_index=1
252 )
253 assert vivado_tcl_test.a_vhd in tcl
254 assert vivado_tcl_test.c_v in tcl
255 assert vivado_tcl_test.tb_a_vhd not in tcl
256 assert "tb_a.vhd" not in tcl
259def test_constraints(vivado_tcl_test):
260 tcl = vivado_tcl_test.tcl.create(
261 project_folder=Path(), modules=vivado_tcl_test.modules, part="part", top="", run_index=1
262 )
264 expected = f'\nread_xdc -ref "a" { {vivado_tcl_test.a_xdc}} \n'
265 assert expected in tcl
266 expected = f'\nread_xdc -ref "b" -unmanaged { {vivado_tcl_test.b_tcl}} \n'
267 assert expected in tcl
270def test_ip_core_files(vivado_tcl_test):
271 ip_core_file_path = vivado_tcl_test.modules_folder.parent / "my_name.tcl"
272 module = MagicMock(spec=BaseModule)
273 module.get_ip_core_files.return_value = [
274 IpCoreFile(path=ip_core_file_path, apa="hest", zebra=123)
275 ]
277 vivado_tcl_test.modules.append(module)
279 tcl = vivado_tcl_test.tcl.create(
280 project_folder=Path(), modules=vivado_tcl_test.modules, part="part", top="", run_index=1
281 )
283 assert (
284 f"""
285proc create_ip_core_c { } {
286 source -notrace { {vivado_tcl_test.c_tcl}}
287}
288create_ip_core_c
289"""
290 in tcl
291 )
293 assert (
294 f"""
295proc create_ip_core_my_name { } {
296 set apa "hest"
297 set zebra "123"
298 source -notrace { {to_tcl_path(ip_core_file_path)}}
299}
300create_ip_core_my_name
301"""
302 in tcl
303 )
306def test_create_with_ip_cores_only(vivado_tcl_test):
307 tcl = vivado_tcl_test.tcl.create(
308 project_folder=Path(),
309 modules=vivado_tcl_test.modules,
310 part="part",
311 top="",
312 run_index=1,
313 ip_cores_only=True,
314 )
315 assert vivado_tcl_test.c_tcl in tcl
316 assert vivado_tcl_test.a_vhd not in tcl
319def test_empty_library_not_in_create_project_tcl(vivado_tcl_test):
320 tcl = vivado_tcl_test.tcl.create(
321 project_folder=Path(), modules=vivado_tcl_test.modules, part="part", top="", run_index=1
322 )
323 assert "zebra" not in tcl
326def test_multiple_tcl_sources(vivado_tcl_test):
327 extra_tcl_sources = [Path("dummy.tcl"), Path("files.tcl")]
328 tcl = vivado_tcl_test.tcl.create(
329 project_folder=Path(),
330 modules=vivado_tcl_test.modules,
331 part="part",
332 top="",
333 run_index=1,
334 tcl_sources=extra_tcl_sources,
335 )
337 for filename in extra_tcl_sources:
338 assert f"\nsource -notrace { {to_tcl_path(filename)}} \n" in tcl
341def test_io_buffer_setting(vivado_tcl_test):
342 tcl = vivado_tcl_test.tcl.create(
343 project_folder=Path(),
344 modules=vivado_tcl_test.modules,
345 part="part",
346 top="",
347 run_index=1,
348 disable_io_buffers=True,
349 )
351 no_io_buffers_tcl = (
352 '\nset_property -name "STEPS.SYNTH_DESIGN.ARGS.MORE OPTIONS" '
353 '-value "-no_iobuf" -objects [get_runs "synth_1"]\n'
354 )
355 assert no_io_buffers_tcl in tcl
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=False,
364 )
366 assert no_io_buffers_tcl not in tcl
369def test_analyze_synthesis_settings_on_and_off(vivado_tcl_test):
370 tcl = vivado_tcl_test.tcl.build(
371 project_file=Path(),
372 output_path=Path(),
373 num_threads=1,
374 run_index=1,
375 analyze_synthesis_timing=True,
376 )
377 assert "open_run" in tcl
378 assert "report_clock_interaction" in tcl
380 tcl = vivado_tcl_test.tcl.build(
381 project_file=Path(),
382 output_path=Path(),
383 num_threads=1,
384 run_index=1,
385 analyze_synthesis_timing=False,
386 )
387 # When disabled, the run should not even be opened, which saves time
388 assert "open_run" not in tcl
389 assert "report_clock_interaction" not in tcl
392def test_impl_explore(vivado_tcl_test):
393 num_runs = 4
395 tcl = vivado_tcl_test.tcl.build(
396 project_file=Path(),
397 output_path=Path(),
398 num_threads=num_runs,
399 run_index=1,
400 impl_explore=True,
401 )
403 assert (
404 f'launch_runs -jobs {num_runs} [get_runs "impl_explore_*"] -to_step "write_bitstream"'
405 in tcl
406 )
407 assert (
408 'wait_on_runs -quiet -exit_condition ANY_ONE_MET_TIMING [get_runs "impl_explore_*"]' in tcl
409 )
410 assert 'reset_runs [get_runs -filter {STATUS == "Queued..."}]' in tcl
411 assert (
412 'wait_on_runs -quiet [get_runs -filter {STATUS != "Not started"} "impl_explore_*"]' in tcl
413 )
414 assert 'foreach run [get_runs -filter {PROGRESS == "100%"} "impl_explore_*"]' in tcl