Skip to content

Commit 4e0b365

Browse files
authored
test: add unit testings for specific checkers (#225)
Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp>
1 parent 95151b2 commit 4e0b365

6 files changed

Lines changed: 839 additions & 1 deletion

File tree

t4_devkit/sanity/tier4/tiv001.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def can_skip(self, context: SanityContext) -> Maybe[Reason]:
3434
return Maybe.from_value(Reason(f"'{x.as_posix()}' not found"))
3535
return Nothing
3636
case _:
37-
return Nothing
37+
return Maybe.from_value(Reason("Data root not found"))
3838

3939
def check(self, context: SanityContext) -> list[Reason] | None:
4040
result = _load_tier4_safe(context)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from t4_devkit.sanity.context import SanityContext
8+
9+
# Import all format field type checkers
10+
from t4_devkit.sanity.format.fmt001 import FMT001
11+
from t4_devkit.sanity.format.fmt002 import FMT002
12+
from t4_devkit.sanity.format.fmt003 import FMT003
13+
from t4_devkit.sanity.format.fmt004 import FMT004
14+
from t4_devkit.sanity.format.fmt005 import FMT005
15+
from t4_devkit.sanity.format.fmt006 import FMT006
16+
from t4_devkit.sanity.format.fmt007 import FMT007
17+
from t4_devkit.sanity.format.fmt008 import FMT008
18+
from t4_devkit.sanity.format.fmt009 import FMT009
19+
from t4_devkit.sanity.format.fmt010 import FMT010
20+
from t4_devkit.sanity.format.fmt011 import FMT011
21+
from t4_devkit.sanity.format.fmt012 import FMT012
22+
from t4_devkit.sanity.format.fmt013 import FMT013
23+
from t4_devkit.sanity.format.fmt014 import FMT014 # optional (lidarseg) - missing in sample
24+
from t4_devkit.sanity.format.fmt015 import FMT015
25+
from t4_devkit.sanity.format.fmt016 import FMT016
26+
from t4_devkit.sanity.format.fmt017 import FMT017 # optional (keypoint) - missing in sample
27+
from t4_devkit.sanity.format.fmt018 import FMT018
28+
29+
# Root of the provided sample dataset (non-versioned)
30+
SAMPLE_ROOT = Path(__file__).parent.parent.joinpath("sample", "t4dataset")
31+
32+
33+
def _context() -> SanityContext:
34+
return SanityContext.from_path(SAMPLE_ROOT.as_posix())
35+
36+
37+
# All checkers expected to PASS on the sample dataset.
38+
# Optional schemas (lidarseg, keypoint) are absent but considered PASS (not skipped) by logic.
39+
ALL_FORMAT_CHECKERS: list[type] = [
40+
FMT001,
41+
FMT002,
42+
FMT003,
43+
FMT004,
44+
FMT005,
45+
FMT006,
46+
FMT007,
47+
FMT008,
48+
FMT009,
49+
FMT010,
50+
FMT011,
51+
FMT012,
52+
FMT013,
53+
FMT014,
54+
FMT015,
55+
FMT016,
56+
FMT017,
57+
FMT018,
58+
]
59+
60+
61+
@pytest.mark.parametrize("checker_cls", ALL_FORMAT_CHECKERS)
62+
def test_format_checker_passes(checker_cls: type) -> None:
63+
"""
64+
Each format field type checker should pass against the sample dataset.
65+
66+
Behavior expectations:
67+
- Mandatory schema files exist and records convert cleanly => PASSED (no reasons).
68+
- Optional schema files (missing) => treated as PASSED (no reasons).
69+
"""
70+
context = _context()
71+
checker = checker_cls()
72+
report = checker(context)
73+
# Must be passed strictly (no failures or warnings with reasons)
74+
assert report.is_passed(strict=True), f"{checker_cls.__name__} expected to pass"
75+
# For passed reports reasons must be None
76+
assert report.reasons is None, f"{checker_cls.__name__} should not produce reasons when passing"
77+
78+
79+
def test_optional_missing_schemas_present_in_param_list() -> None:
80+
"""Sanity guard: ensure we explicitly covered optional missing schemas."""
81+
optional_missing = {"FMT014": FMT014, "FMT017": FMT017}
82+
for name, cls in optional_missing.items():
83+
report = cls()(_context())
84+
assert report.is_passed(strict=True), f"{name} (missing optional file) should pass"
85+
assert report.reasons is None
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import shutil
5+
from pathlib import Path
6+
7+
import pytest
8+
9+
from t4_devkit.sanity.context import SanityContext
10+
from t4_devkit.sanity.record.rec001 import REC001
11+
from t4_devkit.sanity.record.rec002 import REC002
12+
from t4_devkit.sanity.record.rec003 import REC003
13+
from t4_devkit.sanity.record.rec004 import REC004
14+
from t4_devkit.sanity.record.rec005 import REC005
15+
from t4_devkit.sanity.record.rec006 import REC006
16+
17+
# Base sample dataset root (contains all mandatory annotation json files with non-empty records)
18+
SAMPLE_ROOT = Path(__file__).parent.parent.joinpath("sample", "t4dataset")
19+
ANNOTATION_DIR = SAMPLE_ROOT / "annotation"
20+
21+
22+
def _copy_dataset(dst_root: Path) -> Path:
23+
"""Copy the entire sample dataset tree into a destination root."""
24+
shutil.copytree(SAMPLE_ROOT, dst_root)
25+
return dst_root
26+
27+
28+
def _make_mutated_dataset(tmp_path: Path, mutations: dict[str, list[dict]]) -> Path:
29+
"""
30+
Create a dataset root copying the sample dataset and then apply record mutations.
31+
32+
mutations: mapping of filename (e.g. 'scene.json') to new list[dict] content.
33+
"""
34+
dst_root = _copy_dataset(tmp_path / "mutated_dataset")
35+
ann_dir = dst_root / "annotation"
36+
for filename, new_records in mutations.items():
37+
target = ann_dir / filename
38+
with target.open("w", encoding="utf-8") as f:
39+
json.dump(new_records, f, indent=2)
40+
return dst_root
41+
42+
43+
def _context(root: Path) -> SanityContext:
44+
return SanityContext.from_path(root.as_posix())
45+
46+
47+
# ---------------- REC001 (scene-single) ----------------
48+
49+
50+
def test_rec001_pass_single_scene() -> None:
51+
checker = REC001()
52+
report = checker(_context(SAMPLE_ROOT))
53+
assert report.is_passed(strict=True)
54+
assert report.reasons is None
55+
56+
57+
@pytest.mark.parametrize("num_scenes", [0, 2])
58+
def test_rec001_fail_scene_count(num_scenes: int, tmp_path: Path) -> None:
59+
original = json.loads((ANNOTATION_DIR / "scene.json").read_text(encoding="utf-8"))
60+
if num_scenes == 0:
61+
mutated = []
62+
else:
63+
# duplicate original first record to create 2 scenes (different token for uniqueness)
64+
first = original[0]
65+
duplicate = {**first, "token": "duplicate_scene_token"}
66+
mutated = [first, duplicate]
67+
root = _make_mutated_dataset(tmp_path, {"scene.json": mutated})
68+
checker = REC001()
69+
report = checker(_context(root))
70+
assert not report.is_passed(strict=True)
71+
assert report.reasons and report.reasons[0].startswith(
72+
"'Scene' must contain exactly one element"
73+
)
74+
75+
76+
# ---------------- REC002 (sample-not-empty) ----------------
77+
78+
79+
def test_rec002_pass_sample_not_empty() -> None:
80+
checker = REC002()
81+
report = checker(_context(SAMPLE_ROOT))
82+
assert report.is_passed(strict=True)
83+
assert report.reasons is None
84+
85+
86+
def test_rec002_fail_sample_empty(tmp_path: Path) -> None:
87+
root = _make_mutated_dataset(tmp_path, {"sample.json": []})
88+
checker = REC002()
89+
report = checker(_context(root))
90+
assert not report.is_passed(strict=True)
91+
assert report.reasons and report.reasons[0] == "'Sample' record must not be empty"
92+
93+
94+
# ---------------- REC003 (sample-data-not-empty) ----------------
95+
96+
97+
def test_rec003_pass_sample_data_not_empty() -> None:
98+
checker = REC003()
99+
report = checker(_context(SAMPLE_ROOT))
100+
assert report.is_passed(strict=True)
101+
assert report.reasons is None
102+
103+
104+
def test_rec003_fail_sample_data_empty(tmp_path: Path) -> None:
105+
root = _make_mutated_dataset(tmp_path, {"sample_data.json": []})
106+
checker = REC003()
107+
report = checker(_context(root))
108+
assert not report.is_passed(strict=True)
109+
assert report.reasons and report.reasons[0] == "'SampleData' record must not be empty"
110+
111+
112+
# ---------------- REC004 (ego-pose-not-empty) ----------------
113+
114+
115+
def test_rec004_pass_ego_pose_not_empty() -> None:
116+
checker = REC004()
117+
report = checker(_context(SAMPLE_ROOT))
118+
assert report.is_passed(strict=True)
119+
assert report.reasons is None
120+
121+
122+
def test_rec004_fail_ego_pose_empty(tmp_path: Path) -> None:
123+
root = _make_mutated_dataset(tmp_path, {"ego_pose.json": []})
124+
checker = REC004()
125+
report = checker(_context(root))
126+
assert not report.is_passed(strict=True)
127+
assert report.reasons and report.reasons[0] == "'EgoPose' record must not be empty"
128+
129+
130+
# ---------------- REC005 (calibrated-sensor-not-empty) ----------------
131+
132+
133+
def test_rec005_pass_calibrated_sensor_not_empty() -> None:
134+
checker = REC005()
135+
report = checker(_context(SAMPLE_ROOT))
136+
assert report.is_passed(strict=True)
137+
assert report.reasons is None
138+
139+
140+
def test_rec005_fail_calibrated_sensor_empty(tmp_path: Path) -> None:
141+
root = _make_mutated_dataset(tmp_path, {"calibrated_sensor.json": []})
142+
checker = REC005()
143+
report = checker(_context(root))
144+
assert not report.is_passed(strict=True)
145+
assert report.reasons and report.reasons[0] == "'CalibratedSensor' record must not be empty"
146+
147+
148+
# ---------------- REC006 (instance-not-empty) ----------------
149+
150+
151+
def test_rec006_pass_instance_not_empty() -> None:
152+
checker = REC006()
153+
report = checker(_context(SAMPLE_ROOT))
154+
assert report.is_passed(strict=True)
155+
assert report.reasons is None
156+
157+
158+
def test_rec006_fail_instance_empty(tmp_path: Path) -> None:
159+
root = _make_mutated_dataset(tmp_path, {"instance.json": []})
160+
checker = REC006()
161+
report = checker(_context(root))
162+
assert not report.is_passed(strict=True)
163+
assert report.reasons and report.reasons[0] == "'Instance' record must not be empty"

0 commit comments

Comments
 (0)