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

134 statements  

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

113 } 

114 ) 

115 

116 apio_ctx = ApioContext( 

117 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

118 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

119 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

120 ) 

121 programmer_cmd = _construct_cmd_template(apio_ctx) 

122 

123 # -- Check result. 

124 assert ( 

125 programmer_cmd == "openFPGALoader --verify -b ice40_generic " 

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

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

128 "${BIN_FILE}" 

129 ) 

130 

131 # -- Check no 'custom' warning. 

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

133 

134 

135def test_custom_cmd_template( 

136 apio_runner: ApioRunner, capsys: LogCaptureFixture 

137): 

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 "programmer-cmd": "my template ${VID} ${PID}", 

148 } 

149 } 

150 ) 

151 

152 apio_ctx = ApioContext( 

153 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

154 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

155 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

156 ) 

157 programmer_cmd = _construct_cmd_template(apio_ctx) 

158 

159 # -- Check the result. 

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

161 

162 # -- Check the 'custom' warning. 

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

164 

165 

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

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

168 with apio_runner.in_sandbox() as sb: 

169 

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

171 sb.write_apio_ini( 

172 { 

173 "[env:default]": { 

174 "board": "alhambra-ii", 

175 "programmer-cmd": ( 

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

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

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

179 ), 

180 } 

181 } 

182 ) 

183 

184 # -- Construct the apio context. 

185 apio_ctx = ApioContext( 

186 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

187 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

188 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

189 ) 

190 

191 # -- Create fake devices 

192 scanner = FakeDeviceScanner( 

193 usb_devices=[ 

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

195 fake_usb_device(dev=1), 

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

197 ], 

198 ) 

199 

200 # -- Call the tested function 

201 cmd = _construct_programmer_cmd( 

202 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

203 ) 

204 

205 # -- Test the result programmer command. 

206 assert cmd == ( 

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

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

209 ) 

210 

211 # -- Check the log. 

212 log = capsys.readouterr().out 

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

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

215 assert ( 

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

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

218 ) in log 

219 

220 

221def test_get_cmd_usb_no_match( 

222 apio_runner: ApioRunner, capsys: LogCaptureFixture 

223): 

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

225 with apio_runner.in_sandbox() as sb: 

226 

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

228 sb.write_apio_ini( 

229 { 

230 "[env:default]": { 

231 "board": "alhambra-ii", 

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

233 } 

234 } 

235 ) 

236 

237 # -- Construct the apio context. 

238 apio_ctx = ApioContext( 

239 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

240 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

241 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

242 ) 

243 

244 # -- Create fake devices 

245 scanner = FakeDeviceScanner( 

246 usb_devices=[ 

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

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

249 ], 

250 ) 

251 

252 # -- Call the tested function 

253 

254 with raises(SystemExit) as e: 

255 _construct_programmer_cmd( 

256 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

257 ) 

258 

259 assert e.value.code == 1 

260 

261 log = capsys.readouterr().out 

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

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

264 assert "No matching USB device" in log 

265 

266 

267def test_get_cmd_usb_multiple_matches( 

268 apio_runner: ApioRunner, capsys: LogCaptureFixture 

269): 

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

