"""Unit tests for src.analysis.validation_engine.""" from __future__ import annotations import copy from src.analysis.validation_engine import ValidationEngine, summarize from src.config.schema import DifficultyWeights, ValidationConfig VALID_METADATA = { "document_id": "sample_000001", "template": "bank_doc_01", "page": 1, "page_size": [1000, 800], "signer": {"name": "Jane Doe", "role": "Account Holder"}, "signature_source": {"signature_id": "sig_0001", "path": "dataset/genuine/x.png"}, "placement": {"rotation_deg": 0.0}, "rendering": {"blend_mode": "multiply"}, "difficulty": { "misplacement": 0.6, "whitespace": 0.5, "faintness": 0.5, "smallness": 0.5, "low_texture": 0.5, "score": round(0.35 * 0.6 + 0.20 * 0.5 + 0.20 * 0.5 + 0.15 * 0.5 + 0.10 * 0.5, 4), "tier": "medium", }, "quality": {"degraded": False}, "verification": { "case_type": None, "verdict": None, "reject_code": None, "target_box_id": None, "expected_signer_group": None, "expected_signer_script": None, "actual_signer_group": None, "actual_source_type": None, "actual_signer_script": None, "companion": None, }, "augmentation": {}, "geometry": { "detected": True, "ink_pixels": 120, "target_box": [100, 100, 200, 150], "actual_box": [110, 105, 190, 145], "signature_bbox_px": [110, 105, 190, 145], "box_px": [100, 100, 200, 150], "signature_polygon_px": [[110, 105], [190, 105], [190, 145], [110, 145]], }, "metrics": {"box_coverage_pct": 80.0}, } def _engine(strict: bool = False) -> ValidationEngine: return ValidationEngine(ValidationConfig(strict=strict), DifficultyWeights()) def test_valid_metadata_passes(): report = _engine().validate(copy.deepcopy(VALID_METADATA)) assert report.passed, [i.message for i in report.issues] assert not report.errors def test_missing_top_level_key_fails(): bad = copy.deepcopy(VALID_METADATA) del bad["geometry"] report = _engine().validate(bad) assert not report.passed assert any("geometry" in i.field for i in report.errors) def test_signature_not_detected_fails(): bad = copy.deepcopy(VALID_METADATA) bad["geometry"]["detected"] = False report = _engine().validate(bad) assert not report.passed def test_degenerate_polygon_fails(): bad = copy.deepcopy(VALID_METADATA) bad["geometry"]["signature_polygon_px"] = [[0, 0], [1, 1]] report = _engine().validate(bad) assert not report.passed def test_invalid_box_ordering_fails(): bad = copy.deepcopy(VALID_METADATA) bad["geometry"]["target_box"] = [200, 100, 100, 150] # x2 < x1 report = _engine().validate(bad) assert not report.passed def test_box_outside_page_is_warning_not_error_by_default(): bad = copy.deepcopy(VALID_METADATA) bad["geometry"]["target_box"] = [900, 700, 1200, 900] # exceeds page_size [1000, 800] report = _engine(strict=False).validate(bad) assert report.passed # warning only, not strict assert report.warnings report_strict = _engine(strict=True).validate(bad) assert not report_strict.passed def test_nan_value_fails(): bad = copy.deepcopy(VALID_METADATA) bad["metrics"]["box_coverage_pct"] = float("nan") report = _engine().validate(bad) assert not report.passed def test_difficulty_score_mismatch_fails(): bad = copy.deepcopy(VALID_METADATA) bad["difficulty"]["score"] = 0.999 # inconsistent with components report = _engine().validate(bad) assert not report.passed def test_difficulty_tier_mismatch_fails(): bad = copy.deepcopy(VALID_METADATA) bad["difficulty"]["tier"] = "hard" # inconsistent with score ~0.49 -> medium report = _engine().validate(bad) assert not report.passed def test_summarize_batch(): good = _engine().validate(copy.deepcopy(VALID_METADATA)) bad_meta = copy.deepcopy(VALID_METADATA) bad_meta["geometry"]["detected"] = False bad = _engine().validate(bad_meta) summary = summarize([good, bad]) assert summary.total == 2 assert summary.passed == 1 assert summary.failed == 1 assert "sample_000001" in summary.failed_sample_ids