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

135 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-23 03:53 +0000

1""" 

2Tests of the apio.managers.programmers.py module. 

3""" 

4 

5from pytest import raises 

6from tests.conftest import ApioRunner 

7from apio.apio_context import ( 

8 ApioContext, 

9 PackagesPolicy, 

10 ProjectPolicy, 

11 RemoteConfigPolicy, 

12) 

13from apio.utils.usb_util import UsbDevice 

14from apio.utils.serial_util import SerialDevice 

15 

16from apio.managers.programmers import ( 

17 _construct_cmd_template, 

18 _construct_programmer_cmd, 

19 _DeviceScanner, 

20) 

21 

22 

23class FakeDeviceScanner(_DeviceScanner): 

24 """A fake device scanner for testing.""" 

25 

26 def __init__( 

27 self, 

28 apio_ctx: ApioContext, 

29 usb_devices: list[UsbDevice] | None = None, 

30 serial_devices: list[SerialDevice] | None = None, 

31 ): 

32 super().__init__(apio_ctx) 

33 self._usb_devices = usb_devices 

34 self._serial_devices = serial_devices 

35 

36 # @override 

37 def get_usb_devices(self) -> list[UsbDevice]: 

38 """Returns the fake usb devices.""" 

39 assert self._usb_devices 

40 return self._usb_devices 

41 

42 # @override 

43 def get_serial_devices(self) -> list[SerialDevice]: 

44 """Returns the fake serial devices.""" 

45 assert self._serial_devices 

46 return self._serial_devices 

47 

48 

49def fake_usb_device( 

50 *, 

51 vid="0403", 

52 pid="6010", 

53 bus=0, 

54 dev=0, 

55 manuf="AlhambraBits", 

56 prod="Alhambra II v1.0A", 

57 sn="SNXXXX", 

58 device_type="FT2232H", 

59) -> UsbDevice: 

60 """Create a fake usb device for resting.""" 

61 # pylint: disable=too-many-arguments 

62 return UsbDevice( 

63 vid=vid, 

64 pid=pid, 

65 bus=bus, 

66 device=dev, 

67 manufacturer=manuf, 

68 product=prod, 

69 serial_number=sn, 

70 device_type=device_type, 

71 ) 

72 

73 

74def fake_serial_device( 

75 *, 

76 port_name="port0", 

77 vid="04D8", 

78 pid="FFEE", 

79 manuf="IceFUN", 

80 prod="Ice Fun", 

81 sn="SNXXXX", 

82 device_type="FT2232H", 

83 location="0.1", 

84) -> SerialDevice: 

85 """Create a fake serial device for resting.""" 

86 # pylint: disable=too-many-arguments 

87 return SerialDevice( 

88 port="/dev/" + port_name, 

89 port_name=port_name, 

90 vid=vid, 

91 pid=pid, 

92 manufacturer=manuf, 

93 product=prod, 

94 serial_number=sn, 

95 device_type=device_type, 

96 location=location, 

97 ) 

98 

99 

100def test_default_cmd_template(apio_runner: ApioRunner): 

101 """Tests _construct_cmd_template() with the default board template.""" 

102 

103 with apio_runner.in_sandbox() as sb: 

104 

105 # -- Construct an apio context. 

106 sb.write_apio_ini( 

107 { 

108 "[env:default]": { 

109 "board": "alhambra-ii", 

110 "top-module": "main", 

111 } 

112 } 

113 ) 

114 

115 # -- Run while capturing log. 

116 with apio_runner.with_logger() as log: 

117 apio_ctx = ApioContext( 

118 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

119 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

120 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

121 ) 

122 programmer_cmd = _construct_cmd_template(apio_ctx) 

123 

124 # -- Check result. 

125 assert ( 

126 programmer_cmd == "openFPGALoader --force-terminal-mode --verify " 

127 "-b ice40_generic " 

128 "--vid ${VID} --pid ${PID} " 

129 "--busdev-num ${BUS}:${DEV} " 

130 "${BIN_FILE}" 

131 ) 

