"""Tests Sprint 37 — section inter-moteurs dans le rapport HTML. Couvre : 1. ``build_divergence_matrix_html`` rend une table HTML colorée avec une ligne par moteur, masque la diagonale, et affiche la paire la plus divergente quand disponible. 2. ``build_oracle_gap_html`` rend l'encart factuel (best engine, oracle, gap absolu/relatif, nombre de docs). 3. **Masquage adaptatif** : les deux fonctions retournent ``""`` si ``inter_engine_analysis`` est ``None``, vide, ou s'il n'y a pas assez de données. 4. **Intégration template** : le rapport HTML inclut la section quand ``inter_engine_analysis`` est peuplé, et l'omet sinon (principe de masquage automatique du rapport adaptatif). 5. **i18n** : les clés FR/EN existent et sont utilisées (texte localisé). 6. **Anti-injection** : un nom de moteur contenant des caractères HTML est échappé. """ from __future__ import annotations import json from pathlib import Path import pytest from picarones.fixtures import generate_sample_benchmark from picarones.report.generator import ReportGenerator from picarones.report.inter_engine_render import ( build_divergence_matrix_html, build_oracle_gap_html, ) # ────────────────────────────────────────────────────────────────────────── # Fixtures # ────────────────────────────────────────────────────────────────────────── @pytest.fixture def realistic_iea() -> dict: """Analyse inter-moteurs réaliste avec 3 moteurs.""" return { "engines": ["tess", "pero", "mistral"], "complementarity": { "oracle_recall": 0.95, "best_single_recall": 0.70, "best_engine": "pero", "absolute_gap": 0.25, "relative_gap": 0.83, "doc_count": 47, "per_engine_recall": {"pero": 0.7, "tess": 0.5, "mistral": 0.65}, "per_doc": [], }, "taxonomy_divergence": { "metric": "js", "matrix": { "tess": {"tess": 0.0, "pero": 0.42, "mistral": 0.18}, "pero": {"tess": 0.42, "pero": 0.0, "mistral": 0.31}, "mistral": {"tess": 0.18, "pero": 0.31, "mistral": 0.0}, }, "max_pair": ["tess", "pero", 0.42], }, } # ────────────────────────────────────────────────────────────────────────── # 1. build_divergence_matrix_html # ────────────────────────────────────────────────────────────────────────── class TestDivergenceMatrixHTML: def test_renders_full_matrix(self, realistic_iea: dict) -> None: html = build_divergence_matrix_html(realistic_iea) assert "divergence-matrix" in html # Trois moteurs en têtes de colonne for name in ("tess", "pero", "mistral"): assert name in html # La diagonale est étiquetée explicitement (pas une valeur) assert "(identité)" in html or "(identity)" in html def test_off_diagonal_values_present(self, realistic_iea: dict) -> None: html = build_divergence_matrix_html(realistic_iea) # Les trois valeurs hors-diagonale doivent apparaître (à l'arrondi près) assert "0.420" in html # tess ↔ pero assert "0.310" in html # pero ↔ mistral assert "0.180" in html # tess ↔ mistral def test_max_pair_labelled(self, realistic_iea: dict) -> None: html = build_divergence_matrix_html(realistic_iea) # Doit annoncer la paire la plus divergente (tess ↔ pero) assert "tess" in html assert "pero" in html # Et la valeur de divergence dans cette annonce assert "0.420" in html def test_empty_when_no_analysis(self) -> None: assert build_divergence_matrix_html(None) == "" assert build_divergence_matrix_html({}) == "" assert build_divergence_matrix_html({"taxonomy_divergence": None}) == "" def test_empty_when_single_engine(self) -> None: # Une matrice à un seul moteur n'a pas de sens iea = { "taxonomy_divergence": { "metric": "js", "matrix": {"only": {"only": 0.0}}, "max_pair": None, } } assert build_divergence_matrix_html(iea) == "" def test_uses_i18n_labels(self, realistic_iea: dict) -> None: labels = { "divergence_caption": "CUSTOM_CAPTION", "divergence_metric_label": "CUSTOM_METRIC", "divergence_max_pair_label": "CUSTOM_MAX_PAIR", "divergence_diagonal_label": "CUSTOM_DIAG", } html = build_divergence_matrix_html(realistic_iea, labels=labels) assert "CUSTOM_CAPTION" in html assert "CUSTOM_METRIC" in html assert "CUSTOM_MAX_PAIR" in html assert "CUSTOM_DIAG" in html # ────────────────────────────────────────────────────────────────────────── # 2. build_oracle_gap_html # ────────────────────────────────────────────────────────────────────────── class TestOracleGapHTML: def test_shows_all_key_numbers(self, realistic_iea: dict) -> None: html = build_oracle_gap_html(realistic_iea) assert "inter-engine-oracle" in html assert "pero" in html # best_engine assert "70.0 %" in html # best_single_recall assert "95.0 %" in html # oracle_recall assert "+25.0 pts" in html # absolute_gap_pct assert "83.0 %" in html # relative_gap_pct assert ">47<" in html # doc_count def test_empty_when_no_complementarity(self) -> None: assert build_oracle_gap_html(None) == "" assert build_oracle_gap_html({}) == "" assert build_oracle_gap_html({"complementarity": None}) == "" def test_uses_i18n_labels(self, realistic_iea: dict) -> None: labels = { "oracle_caption": "CUSTOM_CAP", "oracle_best_engine": "CUSTOM_BEST", "oracle_explanation": "CUSTOM_EXPL", } html = build_oracle_gap_html(realistic_iea, labels=labels) assert "CUSTOM_CAP" in html assert "CUSTOM_BEST" in html assert "CUSTOM_EXPL" in html # ────────────────────────────────────────────────────────────────────────── # 3. Anti-injection HTML # ────────────────────────────────────────────────────────────────────────── class TestAntiInjection: def test_engine_name_with_html_chars_is_escaped(self) -> None: iea = { "taxonomy_divergence": { "metric": "js", "matrix": { "