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
« 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"""
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 --verify -b ice40_generic "
127 "--vid ${VID} --pid ${PID} "
128 "--busdev-num ${BUS}:${DEV} "
129 "${BIN_FILE}"
130 )
132 # -- Check no 'custom' warning.
133 assert "Using custom programmer cmd" not in capsys.readouterr().out
136def test_custom_cmd_template(
137 apio_runner: ApioRunner, capsys: LogCaptureFixture
138):
139 """Tests _construct_cmd_template() with custom command template."""
141 with apio_runner.in_sandbox() as sb:
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 )
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)
161 # -- Check the result.
162 assert programmer_cmd == "my template ${VID} ${PID}"
164 # -- Check the 'custom' warning.
165 assert "Using custom programmer cmd" in capsys.readouterr().out
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:
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 )
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 )
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 )
203 # -- Call the tested function
204 cmd = _construct_programmer_cmd(
205 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None
206 )
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 )
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
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:
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 usb_devices=[
251 fake_usb_device(dev=0, prod="non alhambra"),
252 fake_usb_device(dev=2, prod="non alhambra"),
253 ],
254 )
256 # -- Call the tested function
258 with raises(SystemExit) as e:
259 _construct_programmer_cmd(
260 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None
261 )
263 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
311 assert e.value.code == 1
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
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:
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 )
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 )
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 )
358 # -- Call the tested function
359 cmd = _construct_programmer_cmd(
360 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None
361 )
363 # -- Test the result programmer command.
364 assert cmd == "my-programmer --port /dev/port2"
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 )
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:
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 )
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 )
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 )
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 )
414 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
462 assert e.value.code == 1
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
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:
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 )
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 )
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 )
510 # -- Call the tested function
511 cmd = _construct_programmer_cmd(
512 apio_ctx, scanner, serial_port_flag=None, serial_num_flag=None
513 )
515 # -- Test the result programmer command.
516 assert cmd == "my programmer command $SOURCE"
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
527 assert (
528 "DEVICE [0403:6010] [0:2] [AlhambraBits] "
529 "[Alhambra II v1.0A] [SNXXXX]"
530 ) in log
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:
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 )
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 )
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 )
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 )
572 assert e.value.code == 1
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