Files
NODEDC_MISSION_CORE/tests/test_artifact_gateway.py
T

210 lines
7.1 KiB
Python

from __future__ import annotations
import os
from pathlib import Path
import pytest
from k1link.artifact_gateway import (
ArtifactCacheCapacityError,
ArtifactGateway,
ArtifactIntegrityError,
ArtifactStoreUnavailable,
CentralArtifactStore,
LocalArtifactCache,
)
def _gateway(
tmp_path: Path,
*,
max_bytes: int = 1024,
) -> tuple[ArtifactGateway, Path, Path]:
central_root = tmp_path / "central"
local_root = tmp_path / "local"
return (
ArtifactGateway(
CentralArtifactStore(central_root, create=True),
LocalArtifactCache(
local_root,
max_bytes=max_bytes,
free_space_reserve_bytes=0,
),
),
central_root,
local_root,
)
def test_gateway_publishes_deduplicated_objects_and_resolves_through_local_cache(
tmp_path: Path,
) -> None:
gateway, central_root, _local_root = _gateway(tmp_path)
first = tmp_path / "first.rrd"
duplicate = tmp_path / "duplicate.rrd"
first.write_bytes(b"RRF2same-content")
duplicate.write_bytes(first.read_bytes())
manifest = gateway.publish(
namespace="sessions",
key="session-1",
artifact_type="recorded-session",
subject_id="session-1",
sources=(
("base-rrd", "application/vnd.rerun.rrd", first),
("overlay-rrd", "application/vnd.rerun.rrd", duplicate),
),
metadata={"display-name": "Session 1"},
)
assert len(manifest.members) == 2
assert manifest.members[0].sha256 == manifest.members[1].sha256
objects = tuple((central_root / "objects" / "sha256").glob("*/*"))
assert len(objects) == 1
cold = gateway.resolve_role("sessions", "session-1", "overlay-rrd")
assert cold.path.read_bytes() == first.read_bytes()
assert cold.cache_hit is False
assert cold.central_available is True
warm = gateway.resolve_role("sessions", "session-1", "overlay-rrd")
assert warm.path == cold.path
assert warm.cache_hit is True
def test_persistent_pin_survives_restart_and_resolves_with_central_offline(
tmp_path: Path,
) -> None:
gateway, central_root, local_root = _gateway(tmp_path)
source = tmp_path / "overlay.rrd"
source.write_bytes(b"RRF2offline-ready")
gateway.publish(
namespace="sessions",
key="session-1",
artifact_type="recorded-session",
subject_id="session-1",
sources=(("overlay-rrd", "application/vnd.rerun.rrd", source),),
)
gateway.pin_reference("sessions", "session-1", pin_id="session-1")
offline_root = tmp_path / "central-offline"
os.replace(central_root, offline_root)
restarted = ArtifactGateway(
gateway.store,
LocalArtifactCache(
local_root,
max_bytes=1024,
free_space_reserve_bytes=0,
),
)
resolved = restarted.resolve_role("sessions", "session-1", "overlay-rrd")
assert resolved.path.read_bytes() == source.read_bytes()
assert resolved.cache_hit is True
assert resolved.central_available is False
status = restarted.cache.status()
assert status.pinned_object_count == 1
assert status.pinned_bytes == len(source.read_bytes())
def test_offline_cache_miss_is_explicit(tmp_path: Path) -> None:
gateway, central_root, local_root = _gateway(tmp_path)
source = tmp_path / "overlay.rrd"
source.write_bytes(b"RRF2not-fetched")
gateway.publish(
namespace="sessions",
key="session-1",
artifact_type="recorded-session",
subject_id="session-1",
sources=(("overlay-rrd", "application/vnd.rerun.rrd", source),),
)
os.replace(central_root, tmp_path / "central-offline")
restarted = ArtifactGateway(
gateway.store,
LocalArtifactCache(local_root, max_bytes=1024, free_space_reserve_bytes=0),
)
with pytest.raises(ArtifactStoreUnavailable, match="offline cache"):
restarted.resolve_role("sessions", "session-1", "overlay-rrd")
def test_unpinned_lru_is_evicted_but_pinned_object_is_preserved(tmp_path: Path) -> None:
gateway, _central_root, _local_root = _gateway(tmp_path, max_bytes=18)
first = tmp_path / "first.bin"
second = tmp_path / "second.bin"
third = tmp_path / "third.bin"
first.write_bytes(b"a" * 9)
second.write_bytes(b"b" * 9)
third.write_bytes(b"c" * 9)
for key, source in (("first", first), ("second", second), ("third", third)):
gateway.publish(
namespace="objects",
key=key,
artifact_type="test-object",
subject_id=key,
sources=(("payload", "application/octet-stream", source),),
)
gateway.pin_reference("objects", "first", pin_id="keep-first")
second_resolved = gateway.resolve_role("objects", "second", "payload")
assert second_resolved.path.is_file()
gateway.resolve_role("objects", "third", "payload")
first_member = gateway.store.resolve_reference("objects", "first").member("payload")
second_member = gateway.store.resolve_reference("objects", "second").member("payload")
third_member = gateway.store.resolve_reference("objects", "third").member("payload")
assert gateway.cache.get(first_member) is not None
assert gateway.cache.get(second_member) is None
assert gateway.cache.get(third_member) is not None
def test_cache_refuses_object_larger_than_its_quota(tmp_path: Path) -> None:
gateway, _central_root, _local_root = _gateway(tmp_path, max_bytes=8)
source = tmp_path / "large.bin"
source.write_bytes(b"x" * 9)
gateway.publish(
namespace="objects",
key="large",
artifact_type="test-object",
subject_id="large",
sources=(("payload", "application/octet-stream", source),),
)
with pytest.raises(ArtifactCacheCapacityError, match="exceeds"):
gateway.resolve_role("objects", "large", "payload")
def test_same_size_local_corruption_is_detected_after_stat_identity_changes(
tmp_path: Path,
) -> None:
gateway, _central_root, _local_root = _gateway(tmp_path)
source = tmp_path / "payload.bin"
source.write_bytes(b"original")
gateway.publish(
namespace="objects",
key="payload",
artifact_type="test-object",
subject_id="payload",
sources=(("payload", "application/octet-stream", source),),
)
resolved = gateway.resolve_role("objects", "payload", "payload")
resolved.path.write_bytes(b"tampered")
os.utime(resolved.path, ns=(resolved.path.stat().st_atime_ns, 1))
member = gateway.store.resolve_reference("objects", "payload").member("payload")
assert gateway.cache.get(member) is None
repaired = gateway.resolve_role("objects", "payload", "payload")
assert repaired.path.read_bytes() == b"original"
def test_store_rejects_corrupt_existing_content_addressed_object(tmp_path: Path) -> None:
store = CentralArtifactStore(tmp_path / "central", create=True)
source = tmp_path / "source.bin"
source.write_bytes(b"original")
published = store.publish_file(source)
store.object_path(published.sha256).write_bytes(b"tampered")
with pytest.raises(ArtifactIntegrityError, match="corrupt"):
store.publish_file(source)