File size: 7,399 Bytes
eb547cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d109222
eb547cb
 
 
 
 
 
 
 
9011070
eb547cb
 
 
 
 
9011070
eb547cb
 
 
 
 
 
9011070
eb547cb
 
 
 
 
 
 
 
 
 
 
9011070
eb547cb
 
 
 
 
 
 
 
ff7895c
 
 
 
 
 
eb547cb
 
 
 
 
 
 
 
9011070
eb547cb
 
 
 
 
 
 
9011070
eb547cb
 
 
 
 
 
 
 
 
 
 
 
9011070
eb547cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e45d507
9011070
eb547cb
 
 
 
 
 
 
 
 
 
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
"""Tests Sprint 30 — polish, accessibilité et DX.

Sprint 30 livre quatre durcissements transverses :

1. ``picarones/i18n.py`` : chargement thread-safe via verrou explicite,
   ``lru_cache`` sur ``get_labels``, ``reload_translations()`` exposé.
2. ``BaseOCREngine._safe_version()`` log la stacktrace en DEBUG au
   lieu de swallow vers ``"unknown"`` silencieusement.
3. Badges CER WCAG : icône unicode + pattern de bordure + ``aria-label``
   contextuel — la couleur n'est plus la seule info visuelle.
4. Pre-commit hooks : ``.pre-commit-config.yaml`` + section
   ``CONTRIBUTING.md``.
5. ``CHANGELOG.md`` rattrapé Sprints 10-30.
6. ``SPECS.md`` annexé d'un addendum couvrant Sprints 16-30.
"""

from __future__ import annotations

from pathlib import Path


ROOT = Path(__file__).parent.parent.parent


# ---------------------------------------------------------------------------
# 1. i18n thread-safe + lru_cache
# ---------------------------------------------------------------------------

class TestI18nCache:
    def test_get_labels_returns_dict(self):
        from picarones.reports.i18n import get_labels
        labels = get_labels("fr")
        assert isinstance(labels, dict)
        assert len(labels) > 5

    def test_get_labels_unknown_falls_back_to_fr(self):
        from picarones.reports.i18n import get_labels
        fr = get_labels("fr")
        unknown = get_labels("xx-pas-existante")
        # Le fallback doit être le contenu fr
        assert unknown == fr

    def test_get_labels_cached(self):
        from picarones.reports import i18n
        i18n.reload_translations()
        # Premier appel — peuple le cache
        i18n.get_labels("fr")
        # Inspection : le cache lru a un hit
        # (lru_cache expose cache_info() sur la fonction wrappée)
        info_before = i18n._get_labels_cached.cache_info()
        i18n.get_labels("fr")
        info_after = i18n._get_labels_cached.cache_info()
        assert info_after.hits > info_before.hits

    def test_reload_translations_clears_cache(self):
        from picarones.reports import i18n
        i18n.get_labels("fr")
        info_before = i18n._get_labels_cached.cache_info()
        assert info_before.currsize >= 1
        i18n.reload_translations()
        info_after = i18n._get_labels_cached.cache_info()
        assert info_after.currsize == 0


# Section retirée au sprint H.2.d : ``BaseOCREngine._safe_version()``
# n'existe plus (BaseOCREngine supprimé avec ``adapters/legacy_engines/``).
# Le contrat équivalent côté canonique (``BaseOCRAdapter`` n'a pas de
# ``version()``) est testé dans
# ``tests/app/test_sprint_h2b_canonical_in_runner.py`` via
# ``test_canonical_adapter_version_unknown``.


# ---------------------------------------------------------------------------
# 3. Badges CER WCAG (rapport HTML)
# ---------------------------------------------------------------------------

