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
« 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"""
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
16from apio.managers.programmers import (
17 _construct_cmd_template,
18 _construct_programmer_cmd,
19 _DeviceScanner,
20)
23class FakeDeviceScanner(_DeviceScanner):
24 """A fake device scanner for testing."""
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
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
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
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 )
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 )
100def test_default_cmd_template(apio_runner: ApioRunner):
101 """Tests _construct_cmd_template() with the default board template."""
103 with apio_runner.in_sandbox() as sb:
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 )
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)
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 )
133 # -- Check no 'custom' warning.
134 assert "Using custom programmer cmd" not in log.out
137def test_custom_cmd_template(apio_runner: ApioRunner):
138 """Tests _construct_cmd_template() with custom command template."""
140 with apio_runner.in_sandbox() as sb:
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 )
153 apio_ctx = ApioContext(
154 project_policy=ProjectPolicy.PROJECT_REQUIRED,
155 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
156 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
157 )
159 with apio_runner.with_logger() as log:
160 programmer_cmd = _construct_cmd_template(apio_ctx)
162 # -- Check the result.
163 assert programmer_cmd == "my template ${VID} ${PID}"
165 # -- Check the 'custom' warning.
166 assert "Using custom programmer cmd" in log.out
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:
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 )
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 )
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 )
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 )
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 )
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
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:
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 )
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 )
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 )
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 )
267 # -- Verify.
268 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
318 # -- Verify.
319 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
372 # -- Test the result programmer command.
373 assert cmd == "my-programmer --port /dev/port2"
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 )
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:
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 )
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 )
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 )
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 )
427 # -- Verify
428 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
478 # -- Verify.
479 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
531 # -- Test the result programmer command.
532 assert cmd == "my programmer command $SOURCE"
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
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:
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 )
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 )
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 )
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 )
589 # -- Verify
590 assert e.value.code == 1
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