feat: prove and decode K1 realtime MQTT streams
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import plistlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from k1link.cli import app
|
||||
from k1link.usb.snapshot import (
|
||||
DISKUTIL_COMMAND,
|
||||
SERIAL_IOREG_COMMAND,
|
||||
USB_IOREG_COMMAND,
|
||||
CommandOutput,
|
||||
snapshot,
|
||||
)
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def _plist_output(argv: tuple[str, ...], payload: object) -> CommandOutput:
|
||||
return CommandOutput(
|
||||
argv=argv,
|
||||
returncode=0,
|
||||
stdout=plistlib.dumps(payload),
|
||||
stderr=b"",
|
||||
error=None,
|
||||
)
|
||||
|
||||
|
||||
def _interface(
|
||||
name: str,
|
||||
number: int,
|
||||
interface_class: int,
|
||||
subclass: int,
|
||||
protocol: int,
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"IOObjectClass": "IOUSBHostInterface",
|
||||
"IORegistryEntryName": name,
|
||||
"kUSBString": name,
|
||||
"bInterfaceNumber": number,
|
||||
"bInterfaceClass": interface_class,
|
||||
"bInterfaceSubClass": subclass,
|
||||
"bInterfaceProtocol": protocol,
|
||||
"bAlternateSetting": 0,
|
||||
"bConfigurationValue": 1,
|
||||
"bNumEndpoints": 2,
|
||||
}
|
||||
|
||||
|
||||
def test_snapshot_parses_xgrids_interfaces_storage_and_usbmodem() -> None:
|
||||
usb_plist = [
|
||||
{
|
||||
"IOObjectClass": "IOUSBHostDevice",
|
||||
"IORegistryEntryName": "XGRIDS Device",
|
||||
"USB Product Name": "XGRIDS Device",
|
||||
"USB Vendor Name": "rockchip",
|
||||
"USB Serial Number": "synthetic-serial",
|
||||
"idVendor": 0x2207,
|
||||
"idProduct": 0x0019,
|
||||
"bDeviceClass": 0xEF,
|
||||
"bDeviceSubClass": 0x02,
|
||||
"bDeviceProtocol": 0x01,
|
||||
"bcdUSB": 0x0210,
|
||||
"bcdDevice": 0x0310,
|
||||
"USBSpeed": 3,
|
||||
"UsbLinkSpeed": 480_000_000,
|
||||
"USB Address": 1,
|
||||
"locationID": 0x01100000,
|
||||
"IORegistryEntryID": 12345,
|
||||
"IORegistryEntryChildren": [
|
||||
_interface("RNDIS Communications Control", 0, 0xE0, 0x01, 0x03),
|
||||
_interface("RNDIS Ethernet Data", 1, 0x0A, 0x00, 0x00),
|
||||
_interface("Mass Storage", 2, 0x08, 0x06, 0x50),
|
||||
_interface("CDC NCM Control", 3, 0x02, 0x0D, 0x00),
|
||||
_interface("CDC ACM Serial", 4, 0x02, 0x02, 0x01),
|
||||
{
|
||||
"IOObjectClass": "IOMedia",
|
||||
"IORegistryEntryName": "Synthetic Media",
|
||||
"BSD Name": "disk4",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"IOObjectClass": "IOUSBHostDevice",
|
||||
"IORegistryEntryName": "Unrelated Camera",
|
||||
"USB Product Name": "Unrelated Camera",
|
||||
"idVendor": 999,
|
||||
},
|
||||
]
|
||||
serial_plist = [
|
||||
{
|
||||
"IOCalloutDevice": "/dev/cu.usbmodemK1TEST",
|
||||
"IODialinDevice": "/dev/tty.usbmodemK1TEST",
|
||||
"IOTTYBaseName": "usbmodemK1TEST",
|
||||
"IOSerialBSDClientType": "IOSerialStream",
|
||||
},
|
||||
{
|
||||
"IOCalloutDevice": "/dev/cu.debug-console",
|
||||
"IODialinDevice": "/dev/tty.debug-console",
|
||||
},
|
||||
]
|
||||
storage_plist = {
|
||||
"AllDisks": ["disk4", "disk4s1"],
|
||||
"WholeDisks": ["disk4"],
|
||||
"VolumesFromDisks": ["K1_DATA"],
|
||||
"AllDisksAndPartitions": [
|
||||
{
|
||||
"DeviceIdentifier": "disk4",
|
||||
"Content": "GUID_partition_scheme",
|
||||
"Size": 64_000_000,
|
||||
"OSInternal": False,
|
||||
"Partitions": [
|
||||
{
|
||||
"DeviceIdentifier": "disk4s1",
|
||||
"Content": "Microsoft Basic Data",
|
||||
"Size": 63_000_000,
|
||||
"VolumeName": "K1_DATA",
|
||||
"MountPoint": "/Volumes/K1_DATA",
|
||||
"OSInternal": False,
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
outputs = {
|
||||
USB_IOREG_COMMAND: _plist_output(USB_IOREG_COMMAND, usb_plist),
|
||||
SERIAL_IOREG_COMMAND: _plist_output(SERIAL_IOREG_COMMAND, serial_plist),
|
||||
DISKUTIL_COMMAND: _plist_output(DISKUTIL_COMMAND, storage_plist),
|
||||
}
|
||||
|
||||
result = snapshot(lambda argv: outputs[tuple(argv)])
|
||||
|
||||
assert result["xgrids_device_count"] == 1
|
||||
device = result["xgrids_devices"][0]
|
||||
assert device["vendor_id_hex"] == "0x2207"
|
||||
assert device["product_id_hex"] == "0x0019"
|
||||
assert device["bsd_names"] == ["disk4"]
|
||||
assert device["interface_capabilities"] == [
|
||||
"cdc_data",
|
||||
"mass_storage",
|
||||
"ncm",
|
||||
"rndis",
|
||||
"serial",
|
||||
]
|
||||
assert [interface["interface_number"] for interface in device["interfaces"]] == [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
]
|
||||
assert all(entry["xgrids_related"] for entry in result["external_storage"]["entries"])
|
||||
assert result["usbmodem_device_names"] == [
|
||||
"/dev/cu.usbmodemK1TEST",
|
||||
"/dev/tty.usbmodemK1TEST",
|
||||
]
|
||||
assert len(result["usbmodem_devices"]) == 1
|
||||
assert all(source["ok"] for source in result["sources"])
|
||||
assert all("sudo" not in source["argv"] for source in result["sources"])
|
||||
assert result["safety"] == {
|
||||
"metadata_only": True,
|
||||
"sudo_used": False,
|
||||
"device_file_contents_read": False,
|
||||
"device_writes_performed": False,
|
||||
}
|
||||
|
||||
|
||||
def test_snapshot_reports_command_and_plist_errors_without_raising() -> None:
|
||||
outputs = {
|
||||
USB_IOREG_COMMAND: CommandOutput(
|
||||
argv=USB_IOREG_COMMAND,
|
||||
returncode=0,
|
||||
stdout=b"not a plist",
|
||||
stderr=b"",
|
||||
error=None,
|
||||
),
|
||||
SERIAL_IOREG_COMMAND: CommandOutput(
|
||||
argv=SERIAL_IOREG_COMMAND,
|
||||
returncode=1,
|
||||
stdout=b"",
|
||||
stderr=b"serial registry unavailable",
|
||||
error=None,
|
||||
),
|
||||
DISKUTIL_COMMAND: _plist_output(
|
||||
DISKUTIL_COMMAND,
|
||||
{
|
||||
"AllDisks": [],
|
||||
"WholeDisks": [],
|
||||
"VolumesFromDisks": [],
|
||||
"AllDisksAndPartitions": [],
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
result = snapshot(lambda argv: outputs[tuple(argv)])
|
||||
|
||||
assert result["xgrids_devices"] == []
|
||||
statuses = {source["name"]: source for source in result["sources"]}
|
||||
assert statuses["usb_ioreg"]["ok"] is False
|
||||
assert statuses["usb_ioreg"]["error"].startswith("invalid plist:")
|
||||
assert statuses["serial_ioreg"]["ok"] is False
|
||||
assert statuses["serial_ioreg"]["error"] == "serial registry unavailable"
|
||||
assert statuses["external_disks"]["ok"] is True
|
||||
|
||||
|
||||
def test_snapshot_treats_empty_ioreg_output_as_no_devices() -> None:
|
||||
outputs = {
|
||||
USB_IOREG_COMMAND: CommandOutput(USB_IOREG_COMMAND, 0, b"", b"", None),
|
||||
SERIAL_IOREG_COMMAND: CommandOutput(SERIAL_IOREG_COMMAND, 0, b"", b"", None),
|
||||
DISKUTIL_COMMAND: _plist_output(
|
||||
DISKUTIL_COMMAND,
|
||||
{
|
||||
"AllDisks": [],
|
||||
"WholeDisks": [],
|
||||
"VolumesFromDisks": [],
|
||||
"AllDisksAndPartitions": [],
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
result = snapshot(lambda argv: outputs[tuple(argv)])
|
||||
|
||||
assert result["xgrids_device_count"] == 0
|
||||
assert all(source["ok"] for source in result["sources"])
|
||||
|
||||
|
||||
def test_usb_snapshot_cli_writes_json(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
output = tmp_path / "nested" / "usb-snapshot.json"
|
||||
payload = {
|
||||
"schema_version": 1,
|
||||
"xgrids_device_count": 0,
|
||||
"safety": {
|
||||
"metadata_only": True,
|
||||
"sudo_used": False,
|
||||
"device_file_contents_read": False,
|
||||
"device_writes_performed": False,
|
||||
},
|
||||
}
|
||||
monkeypatch.setattr("k1link.cli.usb_snapshot", lambda: payload)
|
||||
|
||||
result = runner.invoke(app, ["usb", "snapshot", "--out", str(output)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert json.loads(output.read_text(encoding="utf-8")) == payload
|
||||
assert "no sudo" in result.stdout
|
||||
Reference in New Issue
Block a user