Picarones / tests /report /test_sprint37_inter_engine_html.py
Claude
test: rรฉorganiser les 110 fichiers tests/test_*.py par cercle architectural
d109222 unverified
Raw
History Blame
12.1 kB
"""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 "<strong>tess</strong>" in html
assert "<strong>pero</strong>" 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": {
"<script>": {"<script>": 0.0, "safe": 0.4},
"safe": {"<script>": 0.4, "safe": 0.0},
},
"max_pair": ["<script>", "safe", 0.4],
}
}
html = build_divergence_matrix_html(iea)
# Le tag <script> ne doit jamais apparaรฎtre brut dans la sortie
assert "<script>" not in html
# Et on doit voir la version รฉchappรฉe
assert "&lt;script&gt;" in html
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 4. Intรฉgration ReportGenerator
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class TestReportIntegration:
def test_section_absent_when_no_inter_engine(self, tmp_path: Path) -> None:
bench = generate_sample_benchmark()
# La fixture ne calcule pas inter_engine_analysis โ†’ None
assert bench.inter_engine_analysis is None
out = tmp_path / "report.html"
ReportGenerator(bench).generate(out)
html = out.read_text(encoding="utf-8")
assert "divergence-matrix" not in html
assert "inter-engine-oracle" not in html
def test_section_present_when_populated(
self, tmp_path: Path, realistic_iea: dict
) -> None:
bench = generate_sample_benchmark()
bench.inter_engine_analysis = realistic_iea
out = tmp_path / "report.html"
ReportGenerator(bench).generate(out)
html = out.read_text(encoding="utf-8")
assert "divergence-matrix" in html
assert "inter-engine-oracle" in html
assert "0.420" in html # divergence tessโ†”pero
assert "95.0 %" in html # oracle recall
def test_french_locale_uses_french_labels(
self, tmp_path: Path, realistic_iea: dict
) -> None:
bench = generate_sample_benchmark()
bench.inter_engine_analysis = realistic_iea
out = tmp_path / "report_fr.html"
ReportGenerator(bench, lang="fr").generate(out)
html = out.read_text(encoding="utf-8")
assert "Divergence taxonomique" in html
assert "Oracle" in html
def test_english_locale_uses_english_labels(
self, tmp_path: Path, realistic_iea: dict
) -> None:
bench = generate_sample_benchmark()
bench.inter_engine_analysis = realistic_iea
out = tmp_path / "report_en.html"
ReportGenerator(bench, lang="en").generate(out)
html = out.read_text(encoding="utf-8")
assert "Taxonomic divergence" in html
assert "Oracle" in html
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 5. i18n FR/EN โ€” clรฉs prรฉsentes dans les deux fichiers
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
REQUIRED_KEYS = (
"h_inter_engine",
"inter_engine_note",
"divergence_caption",
"divergence_metric_label",
"divergence_max_pair_label",
"divergence_diagonal_label",
"oracle_caption",
"oracle_best_engine",
"oracle_best_recall",
"oracle_recall",
"oracle_gap",
"oracle_doc_count",
"oracle_recoverable",
"oracle_explanation",
)
class TestI18NCompleteness:
@pytest.mark.parametrize("lang", ["fr", "en"])
@pytest.mark.parametrize("key", REQUIRED_KEYS)
def test_key_present(self, lang: str, key: str) -> None:
path = (
Path(__file__).parent.parent.parent
/ "picarones"
/ "report"
/ "i18n"
/ f"{lang}.json"
)
data = json.loads(path.read_text(encoding="utf-8"))
assert key in data, f"Clรฉ {key!r} manquante dans {lang}.json"
assert data[key].strip(), f"Clรฉ {key!r} vide dans {lang}.json"