Coverage for tests / unit_tests / managers / test_project.py: 100%

77 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-25 02:31 +0000

1""" 

2Tests of project.py 

3""" 

4 

5from typing import Dict, Optional, Tuple 

6from pytest import LogCaptureFixture 

7import pytest 

8from tests.conftest import ApioRunner 

9from apio.managers.project import Project, ENV_OPTIONS_SPEC 

10from apio.common.apio_console import cunstyle 

11from apio.apio_context import ( 

12 ApioContext, 

13 PackagesPolicy, 

14 ProjectPolicy, 

15 RemoteConfigPolicy, 

16) 

17 

18# TODO: Add more tests. 

19 

20 

21def load_apio_ini( 

22 apio_ini: Dict[str, Dict[str, str]], 

23 env_arg: Optional[str], 

24 apio_runner: ApioRunner, 

25 capsys: LogCaptureFixture, 

26) -> Tuple[Project, str]: 

27 """A helper function load apio.ini. Returns (project, stdout)""" 

28 

29 with apio_runner.in_sandbox() as sb: 

30 # -- Create the apio.ini file 

31 sb.write_apio_ini(apio_ini) 

32 

33 # -- Try to create the context with the project info. 

34 capsys.readouterr() # Reset capture 

35 apio_ctx = ApioContext( 

36 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

37 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

38 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

39 env_arg=env_arg, 

40 ) 

41 

42 # -- Return the values. 

43 return ( 

44 apio_ctx.project, 

45 cunstyle(capsys.readouterr().out), 

46 ) 

47 

48 

49def test_env_options_specs(): 

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

51 

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

53 assert name == env_options_spec.name 

54 

55 

56def test_all_options_env(apio_runner: ApioRunner, capsys: LogCaptureFixture): 

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

58 

59 apio_ini = { 

60 "[env:default]": { 

61 "board": "alhambra-ii", # required. 

62 "default-testbench": "main_tb.v", 

63 "defines": "\n aaa=111\n bbb=222", 

64 "format-verible-options": "\n --aaa bbb\n --ccc ddd", 

65 "programmer-cmd": "iceprog ${VID}:${PID}", 

66 "top-module": "my_module", 

67 "yosys-extra-options": "-dsp -xyz", 

68 "nextpnr-extra-options": "--freq 13", 

69 "gtkwave-extra-options": "--rcvar=do_initial_zoom_fit 1", 

70 "verilator-extra-options": "-Wno-fatal", 

71 "constraint-file": "pinout.lpf", 

72 } 

73 } 

74 

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

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

77 

78 project, _ = load_apio_ini( 

79 apio_ini=apio_ini, 

80 env_arg=None, 

81 apio_runner=apio_runner, 

82 capsys=capsys, 

83 ) 

84 

85 assert project.env_name == "default" 

86 assert project.env_options == { 

87 "board": "alhambra-ii", 

88 "default-testbench": "main_tb.v", 

89 "defines": ["aaa=111", "bbb=222"], 

90 "format-verible-options": ["--aaa bbb", "--ccc ddd"], 

91 "programmer-cmd": "iceprog ${VID}:${PID}", 

92 "top-module": "my_module", 

93 "yosys-extra-options": ["-dsp -xyz"], 

94 "nextpnr-extra-options": ["--freq 13"], 

95 "gtkwave-extra-options": ["--rcvar=do_initial_zoom_fit 1"], 

96 "verilator-extra-options": ["-Wno-fatal"], 

97 "constraint-file": "pinout.lpf", 

98 } 

99 

100 # -- Try a few as dict lookup on the project object. 

101 assert project.get_str_option("board") == "alhambra-ii" 

102 assert project.get_str_option("top-module") == "my_module" 

103 assert project.get_str_option("constraint-file") == "pinout.lpf" 

104 

105 

106def test_required_options_only_env( 

107 apio_runner: ApioRunner, capsys: LogCaptureFixture 

108): 

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

110 

111 project, _ = load_apio_ini( 

112 apio_ini={ 

113 "[env:default]": { 

114 "board": "alhambra-ii", 

115 "top-module": "main", 

116 } 

117 }, 

118 env_arg=None, 

119 apio_runner=apio_runner, 

120 capsys=capsys, 

121 ) 

122 

123 assert project.env_name == "default" 

124 assert project.env_options == { 

125 "board": "alhambra-ii", 

126 "top-module": "main", 

127 } 

128 

129 

130def test_list_options(apio_runner: ApioRunner, capsys: LogCaptureFixture): 

131 """Tests list options.""" 

132 

133 project, _ = load_apio_ini( 

134 apio_ini={ 

135 "[env:default]": { 

136 "board": "alhambra-ii", 

137 "top-module": "my_top_module", 

138 "yosys-extra-options": " k1=v1 k2=v2 \n\n k3=v3 \n\n", 

139 "nextpnr-extra-options": " k5=v5 k6=v6 \n\n k7=v7 \n\n", 

140 } 

141 }, 

142 env_arg=None, 

143 apio_runner=apio_runner, 

144 capsys=capsys, 

145 ) 

146 

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

