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

71 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-06 10:20 +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 

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

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

51 

52 apio_ini = { 

53 "[env:default]": { 

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

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

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

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

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

59 "top-module": "my_module", 

60 "yosys-synth-extra-options": "-dsp -xyz", 

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

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

63 } 

64 } 

65 

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

67 assert len(apio_ini["[env:default]"]) == len(ENV_OPTIONS) 

68 

69 project, _ = load_apio_ini( 

70 apio_ini=apio_ini, 

71 env_arg=None, 

72 apio_runner=apio_runner, 

73 capsys=capsys, 

74 ) 

75 

76 assert project.env_name == "default" 

77 assert project.env_options == { 

78 "board": "alhambra-ii", 

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

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

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

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

83 "top-module": "my_module", 

84 "yosys-synth-extra-options": ["-dsp -xyz"], 

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

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

87 } 

88 

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

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

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

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

93 

94 

95def test_required_options_only_env( 

96 apio_runner: ApioRunner, capsys: LogCaptureFixture 

97): 

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

99 

100 project, stdout = load_apio_ini( 

101 apio_ini={ 

102 "[env:default]": { 

103 "board": "alhambra-ii", 

104 } 

105 }, 

106 env_arg=None, 

107 apio_runner=apio_runner, 

108 capsys=capsys, 

109 ) 

110 

111 assert project.env_name == "default" 

112 assert project.env_options == { 

113 "board": "alhambra-ii", 

114 "top-module": "main", 

115 } 

116 assert ( 

117 "Option 'top-module' is missing for env default, assuming 'main'" 

118 in stdout 

119 ) 

120 

121 

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

123 """Tests list optionss.""" 

124 

125 project, _ = load_apio_ini( 

126 apio_ini={ 

127 "[env:default]": { 

128 "board": "alhambra-ii", 

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

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

131 } 

132 }, 

133 env_arg=None, 

134 apio_runner=apio_runner, 

135 capsys=capsys, 

136 ) 

137 

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

139 "k1=v1 k2=v2", 

140 "k3=v3", 

141 ] 

142 

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

144 "k5=v5 k6=v6", 

145 "k7=v7", 

146 ] 

147 

148 

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

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

151 be converted to the canonical board id""" 

152 

153 project, stdout = load_apio_ini( 

154 apio_ini={ 

155 "[env:default]": { 

156 "board": "iCE40-HX8K", 

157 "top-module": "my_top_module", 

158 } 

159 }, 

160 env_arg=None, 

161 apio_runner=apio_runner, 

162 capsys=capsys, 

163 ) 

164 

165 assert project.env_name == "default" 

166 assert project.env_options == { 

167 "board": "ice40-hx8k", 

168 "top-module": "my_top_module", 

169 } 

170 assert ( 

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

172 "Please update apio.ini" in stdout 

173 ) 

174 

175 

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

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

178 

179 project, stdout = load_apio_ini( 

180 apio_ini={ 

181 "[env]": { 

182 "board": "alhambra-ii", 

183 } 

184 }, 

185 env_arg=None, 

186 apio_runner=apio_runner, 

187 capsys=capsys, 

188 ) 

189 

190 assert project.env_name == "default" 

191 assert project.env_options == { 

192 "board": "alhambra-ii", 

193 "top-module": "main", 

194 } 

195 assert ( 

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

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

198 ) 

199 

200 

201def test_first_env_is_default( 

202 apio_runner: ApioRunner, capsys: LogCaptureFixture 

203): 

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

205 apio.ini is selected""" 

206 

207 project, _ = load_apio_ini( 

208 apio_ini={ 

209 "[common]": { 

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

211 }, 

212 "[env:env1]": { 

213 "board": "alhambra-ii", 

214 "top-module": "module1", 

215 }, 

216 "[env:env2]": { 

217 "board": "ice40-hx8k", 

218 "top-module": "module2", 

219 }, 

220 }, 

221 env_arg=None, 

222 apio_runner=apio_runner, 

223 capsys=capsys, 

224 ) 

225 

226 assert project.env_name == "env1" 

227 assert project.env_options == { 

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

229 "board": "alhambra-ii", 

230 "top-module": "module1", 

231 } 

232 

233 

234def test_env_selection_from_apio_ini( 

235 apio_runner: ApioRunner, capsys: LogCaptureFixture 

236): 

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

