Coverage for tests/unit_tests/scons/test_plugin_util.py: 100%
114 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +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 TargetParams, UploadParams, LintParams
17from apio.scons.plugin_util import (
18 get_constraint_file,
19 verilog_src_scanner,
20 get_programmer_cmd,
21 map_params,
22 make_verilator_config_builder,
23 verilator_lint_action,
24)
27def test_get_constraint_file(
28 apio_runner: ApioRunner, capsys: LogCaptureFixture
29):
30 """Test the get_constraint_file() method."""
32 with apio_runner.in_sandbox() as sb:
34 apio_env = make_test_apio_env()
36 # -- If not .pcf files, should print an error and exit.
37 capsys.readouterr() # Reset capture
38 with pytest.raises(SystemExit) as e:
39 result = get_constraint_file(apio_env, ".pcf")
40 captured = capsys.readouterr()
41 assert e.value.code == 1
42 assert (
43 "Error: No constraint file '*.pcf' found, expected exactly one"
44 in cunstyle(captured.out)
45 )
47 # -- If a single .pcf file, return it.
48 sb.write_file("pinout.pcf", "content")
49 result = get_constraint_file(apio_env, ".pcf")
50 captured = capsys.readouterr()
51 assert captured.out == ""
52 assert result == "pinout.pcf"
54 # -- If there is more than one, exit with an error message.
55 sb.write_file("other.pcf", "content")
56 capsys.readouterr() # Reset capture
57 with pytest.raises(SystemExit) as e:
58 result = get_constraint_file(apio_env, ".pcf")
59 captured = capsys.readouterr()
60 assert e.value.code == 1
61 assert "Error: Found multiple '*.pcf'" in cunstyle(captured.out)
63 # -- If the user specified a valid file then return it.
64 apio_env.params.apio_env_params.constraint_file = "xyz.pcf"
65 capsys.readouterr() # Reset capture
66 result = get_constraint_file(apio_env, ".pcf")
67 captured = capsys.readouterr()
68 assert captured.out == ""
69 assert result == "xyz.pcf"
71 # -- If the user specified a constrain file with an extension that
72 # -- doesn't match the architecture, exit with an error
73 apio_env.params.apio_env_params.constraint_file = "xyz.bad"
74 capsys.readouterr() # Reset capture
75 with pytest.raises(SystemExit) as e:
76 result = get_constraint_file(apio_env, ".pcf")
77 captured = capsys.readouterr()
78 assert e.value.code == 1
79 assert (
80 "Constraint file 'xyz.bad' should have the extension '.pcf'"
81 in cunstyle(captured.out)
82 )
84 # -- If the user specified a constrain file with forbidden char,
85 # -- exit with an error
86 apio_env.params.apio_env_params.constraint_file = "a/xyz.pcf"
87 capsys.readouterr() # Reset capture
88 with pytest.raises(SystemExit) as e:
89 result = get_constraint_file(apio_env, ".pcf")
90 captured = capsys.readouterr()
91 assert e.value.code == 1
92 assert (
93 "Constrain filename 'a/xyz.pcf' contains an illegal character: '/'"
94 in cunstyle(captured.out)
95 )
98def test_verilog_src_scanner(apio_runner: ApioRunner):
99 """Test the verilog scanner which scans a verilog file and extract
100 reference of files it uses.
101 """
103 # -- Test file content with references. Contains duplicates and
104 # -- references out of alphabetical order.
105 file_content = """
106 // Dummy file for testing.
108 // Icestudio reference.
109 parameter v771499 = "v771499.list"
111 // System verilog include reference.
112 `include "apio_testing.vh"
114 // Duplicate icestudio reference.
115 parameter v771499 = "v771499.list"
117 // Verilog include reference.
118 `include "apio_testing.v
120 // $readmemh() function reference.
121 $readmemh("subdir2/my_data.hex", State_buff);
122 """
124 with apio_runner.in_sandbox() as sb:
126 # -- Write a test file name in the current directory.
127 sb.write_file("subdir1/test_file.v", file_content)
129 # -- Create a scanner
130 apio_env = make_test_apio_env()
131 scanner = verilog_src_scanner(apio_env)
133 # -- Run the scanner. It returns a list of File.
134 file = FS.File(FS(), "subdir1/test_file.v")
135 dependency_files = scanner.function(file, apio_env, None)
137 # -- Files list should be empty since none of the dependency candidate
138 # has a file.
139 file_names = [f.name for f in dependency_files]
140 assert file_names == []
142 # -- Create file lists
143 core_dependencies = [
144 "apio.ini",
145 "boards.jsonc",
146 "programmers.jsonc",
147 "fpgas.jsonc",
148 ]
150 file_dependencies = [
151 "apio_testing.vh",
152 join("subdir2", "my_data.hex"),
153 join("subdir1", "v771499.list"),
154 ]
156 # -- Create dummy files. This should cause the dependencies to be
157 # -- reported. (Candidate dependencies with no matching file are
158 # -- filtered out)
159 for f in core_dependencies + file_dependencies + ["non-related.txt"]:
160 sb.write_file(f, "dummy-file")
162 # -- Run the scanner again
163 dependency_files = scanner.function(file, apio_env, None)
165 # -- Check the dependencies
166 file_names = [f.path for f in dependency_files]
167 assert file_names == sorted(core_dependencies + file_dependencies)
170def test_get_programmer_cmd():
171 """Tests the function programmer_cmd()."""
173 apio_console.configure()
175 # -- Test a valid programmer command.
176 apio_env = make_test_apio_env(
177 targets=["upload"],
178 target_params=TargetParams(
179 upload=UploadParams(programmer_cmd="my_prog aa $SOURCE bb")
180 ),
181 )
182 assert get_programmer_cmd(apio_env) == "my_prog aa $SOURCE bb"
185def test_map_params():
186 """Test the map_params() function."""
188 # -- Empty cases
189 assert map_params([], "x_{}_y") == ""
190 assert map_params(["", " "], "x_{}_y") == ""
192 # -- Non empty cases
193 assert map_params(["a"], "x_{}_y") == "x_a_y"
194 assert map_params([" a "], "x_{}_y") == "x_a_y"
195 assert map_params(["a", "a", "b"], "x_{}_y") == "x_a_y x_a_y x_b_y"
198def test_make_verilator_config_builder(apio_runner: ApioRunner):
199 """Tests the make_verilator_config_builder() function."""
201 with apio_runner.in_sandbox() as sb:
203 # -- Create a test scons env.
204 apio_env = make_test_apio_env()
206 # -- Call the tested method to create a builder.
207 builder = make_verilator_config_builder(sb.packages_dir)
209 # -- Verify builder suffixes.
210 assert builder.suffix == ".vlt"
211 assert builder.src_suffix == []
213 # -- Create a target that doesn't exist yet.
214 assert not exists("hardware.vlt")
215 target = FS.File(FS(), "hardware.vlt")
217 # -- Invoke the builder's action to create the target.
218 builder.action(target, [], apio_env.scons_env)
219 assert isfile("hardware.vlt")
221 # -- Verify that the file was created with the given text.
222 text = sb.read_file("hardware.vlt")
223 assert "verilator_config" in text, text
224 assert "lint_off -rule COMBDLY" in text, text
227def test_verilator_lint_action_min(apio_runner: ApioRunner):
228 """Tests the verilator_lint_action() function with minimal params."""
230 with apio_runner.in_sandbox():
232 # -- Create apio scons env.
233 apio_env = make_test_apio_env(
234 targets=["lint"], target_params=TargetParams(lint=LintParams())
235 )
237 # -- Call the tested function with minimal args.
238 action = verilator_lint_action(
239 apio_env, extra_params=None, lib_dirs=None, lib_files=None
240 )
242 # -- The return action is a list of two steps, a function to call and
243 # -- a string with a command.
244 assert isinstance(action, list)
245 assert len(action) == 2
246 assert isinstance(action[0], FunctionAction)
247 assert isinstance(action[1], str)
249 # -- Collapse consecutive spaces in the string.
250 normalized_cmd = re.sub(r"\s+", " ", action[1])
252 # -- Verify the string
253 assert (
254 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
255 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DAPIO_SIM=0 --top-module main "
256 f"_build{os.sep}default{os.sep}hardware.vlt $SOURCES"
257 == normalized_cmd
258 )
261def test_verilator_lint_action_max(apio_runner: ApioRunner):
262 """Tests the verilator_lint_action() function with maximal params."""
264 with apio_runner.in_sandbox():
266 # -- Create apio scons env.
267 apio_env = make_test_apio_env(
268 targets=["lint"],
269 target_params=TargetParams(
270 lint=LintParams(
271 top_module="my_top_module",
272 verilator_all=True,
273 verilator_no_style=True,
274 verilator_no_warns=["aa", "bb"],
275 verilator_warns=["cc", "dd"],
276 )
277 ),
278 )
280 # -- Call the tested function with minimal args.
281 action = verilator_lint_action(
282 apio_env,
283 extra_params=["param1", "param2"],
284 lib_dirs=["dir1", "dir2"],
285 lib_files=["file1", "file2"],
286 )
288 # -- The return action is a list of two steps, a function to call and
289 # -- a string with a command.
290 assert isinstance(action, list)
291 assert len(action) == 2
292 assert isinstance(action[0], FunctionAction)
293 assert isinstance(action[1], str)
295 # -- Collapse consecutive spaces in the string.
296 normalized_cmd = re.sub(r"\s+", " ", action[1])
297 # -- Verify the string
298 assert (
299 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
300 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DAPIO_SIM=0 -Wall -Wno-style "
301 "-Wno-aa -Wno-bb -Wwarn-cc -Wwarn-dd --top-module my_top_module "
302 'param1 param2 -I"dir1" -I"dir2" '
303 f"_build{os.sep}default{os.sep}hardware.vlt "
304 '"file1" "file2" $SOURCES' == normalized_cmd
305 )