Coverage for tests/unit_tests/commands/test_apio_api.py: 100%
206 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"""Test for the "apio api" command."""
11# NOTE: 'apio api scan-devices' require apio packages (for libusb1 lib)
12# and thus, is tested in the integration tests.
14import json
15import os
16from pathlib import Path
17from tests.conftest import ApioRunner
18from apio.commands.apio import apio_top_cli as apio
19from apio.utils import env_options
21# TODO: Add more tests
24def test_apio_api_get_boards(apio_runner: ApioRunner):
25 """Test "apio api get-boards" """
27 with apio_runner.in_sandbox() as sb:
29 # -- Execute "apio api get-boards -t xyz" (stdout)
30 result = sb.invoke_apio_cmd(apio, ["api", "get-boards", "-t", "xyz"])
31 sb.assert_result_ok(result)
32 assert "xyz" in result.output
33 assert "alhambra-ii" in result.output
35 # -- Execute "apio api get-boards -t xyz -o <dir>" (file)
36 path = sb.proj_dir / "apio.json"
37 result = sb.invoke_apio_cmd(
38 apio, ["api", "get-boards", "-t", "xyz", "-o", str(path)]
39 )
40 sb.assert_result_ok(result)
42 # -- Read and verify the file.
43 text = sb.read_file_text(path)
44 data = json.loads(text)
45 assert data["timestamp"] == "xyz"
46 assert data["boards"]["alhambra-ii"] == {
47 "description": "Alhambra II",
48 "fpga": {
49 "id": "ice40hx4k-tq144-8k",
50 "definition": {
51 "arch": "ice40",
52 "vendor-part": "ICE40HX4K-TQ144",
53 "size": "8k",
54 "ice40-params": {
55 "nextpnr-type": "hx8k",
56 "nextpnr-package": "tq144:4k",
57 },
58 },
59 },
60 "programmer": {
61 "id": "openfpgaloader",
62 "definition": {
63 "command": "openFPGALoader",
64 "args": "--force-terminal-mode --verify",
65 },
66 },
67 }
70def test_apio_api_get_fpgas(apio_runner: ApioRunner):
71 """Test "apio api get-fpgas" """
73 with apio_runner.in_sandbox() as sb:
75 # -- Execute "apio api get-fpgas -t xyz" (stdout)
76 result = sb.invoke_apio_cmd(apio, ["api", "get-fpgas", "-t", "xyz"])
77 sb.assert_result_ok(result)
78 assert "xyz" in result.output
79 assert "ice40hx4k-tq144-8k" in result.output
81 # -- Execute "apio api get-fpgas -t xyz -o <dir>" (file)
82 path = sb.proj_dir / "apio.json"
83 result = sb.invoke_apio_cmd(
84 apio, ["api", "get-fpgas", "-t", "xyz", "-o", str(path)]
85 )
86 sb.assert_result_ok(result)
88 # -- Read and verify the file.
89 text = sb.read_file_text(path)
90 data = json.loads(text)
91 assert data["timestamp"] == "xyz"
92 assert data["fpgas"]["ice40hx4k-tq144-8k"] == {
93 "arch": "ice40",
94 "vendor-part": "ICE40HX4K-TQ144",
95 "size": "8k",
96 "ice40-params": {
97 "nextpnr-type": "hx8k",
98 "nextpnr-package": "tq144:4k",
99 },
100 }
103def test_apio_api_get_programmers(apio_runner: ApioRunner):
104 """Test "apio api get-programmers" """
106 with apio_runner.in_sandbox() as sb:
108 # -- Execute "apio api get-programmers -t xyz" (stdout)
109 result = sb.invoke_apio_cmd(
110 apio, ["api", "get-programmers", "-t", "xyz"]
111 )
112 sb.assert_result_ok(result)
113 assert "xyz" in result.output
114 assert "openfpgaloader" in result.output
116 # -- Execute "apio api get-programmers -t xyz -o <dir>" (file)
117 path = sb.proj_dir / "apio.json"
118 result = sb.invoke_apio_cmd(
119 apio, ["api", "get-programmers", "-t", "xyz", "-o", str(path)]
120 )
121 sb.assert_result_ok(result)
123 # -- Read and verify the file.
124 text = sb.read_file_text(path)
125 data = json.loads(text)
126 assert data["timestamp"] == "xyz"
127 assert data["programmers"]["openfpgaloader"] == {
128 "command": "openFPGALoader",
129 "args": "--force-terminal-mode --verify",
130 }
133def test_apio_api_get_commands(apio_runner: ApioRunner):
134 """Test "apio api get-commands" """
136 with apio_runner.in_sandbox() as sb:
138 # -- Execute "apio api get-commands -t xyz" (stdout)
139 result = sb.invoke_apio_cmd(apio, ["api", "get-commands", "-t", "xyz"])
140 sb.assert_result_ok(result)
141 assert "xyz" in result.output
142 assert '"apio"' in result.output
143 assert '"api"' in result.output
144 assert '"get-boards"' in result.output
146 # -- Execute "apio api get-commands -t xyz -o <dir>" (file)
147 path = sb.proj_dir / "apio.json"
148 result = sb.invoke_apio_cmd(
149 apio, ["api", "get-commands", "-t", "xyz", "-o", str(path)]
150 )
151 sb.assert_result_ok(result)
153 # -- Read and verify the file.
154 text = sb.read_file_text(path)
155 data = json.loads(text)
156 assert data["timestamp"] == "xyz"
157 assert (
158 data["commands"]["apio"]["commands"]["api"]["commands"][
159 "get-boards"
160 ]
161 == {}
162 )
165def test_apio_api_get_packages(apio_runner: ApioRunner):
166 """Test "apio api get-packages" """
168 with apio_runner.in_sandbox() as sb:
170 # -- Execute "apio api get-packages -t xyz" (stdout)
171 result = sb.invoke_apio_cmd(apio, ["api", "get-packages", "-t", "xyz"])
172 sb.assert_result_ok(result)
173 assert '"packages":' in result.output
174 assert '"oss-cad-suite":' in result.output
176 # -- Execute "apio api get-packages -t xyz -o <dir>" (file)
177 path = sb.proj_dir / "apio.json"
178 result = sb.invoke_apio_cmd(
179 apio, ["api", "get-packages", "-t", "xyz", "-o", str(path)]
180 )
181 sb.assert_result_ok(result)
183 # -- Read and verify the file.
184 text = sb.read_file_text(path)
185 data = json.loads(text)
186 assert data["timestamp"] == "xyz"
187 assert (
188 "yosys-release-tag"
189 in data["packages"]["oss-cad-suite"]["build-info"]
190 )
193def test_apio_api_get_config(apio_runner: ApioRunner):
194 """Test "apio api get-config" """
196 with apio_runner.in_sandbox() as sb:
198 # -- Execute "apio api get-config -t xyz" (stdout)
199 result = sb.invoke_apio_cmd(apio, ["api", "get-config", "-t", "xyz"])
200 sb.assert_result_ok(result)
201 assert '"config":' in result.output
202 assert '"YOSYS_LIB":' in result.output
204 # -- Execute "apio api get-config -t xyz -o <dir>" (file)
205 path = sb.proj_dir / "apio.json"
206 result = sb.invoke_apio_cmd(
207 apio, ["api", "get-config", "-t", "xyz", "-o", str(path)]
208 )
209 sb.assert_result_ok(result)
211 # -- Read and verify the file.
212 text = sb.read_file_text(path)
213 data = json.loads(text)
214 assert data["timestamp"] == "xyz"
215 assert "YOSYS_LIB" in data["config"]["platform-env"]["define-consts"]
218def test_apio_api_get_system(apio_runner: ApioRunner):
219 """Test "apio api get-system" """
221 with apio_runner.in_sandbox() as sb:
223 # -- Execute "apio api get-system -t xyz" (stdout)
224 result = sb.invoke_apio_cmd(apio, ["api", "get-system", "-t", "xyz"])
225 sb.assert_result_ok(result)
226 assert "xyz" in result.output
227 assert '"apio-cli-version"' in result.output
228 assert '"python-version"' in result.output
230 # -- Execute "apio api get-system -t xyz -o <dir>" (file)
231 path = sb.proj_dir / "apio.json"
232 result = sb.invoke_apio_cmd(
233 apio, ["api", "get-system", "-t", "xyz", "-o", str(path)]
234 )
235 sb.assert_result_ok(result)
237 # -- Read and verify the file.
238 text = sb.read_file_text(path)
239 data = json.loads(text)
240 assert data["timestamp"] == "xyz"
241 assert data["system"]["remote-config-file-url"].endswith(".jsonc")
242 # -- APIO_DEFINITIONS may be predefined by the developer.
243 expected_dir = env_options.get(
244 env_options.APIO_DEFINITIONS, str(sb.packages_dir / "definitions")
245 )
246 assert Path(data["system"]["apio-definitions-dir"]) == Path(
247 str(expected_dir)
248 )
251def test_apio_api_get_project(apio_runner: ApioRunner):
252 """Test "apio api get-project" """
254 with apio_runner.in_sandbox() as sb:
256 # -- Create a fake apio project
257 sb.write_default_apio_ini()
259 sb.write_file("synth0.v", "")
260 sb.write_file("tb_0.sv", "")
262 os.makedirs("src1")
263 sb.write_file("src1/synth1.sv", "")
264 sb.write_file("src1/synth2.sv", "")
265 sb.write_file("src1/tb1_tb.sv", "")
267 # -- Execute "apio api get-project -t xyz" (stdout)
268 result = sb.invoke_apio_cmd(apio, ["api", "get-project", "-t", "xyz"])
269 sb.assert_result_ok(result)
270 assert '"default"' in result.output
271 assert '"envs"' in result.output
273 # -- Execute "apio api get-project -t xyz -o <dir>" (file)
274 path = sb.proj_dir / "apio.json"
275 result = sb.invoke_apio_cmd(
276 apio, ["api", "get-project", "-t", "xyz", "-o", str(path)]
277 )
278 sb.assert_result_ok(result)
280 # -- Read and verify the file.
281 text = sb.read_file_text(path)
282 data = json.loads(text)
284 print("\nActual data:")
285 print(json.dumps(data, indent=2))
286 print()
288 print(data)
290 assert data == {
291 "timestamp": "xyz",
292 "project": {
293 "active-env": {
294 "name": "default",
295 "options": {"board": "alhambra-ii", "top-module": "main"},
296 },
297 "envs": ["default"],
298 "synth-files": [
299 "synth0.v",
300 "tb_0.sv",
301 f"src1{os.sep}synth1.sv",
302 f"src1{os.sep}synth2.sv",
303 ],
304 "test-benches": [f"src1{os.sep}tb1_tb.sv"],
305 "board": {
306 "id": "alhambra-ii",
307 "is-custom": False,
308 "definition": {
309 "description": "Alhambra II",
310 "fpga-id": "ice40hx4k-tq144-8k",
311 "programmer": {
312 "id": "openfpgaloader",
313 "extra-args": "-b ice40_generic --vid ${VID} "
314 + "--pid ${PID} --busdev-num ${BUS}:${DEV}",
315 },
316 "usb": {
317 "vid": "0403",
318 "pid": "6010",
319 "product-regex": "^Alhambra II.*",
320 },
321 },
322 },
323 "fpga": {
324 "id": "ice40hx4k-tq144-8k",
325 "is-custom": False,
326 "definition": {
327 "arch": "ice40",
328 "vendor-part": "ICE40HX4K-TQ144",
329 "size": "8k",
330 "ice40-params": {
331 "nextpnr-type": "hx8k",
332 "nextpnr-package": "tq144:4k",
333 },
334 },
335 },
336 "programmer": {
337 "id": "openfpgaloader",
338 "is-custom": False,
339 "definition": {
340 "command": "openFPGALoader",
341 "args": "--force-terminal-mode --verify",
342 },
343 },
344 },
345 }
348def test_apio_api_get_build_report(apio_runner: ApioRunner):
349 """Test "apio api get-project" """
351 with apio_runner.in_sandbox() as sb:
353 # -- Execute "apio examples fetch alhambra-ii/blinky"
354 result = sb.invoke_apio_cmd(
355 apio, ["examples", "fetch", "alhambra-ii/blinky"]
356 )
357 sb.assert_result_ok(result)
359 # -- Execute "apio build"
360 result = sb.invoke_apio_cmd(apio, ["build"])
361 sb.assert_result_ok(result)
363 # -- Execute "apio api get-build-report -t xyz" (stdout)
364 result = sb.invoke_apio_cmd(
365 apio, ["api", "get-build-report", "-t", "xyz"]
366 )
367 sb.assert_result_ok(result)
369 # -- Sanity check the output
370 assert '"env": "default"' in result.output
371 assert '"ICESTORM_LC":' in result.output
372 assert '"sys_clk":' in result.output
373 assert '"fmax_mhz":' in result.output
375 # -- Execute "apio api get-build-report -t xyz -o <dir>" (file)
376 path = sb.proj_dir / "apio.json"
377 result = sb.invoke_apio_cmd(
378 apio, ["api", "get-build-report", "-t", "xyz", "-o", str(path)]
379 )
380 sb.assert_result_ok(result)
382 # -- Read and sanity check the file.
383 text = sb.read_file_text(path)
384 data = json.loads(text)
385 print(data)
387 assert data["timestamp"] == "xyz"
388 assert data["build-report"]["env"] == "default"
389 assert (
390 data["build-report"]["resources"]["ICESTORM_LC"]["available"]
391 == 7680
392 )
393 assert "fmax_mhz" in data["build-report"]["clocks"]["sys_clk"]
396def test_apio_api_get_build_report_invalid_utilization(
397 apio_runner: ApioRunner,
398):
399 """Test "apio api get-build-report" with invalid utilization values in
400 hardware.pnr. Percentages that can't be computed or are out of range
401 should be reported as null."""
403 with apio_runner.in_sandbox() as sb:
405 sb.write_default_apio_ini()
406 sb.write_json_file(
407 "_build/default/hardware.pnr",
408 {
409 "utilization": {
410 "ICESTORM_LC": {"available": 7680, "used": 768},
411 "ICESTORM_PLL": {"available": 0, "used": 2},
412 "SB_IO": {"available": 10, "used": 20},
413 },
414 "fmax": {},
415 },
416 )
418 # -- Execute "apio api get-build-report -o <file>"
419 path = sb.proj_dir / "apio.json"
420 result = sb.invoke_apio_cmd(
421 apio, ["api", "get-build-report", "-o", str(path)]
422 )
423 sb.assert_result_ok(result)
425 # -- Check the percentages.
426 resources = json.loads(sb.read_file_text(path))["build-report"][
427 "resources"
428 ]
429 assert resources["ICESTORM_LC"]["percentage"] == 10.0
430 assert resources["ICESTORM_PLL"]["percentage"] is None
431 assert resources["SB_IO"]["percentage"] is None
434def test_apio_api_get_examples(apio_runner: ApioRunner):
435 """Test "apio api get-examples" """
437 with apio_runner.in_sandbox() as sb:
439 # -- Execute "apio api get-examples -t xyz" (stdout)
440 result = sb.invoke_apio_cmd(apio, ["api", "get-examples", "-t", "xyz"])
441 sb.assert_result_ok(result)
442 assert "xyz" in result.output
443 assert '"alhambra-ii"' in result.output
444 assert '"blinky"' in result.output
446 # -- Execute "apio api get-examples -t xyz -s boards -o <dir>" (file)
447 path = sb.proj_dir / "apio.json"
448 result = sb.invoke_apio_cmd(
449 apio, ["api", "get-examples", "-t", "xyz", "-o", str(path)]
450 )
451 sb.assert_result_ok(result)
453 # -- Read and verify the file.
454 text = sb.read_file_text(path)
455 data = json.loads(text)
456 assert data["timestamp"] == "xyz"
457 assert (
458 data["examples"]["alhambra-ii"]["blinky"]["description"]
459 == "Blinking led"
460 )
463def test_apio_api_scan_path(apio_runner: ApioRunner):
464 """Test "apio api scan-path" """
466 with apio_runner.in_sandbox() as sb:
467 # -- Execute "apio api scan-path -t xyz" (stdout)
468 result = sb.invoke_apio_cmd(apio, ["api", "scan-path", "-t", "xyz"])
469 sb.assert_result_ok(result)
470 assert "xyz" in result.output
471 assert '"conflicts"' in result.output
472 assert '"duplicates"' in result.output
474 # -- Execute "apio api scan-path -t xyz -o <path>"
475 path = sb.proj_dir / "apio.json"
476 result = sb.invoke_apio_cmd(
477 apio, ["api", "scan-path", "-t", "xyz", "-o", str(path)]
478 )
479 sb.assert_result_ok(result)
481 # -- Read and verify the file.
482 text = sb.read_file_text(path)
483 data = json.loads(text)
484 assert data["timestamp"] == "xyz"
485 assert len(data["apio-path-scan"]["path-dirs"]) > 0
488def test_apio_api_scan_devices(apio_runner: ApioRunner):
489 """Test "apio api scan-devices" """
491 with apio_runner.in_sandbox() as sb:
493 # -- Execute "apio api scan-devices -t xyz". We run it in a
494 # -- subprocess such that it releases the libusb1 file it uses.
495 # -- This also means that it's not included in the pytest test
496 # -- coverage report.
497 result = sb.invoke_apio_cmd(
498 apio, ["api", "scan-devices", "-t", "xyz"], in_subprocess=True
499 )
500 sb.assert_result_ok(result)
502 assert "xyz" in result.output
503 assert "usb-devices" in result.output
504 assert "serial-devices" in result.output
506 # -- Execute "apio api get-boards -t xyz -s boards -o <dir>" (file)
507 path = sb.proj_dir / "apio.json"
509 result = sb.invoke_apio_cmd(
510 apio,
511 ["api", "scan-devices", "-t", "xyz", "-o", str(path)],
512 in_subprocess=True,
513 )
514 sb.assert_result_ok(result)
516 # -- Read and verify the output file. Since we don't know what
517 # -- devices the platform has, we just check for the section keys.
518 text = sb.read_file_text(path)
519 data = json.loads(text)
520 assert data["timestamp"] == "xyz"
521 assert "usb-devices" in data
522 assert "serial-devices" in data
525def test_apio_api_echo(apio_runner: ApioRunner):
526 """Test "apio api echo" """
528 with apio_runner.in_sandbox() as sb:
529 result = sb.invoke_apio_cmd(
530 apio,
531 ["api", "echo", "-t", "Hello world", "-s", "OK"],
532 in_subprocess=True,
533 )
534 sb.assert_result_ok(result)
536 assert "Hello world" in result.output