Coverage for tests/unit_tests/scons/test_plugin_util.py: 100%
131 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
1"""
2Tests of the scons plugin_util.py functions.
3"""
5import re
6import os
7from os.path import isfile, exists, join
8from pathlib import Path
9import pytest
10from SCons.Node.FS import FS
11from SCons.Action import FunctionAction
12from tests.unit_tests.scons.testing import make_test_apio_env
13from tests.conftest import ApioRunner
14from apio.common import apio_console
15from apio.common.proto.apio_scons_pb2 import (
16 TargetParams,
17 UploadParams,
18 LintParams,
19 ApioEnvParams,
20)
21from apio.scons.plugin_util import (
22 get_constraint_file,
23 verilog_src_scanner,
24 get_programmer_cmd,
25 map_str_params,
26 map_path_params,
27 make_verilator_config_builder,
28 verilator_lint_action,
29 iverilog_action,
30)
33def test_get_constraint_file(apio_runner: ApioRunner):
34 """Test the get_constraint_file() method."""
36 with apio_runner.in_sandbox() as sb:
38 apio_env = make_test_apio_env()
40 # -- If not .pcf files, should print an error and exit.
41 with apio_runner.with_logger() as log:
42 with pytest.raises(SystemExit) as e:
43 result = get_constraint_file(apio_env, ".pcf")
44 assert e.value.code == 1
45 assert "No constraint file '*.pcf' found" in log.out
47 # -- If a single .pcf file, return it. Constraint file can also be
48 # -- in subdirectories as we test here
49 file1 = os.path.join("lib", "pinout.pcf")
50 sb.write_file(file1, "content1")
51 with apio_runner.with_logger() as log:
52 result = get_constraint_file(apio_env, ".pcf")
53 assert log.out == ""
54 assert result == file1
56 # -- If there is more than one, exit with an error message.
57 file2 = "other.pcf"
58 sb.write_file(file2, "content2")
59 with apio_runner.with_logger() as log:
60 with pytest.raises(SystemExit) as e:
61 result = get_constraint_file(apio_env, ".pcf")
62 assert e.value.code == 1
63 assert "Error: Found 2 constraint files '*.pcf'" in log.out
65 # -- If the user specified a valid file then return it, regardless
66 # -- if it exists or not.
67 apio_env.params.apio_env_params.constraint_file = "xyz.pcf"
68 with apio_runner.with_logger() as log:
69 result = get_constraint_file(apio_env, ".pcf")
70 assert log.out == ""
71 assert result == "xyz.pcf"
73 # -- File extension should match the architecture.
74 apio_env.params.apio_env_params.constraint_file = "xyz.bad"
75 with apio_runner.with_logger() as log:
76 with pytest.raises(SystemExit) as e:
77 result = get_constraint_file(apio_env, ".pcf")
78 assert e.value.code == 1
79 assert (
80 "Constraint file should have the extension '.pcf': xyz.bad"
81 in log.out
82 )
84 # -- Path under _build is not allowed.
85 apio_env.params.apio_env_params.constraint_file = "_build/xyz.pcf"
86 with apio_runner.with_logger() as log:
87 with pytest.raises(SystemExit) as e:
88 result = get_constraint_file(apio_env, ".pcf")
89 assert e.value.code == 1
90 assert (
91 "Error: Constraint file should not be under _build: _build/xyz.pcf"
92 in log.out
93 )
95 # -- Path should not contain '../
96 apio_env.params.apio_env_params.constraint_file = "a/../xyz.pcf"
97 with apio_runner.with_logger() as log:
98 with pytest.raises(SystemExit) as e:
99 result = get_constraint_file(apio_env, ".pcf")
100 assert e.value.code == 1
101 assert (
102 "Error: Constraint file path should not contain '..': a/../xyz.pcf"
103 in log.out
104 )
107def test_verilog_src_scanner(apio_runner: ApioRunner):
108 """Test the verilog scanner which scans a verilog file and extract
109 reference of files it uses.
110 """
112 # -- Test file content with references. Contains duplicates and
113 # -- references out of alphabetical order.
114 file_content = """
115 // Dummy file for testing.
117 // Icestudio reference.
118 parameter v771499 = "v771499.list"
120 // System verilog include reference.
121 `include "apio_testing.vh"
123 // Duplicate icestudio reference.
124 parameter v771499 = "v771499.list"
126 // Verilog include reference.
127 `include "apio_testing.v
129 // $readmemh() function reference.
130 $readmemh("subdir2/my_data.hex", State_buff);
131 """
133 with apio_runner.in_sandbox() as sb:
135 # -- Write a test file name in the current directory.
136 sb.write_file("subdir1/test_file.v", file_content)
138 # -- Create a scanner
139 apio_env = make_test_apio_env()
140 scanner = verilog_src_scanner(apio_env)
142 # -- Run the scanner. It returns a list of File.
143 file = FS.File(FS(), "subdir1/test_file.v")
144 dependency_files = scanner.function(file, apio_env, None)
146 # -- Files list should be empty since none of the dependency candidate
147 # has a file.
148 file_names = [f.name for f in dependency_files]
149 assert file_names == []
151 # -- Create file lists
152 core_dependencies = [
153 "apio.ini",
154 "boards.jsonc",
155 "programmers.jsonc",
156 "fpgas.jsonc",
157 ]
159 file_dependencies = [
160 "apio_testing.vh",
161 join("subdir2", "my_data.hex"),
162 join("subdir1", "v771499.list"),
163 ]
165 # -- Create dummy files. This should cause the dependencies to be
166 # -- reported. (Candidate dependencies with no matching file are
167 # -- filtered out)
168 for f in core_dependencies + file_dependencies + ["non-related.txt"]:
169 sb.write_file(f, "dummy-file")
171 # -- Run the scanner again
172 dependency_files = scanner.function(file, apio_env, None)
174 # -- Check the dependencies
175 file_names = [f.path for f in dependency_files]
176 assert file_names == sorted(core_dependencies + file_dependencies)
179def test_get_programmer_cmd():
180 """Tests the function programmer_cmd()."""
182 apio_console.configure()
184 # -- Test a valid programmer command.
185 apio_env = make_test_apio_env(
186 targets=["upload"],
187 target_params=TargetParams(
188 upload=UploadParams(programmer_cmd="my_prog aa $SOURCE bb")
189 ),
190 )
191 assert get_programmer_cmd(apio_env) == "my_prog aa $SOURCE bb"
194def test_map_str_params():
195 """Test the map_str_params() function."""
197 # -- Empty cases
198 assert map_str_params([], "x_{}_y") == ""
199 assert map_str_params(["", " "], "x_{}_y") == ""
201 # -- Non empty cases
202 assert map_str_params(["a"], "x_{}_y") == "x_a_y"
203 assert map_str_params([" a "], "x_{}_y") == "x_a_y"
204 assert map_str_params(["a", "a", "b"], "x_{}_y") == "x_a_y x_a_y x_b_y"
207def test_map_path_params():
208 """Test the map_path_params() function."""
210 assert map_path_params([], "x_{}_y") == ""
212 assert (
213 map_path_params([Path("aa/bb"), Path(".")], "x/_{}_/y")
214 == "x/_aa" + os.sep + "bb_/y" + " " + "x/_._/y"
215 )
218def test_make_verilator_config_builder(apio_runner: ApioRunner):
219 """Tests the make_verilator_config_builder() function."""
221 with apio_runner.in_sandbox() as sb:
223 # -- Create a test scons env.
224 apio_env = make_test_apio_env()
226 # -- Call the tested method to create a builder.
227 builder = make_verilator_config_builder(
228 sb.packages_dir,
229 rules_to_suppress=[
230 "SPECIFYIGN",
231 ],
232 )
234 # -- Verify builder suffixes.
235 assert builder.src_suffix == []
236 assert builder.suffix == ".vlt"
238 # -- Create a target that doesn't exist yet.
239 assert not exists("hardware.vlt")
240 target = FS.File(FS(), "hardware.vlt")
242 # -- Invoke the builder's action to create the target.
243 builder.action(target, [], apio_env.scons_env)
244 assert isfile("hardware.vlt")
246 # -- Verify that the file was created with the given text.
247 text = sb.read_file_text("hardware.vlt")
248 assert "verilator_config" in text, text
249 assert "lint_off -rule SPECIFYIGN" in text, text
252def test_iverilog_action_has_no_vcd_output_macro(apio_runner: ApioRunner):
253 """Tests iverilog_action() does not define the retired VCD_OUTPUT macro."""
255 with apio_runner.in_sandbox():
257 apio_env = make_test_apio_env()
258 action = iverilog_action(apio_env, verbose=False, is_interactive=False)
260 normalized_cmd = re.sub(r"\s+", " ", action)
262 assert "VCD_OUTPUT" not in normalized_cmd
263 assert "-DAPIO_SIM=0" in normalized_cmd
264 assert normalized_cmd.startswith("iverilog -g2012")
266 action_sim = iverilog_action(
267 apio_env, verbose=True, is_interactive=True
268 )
269 normalized_sim = re.sub(r"\s+", " ", action_sim)
270 assert "-DAPIO_SIM=1" in normalized_sim
271 assert "iverilog -g2012 -v " in normalized_sim
274def test_verilator_lint_action_min(apio_runner: ApioRunner):
275 """Tests the verilator_lint_action() function with minimal params."""
277 with apio_runner.in_sandbox():
279 # -- Create apio scons env.
280 apio_env = make_test_apio_env(
281 targets=["lint"], target_params=TargetParams(lint=LintParams())
282 )
284 # -- Call the tested function with minimal args.
285 action = verilator_lint_action(
286 apio_env, extra_params=None, lib_dirs=None, lib_files=None
287 )
289 # -- The return action is a list of two steps, a function to call and
290 # -- a string with a command.
291 assert isinstance(action, list)
292 assert len(action) == 2
293 assert isinstance(action[0], FunctionAction)
294 assert isinstance(action[1], str)
296 # -- Collapse consecutive spaces in the string.
297 normalized_cmd = re.sub(r"\s+", " ", action[1])
299 # -- Verify the string
300 assert (
301 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
302 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DSYNTHESIZE -DAPIO_SIM=0 "
303 "--top-module main "
304 f"_build{os.sep}default{os.sep}hardware.vlt $SOURCES"
305 == normalized_cmd
306 )
309def test_verilator_lint_action_max(apio_runner: ApioRunner):
310 """Tests the verilator_lint_action() function with maximal params."""
312 with apio_runner.in_sandbox():
314 # -- Create apio scons env.
315 apio_env = make_test_apio_env(
316 targets=["lint"],
317 apio_env_params=ApioEnvParams(
318 verilator_extra_options=["--opt1", "--opt2"]
319 ),
320 target_params=TargetParams(
321 lint=LintParams(
322 top_module="my_top_module",
323 )
324 ),
325 )
327 # -- Call the tested function with minimal args.
328 action = verilator_lint_action(
329 apio_env,
330 extra_params=["param1", "param2"],
331 lib_dirs=[Path("dir1"), Path("dir2")],
332 lib_files=[Path("file1"), Path("file2")],
333 )
335 # -- The return action is a list of two steps, a function to call and
336 # -- a string with a command.
337 assert isinstance(action, list)
338 assert len(action) == 2
339 assert isinstance(action[0], FunctionAction)
340 assert isinstance(action[1], str)
342 # -- Collapse consecutive spaces in the string.
343 normalized_cmd = re.sub(r"\s+", " ", action[1])
345 # -- Verify the string
346 assert (
347 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
348 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DSYNTHESIZE -DAPIO_SIM=0 "
349 "--opt1 --opt2 "
350 '--top-module my_top_module param1 param2 -I"dir1" -I"dir2" '
351 f'_build{os.sep}default{os.sep}hardware.vlt "file1" "file2" '
352 "$SOURCES" == normalized_cmd
353 )