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

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 

8 

9"""Test for build_report.py.""" 

10 

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) 

22 

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} 

34 

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} 

47 

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} 

58 

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} 

73 

74 

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} 

91 

92 

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 ) 

101 

102 

103def test_read_build_report_ice40(apio_runner: ApioRunner): 

104 """Tests the read_build_report() function for ICE40 hardware.pnr.""" 

105 

106 with apio_runner.in_sandbox() as sb: 

107 

108 file_path = Path("_build/default/hardware.pnr") 

109 

110 sb.write_json_file(file_path, TEST_SUMMARY_ICE40) 

111 

112 build_report = read_build_report(file_path) 

113 assert isinstance(build_report, BuildReport) 

114 

115 print(build_report) 

116 

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 ) 

125 

126 

127def test_read_build_report_ecp5(apio_runner): 

128 """Tests the read_build_report() function for ECP5 hardware.pnr.""" 

129 

130 with apio_runner.in_sandbox() as sb: 

131 

132 file_path = Path("_build/default/hardware.pnr") 

133 

134 sb.write_json_file(file_path, TEST_SUMMARY_ECP5) 

135 

136 build_report = read_build_report(file_path) 

137 assert isinstance(build_report, BuildReport) 

138 

139 print(build_report) 

140 

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 ) 

150 

151 

152def test_read_build_report_gowin(apio_runner): 

153 """Tests the read_build_report() function for GOWIN hardware.pnr.""" 

154 

155 with apio_runner.in_sandbox() as sb: 

156 

157 file_path = Path("_build/default/hardware.pnr") 

158 

159 sb.write_json_file(file_path, TEST_SUMMARY_GOWIN) 

160 

161 build_report = read_build_report(file_path) 

162 assert isinstance(build_report, BuildReport) 

163 

164 print(build_report) 

165 

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 ) 

175 

176 

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) 

182 

183 build_report = read_build_report(file_path) 

184 

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 != "" 

197 

198 

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) 

205 

206 build_report = read_build_report(file_path) 

207 

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 ) 

219 

220 

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 

231 

232 

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