"""Sprint A14-S1 — A.I.0 P0 : ``normalization_profile`` propagé end-to-end. Avant ce sprint, le paramètre ``normalization_profile`` était : - exposé par l'API web (``BenchmarkRequest`` / ``BenchmarkRunRequest``) ; - transporté jusqu'à ``benchmark_utils.run_benchmark_thread*`` ; - **silencieusement ignoré** : jamais transmis à ``run_benchmark`` ; - ``run_benchmark`` n'avait même pas le paramètre dans sa signature. Conséquence : tout benchmark lancé depuis l'API web utilisait le profil par défaut (``medieval_french``) quel que soit le choix utilisateur. L'option de l'UI était un faux bouton. Ce module verrouille la propagation depuis la signature publique de ``run_benchmark`` jusqu'à ``compute_metrics`` via les workers. """ from __future__ import annotations import inspect from picarones.measurements.normalization import ( NORMALIZATION_PROFILES, get_builtin_profile, ) from picarones.measurements.runner import run_benchmark from picarones.measurements.runner.document import _compute_document_result from picarones.measurements.runner.workers import ( _cpu_doc_worker, _io_doc_worker, ) class TestRunBenchmarkSignature: def test_run_benchmark_accepts_normalization_profile(self) -> None: """La signature publique doit exposer ``normalization_profile``.""" sig = inspect.signature(run_benchmark) assert "normalization_profile" in sig.parameters # Et avec une valeur par défaut sûre. assert sig.parameters["normalization_profile"].default is None def test_io_worker_accepts_normalization_profile(self) -> None: sig = inspect.signature(_io_doc_worker) assert "normalization_profile" in sig.parameters def test_compute_document_result_accepts_normalization_profile(self) -> None: sig = inspect.signature(_compute_document_result) assert "normalization_profile" in sig.parameters class TestProfileResolution: def test_all_eleven_profiles_resolvable(self) -> None: """Les 11 profils annoncés dans le README sont tous résolvables. Verrouille la cohérence entre ``NORMALIZATION_PROFILES`` (table runtime) et ``NormalizationProfileId`` (Literal Pydantic web). """ expected = { "nfc", "caseless", "minimal", "medieval_french", "early_modern_french", "medieval_latin", "medieval_english", "early_modern_english", "secretary_hand", "sans_ponctuation", "sans_apostrophes", } assert set(NORMALIZATION_PROFILES.keys()) >= expected for name in expected: profile = get_builtin_profile(name) assert profile is not None assert profile.name == name class TestWebModelProfileAlignment: def test_web_literal_lists_all_eleven_profiles(self) -> None: """Le ``Literal`` Pydantic doit lister les 11 profils. Avant S1, le Literal n'en exposait que 8 — Pydantic rejetait donc 3 profils valides du runtime. """ from picarones.web.models import NormalizationProfileId from typing import get_args literals = set(get_args(NormalizationProfileId)) runtime = set(NORMALIZATION_PROFILES.keys()) # Le web peut être un sous-ensemble strict en théorie, mais # l'alignement README ↔ web ↔ runtime exige égalité. assert literals == runtime, ( f"Décalage README/web/runtime. Web a {literals}, " f"runtime a {runtime}. Diff missing-from-web: " f"{runtime - literals}, extra-in-web: {literals - runtime}." ) class TestNormalizationActuallyApplied: """Vérifie via une intégration unitaire que le profil arrive bien jusqu'à ``compute_metrics`` et change le ``cer_diplomatic`` calculé.""" def test_cer_diplomatic_uses_specified_profile(self) -> None: """Avec deux profils différents, le ``cer_diplomatic`` est différent sur la même paire de textes. Si le profil n'était pas propagé, on aurait toujours la même valeur.""" from picarones.measurements.metrics import compute_metrics # Texte avec un ſ médiéval + un v moderne (la GT a l'ancienne # graphie, l'OCR la moderne). gt = "ſuper aqua viuens" hyp = "super aqua vivens" # Profil "minimal" : seul ſ → s. v reste v de chaque côté. prof_minimal = get_builtin_profile("minimal") m_minimal = compute_metrics(gt, hyp, normalization_profile=prof_minimal) # Profil "medieval_latin" : ſ → s, u → v, etc. Sera plus permissif. prof_latin = get_builtin_profile("medieval_latin") m_latin = compute_metrics(gt, hyp, normalization_profile=prof_latin) # Les deux doivent être calculés. assert m_minimal.cer_diplomatic is not None assert m_latin.cer_diplomatic is not None assert m_minimal.diplomatic_profile_name == "minimal" assert m_latin.diplomatic_profile_name == "medieval_latin" # Les profils diffèrent → le score change. S'ils étaient # confondus (bug de propagation), ce serait égal. assert m_minimal.diplomatic_profile_name != m_latin.diplomatic_profile_name