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

77 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-24 03:51 +0000

1""" 

2Tests of project.py 

3""" 

4 

5from typing import Dict, Optional, Tuple 

6from _pytest.capture import CaptureFixture 

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: CaptureFixture[str], 

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: CaptureFixture[str]): 

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: CaptureFixture 

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: CaptureFixture): 

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: CaptureFixture): 

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: CaptureFixture): 

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: CaptureFixture): 

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(apio_runner: ApioRunner, capsys: CaptureFixture): 

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

239 apio.ini is selected""" 

240 

241 project, _ = load_apio_ini( 

242 apio_ini={ 

243 "[common]": { 

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

245 }, 

246 "[env:env1]": { 

247 "board": "alhambra-ii", 

248 "top-module": "module1", 

249 }, 

250 "[env:env2]": { 

251 "board": "ice40-hx8k", 

252 "top-module": "module2", 

253 }, 

254 }, 

255 env_arg=None, 

256 apio_runner=apio_runner, 

257 capsys=capsys, 

258 ) 

259 

260 assert project.env_name == "env1" 

261 assert project.env_options == { 

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

263 "board": "alhambra-ii", 

264 "top-module": "module1", 

265 } 

266 

267 

268def test_env_selection_from_apio_ini( 

269 apio_runner: ApioRunner, capsys: CaptureFixture 

270): 

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

272 using default-env option.""" 

273 

274 project, _ = load_apio_ini( 

275 apio_ini={ 

276 "[apio]": { 

277 "default-env": "env2", 

278 }, 

279 "[common]": { 

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

281 }, 

282 "[env:env1]": { 

283 "board": "alhambra-ii", 

284 "top-module": "module1", 

285 }, 

286 "[env:env2]": { 

287 "board": "ice40-hx8k", 

288 "top-module": "module2", 

289 }, 

290 }, 

291 env_arg=None, 

292 apio_runner=apio_runner, 

293 capsys=capsys, 

294 ) 

295 

296 assert project.env_name == "env2" 

297 assert project.env_options == { 

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

299 "board": "ice40-hx8k", 

300 "top-module": "module2", 

301 } 

302 

303 

304def test_env_selection_from_env_arg( 

305 apio_runner: ApioRunner, capsys: CaptureFixture 

306): 

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

308 

309 project, _ = load_apio_ini( 

310 apio_ini={ 

311 "[apio]": { 

312 "default-env": "env1", 

313 }, 

314 "[common]": { 

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

316 }, 

317 "[env:env1]": { 

318 "board": "alhambra-ii", 

319 "top-module": "module1", 

320 }, 

321 "[env:env2]": { 

322 "board": "ice40-hx8k", 

323 "top-module": "module2", 

324 }, 

325 }, 

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

327 apio_runner=apio_runner, 

328 capsys=capsys, 

329 ) 

330 

331 assert project.env_name == "env2" 

332 assert project.env_options == { 

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

334 "board": "ice40-hx8k", 

335 "top-module": "module2", 

336 } 

337 

338 

339def error_tester( 

340 env_arg: Optional[str], 

341 apio_ini: Dict[str, Dict[str, str]], 

342 expected_error: str, 

343 apio_runner: ApioRunner, 

344 capsys: CaptureFixture, 

345): 

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

347 exit with an error.""" 

348 

349 with apio_runner.in_sandbox() as sb: 

350 # -- Create the apio.ini file 

351 sb.write_apio_ini(apio_ini) 

352 

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

354 capsys.readouterr() # Reset capture 

355 with pytest.raises(SystemExit) as e: 

356 ApioContext( 

357 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

358 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

359 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

360 env_arg=env_arg, 

361 ) 

362 

363 # -- Check the errors. 

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

365 assert e.value.code == 1 

366 assert expected_error in capture 

367 

368 

369def test_validation_errors(apio_runner: ApioRunner, capsys: CaptureFixture): 

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

371 

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

373 error_tester( 

374 env_arg=None, 

375 apio_ini={ 

376 "[common]": { 

377 "board": "alhambra-ii", 

378 "top-module": "main", 

379 } 

380 }, 

381 expected_error=( 

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

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

384 ), 

385 apio_runner=apio_runner, 

386 capsys=capsys, 

387 ) 

388 

389 # -- Unknown board id. 

390 error_tester( 

391 env_arg=None, 

392 apio_ini={ 

393 "[env:default]": { 

394 "board": "no-such-board", 

395 } 

396 }, 

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

398 apio_runner=apio_runner, 

399 capsys=capsys, 

400 ) 

401 

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

403 error_tester( 

404 env_arg=None, 

405 apio_ini={ 

406 "[env:Default]": { 

407 "board": "alhambra-ii", 

408 "top-module": "main", 

409 } 

410 }, 

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

412 apio_runner=apio_runner, 

413 capsys=capsys, 

414 ) 

415 

416 # -- Env name has an extra space. 

417 error_tester( 

418 env_arg=None, 

419 apio_ini={ 

420 "[env: default]": { 

421 "board": "alhambra-ii", 

422 "top-module": "main", 

423 } 

424 }, 

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

426 apio_runner=apio_runner, 

427 capsys=capsys, 

428 ) 

429 

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

431 error_tester( 

432 env_arg=None, 

433 apio_ini={ 

434 "[apio]": { 

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

436 }, 

437 "[env:default]": { 

438 "board": "alhambra-ii", 

439 "top-module": "main", 

440 }, 

441 }, 

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

443 apio_runner=apio_runner, 

444 capsys=capsys, 

445 ) 

446 

447 # -- Missing required option. 

448 error_tester( 

449 env_arg=None, 

450 apio_ini={ 

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

452 }, 

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

454 "env 'default'.", 

455 apio_runner=apio_runner, 

456 capsys=capsys, 

457 ) 

458 

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

460 error_tester( 

461 env_arg="no-such-env", 

462 apio_ini={ 

463 "[env:default]": { 

464 "board": "alhambra-ii", 

465 "top-module": "main", 

466 }, 

467 }, 

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

469 apio_runner=apio_runner, 

470 capsys=capsys, 

471 ) 

472 

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

474 error_tester( 

475 env_arg="Default", 

476 apio_ini={ 

477 "[env:default]": { 

478 "board": "alhambra-ii", 

479 "top-module": "main", 

480 }, 

481 }, 

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

483 apio_runner=apio_runner, 

484 capsys=capsys, 

485 )