175 lines
6.4 KiB
Python
175 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from test_lidar_replay import _capture
|
|
|
|
from k1link.compute import lidar_replay
|
|
from k1link.compute.lidar_preparation import prepare_lidar_replay_pack_v2
|
|
from k1link.compute.lidar_replay import LidarReplayError
|
|
|
|
|
|
def test_warm_preparation_reuses_legacy_identity_without_source_decode(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
source = _capture(tmp_path)
|
|
producer = Path(lidar_replay.__file__)
|
|
producer_digest = hashlib.sha256(producer.read_bytes()).hexdigest()
|
|
original = lidar_replay.build_lidar_replay_pack_v2(source, tmp_path / "packs")
|
|
manifest_before = (original / "manifest.json").read_bytes()
|
|
closed = []
|
|
original_close = lidar_replay.LidarReplayPackV2.close
|
|
|
|
def no_decode(*args: object, **kwargs: object) -> None:
|
|
pytest.fail("cache hit must not decode the original capture")
|
|
|
|
def close(pack: lidar_replay.LidarReplayPackV2) -> None:
|
|
original_close(pack)
|
|
closed.append(not pack.arrays._arrays)
|
|
|
|
monkeypatch.setattr(lidar_replay, "_capture_arrays", no_decode)
|
|
monkeypatch.setattr(lidar_replay.LidarReplayPackV2, "close", close)
|
|
assert prepare_lidar_replay_pack_v2(source, tmp_path / "packs") == original
|
|
assert (original / "manifest.json").read_bytes() == manifest_before
|
|
assert json.loads(manifest_before)["identity"]["producer_sha256"] == producer_digest
|
|
assert hashlib.sha256(producer.read_bytes()).hexdigest() == producer_digest
|
|
assert closed == [True]
|
|
|
|
|
|
def test_cold_preparation_calls_existing_builder_once(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
source = _capture(tmp_path)
|
|
calls = []
|
|
original = lidar_replay._capture_arrays
|
|
|
|
def decode(path: Path):
|
|
calls.append(path)
|
|
return original(path)
|
|
|
|
monkeypatch.setattr(lidar_replay, "_capture_arrays", decode)
|
|
result = prepare_lidar_replay_pack_v2(source, tmp_path / "packs")
|
|
assert result.is_dir()
|
|
assert calls == [source]
|
|
|
|
|
|
@pytest.mark.parametrize("changed", ["metadata", "origin", "remove-origin", "raw", "session"])
|
|
def test_changed_source_never_reuses_previous_pack(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
changed: str,
|
|
) -> None:
|
|
source = _capture(tmp_path)
|
|
output = tmp_path / "packs"
|
|
old = prepare_lidar_replay_pack_v2(source, output)
|
|
if changed == "metadata":
|
|
path = source.with_name("mqtt.metadata.jsonl")
|
|
path.write_bytes(path.read_bytes().replace(b"5010000000", b"5009000000"))
|
|
elif changed == "origin":
|
|
path = source.with_name("mqtt.timeline.origin.json")
|
|
path.write_bytes(path.read_bytes().replace(b"5000000000", b"4999999999"))
|
|
elif changed == "remove-origin":
|
|
source.with_name("mqtt.timeline.origin.json").unlink()
|
|
elif changed == "raw":
|
|
# The cache boundary checks bytes before trying to decode invalid data.
|
|
source.write_bytes(source.read_bytes()[:-1] + b"x")
|
|
called = []
|
|
|
|
def build(*args: object, **kwargs: object) -> Path:
|
|
called.append((args, kwargs))
|
|
return output / "new-test-pack"
|
|
|
|
monkeypatch.setattr(lidar_replay, "build_lidar_replay_pack_v2", build)
|
|
result = prepare_lidar_replay_pack_v2(
|
|
source,
|
|
output,
|
|
session_id="other-session" if changed == "session" else None,
|
|
)
|
|
assert result != old
|
|
assert len(called) == 1
|
|
assert old.is_dir()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"artifact", ["lidar-replay.npz", "quality-report.json", "equivalence-report.json"]
|
|
)
|
|
def test_corrupt_matching_pack_fails_without_rebuild_or_overwrite(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
artifact: str,
|
|
) -> None:
|
|
source = _capture(tmp_path)
|
|
root = prepare_lidar_replay_pack_v2(source, tmp_path / "packs")
|
|
path = root / artifact
|
|
damaged = b"damaged-evidence"
|
|
path.write_bytes(damaged)
|
|
|
|
def forbidden(*args: object, **kwargs: object) -> None:
|
|
pytest.fail("must preserve corrupt evidence instead of overwriting")
|
|
|
|
monkeypatch.setattr(lidar_replay, "build_lidar_replay_pack_v2", forbidden)
|
|
with pytest.raises(LidarReplayError, match="artifact identity changed"):
|
|
prepare_lidar_replay_pack_v2(source, tmp_path / "packs")
|
|
assert path.read_bytes() == damaged
|
|
|
|
|
|
def test_report_threshold_collision_is_explicit_and_releases_arrays(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
source = _capture(tmp_path)
|
|
root = prepare_lidar_replay_pack_v2(source, tmp_path / "packs")
|
|
closed = []
|
|
original = lidar_replay.LidarReplayPackV2.close
|
|
|
|
def close(pack: lidar_replay.LidarReplayPackV2) -> None:
|
|
original(pack)
|
|
closed.append(not pack.arrays._arrays)
|
|
|
|
monkeypatch.setattr(lidar_replay.LidarReplayPackV2, "close", close)
|
|
with pytest.raises(LidarReplayError, match="threshold differs"):
|
|
prepare_lidar_replay_pack_v2(source, tmp_path / "packs", pose_coverage_threshold_ms=50)
|
|
assert closed == [True]
|
|
assert root.is_dir()
|
|
|
|
|
|
def test_source_mutation_during_cache_validation_is_rejected(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
source = _capture(tmp_path)
|
|
prepare_lidar_replay_pack_v2(source, tmp_path / "packs")
|
|
original = lidar_replay.LidarReplayPackV2.__init__
|
|
|
|
def changed(pack: lidar_replay.LidarReplayPackV2, root: Path) -> None:
|
|
original(pack, root)
|
|
source.with_name("mqtt.metadata.jsonl").touch()
|
|
|
|
monkeypatch.setattr(lidar_replay.LidarReplayPackV2, "__init__", changed)
|
|
with pytest.raises(LidarReplayError, match="changed during preparation"):
|
|
prepare_lidar_replay_pack_v2(source, tmp_path / "packs")
|
|
|
|
|
|
def test_missing_metadata_never_uses_cache(tmp_path: Path) -> None:
|
|
source = _capture(tmp_path)
|
|
prepare_lidar_replay_pack_v2(source, tmp_path / "packs")
|
|
source.with_name("mqtt.metadata.jsonl").unlink()
|
|
with pytest.raises(LidarReplayError, match="exact host timing"):
|
|
prepare_lidar_replay_pack_v2(source, tmp_path / "packs")
|
|
|
|
|
|
def test_symlink_manifest_is_not_followed(tmp_path: Path) -> None:
|
|
source = _capture(tmp_path)
|
|
root = prepare_lidar_replay_pack_v2(source, tmp_path / "packs")
|
|
manifest = root / "manifest.json"
|
|
saved = root / "saved-manifest.json"
|
|
manifest.rename(saved)
|
|
manifest.symlink_to(saved)
|
|
with pytest.raises(LidarReplayError, match="manifest cannot be a symlink"):
|
|
prepare_lidar_replay_pack_v2(source, tmp_path / "packs")
|