Preserve the completed teach-and-repeat laboratory stage: reference preparation, cascaded acquisition, local tracking and recovery, recording lifecycle, replay qualification, and persistent Rerun scene controls. Document the open grid-picking regression and Rerun upgrade contract. No autonomous driving or loop-closure optimization is claimed.
65 lines
2.4 KiB
Python
65 lines
2.4 KiB
Python
"""The archive fault adapter omits receipts without changing surviving clocks."""
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
|
|
def adapter_module():
|
|
path = Path(__file__).resolve().parents[1] / "scripts/planning_archive_source.py"
|
|
spec = importlib.util.spec_from_file_location("archive_fault_probe", path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def test_fault_preserves_survivor_identity_payload_and_receipt_offsets(monkeypatch):
|
|
module = adapter_module()
|
|
original = [
|
|
SimpleNamespace(
|
|
received_monotonic_ns=int((100 + t) * 1e9),
|
|
received_at_epoch_ns=int((1000 + t) * 1e9),
|
|
topic="fixture/lio_pcl" if i % 2 else "fixture/lio_pose",
|
|
sequence=i + 1,
|
|
payload=bytes([i]),
|
|
)
|
|
for i, t in enumerate([0, 1, 2, 3, 3.9, 4, 5])
|
|
]
|
|
monkeypatch.setattr(module, "iter_replay_messages", lambda _: iter(original))
|
|
clock = [int(500e9)]
|
|
|
|
def wait(delay):
|
|
clock[0] += int(delay * 1e9)
|
|
return False
|
|
|
|
monkeypatch.setattr(module, "time", SimpleNamespace(monotonic_ns=lambda: clock[0]))
|
|
source = module.ReceiptQueueArchiveSource(Path("unused"), "B", 10, drop_interval_s=(2, 4))
|
|
source.stop = SimpleNamespace(wait=wait)
|
|
source.started = clock[0]
|
|
published = []
|
|
source.ingress = SimpleNamespace(
|
|
publish=lambda **kw: published.append(kw) or True,
|
|
end_session=lambda session: None,
|
|
)
|
|
source.publish()
|
|
kept = [original[i] for i in [0, 1, 5, 6]]
|
|
assert source.error is None
|
|
assert [x["sequence"] for x in source.dropped_receipts] == [3, 4, 5]
|
|
for out, entry in zip(published, kept, strict=True):
|
|
assert out["source_sequence"] == entry.sequence
|
|
assert out["payload"] is entry.payload
|
|
assert out["captured_at_epoch_ns"] == entry.received_at_epoch_ns
|
|
assert out["received_monotonic_ns"] == source.started + entry.received_monotonic_ns - int(
|
|
100e9
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("interval", [(2, 2), (4, 2), (0, 2), (1, 11), (1, float("nan"))])
|
|
def test_invalid_fault_interval_is_rejected(interval):
|
|
with pytest.raises(ValueError, match="fault interval"):
|
|
adapter_module().ReceiptQueueArchiveSource(
|
|
Path("unused"), "B", 10, drop_interval_s=interval
|
|
)
|