271 filter.""" 

272 with apio_runner.in_sandbox() as sb: 

273 

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

275 sb.write_apio_ini( 

276 { 

277 "[env:default]": { 

278 "board": "alhambra-ii", 

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

280 } 

281 } 

282 ) 

283 

284 # -- Construct the apio context. 

285 apio_ctx = ApioContext( 

286 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

287 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

288 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

289 ) 

290 

291 # -- Create fake devices 

292 scanner = FakeDeviceScanner( 

293 usb_devices=[ 

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

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

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

297 ], 

298 ) 

299 

300 # -- Call the tested function 

301 with raises(SystemExit) as e: 

302 _construct_programmer_cmd( 

303 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

304 ) 

305 

306 assert e.value.code == 1 

307 

308 log = capsys.readouterr().out 

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

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

311 assert ( 

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

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

314 ) in log 

315 assert ( 

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

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

318 ) in log 

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

320 

321 

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

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

324 with apio_runner.in_sandbox() as sb: 

325 

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

327 sb.write_apio_ini( 

328 { 

329 "[env:default]": { 

330 "board": "icefun", 

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

332 } 

333 } 

334 ) 

335 

336 # -- Construct the apio context. 

337 apio_ctx = ApioContext( 

338 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

339 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

340 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

341 ) 

342 

343 # -- Create fake devices 

344 scanner = FakeDeviceScanner( 

345 serial_devices=[ 

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

347 fake_serial_device(port_name="port2"), 

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

349 ], 

350 ) 

351 

352 # -- Call the tested function 

353 cmd = _construct_programmer_cmd( 

354 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

355 ) 

356 

357 # -- Test the result programmer command. 

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

359 

360 # -- Check the log. 

361 log = capsys.readouterr().out 

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

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

364 assert ( 

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

366 in log 

367 ) 

368 

369 

370def test_get_cmd_serial_no_match( 

371 apio_runner: ApioRunner, capsys: LogCaptureFixture 

372): 

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

374 with apio_runner.in_sandbox() as sb: 

375 

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

377 sb.write_apio_ini( 

378 { 

379 "[env:default]": { 

380 "board": "icefun", 

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

382 } 

383 } 

384 ) 

385 

386 # -- Construct the apio context. 

387 apio_ctx = ApioContext( 

388 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

389 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

390 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

391 ) 

392 

393 # -- Create fake devices 

394 scanner = FakeDeviceScanner( 

395 serial_devices=[ 

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

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

398 ], 

399 ) 

400 

401 # -- Call the tested function 

402 with raises(SystemExit) as e: 

403 _construct_programmer_cmd( 

404 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

405 ) 

406 

407 assert e.value.code == 1 

408 

409 log = capsys.readouterr().out 

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

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

412 assert "No matching serial device" in log 

413 

414 

415def test_get_cmd_serial_multiple_matches( 

416 apio_runner: ApioRunner, capsys: LogCaptureFixture 

417): 

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

419 filter.""" 

420 with apio_runner.in_sandbox() as sb: 

421 

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

423 sb.write_apio_ini( 

424 { 

425 "[env:default]": { 

426 "board": "icefun", 

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

428 } 

429 } 

430 ) 

431 

432 # -- Construct the apio context. 

433 apio_ctx = ApioContext( 

434 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

435 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

436 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

437 ) 

438 

439 # -- Create fake devices 

440 scanner = FakeDeviceScanner( 

441 serial_devices=[ 

442 fake_serial_device(port_name="port1"), 

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

444 fake_serial_device(port_name="port3"), 

445 ], 

446 ) 

447 

448 # -- Call the tested function 

449 with raises(SystemExit) as e: 

450 _construct_programmer_cmd( 

451 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

452 ) 

453 

454 assert e.value.code == 1 

455 

456 log = capsys.readouterr().out 

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

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

459 assert ( 

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

461 ) in log 

462 assert ( 

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

464 ) in log 

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

466 

467 

468def test_device_presence_ok( 

469 apio_runner: ApioRunner, capsys: LogCaptureFixture 

470): 

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

472 with apio_runner.in_sandbox() as sb: 

473 

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

475 sb.write_apio_ini( 

476 { 

477 "[env:default]": { 

478 "board": "alhambra-ii", 

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

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

481 } 

482 } 

483 ) 

484 

485 # -- Construct the apio context. 

486 apio_ctx = ApioContext( 

487 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

488 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

489 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

490 ) 

491 

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

493 scanner = FakeDeviceScanner( 

494 usb_devices=[ 

495 fake_usb_device(dev=0), 

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

497 fake_usb_device(dev=2), 

498 ], 

499 ) 

500 

501 # -- Call the tested function 

502 cmd = _construct_programmer_cmd( 

503 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

504 ) 

505 

506 # -- Test the result programmer command. 

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

508 

509 # -- Check the log. 

510 log = capsys.readouterr().out 

511 assert "Checking device presence" in log 

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

513 assert ( 

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

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

516 ) in log 

517 

518 assert ( 

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

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

521 ) in log 

522 

523 

524def test_device_presence_not_found( 

525 apio_runner: ApioRunner, capsys: LogCaptureFixture 

526): 

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

528 with apio_runner.in_sandbox() as sb: 

529 

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

531 sb.write_apio_ini( 

532 { 

533 "[env:default]": { 

534 "board": "alhambra-ii", 

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

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

537 } 

538 } 

539 ) 

540 

541 # -- Construct the apio context. 

542 apio_ctx = ApioContext( 

543 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

544 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

545 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

546 ) 

547 

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

549 scanner = FakeDeviceScanner( 

550 usb_devices=[ 

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

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

553 ], 

554 ) 

555 

556 # -- Call the tested function 

557 with raises(SystemExit) as e: 

558 _construct_programmer_cmd( 

559 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

560 ) 

561 

562 assert e.value.code == 1 

563 

564 # -- Check the log. 

565 log = capsys.readouterr().out 

566 assert "Checking device presence" in log 

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

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