238 using default-env option.""" 

239 

240 project, _ = load_apio_ini( 

241 apio_ini={ 

242 "[apio]": { 

243 "default-env": "env2", 

244 }, 

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

263 assert project.env_options == { 

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

265 "board": "ice40-hx8k", 

266 "top-module": "module2", 

267 } 

268 

269 

270def test_env_selection_from_env_arg( 

271 apio_runner: ApioRunner, capsys: LogCaptureFixture 

272): 

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

274 

275 project, _ = load_apio_ini( 

276 apio_ini={ 

277 "[apio]": { 

278 "default-env": "env1", 

279 }, 

280 "[common]": { 

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

282 }, 

283 "[env:env1]": { 

284 "board": "alhambra-ii", 

285 "top-module": "module1", 

286 }, 

287 "[env:env2]": { 

288 "board": "ice40-hx8k", 

289 "top-module": "module2", 

290 }, 

291 }, 

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

293 apio_runner=apio_runner, 

294 capsys=capsys, 

295 ) 

296 

297 assert project.env_name == "env2" 

298 assert project.env_options == { 

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

300 "board": "ice40-hx8k", 

301 "top-module": "module2", 

302 } 

303 

304 

305def error_tester( 

306 env_arg: Optional[str], 

307 apio_ini: Dict[str, Dict[str, str]], 

308 expected_error: str, 

309 apio_runner: ApioRunner, 

310 capsys: LogCaptureFixture, 

311): 

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

313 exit with an error.""" 

314 

315 with apio_runner.in_sandbox() as sb: 

316 # -- Create the apio.ini file 

317 sb.write_apio_ini(apio_ini) 

318 

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

320 capsys.readouterr() # Reset capture 

321 with pytest.raises(SystemExit) as e: 

322 ApioContext( 

323 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

324 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

325 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

326 env_arg=env_arg, 

327 ) 

328 

329 # -- Check the errors. 

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

331 assert e.value.code == 1 

332 assert expected_error in capture 

333 

334 

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

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

337 

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

339 error_tester( 

340 env_arg=None, 

341 apio_ini={ 

342 "[common]": { 

343 "board": "alhambra-ii", 

344 "top-module": "main", 

345 } 

346 }, 

347 expected_error=( 

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

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

350 ), 

351 apio_runner=apio_runner, 

352 capsys=capsys, 

353 ) 

354 

355 # -- Unknown board id. 

356 error_tester( 

357 env_arg=None, 

358 apio_ini={ 

359 "[env:default]": { 

360 "board": "no-such-board", 

361 } 

362 }, 

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

364 apio_runner=apio_runner, 

365 capsys=capsys, 

366 ) 

367 

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

369 error_tester( 

370 env_arg=None, 

371 apio_ini={ 

372 "[env:Default]": { 

373 "board": "alhambra-ii", 

374 } 

375 }, 

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

377 apio_runner=apio_runner, 

378 capsys=capsys, 

379 ) 

380 

381 # -- Env name has an extra space. 

382 error_tester( 

383 env_arg=None, 

384 apio_ini={ 

385 "[env: default]": { 

386 "board": "alhambra-ii", 

387 } 

388 }, 

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

390 apio_runner=apio_runner, 

391 capsys=capsys, 

392 ) 

393 

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

395 error_tester( 

396 env_arg=None, 

397 apio_ini={ 

398 "[apio]": { 

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

400 }, 

401 "[env:default]": { 

402 "board": "alhambra-ii", 

403 }, 

404 }, 

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

406 apio_runner=apio_runner, 

407 capsys=capsys, 

408 ) 

409 

410 # -- Missing required option. 

411 error_tester( 

412 env_arg=None, 

413 apio_ini={ 

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

415 }, 

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

417 "env 'default'.", 

418 apio_runner=apio_runner, 

419 capsys=capsys, 

420 ) 

421 

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

423 error_tester( 

424 env_arg="no-such-env", 

425 apio_ini={ 

426 "[env:default]": {"board": "alhambra-ii"}, 

427 }, 

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

429 apio_runner=apio_runner, 

430 capsys=capsys, 

431 ) 

432 

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

434 error_tester( 

435 env_arg="Default", 

436 apio_ini={ 

437 "[env:default]": {"board": "alhambra-ii"}, 

438 }, 

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

440 apio_runner=apio_runner, 

441 capsys=capsys, 

442 )