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

134 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-08 02:47 +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 --verify -b ice40_generic " 

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

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

129 "${BIN_FILE}" 

130 ) 

131 

132 # -- Check no 'custom' warning. 

133 assert "Using custom programmer cmd" not in capsys.readouterr().out 

134 

135 

136def test_custom_cmd_template( 

137 apio_runner: ApioRunner, capsys: LogCaptureFixture 

138): 

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

140 

141 with apio_runner.in_sandbox() as sb: 

142 

143 # -- Construct an apio context. 

144 sb.write_apio_ini( 

145 { 

146 "[env:default]": { 

147 "board": "alhambra-ii", 

148 "top-module": "main", 

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

150 } 

151 } 

152 ) 

153 

154 apio_ctx = ApioContext( 

155 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

156 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

157 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

158 ) 

159 programmer_cmd = _construct_cmd_template(apio_ctx) 

160 

161 # -- Check the result. 

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

163 

164 # -- Check the 'custom' warning. 

165 assert "Using custom programmer cmd" in capsys.readouterr().out 

166 

167 

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

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

170 with apio_runner.in_sandbox() as sb: 

171 

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

173 sb.write_apio_ini( 

174 { 

175 "[env:default]": { 

176 "board": "alhambra-ii", 

177 "top-module": "main", 

178 "programmer-cmd": ( 

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

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

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

182 ), 

183 } 

184 } 

185 ) 

186 

187 # -- Construct the apio context. 

188 apio_ctx = ApioContext( 

189 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

190 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

191 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

192 ) 

193 

194 # -- Create fake devices 

195 scanner = FakeDeviceScanner( 

196 usb_devices=[ 

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

198 fake_usb_device(dev=1), 

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

200 ], 

201 ) 

202 

203 # -- Call the tested function 

204 cmd = _construct_programmer_cmd( 

205 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

206 ) 

207 

208 # -- Test the result programmer command. 

209 assert cmd == ( 

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

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

212 ) 

213 

214 # -- Check the log. 

215 log = capsys.readouterr().out 

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

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

218 assert ( 

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

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

221 ) in log 

222 

223 

224def test_get_cmd_usb_no_match( 

225 apio_runner: ApioRunner, capsys: LogCaptureFixture 

226): 

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

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

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

253 ], 

254 ) 

255 

256 # -- Call the tested function 

257 

258 with raises(SystemExit) as e: 

259 _construct_programmer_cmd( 

260 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

261 ) 

262 

263 assert e.value.code == 1 

264 

265 log = capsys.readouterr().out 

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

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

268 assert "No matching USB device" in log 

269 

270 

271def test_get_cmd_usb_multiple_matches( 

272 apio_runner: ApioRunner, capsys: LogCaptureFixture 

273): 

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

275 filter.""" 

276 with apio_runner.in_sandbox() as sb: 

277 

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

279 sb.write_apio_ini( 

280 { 

281 "[env:default]": { 

282 "board": "alhambra-ii", 

283 "top-module": "main", 

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

285 } 

286 } 

287 ) 

288 

289 # -- Construct the apio context. 

290 apio_ctx = ApioContext( 

291 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

292 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

293 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

294 ) 

295 

296 # -- Create fake devices 

297 scanner = FakeDeviceScanner( 

298 usb_devices=[ 

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

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

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

302 ], 

303 ) 

304 

305 # -- Call the tested function 

306 with raises(SystemExit) as e: 

307 _construct_programmer_cmd( 

308 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

309 ) 

310 

311 assert e.value.code == 1 

312 

313 log = capsys.readouterr().out 

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

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

316 assert ( 

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

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

319 ) in log 

320 assert ( 

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

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

323 ) in log 

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

325 

326 

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

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

329 with apio_runner.in_sandbox() as sb: 

330 

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

332 sb.write_apio_ini( 

333 { 

334 "[env:default]": { 

335 "board": "icefun", 

336 "top-module": "main", 

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

338 } 

339 } 

340 ) 

341 

342 # -- Construct the apio context. 

343 apio_ctx = ApioContext( 

344 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

345 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

346 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

347 ) 

348 

349 # -- Create fake devices 

350 scanner = FakeDeviceScanner( 

351 serial_devices=[ 

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

353 fake_serial_device(port_name="port2"), 

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

355 ], 

356 ) 

357 

358 # -- Call the tested function 

359 cmd = _construct_programmer_cmd( 

360 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

361 ) 

362 

363 # -- Test the result programmer command. 

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

365 

366 # -- Check the log. 

367 log = capsys.readouterr().out 

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

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

370 assert ( 

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

372 in log 

373 ) 

374 

375 

376def test_get_cmd_serial_no_match( 

377 apio_runner: ApioRunner, capsys: LogCaptureFixture 

378): 

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

380 with apio_runner.in_sandbox() as sb: 

381 

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

383 sb.write_apio_ini( 

384 { 

385 "[env:default]": { 

386 "board": "icefun", 

387 "top-module": "main", 

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

389 } 

390 } 

391 ) 

392 

393 # -- Construct the apio context. 

394 apio_ctx = ApioContext( 

395 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

396 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

397 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

398 ) 

399 

400 # -- Create fake devices 

401 scanner = FakeDeviceScanner( 

402 serial_devices=[ 

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

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

405 ], 

406 ) 

407 

408 # -- Call the tested function 

409 with raises(SystemExit) as e: 

410 _construct_programmer_cmd( 

411 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

412 ) 

413 

414 assert e.value.code == 1 

415 

416 log = capsys.readouterr().out 

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

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

419 assert "No matching serial device" in log 

420 

421 

422def test_get_cmd_serial_multiple_matches( 

423 apio_runner: ApioRunner, capsys: LogCaptureFixture 

424): 

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

426 filter.""" 

