Coverage for tests / unit_tests / scons / test_plugin_util.py: 100%
123 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-24 01:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-24 01:53 +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 "No constraint file '*.pcf' found" in cunstyle(captured.out)
44 # -- If a single .pcf file, return it. Constraint file can also be
45 # -- in subdirectories as we test here
46 file1 = os.path.join("lib", "pinout.pcf")
47 sb.write_file(file1, "content1")
48 result = get_constraint_file(apio_env, ".pcf")
49 captured = capsys.readouterr()
50 assert captured.out == ""
51 assert result == file1
53 # -- If there is more than one, exit with an error message.
54 file2 = "other.pcf"
55 sb.write_file(file2, "content2")
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 2 constraint files '*.pcf'" in cunstyle(
62 captured.out
63 )
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 capsys.readouterr() # Reset capture
69 result = get_constraint_file(apio_env, ".pcf")
70 captured = capsys.readouterr()
71 assert captured.out == ""
72 assert result == "xyz.pcf"
74 # -- File extension should match the architecture.
75 apio_env.params.apio_env_params.constraint_file = "xyz.bad"
76 capsys.readouterr() # Reset capture
77 with pytest.raises(SystemExit) as e:
78 result = get_constraint_file(apio_env, ".pcf")
79 captured = capsys.readouterr()
80 assert e.value.code == 1
81 assert (
82 "Constraint file should have the extension '.pcf': xyz.bad"
83 in cunstyle(captured.out)
84 )
86 # -- Path under _build is not allowed.
87 apio_env.params.apio_env_params.constraint_file = "_build/xyz.pcf"
88 capsys.readouterr() # Reset capture
89 with pytest.raises(SystemExit) as e:
90 result = get_constraint_file(apio_env, ".pcf")
91 captured = capsys.readouterr()
92 assert e.value.code == 1
93 assert (
94 "Error: Constraint file should not be under _build: _build/xyz.pcf"
95 in cunstyle(captured.out)
96 )
98 # -- Path should not contain '../
99 apio_env.params.apio_env_params.constraint_file = "a/../xyz.pcf"
100 capsys.readouterr() # Reset capture
101 with pytest.raises(SystemExit) as e:
102 result = get_constraint_file(apio_env, ".pcf")
103 captured = capsys.readouterr()
104 assert e.value.code == 1
105 assert (
106 "Error: Constraint file path should not contain '..': a/../xyz.pcf"
107 in cunstyle(captured.out)
108 )
111def test_verilog_src_scanner(apio_runner: ApioRunner):
112 """Test the verilog scanner which scans a verilog file and extract
113 reference of files it uses.
114 """
116 # -- Test file content with references. Contains duplicates and
117 # -- references out of alphabetical order.
118 file_content = """
119 // Dummy file for testing.
121 // Icestudio reference.
122 parameter v771499 = "v771499.list"
124 // System verilog include reference.
125 `include "apio_testing.vh"
127 // Duplicate icestudio reference.
128 parameter v771499 = "v771499.list"
130 // Verilog include reference.
131 `include "apio_testing.v
133 // $readmemh() function reference.
134 $readmemh("subdir2/my_data.hex", State_buff);
135 """
137 with apio_runner.in_sandbox() as sb:
139 # -- Write a test file name in the current directory.
140 sb.write_file("subdir1/test_file.v", file_content)
142 # -- Create a scanner
143 apio_env = make_test_apio_env()
144 scanner = verilog_src_scanner(apio_env)
146 # -- Run the scanner. It returns a list of File.
147 file = FS.File(FS(), "subdir1/test_file.v")
148 dependency_files = scanner.function(file, apio_env, None)
150 # -- Files list should be empty since none of the dependency candidate
151 # has a file.
152 file_names = [f.name for f in dependency_files]
153 assert file_names == []
155 # -- Create file lists
156 core_dependencies = [
157 "apio.ini",
158 "boards.jsonc",
159 "programmers.jsonc",
160 "fpgas.jsonc",
161 ]
163 file_dependencies = [
164 "apio_testing.vh",
165 join("subdir2", "my_data.hex"),
166 join("subdir1", "v771499.list"),
167 ]
169 # -- Create dummy files. This should cause the dependencies to be
170 # -- reported. (Candidate dependencies with no matching file are
171 # -- filtered out)
172 for f in core_dependencies + file_dependencies + ["non-related.txt"]:
173 sb.write_file(f, "dummy-file")
175 # -- Run the scanner again
176 dependency_files = scanner.function(file, apio_env, None)
178 # -- Check the dependencies
179 file_names = [f.path for f in dependency_files]
180 assert file_names == sorted(core_dependencies + file_dependencies)
183def test_get_programmer_cmd():
184 """Tests the function programmer_cmd()."""
186 apio_console.configure()
188 # -- Test a valid programmer command.
189 apio_env = make_test_apio_env(
190 targets=["upload"],
191 target_params=TargetParams(
192 upload=UploadParams(programmer_cmd="my_prog aa $SOURCE bb")
193 ),
194 )
195 assert get_programmer_cmd(apio_env) == "my_prog aa $SOURCE bb"
198def test_map_params():
199 """Test the map_params() function."""
201 # -- Empty cases
202 assert map_params([], "x_{}_y") == ""
203 assert map_params(["", " "], "x_{}_y") == ""
205 # -- Non empty cases
206 assert map_params(["a"], "x_{}_y") == "x_a_y"
207 assert map_params([" a "], "x_{}_y") == "x_a_y"
208 assert map_params(["a", "a", "b"], "x_{}_y") == "x_a_y x_a_y x_b_y"
211def test_make_verilator_config_builder(apio_runner: ApioRunner):
212 """Tests the make_verilator_config_builder() function."""
214 with apio_runner.in_sandbox() as sb:
216 # -- Create a test scons env.
217 apio_env = make_test_apio_env()
219 # -- Call the tested method to create a builder.
220 builder = make_verilator_config_builder(sb.packages_dir)
222 # -- Verify builder suffixes.
223 assert builder.suffix == ".vlt"
224 assert builder.src_suffix == []
226 # -- Create a target that doesn't exist yet.
227 assert not exists("hardware.vlt")
228 target = FS.File(FS(), "hardware.vlt")
230 # -- Invoke the builder's action to create the target.
231 builder.action(target, [], apio_env.scons_env)
232 assert isfile("hardware.vlt")
234 # -- Verify that the file was created with the given text.
235 text = sb.read_file("hardware.vlt")
236 assert "verilator_config" in text, text
237 assert "lint_off -rule COMBDLY" in text, text
240def test_verilator_lint_action_min(apio_runner: ApioRunner):
241 """Tests the verilator_lint_action() function with minimal params."""
243 with apio_runner.in_sandbox():
245 # -- Create apio scons env.
246 apio_env = make_test_apio_env(
247 targets=["lint"], target_params=TargetParams(lint=LintParams())
248 )
250 # -- Call the tested function with minimal args.
251 action = verilator_lint_action(
252 apio_env, extra_params=None, lib_dirs=None, lib_files=None
253 )
255 # -- The return action is a list of two steps, a function to call and
256 # -- a string with a command.
257 assert isinstance(action, list)
258 assert len(action) == 2
259 assert isinstance(action[0], FunctionAction)
260 assert isinstance(action[1], str)
262 # -- Collapse consecutive spaces in the string.
263 normalized_cmd = re.sub(r"\s+", " ", action[1])
265 # -- Verify the string
266 assert (
267 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
268 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DAPIO_SIM=0 --top-module main "
269 f"_build{os.sep}default{os.sep}hardware.vlt $SOURCES"
270 == normalized_cmd
271 )
274def test_verilator_lint_action_max(apio_runner: ApioRunner):
275 """Tests the verilator_lint_action() function with maximal params."""
277 with apio_runner.in_sandbox():
279 # -- Create apio scons env.
280 apio_env = make_test_apio_env(
281 targets=["lint"],
282 target_params=TargetParams(
283 lint=LintParams(
284 top_module="my_top_module",
285 verilator_all=True,
286 verilator_no_style=True,
287 verilator_no_warns=["aa", "bb"],
288 verilator_warns=["cc", "dd"],
289 )
290 ),
291 )
293 # -- Call the tested function with minimal args.
294 action = verilator_lint_action(
295 apio_env,
296 extra_params=["param1", "param2"],
297 lib_dirs=["dir1", "dir2"],
298 lib_files=["file1", "file2"],
299 )
301 # -- The return action is a list of two steps, a function to call and
302 # -- a string with a command.
303 assert isinstance(action, list)
304 assert len(action) == 2
305 assert isinstance(action[0], FunctionAction)
306 assert isinstance(action[1], str)
308 # -- Collapse consecutive spaces in the string.
309 normalized_cmd = re.sub(r"\s+", " ", action[1])
310 # -- Verify the string
311 assert (
312 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
313 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DAPIO_SIM=0 -Wall -Wno-style "
314 "-Wno-aa -Wno-bb -Wwarn-cc -Wwarn-dd --top-module my_top_module "
315 'param1 param2 -I"dir1" -I"dir2" '
316 f"_build{os.sep}default{os.sep}hardware.vlt "
317 '"file1" "file2" $SOURCES' == normalized_cmd
318 )