132 

133 # -- Check no 'custom' warning. 

134 assert "Using custom programmer cmd" not in log.out 

135 

136 

137def test_custom_cmd_template(apio_runner: ApioRunner): 

138 """Tests _construct_cmd_template() with custom command template.""" 

139 

140 with apio_runner.in_sandbox() as sb: 

141 

142 # -- Construct an apio context. 

143 sb.write_apio_ini( 

144 { 

145 "[env:default]": { 

146 "board": "alhambra-ii", 

147 "top-module": "main", 

148 "programmer-cmd": "my template ${VID} ${PID}", 

149 } 

150 } 

151 ) 

152 

153 apio_ctx = ApioContext( 

154 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

155 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

156 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

157 ) 

158 

159 with apio_runner.with_logger() as log: 

160 programmer_cmd = _construct_cmd_template(apio_ctx) 

161 

162 # -- Check the result. 

163 assert programmer_cmd == "my template ${VID} ${PID}" 

164 

165 # -- Check the 'custom' warning. 

166 assert "Using custom programmer cmd" in log.out 

167 

168 

169def test_get_cmd_usb(apio_runner: ApioRunner): 

170 """Test generation of a programmer command for a usb device.""" 

171 with apio_runner.in_sandbox() as sb: 

172 

173 # -- Create a fake apio.ini file. 

174 sb.write_apio_ini( 

175 { 

176 "[env:default]": { 

177 "board": "alhambra-ii", 

178 "top-module": "main", 

179 "programmer-cmd": ( 

180 "my-programmer --bus ${BUS} --dev ${DEV} " 

181 "--vid ${VID} --pid ${PID} " 

182 "--serial-num ${SERIAL_NUM} --bin-file ${BIN_FILE}" 

183 ), 

184 } 

185 } 

186 ) 

187 

188 # -- Construct the apio context. 

189 apio_ctx = ApioContext( 

190 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

191 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

192 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

193 ) 

194 

195 # -- Create fake devices 

196 scanner = FakeDeviceScanner( 

197 apio_ctx, 

198 usb_devices=[ 

199 fake_usb_device(dev=0, prod="non alhambra"), 

200 fake_usb_device(dev=1), 

201 fake_usb_device(dev=2, prod="non alhambra"), 

202 ], 

203 ) 

204 

205 # -- Call the tested function while capturing log. 

206 with apio_runner.with_logger() as log: 

207 cmd = _construct_programmer_cmd( 

208 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

209 ) 

210 

211 # -- Test the result programmer command. 

212 assert cmd == ( 

213 "my-programmer --bus 0 --dev 1 --vid 0403 --pid 6010 " 

214 "--serial-num SNXXXX --bin-file $SOURCE" 

215 ) 

216 

217 # -- Check the log. 

218 assert "Scanning for a USB device:" in log.out 

219 assert 'FILTER [VID=0403, PID=6010, REGEX="^Alhambra II.*"]' in log.out 

220 assert ( 

221 "DEVICE [0403:6010] [0:1] [AlhambraBits] " 

222 "[Alhambra II v1.0A] [SNXXXX]" 

223 ) in log.out 

224 

225 

226def test_get_cmd_usb_no_match(apio_runner: ApioRunner): 

227 """Test command generation error when the usb device is not found.""" 

228 with apio_runner.in_sandbox() as sb: 

229 

230 # -- Create a fake apio.ini file. 

231 sb.write_apio_ini( 

232 { 

233 "[env:default]": { 

234 "board": "alhambra-ii", 

235 "top-module": "main", 

236 "programmer-cmd": "my-programmer ${VID} ${PID}", 

237 } 

238 } 

239 ) 

240 

241 # -- Construct the apio context. 

242 apio_ctx = ApioContext( 

243 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

244 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

245 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

246 ) 

247 

