Source: tests/test_episode_access.py · Plain text

tests/test_episode_access.py

"""Regression tests for the online-only episode reader and compiler."""

from __future__ import annotations

import importlib.util
import json
import tempfile
from pathlib import Path

import pytest
import torch
from PIL import Image

from mcss.data.episodes import OnlineEpisodeSource

PROJECT = Path(__file__).resolve().parents[1]
_COMPILER_SPEC = importlib.util.spec_from_file_location(
    "compile_dynamic_episodes", PROJECT / "scripts/compile_dynamic_episodes.py"
)
assert _COMPILER_SPEC is not None and _COMPILER_SPEC.loader is not None
compiler = importlib.util.module_from_spec(_COMPILER_SPEC)
_COMPILER_SPEC.loader.exec_module(compiler)


@pytest.fixture
def artifact_dir() -> Path:
    output_root = PROJECT / "outputs"
    output_root.mkdir(exist_ok=True)
    with tempfile.TemporaryDirectory(prefix="test_episode_access_", dir=output_root) as directory:
        yield Path(directory)


def test_compiler_strips_labels_and_source_reads_only_arrived_rgb(artifact_dir: Path) -> None:
    _write_rgb(artifact_dir / "warmup.png", 16)
    _write_rgb(artifact_dir / "stream_1.png", 32)
    pilot_path = _write_pilot(artifact_dir)

    compiled_dir = artifact_dir / "compiled"
    index = compiler.compile_pilot_manifest(pilot_path, compiled_dir)
    online_path = Path(index[0]["online_manifest"])
    online = json.loads(online_path.read_text(encoding="utf-8"))
    vault = json.loads(Path(index[0]["query_manifest"]).read_text(encoding="utf-8"))

    assert not _contains_key(online, "depth")
    assert not _contains_key(online, "normal")
    assert not _contains_key(online, "query")
    assert vault["query"][0]["depth"].endswith("3.depth.npy")

    source = OnlineEpisodeSource.from_file(online_path)
    warmup = source.warmup()
    assert [observation.frame_id for observation in warmup] == [0]
    assert torch.equal(warmup[0].camera.c2w, torch.eye(4))
    assert source.remaining_steps == 2

    first_stream = source.next_observation()
    assert first_stream.frame_id == 1
    assert source.remaining_steps == 1

    # The final stream image is deliberately absent: no future RGB was touched earlier.
    with pytest.raises(FileNotFoundError):
        source.next_observation()


def test_online_reader_rejects_query_or_label_fields(artifact_dir: Path) -> None:
    _write_rgb(artifact_dir / "warmup.png", 16)
    _write_rgb(artifact_dir / "stream_1.png", 32)
    pilot_path = _write_pilot(artifact_dir)
    compiled_dir = artifact_dir / "compiled"
    index = compiler.compile_pilot_manifest(pilot_path, compiled_dir)
    online_path = Path(index[0]["online_manifest"])
    online = json.loads(online_path.read_text(encoding="utf-8"))
    online["warmup"][0]["depth"] = "poison.depth.npy"
    online_path = artifact_dir / "poisoned_online.json"
    online_path.write_text(json.dumps(online), encoding="utf-8")

    with pytest.raises(ValueError, match="fields mismatch"):
        OnlineEpisodeSource.from_file(online_path)


def test_compiler_rejects_test_split_without_opening_references(artifact_dir: Path) -> None:
    pilot_path = _write_pilot(artifact_dir, split="test")

    with pytest.raises(ValueError, match="refuses split"):
        compiler.compile_pilot_manifest(pilot_path, artifact_dir / "compiled")


def _write_pilot(directory: Path, *, split: str = "dev") -> Path:
    records = {
        "warmup": [_record(directory, 0, "warmup.png")],
        "stream": [
            _record(directory, 1, "stream_1.png"),
            _record(directory, 2, "future_stream.png"),
        ],
        "query": [_record(directory, 3, "query.png")],
    }
    pilot = {
        "schema": "mcss.dynamic_ttt.pilot_plan.v1",
        "final_holdout_used": False,
        "diagnostic_test_used": False,
        "episodes": [
            {
                "scene_id": "scene_a",
                "split": split,
                "camera": "cam_00",
                "image_size": [2, 2],
                **records,
            }
        ],
    }
    path = directory / "pilot.json"
    path.write_text(json.dumps(pilot), encoding="utf-8")
    return path


def _record(directory: Path, frame_id: int, rgb_name: str) -> dict[str, object]:
    return {
        "frame_id": frame_id,
        "intrinsics": [[2.0, 0.0, 0.5], [0.0, 2.0, 0.5], [0.0, 0.0, 1.0]],
        "c2w": torch.eye(4).tolist(),
        "rgb": str(directory / rgb_name),
        "depth": str(directory / f"{frame_id}.depth.npy"),
        "normal": str(directory / f"{frame_id}.normal.npy"),
    }


def _write_rgb(path: Path, value: int) -> None:
    Image.new("RGB", (2, 2), (value, value, value)).save(path)


def _contains_key(value: object, expected: str) -> bool:
    if isinstance(value, dict):
        return expected in value or any(_contains_key(item, expected) for item in value.values())
    if isinstance(value, list):
        return any(_contains_key(item, expected) for item in value)
    return False