Coverage for tests/unit_tests/scons/test_plugin_util.py: 100%
145 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
1# This file is part of the Apio project
2# (C) 2016-present FPGAwars
3# Project authors:
4# * Jesús Arroyo (Jesus89) (2016-2019)
5# * Juan Gonzalez (obijuan) (2019-2024)
6# * The Apio Team (2024-present)
7# License: GPLv2
9"""
10Tests of the scons plugin_util.py functions.
11"""
13import re
14import os
15from os.path import isfile, exists, join
16from pathlib import Path
17import pytest
18from SCons.Node.FS import FS
19from SCons.Action import FunctionAction
20from tests.unit_tests.scons.testing import make_test_apio_env
21from tests.conftest import ApioRunner
22from apio.common import apio_console
23from apio.common.proto.stubs.apio_common_pb2 import ApioArch
24from apio.common.build_report import BuildReport, ResourceReport
25from apio.common.proto.stubs.apio_scons_pb2 import (
26 TargetParams,
27 UploadParams,
28 LintParams,
29 ApioEnvParams,
30)
31from apio.scons.plugin_util import (
32 get_constraint_file,
33 verilog_src_scanner,
34 get_programmer_cmd,
35 map_str_params,
36 map_path_params,
37 make_verilator_config_builder,
38 verilator_lint_action,
39 iverilog_action,
40 _print_pnr_report,
41)
44def test_get_constraint_file(apio_runner: ApioRunner):
45 """Test the get_constraint_file() method."""
47 with apio_runner.in_sandbox() as sb:
49 apio_env = make_test_apio_env()
51 # -- If not .pcf files, should print an error and exit.
52 with apio_runner.with_logger() as log:
53 with pytest.raises(SystemExit) as e:
54 result = get_constraint_file(apio_env, ".pcf")
55 assert e.value.code == 1
56 assert "No constraint file '*.pcf' found" in log.out
58 # -- If a single .pcf file, return it. Constraint file can also be
59 # -- in subdirectories as we test here
60 file1 = os.path.join("lib", "pinout.pcf")
61 sb.write_file(file1, "content1")
62 with apio_runner.with_logger() as log:
63 result = get_constraint_file(apio_env, ".pcf")
64 assert log.out == ""
65 assert result == file1
67 # -- If there is more than one, exit with an error message.
68 file2 = "other.pcf"
69 sb.write_file(file2, "content2")
70 with apio_runner.with_logger() as log:
71 with pytest.raises(SystemExit) as e:
72 result = get_constraint_file(apio_env, ".pcf")
73 assert e.value.code == 1
74 assert "Error: Found 2 constraint files '*.pcf'" in log.out
76 # -- If the user specified a valid file then return it, regardless
77 # -- if it exists or not.
78 apio_env.params.apio_env_params.constraint_file = "xyz.pcf"
79 with apio_runner.with_logger() as log:
80 result = get_constraint_file(apio_env, ".pcf")
81 assert log.out == ""
82 assert result == "xyz.pcf"
84 # -- File extension should match the architecture.
85 apio_env.params.apio_env_params.constraint_file = "xyz.bad"
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 "Constraint file should have the extension '.pcf': xyz.bad"
92 in log.out
93 )
95 # -- Path under _build is not allowed.
96 apio_env.params.apio_env_params.constraint_file = "_build/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 should not be under _build: _build/xyz.pcf"
103 in log.out
104 )
106 # -- Path should not contain '../
107 apio_env.params.apio_env_params.constraint_file = "a/../xyz.pcf"
108 with apio_runner.with_logger() as log:
109 with pytest.raises(SystemExit) as e:
110 result = get_constraint_file(apio_env, ".pcf")
111 assert e.value.code == 1
112 assert (
113 "Error: Constraint file path should not contain '..': a/../xyz.pcf"
114 in log.out
115 )
118def test_verilog_src_scanner(apio_runner: ApioRunner):
119 """Test the verilog scanner which scans a verilog file and extract
120 reference of files it uses.
121 """
123 # -- Test file content with references. Contains duplicates and
124 # -- references out of alphabetical order.
125 file_content = """
126 // Dummy file for testing.
128 // Icestudio reference.
129 parameter v771499 = "v771499.list"
131 // System verilog include reference.
132 `include "apio_testing.vh"
134 // Duplicate icestudio reference.
135 parameter v771499 = "v771499.list"
137 // Verilog include reference.
138 `include "apio_testing.v
140 // $readmemh() function reference.
141 $readmemh("subdir2/my_data.hex", State_buff);
142 """
144 with apio_runner.in_sandbox() as sb:
146 # -- Write a test file name in the current directory.
147 sb.write_file("subdir1/test_file.v", file_content)
149 # -- Create a scanner
150 apio_env = make_test_apio_env()
151 scanner = verilog_src_scanner(apio_env)
153 # -- Run the scanner. It returns a list of File.
154 file = FS.File(FS(), "subdir1/test_file.v")
155 dependency_files = scanner.function(file, apio_env, None)
157 # -- Files list should be empty since none of the dependency candidate
158 # has a file.
159 file_names = [f.name for f in dependency_files]
160 assert file_names == []
162 # -- Create file lists
163 core_dependencies = [
164 "apio.ini",
165 "boards.jsonc",
166 "programmers.jsonc",
167 "fpgas.jsonc",
168 ]
170 file_dependencies = [
171 "apio_testing.vh",
172 join("subdir2", "my_data.hex"),
173 join("subdir1", "v771499.list"),
174 ]
176 # -- Create dummy files. This should cause the dependencies to be
177 # -- reported. (Candidate dependencies with no matching file are
178 # -- filtered out)
179 for f in core_dependencies + file_dependencies + ["non-related.txt"]:
180 sb.write_file(f, "dummy-file")
182 # -- Run the scanner again
183 dependency_files = scanner.function(file, apio_env, None)
185 # -- Check the dependencies
186 file_names = [f.path for f in dependency_files]
187 assert file_names == sorted(core_dependencies + file_dependencies)
190def test_get_programmer_cmd():
191 """Tests the function programmer_cmd()."""
193 apio_console.configure()
195 # -- Test a valid programmer command.
196 apio_env = make_test_apio_env(
197 targets=["upload"],
198 target_params=TargetParams(
199 upload=UploadParams(programmer_cmd="my_prog aa $SOURCE bb")
200 ),
201 )
202 assert get_programmer_cmd(apio_env) == "my_prog aa $SOURCE bb"
205def test_map_str_params():
206 """Test the map_str_params() function."""
208 # -- Empty cases
209 assert map_str_params([], "x_{}_y") == ""
210 assert map_str_params(["", " "], "x_{}_y") == ""
212 # -- Non empty cases
213 assert map_str_params(["a"], "x_{}_y") == "x_a_y"
214 assert map_str_params([" a "], "x_{}_y") == "x_a_y"
215 assert map_str_params(["a", "a", "b"], "x_{}_y") == "x_a_y x_a_y x_b_y"
218def test_map_path_params():
219 """Test the map_path_params() function."""
221 assert map_path_params([], "x_{}_y") == ""
223 assert (
224 map_path_params([Path("aa/bb"), Path(".")], "x/_{}_/y")
225 == "x/_aa" + os.sep + "bb_/y" + " " + "x/_._/y"
226 )
229def test_make_verilator_config_builder(apio_runner: ApioRunner):
230 """Tests the make_verilator_config_builder() function."""
232 with apio_runner.in_sandbox() as sb:
234 # -- Create a test scons env.
235 apio_env = make_test_apio_env()
237 # -- Call the tested method to create a builder.
238 builder = make_verilator_config_builder(
239 sb.packages_dir,
240 rules_to_suppress=[
241 "SPECIFYIGN",
242 ],
243 )
245 # -- Verify builder suffixes.
246 assert builder.src_suffix == []
247 assert builder.suffix == ".vlt"
249 # -- Create a target that doesn't exist yet.
250 assert not exists("hardware.vlt")
251 target = FS.File(FS(), "hardware.vlt")
253 # -- Invoke the builder's action to create the target.
254 builder.action(target, [], apio_env.scons_env)
255 assert isfile("hardware.vlt")
257 # -- Verify that the file was created with the given text.
258 text = sb.read_file_text("hardware.vlt")
259 assert "verilator_config" in text, text
260 assert "lint_off -rule SPECIFYIGN" in text, text
263def test_iverilog_action_has_no_vcd_output_macro(apio_runner: ApioRunner):
264 """Tests iverilog_action() does not define the retired VCD_OUTPUT macro."""
266 with apio_runner.in_sandbox():
268 apio_env = make_test_apio_env()
269 action = iverilog_action(apio_env, verbose=False, is_interactive=False)
271 normalized_cmd = re.sub(r"\s+", " ", action)
273 assert "VCD_OUTPUT" not in normalized_cmd
274 assert "-DAPIO_SIM=0" in normalized_cmd
275 assert normalized_cmd.startswith("iverilog -g2012")
277 action_sim = iverilog_action(
278 apio_env, verbose=True, is_interactive=True
279 )
280 normalized_sim = re.sub(r"\s+", " ", action_sim)
281 assert "-DAPIO_SIM=1" in normalized_sim
282 assert "iverilog -g2012 -v " in normalized_sim
285def test_verilator_lint_action_min(apio_runner: ApioRunner):
286 """Tests the verilator_lint_action() function with minimal params."""
288 with apio_runner.in_sandbox():
290 # -- Create apio scons env.
291 apio_env = make_test_apio_env(
292 targets=["lint"], target_params=TargetParams(lint=LintParams())
293 )
295 # -- Call the tested function with minimal args.
296 action = verilator_lint_action(
297 apio_env, extra_params=None, lib_dirs=None, lib_files=None
298 )
300 # -- The return action is a list of two steps, a function to call and
301 # -- a string with a command.
302 assert isinstance(action, list)
303 assert len(action) == 2
304 assert isinstance(action[0], FunctionAction)
305 assert isinstance(action[1], str)
307 # -- Collapse consecutive spaces in the string.
308 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 -DSYNTHESIZE -DAPIO_SIM=0 "
314 "--top-module main "
315 f"_build{os.sep}default{os.sep}hardware.vlt $SOURCES"
316 == normalized_cmd
317 )
320def test_verilator_lint_action_max(apio_runner: ApioRunner):
321 """Tests the verilator_lint_action() function with maximal params."""
323 with apio_runner.in_sandbox():
325 # -- Create apio scons env.
326 apio_env = make_test_apio_env(
327 targets=["lint"],
328 apio_env_params=ApioEnvParams(
329 verilator_extra_options=["--opt1", "--opt2"]
330 ),
331 target_params=TargetParams(
332 lint=LintParams(
333 top_module="my_top_module",
334 )
335 ),
336 )
338 # -- Call the tested function with minimal args.
339 action = verilator_lint_action(
340 apio_env,
341 extra_params=["param1", "param2"],
342 lib_dirs=[Path("dir1"), Path("dir2")],
343 lib_files=[Path("file1"), Path("file2")],
344 )
346 # -- The return action is a list of two steps, a function to call and
347 # -- a string with a command.
348 assert isinstance(action, list)
349 assert len(action) == 2
350 assert isinstance(action[0], FunctionAction)
351 assert isinstance(action[1], str)
353 # -- Collapse consecutive spaces in the string.
354 normalized_cmd = re.sub(r"\s+", " ", action[1])
356 # -- Verify the string
357 assert (
358 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
359 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DSYNTHESIZE -DAPIO_SIM=0 "
360 "--opt1 --opt2 "
361 '--top-module my_top_module param1 param2 -I"dir1" -I"dir2" '
362 f'_build{os.sep}default{os.sep}hardware.vlt "file1" "file2" '
363 "$SOURCES" == normalized_cmd
364 )
367def test_print_pnr_report_invalid_percentage(apio_runner: ApioRunner):
368 """Tests that the pnr report shows '--' for resources with a
369 percentage that can't be computed or is out of range."""
371 with apio_runner.in_sandbox():
373 apio_console.configure()
375 build_report = BuildReport(
376 arch=ApioArch.ice40,
377 resources=[
378 ResourceReport("ICESTORM_LC", 7680, 768, 10.0),
379 ResourceReport("ICESTORM_PLL", 0, 2, None),
380 ],
381 clocks=[],
382 )
384 with apio_runner.with_logger() as log:
385 _print_pnr_report(build_report, verbose=True)
387 # -- Get the table lines of the two resources.
388 lines = log.out.splitlines()
389 lc_line = next(line for line in lines if "ICESTORM_LC" in line)
390 pll_line = next(line for line in lines if "ICESTORM_PLL" in line)
392 assert "10%" in lc_line
393 assert "--" in pll_line
394 assert "%" not in pll_line