148 "k1=v1 k2=v2", 

149 "k3=v3", 

150 ] 

151 

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

153 "k5=v5 k6=v6", 

154 "k7=v7", 

155 ] 

156 

157 

158def test_macro_expansion(apio_runner: ApioRunner, capsys: LogCaptureFixture): 

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

160 

161 project, _ = load_apio_ini( 

162 apio_ini={ 

163 "[env:default]": { 

164 "board": "alhambra-ii", 

165 "top-module": "my_top_module", 

166 "constraint-file": " ${SEMICOLON} value ", 

167 "yosys-extra-options": " k1=${ENV_BUILD}/v1 k2=v2; \n ; " 

168 "Comment \n k3=v3${HASH} ${ENV_NAME}\n\n", 

169 } 

170 }, 

171 env_arg=None, 

172 apio_runner=apio_runner, 

173 capsys=capsys, 

174 ) 

175 

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

177 

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

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

180 "k3=v3# default", 

181 ] 

182 

183 

184def test_legacy_board_id(apio_runner: ApioRunner, capsys: LogCaptureFixture): 

185 """Tests with 'board' option having a legacy board id. It should 

186 be converted to the canonical board id""" 

187 

188 project, stdout = load_apio_ini( 

189 apio_ini={ 

190 "[env:default]": { 

191 "board": "iCE40-HX8K", 

192 "top-module": "my_top_module", 

193 } 

194 }, 

195 env_arg=None, 

196 apio_runner=apio_runner, 

197 capsys=capsys, 

198 ) 

199 

200 assert project.env_name == "default" 

201 assert project.env_options == { 

202 "board": "ice40-hx8k", 

203 "top-module": "my_top_module", 

204 } 

205 assert ( 

206 "Warning: 'Board iCE40-HX8K' was renamed to 'ice40-hx8k'. " 

207 "Please update apio.ini" in stdout 

208 ) 

209 

210 

211def test_legacy_apio_ini(apio_runner: ApioRunner, capsys: LogCaptureFixture): 

212 """Tests with an old style apio.ini that has a single [env] section.""" 

213 

214 project, stdout = load_apio_ini( 

215 apio_ini={ 

216 "[env]": { 

217 "board": "alhambra-ii", 

218 "top-module": "main", 

219 } 

220 }, 

221 env_arg=None, 

222 apio_runner=apio_runner, 

223 capsys=capsys, 

224 ) 

225 

226 assert project.env_name == "default" 

227 assert project.env_options == { 

228 "board": "alhambra-ii", 

229 "top-module": "main", 

230 } 

231 assert ( 

232 "Warning: Apio.ini has a legacy [env] section. " 

233 "Please rename it to [env:default]" in stdout 

234 ) 

235 

236 

237def test_first_env_is_default( 

238 apio_runner: ApioRunner, capsys: LogCaptureFixture 

239): 

240 """Tests that with no --env and no default-env, the first env in 

241 apio.ini is selected""" 

242 

243 project, _ = load_apio_ini( 

244 apio_ini={ 

245 "[common]": { 

246 "default-testbench": "main_tb.v", 

247 }, 

248 "[env:env1]": { 

249 "board": "alhambra-ii", 

250 "top-module": "module1", 

251 }, 

252 "[env:env2]": { 

253 "board": "ice40-hx8k", 

254 "top-module": "module2", 

255 }, 

256 }, 

257 env_arg=None, 

258 apio_runner=apio_runner, 

259 capsys=capsys, 

260 ) 

261 

262 assert project.env_name == "env1" 

263 assert project.env_options == { 

264 "default-testbench": "main_tb.v", 

265 "board": "alhambra-ii", 

266 "top-module": "module1", 

267 } 

268 

269 

270def test_env_selection_from_apio_ini( 

271 apio_runner: ApioRunner, capsys: LogCaptureFixture 

272): 

273 """Tests that with no --env, and with default env defined in apio.ini 

274 using default-env option.""" 

275 

276 project, _ = load_apio_ini( 

277 apio_ini={ 

278 "[apio]": { 

279 "default-env": "env2", 

280 }, 

281 "[common]": { 

282 "default-testbench": "main_tb.v", 

283 }, 

284 "[env:env1]": { 

285 "board": "alhambra-ii", 

286 "top-module": "module1", 

287 }, 

288 "[env:env2]": { 

289 "board": "ice40-hx8k", 

290 "top-module": "module2", 

291 }, 

292 }, 

293 env_arg=None, 

294 apio_runner=apio_runner, 

295 capsys=capsys, 

296 ) 

297 

298 assert project.env_name == "env2" 

299 assert project.env_options == { 

300 "default-testbench": "main_tb.v", 

301 "board": "ice40-hx8k", 

302 "top-module": "module2", 

303 } 

304 

305 

306def test_env_selection_from_env_arg( 

307 apio_runner: ApioRunner, capsys: LogCaptureFixture 

308): 

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

310 