248 # -- Create fake devices 

249 scanner = FakeDeviceScanner( 

250 apio_ctx, 

251 usb_devices=[ 

252 fake_usb_device(dev=0, prod="non alhambra"), 

253 fake_usb_device(dev=2, prod="non alhambra"), 

254 ], 

255 ) 

256 

257 # -- Call the tested function while capturing log 

258 with apio_runner.with_logger() as log: 

259 with raises(SystemExit) as e: 

260 _construct_programmer_cmd( 

261 apio_ctx, 

262 scanner, 

263 serial_port_flag=None, 

264 serial_num_flag=None, 

265 ) 

266 

267 # -- Verify. 

268 assert e.value.code == 1 

269 

270 assert "Scanning for a USB device:" in log.out 

271 assert 'FILTER [VID=0403, PID=6010, REGEX="^Alhambra II.*"]' in log.out 

272 assert "No matching USB device" in log.out 

273 

274 

275def test_get_cmd_usb_multiple_matches(apio_runner: ApioRunner): 

276 """Test command generation error when multiple usb devices match the 

277 filter.""" 

278 with apio_runner.in_sandbox() as sb: 

279 

280 # -- Create a fake apio.ini file. 

281 sb.write_apio_ini( 

282 { 

283 "[env:default]": { 

284 "board": "alhambra-ii", 

285 "top-module": "main", 

286 "programmer-cmd": "my-programmer ${VID} ${PID}", 

287 } 

288 } 

289 ) 

290 

291 # -- Construct the apio context. 

292 apio_ctx = ApioContext( 

293 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

294 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

295 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

296 ) 

297 

298 # -- Create fake devices 

299 scanner = FakeDeviceScanner( 

300 apio_ctx, 

301 usb_devices=[ 

302 fake_usb_device(dev=0, sn="SN001"), 

303 fake_usb_device(dev=1, prod="non alhambra"), 

304 fake_usb_device(dev=2, sn="SN002"), 

305 ], 

306 ) 

307 

308 # -- Call the tested function while capturing log. 

309 with apio_runner.with_logger() as log: 

310 with raises(SystemExit) as e: 

311 _construct_programmer_cmd( 

312 apio_ctx, 

313 scanner, 

314 serial_port_flag=None, 

315 serial_num_flag=None, 

316 ) 

317 

318 # -- Verify. 

319 assert e.value.code == 1 

320 

321 assert "Scanning for a USB device:" in log.out 

322 assert 'FILTER [VID=0403, PID=6010, REGEX="^Alhambra II.*"]' in log.out 

323 assert ( 

324 "DEVICE [0403:6010] [0:0] [AlhambraBits] " 

325 "[Alhambra II v1.0A] [SN001]" 

326 ) in log.out 

327 assert ( 

328 "DEVICE [0403:6010] [0:2] [AlhambraBits] " 

329 "[Alhambra II v1.0A] [SN002]" 

330 ) in log.out 

331 assert "Error: Found multiple matching usb devices" in log.out 

332 

333 

334def test_get_cmd_serial(apio_runner: ApioRunner): 

335 """Test generation of a programmer command for a serial device.""" 

336 with apio_runner.in_sandbox() as sb: 

337 

338 # -- Create a fake apio.ini file. 

339 sb.write_apio_ini( 

340 { 

341 "[env:default]": { 

342 "board": "icefun", 

343 "top-module": "main", 

344 "programmer-cmd": "my-programmer --port ${SERIAL_PORT}", 

345 } 

346 } 

347 ) 

348 

349 # -- Construct the apio context. 

350 apio_ctx = ApioContext( 

351 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

352 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

353 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

354 ) 

355 

356 # -- Create fake devices 

357 scanner = FakeDeviceScanner( 

358 apio_ctx, 

359 serial_devices=[ 

360 fake_serial_device(port_name="port1", pid="1234"), 

361 fake_serial_device(port_name="port2"), 

362 fake_serial_device(port_name="port3", pid="1234"), 

363 ], 

364 ) 

