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

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

10Tests of project.py 

11""" 

12 

13import pytest 

14from tests.conftest import ApioRunner 

15from apio.managers.project import Project, ENV_OPTIONS_SPEC 

16from apio.apio_context import ( 

17 ApioContext, 

18 PackagesPolicy, 

19 ProjectPolicy, 

20 RemoteConfigPolicy, 

21) 

22 

23# TODO: Add more tests. 

24 

25 

26def load_apio_ini( 

27 apio_ini: dict[str, dict[str, str]], 

28 env_arg: str | None, 

29 apio_runner: ApioRunner, 

30) -> tuple[Project, str]: 

31 """A helper function load apio.ini. Returns (project, log-text)""" 

32 

33 with apio_runner.in_sandbox() as sb: 

34 # -- Create the apio.ini file 

35 sb.write_apio_ini(apio_ini) 

36 

37 # -- Try to create the context with the project info while 

38 # -- capturing log. 

39 with apio_runner.with_logger() as log: 

40 apio_ctx = ApioContext( 

41 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

42 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

43 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

44 env_arg=env_arg, 

45 ) 

46 

47 # -- Return the values. 

48 return (apio_ctx.project, log.out) 

49 

50 

51def test_env_options_specs(): 

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

53 

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

55 assert name == env_options_spec.name 

56 

57 

58def test_all_options_env(apio_runner: ApioRunner): 

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

60 

61 apio_ini = { 

62 "[env:default]": { 

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

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

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

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

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

68 "top-module": "my_module", 

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

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

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

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

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

74 } 

75 } 

76 

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

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

79 

80 project, _ = load_apio_ini( 

81 apio_ini=apio_ini, 

82 env_arg=None, 

83 apio_runner=apio_runner, 

84 ) 

85 

86 assert project.env_name == "default" 

87 assert project.env_options == { 

88 "board": "alhambra-ii", 

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

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

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

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

93 "top-module": "my_module", 

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

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

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

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

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

99 } 

100 

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

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

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

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

105 

106 

107def test_required_options_only_env(apio_runner: ApioRunner): 

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

109 

110 project, _ = load_apio_ini( 

111 apio_ini={ 

112 "[env:default]": { 

113 "board": "alhambra-ii", 

114 "top-module": "main", 

115 } 

116 }, 

117 env_arg=None, 

118 apio_runner=apio_runner, 

119 ) 

120 

121 assert project.env_name == "default" 

122 assert project.env_options == { 

123 "board": "alhambra-ii", 

124 "top-module": "main", 

125 } 

126 

127 

128def test_list_options(apio_runner: ApioRunner): 

129 """Tests list options.""" 

130 

131 project, _ = load_apio_ini( 

132 apio_ini={ 

133 "[env:default]": { 

134 "board": "alhambra-ii", 

135 "top-module": "my_top_module", 

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

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

138 } 

139 }, 

140 env_arg=None, 

141 apio_runner=apio_runner, 

142 ) 

143 

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

145 "k1=v1 k2=v2", 

146 "k3=v3", 

147 ] 

148 

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

150 "k5=v5 k6=v6", 

151 "k7=v7", 

152 ] 

153 

154 

155def test_macro_expansion(apio_runner: ApioRunner): 

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

157 

158 project, _ = load_apio_ini( 

159 apio_ini={ 

160 "[env:default]": { 

161 "board": "alhambra-ii", 

162 "top-module": "my_top_module", 

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

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

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

166 } 

167 }, 

168 env_arg=None, 

169 apio_runner=apio_runner, 

170 ) 

171 

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

173 

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

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

176 "k3=v3# default", 

177 ] 

178 

179 

180def test_legacy_env_section_rejected(apio_runner: ApioRunner): 

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

182 

183 error_tester( 

184 env_arg=None, 

185 apio_ini={ 

186 "[env]": { 

187 "board": "alhambra-ii", 

188 "top-module": "main", 

189 } 

190 }, 

191 expected_error="Rename it to [env:default]", 

192 apio_runner=apio_runner, 

193 ) 

194 

195 

196def test_first_env_is_default(apio_runner: ApioRunner): 

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

198 apio.ini is selected""" 

199 

200 project, _ = load_apio_ini( 

201 apio_ini={ 

202 "[common]": { 

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

204 }, 

205 "[env:env1]": { 

206 "board": "alhambra-ii", 

207 "top-module": "module1", 

208 }, 

209 "[env:env2]": { 

210 "board": "ice40-hx8k", 

211 "top-module": "module2", 

212 }, 

213 }, 

214 env_arg=None, 

215 apio_runner=apio_runner, 

216 ) 

217 

218 assert project.env_name == "env1" 

219 assert project.env_options == { 

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

221 "board": "alhambra-ii", 

222 "top-module": "module1", 

223 } 

