File size: 5,242 Bytes
a2bea75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""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