Coverage for tests/unit_tests/managers/test_project.py: 100%
65 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 project.py
11"""
13import pytest
14from tests.conftest import ApioRunner
15from apio.managers.project import Project, ENV_OPTIONS_SPEC
16from apio.apio_context import (
17 ApioContext,
18 PackagesPolicy,
19 ProjectPolicy,
20 RemoteConfigPolicy,
21)
23# TODO: Add more tests.
26def load_apio_ini(
27 apio_ini: dict[str, dict[str, str]],
28 env_arg: str | None,
29 apio_runner: ApioRunner,
30) -> tuple[Project, str]:
31 """A helper function load apio.ini. Returns (project, log-text)"""
33 with apio_runner.in_sandbox() as sb:
34 # -- Create the apio.ini file
35 sb.write_apio_ini(apio_ini)
37 # -- Try to create the context with the project info while
38 # -- capturing log.
39 with apio_runner.with_logger() as log:
40 apio_ctx = ApioContext(
41 project_policy=ProjectPolicy.PROJECT_REQUIRED,
42 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
43 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
44 env_arg=env_arg,
45 )
47 # -- Return the values.
48 return (apio_ctx.project, log.out)
51def test_env_options_specs():
52 """Tests the option spec keys match options specs names"""
54 for name, env_options_spec in ENV_OPTIONS_SPEC.items():
55 assert name == env_options_spec.name
58def test_all_options_env(apio_runner: ApioRunner):
59 """Tests an apio.ini with all options"""
61 apio_ini = {
62 "[env:default]": {
63 "board": "alhambra-ii", # required.
64 "default-testbench": "main_tb.v",
65 "defines": "\n aaa=111\n bbb=222",
66 "format-verible-options": "\n --aaa bbb\n --ccc ddd",
67 "programmer-cmd": "iceprog ${VID}:${PID}",
68 "top-module": "my_module",
69 "yosys-extra-options": "-dsp -xyz",
70 "nextpnr-extra-options": "--freq 13",
71 "gtkwave-extra-options": "--rcvar=do_initial_zoom_fit 1",
72 "verilator-extra-options": "-Wno-fatal",
73 "constraint-file": "pinout.lpf",
74 }
75 }
77 # -- Make sure we covered all the options.
78 assert len(apio_ini["[env:default]"]) == len(ENV_OPTIONS_SPEC)
80 project, _ = load_apio_ini(
81 apio_ini=apio_ini,
82 env_arg=None,
83 apio_runner=apio_runner,
84 )
86 assert project.env_name == "default"
87 assert project.env_options == {
88 "board": "alhambra-ii",
89 "default-testbench": "main_tb.v",
90 "defines": ["aaa=111", "bbb=222"],
91 "format-verible-options": ["--aaa bbb", "--ccc ddd"],
92 "programmer-cmd": "iceprog ${VID}:${PID}",
93 "top-module": "my_module",
94 "yosys-extra-options": ["-dsp -xyz"],
95 "nextpnr-extra-options": ["--freq 13"],
96 "gtkwave-extra-options": ["--rcvar=do_initial_zoom_fit 1"],
97 "verilator-extra-options": ["-Wno-fatal"],
98 "constraint-file": "pinout.lpf",
99 }
101 # -- Try a few as dict lookup on the project object.
102 assert project.get_str_option("board") == "alhambra-ii"
103 assert project.get_str_option("top-module") == "my_module"
104 assert project.get_str_option("constraint-file") == "pinout.lpf"
107def test_required_options_only_env(apio_runner: ApioRunner):
108 """Tests a minimal apio.ini with required only options."""
110 project, _ = load_apio_ini(
111 apio_ini={
112 "[env:default]": {
113 "board": "alhambra-ii",
114 "top-module": "main",
115 }
116 },
117 env_arg=None,
118 apio_runner=apio_runner,
119 )
121 assert project.env_name == "default"
122 assert project.env_options == {
123 "board": "alhambra-ii",
124 "top-module": "main",
125 }
128def test_list_options(apio_runner: ApioRunner):
129 """Tests list options."""
131 project, _ = load_apio_ini(
132 apio_ini={
133 "[env:default]": {
134 "board": "alhambra-ii",
135 "top-module": "my_top_module",
136 "yosys-extra-options": " k1=v1 k2=v2 \n\n k3=v3 \n\n",
137 "nextpnr-extra-options": " k5=v5 k6=v6 \n\n k7=v7 \n\n",
138 }
139 },
140 env_arg=None,
141 apio_runner=apio_runner,
142 )
144 assert project.get_list_option("yosys-extra-options") == [
145 "k1=v1 k2=v2",
146 "k3=v3",
147 ]
149 assert project.get_list_option("nextpnr-extra-options") == [
150 "k5=v5 k6=v6",
151 "k7=v7",
152 ]
155def test_macro_expansion(apio_runner: ApioRunner):
156 """Tests the expansion of macros within values."""
158 project, _ = load_apio_ini(
159 apio_ini={
160 "[env:default]": {
161 "board": "alhambra-ii",
162 "top-module": "my_top_module",
163 "constraint-file": " ${SEMICOLON} value ",
164 "yosys-extra-options": " k1=${ENV_BUILD}/v1 k2=v2; \n ; "
165 "Comment \n k3=v3${HASH} ${ENV_NAME}\n\n",
166 }
167 },
168 env_arg=None,
169 apio_runner=apio_runner,
170 )
172 assert project.get_str_option("constraint-file") == "; value"
174 assert project.get_list_option("yosys-extra-options") == [
175 "k1=_build/default/v1 k2=v2;",
176 "k3=v3# default",
177 ]
180def test_legacy_env_section_rejected(apio_runner: ApioRunner):
181 """A bare [env] section is rejected with a rename hint."""
183 error_tester(
184 env_arg=None,
185 apio_ini={
186 "[env]": {
187 "board": "alhambra-ii",
188 "top-module": "main",
189 }
190 },
191 expected_error="Rename it to [env:default]",
192 apio_runner=apio_runner,
193 )
196def test_first_env_is_default(apio_runner: ApioRunner):
197 """Tests that with no --env and no default-env, the first env in
198 apio.ini is selected"""
200 project, _ = load_apio_ini(
201 apio_ini={
202 "[common]": {
203 "default-testbench": "main_tb.v",
204 },
205 "[env:env1]": {
206 "board": "alhambra-ii",
207 "top-module": "module1",
208 },
209 "[env:env2]": {
210 "board": "ice40-hx8k",
211 "top-module": "module2",
212 },
213 },
214 env_arg=None,
215 apio_runner=apio_runner,
216 )
218 assert project.env_name == "env1"
219 assert project.env_options == {
220 "default-testbench": "main_tb.v",
221 "board": "alhambra-ii",
222 "top-module": "module1",
223 }
226def test_env_selection_from_apio_ini(apio_runner: ApioRunner):
227 """Tests that with no --env, and with default env defined in apio.ini
228 using default-env option."""
230 project, _ = load_apio_ini(
231 apio_ini={
232 "[apio]": {
233 "default-env": "env2",
234 },
235 "[common]": {
236 "default-testbench": "main_tb.v",
237 },
238 "[env:env1]": {
239 "board": "alhambra-ii",
240 "top-module": "module1",
241 },
242 "[env:env2]": {
243 "board": "ice40-hx8k",
244 "top-module": "module2",
245 },
246 },
247 env_arg=None,
248 apio_runner=apio_runner,
249 )
251 assert project.env_name == "env2"
252 assert project.env_options == {
253 "default-testbench": "main_tb.v",
254 "board": "ice40-hx8k",
255 "top-module": "module2",
256 }
259def test_env_selection_from_env_arg(apio_runner: ApioRunner):
260 """Tests that with --env overriding default-env in apio.ini."""
262 project, _ = load_apio_ini(
263 apio_ini={
264 "[apio]": {
265 "default-env": "env1",
266 },
267 "[common]": {
268 "default-testbench": "main_tb.v",
269 },
270 "[env:env1]": {
271 "board": "alhambra-ii",
272 "top-module": "module1",
273 },
274 "[env:env2]": {
275 "board": "ice40-hx8k",
276 "top-module": "module2",
277 },
278 },
279 env_arg="env2", # --env env2
280 apio_runner=apio_runner,
281 )
283 assert project.env_name == "env2"
284 assert project.env_options == {
285 "default-testbench": "main_tb.v",
286 "board": "ice40-hx8k",
287 "top-module": "module2",
288 }
291def error_tester(
292 env_arg: str | None,
293 apio_ini: dict[str, dict[str, str]],
294 expected_error: str,
295 apio_runner: ApioRunner,
296):
297 """A helper function to tests apio.ini content that is expected to
298 exit with an error."""
300 with apio_runner.in_sandbox() as sb:
301 # -- Create the apio.ini file
302 sb.write_apio_ini(apio_ini)
304 # -- Try to create the context with the project info.
305 with apio_runner.with_logger() as log:
306 with pytest.raises(SystemExit) as e:
307 ApioContext(
308 project_policy=ProjectPolicy.PROJECT_REQUIRED,
309 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
310 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
311 env_arg=env_arg,
312 )
314 # -- Check the errors.
315 assert e.value.code == 1
316 assert expected_error in log.out
319def test_validation_errors(apio_runner: ApioRunner):
320 """Tests the validation of apio.ini errors."""
322 # -- No [env:name] section.
323 error_tester(
324 env_arg=None,
325 apio_ini={
326 "[common]": {
327 "board": "alhambra-ii",
328 "top-module": "main",
329 }
330 },
331 expected_error=(
332 "Error: Project file 'apio.ini' should have at "
333 "least one [env:name] section."
334 ),
335 apio_runner=apio_runner,
336 )
338 # -- Unknown board id.
339 error_tester(
340 env_arg=None,
341 apio_ini={
342 "[env:default]": {
343 "board": "no-such-board",
344 }
345 },
346 expected_error="Error: Unknown board id 'no-such-board' in apio.ini",
347 apio_runner=apio_runner,
348 )
350 # -- Env name has an invalid char (Uppercase).
351 error_tester(
352 env_arg=None,
353 apio_ini={
354 "[env:Default]": {
355 "board": "alhambra-ii",
356 "top-module": "main",
357 }
358 },
359 expected_error="Error: Invalid env name 'Default'",
360 apio_runner=apio_runner,
361 )
363 # -- Env name has an extra space.
364 error_tester(
365 env_arg=None,
366 apio_ini={
367 "[env: default]": {
368 "board": "alhambra-ii",
369 "top-module": "main",
370 }
371 },
372 expected_error="Error: Invalid env name ' default'",
373 apio_runner=apio_runner,
374 )
376 # -- default-env points to a non existing env.
377 error_tester(
378 env_arg=None,
379 apio_ini={
380 "[apio]": {
381 "default-env": "no-such-env",
382 },
383 "[env:default]": {
384 "board": "alhambra-ii",
385 "top-module": "main",
386 },
387 },
388 expected_error="Error: Env 'no-such-env' not found in apio.ini",
389 apio_runner=apio_runner,
390 )
392 # -- Missing required option.
393 error_tester(
394 env_arg=None,
395 apio_ini={
396 "[env:default]": {"top-module": "main"},
397 },
398 expected_error="Error: Missing required option 'board' for "
399 "env 'default'.",
400 apio_runner=apio_runner,
401 )
403 # -- env_arg has a non existing env name.
404 error_tester(
405 env_arg="no-such-env",
406 apio_ini={
407 "[env:default]": {
408 "board": "alhambra-ii",
409 "top-module": "main",
410 },
411 },
412 expected_error="Error: Env 'no-such-env' not found in apio.ini",
413 apio_runner=apio_runner,
414 )
416 # -- env_arg has an env name with an invalid char (upper case).
417 error_tester(
418 env_arg="Default",
419 apio_ini={
420 "[env:default]": {
421 "board": "alhambra-ii",
422 "top-module": "main",
423 },
424 },
425 expected_error="Error: Invalid --env value 'Default'",
426 apio_runner=apio_runner,
427 )