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

138 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 the apio.managers.programmers.py module. 

11""" 

12 

13from typing_extensions import override 

14from pytest import raises 

15from tests.conftest import ApioRunner 

16from apio.apio_context import ( 

17 ApioContext, 

18 PackagesPolicy, 

19 ProjectPolicy, 

20 RemoteConfigPolicy, 

21) 

22from apio.utils.usb_util import UsbDevice 

23from apio.utils.serial_util import SerialDevice 

24 

25from apio.managers.programmers import ( 

26 _construct_cmd_template, 

27 _construct_programmer_cmd, 

28 _DeviceScanner, 

29) 

30 

31 

32class FakeDeviceScanner(_DeviceScanner): 

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

34 

35 def __init__( 

36 self, 

37 apio_ctx: ApioContext, 

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

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

40 ): 

41 super().__init__(apio_ctx) 

42 self._usb_devices = usb_devices 

43 self._serial_devices = serial_devices 

44 

45 @override 

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

47 """Returns the fake usb devices.""" 

48 assert self._usb_devices 

49 return self._usb_devices 

50 

51 @override 

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

53 """Returns the fake serial devices.""" 

54 assert self._serial_devices 

55 return self._serial_devices 

56 

57 

58def fake_usb_device( 

59 *, 

60 vid="0403", 

61 pid="6010", 

62 bus=0, 

63 dev=0, 

64 manuf="AlhambraBits", 

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

66 sn="SNXXXX", 

67 device_type="FT2232H", 

68) -> UsbDevice: 

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

70 # pylint: disable=too-many-arguments 

71 return UsbDevice( 

72 vid=vid, 

73 pid=pid, 

74 bus=bus, 

75 device=dev, 

76 manufacturer=manuf, 

77 product=prod, 

78 serial_number=sn, 

79 device_type=device_type, 

80 ) 

81 

82 

83def fake_serial_device( 

84 *, 

85 port_name="port0", 

86 vid="04D8", 

87 pid="FFEE", 

88 manuf="IceFUN", 

89 prod="Ice Fun", 

90 sn="SNXXXX", 

91 device_type="FT2232H", 

92 location="0.1", 

93) -> SerialDevice: 

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

95 # pylint: disable=too-many-arguments 

96 return SerialDevice( 

97 port="/dev/" + port_name, 

98 port_name=port_name, 

99 vid=vid, 

100 pid=pid, 

101 manufacturer=manuf, 

102 product=prod, 

103 serial_number=sn, 

104 device_type=device_type, 

105 location=location, 

106 ) 

107 

108 

109def test_default_cmd_template(apio_runner: ApioRunner): 

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

111 

112 with apio_runner.in_sandbox() as sb: 

113 

114 # -- Construct an apio context. 

115 sb.write_apio_ini( 

116 { 

117 "[env:default]": { 

118 "board": "alhambra-ii", 

119 "top-module": "main", 

120 } 

121 } 

122 ) 

123 

124 # -- Run while capturing log. 

125 with apio_runner.with_logger() as log: 

126 apio_ctx = ApioContext( 

127 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

128 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

129 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

130 ) 

131 programmer_cmd = _construct_cmd_template(apio_ctx) 

132 

133 # -- Check result. 

134 assert ( 

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

136 "-b ice40_generic " 

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

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

139 "${BIN_FILE}" 

140 ) 

141 

142 # -- Check no 'custom' warning. 

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

144 

145 

146def test_custom_cmd_template(apio_runner: ApioRunner): 

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

148 

149 with apio_runner.in_sandbox() as sb: 

150 

151 # -- Construct an apio context. 

152 sb.write_apio_ini( 

153 { 

154 "[env:default]": { 

155 "board": "alhambra-ii", 

156 "top-module": "main", 

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

158 } 

159 } 

160 ) 

161 

162 apio_ctx = ApioContext( 

163 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

164 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

165 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

166 ) 

167 

168 with apio_runner.with_logger() as log: 

169 programmer_cmd = _construct_cmd_template(apio_ctx) 

170 

171 # -- Check the result. 

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

173 

174 # -- Check the 'custom' warning. 

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

176 

177 

178def test_get_cmd_usb(apio_runner: ApioRunner): 

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

180 with apio_runner.in_sandbox() as sb: 

181 

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

183 sb.write_apio_ini( 

184 { 

185 "[env:default]": { 

186 "board": "alhambra-ii", 

187 "top-module": "main", 

188 "programmer-cmd": ( 

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

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

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

192 ), 

193 } 

194 } 

195 ) 

196 

197 # -- Construct the apio context. 

198 apio_ctx = ApioContext( 

199 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

200 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

201 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

202 ) 

203 

204 # -- Create fake devices 

205 scanner = FakeDeviceScanner( 

206 apio_ctx, 

207 usb_devices=[ 

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

209 fake_usb_device(dev=1), 

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

211 ], 

212 ) 

213 

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

215 with apio_runner.with_logger() as log: 

216 cmd = _construct_programmer_cmd( 

217 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

218 ) 

219 

220 # -- Test the result programmer command. 

221 assert cmd == ( 

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

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

224 ) 

225 

226 # -- Check the log. 

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

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

229 assert ( 

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

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

232 ) in log.out 

233 

234 

235def test_get_cmd_usb_no_match(apio_runner: ApioRunner): 

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

237 with apio_runner.in_sandbox() as sb: 

238 

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

240 sb.write_apio_ini( 

241 { 

242 "[env:default]": { 

243 "board": "alhambra-ii", 

244 "top-module": "main", 

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

246 } 

247 } 

248 ) 

249 

250 # -- Construct the apio context. 

251 apio_ctx = ApioContext( 

252 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

253 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

254 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

255 ) 

256 

257 # -- Create fake devices 

258 scanner = FakeDeviceScanner( 

259 apio_ctx, 

260 usb_devices=[ 

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

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

263 ], 

264 ) 

265 

266 # -- Call the tested function while capturing log 

267 with apio_runner.with_logger() as log: 

268 with raises(SystemExit) as e: 

269 _construct_programmer_cmd( 

270 apio_ctx, 

271 scanner, 

272 serial_port_flag=None, 

273 serial_num_flag=None, 

274 ) 

275 

276 # -- Verify. 

277 assert e.value.code == 1 

278 

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

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

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

282 

283 

284def test_get_cmd_usb_multiple_matches(apio_runner: ApioRunner): 

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

286 filter.""" 