class TestBadgesAccessibility:
    def test_app_js_exposes_tier_helpers(self):
        path = ROOT / "picarones" / "reports" / "html" / "templates" / "_app.js"
        src = path.read_text(encoding="utf-8")
        for fn in ("cerTier", "cerTierIcon", "cerTierLabel"):
            assert f"function {fn}" in src, (
                f"_app.js doit exposer ``function {fn}`` (Sprint 30 a11y)"
            )

    def test_styles_define_tier_patterns(self):
        path = ROOT / "picarones" / "reports" / "html" / "templates" / "_styles.css"
        src = path.read_text(encoding="utf-8")
        for tier in ("excellent", "acceptable", "mediocre", "critical"):
            assert f'data-cer-tier="{tier}"' in src, (
                f"_styles.css doit définir un pattern pour le tier {tier!r}"
            )
        # Au moins quatre styles de bordure différents
        assert "border: 1.5px solid"  in src
        assert "border: 1.5px dashed" in src
        assert "border: 1.5px dotted" in src
        assert "border: 1.5px double" in src

    def test_main_badge_carries_data_attr_and_aria(self):
        path = ROOT / "picarones" / "reports" / "html" / "templates" / "_app.js"
        src = path.read_text(encoding="utf-8")
        assert "setAttribute('data-cer-tier'" in src
        assert "setAttribute('aria-label'" in src


# ---------------------------------------------------------------------------
# 4. Pre-commit + CONTRIBUTING
# ---------------------------------------------------------------------------

class TestPreCommitInfra:
    def test_pre_commit_config_exists(self):
        path = ROOT / ".pre-commit-config.yaml"
        assert path.exists()
        text = path.read_text(encoding="utf-8")
        # Doit référencer ruff (alignement avec le job CI ``lint``)
        assert "ruff" in text.lower()

    def test_pre_commit_yaml_is_well_formed(self):
        import yaml
        path = ROOT / ".pre-commit-config.yaml"
        data = yaml.safe_load(path.read_text(encoding="utf-8"))
        assert isinstance(data, dict)
        assert "repos" in data
        assert isinstance(data["repos"], list)
        assert any(
            "ruff" in (repo.get("repo") or "").lower()
            for repo in data["repos"]
        )

    def test_contributing_documents_pre_commit(self):
        path = ROOT / "CONTRIBUTING.md"
        text = path.read_text(encoding="utf-8")
        assert "pre-commit" in text.lower()
        assert "pre-commit install" in text


# ---------------------------------------------------------------------------
# 5. Documentation rattrapée
# ---------------------------------------------------------------------------

class TestChangelogAndSpecsUpdated:
    def test_changelog_mentions_recent_sprints(self):
        text = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
        # Backport Sprints 10-22 et 23-30 doivent être mentionnés
        for sprint in ("Sprint 11", "Sprint 17", "Sprint 19", "Sprint 22",
                       "Sprint 24", "Sprint 27", "Sprint 30"):
            assert sprint in text, (
                f"CHANGELOG.md doit mentionner {sprint} (Sprint 30 backport)"
            )

    def test_specs_addendum_present(self):
        text = (ROOT / "SPECS.md").read_text(encoding="utf-8")
        assert "Addendum" in text
        # Au moins quatre des nouvelles fonctionnalités annexées
        for keyword in ("narrative", "Pareto", "glossaire", "snapshots"):
            assert keyword in text.lower() or keyword in text, (
                f"SPECS.md addendum doit couvrir {keyword!r}"
            )


# ---------------------------------------------------------------------------
# 6. Intégration : un rapport généré porte les attributs WCAG dans son JS
# ---------------------------------------------------------------------------

class TestGeneratedReportCarriesA11y:
    def test_generated_html_embeds_tier_helpers(self, tmp_path):
        from picarones.evaluation import synthetic as fixtures
        from picarones.reports.html.generator import ReportGenerator

        b = fixtures.generate_sample_benchmark(n_docs=4)
        out = tmp_path / "rapport.html"
        ReportGenerator(b, lang="fr").generate(out)
        html = out.read_text(encoding="utf-8")
        # Les fonctions JS doivent figurer dans le bundle inline
        assert "cerTier" in html
        assert "cerTierIcon" in html
        # Les règles CSS pour les patterns aussi
        assert 'data-cer-tier="excellent"' in html