Files
IQ.Pilot/iqpilot/system/hardware/tests/test_egpu_dock_flash.py
2026-09-03 18:23:24 -05:00

133 lines
5.3 KiB
Python

"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
The eGPU dock flasher writes SPI flash, so the parts that decide WHETHER and
WHAT to write are pinned here. The transfer path itself needs the hardware; the
image validator, product parsing, config preservation and the needs-flash
decision do not, and those are what stop a bad write.
"""
import os
import zlib
import pytest
from iqpilot.system.hardware.egpu_dock import flash as f
def _wrap(body: bytes, *, magic=0xA5, checksum=None, crc=None, body_len=None) -> bytes:
n = len(body) if body_len is None else body_len
cs = (sum(body) & 0xFF) if checksum is None else checksum
c = zlib.crc32(body).to_bytes(4, "little") if crc is None else crc
return n.to_bytes(4, "little") + body + bytes([magic, cs]) + c
def test_bundled_firmware_is_valid_and_named():
image = f.FIRMWARE_PATH.read_bytes()
f.validate_image(image) # raises if the shipped blob is corrupt
assert f.image_product(image).startswith("custom ")
assert f.image_product(image).endswith("-CLEAN")
assert f.bundled_version() == f.image_product(image)
def test_validate_rejects_corruption():
body = b"custom deadbeef-CLEAN" + bytes(64)
f.validate_image(_wrap(body)) # the good case
with pytest.raises(ValueError):
f.validate_image(b"\x00" * 4) # too short
with pytest.raises(ValueError):
f.validate_image(_wrap(body, magic=0x00)) # bad magic
with pytest.raises(ValueError):
f.validate_image(_wrap(body, checksum=0x00))
with pytest.raises(ValueError):
f.validate_image(_wrap(body, crc=b"\x00\x00\x00\x00"))
with pytest.raises(ValueError):
f.validate_image(_wrap(body, body_len=len(body) + 1)) # length disagrees
with pytest.raises(ValueError):
f.validate_image((f.MAX_CODE_SIZE + 1).to_bytes(4, "little") + bytes(16))
def test_image_product_requires_a_version_string():
with pytest.raises(ValueError):
f.image_product(b"no version here")
def test_saved_config_preserves_the_first_backup(tmp_path):
# the config page is per-unit; a reflash must rewrite the ORIGINAL, never the
# bytes read back from a half-written dock
p = str(tmp_path / "dock.bin")
original = bytes(range(256))
assert f.saved_config(p, original) == original
# later flash reads something different -> the stored original wins
assert f.saved_config(p, bytes(256)) == original
with open(p, "rb") as fh:
assert fh.read() == original
def test_saved_config_rejects_wrong_size_backup(tmp_path):
p = str(tmp_path / "dock.bin")
with open(p, "wb") as fh:
fh.write(b"\x00" * 8)
with pytest.raises(RuntimeError):
f.saved_config(p, bytes(256))
def test_needs_flash_only_for_a_dock_on_wrong_firmware():
expected = f.bundled_version()
vid, pid = (int(x, 16) for x in f.VID_PIDS[0])
rom_vid, rom_pid = (int(x, 16) for x in f.ROM_VID_PIDS[0])
assert not f.dock_needs_flash([])
assert not f.dock_needs_flash([{"vendorId": 0x1234, "productId": 0x5678, "product": "something else"}])
assert not f.dock_needs_flash([{"vendorId": vid, "productId": pid, "product": expected}])
assert f.dock_needs_flash([{"vendorId": vid, "productId": pid, "product": "custom 00000000-CLEAN"}])
# a ROM-mode board always needs flashing
assert f.dock_needs_flash([{"vendorId": rom_vid, "productId": rom_pid, "product": f.ROM_PRODUCT}])
def test_both_shipped_ids_trigger_the_check():
for pair in f.VID_PIDS:
vid, pid = (int(x, 16) for x in pair)
assert f.dock_needs_flash([{"vendorId": vid, "productId": pid, "product": "custom 00000000-CLEAN"}])
def test_rom_detection():
assert f.in_rom_bootloader(f.ROM_VID_PIDS[0], "anything")
assert f.in_rom_bootloader(("add1", "0001"), f.ROM_PRODUCT)
assert f.in_rom_bootloader(("add1", "0001"), "AS2462something")
assert not f.in_rom_bootloader(("add1", "0001"), f.bundled_version())
assert not f.in_rom_bootloader(("add1", "0001"), None)
def test_config_paths_are_per_host_and_have_a_legacy_fallback():
host = os.uname().nodename
assert f.config_path().endswith(f"{host}.bin")
assert f.config_path().startswith(f.CONFIG_DIR)
# a dock flashed on this device by stock openpilot left its backup elsewhere
assert f.legacy_config_path().startswith(f.LEGACY_CONFIG_DIR)
assert f.legacy_config_path() != f.config_path()
def test_we_do_not_autoflash():
# upstream flashes from hardwared automatically; ours must stay deliberate
# until it has been validated against a real dock
import subprocess
root = os.path.join(os.path.dirname(f.__file__), "..", "..", "..")
hits = subprocess.run(["grep", "-rnI", "--exclude-dir=__pycache__", "flash_dock", os.path.join(root, "system"),
os.path.join(root, "iqpilot")], capture_output=True, text=True).stdout
callers = [ln for ln in hits.splitlines() if "egpu_dock/flash.py" not in ln and "test_" not in ln]
assert callers == [], f"unexpected automatic flash caller: {callers}"
def test_runtime_fw_gate_is_pinned_to_the_bundled_firmware():
from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT
assert EGPU_DOCK_FW_PRODUCT == f.bundled_version()
def test_register_reads_default_to_superspeed_size():
assert f.MAX_REGISTER_READ_SIZE == 255
assert f.Flash().max_register_read_size == f.MAX_REGISTER_READ_SIZE
def test_link_up_is_false_without_a_dock():
assert f.link_up() is False