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

134 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-10 03:35 +0000

1""" 

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

3""" 

4 

5from typing import List 

6from pytest import LogCaptureFixture, raises 

7from tests.conftest import ApioRunner 

8from apio.apio_context import ( 

9 ApioContext, 

10 PackagesPolicy, 

11 ProjectPolicy, 

12 RemoteConfigPolicy, 

13) 

14from apio.utils.usb_util import UsbDevice 

15from apio.utils.serial_util import SerialDevice 

16 

17from apio.managers.programmers import ( 

18 _construct_cmd_template, 

19 _construct_programmer_cmd, 

20 _DeviceScanner, 

21) 

22 

23 

24class FakeDeviceScanner(_DeviceScanner): 

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

26 

27 def __init__( 

28 self, 

29 usb_devices: List[UsbDevice] = None, 

30 serial_devices: List[SerialDevice] = None, 

31 ): 

32 super().__init__(apio_ctx=None) 

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[UsbDevice]: 

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 vendor_id=vid, 

64 product_id=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) -> UsbDevice: 

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 vendor_id=vid, 

91 product_id=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( 

101 apio_runner: ApioRunner, capsys: LogCaptureFixture 

102): 

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

104 

105 with apio_runner.in_sandbox() as sb: 

106 

107 # -- Construct an apio context. 

108 sb.write_apio_ini( 

109 { 

110 "[env:default]": { 

111 "board": "alhambra-ii", 

112 "top-module": "main", 

113 } 

114 } 

115 ) 

116 

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 capsys.readouterr().out 

135 

136 

137def test_custom_cmd_template( 

138 apio_runner: ApioRunner, capsys: LogCaptureFixture 

139): 

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

141 

142 with apio_runner.in_sandbox() as sb: 

143 

144 # -- Construct an apio context. 

145 sb.write_apio_ini( 

146 { 

147 "[env:default]": { 

148 "board": "alhambra-ii", 

149 "top-module": "main", 

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

151 } 

152 } 

153 ) 

154 

155 apio_ctx = ApioContext( 

156 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

157 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

158 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

159 ) 

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 capsys.readouterr().out 

167 

168 

169def test_get_cmd_usb(apio_runner: ApioRunner, capsys: LogCaptureFixture): 

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 usb_devices=[ 

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

199 fake_usb_device(dev=1), 

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

201 ], 

202 ) 

203 

204 # -- Call the tested function 

205 cmd = _construct_programmer_cmd( 

206 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

207 ) 

208 

209 # -- Test the result programmer command. 

210 assert cmd == ( 

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

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

213 ) 

214 

215 # -- Check the log. 

216 log = capsys.readouterr().out 

217 assert "Scanning for a USB device:" in log 

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

219 assert ( 

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

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

222 ) in log 

223 

224 

225def test_get_cmd_usb_no_match( 

226 apio_runner: ApioRunner, capsys: LogCaptureFixture 

227): 

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

229 with apio_runner.in_sandbox() as sb: 

230 

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

232 sb.write_apio_ini( 

233 { 

234 "[env:default]": { 

235 "board": "alhambra-ii", 

236 "top-module": "main", 

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

238 } 

239 } 

240 ) 

241 

242 # -- Construct the apio context. 

243 apio_ctx = ApioContext( 

244 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

245 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

246 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

247 ) 

248 

249 # -- Create fake devices 