365 

366 # -- Call the tested function while capturing log. 

367 with apio_runner.with_logger() as log: 

368 cmd = _construct_programmer_cmd( 

369 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

370 ) 

371 

372 # -- Test the result programmer command. 

373 assert cmd == "my-programmer --port /dev/port2" 

374 

375 # -- Check the log. 

376 assert "Scanning for a serial device:" in log.out 

377 assert "FILTER [VID=04D8, PID=FFEE]" in log.out 

378 assert ( 

379 "DEVICE [/dev/port2] [04D8:FFEE] [IceFUN] [Ice Fun] [SNXXXX]" 

380 in log.out 

381 ) 

382 

383 

384def test_get_cmd_serial_no_match( 

385 apio_runner: ApioRunner, 

386): 

387 """Test command generation error when the serial device is not found.""" 

388 with apio_runner.in_sandbox() as sb: 

389 

390 # -- Create a fake apio.ini file. 

391 sb.write_apio_ini( 

392 { 

393 "[env:default]": { 

394 "board": "icefun", 

395 "top-module": "main", 

396 "programmer-cmd": "my-programmer --port ${SERIAL_PORT}", 

397 } 

398 } 

399 ) 

400 

401 # -- Construct the apio context. 

402 apio_ctx = ApioContext( 

403 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

404 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

405 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

406 ) 

407 

408 # -- Create fake devices 

409 scanner = FakeDeviceScanner( 

410 apio_ctx, 

411 serial_devices=[ 

412 fake_serial_device(port_name="port1", pid="1234"), 

413 fake_serial_device(port_name="port3", pid="1234"), 

414 ], 

415 ) 

416 

417 # -- Call the tested function while capturing log 

418 with apio_runner.with_logger() as log: 

419 with raises(SystemExit) as e: 

420 _construct_programmer_cmd( 

421 apio_ctx, 

422 scanner, 

423 serial_port_flag=None, 

424 serial_num_flag=None, 

425 ) 

426 

427 # -- Verify 

428 assert e.value.code == 1 

429 

430 assert "Scanning for a serial device:" in log.out 

431 assert "FILTER [VID=04D8, PID=FFEE]" in log.out 

432 assert "No matching serial device" in log.out 

433 

434 

435def test_get_cmd_serial_multiple_matches(apio_runner: ApioRunner): 

436 """Test command generation error when multiple serial devices match the 

437 filter.""" 

438 with apio_runner.in_sandbox() as sb: 

439 

440 # -- Create a fake apio.ini file. 

441 sb.write_apio_ini( 

442 { 

443 "[env:default]": { 

444 "board": "icefun", 

445 "top-module": "main", 

446 "programmer-cmd": "my-programmer --port ${SERIAL_PORT}", 

447 } 

448 } 

449 ) 

450 

451 # -- Construct the apio context. 

452 apio_ctx = ApioContext( 

453 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

454 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

455 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

456 ) 

457 

458 # -- Create fake devices 

459 scanner = FakeDeviceScanner( 

460 apio_ctx, 

461 serial_devices=[ 

462 fake_serial_device(port_name="port1"), 

463 fake_serial_device(port_name="port2", pid="1234"), 

464 fake_serial_device(port_name="port3"), 

465 ], 

466 ) 

467 

468 # -- Call the tested function while capturing log 

469 with apio_runner.with_logger() as log: 

470 with raises(SystemExit) as e: 

471 _construct_programmer_cmd( 

472 apio_ctx, 

473 scanner, 

474 serial_port_flag=None, 

475 serial_num_flag=None, 

476 ) 

477 

478 # -- Verify. 

479 assert e.value.code == 1 

480 

481 assert "Scanning for a serial device:" in log.out 

482 assert "FILTER [VID=04D8, PID=FFEE]" in log.out 

