Coverage for tests / unit_tests / scons / test_plugin_util.py: 100%
123 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 02:31 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 02:31 +0000
1"""
2Tests of the scons plugin_util.py functions.
3"""
5import re
6import os
7from os.path import isfile, exists, join
8import pytest
9from SCons.Node.FS import FS
10from SCons.Action import FunctionAction
11from pytest import LogCaptureFixture
12from tests.unit_tests.scons.testing import make_test_apio_env
13from tests.conftest import ApioRunner
14from apio.common.apio_console import cunstyle
15from apio.common import apio_console
16from apio.common.proto.apio_pb2 import (
17 TargetParams,
18 UploadParams,
19 LintParams,
20 ApioEnvParams,
21)
22from apio.scons.plugin_util import (
23 get_constraint_file,
24 verilog_src_scanner,
25 get_programmer_cmd,
26 map_params,
27 make_verilator_config_builder,
28 verilator_lint_action,
29)
32def test_get_constraint_file(
33 apio_runner: ApioRunner, capsys: LogCaptureFixture
34):
35 """Test the get_constraint_file() method."""
37 with apio_runner.in_sandbox() as sb:
39 apio_env = make_test_apio_env()
41 # -- If not .pcf files, should print an error and exit.
42 capsys.readouterr() # Reset capture
43 with pytest.raises(SystemExit) as e:
44 result = get_constraint_file(apio_env, ".pcf")
45 captured = capsys.readouterr()
46 assert e.value.code == 1
47 assert "No constraint file '*.pcf' found" in cunstyle(captured.out)
49 # -- If a single .pcf file, return it. Constraint file can also be
50 # -- in subdirectories as we test here
51 file1 = os.path.join("lib", "pinout.pcf")
52 sb.write_file(file1, "content1")
53 result = get_constraint_file(apio_env, ".pcf")
54 captured = capsys.readouterr()
55 assert captured.out == ""
56 assert result == file1
58 # -- If there is more than one, exit with an error message.
59 file2 = "other.pcf"
60 sb.write_file(file2, "content2")
61 capsys.readouterr() # Reset capture
62 with pytest.raises(SystemExit) as e:
63 result = get_constraint_file(apio_env, ".pcf")
64 captured = capsys.readouterr()
65 assert e.value.code == 1
66 assert "Error: Found 2 constraint files '*.pcf'" in cunstyle(
67 captured.out
68 )
70 # -- If the user specified a valid file then return it, regardless
71 # -- if it exists or not.
72 apio_env.params.apio_env_params.constraint_file = "xyz.pcf"
73 capsys.readouterr() # Reset capture
74 result = get_constraint_file(apio_env, ".pcf")
75 captured = capsys.readouterr()
76 assert captured.out == ""
77 assert result == "xyz.pcf"
79 # -- File extension should match the architecture.
80 apio_env.params.apio_env_params.constraint_file = "xyz.bad"
81 capsys.readouterr() # Reset capture
82 with pytest.raises(SystemExit) as e:
83 result = get_constraint_file(apio_env, ".pcf")
84 captured = capsys.readouterr()
85 assert e.value.code == 1
86 assert (
87 "Constraint file should have the extension '.pcf': xyz.bad"
88 in cunstyle(captured.out)
89 )
91 # -- Path under _build is not allowed.
92 apio_env.params.apio_env_params.constraint_file = "_build/xyz.pcf"
93 capsys.readouterr() # Reset capture
94 with pytest.raises(SystemExit) as e:
95 result = get_constraint_file(apio_env, ".pcf")
96 captured = capsys.readouterr()
97 assert e.value.code == 1
98 assert (
99 "Error: Constraint file should not be under _build: _build/xyz.pcf"
100 in cunstyle(captured.out)
101 )
103 # -- Path should not contain '../
104 apio_env.params.apio_env_params.constraint_file = "a/../xyz.pcf"
105 capsys.readouterr() # Reset capture
106 with pytest.raises(SystemExit) as e:
107 result = get_constraint_file(apio_env, ".pcf")
108 captured = capsys.readouterr()
109 assert e.value.code == 1
110 assert (
111 "Error: Constraint file path should not contain '..': a/../xyz.pcf"
112 in cunstyle(captured.out)
113 )
116def test_verilog_src_scanner(apio_runner: ApioRunner):
117 """Test the verilog scanner which scans a verilog file and extract
118 reference of files it uses.
119 """
121 # -- Test file content with references. Contains duplicates and
122 # -- references out of alphabetical order.
123 file_content = """
124 // Dummy file for testing.
126 // Icestudio reference.
127 parameter v771499 = "v771499.list"
129 // System verilog include reference.
130 `include "apio_testing.vh"
132 // Duplicate icestudio reference.
133 parameter v771499 = "v771499.list"
135 // Verilog include reference.
136 `include "apio_testing.v
138 // $readmemh() function reference.
139 $readmemh("subdir2/my_data.hex", State_buff);
140 """
142 with apio_runner.in_sandbox() as sb:
144 # -- Write a test file name in the current directory.
145 sb.write_file("subdir1/test_file.v", file_content)
147 # -- Create a scanner
148 apio_env = make_test_apio_env()
149 scanner = verilog_src_scanner(apio_env)
151 # -- Run the scanner. It returns a list of File.
152 file = FS.File(FS(), "subdir1/test_file.v")
153 dependency_files = scanner.function(file, apio_env, None)
155 # -- Files list should be empty since none of the dependency candidate
156 # has a file.
157 file_names = [f.name for f in dependency_files]
158 assert file_names == []
160 # -- Create file lists
161 core_dependencies = [
162 "apio.ini",
163 "boards.jsonc",
164 "programmers.jsonc",
165 "fpgas.jsonc",
166 ]
168 file_dependencies = [
169 "apio_testing.vh",
170 join("subdir2", "my_data.hex"),
171 join("subdir1", "v771499.list"),
172 ]
174 # -- Create dummy files. This should cause the dependencies to be
175 # -- reported. (Candidate dependencies with no matching file are
176 # -- filtered out)
177 for f in core_dependencies + file_dependencies + ["non-related.txt"]:
178 sb.write_file(f, "dummy-file")
180 # -- Run the scanner again
181 dependency_files = scanner.function(file, apio_env, None)
183 # -- Check the dependencies
184 file_names = [f.path for f in dependency_files]
185 assert file_names == sorted(core_dependencies + file_dependencies)
188def test_get_programmer_cmd():
189 """Tests the function programmer_cmd()."""
191 apio_console.configure()
193 # -- Test a valid programmer command.
194 apio_env = make_test_apio_env(
195 targets=["upload"],
196 target_params=TargetParams(
197 upload=UploadParams(programmer_cmd="my_prog aa $SOURCE bb")
198 ),
199 )
200 assert get_programmer_cmd(apio_env) == "my_prog aa $SOURCE bb"
203def test_map_params():
204 """Test the map_params() function."""
206 # -- Empty cases
207 assert map_params([], "x_{}_y") == ""
208 assert map_params(["", " "], "x_{}_y") == ""
210 # -- Non empty cases
211 assert map_params(["a"], "x_{}_y") == "x_a_y"
212 assert map_params([" a "], "x_{}_y") == "x_a_y"
213 assert map_params(["a", "a", "b"], "x_{}_y") == "x_a_y x_a_y x_b_y"
216def test_make_verilator_config_builder(apio_runner: ApioRunner):
217 """Tests the make_verilator_config_builder() function."""
219 with apio_runner.in_sandbox() as sb:
221 # -- Create a test scons env.
222 apio_env = make_test_apio_env()
224 # -- Call the tested method to create a builder.
225 builder = make_verilator_config_builder(
226 sb.packages_dir,
227 rules_to_supress=[
228 "SPECIFYIGN",
229 ],
230 )
232 # -- Verify builder suffixes.
233 assert builder.suffix == ".vlt"
234 assert builder.src_suffix == []
236 # -- Create a target that doesn't exist yet.
237 assert not exists("hardware.vlt")
238 target = FS.File(FS(), "hardware.vlt")
240 # -- Invoke the builder's action to create the target.
241 builder.action(target, [], apio_env.scons_env)
242 assert isfile("hardware.vlt")
244 # -- Verify that the file was created with the given text.
245 text = sb.read_file("hardware.vlt")
246 assert "verilator_config" in text, text
247 assert "lint_off -rule SPECIFYIGN" in text, text
250def test_verilator_lint_action_min(apio_runner: ApioRunner):
251 """Tests the verilator_lint_action() function with minimal params."""
253 with apio_runner.in_sandbox():
255 # -- Create apio scons env.
256 apio_env = make_test_apio_env(
257 targets=["lint"], target_params=TargetParams(lint=LintParams())
258 )
260 # -- Call the tested function with minimal args.
261 action = verilator_lint_action(
262 apio_env, extra_params=None, lib_dirs=None, lib_files=None
263 )
265 # -- The return action is a list of two steps, a function to call and
266 # -- a string with a command.
267 assert isinstance(action, list)
268 assert len(action) == 2
269 assert isinstance(action[0], FunctionAction)
270 assert isinstance(action[1], str)
272 # -- Collapse consecutive spaces in the string.
273 normalized_cmd = re.sub(r"\s+", " ", action[1])
275 # -- Verify the string
276 assert (
277 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
278 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DSYNTHESIZE -DAPIO_SIM=0 "
279 "--top-module main "
280 f"_build{os.sep}default{os.sep}hardware.vlt $SOURCES"
281 == normalized_cmd
282 )
285def test_verilator_lint_action_max(apio_runner: ApioRunner):
286 """Tests the verilator_lint_action() function with maximal params."""
288 with apio_runner.in_sandbox():
290 # -- Create apio scons env.
291 apio_env = make_test_apio_env(
292 targets=["lint"],
293 apio_env_params=ApioEnvParams(
294 verilator_extra_options=["--opt1", "--opt2"]
295 ),
296 target_params=TargetParams(
297 lint=LintParams(
298 top_module="my_top_module",
299 )
300 ),
301 )
303 # -- Call the tested function with minimal args.
304 action = verilator_lint_action(
305 apio_env,
306 extra_params=["param1", "param2"],
307 lib_dirs=["dir1", "dir2"],
308 lib_files=["file1", "file2"],
309 )
311 # -- The return action is a list of two steps, a function to call and
312 # -- a string with a command.
313 assert isinstance(action, list)
314 assert len(action) == 2
315 assert isinstance(action[0], FunctionAction)
316 assert isinstance(action[1], str)
318 # -- Collapse consecutive spaces in the string.
319 normalized_cmd = re.sub(r"\s+", " ", action[1])
321 # -- Verify the string
322 assert (
323 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
324 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DSYNTHESIZE -DAPIO_SIM=0 "
325 "--opt1 --opt2 "
326 '--top-module my_top_module param1 param2 -I"dir1" -I"dir2" '
327 f'_build{os.sep}default{os.sep}hardware.vlt "file1" "file2" '
328 "$SOURCES" == normalized_cmd
329 )