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
« 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
9"""
10Tests of the apio.managers.programmers.py module.
11"""
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
25from apio.managers.programmers import (
26 _construct_cmd_template,
27 _construct_programmer_cmd,
28 _DeviceScanner,
29)
32class FakeDeviceScanner(_DeviceScanner):
33 """A fake device scanner for testing."""
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
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
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
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 )
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 )
109def test_default_cmd_template(apio_runner: ApioRunner):
110 """Tests _construct_cmd_template() with the default board template."""
112 with apio_runner.in_sandbox() as sb:
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 )
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)
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 )
142 # -- Check no 'custom' warning.
143 assert "Using custom programmer cmd" not in log.out
146def test_custom_cmd_template(apio_runner: ApioRunner):
147 """Tests _construct_cmd_template() with custom command template."""
149 with apio_runner.in_sandbox() as sb:
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 )
162 apio_ctx = ApioContext(
163 project_policy=ProjectPolicy.PROJECT_REQUIRED,
164 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
165 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
166 )
168 with apio_runner.with_logger() as log:
169 programmer_cmd = _construct_cmd_template(apio_ctx)
171 # -- Check the result.
172 assert programmer_cmd == "my template ${VID} ${PID}"
174 # -- Check the 'custom' warning.
175 assert "Using custom programmer cmd" in log.out
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:
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 )
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 )
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 )
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 )
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 )
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
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:
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 )
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 )
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 )
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 )
276 # -- Verify.
277 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
327 # -- Verify.
328 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
381 # -- Test the result programmer command.
382 assert cmd == "my-programmer --port /dev/port2"
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 )
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:
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 )
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 )
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 )
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 )
436 # -- Verify
437 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
487 # -- Verify.
488 assert e.value.code == 1
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
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:
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 )
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 )
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 )
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 )
540 # -- Test the result programmer command.
541 assert cmd == "my programmer command $SOURCE"
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
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:
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 )
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 )
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 )
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 )
598 # -- Verify
599 assert e.value.code == 1
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