Coverage for tests/unit_tests/common/test_build_report.py: 100%
67 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 build_report.py."""
11import json
12from pathlib import Path
13import pytest
14from tests.conftest import ApioRunner
15from apio.common.proto.stubs.apio_common_pb2 import ApioArch
16from apio.common.build_report import (
17 ResourceReport,
18 ClockReport,
19 BuildReport,
20 read_build_report,
21)
23# -- Simplified hardware.pnr for testing ICE40.
24TEST_SUMMARY_ICE40 = {
25 "other-stuff": {"bla-bla": "bla-bla"},
26 "utilization": {
27 "ICESTORM_PLL": {"available": 2, "used": 0},
28 "ICESTORM_LC": {"available": 7680, "used": 27},
29 },
30 "fmax": {
31 "my_clk$SB_IO_IN_$glb_clk": {"achieved": 194.363, "constraint": 12}
32 },
33}
35# -- Simplified hardware.pnr for testing ECP5.
36TEST_SUMMARY_ECP5 = {
37 "other-stuff": {"bla-bla": "bla-bla"},
38 "utilization": {
39 "TRELLIS_ECLKBUF": {"available": 8, "used": 0},
40 "SIOLOGIC": {"available": 69, "used": 0},
41 "TRELLIS_COMB": {"available": 24288, "used": 12144},
42 },
43 "fmax": {
44 "$glbnet$my_clk$TRELLIS_IO_IN": {"achieved": 295.420, "constraint": 12}
45 },
46}
48# -- Simplified hardware.pnr for testing GOWIN.
49TEST_SUMMARY_GOWIN = {
50 "other-stuff": {"bla-bla": "bla-bla"},
51 "utilization": {
52 "IOB": {"available": 276, "used": 2},
53 "IOLOGICI": {"available": 276, "used": 0},
54 "LUT4": {"available": 8640, "used": 12},
55 },
56 "fmax": {"sys_clk_IBUF_I_O": {"achieved": 197.745, "constraint": 12}},
57}
59# -- Simplified hardware.pnr for testing Xilinx.
60TEST_SUMMARY_XILINX = {
61 "utilization": {
62 "BUFGCTRL": {"available": 32, "used": 1},
63 "PSEUDO_GND": {"available": 1, "used": 1},
64 "SLICE_LUTX": {"available": 65200, "used": 48},
65 },
66 "fmax": {
67 "$iopadmap$clk": {
68 "achieved": 398.88311767578125,
69 "constraint": 12,
70 }
71 },
72}
75# -- Simplified ICE40 hardware.pnr with invalid utilization values.
76TEST_SUMMARY_INVALID_UTILIZATION = {
77 "utilization": {
78 # -- Zero available, used > 0 (division by zero).
79 "ICESTORM_PLL": {"available": 0, "used": 2},
80 # -- Zero available, zero used (division by zero).
81 "ICESTORM_RAM": {"available": 0, "used": 0},
82 # -- Used > available (above 100%).
83 "ICESTORM_LC": {"available": 10, "used": 20},
84 # -- Negative used (below 0%).
85 "SB_IO": {"available": 10, "used": -1},
86 # -- Valid, exactly 100%.
87 "SB_GB": {"available": 8, "used": 8},
88 },
89 "fmax": {},
90}
93def _resource(name: str, available: int, used: int) -> ResourceReport:
94 """Construct a ResourceReport for testing."""
95 return ResourceReport(
96 name=name,
97 available=available,
98 used=used,
99 percentage=100 * used / available,
100 )
103def test_read_build_report_ice40(apio_runner: ApioRunner):
104 """Tests the read_build_report() function for ICE40 hardware.pnr."""
106 with apio_runner.in_sandbox() as sb:
108 file_path = Path("_build/default/hardware.pnr")
110 sb.write_json_file(file_path, TEST_SUMMARY_ICE40)
112 build_report = read_build_report(file_path)
113 assert isinstance(build_report, BuildReport)
115 print(build_report)
117 assert build_report == BuildReport(
118 arch=ApioArch.ice40,
119 resources=[
120 _resource("ICESTORM_LC", 7680, 27),
121 _resource("ICESTORM_PLL", 2, 0),
122 ],
123 clocks=[ClockReport(name="my_clk", fmax_mhz=194.363)],
124 )
127def test_read_build_report_ecp5(apio_runner):
128 """Tests the read_build_report() function for ECP5 hardware.pnr."""
130 with apio_runner.in_sandbox() as sb:
132 file_path = Path("_build/default/hardware.pnr")
134 sb.write_json_file(file_path, TEST_SUMMARY_ECP5)
136 build_report = read_build_report(file_path)
137 assert isinstance(build_report, BuildReport)
139 print(build_report)
141 assert build_report == BuildReport(
142 arch=ApioArch.ecp5,
143 resources=[
144 _resource("SIOLOGIC", 69, 0),
145 _resource("TRELLIS_COMB", 24288, 12144),
146 _resource("TRELLIS_ECLKBUF", 8, 0),
147 ],
148 clocks=[ClockReport(name="my_clk", fmax_mhz=295.420)],
149 )
152def test_read_build_report_gowin(apio_runner):
153 """Tests the read_build_report() function for GOWIN hardware.pnr."""
155 with apio_runner.in_sandbox() as sb:
157 file_path = Path("_build/default/hardware.pnr")
159 sb.write_json_file(file_path, TEST_SUMMARY_GOWIN)
161 build_report = read_build_report(file_path)
162 assert isinstance(build_report, BuildReport)
164 print(build_report)
166 assert build_report == BuildReport(
167 arch=ApioArch.gowin,
168 resources=[
169 _resource("IOB", 276, 2),
170 _resource("IOLOGICI", 276, 0),
171 _resource("LUT4", 8640, 12),
172 ],
173 clocks=[ClockReport(name="sys_clk", fmax_mhz=197.745)],
174 )
177def test_read_build_report_xilinx(apio_runner):
178 """Tests the read_build_report() function for XILINX hardware.pnr."""
179 with apio_runner.in_sandbox() as sb:
180 file_path = Path("_build/default/hardware.pnr")
181 sb.write_json_file(file_path, TEST_SUMMARY_XILINX)
183 build_report = read_build_report(file_path)
185 assert build_report == BuildReport(
186 arch=ApioArch.xilinx,
187 resources=[
188 _resource("BUFGCTRL", 32, 1),
189 _resource("PSEUDO_GND", 1, 1),
190 _resource("SLICE_LUTX", 65200, 48),
191 ],
192 clocks=[
193 ClockReport(name="clk", fmax_mhz=398.88311767578125),
194 ],
195 )
196 assert build_report.clocks[0].name != ""
199def test_read_build_report_invalid_utilization(apio_runner: ApioRunner):
200 """Tests that invalid utilization values in hardware.pnr don't crash
201 and result in a None percentage."""
202 with apio_runner.in_sandbox() as sb:
203 file_path = Path("_build/default/hardware.pnr")
204 sb.write_json_file(file_path, TEST_SUMMARY_INVALID_UTILIZATION)
206 build_report = read_build_report(file_path)
208 assert build_report == BuildReport(
209 arch=ApioArch.ice40,
210 resources=[
211 ResourceReport("ICESTORM_LC", 10, 20, None),
212 ResourceReport("ICESTORM_PLL", 0, 2, None),
213 ResourceReport("ICESTORM_RAM", 0, 0, None),
214 ResourceReport("SB_GB", 8, 8, 100.0),
215 ResourceReport("SB_IO", 10, -1, None),
216 ],
217 clocks=[],
218 )
221def test_hardware_pnr_reading_failure(apio_runner: ApioRunner):
222 """Tests the case where reading hardware.pne fails."""
223 with apio_runner.in_sandbox():
224 file_path = Path("_build/default/hardware.pnr")
225 with apio_runner.with_logger() as log:
226 with pytest.raises(SystemExit) as e:
227 # -- Since we didn't create hardware.pnr, reading should fail.
228 read_build_report(file_path)
229 assert e.value.code == 1
230 assert "Error: Failed to read" in log.out
233def test_hardware_pnr_parsing_failure(apio_runner: ApioRunner):
234 """Tests the case where reading hardware.pne fails."""
235 with apio_runner.in_sandbox() as sb:
236 file_path = Path("_build/default/hardware.pnr")
237 sb.write_file(file_path, "Broken JSON file")
238 with apio_runner.with_logger() as log:
239 with pytest.raises(SystemExit) as e:
240 # -- Since we didn't create hardware.pnr, reading should fail.
241 read_build_report(file_path)
242 assert e.value.code == 1
243 assert "Error: Failed parsing json file" in log.out