Coverage for tests / unit_tests / managers / test_project.py: 100%
77 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-26 02:38 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-26 02:38 +0000
1"""
2Tests of project.py
3"""
5from typing import Dict, Optional, Tuple
6from pytest import LogCaptureFixture
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: LogCaptureFixture,
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: LogCaptureFixture):
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: LogCaptureFixture
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: LogCaptureFixture):
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: LogCaptureFixture):
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: LogCaptureFixture):
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: LogCaptureFixture):
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(
238 apio_runner: ApioRunner, capsys: LogCaptureFixture
239):
240 """Tests that with no --env and no default-env, the first env in
241 apio.ini is selected"""
243 project, _ = load_apio_ini(
244 apio_ini={
245 "[common]": {
246 "default-testbench": "main_tb.v",
247 },
248 "[env:env1]": {
249 "board": "alhambra-ii",
250 "top-module": "module1",
251 },
252 "[env:env2]": {
253 "board": "ice40-hx8k",
254 "top-module": "module2",
255 },
256 },
257 env_arg=None,
258 apio_runner=apio_runner,
259 capsys=capsys,
260 )
262 assert project.env_name == "env1"
263 assert project.env_options == {
264 "default-testbench": "main_tb.v",
265 "board": "alhambra-ii",
266 "top-module": "module1",
267 }
270def test_env_selection_from_apio_ini(
271 apio_runner: ApioRunner, capsys: LogCaptureFixture
272):
273 """Tests that with no --env, and with default env defined in apio.ini
274 using default-env option."""
276 project, _ = load_apio_ini(
277 apio_ini={
278 "[apio]": {
279 "default-env": "env2",
280 },
281 "[common]": {
282 "default-testbench": "main_tb.v",
283 },
284 "[env:env1]": {
285 "board": "alhambra-ii",
286 "top-module": "module1",
287 },
288 "[env:env2]": {
289 "board": "ice40-hx8k",
290 "top-module": "module2",
291 },
292 },
293 env_arg=None,
294 apio_runner=apio_runner,
295 capsys=capsys,
296 )
298 assert project.env_name == "env2"
299 assert project.env_options == {
300 "default-testbench": "main_tb.v",
301 "board": "ice40-hx8k",
302 "top-module": "module2",
303 }
306def test_env_selection_from_env_arg(
307 apio_runner: ApioRunner, capsys: LogCaptureFixture
308):
309 """Tests that with --env overriding default-env in apio.ini."""
311 project, _ = load_apio_ini(
312 apio_ini={
313 "[apio]": {
314 "default-env": "env1",
315 },
316 "[common]": {
317 "default-testbench": "main_tb.v",
318 },
319 "[env:env1]": {
320 "board": "alhambra-ii",
321 "top-module": "module1",
322 },
323 "[env:env2]": {
324 "board": "ice40-hx8k",
325 "top-module": "module2",
326 },
327 },
328 env_arg="env2", # --env env2
329 apio_runner=apio_runner,
330 capsys=capsys,
331 )
333 assert project.env_name == "env2"
334 assert project.env_options == {
335 "default-testbench": "main_tb.v",
336 "board": "ice40-hx8k",
337 "top-module": "module2",
338 }
341def error_tester(
342 env_arg: Optional[str],
343 apio_ini: Dict[str, Dict[str, str]],
344 expected_error: str,
345 apio_runner: ApioRunner,
346 capsys: LogCaptureFixture,
347):
348 """A helper function to tests apio.ini content that is expected to
349 exit with an error."""
351 with apio_runner.in_sandbox() as sb:
352 # -- Create the apio.ini file
353 sb.write_apio_ini(apio_ini)
355 # -- Try to create the context with the project info.
356 capsys.readouterr() # Reset capture
357 with pytest.raises(SystemExit) as e:
358 ApioContext(
359 project_policy=ProjectPolicy.PROJECT_REQUIRED,
360 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
361 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
362 env_arg=env_arg,
363 )
365 # -- Check the errors.
366 capture = cunstyle(capsys.readouterr().out)
367 assert e.value.code == 1
368 assert expected_error in capture
371def test_validation_errors(apio_runner: ApioRunner, capsys: LogCaptureFixture):
372 """Tests the validation of apio.ini errors."""
374 # -- No [env:name] section.
375 error_tester(
376 env_arg=None,
377 apio_ini={
378 "[common]": {
379 "board": "alhambra-ii",
380 "top-module": "main",
381 }
382 },
383 expected_error=(
384 "Error: Project file 'apio.ini' should have at "
385 "least one [env:name] section."
386 ),
387 apio_runner=apio_runner,
388 capsys=capsys,
389 )
391 # -- Unknown board id.
392 error_tester(
393 env_arg=None,
394 apio_ini={
395 "[env:default]": {
396 "board": "no-such-board",
397 }
398 },
399 expected_error="Error: Unknown board id 'no-such-board' in apio.ini",
400 apio_runner=apio_runner,
401 capsys=capsys,
402 )
404 # -- Env name has an invalid char (Uppercase).
405 error_tester(
406 env_arg=None,
407 apio_ini={
408 "[env:Default]": {
409 "board": "alhambra-ii",
410 "top-module": "main",
411 }
412 },
413 expected_error="Error: Invalid env name 'Default'",
414 apio_runner=apio_runner,
415 capsys=capsys,
416 )
418 # -- Env name has an extra space.
419 error_tester(
420 env_arg=None,
421 apio_ini={
422 "[env: default]": {
423 "board": "alhambra-ii",
424 "top-module": "main",
425 }
426 },
427 expected_error="Error: Invalid env name ' default'",
428 apio_runner=apio_runner,
429 capsys=capsys,
430 )
432 # -- default-env points to a non existing env.
433 error_tester(
434 env_arg=None,
435 apio_ini={
436 "[apio]": {
437 "default-env": "no-such-env",
438 },
439 "[env:default]": {
440 "board": "alhambra-ii",
441 "top-module": "main",
442 },
443 },
444 expected_error="Error: Env 'no-such-env' not found in apio.ini",
445 apio_runner=apio_runner,
446 capsys=capsys,
447 )
449 # -- Missing required option.
450 error_tester(
451 env_arg=None,
452 apio_ini={
453 "[env:default]": {"top-module": "main"},
454 },
455 expected_error="Error: Missing required option 'board' for "
456 "env 'default'.",
457 apio_runner=apio_runner,
458 capsys=capsys,
459 )
461 # -- env_arg has a non existing env name.
462 error_tester(
463 env_arg="no-such-env",
464 apio_ini={
465 "[env:default]": {
466 "board": "alhambra-ii",
467 "top-module": "main",
468 },
469 },
470 expected_error="Error: Env 'no-such-env' not found in apio.ini",
471 apio_runner=apio_runner,
472 capsys=capsys,
473 )
475 # -- env_arg has an env name with an invalid char (upper case).
476 error_tester(
477 env_arg="Default",
478 apio_ini={
479 "[env:default]": {
480 "board": "alhambra-ii",
481 "top-module": "main",
482 },
483 },
484 expected_error="Error: Invalid --env value 'Default'",
485 apio_runner=apio_runner,
486 capsys=capsys,
487 )