427 with apio_runner.in_sandbox() as sb: 

428 

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

430 sb.write_apio_ini( 

431 { 

432 "[env:default]": { 

433 "board": "icefun", 

434 "top-module": "main", 

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

436 } 

437 } 

438 ) 

439 

440 # -- Construct the apio context. 

441 apio_ctx = ApioContext( 

442 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

443 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

444 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

445 ) 

446 

447 # -- Create fake devices 

448 scanner = FakeDeviceScanner( 

449 serial_devices=[ 

450 fake_serial_device(port_name="port1"), 

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

452 fake_serial_device(port_name="port3"), 

453 ], 

454 ) 

455 

456 # -- Call the tested function 

457 with raises(SystemExit) as e: 

458 _construct_programmer_cmd( 

459 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

460 ) 

461 

462 assert e.value.code == 1 

463 

464 log = capsys.readouterr().out 

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

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

467 assert ( 

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

469 ) in log 

470 assert ( 

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

472 ) in log 

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

474 

475 

476def test_device_presence_ok( 

477 apio_runner: ApioRunner, capsys: LogCaptureFixture 

478): 

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

480 with apio_runner.in_sandbox() as sb: 

481 

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

483 sb.write_apio_ini( 

484 { 

485 "[env:default]": { 

486 "board": "alhambra-ii", 

487 "top-module": "main", 

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

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

490 } 

491 } 

492 ) 

493 

494 # -- Construct the apio context. 

495 apio_ctx = ApioContext( 

496 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

497 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

498 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

499 ) 

500 

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

502 scanner = FakeDeviceScanner( 

503 usb_devices=[ 

504 fake_usb_device(dev=0), 

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

506 fake_usb_device(dev=2), 

507 ], 

508 ) 

509 

510 # -- Call the tested function 

511 cmd = _construct_programmer_cmd( 

512 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

513 ) 

514 

515 # -- Test the result programmer command. 

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

517 

518 # -- Check the log. 

519 log = capsys.readouterr().out 

520 assert "Checking device presence" in log 

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

522 assert ( 

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

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

525 ) in log 

526 

527 assert ( 

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

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

530 ) in log 

531 

532 

533def test_device_presence_not_found( 

534 apio_runner: ApioRunner, capsys: LogCaptureFixture 

535): 

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

537 with apio_runner.in_sandbox() as sb: 

538 

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

540 sb.write_apio_ini( 

541 { 

542 "[env:default]": { 

543 "board": "alhambra-ii", 

544 "top-module": "main", 

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

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

547 } 

548 } 

549 ) 

550 

551 # -- Construct the apio context. 

552 apio_ctx = ApioContext( 

553 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

554 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

555 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

556 ) 

557 

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

559 scanner = FakeDeviceScanner( 

560 usb_devices=[ 

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

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

563 ], 

564 ) 

565 

566 # -- Call the tested function 

567 with raises(SystemExit) as e: 

568 _construct_programmer_cmd( 

569 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

570 ) 

571 

572 assert e.value.code == 1 

573 

574 # -- Check the log. 

575 log = capsys.readouterr().out 

576 assert "Checking device presence" in log 

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

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