224 

225 

226def test_env_selection_from_apio_ini(apio_runner: ApioRunner): 

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

228 using default-env option.""" 

229 

230 project, _ = load_apio_ini( 

231 apio_ini={ 

232 "[apio]": { 

233 "default-env": "env2", 

234 }, 

235 "[common]": { 

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

237 }, 

238 "[env:env1]": { 

239 "board": "alhambra-ii", 

240 "top-module": "module1", 

241 }, 

242 "[env:env2]": { 

243 "board": "ice40-hx8k", 

244 "top-module": "module2", 

245 }, 

246 }, 

247 env_arg=None, 

248 apio_runner=apio_runner, 

249 ) 

250 

251 assert project.env_name == "env2" 

252 assert project.env_options == { 

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

254 "board": "ice40-hx8k", 

255 "top-module": "module2", 

256 } 

257 

258 

259def test_env_selection_from_env_arg(apio_runner: ApioRunner): 

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

261 

262 project, _ = load_apio_ini( 

263 apio_ini={ 

264 "[apio]": { 

265 "default-env": "env1", 

266 }, 

267 "[common]": { 

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

269 }, 

270 "[env:env1]": { 

271 "board": "alhambra-ii", 

272 "top-module": "module1", 

273 }, 

274 "[env:env2]": { 

275 "board": "ice40-hx8k", 

276 "top-module": "module2", 

277 }, 

278 }, 

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

280 apio_runner=apio_runner, 

281 ) 

282 

283 assert project.env_name == "env2" 

284 assert project.env_options == { 

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

286 "board": "ice40-hx8k", 

287 "top-module": "module2", 

288 } 

289 

290 

291def error_tester( 

292 env_arg: str | None, 

293 apio_ini: dict[str, dict[str, str]], 

294 expected_error: str, 

295 apio_runner: ApioRunner, 

296): 

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

298 exit with an error.""" 

299 

300 with apio_runner.in_sandbox() as sb: 

301 # -- Create the apio.ini file 

302 sb.write_apio_ini(apio_ini) 

303 

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

305 with apio_runner.with_logger() as log: 

306 with pytest.raises(SystemExit) as e: 

307 ApioContext( 

308 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

309 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

310 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

311 env_arg=env_arg, 

312 ) 

313 

314 # -- Check the errors. 

315 assert e.value.code == 1 

316 assert expected_error in log.out 

317 

318 

319def test_validation_errors(apio_runner: ApioRunner): 

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

321 

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

323 error_tester( 

324 env_arg=None, 

325 apio_ini={ 

326 "[common]": { 

327 "board": "alhambra-ii", 

328 "top-module": "main", 

329 } 

330 }, 

331 expected_error=( 

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

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

334 ), 

335 apio_runner=apio_runner, 

336 ) 

337 

338 # -- Unknown board id. 

339 error_tester( 

340 env_arg=None, 

341 apio_ini={ 

342 "[env:default]": { 

343 "board": "no-such-board", 

344 } 

345 }, 

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

347 apio_runner=apio_runner, 

348 ) 

349 

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

351 error_tester( 

352 env_arg=None, 

353 apio_ini={ 

354 "[env:Default]": { 

355 "board": "alhambra-ii", 

356 "top-module": "main", 

357 } 

358 }, 

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

360 apio_runner=apio_runner, 

361 ) 

362 

363 # -- Env name has an extra space. 

364 error_tester( 

365 env_arg=None, 

366 apio_ini={ 

367 "[env: default]": { 

368 "board": "alhambra-ii", 

369 "top-module": "main", 

370 } 

371 }, 

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

373 apio_runner=apio_runner, 

374 ) 

375 

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

377 error_tester( 

378 env_arg=None, 

379 apio_ini={ 

380 "[apio]": { 

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

382 }, 

383 "[env:default]": { 

384 "board": "alhambra-ii", 

385 "top-module": "main", 

386 }, 

387 }, 

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

389 apio_runner=apio_runner, 

390 ) 

391 

392 # -- Missing required option. 

393 error_tester( 

394 env_arg=None, 

395 apio_ini={ 

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

397 }, 

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

399 "env 'default'.", 

400 apio_runner=apio_runner, 

401 ) 

402 

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

404 error_tester( 

405 env_arg="no-such-env", 

406 apio_ini={ 

407 "[env:default]": { 

408 "board": "alhambra-ii", 

409 "top-module": "main", 

410 }, 

411 }, 

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

413 apio_runner=apio_runner, 

414 ) 

415 

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

417 error_tester( 

418 env_arg="Default", 

419 apio_ini={ 

420 "[env:default]": { 

421 "board": "alhambra-ii", 

422 "top-module": "main", 

423 }, 

424 }, 

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

426 apio_runner=apio_runner, 

427 )