250 scanner = FakeDeviceScanner( 

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 

258 

259 with raises(SystemExit) as e: 

260 _construct_programmer_cmd( 

261 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

262 ) 

263 

264 assert e.value.code == 1 

265 

266 log = capsys.readouterr().out 

267 assert "Scanning for a USB device:" in log 

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

269 assert "No matching USB device" in log 

270 

271 

272def test_get_cmd_usb_multiple_matches( 

273 apio_runner: ApioRunner, capsys: LogCaptureFixture 

274): 

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

276 filter.""" 

277 with apio_runner.in_sandbox() as sb: 

278 

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

280 sb.write_apio_ini( 

281 { 

282 "[env:default]": { 

283 "board": "alhambra-ii", 

284 "top-module": "main", 

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

286 } 

287 } 

288 ) 

289 

290 # -- Construct the apio context. 

291 apio_ctx = ApioContext( 

292 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

293 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

294 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

295 ) 

296 

297 # -- Create fake devices 

298 scanner = FakeDeviceScanner( 

299 usb_devices=[ 

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

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

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

303 ], 

304 ) 

305 

306 # -- Call the tested function 

307 with raises(SystemExit) as e: 

308 _construct_programmer_cmd( 

309 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

310 ) 

311 

312 assert e.value.code == 1 

313 

314 log = capsys.readouterr().out 

315 assert "Scanning for a USB device:" in log 

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

317 assert ( 

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

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

320 ) in log 

321 assert ( 

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

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

324 ) in log 

325 assert "Error: Found multiple matching usb devices" in log 

326 

327 

328def test_get_cmd_serial(apio_runner: ApioRunner, capsys: LogCaptureFixture): 

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

330 with apio_runner.in_sandbox() as sb: 

331 

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

333 sb.write_apio_ini( 

334 { 

335 "[env:default]": { 

336 "board": "icefun", 

337 "top-module": "main", 

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

339 } 

340 } 

341 ) 

342 

343 # -- Construct the apio context. 

344 apio_ctx = ApioContext( 

345 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

346 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

347 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

348 ) 

349 

350 # -- Create fake devices 

351 scanner = FakeDeviceScanner( 

352 serial_devices=[ 

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

354 fake_serial_device(port_name="port2"), 

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

356 ], 

357 ) 

358 

359 # -- Call the tested function 

360 cmd = _construct_programmer_cmd( 

361 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

362 ) 

363 

364 # -- Test the result programmer command. 

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

366 

367 # -- Check the log. 

368 log = capsys.readouterr().out 

369 assert "Scanning for a serial device:" in log 

370 assert "FILTER [VID=04D8, PID=FFEE]" in log 

371 assert ( 

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

373 in log 

374 ) 

375 

376 

377def test_get_cmd_serial_no_match( 

378 apio_runner: ApioRunner, capsys: LogCaptureFixture 

379): 

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

381 with apio_runner.in_sandbox() as sb: 

382 

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

384 sb.write_apio_ini( 

385 { 

386 "[env:default]": { 

387 "board": "icefun", 

388 "top-module": "main", 

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

390 } 

391 } 

392 ) 

393 

394 # -- Construct the apio context. 

395 apio_ctx = ApioContext( 

396 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

397 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

398 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

399 ) 

400 

401 # -- Create fake devices 

402 scanner = FakeDeviceScanner( 

403 serial_devices=[ 

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

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

406 ], 

407 ) 

408 

409 # -- Call the tested function 

410 with raises(SystemExit) as e: 

411 _construct_programmer_cmd( 

412 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

413 ) 

414 

415 assert e.value.code == 1 

416 

417 log = capsys.readouterr().out 

418 assert "Scanning for a serial device:" in log 

419 assert "FILTER [VID=04D8, PID=FFEE]" in log 

420 assert "No matching serial device" in log 

421 

422 

423def test_get_cmd_serial_multiple_matches( 

424 apio_runner: ApioRunner, capsys: LogCaptureFixture 

425): 

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

427 filter.""" 

428 with apio_runner.in_sandbox() as sb: 

429 

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

431 sb.write_apio_ini( 

432 { 

433 "[env:default]": { 

434 "board": "icefun", 

435 "top-module": "main", 

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

437 } 

438 } 

439 ) 

440 

441 # -- Construct the apio context. 

442 apio_ctx = ApioContext( 

443 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

444 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

445 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

446 ) 

447 

448 # -- Create fake devices 

449 scanner = FakeDeviceScanner( 

450 serial_devices=[ 

451 fake_serial_device(port_name="port1"), 

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

453 fake_serial_device(port_name="port3"), 

454 ], 

455 ) 

456 

457 # -- Call the tested function 

458 with raises(SystemExit) as e: 

459 _construct_programmer_cmd( 

460 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

461 ) 

462 

463 assert e.value.code == 1 

464 

465 log = capsys.readouterr().out 

466 assert "Scanning for a serial device:" in log 

467 assert "FILTER [VID=04D8, PID=FFEE]" in log 

468 assert ( 

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

470 ) in log 

471 assert ( 

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

473 ) in log 

474 assert "Error: Found multiple matching serial devices" in log 

475 

476 

477def test_device_presence_ok( 

478 apio_runner: ApioRunner, capsys: LogCaptureFixture 

479): 

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

481 with apio_runner.in_sandbox() as sb: 

482 

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

484 sb.write_apio_ini( 

485 { 

486 "[env:default]": { 

487 "board": "alhambra-ii", 

488 "top-module": "main", 

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

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

491 } 

492 } 

493 ) 

494 

495 # -- Construct the apio context. 

496 apio_ctx = ApioContext( 

497 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

498 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

499 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

500 ) 

501 

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

503 scanner = FakeDeviceScanner( 

504 usb_devices=[ 

505 fake_usb_device(dev=0), 

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

507 fake_usb_device(dev=2), 

508 ], 

509 ) 

510 

511 # -- Call the tested function 

512 cmd = _construct_programmer_cmd( 

513 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

514 ) 

515 

516 # -- Test the result programmer command. 

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

518 

519 # -- Check the log. 

520 log = capsys.readouterr().out 

521 assert "Checking device presence" in log 

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

523 assert ( 

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

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

526 ) in log 

527 

528 assert ( 

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

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

531 ) in log 

532 

533 

534def test_device_presence_not_found( 

535 apio_runner: ApioRunner, capsys: LogCaptureFixture 

536): 

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

538 with apio_runner.in_sandbox() as sb: 

539 

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

541 sb.write_apio_ini( 

542 { 

543 "[env:default]": { 

544 "board": "alhambra-ii", 

545 "top-module": "main", 

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

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

548 } 

549 } 

550 ) 

551 

552 # -- Construct the apio context. 

553 apio_ctx = ApioContext( 

554 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

555 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

556 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

557 ) 

558 

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

560 scanner = FakeDeviceScanner( 

561 usb_devices=[ 

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

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

564 ], 

565 ) 

566 

567 # -- Call the tested function 

568 with raises(SystemExit) as e: 

569 _construct_programmer_cmd( 

570 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

571 ) 

572 

573 assert e.value.code == 1 

574 

575 # -- Check the log. 

576 log = capsys.readouterr().out 

577 assert "Checking device presence" in log 

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

579 assert "Error: No matching device." in log