"""Tests unitaires pour pipeline.checkboxes.""" from __future__ import annotations import numpy as np import pytest from PIL import Image from pipeline.checkboxes import ( AMBIGU_MARGIN, CheckboxZones, RECUEIL_ACCORD_DESACCORD, dark_ratio, detect_accord_desaccord, parse_ghs_injustifie, ) # ============================================================ # parse_ghs_injustifie # ============================================================ class TestParseGhsInjustifie: @pytest.mark.parametrize("raw,expected", [ ("0", "0"), ("1", "1"), ("0 SE 1 2 3 4 ATU FFM FSD", "0"), ("1 SE 2 ATU", "1"), (" 0 ", "0"), ("", ""), (None, ""), ("SE 1 2 3 4 ATU FFM FSD", ""), # pas de chiffre de tête ("abc", ""), ("2 SE 1", ""), # 2 n'est ni 0 ni 1 ]) def test_cas_varies(self, raw, expected): assert parse_ghs_injustifie(raw) == expected # ============================================================ # dark_ratio (avec images synthétiques) # ============================================================ def _solid_image(w: int, h: int, gray_value: int = 255) -> Image.Image: arr = np.full((h, w), gray_value, dtype=np.uint8) return Image.fromarray(arr, mode="L").convert("RGB") def _image_with_dark_square(w: int, h: int, square_bbox: tuple[float, float, float, float]) -> Image.Image: """Image blanche avec un carré noir dans la zone bbox (coords relatives).""" arr = np.full((h, w), 255, dtype=np.uint8) x1, y1, x2, y2 = square_bbox arr[int(y1*h):int(y2*h), int(x1*w):int(x2*w)] = 0 return Image.fromarray(arr, mode="L").convert("RGB") class TestDarkRatio: def test_image_blanche(self): img = _solid_image(100, 100, 255) ratio = dark_ratio(img, (0.2, 0.2, 0.8, 0.8)) assert ratio == 0.0 def test_image_noire(self): img = _solid_image(100, 100, 0) ratio = dark_ratio(img, (0.2, 0.2, 0.8, 0.8)) assert ratio == 1.0 def test_inner_frac_ignore_les_bords(self): """Un carré noir occupe toute la zone mais avec un grand inner_frac on ne voit que le centre, qui reste dans la zone noire.""" img = _image_with_dark_square(100, 100, (0.0, 0.0, 1.0, 1.0)) # Tout noir, peu importe inner_frac assert dark_ratio(img, (0.0, 0.0, 1.0, 1.0), inner_frac=0.35) == 1.0 def test_cadre_seul_vs_contenu_central(self): """Une case 'vide' (cadre seul) doit avoir un ratio inner_frac faible ; une case 'cochée' (croix au centre) doit avoir un ratio plus élevé.""" # Simuler un cadre : carré noir sur le pourtour uniquement w, h = 100, 100 arr = np.full((h, w), 255, dtype=np.uint8) arr[:5, :] = 0; arr[-5:, :] = 0; arr[:, :5] = 0; arr[:, -5:] = 0 frame_only = Image.fromarray(arr, mode="L").convert("RGB") # Cadre + croix au centre arr2 = arr.copy() # Une croix : 2 diagonales for i in range(20, 80): arr2[i, i] = 0 arr2[i, 100 - 1 - i] = 0 checked = Image.fromarray(arr2, mode="L").convert("RGB") ratio_empty = dark_ratio(frame_only, (0.0, 0.0, 1.0, 1.0), inner_frac=0.35) ratio_full = dark_ratio(checked, (0.0, 0.0, 1.0, 1.0), inner_frac=0.35) # La case cochée doit avoir un ratio clairement plus élevé assert ratio_full > ratio_empty + 0.05 # ============================================================ # detect_accord_desaccord (fixtures cache) # ============================================================ class TestDetectAccordDesaccord: """Tests sur les images réelles du cache, avec ground truth vérifié visuellement (cf. historique du projet, crops audités un par un). Ground truth indexé par numéro d'OGC — le mapping vers le hash du cache est résolu au runtime via pipeline.ingest.pdf_hash pour éviter de coder les hashes en dur (fragile). """ # Ground truth vérifié visuellement sur les 18 dossiers 2018 CARC GROUND_TRUTH_BY_OGC = { 1: "accord", 7: "accord", 9: "désaccord", 18: "désaccord", 20: "désaccord", 27: "désaccord", 29: "accord", 55: "accord", 66: "désaccord", 68: "accord", 69: "accord", 74: "désaccord", 76: "désaccord", 84: "accord", 86: "désaccord", 97: "accord", 99: "désaccord", } @pytest.fixture def cached_pages_with_truth(self): """Résout le mapping numéro OGC → page_01.png disponible au runtime.""" from pathlib import Path from pipeline.ingest import pdf_hash pdf_dir = Path("2018 CARC") if not pdf_dir.is_dir(): pytest.skip("répertoire 2018 CARC/ absent") found = {} for n, expected in self.GROUND_TRUTH_BY_OGC.items(): pdf = pdf_dir / f"OGC {n}.pdf" if not pdf.exists(): continue h = pdf_hash(str(pdf)) img = Path(f".cache/images/{h}/page_01.png") if img.exists(): found[f"OGC {n}"] = (str(img), expected) if not found: pytest.skip("pas de cache d'images disponible — lance le pipeline d'abord") return found def test_ground_truth_echantillon(self, cached_pages_with_truth): """Sur les cas vérifiés visuellement, le détecteur doit matcher.""" errors = [] for name, (path, expected) in cached_pages_with_truth.items(): r = detect_accord_desaccord(path) if r["decision"] != expected: errors.append(f"{name}: attendu={expected}, got={r}") assert not errors, "\n".join(errors)