Coverage for tests/unit_tests/managers/test_project.py: 100%
65 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
1"""
2Tests of project.py
3"""
5import pytest
6from tests.conftest import ApioRunner
7from apio.managers.project import Project, ENV_OPTIONS_SPEC
8from apio.apio_context import (
9 ApioContext,
10 PackagesPolicy,
11 ProjectPolicy,
12 RemoteConfigPolicy,
13)
15# TODO: Add more tests.
18def load_apio_ini(
19 apio_ini: dict[str, dict[str, str]],
20 env_arg: str | None,
21 apio_runner: ApioRunner,
22) -> tuple[Project, str]:
23 """A helper function load apio.ini. Returns (project, log-text)"""
25 with apio_runner.in_sandbox() as sb:
26 # -- Create the apio.ini file
27 sb.write_apio_ini(apio_ini)
29 # -- Try to create the context with the project info while
30 # -- capturing log.
31 with apio_runner.with_logger() as log:
32 apio_ctx = ApioContext(
33 project_policy=ProjectPolicy.PROJECT_REQUIRED,
34 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
35 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
36 env_arg=env_arg,
37 )
39 # -- Return the values.
40 return (apio_ctx.project, log.out)
43def test_env_options_specs():
44 """Tests the option spec keys match options specs names"""
46 for name, env_options_spec in ENV_OPTIONS_SPEC.items():
47 assert name == env_options_spec.name
50def test_all_options_env(apio_runner: ApioRunner):
51 """Tests an apio.ini with all options"""
53 apio_ini = {
54 "[env:default]": {
55 "board": "alhambra-ii", # required.
56 "default-testbench": "main_tb.v",
57 "defines": "\n aaa=111\n bbb=222",
58 "format-verible-options": "\n --aaa bbb\n --ccc ddd",
59 "programmer-cmd": "iceprog ${VID}:${PID}",
60 "top-module": "my_module",
61 "yosys-extra-options": "-dsp -xyz",
62 "nextpnr-extra-options": "--freq 13",
63 "gtkwave-extra-options": "--rcvar=do_initial_zoom_fit 1",
64 "verilator-extra-options": "-Wno-fatal",
65 "constraint-file": "pinout.lpf",
66 }
67 }
69 # -- Make sure we covered all the options.
70 assert len(apio_ini["[env:default]"]) == len(ENV_OPTIONS_SPEC)
72 project, _ = load_apio_ini(
73 apio_ini=apio_ini,
74 env_arg=None,
75 apio_runner=apio_runner,
76 )
78 assert project.env_name == "default"
79 assert project.env_options == {
80 "board": "alhambra-ii",
81 "default-testbench": "main_tb.v",
82 "defines": ["aaa=111", "bbb=222"],
83 "format-verible-options": ["--aaa bbb", "--ccc ddd"],
84 "programmer-cmd": "iceprog ${VID}:${PID}",
85 "top-module": "my_module",
86 "yosys-extra-options": ["-dsp -xyz"],
87 "nextpnr-extra-options": ["--freq 13"],
88 "gtkwave-extra-options": ["--rcvar=do_initial_zoom_fit 1"],
89 "verilator-extra-options": ["-Wno-fatal"],
90 "constraint-file": "pinout.lpf",
91 }
93 # -- Try a few as dict lookup on the project object.
94 assert project.get_str_option("board") == "alhambra-ii"
95 assert project.get_str_option("top-module") == "my_module"
96 assert project.get_str_option("constraint-file") == "pinout.lpf"
99def test_required_options_only_env(apio_runner: ApioRunner):
100 """Tests a minimal apio.ini with required only options."""
102 project, _ = load_apio_ini(
103 apio_ini={
104 "[env:default]": {
105 "board": "alhambra-ii",
106 "top-module": "main",
107 }
108 },
109 env_arg=None,
110 apio_runner=apio_runner,
111 )
113 assert project.env_name == "default"
114 assert project.env_options == {
115 "board": "alhambra-ii",
116 "top-module": "main",
117 }
120def test_list_options(apio_runner: ApioRunner):
121 """Tests list options."""
123 project, _ = load_apio_ini(
124 apio_ini={
125 "[env:default]": {
126 "board": "alhambra-ii",
127 "top-module": "my_top_module",
128 "yosys-extra-options": " k1=v1 k2=v2 \n\n k3=v3 \n\n",
129 "nextpnr-extra-options": " k5=v5 k6=v6 \n\n k7=v7 \n\n",
130 }
131 },
132 env_arg=None,
133 apio_runner=apio_runner,
134 )
136 assert project.get_list_option("yosys-extra-options") == [
137 "k1=v1 k2=v2",
138 "k3=v3",
139 ]
141 assert project.get_list_option("nextpnr-extra-options") == [
142 "k5=v5 k6=v6",
143 "k7=v7",
144 ]
147def test_macro_expansion(apio_runner: ApioRunner):
148 """Tests the expansion of macros within values."""
150 project, _ = load_apio_ini(
151 apio_ini={
152 "[env:default]": {
153 "board": "alhambra-ii",
154 "top-module": "my_top_module",
155 "constraint-file": " ${SEMICOLON} value ",
156 "yosys-extra-options": " k1=${ENV_BUILD}/v1 k2=v2; \n ; "
157 "Comment \n k3=v3${HASH} ${ENV_NAME}\n\n",
158 }
159 },
160 env_arg=None,
161 apio_runner=apio_runner,
162 )
164 assert project.get_str_option("constraint-file") == "; value"
166 assert project.get_list_option("yosys-extra-options") == [
167 "k1=_build/default/v1 k2=v2;",
168 "k3=v3# default",
169 ]
172def test_legacy_env_section_rejected(apio_runner: ApioRunner):
173 """A bare [env] section is rejected with a rename hint."""
175 error_tester(
176 env_arg=None,
177 apio_ini={
178 "[env]": {
179 "board": "alhambra-ii",
180 "top-module": "main",
181 }
182 },
183 expected_error="Rename it to [env:default]",
184 apio_runner=apio_runner,
185 )
188def test_first_env_is_default(apio_runner: ApioRunner):
189 """Tests that with no --env and no default-env, the first env in
190 apio.ini is selected"""
192 project, _ = load_apio_ini(
193 apio_ini={
194 "[common]": {
195 "default-testbench": "main_tb.v",
196 },
197 "[env:env1]": {
198 "board": "alhambra-ii",
199 "top-module": "module1",
200 },
201 "[env:env2]": {
202 "board": "ice40-hx8k",
203 "top-module": "module2",
204 },
205 },
206 env_arg=None,
207 apio_runner=apio_runner,
208 )
210 assert project.env_name == "env1"
211 assert project.env_options == {
212 "default-testbench": "main_tb.v",
213 "board": "alhambra-ii",
214 "top-module": "module1",
215 }
218def test_env_selection_from_apio_ini(apio_runner: ApioRunner):
219 """Tests that with no --env, and with default env defined in apio.ini
220 using default-env option."""
222 project, _ = load_apio_ini(
223 apio_ini={
224 "[apio]": {
225 "default-env": "env2",
226 },
227 "[common]": {
228 "default-testbench": "main_tb.v",
229 },
230 "[env:env1]": {
231 "board": "alhambra-ii",
232 "top-module": "module1",
233 },
234 "[env:env2]": {
235 "board": "ice40-hx8k",
236 "top-module": "module2",
237 },
238 },
239 env_arg=None,
240 apio_runner=apio_runner,
241 )
243 assert project.env_name == "env2"
244 assert project.env_options == {
245 "default-testbench": "main_tb.v",
246 "board": "ice40-hx8k",
247 "top-module": "module2",
248 }
251def test_env_selection_from_env_arg(apio_runner: ApioRunner):
252 """Tests that with --env overriding default-env in apio.ini."""
254 project, _ = load_apio_ini(
255 apio_ini={
256 "[apio]": {
257 "default-env": "env1",
258 },
259 "[common]": {
260 "default-testbench": "main_tb.v",
261 },
262 "[env:env1]": {
263 "board": "alhambra-ii",
264 "top-module": "module1",
265 },
266 "[env:env2]": {
267 "board": "ice40-hx8k",
268 "top-module": "module2",
269 },
270 },
271 env_arg="env2", # --env env2
272 apio_runner=apio_runner,
273 )
275 assert project.env_name == "env2"
276 assert project.env_options == {
277 "default-testbench": "main_tb.v",
278 "board": "ice40-hx8k",
279 "top-module": "module2",
280 }
283def error_tester(
284 env_arg: str | None,
285 apio_ini: dict[str, dict[str, str]],
286 expected_error: str,
287 apio_runner: ApioRunner,
288):
289 """A helper function to tests apio.ini content that is expected to
290 exit with an error."""
292 with apio_runner.in_sandbox() as sb:
293 # -- Create the apio.ini file
294 sb.write_apio_ini(apio_ini)
296 # -- Try to create the context with the project info.
297 with apio_runner.with_logger() as log:
298 with pytest.raises(SystemExit) as e:
299 ApioContext(
300 project_policy=ProjectPolicy.PROJECT_REQUIRED,
301 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
302 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
303 env_arg=env_arg,
304 )
306 # -- Check the errors.
307 assert e.value.code == 1
308 assert expected_error in log.out
311def test_validation_errors(apio_runner: ApioRunner):
312 """Tests the validation of apio.ini errors."""
314 # -- No [env:name] section.
315 error_tester(
316 env_arg=None,
317 apio_ini={
318 "[common]": {
319 "board": "alhambra-ii",
320 "top-module": "main",
321 }
322 },
323 expected_error=(
324 "Error: Project file 'apio.ini' should have at "
325 "least one [env:name] section."
326 ),
327 apio_runner=apio_runner,
328 )
330 # -- Unknown board id.
331 error_tester(
332 env_arg=None,
333 apio_ini={
334 "[env:default]": {
335 "board": "no-such-board",
336 }
337 },
338 expected_error="Error: Unknown board id 'no-such-board' in apio.ini",
339 apio_runner=apio_runner,
340 )
342 # -- Env name has an invalid char (Uppercase).
343 error_tester(
344 env_arg=None,
345 apio_ini={
346 "[env:Default]": {
347 "board": "alhambra-ii",
348 "top-module": "main",
349 }
350 },
351 expected_error="Error: Invalid env name 'Default'",
352 apio_runner=apio_runner,
353 )
355 # -- Env name has an extra space.
356 error_tester(
357 env_arg=None,
358 apio_ini={
359 "[env: default]": {
360 "board": "alhambra-ii",
361 "top-module": "main",
362 }
363 },
364 expected_error="Error: Invalid env name ' default'",
365 apio_runner=apio_runner,
366 )
368 # -- default-env points to a non existing env.
369 error_tester(
370 env_arg=None,
371 apio_ini={
372 "[apio]": {
373 "default-env": "no-such-env",
374 },
375 "[env:default]": {
376 "board": "alhambra-ii",
377 "top-module": "main",
378 },
379 },
380 expected_error="Error: Env 'no-such-env' not found in apio.ini",
381 apio_runner=apio_runner,
382 )
384 # -- Missing required option.
385 error_tester(
386 env_arg=None,
387 apio_ini={
388 "[env:default]": {"top-module": "main"},
389 },
390 expected_error="Error: Missing required option 'board' for "
391 "env 'default'.",
392 apio_runner=apio_runner,
393 )
395 # -- env_arg has a non existing env name.
396 error_tester(
397 env_arg="no-such-env",
398 apio_ini={
399 "[env:default]": {
400 "board": "alhambra-ii",
401 "top-module": "main",
402 },
403 },
404 expected_error="Error: Env 'no-such-env' not found in apio.ini",
405 apio_runner=apio_runner,
406 )
408 # -- env_arg has an env name with an invalid char (upper case).
409 error_tester(
410 env_arg="Default",
411 apio_ini={
412 "[env:default]": {
413 "board": "alhambra-ii",
414 "top-module": "main",
415 },
416 },
417 expected_error="Error: Invalid --env value 'Default'",
418 apio_runner=apio_runner,
419 )