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

1""" 

2Tests of project.py 

3""" 

4 

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) 

14 

15# TODO: Add more tests. 

16 

17 

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)""" 

24 

25 with apio_runner.in_sandbox() as sb: 

26 # -- Create the apio.ini file 

27 sb.write_apio_ini(apio_ini) 

28 

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 ) 

38 

39 # -- Return the values. 

40 return (apio_ctx.project, log.out) 

41 

42 

43def test_env_options_specs(): 

44 """Tests the option spec keys match options specs names""" 

45 

46 for name, env_options_spec in ENV_OPTIONS_SPEC.items(): 

47 assert name == env_options_spec.name 

48 

49 

50def test_all_options_env(apio_runner: ApioRunner): 

51 """Tests an apio.ini with all options""" 

52 

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 } 

68 

69 # -- Make sure we covered all the options. 

70 assert len(apio_ini["[env:default]"]) == len(ENV_OPTIONS_SPEC) 

71 

72 project, _ = load_apio_ini( 

73 apio_ini=apio_ini, 

74 env_arg=None, 

75 apio_runner=apio_runner, 

76 ) 

77 

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 } 

92 

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" 

97 

98 

99def test_required_options_only_env(apio_runner: ApioRunner): 

100 """Tests a minimal apio.ini with required only options.""" 

101 

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 ) 

112 

113 assert project.env_name == "default" 

114 assert project.env_options == { 

115 "board": "alhambra-ii", 

116 "top-module": "main", 

117 } 

118 

119 

120def test_list_options(apio_runner: ApioRunner): 

121 """Tests list options.""" 

122 

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 ) 

135 

136 assert project.get_list_option("yosys-extra-options") == [ 

137 "k1=v1 k2=v2", 

138 "k3=v3", 

139 ] 

140 

141 assert project.get_list_option("nextpnr-extra-options") == [ 

142 "k5=v5 k6=v6", 

143 "k7=v7", 

144 ] 

145 

146 

147def test_macro_expansion(apio_runner: ApioRunner): 

148 """Tests the expansion of macros within values.""" 

149 

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 ) 

163 

164 assert project.get_str_option("constraint-file") == "; value" 

165 

166 assert project.get_list_option("yosys-extra-options") == [ 

167 "k1=_build/default/v1 k2=v2;", 

168 "k3=v3# default", 

169 ] 

170 

171 

172def test_legacy_env_section_rejected(apio_runner: ApioRunner): 

173 """A bare [env] section is rejected with a rename hint.""" 

174 

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 ) 

186 

187 

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

191 

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 ) 

209 

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 } 

216 

217 

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.""" 

221 

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 ) 

242 

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 } 

249 

250 

251def test_env_selection_from_env_arg(apio_runner: ApioRunner): 

252 """Tests that with --env overriding default-env in apio.ini.""" 

253 

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 ) 

274 

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 } 

281 

282 

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.""" 

291 

292 with apio_runner.in_sandbox() as sb: 

293 # -- Create the apio.ini file 

294 sb.write_apio_ini(apio_ini) 

295 

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 ) 

305 

306 # -- Check the errors. 

307 assert e.value.code == 1 

308 assert expected_error in log.out 

309 

310 

311def test_validation_errors(apio_runner: ApioRunner): 

312 """Tests the validation of apio.ini errors.""" 

313 

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 ) 

329 

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 ) 

341 

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 ) 

354 

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 ) 

367 

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 ) 

383 

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 ) 

394 

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 ) 

407 

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 )