Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion t4_devkit/sanity/tier4/tiv001.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def can_skip(self, context: SanityContext) -> Maybe[Reason]:
return Maybe.from_value(Reason(f"'{x.as_posix()}' not found"))
return Nothing
case _:
return Nothing
return Maybe.from_value(Reason("Data root not found"))

def check(self, context: SanityContext) -> list[Reason] | None:
result = _load_tier4_safe(context)
Expand Down
85 changes: 85 additions & 0 deletions tests/sanity/test_format_checkers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from __future__ import annotations

from pathlib import Path

import pytest

from t4_devkit.sanity.context import SanityContext

# Import all format field type checkers
from t4_devkit.sanity.format.fmt001 import FMT001
from t4_devkit.sanity.format.fmt002 import FMT002
from t4_devkit.sanity.format.fmt003 import FMT003
from t4_devkit.sanity.format.fmt004 import FMT004
from t4_devkit.sanity.format.fmt005 import FMT005
from t4_devkit.sanity.format.fmt006 import FMT006
from t4_devkit.sanity.format.fmt007 import FMT007
from t4_devkit.sanity.format.fmt008 import FMT008
from t4_devkit.sanity.format.fmt009 import FMT009
from t4_devkit.sanity.format.fmt010 import FMT010
from t4_devkit.sanity.format.fmt011 import FMT011
from t4_devkit.sanity.format.fmt012 import FMT012
from t4_devkit.sanity.format.fmt013 import FMT013
from t4_devkit.sanity.format.fmt014 import FMT014 # optional (lidarseg) - missing in sample
from t4_devkit.sanity.format.fmt015 import FMT015
from t4_devkit.sanity.format.fmt016 import FMT016
from t4_devkit.sanity.format.fmt017 import FMT017 # optional (keypoint) - missing in sample
from t4_devkit.sanity.format.fmt018 import FMT018

# Root of the provided sample dataset (non-versioned)
SAMPLE_ROOT = Path(__file__).parent.parent.joinpath("sample", "t4dataset")


def _context() -> SanityContext:
return SanityContext.from_path(SAMPLE_ROOT.as_posix())


# All checkers expected to PASS on the sample dataset.
# Optional schemas (lidarseg, keypoint) are absent but considered PASS (not skipped) by logic.
ALL_FORMAT_CHECKERS: list[type] = [
FMT001,
FMT002,
FMT003,
FMT004,
FMT005,
FMT006,
FMT007,
FMT008,
FMT009,
FMT010,
FMT011,
FMT012,
FMT013,
FMT014,
FMT015,
FMT016,
FMT017,
FMT018,
]


@pytest.mark.parametrize("checker_cls", ALL_FORMAT_CHECKERS)
def test_format_checker_passes(checker_cls: type) -> None:
"""
Each format field type checker should pass against the sample dataset.

Behavior expectations:
- Mandatory schema files exist and records convert cleanly => PASSED (no reasons).
- Optional schema files (missing) => treated as PASSED (no reasons).
"""
context = _context()
checker = checker_cls()
report = checker(context)
# Must be passed strictly (no failures or warnings with reasons)
assert report.is_passed(strict=True), f"{checker_cls.__name__} expected to pass"
# For passed reports reasons must be None
assert report.reasons is None, f"{checker_cls.__name__} should not produce reasons when passing"


def test_optional_missing_schemas_present_in_param_list() -> None:
"""Sanity guard: ensure we explicitly covered optional missing schemas."""
optional_missing = {"FMT014": FMT014, "FMT017": FMT017}
for name, cls in optional_missing.items():
report = cls()(_context())
assert report.is_passed(strict=True), f"{name} (missing optional file) should pass"
assert report.reasons is None
163 changes: 163 additions & 0 deletions tests/sanity/test_record_checkers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from __future__ import annotations

import json
import shutil
from pathlib import Path

import pytest

from t4_devkit.sanity.context import SanityContext
from t4_devkit.sanity.record.rec001 import REC001
from t4_devkit.sanity.record.rec002 import REC002
from t4_devkit.sanity.record.rec003 import REC003
from t4_devkit.sanity.record.rec004 import REC004
from t4_devkit.sanity.record.rec005 import REC005
from t4_devkit.sanity.record.rec006 import REC006

# Base sample dataset root (contains all mandatory annotation json files with non-empty records)
SAMPLE_ROOT = Path(__file__).parent.parent.joinpath("sample", "t4dataset")
ANNOTATION_DIR = SAMPLE_ROOT / "annotation"


def _copy_dataset(dst_root: Path) -> Path:
"""Copy the entire sample dataset tree into a destination root."""
shutil.copytree(SAMPLE_ROOT, dst_root)
return dst_root


