diff --git a/docs/schema/requirement.md b/docs/schema/requirement.md index c29ddfa..0717efe 100644 --- a/docs/schema/requirement.md +++ b/docs/schema/requirement.md @@ -16,14 +16,14 @@ ## Schema Record (`REC`) -| ID | Name | Severity | Description | -| -------- | ----------------------------- | -------- | --------------------------------------- | -| `REC001` | `scene-single` | `ERROR` | `Scene` record is a single. | -| `REC002` | `sample-not-empty` | `ERROR` | `Sample` record is not empty. | -| `REC003` | `sample-data-not-empty` | `ERROR` | `SampleData` record is not empty. | -| `REC004` | `ego-pose-not-empty` | `ERROR` | `EgoPose` record is not empty. | -| `REC005` | `calibrated-sensor-non-empty` | `ERROR` | `CalibratedSensor` record is not empty. | -| `REC006` | `instance-not-empty` | `ERROR` | `Instance` record is not empty. | +| ID | Name | Severity | Description | +| -------- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------- | +| `REC001` | `scene-single` | `ERROR` | `Scene` record is a single. | +| `REC002` | `sample-not-empty` | `ERROR` | `Sample` record is not empty. | +| `REC003` | `sample-data-not-empty` | `ERROR` | `SampleData` record is not empty. | +| `REC004` | `ego-pose-not-empty` | `ERROR` | `EgoPose` record is not empty. | +| `REC005` | `calibrated-sensor-non-empty` | `ERROR` | `CalibratedSensor` record is not empty. | +| `REC006` | `instance-not-empty` | `ERROR` | `Instance` record is not empty if either 'SampleAnnotation' or 'ObjectAnn' is not empty. | ## Reference (`REF`) diff --git a/t4_devkit/sanity/record/rec006.py b/t4_devkit/sanity/record/rec006.py index 0edee0c..1ed9ecc 100644 --- a/t4_devkit/sanity/record/rec006.py +++ b/t4_devkit/sanity/record/rec006.py @@ -1,12 +1,20 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +from returns.maybe import Maybe, Nothing, Some + from t4_devkit.schema import SchemaName from ..checker import RuleID, RuleName, Severity from ..registry import CHECKERS from ..result import Reason +from ..safety import load_json_safe from .base import RecordCountChecker +if TYPE_CHECKING: + from ..context import SanityContext + __all__ = ["REC006"] @@ -17,9 +25,39 @@ class REC006(RecordCountChecker): id = RuleID("REC006") name = RuleName("instance-not-empty") severity = Severity.ERROR - description = "'Instance' record is not empty." + description = ( + "'Instance' record is not empty if either 'SampleAnnotation' or 'ObjectAnn' is not empty." + ) schema = SchemaName.INSTANCE + def can_skip(self, context: SanityContext) -> Maybe[Reason]: + # return skip reason if instance.json does not exist + match super().can_skip(context): + case Some(x): + return Maybe.from_value(x) + + # instance.json should contain any records if either + # SampleAnnotation or ObjectAnn records exist + sample_ann_file = context.to_schema_file(SchemaName.SAMPLE_ANNOTATION) + object_ann_file = context.to_schema_file(SchemaName.OBJECT_ANN) + + match (sample_ann_file.value_or(None), object_ann_file.value_or(None)): + case (None, _) | (_, None): + return Maybe.from_value(Reason("Missing 'annotation' directory")) + case (s, o): + sample_ann_count = len(load_json_safe(s).unwrap()) if s.exists() else 0 + object_ann_count = len(load_json_safe(o).unwrap()) if o.exists() else 0 + return ( + Maybe.from_value( + Reason( + f"Both {SchemaName.SAMPLE_ANNOTATION} " + f"and {SchemaName.OBJECT_ANN} records are empty" + ) + ) + if sample_ann_count == 0 and object_ann_count == 0 + else Nothing + ) + def check_count(self, records: list[dict]) -> list[Reason] | None: num_instance = len(records) return [Reason("'Instance' record must not be empty")] if num_instance == 0 else None diff --git a/tests/sanity/test_record_checkers.py b/tests/sanity/test_record_checkers.py index 5c3710f..9ada4b8 100644 --- a/tests/sanity/test_record_checkers.py +++ b/tests/sanity/test_record_checkers.py @@ -13,6 +13,7 @@ from t4_devkit.sanity.record.rec004 import REC004 from t4_devkit.sanity.record.rec005 import REC005 from t4_devkit.sanity.record.rec006 import REC006 +from t4_devkit.schema.name import SchemaName # Base sample dataset root (contains all mandatory annotation json files with non-empty records) SAMPLE_ROOT = Path(__file__).parent.parent.joinpath("sample", "t4dataset") @@ -155,6 +156,18 @@ def test_rec006_pass_instance_not_empty() -> None: assert report.reasons is None +def test_rec006_skip_annotation_empty(tmp_path: Path) -> None: + root = _make_mutated_dataset(tmp_path, {"sample_annotation.json": [], "object_ann.json": []}) + checker = REC006() + report = checker(_context(root)) + assert report.is_passed(strict=True) + assert ( + report.reasons + and report.reasons[0] + == f"Both {SchemaName.SAMPLE_ANNOTATION} and {SchemaName.OBJECT_ANN} records are empty" + ) + + def test_rec006_fail_instance_empty(tmp_path: Path) -> None: root = _make_mutated_dataset(tmp_path, {"instance.json": []}) checker = REC006()