Coverage for tests/unit_tests/managers/test_project.py: 100%
77 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +0000
1"""
2Tests of project.py
3"""
5from typing import Dict, Optional, Tuple
6from _pytest.capture import CaptureFixture
7import pytest
8from tests.conftest import ApioRunner
9from apio.managers.project import Project, ENV_OPTIONS_SPEC
10from apio.common.apio_console import cunstyle
11from apio.apio_context import (
12 ApioContext,
13 PackagesPolicy,
14 ProjectPolicy,
15 RemoteConfigPolicy,
16)
18# TODO: Add more tests.
21def load_apio_ini(
22 apio_ini: Dict[str, Dict[str, str]],
23 env_arg: Optional[str],
24 apio_runner: ApioRunner,
25 capsys: CaptureFixture[str],
26) -> Tuple[Project, str]:
27 """A helper function load apio.ini. Returns (project, stdout)"""
29 with apio_runner.in_sandbox() as sb:
30 # -- Create the apio.ini file
31 sb.write_apio_ini(apio_ini)
33 # -- Try to create the context with the project info.
34 capsys.readouterr() # Reset capture
35 apio_ctx = ApioContext(
36 project_policy=ProjectPolicy.PROJECT_REQUIRED,
37 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
38 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
39 env_arg=env_arg,
40 )
42 # -- Return the values.
43 return (
44 apio_ctx.project,
45 cunstyle(capsys.readouterr().out),
46 )
49def test_env_options_specs():
50 """Tests the option spec keys match options specs names"""
52 for name, env_options_spec in ENV_OPTIONS_SPEC.items():
53 assert name == env_options_spec.name
56def test_all_options_env(apio_runner: ApioRunner, capsys: CaptureFixture[str]):
57 """Tests an apio.ini with all options"""
59 apio_ini = {
60 "[env:default]": {
61 "board": "alhambra-ii", # required.
62 "default-testbench": "main_tb.v",
63 "defines": "\n aaa=111\n bbb=222",
64 "format-verible-options": "\n --aaa bbb\n --ccc ddd",
65 "programmer-cmd": "iceprog ${VID}:${PID}",
66 "top-module": "my_module",
67 "yosys-extra-options": "-dsp -xyz",
68 "nextpnr-extra-options": "--freq 13",
69 "gtkwave-extra-options": "--rcvar=do_initial_zoom_fit 1",
70 "verilator-extra-options": "-Wno-fatal",
71 "constraint-file": "pinout.lpf",
72 }
73 }
75 # -- Make sure we covered all the options.
76 assert len(apio_ini["[env:default]"]) == len(ENV_OPTIONS_SPEC)
78 project, _ = load_apio_ini(
79 apio_ini=apio_ini,
80 env_arg=None,
81 apio_runner=apio_runner,
82 capsys=capsys,
83 )
85 assert project.env_name == "default"
86 assert project.env_options == {
87 "board": "alhambra-ii",
88 "default-testbench": "main_tb.v",
89 "defines": ["aaa=111", "bbb=222"],
90 "format-verible-options": ["--aaa bbb", "--ccc ddd"],
91 "programmer-cmd": "iceprog ${VID}:${PID}",
92 "top-module": "my_module",
93 "yosys-extra-options": ["-dsp -xyz"],
94 "nextpnr-extra-options": ["--freq 13"],
95 "gtkwave-extra-options": ["--rcvar=do_initial_zoom_fit 1"],
96 "verilator-extra-options": ["-Wno-fatal"],
97 "constraint-file": "pinout.lpf",
98 }
100 # -- Try a few as dict lookup on the project object.
101 assert project.get_str_option("board") == "alhambra-ii"
102 assert project.get_str_option("top-module") == "my_module"
103 assert project.get_str_option("constraint-file") == "pinout.lpf"
106def test_required_options_only_env(
107 apio_runner: ApioRunner, capsys: CaptureFixture
108):
109 """Tests a minimal apio.ini with required only options."""
111 project, _ = load_apio_ini(
112 apio_ini={
113 "[env:default]": {
114 "board": "alhambra-ii",
115 "top-module": "main",
116 }
117 },
118 env_arg=None,
119 apio_runner=apio_runner,
120 capsys=capsys,
121 )
123 assert project.env_name == "default"
124 assert project.env_options == {
125 "board": "alhambra-ii",
126 "top-module": "main",
127 }
130def test_list_options(apio_runner: ApioRunner, capsys: CaptureFixture):
131 """Tests list options."""
133 project, _ = load_apio_ini(
134 apio_ini={
135 "[env:default]": {
136 "board": "alhambra-ii",
137 "top-module": "my_top_module",
138 "yosys-extra-options": " k1=v1 k2=v2 \n\n k3=v3 \n\n",
139 "nextpnr-extra-options": " k5=v5 k6=v6 \n\n k7=v7 \n\n",
140 }
141 },
142 env_arg=None,
143 apio_runner=apio_runner,
144 capsys=capsys,
145 )
147 assert project.get_list_option("yosys-extra-options") == [
148 "k1=v1 k2=v2",
149 "k3=v3",
150 ]
152 assert project.get_list_option("nextpnr-extra-options") == [
153 "k5=v5 k6=v6",
154 "k7=v7",
155 ]
158def test_macro_expansion(apio_runner: ApioRunner, capsys: CaptureFixture):
159 """Tests the expansion of macros within values."""
161 project, _ = load_apio_ini(
162 apio_ini={
163 "[env:default]": {
164 "board": "alhambra-ii",
165 "top-module": "my_top_module",
166 "constraint-file": " ${SEMICOLON} value ",
167 "yosys-extra-options": " k1=${ENV_BUILD}/v1 k2=v2; \n ; "
168 "Comment \n k3=v3${HASH} ${ENV_NAME}\n\n",
169 }
170 },
171 env_arg=None,
172 apio_runner=apio_runner,
173 capsys=capsys,
174 )
176 assert project.get_str_option("constraint-file") == "; value"
178 assert project.get_list_option("yosys-extra-options") == [
179 "k1=_build/default/v1 k2=v2;",
180 "k3=v3# default",
181 ]
184def test_legacy_board_id(apio_runner: ApioRunner, capsys: CaptureFixture):
185 """Tests with 'board' option having a legacy board id. It should
186 be converted to the canonical board id"""
188 project, stdout = load_apio_ini(
189 apio_ini={
190 "[env:default]": {
191 "board": "iCE40-HX8K",
192 "top-module": "my_top_module",
193 }
194 },
195 env_arg=None,
196 apio_runner=apio_runner,
197 capsys=capsys,
198 )
200 assert project.env_name == "default"
201 assert project.env_options == {
202 "board": "ice40-hx8k",
203 "top-module": "my_top_module",
204 }
205 assert (
206 "Warning: 'Board iCE40-HX8K' was renamed to 'ice40-hx8k'. "
207 "Please update apio.ini" in stdout
208 )
211def test_legacy_apio_ini(apio_runner: ApioRunner, capsys: CaptureFixture):
212 """Tests with an old style apio.ini that has a single [env] section."""
214 project, stdout = load_apio_ini(
215 apio_ini={
216 "[env]": {
217 "board": "alhambra-ii",
218 "top-module": "main",
219 }
220 },
221 env_arg=None,
222 apio_runner=apio_runner,
223 capsys=capsys,
224 )
226 assert project.env_name == "default"
227 assert project.env_options == {
228 "board": "alhambra-ii",
229 "top-module": "main",
230 }
231 assert (
232 "Warning: Apio.ini has a legacy [env] section. "
233 "Please rename it to [env:default]" in stdout
234 )
237def test_first_env_is_default(apio_runner: ApioRunner, capsys: CaptureFixture):
238 """Tests that with no --env and no default-env, the first env in
239 apio.ini is selected"""
241 project, _ = load_apio_ini(
242 apio_ini={
243 "[common]": {
244 "default-testbench": "main_tb.v",
245 },
246 "[env:env1]": {
247 "board": "alhambra-ii",
248 "top-module": "module1",
249 },
250 "[env:env2]": {
251 "board": "ice40-hx8k",
252 "top-module": "module2",
253 },
254 },
255 env_arg=None,
256 apio_runner=apio_runner,
257 capsys=capsys,
258 )
260 assert project.env_name == "env1"
261 assert project.env_options == {
262 "default-testbench": "main_tb.v",
263 "board": "alhambra-ii",
264 "top-module": "module1",
265 }
268def test_env_selection_from_apio_ini(
269 apio_runner: ApioRunner, capsys: CaptureFixture
270):
271 """Tests that with no --env, and with default env defined in apio.ini
272 using default-env option."""
274 project, _ = load_apio_ini(
275 apio_ini={
276 "[apio]": {
277 "default-env": "env2",
278 },
279 "[common]": {
280 "default-testbench": "main_tb.v",
281 },
282 "[env:env1]": {
283 "board": "alhambra-ii",
284 "top-module": "module1",
285 },
286 "[env:env2]": {
287 "board": "ice40-hx8k",
288 "top-module": "module2",
289 },
290 },
291 env_arg=None,
292 apio_runner=apio_runner,
293 capsys=capsys,
294 )
296 assert project.env_name == "env2"
297 assert project.env_options == {
298 "default-testbench": "main_tb.v",
299 "board": "ice40-hx8k",
300 "top-module": "module2",
301 }
304def test_env_selection_from_env_arg(
305 apio_runner: ApioRunner, capsys: CaptureFixture
306):
307 """Tests that with --env overriding default-env in apio.ini."""
309 project, _ = load_apio_ini(
310 apio_ini={
311 "[apio]": {
312 "default-env": "env1",
313 },
314 "[common]": {
315 "default-testbench": "main_tb.v",
316 },
317 "[env:env1]": {
318 "board": "alhambra-ii",
319 "top-module": "module1",
320 },
321 "[env:env2]": {
322 "board": "ice40-hx8k",
323 "top-module": "module2",
324 },
325 },
326 env_arg="env2", # --env env2
327 apio_runner=apio_runner,
328 capsys=capsys,
329 )
331 assert project.env_name == "env2"
332 assert project.env_options == {
333 "default-testbench": "main_tb.v",
334 "board": "ice40-hx8k",
335 "top-module": "module2",
336 }
339def error_tester(
340 env_arg: Optional[str],
341 apio_ini: Dict[str, Dict[str, str]],
342 expected_error: str,
343 apio_runner: ApioRunner,
344 capsys: CaptureFixture,
345):
346 """A helper function to tests apio.ini content that is expected to
347 exit with an error."""
349 with apio_runner.in_sandbox() as sb:
350 # -- Create the apio.ini file
351 sb.write_apio_ini(apio_ini)
353 # -- Try to create the context with the project info.
354 capsys.readouterr() # Reset capture
355 with pytest.raises(SystemExit) as e:
356 ApioContext(
357 project_policy=ProjectPolicy.PROJECT_REQUIRED,
358 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
359 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
360 env_arg=env_arg,
361 )
363 # -- Check the errors.
364 capture = cunstyle(capsys.readouterr().out)
365 assert e.value.code == 1
366 assert expected_error in capture
369def test_validation_errors(apio_runner: ApioRunner, capsys: CaptureFixture):
370 """Tests the validation of apio.ini errors."""
372 # -- No [env:name] section.
373 error_tester(
374 env_arg=None,
375 apio_ini={
376 "[common]": {
377 "board": "alhambra-ii",
378 "top-module": "main",
379 }
380 },
381 expected_error=(
382 "Error: Project file 'apio.ini' should have at "
383 "least one [env:name] section."
384 ),
385 apio_runner=apio_runner,
386 capsys=capsys,
387 )
389 # -- Unknown board id.
390 error_tester(
391 env_arg=None,
392 apio_ini={
393 "[env:default]": {
394 "board": "no-such-board",
395 }
396 },
397 expected_error="Error: Unknown board id 'no-such-board' in apio.ini",
398 apio_runner=apio_runner,
399 capsys=capsys,
400 )
402 # -- Env name has an invalid char (Uppercase).
403 error_tester(
404 env_arg=None,
405 apio_ini={
406 "[env:Default]": {
407 "board": "alhambra-ii",
408 "top-module": "main",
409 }
410 },
411 expected_error="Error: Invalid env name 'Default'",
412 apio_runner=apio_runner,
413 capsys=capsys,
414 )
416 # -- Env name has an extra space.
417 error_tester(
418 env_arg=None,
419 apio_ini={
420 "[env: default]": {
421 "board": "alhambra-ii",
422 "top-module": "main",
423 }
424 },
425 expected_error="Error: Invalid env name ' default'",
426 apio_runner=apio_runner,
427 capsys=capsys,
428 )
430 # -- default-env points to a non existing env.
431 error_tester(
432 env_arg=None,
433 apio_ini={
434 "[apio]": {
435 "default-env": "no-such-env",
436 },
437 "[env:default]": {
438 "board": "alhambra-ii",
439 "top-module": "main",
440 },
441 },
442 expected_error="Error: Env 'no-such-env' not found in apio.ini",
443 apio_runner=apio_runner,
444 capsys=capsys,
445 )
447 # -- Missing required option.
448 error_tester(
449 env_arg=None,
450 apio_ini={
451 "[env:default]": {"top-module": "main"},
452 },
453 expected_error="Error: Missing required option 'board' for "
454 "env 'default'.",
455 apio_runner=apio_runner,
456 capsys=capsys,
457 )
459 # -- env_arg has a non existing env name.
460 error_tester(
461 env_arg="no-such-env",
462 apio_ini={
463 "[env:default]": {
464 "board": "alhambra-ii",
465 "top-module": "main",
466 },
467 },
468 expected_error="Error: Env 'no-such-env' not found in apio.ini",
469 apio_runner=apio_runner,
470 capsys=capsys,
471 )
473 # -- env_arg has an env name with an invalid char (upper case).
474 error_tester(
475 env_arg="Default",
476 apio_ini={
477 "[env:default]": {
478 "board": "alhambra-ii",
479 "top-module": "main",
480 },
481 },
482 expected_error="Error: Invalid --env value 'Default'",
483 apio_runner=apio_runner,
484 capsys=capsys,
485 )