Spaces:
Sleeping
Sleeping
Claude
fix(test): Z1 โ รฉlimine theater systรฉmique par sabotage en commentaire
00b263b unverified | """Tests des 5 vues HTML thรฉmatiques (chantier 3 post-Sprint 97). | |
| Couvre : | |
| - Importation et signature des 5 vues. | |
| - Adaptive masking : ``""`` quand aucune sous-section n'a de signal. | |
| - Rendu HTML cohรฉrent quand les donnรฉes sont fournies. | |
| - Anti-injection HTML sur les noms de moteurs et libellรฉs. | |
| - Composition correcte du shell ``<details>`` (premier ouvert, | |
| autres fermรฉs). | |
| - Cรขblage gรฉnรฉrator โ vues (les variables sont passรฉes au template). | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 1. Imports + signatures | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestViewsImport: | |
| def test_all_views_import(self): | |
| from picarones.reports.html.views import ( | |
| build_advanced_taxonomy_view_html, | |
| build_diagnostics_view_html, | |
| build_economics_view_html, | |
| build_pipeline_view_html, | |
| build_robustness_view_html, | |
| ) | |
| assert callable(build_advanced_taxonomy_view_html) | |
| assert callable(build_diagnostics_view_html) | |
| assert callable(build_economics_view_html) | |
| assert callable(build_pipeline_view_html) | |
| assert callable(build_robustness_view_html) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 2. Adaptive masking โ vues vides retournent "" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def empty_report_data() -> dict: | |
| return {"engines": []} | |
| class TestAdaptiveMasking: | |
| def test_economics_empty_returns_empty(self, empty_report_data): | |
| from picarones.reports.html.views import build_economics_view_html | |
| assert build_economics_view_html(empty_report_data, {}) == "" | |
| def test_advanced_taxonomy_empty_returns_empty(self, empty_report_data): | |
| from picarones.reports.html.views import build_advanced_taxonomy_view_html | |
| assert build_advanced_taxonomy_view_html(empty_report_data, {}) == "" | |
| def test_diagnostics_empty_returns_empty(self, empty_report_data): | |
| from picarones.reports.html.views import build_diagnostics_view_html | |
| assert build_diagnostics_view_html(empty_report_data, {}) == "" | |
| def test_pipeline_empty_returns_empty(self, empty_report_data): | |
| from picarones.reports.html.views import build_pipeline_view_html | |
| assert build_pipeline_view_html(empty_report_data, {}) == "" | |
| def test_robustness_empty_returns_empty(self, empty_report_data): | |
| from picarones.reports.html.views import build_robustness_view_html | |
| assert build_robustness_view_html(empty_report_data, {}) == "" | |
| def test_advanced_taxonomy_single_engine_returns_empty(self): | |
| """La comparaison nรฉcessite โฅ 2 moteurs.""" | |
| from picarones.reports.html.views import build_advanced_taxonomy_view_html | |
| single = {"engines": [{ | |
| "name": "tess", | |
| "aggregated_taxonomy": {"class_distribution": {"x": 10}}, | |
| }]} | |
| # Pas de comparison possible โ vue masquรฉe | |
| assert build_advanced_taxonomy_view_html(single, {}) == "" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 3. Rendu HTML quand donnรฉes fournies | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class _MockMetrics: | |
| def __init__(self, *, cer=0.05, wer=0.1, reference_length=500): | |
| self.cer = cer | |
| self.wer = wer | |
| self.reference_length = reference_length | |
| self.error = None | |
| class _MockDocResult: | |
| def __init__(self, duration=1.0): | |
| self.engine_error = None | |
| self.duration_seconds = duration | |
| self.metrics = _MockMetrics() | |
| class _MockEngineReport: | |
| def __init__(self, name, n_docs=10): | |
| self.engine_name = name | |
| self.document_results = [_MockDocResult() for _ in range(n_docs)] | |
| class TestEconomicsView: | |
| def test_throughput_with_realistic_engines(self): | |
| from picarones.reports.html.views import build_economics_view_html | |
| reports = [ | |
| _MockEngineReport("tesseract"), | |
| _MockEngineReport("pero_ocr"), | |
| ] | |
| html = build_economics_view_html( | |
| {"engines": []}, {}, | |
| engine_reports=reports, | |
| ) | |
| assert html != "" | |
| # Les deux moteurs doivent apparaรฎtre dans le HTML | |
| assert "tesseract" in html | |
| assert "pero" in html | |
| def test_extra_html_blocks_appended(self): | |
| from picarones.reports.html.views import build_economics_view_html | |
| extra = ['<div class="custom">CUSTOM_BLOCK</div>'] | |
| html = build_economics_view_html( | |
| {"engines": []}, | |
| {"economics_extra_title": "Coรปt projetรฉ"}, | |
| engine_reports=[_MockEngineReport("tess")], | |
| extra_html_blocks=extra, | |
| ) | |
| assert "CUSTOM_BLOCK" in html | |
| def test_zero_duration_excludes_engine(self): | |
| """Bench depuis cache (durations=0) ne gรฉnรจre pas de throughput.""" | |
| from picarones.reports.html.views import build_economics_view_html | |
| report = _MockEngineReport("cached") | |
| for dr in report.document_results: | |
| dr.duration_seconds = 0.0 | |
| html = build_economics_view_html( | |
| {"engines": []}, {}, engine_reports=[report], | |
| ) | |
| # Aucun moteur n'a de durรฉe โ vue masquรฉe | |
| assert html == "" | |
| class TestAdvancedTaxonomyView: | |
| def test_two_engines_taxonomy_compared(self): | |
| from picarones.reports.html.views import build_advanced_taxonomy_view_html | |
| report_data = { | |
| "engines": [ | |
| { | |
| "name": "tess", "cer": 0.05, | |
| "aggregated_taxonomy": { | |
| "class_distribution": { | |
| "case_error": 100, "ligature_error": 50, | |
| "lacuna": 30, | |
| }, | |
| }, | |
| }, | |
| { | |
| "name": "pero", "cer": 0.07, | |
| "aggregated_taxonomy": { | |
| "class_distribution": { | |
| "case_error": 30, "lacuna": 80, | |
| "diacritic_error": 60, | |
| }, | |
| }, | |
| }, | |
| ], | |
| } | |
| html = build_advanced_taxonomy_view_html(report_data, {}) | |
| assert html != "" | |
| # Le diagramme miroir doit nommer les 2 moteurs | |
| assert "tess" in html | |
| assert "pero" in html | |
| def test_anti_injection_engine_name(self): | |
| """Un nom de moteur avec balises HTML doit รชtre รฉchappรฉ.""" | |
| from picarones.reports.html.views import build_advanced_taxonomy_view_html | |
| report_data = { | |
| "engines": [ | |
| { | |
| "name": "<script>alert(1)</script>", | |
| "cer": 0.05, | |
| "aggregated_taxonomy": { | |
| "class_distribution": {"case_error": 10}, | |
| }, | |
| }, | |
| { | |
| "name": "pero", | |
| "cer": 0.07, | |
| "aggregated_taxonomy": { | |
| "class_distribution": {"lacuna": 10}, | |
| }, | |
| }, | |
| ], | |
| } | |
| html = build_advanced_taxonomy_view_html(report_data, {}) | |
| # Pas de balise script non รฉchappรฉe | |
| assert "<script>alert" not in html | |
| # Mais le contenu doit รชtre prรฉsent sous forme รฉchappรฉe | |
| assert "<script" in html or "alert" not in html.lower() | |
| def test_lexical_modernization_optional(self): | |
| from picarones.reports.html.views import build_advanced_taxonomy_view_html | |
| report_data = { | |
| "engines": [ | |
| { | |
| "name": "tess", "cer": 0.05, | |
| "aggregated_taxonomy": { | |
| "class_distribution": {"case_error": 10}, | |
| }, | |
| }, | |
| { | |
| "name": "pero", "cer": 0.07, | |
| "aggregated_taxonomy": { | |
| "class_distribution": {"case_error": 5}, | |
| }, | |
| }, | |
| ], | |
| } | |
| # Sans lexical_modernization, la sous-section n'apparaรฎt pas | |
| html_no = build_advanced_taxonomy_view_html(report_data, {}) | |
| # Avec, elle apparaรฎt | |
| # Le format attendu par ``top_modernized_tokens`` est | |
| # ``{"tokens": {gt_token: {n_total, n_modernized, rate_modernized, | |
| # variants}}}`` (cf. ``aggregate_lexical_modernization``). | |
| lex_data = { | |
| "tokens": { | |
| "maistre": { | |
| "n_total": 10, "n_modernized": 8, | |
| "rate_modernized": 0.8, | |
| "variants": {"maรฎtre": 8}, | |
| }, | |
| }, | |
| } | |
| html_yes = build_advanced_taxonomy_view_html( | |
| report_data, {}, lexical_modernization=lex_data, | |
| ) | |
| # Au moins une section de plus | |
| assert len(html_yes) > len(html_no) | |
| class TestDiagnosticsView: | |
| def test_levers_only_when_signal(self): | |
| """detect_levers doit รชtre appelรฉ. Si rien ne dรฉclenche, vue masquรฉe.""" | |
| from picarones.reports.html.views import build_diagnostics_view_html | |
| # report_data minimal โ aucun levier ne devrait se dรฉclencher | |
| empty = {"engines": []} | |
| assert build_diagnostics_view_html(empty, {}) == "" | |
| def test_image_predictive_with_qualities(self): | |
| from picarones.reports.html.views import build_diagnostics_view_html | |
| # Liste d'image_qualities synthรฉtiques (>= 1 doc) | |
| qualities = [ | |
| { | |
| "contrast": 0.8, "noise_level": 0.2, | |
| "blur_score": 0.1, "estimated_dpi": 300, | |
| "rotation_estimate": 0.5, "low_contrast_pct": 0.05, | |
| }, | |
| { | |
| "contrast": 0.6, "noise_level": 0.4, | |
| "blur_score": 0.3, "estimated_dpi": 250, | |
| "rotation_estimate": 1.0, "low_contrast_pct": 0.10, | |
| }, | |
| ] | |
| html = build_diagnostics_view_html( | |
| {"engines": []}, {}, image_qualities=qualities, | |
| ) | |
| # La section image_predictive doit s'afficher | |
| assert html != "" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 4. Composition du shell <details> | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestDetailsShell: | |
| def test_first_block_open_others_closed(self): | |
| from picarones.reports.html.views.economics import _render_view_shell | |
| html = _render_view_shell( | |
| view_title="Test", | |
| view_note="Note", | |
| blocks=[("A", "<p>aaa</p>"), ("B", "<p>bbb</p>"), ("C", "<p>ccc</p>")], | |
| ) | |
| # Le premier <details> doit รชtre ouvert | |
| details = html.split("<details") | |
| assert "open" in details[1].split(">")[0] | |
| # Les suivants ne doivent pas l'รชtre | |
| assert "open" not in details[2].split(">")[0] | |
| assert "open" not in details[3].split(">")[0] | |
| # Tous les contenus prรฉsents | |
| assert "aaa" in html and "bbb" in html and "ccc" in html | |
| def test_xml_chars_in_titles_escaped(self): | |
| from picarones.reports.html.views.economics import _render_view_shell | |
| html = _render_view_shell( | |
| view_title="<script>alert(1)</script>", | |
| view_note="Note <b>bold</b>", | |
| blocks=[("Block <X>", "<p>content</p>")], | |
| ) | |
| # Pas d'injection | |
| assert "<script>alert(1)</script>" not in html | |
| # Mais visible sous forme รฉchappรฉe | |
| assert "<script" in html | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 5. Cรขblage gรฉnรฉrator โ vues | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestGeneratorWiring: | |
| def test_generator_imports_three_views(self): | |
| """Test runtime du cรขblage des 3 vues automatiques (economics, | |
| advanced_taxonomy, diagnostics). | |
| Vรฉrifie que la mรฉthode ``ReportGenerator._build_section_html`` | |
| retourne un dict contenant les 3 clรฉs attendues, ce qui | |
| garantit qu'elles seront splatรฉes vers le template Jinja. | |
| Cette version remplace l'ancien test qui scannait textuellement | |
| ``generator.py`` ร la recherche de ``var=`` ou ``"var"`` โ | |
| approche fragile (passait sur n'importe quelle occurrence dans | |
| une docstring) et trop liรฉe ร la forme du code. | |
| """ | |
| from picarones.evaluation.synthetic import generate_sample_benchmark | |
| from picarones.reports.html.generator import ReportGenerator | |
| bench = generate_sample_benchmark() | |
| gen = ReportGenerator(bench, lang="fr") | |
| from picarones.reports.i18n import get_labels | |
| report_data = { | |
| "engines": [], | |
| "inter_engine_analysis": None, | |
| "stratified_ranking": None, | |
| "available_strata": [], | |
| "corpus_homogeneity": None, | |
| } | |
| section_html = gen._build_section_html(report_data, get_labels("fr")) | |
| for name in ( | |
| "economics_view_html", | |
| "advanced_taxonomy_view_html", | |
| "diagnostics_view_html", | |
| ): | |
| assert name in section_html, ( | |
| f"clรฉ {name!r} absente du dict retournรฉ par " | |
| "ReportGenerator._build_section_html โ la vue ne sera " | |
| "pas cรขblรฉe vers le template." | |
| ) | |
| # Adaptive : avec un report_data vide, chaque vue retourne "" | |
| # (rapport adaptatif). On vรฉrifie le type, pas le contenu. | |
| assert isinstance(section_html[name], str), ( | |
| f"section {name!r} doit รชtre une chaรฎne, " | |
| f"pas {type(section_html[name]).__name__}" | |
| ) | |
| def test_template_uses_three_views(self): | |
| """Les 3 vues thรฉmatiques sont cรขblรฉes dans leur destination | |
| XerOCR sรฉmantique (Phase 21 โ dispatch depuis view_analyses) : | |
| - economics_view_html โ Engines/Tableau (throughput rejoint | |
| le tableau de rรฉfรฉrence par moteur). | |
| - advanced_taxonomy_view_html โ Engines/Diagnostics (extends the | |
| basic taxonomy chart). | |
| - diagnostics_view_html โ Engines/Diagnostics (composite | |
| thรฉmatique homonyme). | |
| """ | |
| from pathlib import Path | |
| templates_dir = ( | |
| Path(__file__).parent.parent.parent | |
| / "picarones" / "reports" / "html" / "templates" | |
| ) | |
| from tests._strip_helpers import strip_comments | |
| engines_table_src = strip_comments( | |
| (templates_dir / "views" / "engines_table.html").read_text(encoding="utf-8"), | |
| "jinja2", | |
| ) | |
| engines_diag_src = strip_comments( | |
| (templates_dir / "views" / "engines_diagnostics.html").read_text(encoding="utf-8"), | |
| "jinja2", | |
| ) | |
| # Accepte les deux variantes Jinja2 whitespace control (``{% if %}`` | |
| # ou ``{%- if %}``) โ le second utilisรฉ en engines_diagnostics | |
| # depuis Phase 22+23 pour รฉviter l'accumulation de lignes vides. | |
| def _wired(src: str, var: str) -> bool: | |
| return f"{{% if {var} %}}" in src or f"{{%- if {var} %}}" in src | |
| assert _wired(engines_table_src, "economics_view_html"), ( | |
| "economics_view_html doit รชtre cรขblรฉe dans engines_table.html " | |
| "(Phase 21 dispatch โ throughput va avec le tableau moteurs)" | |
| ) | |
| assert _wired(engines_diag_src, "advanced_taxonomy_view_html"), ( | |
| "advanced_taxonomy_view_html doit รชtre cรขblรฉe dans engines_diagnostics.html" | |
| ) | |
| assert _wired(engines_diag_src, "diagnostics_view_html"), ( | |
| "diagnostics_view_html doit รชtre cรขblรฉe dans engines_diagnostics.html" | |
| ) | |