Claude commited on
Commit
d26fc66
·
unverified ·
1 Parent(s): 9228764

test(cli): tests d'intégration CLI smoke + chemins clés (Phase 6 chantier)

Browse files

Avant : seul ``test_chantier4.py::TestCliWorkflows`` testait
``diagnose``/``economics``/``edition`` via leur ``--help``. Les
autres 10 commandes (``run``, ``history``, ``compare``, ``robustness``,
``metrics``, ``info``, ``engines``, ``demo``, ``report``, ``serve``)
n'avaient AUCUN test d'intégration — une régression sur leur
enregistrement Click ou leur signature passait inaperçue jusqu'à ce
qu'un utilisateur réel les invoque.

``tests/interfaces/cli/test_cli_commands_smoke.py`` (24 tests) couvre :

1. **Smoke ``--help``** paramétré sur les 13 commandes canoniques :
chacune répond sans crash + ``picarones --help`` les liste toutes
à la racine (anti-régression d'enregistrement).
2. **``picarones demo``** : génère un rapport HTML > 5 Ko complet
(sans corpus, sans moteur) — chemin d'évaluation rapide.
3. **``picarones engines``** : tous les 8 moteurs canoniques
(Phase 3 : tesseract, pero_ocr, kraken, calamari, mistral_ocr,
google_vision, azure_doc_intel, precomputed) listés.
4. **``picarones info``** : version + dépendances clé (click, jiwer,
Pillow).
5. **``picarones history --help``** : options de filtre exposées.
6. **``picarones robustness --help``** / **``compare --help``** /
**``metrics --help``** : options principales présentes.
7. **``picarones run --fail-if-cer-above 15.0``** : rejet Click
avec message explicite (Phase 2 chantier post-rewrite —
ancienne sémantique pourcentage refusée).

Ces tests n'invoquent pas de vrai benchmark (corpus + moteur réel) —
ceux-là vivent dans ``tests/integration/`` quand un OCR est dispo
en CI.

https://claude.ai/code/session_01ArfZ8kcgv7Cyda7VbJVmpn

tests/interfaces/cli/test_cli_commands_smoke.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests d'intégration CLI — smoke + chemins clés (Phase 6 chantier).
2
+
3
+ Avant Phase 6 : seul ``test_chantier4.py::TestCliWorkflows`` testait
4
+ les commandes ``diagnose``/``economics``/``edition`` via leur
5
+ ``--help``. Les autres commandes (``run``, ``history``, ``compare``,
6
+ ``robustness``, ``metrics``, ``info``, ``engines``, ``demo``,
7
+ ``report``) n'avaient aucun test d'intégration — une régression sur
8
+ leur enregistrement Click ou leur signature passait inaperçue jusqu'à
9
+ ce qu'un utilisateur réel les invoque.
10
+
11
+ Couvre :
12
+
13
+ 1. **Smoke ``--help``** : toutes les commandes répondent sans
14
+ exit code != 0 et listent leurs options principales.
15
+ 2. **Demo end-to-end** : ``picarones demo`` génère un rapport HTML
16
+ complet (sans corpus, sans moteur réel) — c'est le chemin que
17
+ la doc README pointe pour l'évaluation rapide.
18
+ 3. **Engines matrix** : ``picarones engines`` affiche les 8 moteurs
19
+ du catalogue canonique (cohérence avec ``/api/engines``).
20
+ 4. **Info dependencies** : ``picarones info`` liste Picarones +
21
+ dépendances clé.
22
+ 5. **History --regression** : sans base, retourne un message lisible
23
+ au lieu de planter.
24
+
25
+ Pas de tests qui invoquent un vrai benchmark (corpus + moteur réel) —
26
+ ceux-ci vivent dans ``tests/integration/`` quand un OCR est dispo.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import pytest
32
+
33
+
34
+ @pytest.fixture
35
+ def runner():
36
+ from click.testing import CliRunner
37
+ return CliRunner()
38
+
39
+
40
+ @pytest.fixture
41
+ def cli():
42
+ from picarones.interfaces.cli import cli as cli_group
43
+ return cli_group
44
+
45
+
46
+ # ──────────────────────────────────────────────────────────────────────
47
+ # 1. Smoke --help pour toutes les commandes
48
+ # ──────────────────────────────────────────────────────────────────────
49
+
50
+
51
+ _ALL_COMMANDS = [
52
+ "run", "diagnose", "economics", "edition", "compare",
53
+ "robustness", "history", "metrics", "info", "engines",
54
+ "demo", "report", "serve",
55
+ ]
56
+
57
+
58
+ class TestSmokeHelp:
59
+ """Toutes les commandes CLI doivent répondre à ``--help`` sans
60
+ crash et lister leurs options principales. Garde-fou contre une
61
+ régression d'enregistrement Click."""
62
+
63
+ @pytest.mark.parametrize("cmd_name", _ALL_COMMANDS)
64
+ def test_help_works(self, runner, cli, cmd_name: str) -> None:
65
+ result = runner.invoke(cli, [cmd_name, "--help"])
66
+ assert result.exit_code == 0, (
67
+ f"`picarones {cmd_name} --help` a échoué : "
68
+ f"exit={result.exit_code}, output={result.output[:500]}"
69
+ )
70
+ # Le help doit au moins inclure le nom de la commande.
71
+ assert cmd_name in result.output.lower() or "usage" in result.output.lower()
72
+
73
+ def test_root_help_lists_all_commands(self, runner, cli) -> None:
74
+ """``picarones --help`` doit lister toutes les sous-commandes
75
+ canoniques — sinon une commande enregistrée mais non groupée
76
+ passe inaperçue."""
77
+ result = runner.invoke(cli, ["--help"])
78
+ assert result.exit_code == 0
79
+ for cmd_name in _ALL_COMMANDS:
80
+ assert cmd_name in result.output, (
81
+ f"Sous-commande ``{cmd_name}`` absente de "
82
+ f"``picarones --help`` :\n{result.output}"
83
+ )
84
+
85
+
86
+ # ──────────────────────────────────────────────────────────────────────
87
+ # 2. Demo end-to-end : génération d'un rapport sans moteur
88
+ # ──────────────────────────────────────────────────────────────────────
89
+
90
+
91
+ class TestDemoCommand:
92
+ """``picarones demo`` est le chemin d'évaluation rapide pour un
93
+ utilisateur qui n'a pas Tesseract ni de corpus : il génère un
94
+ rapport HTML synthétique pour explorer l'UI."""
95
+
96
+ def test_demo_generates_html_file(self, runner, cli, tmp_path):
97
+ output = tmp_path / "demo.html"
98
+ result = runner.invoke(cli, ["demo", "--output", str(output)])
99
+ assert result.exit_code == 0, result.output
100
+ assert output.exists()
101
+ assert output.stat().st_size > 5000, (
102
+ "Le rapport démo doit faire au moins 5 Ko "
103
+ "(HTML + Chart.js inline + données synthétiques)"
104
+ )
105
+ # Sanity : c'est bien du HTML.
106
+ head = output.read_text(encoding="utf-8")[:200]
107
+ assert "<!DOCTYPE html>" in head or "<html" in head.lower()
108
+
109
+
110
+ # ──────────────────────────────────────────────────────────────────────
111
+ # 3. Engines matrix : source de vérité unique avec /api/engines
112
+ # ──────────────────────────────────────────────────────────────────────
113
+
114
+
115
+ class TestEnginesCommand:
116
+ """``picarones engines`` doit lister tous les moteurs canoniques
117
+ (Phase 3 chantier post-rewrite : matrice unique avec la factory
118
+ ``adapters/ocr/factory._SUPPORTED``)."""
119
+
120
+ def test_engines_lists_all_canonical(self, runner, cli):
121
+ result = runner.invoke(cli, ["engines"])
122
+ assert result.exit_code == 0, result.output
123
+ # Vérifie que les 8 moteurs canoniques apparaissent (le format
124
+ # est libre — on ne lock pas la mise en page).
125
+ for canonical in (
126
+ "tesseract", "pero_ocr", "kraken", "calamari",
127
+ "mistral_ocr", "google_vision", "azure_doc_intel",
128
+ "precomputed",
129
+ ):
130
+ assert canonical in result.output, (
131
+ f"Moteur canonique ``{canonical}`` absent de "
132
+ f"``picarones engines`` :\n{result.output}"
133
+ )
134
+
135
+
136
+ # ──────────────────────────────────────────────────────────────────────
137
+ # 4. Info : version + dépendances
138
+ # ──────────────────────────────────────────────────────────────────────
139
+
140
+
141
+ class TestInfoCommand:
142
+ def test_info_shows_version(self, runner, cli):
143
+ result = runner.invoke(cli, ["info"])
144
+ assert result.exit_code == 0, result.output
145
+ assert "Picarones" in result.output
146
+
147
+ def test_info_lists_key_dependencies(self, runner, cli):
148
+ result = runner.invoke(cli, ["info"])
149
+ assert result.exit_code == 0
150
+ # Quelques dépendances critiques doivent être listées
151
+ # (statut "v1.2.3" ou "non installé", peu importe).
152
+ for dep in ("click", "jiwer", "Pillow"):
153
+ assert dep in result.output, (
154
+ f"Dépendance ``{dep}`` absente de "
155
+ f"``picarones info`` :\n{result.output}"
156
+ )
157
+
158
+
159
+ # ──────────────────────────────────────────────────────────────────────
160
+ # 5. History : commande sans base disponible
161
+ # ──────────────────────────────────────────────────────────────────────
162
+
163
+
164
+ class TestHistoryCommand:
165
+ def test_history_help(self, runner, cli):
166
+ """Le help doit lister les options de filtre principales."""
167
+ result = runner.invoke(cli, ["history", "--help"])
168
+ assert result.exit_code == 0
169
+ # Au moins une option de filtre / format
170
+ assert ("--list" in result.output
171
+ or "--engine" in result.output
172
+ or "--regression" in result.output)
173
+
174
+
175
+ # ──────────────────────────────────────────────────────────────────────
176
+ # 6. Robustness : help + signature
177
+ # ──────────────────────────────────────────────────────────────────────
178
+
179
+
180
+ class TestRobustnessCommand:
181
+ def test_robustness_help(self, runner, cli):
182
+ result = runner.invoke(cli, ["robustness", "--help"])
183
+ assert result.exit_code == 0
184
+ assert "--corpus" in result.output or "--results" in result.output
185
+
186
+
187
+ # ──────────────────────────────────────────────────────────────────────
188
+ # 7. Compare : prend 2+ JSONs
189
+ # ──────────────────────────────────────────────────────────────────────
190
+
191
+
192
+ class TestCompareCommand:
193
+ def test_compare_help(self, runner, cli):
194
+ result = runner.invoke(cli, ["compare", "--help"])
195
+ assert result.exit_code == 0
196
+ # Compare prend au moins 2 fichiers de résultats.
197
+ assert "RESULTS" in result.output.upper() or "compare" in result.output.lower()
198
+
199
+
200
+ # ──────────────────────────────────────────────────────────────────────
201
+ # 8. Metrics : sanity sur l'aide
202
+ # ──────────────────────────────────────────────────────────────────────
203
+
204
+
205
+ class TestMetricsCommand:
206
+ def test_metrics_help(self, runner, cli):
207
+ result = runner.invoke(cli, ["metrics", "--help"])
208
+ assert result.exit_code == 0
209
+ # ``metrics`` compare une référence et une hypothèse.
210
+ assert "--reference" in result.output or "--hypothesis" in result.output
211
+
212
+
213
+ # ──────────────────────────────────────────────────────────────────────
214
+ # 9. Run : help expose les options sécurité Phase 2
215
+ # ──────────────────────────────────────────────────────────────────────
216
+
217
+
218
+ class TestRunCommand:
219
+ def test_run_help_shows_engines_option(self, runner, cli):
220
+ result = runner.invoke(cli, ["run", "--help"])
221
+ assert result.exit_code == 0
222
+ assert "--engines" in result.output
223
+ assert "--corpus" in result.output
224
+
225
+ def test_run_fail_if_cer_above_validated(self, runner, cli, tmp_path):
226
+ """Phase 2 chantier post-rewrite : ``--fail-if-cer-above 15.0``
227
+ (ancienne sémantique pourcentage) doit être rejeté à
228
+ l'analyse Click avec un message explicite suggérant la
229
+ nouvelle sémantique fraction. On passe un ``--corpus``
230
+ valide pour que la validation du seuil soit atteinte (Click
231
+ valide les options dans l'ordre)."""
232
+ # Corpus minimal valide pour passer la validation Click sur --corpus.
233
+ # Le test stoppe avant l'exécution réelle grâce au seuil invalide.
234
+ result = runner.invoke(
235
+ cli, ["run", "--corpus", str(tmp_path),
236
+ "--fail-if-cer-above", "15.0"],
237
+ )
238
+ assert result.exit_code != 0
239
+ # Le message d'erreur Click cite ``--fail-if-cer-above`` et
240
+ # explique la nouvelle sémantique (fraction ∈ [0, 1]).
241
+ output_low = result.output.lower()
242
+ assert "fail-if-cer-above" in output_low or "fraction" in output_low