133 lines
5.3 KiB
Python
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
|