def _make_mutated_dataset(tmp_path: Path, mutations: dict[str, list[dict]]) -> Path:
"""
Create a dataset root copying the sample dataset and then apply record mutations.

mutations: mapping of filename (e.g. 'scene.json') to new list[dict] content.
"""
dst_root = _copy_dataset(tmp_path / "mutated_dataset")
ann_dir = dst_root / "annotation"
for filename, new_records in mutations.items():
target = ann_dir / filename
with target.open("w", encoding="utf-8") as f:
json.dump(new_records, f, indent=2)
return dst_root


def _context(root: Path) -> SanityContext:
return SanityContext.from_path(root.as_posix())


# ---------------- REC001 (scene-single) ----------------


def test_rec001_pass_single_scene() -> None:
checker = REC001()
report = checker(_context(SAMPLE_ROOT))
assert report.is_passed(strict=True)
assert report.reasons is None


@pytest.mark.parametrize("num_scenes", [0, 2])
def test_rec001_fail_scene_count(num_scenes: int, tmp_path: Path) -> None:
original = json.loads((ANNOTATION_DIR / "scene.json").read_text(encoding="utf-8"))
if num_scenes == 0:
mutated = []
else:
# duplicate original first record to create 2 scenes (different token for uniqueness)
first = original[0]
duplicate = {**first, "token": "duplicate_scene_token"}
mutated = [first, duplicate]
root = _make_mutated_dataset(tmp_path, {"scene.json": mutated})
checker = REC001()
report = checker(_context(root))
assert not report.is_passed(strict=True)
assert report.reasons and report.reasons[0].startswith(
"'Scene' must contain exactly one element"
)


# ---------------- REC002 (sample-not-empty) ----------------


def test_rec002_pass_sample_not_empty() -> None:
checker = REC002()
report = checker(_context(SAMPLE_ROOT))
assert report.is_passed(strict=True)
assert report.reasons is None


def test_rec002_fail_sample_empty(tmp_path: Path) -> None:
root = _make_mutated_dataset(tmp_path, {"sample.json": []})
checker = REC002()
report = checker(_context(root))
assert not report.is_passed(strict=True)
assert report.reasons and report.reasons[0] == "'Sample' record must not be empty"


# ---------------- REC003 (sample-data-not-empty) ----------------


def test_rec003_pass_sample_data_not_empty() -> None:
checker = REC003()
report = checker(_context(SAMPLE_ROOT))
assert report.is_passed(strict=True)
assert report.reasons is None


def test_rec003_fail_sample_data_empty(tmp_path: Path) -> None:
root = _make_mutated_dataset(tmp_path, {"sample_data.json": []})
checker = REC003()
report = checker(_context(root))
assert not report.is_passed(strict=True)
assert report.reasons and report.reasons[0] == "'SampleData' record must not be empty"


# ---------------- REC004 (ego-pose-not-empty) ----------------


def test_rec004_pass_ego_pose_not_empty() -> None:
checker = REC004()
report = checker(_context(SAMPLE_ROOT))
assert report.is_passed(strict=True)
assert report.reasons is None


def test_rec004_fail_ego_pose_empty(tmp_path: Path) -> None:
root = _make_mutated_dataset(tmp_path, {"ego_pose.json": []})
checker = REC004()
report = checker(_context(root))
assert not report.is_passed(strict=True)
assert report.reasons and report.reasons[0] == "'EgoPose' record must not be empty"


# ---------------- REC005 (calibrated-sensor-not-empty) ----------------


def test_rec005_pass_calibrated_sensor_not_empty() -> None:
checker = REC005()
report = checker(_context(SAMPLE_ROOT))
assert report.is_passed(strict=True)
assert report.reasons is None


def test_rec005_fail_calibrated_sensor_empty(tmp_path: Path) -> None:
root = _make_mutated_dataset(tmp_path, {"calibrated_sensor.json": []})
checker = REC005()
report = checker(_context(root))
assert not report.is_passed(strict=True)
assert report.reasons and report.reasons[0] == "'CalibratedSensor' record must not be empty"


# ---------------- REC006 (instance-not-empty) ----------------


def test_rec006_pass_instance_not_empty() -> None:
checker = REC006()
report = checker(_context(SAMPLE_ROOT))
assert report.is_passed(strict=True)
assert report.reasons is None


def test_rec006_fail_instance_empty(tmp_path: Path) -> None:
root = _make_mutated_dataset(tmp_path, {"instance.json": []})
checker = REC006()
report = checker(_context(root))
assert not report.is_passed(strict=True)
assert report.reasons and report.reasons[0] == "'Instance' record must not be empty"
Loading
Loading