287 with apio_runner.in_sandbox() as sb: 

288 

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

290 sb.write_apio_ini( 

291 { 

292 "[env:default]": { 

293 "board": "alhambra-ii", 

294 "top-module": "main", 

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

296 } 

297 } 

298 ) 

299 

300 # -- Construct the apio context. 

301 apio_ctx = ApioContext( 

302 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

303 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

304 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

305 ) 

306 

307 # -- Create fake devices 

308 scanner = FakeDeviceScanner( 

309 apio_ctx, 

310 usb_devices=[ 

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

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

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

314 ], 

315 ) 

316 

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

318 with apio_runner.with_logger() as log: 

319 with raises(SystemExit) as e: 

320 _construct_programmer_cmd( 

321 apio_ctx, 

322 scanner, 

323 serial_port_flag=None, 

324 serial_num_flag=None, 

325 ) 

326 

327 # -- Verify. 

328 assert e.value.code == 1 

329 

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

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

332 assert ( 

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

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

335 ) in log.out 

336 assert ( 

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

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

339 ) in log.out 

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

341 

342 

343def test_get_cmd_serial(apio_runner: ApioRunner): 

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

345 with apio_runner.in_sandbox() as sb: 

346 

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

348 sb.write_apio_ini( 

349 { 

350 "[env:default]": { 

351 "board": "icefun", 

352 "top-module": "main", 

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

354 } 

355 } 

356 ) 

357 

358 # -- Construct the apio context. 

359 apio_ctx = ApioContext( 

360 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

361 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

362 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

363 ) 

364 

365 # -- Create fake devices 

366 scanner = FakeDeviceScanner( 

367 apio_ctx, 

368 serial_devices=[ 

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

370 fake_serial_device(port_name="port2"), 

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

372 ], 

373 ) 

374 

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

376 with apio_runner.with_logger() as log: 

377 cmd = _construct_programmer_cmd( 

378 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

379 ) 

380 

381 # -- Test the result programmer command. 

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

383 

384 # -- Check the log. 

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

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

387 assert ( 

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

389 in log.out 

390 ) 

391 

392 

393def test_get_cmd_serial_no_match( 

394 apio_runner: ApioRunner, 

395): 

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

397 with apio_runner.in_sandbox() as sb: 

398 

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

