Picarones / tests /report /test_views.py
Claude
test: rรฉorganiser les 110 fichiers tests/test_*.py par cercle architectural
d109222 unverified
Raw
History Blame
14.7 kB
"""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.report.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 ""
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@pytest.fixture
def empty_report_data() -> dict:
return {"engines": []}
class TestAdaptiveMasking:
def test_economics_empty_returns_empty(self, empty_report_data):
from picarones.report.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.report.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.report.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.report.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.report.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.report.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.report.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.report.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.report.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.report.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.report.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 "&lt;script" in html or "alert" not in html.lower()
def test_lexical_modernization_optional(self):
from picarones.report.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.report.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.report.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.report.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.report.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 "&lt;script" in html
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 5. Cรขblage gรฉnรฉrator โ†’ vues
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class TestGeneratorWiring:
def test_generator_imports_three_views(self):
"""generator.py doit importer les 3 vues automatiques (economics,
advanced_taxonomy, diagnostics) pour les passer au template."""
from pathlib import Path
gen_src = (
Path(__file__).parent.parent.parent / "picarones" / "report" / "generator.py"
).read_text(encoding="utf-8")
# Les 3 imports doivent รชtre prรฉsents
assert "build_economics_view_html" in gen_src
assert "build_advanced_taxonomy_view_html" in gen_src
assert "build_diagnostics_view_html" in gen_src
# Et les 3 variables doivent รชtre passรฉes au template
assert "economics_view_html=" in gen_src
assert "advanced_taxonomy_view_html=" in gen_src
assert "diagnostics_view_html=" in gen_src
def test_template_uses_three_views(self):
from pathlib import Path
tpl_src = (
Path(__file__).parent.parent.parent
/ "picarones" / "report" / "templates" / "view_analyses.html"
).read_text(encoding="utf-8")
assert "{% if economics_view_html %}" in tpl_src
assert "{% if advanced_taxonomy_view_html %}" in tpl_src
assert "{% if diagnostics_view_html %}" in tpl_src