Add packaged Insta360 X4 integration and recover paired Node channels
Discover independent camera instances and prepare their versioned runtime from Node or remote Core. Add isolated SDK workers, camera controls, raw dual-fisheye WebRTC preview, and shared action/region loading states. Recover existing Node bindings over known Tailscale addresses after a Core LAN address change. Preserve identities and trust, pin both peers, migrate endpoints with revision checks, and require real heartbeats for online status. Fix the Python client certificate profile for Go X509 verification. Pin Design Guideline 8c53f73 and retain installer/build/acceptance history. Node 0.8.19 is installed; X4 0.1.3-3 is bundled but hardware activation is pending. Validation: qualified DG/Node builds and Go race tests; 31 fleet tests; Python-to-Go certificate interoperability and live tailnet recovery with five fresh heartbeats; prior 38 X4 tests and bounded remote WebRTC acceptance. Clean-OS, replug/power autonomy, local X4 video and long-run stability remain open.
This commit is contained in:
@@ -85,6 +85,148 @@ def cert(row):
|
||||
)
|
||||
|
||||
|
||||
def test_migrated_heartbeat_preserves_identity_and_old_reply_cannot_reverse(setup):
|
||||
fleet, _, _, _ = setup
|
||||
public, _ = create(setup)
|
||||
fleet.advance(public["id"])
|
||||
old = fleet.find(public["id"])
|
||||
payload = {
|
||||
**heartbeat(old),
|
||||
"core_endpoint": "https://100.64.20.5:8782",
|
||||
"endpoint_revision": 1,
|
||||
}
|
||||
assert fleet.receive(cert(old), "/v1/node/heartbeat", payload, address="192.168.20.5")[0] == 400
|
||||
assert fleet.find(public["id"])["binding"] == old["binding"]
|
||||
assert fleet.receive(cert(old), "/v1/node/heartbeat", payload, address="100.64.20.5")[0] == 200
|
||||
current = fleet.find(public["id"])
|
||||
assert current["core_address"] == "100.64.20.5"
|
||||
assert current["binding"]["binding_id"] == old["binding"]["binding_id"]
|
||||
assert current["binding"]["client_pem"] == old["binding"]["client_pem"]
|
||||
assert (
|
||||
fleet.receive(cert(old), "/v1/node/heartbeat", heartbeat(old), address="192.168.20.5")[0]
|
||||
== 409
|
||||
)
|
||||
assert fleet.find(public["id"])["binding"] == current["binding"]
|
||||
fleet.revoke(public["id"])
|
||||
assert fleet.receive(cert(old), "/v1/node/heartbeat", payload, address="100.64.20.5")[0] == 410
|
||||
|
||||
|
||||
def test_recovery_only_uses_previously_reported_tailnet_addresses():
|
||||
from k1link.fleet.recovery import node_addresses
|
||||
|
||||
assert node_addresses(
|
||||
{
|
||||
"inventory": {
|
||||
"networks": [
|
||||
{
|
||||
"up": True,
|
||||
"addresses": ["192.168.20.4/24", "8.8.8.8", "100.64.20.4/32", "::1"],
|
||||
},
|
||||
{"up": False, "addresses": ["100.64.1.1/32"]},
|
||||
]
|
||||
}
|
||||
}
|
||||
) == ["100.64.20.4"]
|
||||
|
||||
|
||||
def test_recovery_client_leaf_is_distinct_from_issuer_with_same_pinned_key(setup):
|
||||
from cryptography.x509.oid import ExtendedKeyUsageOID
|
||||
|
||||
from k1link.fleet.recovery import client_context
|
||||
|
||||
fleet, _, _, _ = setup
|
||||
client_context(fleet.trust)
|
||||
leaf, root = x509.load_pem_x509_certificates((fleet.root / "recovery-client.pem").read_bytes())
|
||||
assert leaf.subject != root.subject
|
||||
assert leaf.issuer == root.subject
|
||||
assert leaf.public_key().public_bytes_raw() == root.public_key().public_bytes_raw()
|
||||
assert public_id("core_", leaf.public_key()) == fleet.trust.core_id
|
||||
assert (
|
||||
ExtendedKeyUsageOID.CLIENT_AUTH
|
||||
in leaf.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value
|
||||
)
|
||||
leaf.verify_directly_issued_by(root)
|
||||
|
||||
|
||||
def test_recovery_pins_node_before_application_request(setup, monkeypatch):
|
||||
from k1link.fleet import recovery
|
||||
|
||||
fleet, _, _, _ = setup
|
||||
public, _ = create(setup)
|
||||
fleet.advance(public["id"])
|
||||
row = fleet.find(public["id"])
|
||||
row["inventory"] = {"networks": [{"up": True, "addresses": ["100.64.20.4/32"]}]}
|
||||
fleet.save(row)
|
||||
calls = []
|
||||
|
||||
class Connection:
|
||||
sock = None
|
||||
|
||||
def connect(self):
|
||||
self.sock = self
|
||||
|
||||
def getpeercert(self, **kwargs):
|
||||
return b"not the saved Node certificate"
|
||||
|
||||
def close(self):
|
||||
calls.append("closed")
|
||||
|
||||
monkeypatch.setattr(recovery.http.client, "HTTPSConnection", lambda *a, **kw: Connection())
|
||||
monkeypatch.setattr(recovery, "request", lambda *a: calls.append("request"))
|
||||
with pytest.raises(ValueError):
|
||||
recovery.recover(fleet, row, "100.64.20.4", None)
|
||||
assert calls == ["closed"]
|
||||
|
||||
|
||||
def test_recovery_rechecks_revocation_and_waits_for_heartbeat(setup, monkeypatch):
|
||||
from k1link.fleet import recovery
|
||||
|
||||
fleet, _, _, _ = setup
|
||||
public, _ = create(setup)
|
||||
fleet.advance(public["id"])
|
||||
row = fleet.find(public["id"])
|
||||
row["inventory"] = {"networks": [{"up": True, "addresses": ["100.64.20.4/32"]}]}
|
||||
fleet.save(row)
|
||||
calls = []
|
||||
|
||||
class Connection:
|
||||
def connect(self):
|
||||
self.sock = self
|
||||
|
||||
def getpeercert(self, **kwargs):
|
||||
return b"verified below by test adapter"
|
||||
|
||||
def getsockname(self):
|
||||
return ("100.64.20.5", 30000)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(recovery.http.client, "HTTPSConnection", lambda *a, **kw: Connection())
|
||||
monkeypatch.setattr(recovery, "verify_node_certificate", lambda *a: None)
|
||||
|
||||
def request(connection, path, body):
|
||||
calls.append(path)
|
||||
return {
|
||||
"schema": recovery.SCHEMA,
|
||||
"node_id": row["node_id"],
|
||||
"core_id": fleet.trust.core_id,
|
||||
"binding_id": row["binding"]["binding_id"],
|
||||
"endpoint": row["binding"]["endpoint"],
|
||||
"endpoint_revision": 0,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(recovery, "request", request)
|
||||
recovery.recover(fleet, row, "100.64.20.4", None)
|
||||
assert calls == ["/v1/channel/inspect", "/v1/channel/migrate"]
|
||||
assert fleet.find(public["id"])["binding"] == row["binding"]
|
||||
assert fleet.public(fleet.find(public["id"]))["connectivity"] == "offline"
|
||||
calls.clear()
|
||||
fleet.revoke(public["id"])
|
||||
recovery.recover(fleet, row, "100.64.20.4", None)
|
||||
assert calls == ["/v1/channel/inspect"]
|
||||
|
||||
|
||||
def test_preview_add_is_durable_and_idempotent(setup):
|
||||
fleet, invite, _, calls = setup
|
||||
public, preview = create(setup)
|
||||
@@ -386,3 +528,53 @@ def test_sensor_cannot_claim_another_board_identity(setup):
|
||||
value["devices"][0]["snapshot"]["context"]["execution"]["node_id"] = "node_wrong"
|
||||
with pytest.raises(ValueError):
|
||||
validate_inventory(value, row["node_id"])
|
||||
|
||||
|
||||
def test_uninitialized_x4_can_be_prepared_remotely_with_instance_progress(setup):
|
||||
from k1link.fleet import sensors
|
||||
|
||||
public, _ = create(setup)
|
||||
fleet = setup[0]
|
||||
row = fleet.find(public["id"])
|
||||
inventory = sensor_inventory(row)
|
||||
item = inventory["devices"][0]
|
||||
item.update(id="instax4_" + "a" * 32, kind="insta360.x4", prepared=False, configured=False)
|
||||
item["snapshot"]["context"]["device"].update(
|
||||
device_id=item["id"],
|
||||
model={
|
||||
"plugin_id": "missioncore.insta360",
|
||||
"plugin_version": "0.1.0",
|
||||
"model_id": "insta360.x4",
|
||||
},
|
||||
)
|
||||
assert fleet.receive(cert(row), "/v1/node/heartbeat", inventory)[0] == 200
|
||||
now = datetime.now(UTC)
|
||||
command = {
|
||||
"api_version": "missioncore.nodedc/plugin-sdk/v0alpha2",
|
||||
"kind": "OperationRequest",
|
||||
"operation_id": "op_" + "b" * 32,
|
||||
"idempotency_key": "op_" + "b" * 32,
|
||||
"session": {"device_id": item["id"], "session_id": "sensor_test"},
|
||||
"requested_at": now.isoformat(),
|
||||
"deadline_at": (now + timedelta(seconds=350)).isoformat(),
|
||||
"action_id": "prepare",
|
||||
"parameters": {},
|
||||
}
|
||||
assert sensors.submit(fleet, row["id"], command)["state"] == "queued"
|
||||
_, result = fleet.receive(cert(row), "/v1/node/heartbeat", inventory)
|
||||
assert result["sensor_commands"] == [command]
|
||||
progress = {
|
||||
"operation_id": command["operation_id"],
|
||||
"device_id": item["id"],
|
||||
"model_id": "insta360.x4",
|
||||
"phase": "verify",
|
||||
"state": "running",
|
||||
"steps": [{"id": "profile", "state": "complete"}, {"id": "verify", "state": "running"}],
|
||||
}
|
||||
inventory["sensor_state"]["preparations"] = [progress]
|
||||
inventory["sensor_results"] = [
|
||||
{"command": command, "state": "running", "preparation": progress}
|
||||
]
|
||||
assert fleet.receive(cert(row), "/v1/node/heartbeat", inventory)[0] == 200
|
||||
assert sensors.operation(fleet, row["id"], command["operation_id"])["preparation"] == progress
|
||||
assert fleet.find(row["id"])["sensor_state"]["preparations"] == [progress]
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
"""Camera command boundaries with a synthetic camera; never load a vendor SDK."""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1] / "plugins/insta360-x4/runtime"
|
||||
spec = importlib.util.spec_from_file_location("missioncore_insta360", ROOT / "__init__.py")
|
||||
package = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = package
|
||||
spec.loader.exec_module(package)
|
||||
from missioncore_insta360 import identity, native, operations # noqa: E402
|
||||
|
||||
ID_A = "instax4_" + "a" * 32
|
||||
ID_B = "instax4_" + "b" * 32
|
||||
|
||||
|
||||
def command(action="record.start", *, device=ID_A, session="session_test_a", params=None):
|
||||
now = datetime.now(UTC)
|
||||
op = "op_" + uuid.uuid4().hex
|
||||
return {
|
||||
"api_version": "missioncore.nodedc/plugin-sdk/v0alpha2",
|
||||
"kind": "OperationRequest",
|
||||
"operation_id": op,
|
||||
"idempotency_key": op,
|
||||
"session": {"session_id": session, "device_id": device},
|
||||
"action_id": action,
|
||||
"requested_at": now.isoformat(),
|
||||
"deadline_at": (now + timedelta(seconds=60)).isoformat(),
|
||||
"parameters": params or {},
|
||||
}
|
||||
|
||||
|
||||
class Camera:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
self.answer = {"state": "complete", "result": {"recording": True}}
|
||||
self.hook = None
|
||||
|
||||
def call(self, action, params):
|
||||
self.calls.append((action, params))
|
||||
if self.hook:
|
||||
self.hook()
|
||||
return self.answer
|
||||
|
||||
|
||||
def test_concurrent_remote_and_local_retry_dispatches_once(tmp_path):
|
||||
camera = Camera()
|
||||
executor = operations.Operations(ID_A, "session_test_a", tmp_path, camera)
|
||||
request = command()
|
||||
entered, release = threading.Event(), threading.Event()
|
||||
|
||||
def inspect_receipt():
|
||||
receipt = json.loads((tmp_path / (request["operation_id"] + ".json")).read_text())
|
||||
assert receipt["result"]["state"] == "unknown"
|
||||
entered.set()
|
||||
assert release.wait(3)
|
||||
|
||||
camera.hook = inspect_receipt
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
first = pool.submit(executor.execute, request)
|
||||
assert entered.wait(3)
|
||||
second = pool.submit(executor.execute, request)
|
||||
release.set()
|
||||
assert first.result() == second.result() == camera.answer
|
||||
assert len(camera.calls) == 1
|
||||
|
||||
|
||||
def test_crash_receipt_survives_restart_without_replaying_recording(tmp_path):
|
||||
camera = Camera()
|
||||
|
||||
def crash():
|
||||
raise SystemExit("synthetic crash after dispatch")
|
||||
|
||||
camera.hook = crash
|
||||
request = command()
|
||||
with pytest.raises(SystemExit):
|
||||
operations.Operations(ID_A, "session_test_a", tmp_path, camera).execute(request)
|
||||
fresh = Camera()
|
||||
recovered = operations.Operations(ID_A, "session_after_restart", tmp_path, fresh)
|
||||
assert recovered.execute(request)["state"] == "unknown"
|
||||
assert not fresh.calls
|
||||
|
||||
|
||||
def test_camera_binding_session_deadline_and_parameters_precede_effects(tmp_path):
|
||||
camera = Camera()
|
||||
executor = operations.Operations(ID_A, "session_test_a", tmp_path, camera)
|
||||
wrong = [
|
||||
command(device=ID_B),
|
||||
command(session="session_old"),
|
||||
command(params={"force": True}),
|
||||
command("settings.apply", params={"mode": 7, "key": "library_path", "value": 1}),
|
||||
command("settings.apply", params={"mode": 7, "key": "iso", "value": True}),
|
||||
]
|
||||
expired = command()
|
||||
expired["requested_at"] = (datetime.now(UTC) - timedelta(minutes=2)).isoformat()
|
||||
expired["deadline_at"] = (datetime.now(UTC) - timedelta(minutes=1)).isoformat()
|
||||
wrong.append(expired)
|
||||
for request in wrong:
|
||||
with pytest.raises(ValueError):
|
||||
executor.execute(request)
|
||||
assert not camera.calls
|
||||
assert not list(tmp_path.iterdir())
|
||||
|
||||
|
||||
def test_operation_identity_cannot_be_reused_for_another_effect(tmp_path):
|
||||
camera = Camera()
|
||||
executor = operations.Operations(ID_A, "session_test_a", tmp_path, camera)
|
||||
request = command()
|
||||
executor.execute(request)
|
||||
request["action_id"] = "record.stop"
|
||||
with pytest.raises(ValueError, match="already in use"):
|
||||
executor.execute(request)
|
||||
assert len(camera.calls) == 1
|
||||
|
||||
|
||||
def test_two_instances_keep_receipts_and_recording_independent(tmp_path):
|
||||
a, b = Camera(), Camera()
|
||||
first = operations.Operations(ID_A, "session_test_a", tmp_path / "a", a)
|
||||
second = operations.Operations(ID_B, "session_test_b", tmp_path / "b", b)
|
||||
first.execute(command())
|
||||
second.execute(command("preview.stop", device=ID_B, session="session_test_b"))
|
||||
assert [action for action, _ in a.calls] == ["record.start"]
|
||||
assert [action for action, _ in b.calls] == ["preview.stop"]
|
||||
|
||||
|
||||
def test_setting_acknowledgement_requires_camera_readback(tmp_path):
|
||||
camera = Camera()
|
||||
camera.answer = {"state": "complete", "result": {"values": {"white_balance": 0}}}
|
||||
executor = operations.Operations(ID_A, "session_test_a", tmp_path, camera)
|
||||
request = command("settings.apply", params={"mode": 7, "key": "white_balance", "value": 5000})
|
||||
assert executor.execute(request)["state"] == "unknown"
|
||||
camera.answer = {"state": "complete", "result": {"values": {"white_balance": 5000}}}
|
||||
# The old command remains uncertain. Only a new explicit command may run.
|
||||
assert executor.execute(request)["state"] == "unknown"
|
||||
assert len(camera.calls) == 1
|
||||
assert (
|
||||
executor.execute(command("settings.apply", params=request["parameters"]))["state"]
|
||||
== "complete"
|
||||
)
|
||||
|
||||
|
||||
def test_failed_journal_prevents_sdk_dispatch(tmp_path, monkeypatch):
|
||||
camera = Camera()
|
||||
|
||||
def full(*_):
|
||||
raise OSError("synthetic disk full")
|
||||
|
||||
monkeypatch.setattr(operations, "atomic", full)
|
||||
with pytest.raises(OSError):
|
||||
operations.Operations(ID_A, "session_test_a", tmp_path, camera).execute(command())
|
||||
assert not camera.calls
|
||||
|
||||
|
||||
def test_model_binding_uses_product_and_tracks_usb_replug(tmp_path):
|
||||
usb = tmp_path / "1-2.3"
|
||||
usb.mkdir()
|
||||
values = {
|
||||
"idVendor": "2e1a",
|
||||
"idProduct": "0002",
|
||||
"product": "Insta360 X4",
|
||||
"serial": "SYNTHETIC-X4-A",
|
||||
"busnum": "1",
|
||||
"devnum": "7",
|
||||
}
|
||||
for key, value in values.items():
|
||||
(usb / key).write_text(value)
|
||||
first = identity.read_binding("1-2.3", tmp_path)
|
||||
(usb / "devnum").write_text("8")
|
||||
second = identity.read_binding("1-2.3", tmp_path)
|
||||
assert first.device_id == second.device_id and first != second
|
||||
assert second.device_path == "/dev/bus/usb/001/008"
|
||||
(usb / "product").write_text("Insta360 ONE R")
|
||||
with pytest.raises(ValueError, match="model"):
|
||||
identity.read_binding("1-2.3", tmp_path)
|
||||
with pytest.raises(ValueError, match="binding"):
|
||||
identity.read_binding("../1-2.3", tmp_path)
|
||||
|
||||
|
||||
def test_sdk_library_is_not_loaded_before_isolation_check(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def reject(_):
|
||||
raise RuntimeError("synthetic namespace refusal")
|
||||
|
||||
monkeypatch.setattr(native, "verify_isolation", reject)
|
||||
monkeypatch.setattr(native.ctypes, "CDLL", lambda value: calls.append(value))
|
||||
with pytest.raises(RuntimeError, match="namespace"):
|
||||
native.NativeCamera(object(), "/unused/library.so", "/unused/logs")
|
||||
assert not calls
|
||||
|
||||
|
||||
def test_video_queue_can_drain_while_a_camera_command_is_blocked():
|
||||
# Construct only the Python transport with a fake C ABI. No isolation
|
||||
# override or vendor library is used by the real NativeCamera constructor.
|
||||
camera = native.NativeCamera.__new__(native.NativeCamera)
|
||||
camera.lock, camera.video_lock = threading.RLock(), threading.Lock()
|
||||
camera.handle, camera.buffer = 1, object()
|
||||
|
||||
class API:
|
||||
def mc_x4_read_video(self, *_):
|
||||
return 0
|
||||
|
||||
camera.api = API()
|
||||
with ThreadPoolExecutor(max_workers=1) as pool, camera.lock:
|
||||
assert pool.submit(camera.video).result(timeout=2) is None
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Collect the same native checks shipped in the Ubuntu build artifact."""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(__file__).resolve().parents[1] / "plugins/insta360-x4/native/tests/check_abi.py"
|
||||
spec = importlib.util.spec_from_file_location("missioncore_x4_abi_tests", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
TestNativeBridge = module.TestNativeBridge
|
||||
@@ -0,0 +1,91 @@
|
||||
"""The build must fail before using corrupt, wrong-platform or partial SDK bytes."""
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"x4_sdk_source", ROOT / "plugins/insta360-x4/packaging/fetch_sdk.py"
|
||||
)
|
||||
sdk = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(sdk)
|
||||
|
||||
|
||||
def elf(machine=62):
|
||||
strings = b"\0libc.so.6\0"
|
||||
table_end = 64 + 3 * 64
|
||||
data = bytearray(table_end)
|
||||
data[:7] = b"\x7fELF\x02\x01\x01"
|
||||
struct.pack_into("<H", data, 18, machine)
|
||||
struct.pack_into("<Q", data, 40, 64)
|
||||
struct.pack_into("<HH", data, 58, 64, 3)
|
||||
struct.pack_into("<IIQQQQIIQQ", data, 128, 0, 3, 0, 0, table_end, len(strings), 0, 0, 1, 0)
|
||||
struct.pack_into(
|
||||
"<IIQQQQIIQQ", data, 192, 0, 6, 0, 0, table_end + len(strings), 32, 1, 0, 8, 16
|
||||
)
|
||||
return bytes(data) + strings + struct.pack("<qQqQ", 1, 1, 0, 0)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def locked_source(tmp_path, monkeypatch):
|
||||
source = tmp_path / "source"
|
||||
(source / "lib").mkdir(parents=True)
|
||||
data = elf()
|
||||
(source / "lib/libCameraSDK.so").write_bytes(data)
|
||||
lock = {
|
||||
"schema": "missioncore.insta360.sdk-source/v1",
|
||||
"commit": "a" * 40,
|
||||
"files": [
|
||||
{
|
||||
"path": "lib/libCameraSDK.so",
|
||||
"bytes": len(data),
|
||||
"sha256": hashlib.sha256(data).hexdigest(),
|
||||
"lfs": False,
|
||||
}
|
||||
],
|
||||
}
|
||||
path = tmp_path / "sdk-lock.json"
|
||||
path.write_text(json.dumps(lock))
|
||||
monkeypatch.setattr(sdk, "LOCK", path)
|
||||
return source, tmp_path / "build/sdk", lock
|
||||
|
||||
|
||||
def test_import_repeat_and_static_dependency_inspection(locked_source):
|
||||
source, destination, lock = locked_source
|
||||
assert sdk.fetch(destination, source)["needed"] == ["libc.so.6"]
|
||||
# A completed build no longer depends on the acquisition folder or network.
|
||||
assert sdk.fetch(destination, Path("/nonexistent"))["architecture"] == "linux-x86_64"
|
||||
assert sdk.verify(destination, lock)["needed"] == ["libc.so.6"]
|
||||
|
||||
|
||||
def test_corrupt_source_never_becomes_active_and_stage_is_removed(locked_source):
|
||||
source, destination, _ = locked_source
|
||||
(source / "lib/libCameraSDK.so").write_bytes(b"corrupt")
|
||||
with pytest.raises(ValueError, match="checksum"):
|
||||
sdk.fetch(destination, source)
|
||||
assert not destination.exists()
|
||||
assert not list(destination.parent.glob(".sdk-*"))
|
||||
|
||||
|
||||
def test_existing_corrupt_cache_is_not_silently_reused(locked_source):
|
||||
source, destination, _ = locked_source
|
||||
sdk.fetch(destination, source)
|
||||
(destination / "lib/libCameraSDK.so").write_bytes(b"version https://git-lfs.github.com/spec/v1")
|
||||
with pytest.raises(ValueError, match="checksum"):
|
||||
sdk.fetch(destination, source)
|
||||
|
||||
|
||||
def test_wrong_architecture_and_payload_path_are_rejected(tmp_path, locked_source):
|
||||
path = tmp_path / "aarch64.so"
|
||||
path.write_bytes(elf(183))
|
||||
with pytest.raises(ValueError, match="x86_64"):
|
||||
sdk.inspect_elf(path)
|
||||
_, _, lock = locked_source
|
||||
lock["files"][0]["path"] = "../outside"
|
||||
with pytest.raises(ValueError, match="payload"):
|
||||
list(sdk.entries(lock))
|
||||
Reference in New Issue
Block a user