400 sb.write_apio_ini( 

401 { 

402 "[env:default]": { 

403 "board": "icefun", 

404 "top-module": "main", 

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

406 } 

407 } 

408 ) 

409 

410 # -- Construct the apio context. 

411 apio_ctx = ApioContext( 

412 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

413 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

414 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

415 ) 

416 

417 # -- Create fake devices 

418 scanner = FakeDeviceScanner( 

419 apio_ctx, 

420 serial_devices=[ 

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

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

423 ], 

424 ) 

425 

426 # -- Call the tested function while capturing log 

427 with apio_runner.with_logger() as log: 

428 with raises(SystemExit) as e: 

429 _construct_programmer_cmd( 

430 apio_ctx, 

431 scanner, 

432 serial_port_flag=None, 

433 serial_num_flag=None, 

434 ) 

435 

436 # -- Verify 

437 assert e.value.code == 1 

438 

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

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

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

442 

443 

444def test_get_cmd_serial_multiple_matches(apio_runner: ApioRunner): 

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

446 filter.""" 

447 with apio_runner.in_sandbox() as sb: 

448 

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

450 sb.write_apio_ini( 

451 { 

452 "[env:default]": { 

453 "board": "icefun", 

454 "top-module": "main", 

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

456 } 

457 } 

458 ) 

459 

460 # -- Construct the apio context. 

461 apio_ctx = ApioContext( 

462 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

463 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

464 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

465 ) 

466 

467 # -- Create fake devices 

468 scanner = FakeDeviceScanner( 

469 apio_ctx, 

470 serial_devices=[ 

471 fake_serial_device(port_name="port1"), 

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

473 fake_serial_device(port_name="port3"), 

474 ], 

475 ) 

476 

477 # -- Call the tested function while capturing log 

478 with apio_runner.with_logger() as log: 

479 with raises(SystemExit) as e: 

480 _construct_programmer_cmd( 

481 apio_ctx, 

482 scanner, 

483 serial_port_flag=None, 

484 serial_num_flag=None, 

485 ) 

486 

487 # -- Verify. 

488 assert e.value.code == 1 

489 

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

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

492 assert ( 

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

494 ) in log.out 

495 assert ( 

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

497 ) in log.out 

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

499 

500 

501def test_device_presence_ok(apio_runner: ApioRunner): 

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

503 with apio_runner.in_sandbox() as sb: 

504 

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

506 sb.write_apio_ini( 

507 { 

508 "[env:default]": { 

509 "board": "alhambra-ii", 

510 "top-module": "main", 

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

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

513 } 

514 } 

515 ) 

516 

517 # -- Construct the apio context. 

518 apio_ctx = ApioContext( 

519 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

520 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

521 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

522 ) 

523 

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

525 scanner = FakeDeviceScanner( 

526 apio_ctx, 

527 usb_devices=[ 

528 fake_usb_device(dev=0), 

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

530 fake_usb_device(dev=2), 

531 ], 

532 ) 

533 

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

535 with apio_runner.with_logger() as log: 

536 cmd = _construct_programmer_cmd( 

537 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None 

538 ) 

539 

540 # -- Test the result programmer command. 

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

542 

543 # -- Check the log. 

544 assert "Checking device presence" in log.out 

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

546 assert ( 

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

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

549 ) in log.out 

550 assert ( 

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

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

553 ) in log.out 

554 

555 

556def test_device_presence_not_found(apio_runner: ApioRunner): 

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

558 with apio_runner.in_sandbox() as sb: 

559 

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

561 sb.write_apio_ini( 

562 { 

563 "[env:default]": { 

564 "board": "alhambra-ii", 

565 "top-module": "main", 

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

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

568 } 

569 } 

570 ) 

571 

572 # -- Construct the apio context. 

573 apio_ctx = ApioContext( 

574 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

575 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

576 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

577 ) 

578 

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

580 scanner = FakeDeviceScanner( 

581 apio_ctx, 

582 usb_devices=[ 

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

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

585 ], 

586 ) 

587 

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

589 with apio_runner.with_logger() as log: 

590 with raises(SystemExit) as e: 

591 _construct_programmer_cmd( 

592 apio_ctx, 

593 scanner, 

594 serial_port_flag=None, 

595 serial_num_flag=None, 

596 ) 

597 

598 # -- Verify 

599 assert e.value.code == 1 

600 

601 assert "Checking device presence" in log.out 

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

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