Spaces:
Running
Running
File size: 7,656 Bytes
563a0f0 e45d507 563a0f0 9011070 563a0f0 9011070 563a0f0 9011070 563a0f0 9011070 563a0f0 9011070 563a0f0 9011070 563a0f0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | """Tests Sprint A5 — option ``lazy_images`` du ReportGenerator (M-16).
Vérifie que :
1. Par défaut (``lazy_images=False``), les images restent embarquées
en base64 (rétrocompat — rapport mono-fichier transportable).
2. Avec ``lazy_images=True``, les images sont externalisées dans
``<output_dir>/report-assets/`` et le HTML les référence par URL
relative.
3. Le HTML reste valide et lisible dans les deux modes.
4. La taille du HTML monolithique baisse drastiquement en mode lazy
sur un corpus de plusieurs documents.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from picarones.evaluation.synthetic import generate_sample_benchmark
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@pytest.fixture
def demo_benchmark_with_images(tmp_path: Path):
"""Benchmark démo avec quelques images PNG synthétiques sur disque.
On utilise les fixtures officielles puis on remplace les
``image_path`` par des PNG réels créés à la volée pour que
``_externalize_images_to_dir`` ait de quoi travailler.
"""
from PIL import Image
bench = generate_sample_benchmark(n_docs=3)
# Crée 3 PNG synthétiques minuscules
for i, engine_report in enumerate(bench.engine_reports):
for j, dr in enumerate(engine_report.document_results):
img_path = tmp_path / f"img_{j}.png"
if not img_path.exists():
Image.new("RGB", (200, 100), color=(255, 240, 220)).save(img_path)
dr.image_path = str(img_path)
return bench
# ---------------------------------------------------------------------------
# Mode par défaut (rétrocompat) : images embarquées base64
# ---------------------------------------------------------------------------
def test_default_mode_inlines_images(demo_benchmark_with_images, tmp_path: Path) -> None:
"""``lazy_images=False`` (défaut) : les images vivent en base64
inline dans le HTML, aucun fichier d'asset n'est créé."""
from picarones.reports.html.generator import ReportGenerator
out = tmp_path / "report.html"
gen = ReportGenerator(demo_benchmark_with_images)
path = gen.generate(out)
assert path.exists()
html = path.read_text(encoding="utf-8")
# Rétrocompat : data-URI base64 présent
assert "data:image" in html or "image/png;base64" in html, (
"En mode par défaut, le HTML doit contenir des data-URI base64."
)
# Pas de dossier d'assets externes
assert not (tmp_path / "report-assets").exists(), (
"En mode inline, aucun fichier d'asset ne doit être créé."
)
# ---------------------------------------------------------------------------
# Mode lazy : images externalisées
# ---------------------------------------------------------------------------
def test_lazy_mode_creates_asset_directory(
demo_benchmark_with_images, tmp_path: Path
) -> None:
"""``lazy_images=True`` : ``report-assets/`` est créé à côté du HTML
et contient des fichiers image."""
from picarones.reports.html.generator import ReportGenerator
out = tmp_path / "report.html"
gen = ReportGenerator(demo_benchmark_with_images, lazy_images=True)
path = gen.generate(out)
assert path.exists()
assets_dir = tmp_path / "report-assets"
assert assets_dir.exists() and assets_dir.is_dir()
asset_files = list(assets_dir.iterdir())
assert len(asset_files) >= 1, (
f"Au moins une image doit être externalisée. "
f"Trouvé : {asset_files}"
)
def test_lazy_mode_html_references_relative_urls(
demo_benchmark_with_images, tmp_path: Path
) -> None:
"""En mode lazy, le HTML référence les images via URL relative
``report-assets/...`` plutôt qu'un data-URI."""
from picarones.reports.html.generator import ReportGenerator
out = tmp_path / "report.html"
gen = ReportGenerator(demo_benchmark_with_images, lazy_images=True)
path = gen.generate(out)
html = path.read_text(encoding="utf-8")
assert "report-assets/" in html, (
"Le HTML doit référencer les images via URL relative."
)
# ``loading="lazy"`` doit toujours être présent (le template le pose)
assert 'loading="lazy"' in html
def test_lazy_mode_significantly_reduces_html_size(
demo_benchmark_with_images, tmp_path: Path
) -> None:
"""Le HTML lazy doit être nettement plus petit que le HTML inline.
Sur le corpus démo (3 docs × 200×100 PNG), le ratio doit être
favorable au lazy. Test peu strict (ratio > 1.05) pour ne pas
être flaky en fonction du contenu vendor.
"""
from picarones.reports.html.generator import ReportGenerator
inline_out = tmp_path / "inline.html"
lazy_out = tmp_path / "lazy.html"
ReportGenerator(demo_benchmark_with_images, lazy_images=False).generate(inline_out)
ReportGenerator(demo_benchmark_with_images, lazy_images=True).generate(lazy_out)
inline_size = inline_out.stat().st_size
lazy_size = lazy_out.stat().st_size
assert inline_size > lazy_size, (
f"Le HTML lazy ({lazy_size} B) doit être < HTML inline "
f"({inline_size} B). Diff : {inline_size - lazy_size} B."
)
# ---------------------------------------------------------------------------
# Robustesse
# ---------------------------------------------------------------------------
def test_lazy_mode_with_missing_image_does_not_crash(tmp_path: Path) -> None:
"""Si l'image source n'existe pas, l'externalisation log un warning
et continue (rétrocompat avec ``_encode_image_b64`` qui retourne ''
silencieusement)."""
from picarones.reports.html.generator import ReportGenerator
bench = generate_sample_benchmark(n_docs=2)
# Pointe vers un chemin inexistant
for er in bench.engine_reports:
for dr in er.document_results:
dr.image_path = "/nonexistent/missing.png"
out = tmp_path / "report.html"
# Ne doit PAS lever
path = ReportGenerator(bench, lazy_images=True).generate(out)
assert path.exists()
def test_safe_filename_generation(tmp_path: Path) -> None:
"""Les doc_id contenant des caractères non-FS-safe doivent produire
des noms de fichiers normalisés (pas de path traversal possible)."""
from PIL import Image
from picarones.reports.html.generator import _externalize_images_to_dir
src = tmp_path / "src.png"
Image.new("RGB", (50, 50), color=(0, 0, 0)).save(src)
bench = generate_sample_benchmark(n_docs=1)
bad_id = "../../etc/passwd"
for er in bench.engine_reports:
for dr in er.document_results:
dr.doc_id = bad_id
dr.image_path = str(src)
out_dir = tmp_path / "out"
out_dir.mkdir()
mapping = _externalize_images_to_dir(bench, out_dir)
# Garde-fou de path traversal : aucun fichier ne doit être créé en
# dehors de out_dir/report-assets, **et** le chemin résolu de tout
# fichier d'asset doit rester *à l'intérieur* du dossier d'assets.
forbidden = out_dir.parent / "etc" / "passwd"
assert not forbidden.exists(), "Path traversal détecté !"
assets_dir = (out_dir / "report-assets").resolve()
if mapping:
for url in mapping.values():
assert url.startswith("report-assets/")
# Le chemin résolu doit être contenu dans assets_dir
resolved = (out_dir / url).resolve()
assert str(resolved).startswith(str(assets_dir)), (
f"Path traversal : {resolved} sort de {assets_dir}"
)
|