483 assert ( 

484 "DEVICE [/dev/port1] [04D8:FFEE] [IceFUN] [Ice Fun] [SNXXXX]" 

485 ) in log.out 

486 assert ( 

487 "DEVICE [/dev/port3] [04D8:FFEE] [IceFUN] [Ice Fun] [SNXXXX]" 

488 ) in log.out 

489 assert "Error: Found multiple matching serial devices" in log.out 

490 

491 

492def test_device_presence_ok(apio_runner: ApioRunner): 

493 """Test generation of a presence check only device.""" 

494 with apio_runner.in_sandbox() as sb: 

495 

496 # -- Create a fake apio.ini file. 

497 sb.write_apio_ini( 

498 { 

499 "[env:default]": { 

500 "board": "alhambra-ii", 

501 "top-module": "main", 

502 # -- The command has no serial or usb vars. 

503 "programmer-cmd": "my programmer command ${BIN_FILE}", 

504 } 

505 } 

506 ) 

507 

508 # -- Construct the apio context. 

509 apio_ctx = ApioContext( 

510 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

511 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

512 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

513 ) 

514 

515 # -- Create fake devices, with two matching devices. 

516 scanner = FakeDeviceScanner( 

517 apio_ctx, 

518 usb_devices=[ 

519 fake_usb_device(dev=0), 

520 fake_usb_device(dev=1, prod="non alhambra"), 

521 fake_usb_device(dev=2), 

522 ], 

523 ) 

524 

525 # -- Call the tested function while capturing log. 

526 with apio_runner.with_logger() as log: 

527 cmd = _construct_programmer_cmd( 

528 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

529 ) 

530 

531 # -- Test the result programmer command. 

532 assert cmd == "my programmer command $SOURCE" 

533 

534 # -- Check the log. 

535 assert "Checking device presence" in log.out 

536 assert 'FILTER [VID=0403, PID=6010, REGEX="^Alhambra II.*"]' in log.out 

537 assert ( 

538 "DEVICE [0403:6010] [0:0] [AlhambraBits] " 

539 "[Alhambra II v1.0A] [SNXXXX]" 

540 ) in log.out 

541 assert ( 

542 "DEVICE [0403:6010] [0:2] [AlhambraBits] " 

543 "[Alhambra II v1.0A] [SNXXXX]" 

544 ) in log.out 

545 

546 

547def test_device_presence_not_found(apio_runner: ApioRunner): 

548 """Test generation of a presence only device, with no device.""" 

549 with apio_runner.in_sandbox() as sb: 

550 

551 # -- Create a fake apio.ini file. 

552 sb.write_apio_ini( 

553 { 

554 "[env:default]": { 

555 "board": "alhambra-ii", 

556 "top-module": "main", 

557 # -- The command has no serial or usb vars. 

558 "programmer-cmd": "my programmer command ${BIN_FILE}", 

559 } 

560 } 

561 ) 

562 

563 # -- Construct the apio context. 

564 apio_ctx = ApioContext( 

565 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

566 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

567 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

568 ) 

569 

570 # -- Create fake devices, with two matching devices. 

571 scanner = FakeDeviceScanner( 

572 apio_ctx, 

573 usb_devices=[ 

574 fake_usb_device(dev=0, prod="non alhambra"), 

575 fake_usb_device(dev=1, prod="non alhambra"), 

576 ], 

577 ) 

578 

579 # -- Call the tested function while capturing log. 

580 with apio_runner.with_logger() as log: 

581 with raises(SystemExit) as e: 

582 _construct_programmer_cmd( 

583 apio_ctx, 

584 scanner, 

585 serial_port_flag=None, 

586 serial_num_flag=None, 

587 ) 

588 

589 # -- Verify 

590 assert e.value.code == 1 

591 

592 assert "Checking device presence" in log.out 

593 assert 'FILTER [VID=0403, PID=6010, REGEX="^Alhambra II.*"]' in log.out 

594 assert "Error: No matching device." in log.out