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
« 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"""
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
17from apio.managers.programmers import (
18 _construct_cmd_template,
19 _construct_programmer_cmd,
20 _DeviceScanner,
21)
24class FakeDeviceScanner(_DeviceScanner):
25 """A fake device scanner for testing."""
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
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[UsbDevice]:
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 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 )
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 )
100def test_default_cmd_template(
101 apio_runner: ApioRunner, capsys: LogCaptureFixture
102):
103 """Tests _construct_cmd_template() with the default board template."""
105 with apio_runner.in_sandbox() as sb:
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 )
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 capsys.readouterr().out
137def test_custom_cmd_template(
138 apio_runner: ApioRunner, capsys: LogCaptureFixture
139):
140 """Tests _construct_cmd_template() with custom command template."""
142 with apio_runner.in_sandbox() as sb:
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 )
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)
162 # -- Check the result.
163 assert programmer_cmd == "my template ${VID} ${PID}"
165 # -- Check the 'custom' warning.
166 assert "Using custom programmer cmd" in capsys.readouterr().out
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:
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 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 )
204 # -- Call the tested function
205 cmd = _construct_programmer_cmd(
206 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None
207 )
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 )
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
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:
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 )
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 )
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 )
257 # -- Call the tested function
259 with raises(SystemExit) as e:
260 _construct_programmer_cmd(
261 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None
262 )
264 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
312 assert e.value.code == 1
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
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:
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 )
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 )
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 )
359 # -- Call the tested function
360 cmd = _construct_programmer_cmd(
361 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None
362 )
364 # -- Test the result programmer command.
365 assert cmd == "my-programmer --port /dev/port2"
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 )
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:
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 )
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 )
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 )
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 )
415 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
463 assert e.value.code == 1
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
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:
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 )
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 )
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 )
511 # -- Call the tested function
512 cmd = _construct_programmer_cmd(
513 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None
514 )
516 # -- Test the result programmer command.
517 assert cmd == "my programmer command $SOURCE"
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
528 assert (
529 "DEVICE [0403:6010] [0:2] [AlhambraBits] "
530 "[Alhambra II v1.0A] [SNXXXX]"
531 ) in log
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:
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 )
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 )
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 )
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 )
573 assert e.value.code == 1
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