Spaces:
Sleeping
Sleeping
| """Tests S1 โ toggles UI exposรฉs pour les options `BenchmarkRunRequest`. | |
| Sprint S1 expose dans le template web 6 champs Pydantic qui รฉtaient | |
| figรฉs cรดtรฉ UI : | |
| - ``report_lang`` (FR / EN) | |
| - ``views`` (text_final + alto_documentary + searchability) | |
| - ``expose_alto`` (par-compรฉtiteur, sur Tesseract uniquement) | |
| - ``entity_extractor`` (dotted path NER) | |
| - ``output_json`` (toggle qui dรฉrive un chemin auto) | |
| - ``partial_dir`` (toggle qui dรฉrive un chemin auto) | |
| Ces tests vรฉrifient : | |
| 1. La prรฉsence des รฉlรฉments DOM dans le template HTML rendu | |
| (l'UI peut effectivement collecter ces valeurs). | |
| 2. Le payload complet UI-style est acceptรฉ par | |
| ``POST /api/benchmark/run`` (compat ascendante prรฉservรฉe). | |
| 3. Le payload sans les nouveaux champs continue de marcher (les | |
| dรฉfauts Pydantic restent valides). | |
| Le contrat **API โ Pydantic** lui-mรชme (validation positive/nรฉgative, | |
| path traversal) est testรฉ exhaustivement par | |
| ``tests/web/test_benchmark_run_b3_final_fields.py``. Ce module-ci | |
| cible la couche **UI โ JSON payload**. | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| def client(): | |
| from picarones.interfaces.web.app import app | |
| return TestClient(app) | |
| def workspace_corpus(tmp_path: Path) -> str: | |
| """Crรฉe un corpus minimal sous un workspace autorisรฉ pour les | |
| validators d'``output_dir`` / ``corpus_path``.""" | |
| from PIL import Image | |
| img_path = tmp_path / "doc01.png" | |
| Image.new("RGB", (40, 40), color=(255, 255, 255)).save(img_path) | |
| (tmp_path / "doc01.gt.txt").write_text("hello", encoding="utf-8") | |
| return str(tmp_path) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 1. Prรฉsence des รฉlรฉments DOM dans le template | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestAdvancedOptionsUIElements: | |
| """Le template ``_view_benchmark.html`` doit contenir les IDs DOM | |
| consommรฉs par ``_gather_advanced_options()`` cรดtรฉ JS.""" | |
| def html(self, client) -> str: | |
| response = client.get("/") | |
| assert response.status_code == 200 | |
| return response.text | |
| def test_report_lang_select_present(self, html: str) -> None: | |
| assert 'id="report-lang"' in html, ( | |
| "Sรฉlecteur de langue du rapport manquant โ " | |
| "report_lang ne sera jamais transmis depuis l'UI." | |
| ) | |
| # Options FR + EN visibles | |
| assert 'value="fr"' in html | |
| assert 'value="en"' in html | |
| def test_views_checkboxes_present(self, html: str) -> None: | |
| """Les checkboxes pour alto_documentary et searchability.""" | |
| assert 'id="view-alto-documentary"' in html | |
| assert 'id="view-searchability"' in html | |
| # text_final reste activรฉ en permanence (checkbox disabled) | |
| assert 'id="view-text-final"' in html | |
| def test_entity_extractor_input_present(self, html: str) -> None: | |
| assert 'id="entity-extractor"' in html | |
| # Le datalist propose des presets utiles | |
| assert "spacy.fr_core_news_sm" in html | |
| assert "spacy.en_core_web_sm" in html | |
| def test_partial_resume_toggle_present(self, html: str) -> None: | |
| assert 'id="enable-partial-resume"' in html | |
| def test_output_json_toggle_present(self, html: str) -> None: | |
| assert 'id="enable-output-json"' in html | |
| def test_expose_alto_checkbox_present(self, html: str) -> None: | |
| """``expose_alto`` est dans la section compose (par-compรฉtiteur).""" | |
| assert 'id="compose-expose-alto"' in html | |
| # Wrap dont la visibilitรฉ dรฉpend du moteur (Tesseract uniquement). | |
| assert 'id="compose-expose-alto-wrap"' in html | |
| def test_advanced_options_collapsible_present(self, html: str) -> None: | |
| """Section ``<details>`` qui regroupe les options avancรฉes.""" | |
| assert 'id="bench-advanced-options"' in html | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 2. Payload UI-style complet acceptรฉ par l'API | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestFullUIStylePayloadAccepted: | |
| """Un payload qui reflรจte la sรฉrialisation produite par | |
| ``_gather_advanced_options()`` doit รชtre acceptรฉ tel quel.""" | |
| def test_payload_with_all_advanced_options( | |
| self, client, workspace_corpus: str, | |
| ) -> None: | |
| """Smoke test : payload UI-style avec les 6 toggles activรฉs. | |
| Note : le validator de chemins est strict โ on utilise | |
| ``partial/run-2026-05-23`` (relatif, sans ``..``) et | |
| ``rapport.json`` (basename). | |
| """ | |
| payload = { | |
| "corpus_path": workspace_corpus, | |
| "competitors": [ | |
| { | |
| "name": "tesseract (fra) ยท ALTO", | |
| "engine_name": "tesseract", | |
| "ocr_model": "fra", | |
| "expose_alto": True, | |
| "max_image_dimension": 0, | |
| }, | |
| ], | |
| "normalization_profile": "nfc", | |
| "char_exclude": "", | |
| "output_dir": workspace_corpus, # mรชme workspace | |
| "report_name": "test_s1", | |
| "profile": "standard", | |
| # Options avancรฉes : | |
| "report_lang": "en", | |
| "views": ["text_final", "alto_documentary", "searchability"], | |
| "partial_dir": "partial/run-2026-05-23", | |
| "entity_extractor": "spacy.fr_core_news_sm", | |
| "output_json": "test_s1.json", | |
| } | |
| # On ne lance pas vraiment le benchmark (lourd) โ on vรฉrifie | |
| # juste que Pydantic accepte le payload et que le runner | |
| # dรฉmarre un job. Le retour est ``{"job_id": ..., "status": "pending"}``. | |
| response = client.post("/api/benchmark/run", json=payload) | |
| # Accepte 200 (job dรฉmarrรฉ) ou 429 (rate-limited en CI parallรจle) | |
| # mais surtout pas 422 (validation). | |
| assert response.status_code != 422, ( | |
| f"Payload rejetรฉ par Pydantic : {response.json()}" | |
| ) | |
| def test_payload_with_minimal_advanced_options( | |
| self, client, workspace_corpus: str, | |
| ) -> None: | |
| """Payload sans les nouveaux champs : compat ascendante. | |
| Un client qui n'envoie aucun champ avancรฉ doit continuer ร | |
| marcher. Les dรฉfauts Pydantic prennent le relais. | |
| """ | |
| payload = { | |
| "corpus_path": workspace_corpus, | |
| "competitors": [ | |
| {"name": "tesseract", "engine_name": "tesseract"}, | |
| ], | |
| "output_dir": workspace_corpus, | |
| } | |
| response = client.post("/api/benchmark/run", json=payload) | |
| assert response.status_code != 422, ( | |
| f"Payload minimal rejetรฉ : {response.json()}" | |
| ) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 3. expose_alto par-compรฉtiteur : transmission propre | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestExposeAltoPerCompetitor: | |
| """``expose_alto`` est un champ de ``PipelineConfig``, pas de | |
| ``BenchmarkRunRequest`` : il s'applique par-compรฉtiteur.""" | |
| def test_expose_alto_only_on_tesseract_competitor( | |
| self, client, workspace_corpus: str, | |
| ) -> None: | |
| """Mixer un compรฉtiteur Tesseract+expose_alto et un autre OCR | |
| cloud sans expose_alto doit fonctionner.""" | |
| payload = { | |
| "corpus_path": workspace_corpus, | |
| "competitors": [ | |
| { | |
| "name": "tesseract (ALTO)", | |
| "engine_name": "tesseract", | |
| "ocr_model": "fra", | |
| "expose_alto": True, | |
| }, | |
| { | |
| "name": "mistral_ocr", | |
| "engine_name": "mistral_ocr", | |
| "expose_alto": False, # No-op pour Mistral | |
| }, | |
| ], | |
| "output_dir": workspace_corpus, | |
| "views": ["text_final", "alto_documentary"], | |
| } | |
| response = client.post("/api/benchmark/run", json=payload) | |
| assert response.status_code != 422 | |
| def test_expose_alto_default_false(self, client, workspace_corpus: str) -> None: | |
| """Compat ascendante : un client qui n'envoie pas expose_alto | |
| reรงoit le dรฉfaut ``false``.""" | |
| from picarones.interfaces.web.models import PipelineConfig | |
| config = PipelineConfig(name="t", engine_name="tesseract") | |
| assert config.expose_alto is False | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 4. Synchronisation i18n des nouvelles clรฉs | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestI18nNewKeysCovered: | |
| """Les nouveaux libellรฉs doivent avoir une traduction FR ET EN | |
| pour ne pas dรฉgrader silencieusement en clรฉ brute (``bench_views_label``) | |
| quand la langue d'interface est EN.""" | |
| def js_source(self) -> str: | |
| path = ( | |
| Path(__file__).resolve().parents[2] | |
| / "picarones" / "interfaces" / "web" / "static" / "web-app.js" | |
| ) | |
| return path.read_text(encoding="utf-8") | |
| def test_key_present_in_both_languages(self, js_source: str, key: str) -> None: | |
| """Chaque clรฉ doit apparaรฎtre au moins **deux fois** dans la | |
| source โ une fois dans ``T.fr`` et une fois dans ``T.en``.""" | |
| count = js_source.count(f"{key}:") | |
| assert count >= 2, ( | |
| f"Clรฉ i18n {key!r} dรฉclarรฉe seulement {count} fois ; " | |
| "attendu โฅ 2 (FR + EN). Risque : clรฉ brute affichรฉe en EN." | |
| ) | |