311 project, _ = load_apio_ini( 

312 apio_ini={ 

313 "[apio]": { 

314 "default-env": "env1", 

315 }, 

316 "[common]": { 

317 "default-testbench": "main_tb.v", 

318 }, 

319 "[env:env1]": { 

320 "board": "alhambra-ii", 

321 "top-module": "module1", 

322 }, 

323 "[env:env2]": { 

324 "board": "ice40-hx8k", 

325 "top-module": "module2", 

326 }, 

327 }, 

328 env_arg="env2", # --env env2 

329 apio_runner=apio_runner, 

330 capsys=capsys, 

331 ) 

332 

333 assert project.env_name == "env2" 

334 assert project.env_options == { 

335 "default-testbench": "main_tb.v", 

336 "board": "ice40-hx8k", 

337 "top-module": "module2", 

338 } 

339 

340 

341def error_tester( 

342 env_arg: Optional[str], 

343 apio_ini: Dict[str, Dict[str, str]], 

344 expected_error: str, 

345 apio_runner: ApioRunner, 

346 capsys: LogCaptureFixture, 

347): 

348 """A helper function to tests apio.ini content that is expected to 

349 exit with an error.""" 

350 

351 with apio_runner.in_sandbox() as sb: 

352 # -- Create the apio.ini file 

353 sb.write_apio_ini(apio_ini) 

354 

355 # -- Try to create the context with the project info. 

356 capsys.readouterr() # Reset capture 

357 with pytest.raises(SystemExit) as e: 

358 ApioContext( 

359 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

360 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

361 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

362 env_arg=env_arg, 

363 ) 

364 

365 # -- Check the errors. 

366 capture = cunstyle(capsys.readouterr().out) 

367 assert e.value.code == 1 

368 assert expected_error in capture 

369 

370 

371def test_validation_errors(apio_runner: ApioRunner, capsys: LogCaptureFixture): 

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

373 

374 # -- No [env:name] section. 

375 error_tester( 

376 env_arg=None, 

377 apio_ini={ 

378 "[common]": { 

379 "board": "alhambra-ii", 

380 "top-module": "main", 

381 } 

382 }, 

383 expected_error=( 

384 "Error: Project file 'apio.ini' should have at " 

385 "least one [env:name] section." 

386 ), 

387 apio_runner=apio_runner, 

388 capsys=capsys, 

389 ) 

390 

391 # -- Unknown board id. 

392 error_tester( 

393 env_arg=None, 

394 apio_ini={ 

395 "[env:default]": { 

396 "board": "no-such-board", 

397 } 

398 }, 

399 expected_error="Error: Unknown board id 'no-such-board' in apio.ini", 

400 apio_runner=apio_runner, 

401 capsys=capsys, 

402 ) 

403 

404 # -- Env name has an invalid char (Uppercase). 

405 error_tester( 

406 env_arg=None, 

407 apio_ini={ 

408 "[env:Default]": { 

409 "board": "alhambra-ii", 

410 "top-module": "main", 

411 } 

412 }, 

413 expected_error="Error: Invalid env name 'Default'", 

414 apio_runner=apio_runner, 

415 capsys=capsys, 

416 ) 

417 

418 # -- Env name has an extra space. 

419 error_tester( 

420 env_arg=None, 

421 apio_ini={ 

422 "[env: default]": { 

423 "board": "alhambra-ii", 

424 "top-module": "main", 

425 } 

426 }, 

427 expected_error="Error: Invalid env name ' default'", 

428 apio_runner=apio_runner, 

429 capsys=capsys, 

430 ) 

431 

432 # -- default-env points to a non existing env. 

433 error_tester( 

434 env_arg=None, 

435 apio_ini={ 

436 "[apio]": { 

437 "default-env": "no-such-env", 

438 }, 

439 "[env:default]": { 

440 "board": "alhambra-ii", 

441 "top-module": "main", 

442 }, 

443 }, 

444 expected_error="Error: Env 'no-such-env' not found in apio.ini", 

445 apio_runner=apio_runner, 

446 capsys=capsys, 

447 ) 

448 

449 # -- Missing required option. 

450 error_tester( 

451 env_arg=None, 

452 apio_ini={ 

453 "[env:default]": {"top-module": "main"}, 

454 }, 

455 expected_error="Error: Missing required option 'board' for " 

456 "env 'default'.", 

457 apio_runner=apio_runner, 

458 capsys=capsys, 

459 ) 

460 

461 # -- env_arg has a non existing env name. 

462 error_tester( 

463 env_arg="no-such-env", 

464 apio_ini={ 

465 "[env:default]": { 

466 "board": "alhambra-ii", 

467 "top-module": "main", 

468 }, 

469 }, 

470 expected_error="Error: Env 'no-such-env' not found in apio.ini", 

471 apio_runner=apio_runner, 

472 capsys=capsys, 

473 ) 

474 

475 # -- env_arg has an env name with an invalid char (upper case). 

476 error_tester( 

477 env_arg="Default", 

478 apio_ini={ 

479 "[env:default]": { 

480 "board": "alhambra-ii", 

481 "top-module": "main", 

482 }, 

483 }, 

484 expected_error="Error: Invalid --env value 'Default'", 

485 apio_runner=apio_runner, 

486 capsys=capsys, 

487 )