NODEDC_MISSION_CORE/tests/test_xgrids_calibration_fil...

423 lines
14 KiB
Python

from __future__ import annotations
import json
from collections import deque
from datetime import UTC, datetime
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import paho.mqtt.client as mqtt
import pytest
from k1link.device_plugins.xgrids_k1.calibration_snapshot import (
seal_factory_calibration_snapshot,
)
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ApplicationControlAuthority,
LiveDeviceControlBinding,
)
from k1link.device_plugins.xgrids_k1.protocol.calibration_file import (
CALIBRATION_FILE_READ_COMMAND,
CALIBRATION_FILE_REQUEST_TOPIC,
CALIBRATION_FILE_RESPONSE_TOPIC,
FACTORY_CALIBRATION_PATHS,
FACTORY_CAMERA_CALIBRATION_PATH,
FACTORY_CAMERA_LIDAR_EXTRINSIC_PATH,
CalibrationFileProtocolError,
CalibrationFileRejected,
build_factory_calibration_file_read,
decode_factory_calibration_file_response,
)
from k1link.device_plugins.xgrids_k1.protocol.calibration_mqtt import (
CALIBRATION_READ_SUBSCRIPTIONS,
FactoryCalibrationReadResult,
ReviewedCalibrationMqttReader,
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import OPENAPI_SUCCESS
from k1link.device_plugins.xgrids_k1.protocol.protobuf_wire import iter_fields
APPLICATION_KEY = "11111111-2222-3333-4444-555555555555"
VENDOR_DEVICE_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
DEVICE_SERIAL = "K1SERIAL01"
PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
CALIBRATION_FIXTURE_ROOT = Path(__file__).parent / "fixtures" / "k1" / "calibration"
def _authority() -> ApplicationControlAuthority:
return ApplicationControlAuthority(openapi_key=APPLICATION_KEY)
def _binding(**changes: object) -> LiveDeviceControlBinding:
values = {
"vendor_device_id": VENDOR_DEVICE_ID,
"device_serial": DEVICE_SERIAL,
"software_version": "V3.0.2",
"system_version": "V3.0.2",
"device_model": "LixelKity K1",
"device_type": "A4",
"is_activated": True,
}
values.update(changes)
return LiveDeviceControlBinding(**values) # type: ignore[arg-type]
def _varint(value: int) -> bytes:
encoded = bytearray()
while value > 0x7F:
encoded.append((value & 0x7F) | 0x80)
value >>= 7
encoded.append(value)
return bytes(encoded)
def _uint(number: int, value: int) -> bytes:
return _varint(number << 3) + _varint(value)
def _bytes(number: int, value: bytes) -> bytes:
return _varint((number << 3) | 2) + _varint(len(value)) + value
def _text(number: int, value: str) -> bytes:
return _bytes(number, value.encode("utf-8"))
def _header(session_id: str) -> bytes:
return b"".join(
(
_text(4, VENDOR_DEVICE_ID),
_text(5, session_id),
_text(6, APPLICATION_KEY),
)
)
def _device_info_response() -> bytes:
base_info = b"".join(
(
_text(2, "V3.0.2"),
_text(3, "V3.0.2"),
_text(6, "LixelKity K1"),
_text(7, DEVICE_SERIAL),
_text(8, "A4"),
)
)
working_status = _uint(1, 1)
device_info = _bytes(2, base_info) + _bytes(7, working_status)
return b"".join(
(
_bytes(1, _header(":DeviceInfoRequest")),
_bytes(2, device_info),
_bytes(15, _uint(1, OPENAPI_SUCCESS)),
)
)
def _calibration_response(
path: str,
content: bytes,
*,
command: int = CALIBRATION_FILE_READ_COMMAND,
result_code: int = OPENAPI_SUCCESS,
session_id: str | None = None,
) -> bytes:
return b"".join(
(
_bytes(
1,
_header(
session_id or f"{VENDOR_DEVICE_ID}:CalibFileRequest"
),
),
_uint(2, command),
_text(3, path),
_bytes(4, content),
_bytes(15, _uint(1, result_code)),
)
)
def _content_for(path: str) -> bytes:
if path == FACTORY_CAMERA_CALIBRATION_PATH:
return (CALIBRATION_FIXTURE_ROOT / "camera.yaml").read_bytes()
return (CALIBRATION_FIXTURE_ROOT / "extrinsic_camera_lidar.yaml").read_bytes()
def test_factory_file_request_is_command_five_and_has_no_content_field() -> None:
request = build_factory_calibration_file_read(
_authority(),
_binding(),
FACTORY_CAMERA_CALIBRATION_PATH,
)
fields = {field.number: field.value for field in iter_fields(request.payload)}
header = fields[1]
assert isinstance(header, bytes)
header_fields = {field.number: field.value for field in iter_fields(header)}
assert request.topic == CALIBRATION_FILE_REQUEST_TOPIC
assert request.response_topic == CALIBRATION_FILE_RESPONSE_TOPIC
assert request.mutates_device is False
assert request.automatic_retry is False
assert fields[2] == 5
assert fields[3] == FACTORY_CAMERA_CALIBRATION_PATH.encode()
assert 4 not in fields
assert header_fields[4] == VENDOR_DEVICE_ID.encode()
assert header_fields[5] == f"{VENDOR_DEVICE_ID}:CalibFileRequest".encode()
assert header_fields[6] == APPLICATION_KEY.encode()
@pytest.mark.parametrize(
"path",
(
"/mnt/system/factory-data/config/../Lixel.yaml",
"/mnt/system/factory-data/config/extrinsic_camera_motor.yaml",
"/tmp/camera.yaml",
"",
),
)
def test_factory_file_request_rejects_every_path_outside_two_file_allowlist(
path: str,
) -> None:
with pytest.raises(CalibrationFileProtocolError, match="two-file allowlist"):
build_factory_calibration_file_read(_authority(), _binding(), path)
def test_factory_file_request_rejects_a_nonreviewed_live_binding() -> None:
with pytest.raises(CalibrationFileProtocolError, match="reviewed activated"):
build_factory_calibration_file_read(
_authority(),
_binding(system_version="V3.0.3"),
FACTORY_CAMERA_CALIBRATION_PATH,
)
def test_correlated_factory_file_response_returns_exact_utf8_bytes() -> None:
request = build_factory_calibration_file_read(
_authority(),
_binding(),
FACTORY_CAMERA_CALIBRATION_PATH,
)
content = _content_for(request.path)
result = decode_factory_calibration_file_response(
_calibration_response(request.path, content),
request,
_authority(),
_binding(),
)
assert result.path == request.path
assert result.content == content
assert result.content_bytes == len(content)
assert len(result.content_sha256) == 64
assert result.result_code == OPENAPI_SUCCESS
def test_factory_file_response_rejects_write_command_path_drift_and_duplicates() -> None:
request = build_factory_calibration_file_read(
_authority(),
_binding(),
FACTORY_CAMERA_CALIBRATION_PATH,
)
with pytest.raises(CalibrationFileProtocolError, match="not a file-read"):
decode_factory_calibration_file_response(
_calibration_response(request.path, b"yaml", command=6),
request,
_authority(),
_binding(),
)
with pytest.raises(CalibrationFileProtocolError, match="path mismatch"):
decode_factory_calibration_file_response(
_calibration_response(FACTORY_CAMERA_LIDAR_EXTRINSIC_PATH, b"yaml"),
request,
_authority(),
_binding(),
)
duplicate_path = _calibration_response(request.path, b"yaml") + _text(3, request.path)
with pytest.raises(CalibrationFileProtocolError, match="duplicated"):
decode_factory_calibration_file_response(
duplicate_path,
request,
_authority(),
_binding(),
)
def test_factory_file_response_rejects_device_error_and_non_utf8_content() -> None:
request = build_factory_calibration_file_read(
_authority(),
_binding(),
FACTORY_CAMERA_CALIBRATION_PATH,
)
with pytest.raises(CalibrationFileRejected) as rejected:
decode_factory_calibration_file_response(
_calibration_response(request.path, b"yaml", result_code=123),
request,
_authority(),
_binding(),
)
assert rejected.value.result_code == 123
with pytest.raises(CalibrationFileProtocolError, match="valid UTF-8"):
decode_factory_calibration_file_response(
_calibration_response(request.path, b"\xff"),
request,
_authority(),
_binding(),
)
class FakeReasonCode:
def __init__(self, *, failure: bool = False) -> None:
self.is_failure = failure
class FakeCalibrationClient:
def __init__(self) -> None:
self.on_connect: Any = None
self.on_subscribe: Any = None
self.on_publish: Any = None
self.on_message: Any = None
self.on_disconnect: Any = None
self.connect_timeout = 0.0
self.events: deque[tuple[str, object]] = deque()
self.connect_calls: list[tuple[str, int, int]] = []
self.subscribe_calls: list[list[tuple[str, int]]] = []
self.publish_calls: list[tuple[str, bytes, int, bool]] = []
self.unsubscribe_calls: list[list[str]] = []
self.next_mid = 20
def connect(self, host: str, port: int, keepalive: int) -> mqtt.MQTTErrorCode:
self.connect_calls.append((host, port, keepalive))
self.events.append(("connect", FakeReasonCode()))
return mqtt.MQTT_ERR_SUCCESS
def subscribe(self, topics: list[tuple[str, int]]) -> tuple[mqtt.MQTTErrorCode, int]:
self.subscribe_calls.append(topics)
self.events.append(("subscribe", 7))
return mqtt.MQTT_ERR_SUCCESS, 7
def publish(
self,
topic: str,
payload: bytes,
qos: int,
retain: bool,
) -> SimpleNamespace:
self.publish_calls.append((topic, payload, qos, retain))
mid = self.next_mid
self.next_mid += 1
self.events.append(("publish", mid))
if topic == "lixel/application/request/device_info":
response_topic = "lixel/application/response/device_info"
response_payload = _device_info_response()
else:
fields = {field.number: field.value for field in iter_fields(payload)}
raw_path = fields[3]
assert isinstance(raw_path, bytes)
path = raw_path.decode()
response_topic = CALIBRATION_FILE_RESPONSE_TOPIC
response_payload = _calibration_response(path, _content_for(path))
self.events.append(("message", (response_topic, response_payload)))
return SimpleNamespace(rc=mqtt.MQTT_ERR_SUCCESS, mid=mid)
def loop(self, timeout: float) -> mqtt.MQTTErrorCode:
del timeout
if not self.events:
return mqtt.MQTT_ERR_SUCCESS
kind, value = self.events.popleft()
if kind == "connect":
self.on_connect(self, None, SimpleNamespace(), value, None)
elif kind == "subscribe":
self.on_subscribe(
self,
None,
value,
[FakeReasonCode() for _topic in CALIBRATION_READ_SUBSCRIPTIONS],
None,
)
elif kind == "publish":
self.on_publish(self, None, value, FakeReasonCode(), None)
elif kind == "message":
topic, payload = value
self.on_message(self, None, SimpleNamespace(topic=topic, payload=payload))
return mqtt.MQTT_ERR_SUCCESS
def unsubscribe(self, topics: list[str]) -> tuple[mqtt.MQTTErrorCode, int]:
self.unsubscribe_calls.append(topics)
return mqtt.MQTT_ERR_SUCCESS, 50
def disconnect(self) -> mqtt.MQTTErrorCode:
return mqtt.MQTT_ERR_SUCCESS
def test_reviewed_mqtt_reader_performs_one_identity_read_then_two_exact_file_reads() -> None:
client = FakeCalibrationClient()
reader = ReviewedCalibrationMqttReader(
"10.255.254.54",
client_factory=lambda: client, # type: ignore[arg-type]
)
result = reader.read_factory_calibration(_authority())
assert result.binding == _binding()
assert [item.path for item in result.files] == list(FACTORY_CALIBRATION_PATHS)
assert client.subscribe_calls == [list(CALIBRATION_READ_SUBSCRIPTIONS)]
assert [topic for topic, _payload, _qos, _retain in client.publish_calls] == [
"lixel/application/request/device_info",
CALIBRATION_FILE_REQUEST_TOPIC,
CALIBRATION_FILE_REQUEST_TOPIC,
]
assert all(qos == 2 and not retain for _topic, _payload, qos, retain in client.publish_calls)
assert result.transport["publish_attempts"] == 3
assert result.transport["correlated_responses"] == 3
assert result.transport["write_command_available"] is False
def test_private_snapshot_is_new_only_and_hashes_both_exact_artifacts(tmp_path: Path) -> None:
files = tuple(
decode_factory_calibration_file_response(
_calibration_response(path, _content_for(path)),
build_factory_calibration_file_read(_authority(), _binding(), path),
_authority(),
_binding(),
)
for path in FACTORY_CALIBRATION_PATHS
)
result = FactoryCalibrationReadResult(
binding=_binding(),
files=(files[0], files[1]),
transport={"automatic_retry": False, "publish_attempts": 3},
)
snapshot = seal_factory_calibration_snapshot(
result,
evidence_root=tmp_path,
compatibility_profile_id=PROFILE_ID,
captured_at=datetime(2026, 7, 20, 10, 30, tzinfo=UTC),
)
assert snapshot["status"] == "available"
internal = snapshot["device_internal_calibration"]
assert isinstance(internal, dict)
snapshot_path = Path(str(internal["private_snapshot_path"]))
assert snapshot_path.is_dir()
assert (snapshot_path / "camera.yaml").read_bytes() == _content_for(
FACTORY_CAMERA_CALIBRATION_PATH
)
assert (snapshot_path / "extrinsic_camera_lidar.yaml").read_bytes() == _content_for(
FACTORY_CAMERA_LIDAR_EXTRINSIC_PATH
)
manifest = json.loads((snapshot_path / "manifest.json").read_text(encoding="utf-8"))
assert manifest["source"]["request_command"] == 5
assert manifest["source"]["write_command_available"] is False
assert manifest["normalized_calibration"]["transform_notation"] == (
"T_destination_from_source"
)
assert [item["source_path"] for item in manifest["artifacts"]] == list(
FACTORY_CALIBRATION_PATHS
)
assert snapshot_path.stat().st_mode & 0o777 == 0o700
assert all(path.stat().st_mode & 0o777 == 0o600 for path in snapshot_path.iterdir())
assert internal["camera_stream_mapping"]["status"] == "firmware-profile-verified"