feat(perception): connect full graph to external streaming source and scene receiver
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""Small network/controller seam test, not decoder or GPU qualification."""
|
||||
|
||||
import asyncio
|
||||
import importlib
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("grpc")
|
||||
|
||||
from test_perception_streaming_grpc import eventually, identity, tls # noqa: F401,E402
|
||||
|
||||
from k1link.perception.streaming_grpc import GrpcStreamClient # noqa: E402
|
||||
from k1link.perception.streaming_lifecycle import StreamingLifecycle # noqa: E402
|
||||
from k1link.perception.streaming_queue import StreamMailbox # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter(monkeypatch):
|
||||
monkeypatch.syspath_prepend(
|
||||
str(
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments/perception/worker/streaming_profile_stage1"
|
||||
)
|
||||
)
|
||||
module = importlib.import_module("pilot_grpc_graph")
|
||||
control = importlib.import_module("pilot_network_control")
|
||||
proof = {"boot_id": "synthetic-clock-proof", "time_namespace_offsets": ["monotonic 0 0"]}
|
||||
monkeypatch.setattr(module, "kernel_clock", lambda: proof)
|
||||
monkeypatch.setattr(control, "kernel_clock", lambda: proof)
|
||||
return module, control
|
||||
|
||||
|
||||
def test_adapter_rotates_grant_only_after_old_stream_drains(tmp_path, tls, adapter): # noqa: F811
|
||||
module, control = adapter
|
||||
cert, key = tmp_path / "cert", tmp_path / "key"
|
||||
cert.write_bytes(tls[0])
|
||||
key.write_bytes(tls[1])
|
||||
status = tmp_path / "status"
|
||||
status.mkdir()
|
||||
runtime = StreamingLifecycle(
|
||||
identity(),
|
||||
tmp_path / "lease",
|
||||
StreamMailbox(),
|
||||
threading.Event(),
|
||||
clock_ns=lambda: 1_000_000_000,
|
||||
recover_input=True,
|
||||
source_clock_ns=lambda: 1_000_000_000,
|
||||
)
|
||||
runtime.ready()
|
||||
closed = []
|
||||
bridge = module.NetworkGraphBridge(
|
||||
runtime,
|
||||
SimpleNamespace(close=lambda: closed.append(True)),
|
||||
{},
|
||||
control=tmp_path,
|
||||
source_status=status,
|
||||
source_zero=1,
|
||||
certificate=cert,
|
||||
private_key=key,
|
||||
address="localhost:0",
|
||||
reset_temporal=lambda: None,
|
||||
)
|
||||
# Capture dynamically bound port without opening another listener.
|
||||
real_serve = bridge.endpoint.serve
|
||||
|
||||
async def serve(*args, **kwargs):
|
||||
server, port = await real_serve(*args, **kwargs)
|
||||
bridge.port = port
|
||||
return server, port
|
||||
|
||||
bridge.endpoint.serve = serve
|
||||
|
||||
async def check():
|
||||
bridge.start()
|
||||
_, first = control.read_grant(tmp_path / "grant.json")
|
||||
client = GrpcStreamClient(f"localhost:{bridge.port}", tls[0], first)
|
||||
try:
|
||||
await client.open()
|
||||
await client.close() # True network disconnect, not a synthetic runtime pause.
|
||||
await eventually(
|
||||
lambda: control.read_grant(tmp_path / "grant.json")[1].epoch != first.epoch
|
||||
)
|
||||
_, second = control.read_grant(tmp_path / "grant.json")
|
||||
assert runtime.continuity.phase == "synchronizing"
|
||||
assert second.epoch.lease_generation == first.epoch.lease_generation
|
||||
assert runtime.start == first.epoch and not runtime.stop_event.is_set()
|
||||
assert bridge.endpoint.active is None
|
||||
assert len(bridge.epoch_reports) == 1
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
try:
|
||||
asyncio.run(check())
|
||||
finally:
|
||||
runtime.request_stop("completed")
|
||||
assert bridge.close()
|
||||
assert runtime.close()
|
||||
assert runtime.mailbox.bytes == 0 and closed
|
||||
|
||||
|
||||
def test_foreign_clock_cannot_be_silently_subtracted(tmp_path, adapter):
|
||||
_, control = adapter
|
||||
path = tmp_path / "grant.json"
|
||||
control.write_control(path, {"clock": {"boot_id": "other"}})
|
||||
with pytest.raises(ValueError, match="foreign"):
|
||||
control.read_grant(path)
|
||||
|
||||
|
||||
def test_delayed_source_start_does_not_open_an_idle_connection(adapter):
|
||||
source = importlib.import_module("pilot_grpc_source")
|
||||
assert source.connection_delay(3_000_000_000, 1_000_000_000) == 1.9
|
||||
assert source.connection_delay(3_000_000_000, 2_950_000_000) == 0
|
||||
assert source.connection_delay(3_000_000_000, 3_010_000_000) == 0
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Bounded actual scene/mask codec; no models or real recordings."""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.perception.realtime_contract import StreamStart
|
||||
from k1link.perception.streaming_scene_payload import (
|
||||
MAX_SCENE_JSON,
|
||||
decode_scene_payload,
|
||||
encode_scene_payload,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample(monkeypatch):
|
||||
monkeypatch.syspath_prepend(
|
||||
str(
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "experiments/perception/worker/streaming_profile_stage1"
|
||||
)
|
||||
)
|
||||
pilot = importlib.import_module("pilot_freshness")
|
||||
epoch = StreamStart(
|
||||
"run", "source", "worker", "epoch", 1, *(["a" * 64] * 4), pilot.CLOCK_DOMAIN, "live"
|
||||
)
|
||||
mask = b"\x01\x02\x03\x04"
|
||||
stamp = 9_007_199_254_740_993
|
||||
scene = dict(
|
||||
segmentation_sha256=sha256(mask).hexdigest(),
|
||||
proposals=[],
|
||||
observations=[],
|
||||
tracks=[],
|
||||
threats=[],
|
||||
surface_state="valid",
|
||||
range_estimator={},
|
||||
costmap_states=[1],
|
||||
costmap_material=[1],
|
||||
policy_actions=[0],
|
||||
costmap_grid=[[0.0, 0.0, 0.45, 0.45]],
|
||||
policy_counts={"ALLOW_candidate": 1, "HIGH_COST": 0, "NO_GO": 0},
|
||||
tgs_counts={"oldest_permissive_cell_source_ns": stamp},
|
||||
commands_enabled=False,
|
||||
actuation_allowed=False,
|
||||
sequence=2,
|
||||
original_source_ns=stamp,
|
||||
runtime_binding=epoch.to_dict(),
|
||||
)
|
||||
bundle = dict(
|
||||
sequence=2,
|
||||
time_ns=stamp,
|
||||
due_ns=1_000_000_000,
|
||||
available=True,
|
||||
lineage={
|
||||
"pose_host_monotonic_ns": stamp,
|
||||
"point_increments": [{"host_monotonic_ns": stamp}],
|
||||
},
|
||||
)
|
||||
ddr = dict(state="current", source_sequence=2, source_host_monotonic_ns=stamp)
|
||||
pilot.prepare_publication(scene, bundle, ddr, epoch_id="epoch", now_ns=bundle["due_ns"])
|
||||
return scene, mask, epoch, bundle, pilot
|
||||
|
||||
|
||||
def test_actual_mask_grid_and_int64_source_identity_roundtrip(sample):
|
||||
scene, mask, epoch, bundle, pilot = sample
|
||||
raw = encode_scene_payload(json.dumps(scene).encode(), mask, (2, 2))
|
||||
received, plane, shape = decode_scene_payload(raw, epoch, 2)
|
||||
assert received == scene and bytes(plane) == mask and shape == (2, 2)
|
||||
assert plane.obj is raw and plane.readonly # no second retained mask allocation
|
||||
freshness = pilot.validate_receipt(received, bundle, epoch_id="epoch")
|
||||
view, assessed = pilot.assess_receipt(
|
||||
received, freshness, bundle=bundle, now_ns=bundle["due_ns"] + 251_000_000
|
||||
)
|
||||
assert not assessed.fresh_complete and view["policy_actions"] == [2]
|
||||
assert received["policy_actions"] == [0] # transit age never rewrites published evidence
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mutation",
|
||||
[
|
||||
"mask",
|
||||
"shape",
|
||||
"truncated",
|
||||
"extra",
|
||||
"magic",
|
||||
"sequence",
|
||||
"epoch",
|
||||
"authority",
|
||||
"digest",
|
||||
"nan",
|
||||
"duplicate",
|
||||
],
|
||||
)
|
||||
def test_payload_rejects_bad_framing_binding_and_integrity(sample, mutation):
|
||||
scene, mask, epoch, _, _ = sample
|
||||
if mutation == "sequence":
|
||||
scene["sequence"] = 3
|
||||
if mutation == "epoch":
|
||||
scene["runtime_binding"]["epoch_id"] = "old"
|
||||
if mutation == "authority":
|
||||
scene["actuation_allowed"] = True
|
||||
if mutation == "digest":
|
||||
scene["segmentation_sha256"] = "0" * 64
|
||||
if mutation == "nan":
|
||||
scene["range_estimator"] = {"range": float("nan")}
|
||||
encoded = json.dumps(scene).encode()
|
||||
if mutation == "duplicate":
|
||||
encoded = b'{"sequence":2,' + encoded[1:]
|
||||
raw = encode_scene_payload(encoded, mask, (2, 2))
|
||||
if mutation == "mask":
|
||||
raw = raw[:-1] + b"z"
|
||||
if mutation == "shape":
|
||||
raw = raw[:10] + b"\x00\x03" + raw[12:]
|
||||
if mutation == "truncated":
|
||||
raw = raw[:-1]
|
||||
if mutation == "extra":
|
||||
raw += b"z"
|
||||
if mutation == "magic":
|
||||
raw = b"NOPE" + raw[4:]
|
||||
with pytest.raises(ValueError):
|
||||
decode_scene_payload(raw, epoch, 2)
|
||||
|
||||
|
||||
def test_grid_is_covered_by_the_costmap_layer_digest(sample):
|
||||
scene, mask, epoch, bundle, pilot = sample
|
||||
scene["costmap_grid"][0][0] = 500
|
||||
raw = encode_scene_payload(json.dumps(scene).encode(), mask, (2, 2))
|
||||
received, _, _ = decode_scene_payload(raw, epoch, 2)
|
||||
with pytest.raises(ValueError, match="digest"):
|
||||
pilot.validate_receipt(received, bundle, epoch_id="epoch")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"shape,mask,scene",
|
||||
[
|
||||
((0, 2), b"", b"{}"),
|
||||
((True, 1), b"x", b"{}"),
|
||||
((2, 2), b"abc", b"{}"),
|
||||
((1, 1), b"x", b" " * (MAX_SCENE_JSON + 1)),
|
||||
((1, 1), b"x", b""),
|
||||
],
|
||||
)
|
||||
def test_encoder_bounds(shape, mask, scene):
|
||||
with pytest.raises(ValueError):
|
||||
encode_scene_payload(scene, mask, shape)
|
||||
Reference in New Issue
Block a user