Picarones / tests /test_sprint8_longitudinal_robustness.py
Claude
chore: remplacer toutes les références bnf/picarones par maribakulj/Picarones
cecde1f unverified
"""Tests Sprint 8 — Suivi longitudinal et analyse de robustesse.
Classes de tests
----------------
TestBenchmarkHistory (15 tests) — base SQLite historique
TestHistoryEntry (6 tests) — structure HistoryEntry
TestRegressionResult (8 tests) — détection de régression
TestGenerateDemoHistory (5 tests) — données fictives longitudinales
TestDegradationLevels (6 tests) — paramètres de dégradation
TestDegradationFunctions (10 tests) — fonctions de dégradation image
TestDegradationCurve (6 tests) — structure DegradationCurve
TestRobustnessReport (8 tests) — rapport de robustesse
TestRobustnessAnalyzer (8 tests) — analyseur statique
TestGenerateDemoRobustness (10 tests) — données fictives robustesse
TestCLIDemo (5 tests) — picarones demo --with-history --with-robustness
"""
from __future__ import annotations
import json
import pytest
# ===========================================================================
# TestBenchmarkHistory
# ===========================================================================
class TestBenchmarkHistory:
@pytest.fixture
def db(self):
from picarones.core.history import BenchmarkHistory
return BenchmarkHistory(":memory:")
def test_import_module(self):
from picarones.core.history import BenchmarkHistory
assert BenchmarkHistory is not None
def test_init_in_memory(self, db):
assert db.db_path == ":memory:"
assert db.count() == 0
def test_record_single(self, db):
db.record_single(
run_id="run001",
corpus_name="Corpus Test",
engine_name="tesseract",
cer_mean=0.12,
wer_mean=0.20,
doc_count=10,
)
assert db.count() == 1
def test_record_single_idempotent(self, db):
db.record_single("run001", "C", "tesseract", 0.12, 0.20, 10)
db.record_single("run001", "C", "tesseract", 0.10, 0.18, 10) # même run_id → REPLACE
assert db.count() == 1
def test_query_returns_entries(self, db):
db.record_single("r1", "C", "tesseract", 0.10, 0.18, 5)
db.record_single("r2", "C", "pero_ocr", 0.07, 0.12, 5)
entries = db.query()
assert len(entries) == 2
def test_query_filter_engine(self, db):
db.record_single("r1", "C", "tesseract", 0.10, 0.18, 5)
db.record_single("r2", "C", "pero_ocr", 0.07, 0.12, 5)
entries = db.query(engine="tesseract")
assert len(entries) == 1
assert entries[0].engine_name == "tesseract"
def test_query_filter_corpus(self, db):
db.record_single("r1", "CorpusA", "tesseract", 0.10, 0.18, 5)
db.record_single("r2", "CorpusB", "tesseract", 0.07, 0.12, 5)
entries = db.query(corpus="CorpusA")
assert len(entries) == 1
assert entries[0].corpus_name == "CorpusA"
def test_query_filter_since(self, db):
db.record_single("r1", "C", "tesseract", 0.12, 0.20, 5, timestamp="2024-01-01T00:00:00+00:00")
db.record_single("r2", "C", "tesseract", 0.10, 0.18, 5, timestamp="2025-06-01T00:00:00+00:00")
entries = db.query(since="2025-01-01")
assert len(entries) == 1
assert "2025" in entries[0].timestamp
def test_list_engines(self, db):
db.record_single("r1", "C", "tesseract", 0.10, 0.18, 5)
db.record_single("r2", "C", "pero_ocr", 0.07, 0.12, 5)
engines = db.list_engines()
assert "tesseract" in engines
assert "pero_ocr" in engines
def test_list_corpora(self, db):
db.record_single("r1", "CorpusA", "tesseract", 0.10, 0.18, 5)
db.record_single("r2", "CorpusB", "pero_ocr", 0.07, 0.12, 5)
corpora = db.list_corpora()
assert "CorpusA" in corpora
assert "CorpusB" in corpora
def test_get_cer_curve(self, db):
db.record_single("r1", "C", "tesseract", 0.15, 0.25, 5, timestamp="2024-01-01T00:00:00+00:00")
db.record_single("r2", "C", "tesseract", 0.12, 0.20, 5, timestamp="2024-06-01T00:00:00+00:00")
db.record_single("r3", "C", "tesseract", 0.10, 0.18, 5, timestamp="2025-01-01T00:00:00+00:00")
curve = db.get_cer_curve("tesseract")
assert len(curve) == 3
assert all("cer" in point for point in curve)
assert all("timestamp" in point for point in curve)
def test_get_cer_curve_filters_engine(self, db):
db.record_single("r1", "C", "tesseract", 0.10, 0.18, 5)
db.record_single("r2", "C", "pero_ocr", 0.07, 0.12, 5)
curve = db.get_cer_curve("tesseract")
assert all(point["cer"] is not None for point in curve)
def test_export_json(self, db, tmp_path):
db.record_single("r1", "C", "tesseract", 0.10, 0.18, 5)
path = db.export_json(str(tmp_path / "history.json"))
assert path.exists()
data = json.loads(path.read_text())
assert data["picarones_history"] is True
assert "runs" in data
assert len(data["runs"]) == 1
def test_record_benchmark_result(self, db):
from picarones.fixtures import generate_sample_benchmark
bm = generate_sample_benchmark(n_docs=3, seed=0)
run_id = db.record(bm)
assert isinstance(run_id, str)
# Autant d'entrées que de moteurs dans le benchmark
assert db.count() == len(bm.engine_reports)
def test_repr(self, db):
r = repr(db)
assert "BenchmarkHistory" in r
assert ":memory:" in r
# ===========================================================================
# TestHistoryEntry
# ===========================================================================
class TestHistoryEntry:
def test_import(self):
from picarones.core.history import HistoryEntry
assert HistoryEntry is not None
def test_cer_percent(self):
from picarones.core.history import HistoryEntry
entry = HistoryEntry(
run_id="r1", timestamp="2025-01-01T00:00:00+00:00",
corpus_name="C", engine_name="tesseract",
cer_mean=0.12, wer_mean=0.20, doc_count=10,
)
assert abs(entry.cer_percent - 12.0) < 0.01
def test_cer_percent_none(self):
from picarones.core.history import HistoryEntry
entry = HistoryEntry("r", "2025", "C", "e", None, None, 0)
assert entry.cer_percent is None
def test_as_dict_keys(self):
from picarones.core.history import HistoryEntry
entry = HistoryEntry("r1", "2025-01-01", "C", "tesseract", 0.10, 0.18, 5)
d = entry.as_dict()
assert "run_id" in d
assert "cer_mean" in d
assert "engine_name" in d
def test_as_dict_metadata(self):
from picarones.core.history import HistoryEntry
entry = HistoryEntry("r1", "2025-01-01", "C", "tesseract", 0.10, 0.18, 5,
metadata={"key": "value"})
d = entry.as_dict()
assert d["metadata"] == {"key": "value"}
def test_query_result_is_history_entry(self):
from picarones.core.history import BenchmarkHistory, HistoryEntry
db = BenchmarkHistory(":memory:")
db.record_single("r1", "C", "tesseract", 0.10, 0.18, 5)
entries = db.query()
assert isinstance(entries[0], HistoryEntry)
# ===========================================================================
# TestRegressionResult
# ===========================================================================
class TestRegressionResult:
@pytest.fixture
def db_with_runs(self):
from picarones.core.history import BenchmarkHistory
db = BenchmarkHistory(":memory:")
db.record_single("r1", "C", "tesseract", 0.12, 0.20, 10, timestamp="2025-01-01T00:00:00+00:00")
db.record_single("r2", "C", "tesseract", 0.15, 0.25, 10, timestamp="2025-06-01T00:00:00+00:00")
return db
def test_detect_regression_is_detected(self, db_with_runs):
result = db_with_runs.detect_regression("tesseract", threshold=0.01)
assert result is not None
assert result.is_regression is True
def test_detect_regression_delta_positive(self, db_with_runs):
result = db_with_runs.detect_regression("tesseract")
assert result.delta_cer > 0
def test_detect_regression_fields(self, db_with_runs):
result = db_with_runs.detect_regression("tesseract")
assert result.engine_name == "tesseract"
assert result.baseline_cer is not None
assert result.current_cer is not None
def test_detect_no_regression(self):
from picarones.core.history import BenchmarkHistory
db = BenchmarkHistory(":memory:")
# CER diminue = amélioration = pas de régression
db.record_single("r1", "C", "tesseract", 0.15, 0.25, 5, timestamp="2025-01-01T00:00:00+00:00")
db.record_single("r2", "C", "tesseract", 0.10, 0.18, 5, timestamp="2025-06-01T00:00:00+00:00")
result = db.detect_regression("tesseract", threshold=0.01)
assert result is not None
assert result.is_regression is False
def test_detect_regression_none_if_single_run(self):
from picarones.core.history import BenchmarkHistory
db = BenchmarkHistory(":memory:")
db.record_single("r1", "C", "tesseract", 0.12, 0.20, 5)
result = db.detect_regression("tesseract")
assert result is None
def test_detect_all_regressions(self):
from picarones.core.history import BenchmarkHistory
db = BenchmarkHistory(":memory:")
db.record_single("r1", "C", "tesseract", 0.10, 0.18, 5, timestamp="2025-01-01T00:00:00+00:00")
db.record_single("r2", "C", "tesseract", 0.20, 0.35, 5, timestamp="2025-06-01T00:00:00+00:00")
regressions = db.detect_all_regressions(threshold=0.01)
assert len(regressions) >= 1
def test_regression_result_as_dict(self, db_with_runs):
result = db_with_runs.detect_regression("tesseract")
d = result.as_dict()
assert "is_regression" in d
assert "delta_cer" in d
assert "engine_name" in d
def test_regression_threshold_respected(self):
from picarones.core.history import BenchmarkHistory
db = BenchmarkHistory(":memory:")
db.record_single("r1", "C", "tesseract", 0.100, 0.18, 5, timestamp="2025-01-01T00:00:00+00:00")
db.record_single("r2", "C", "tesseract", 0.105, 0.19, 5, timestamp="2025-06-01T00:00:00+00:00")
# Delta = 0.5%, threshold = 1% → pas de régression
result = db.detect_regression("tesseract", threshold=0.01)
assert result is not None
assert result.is_regression is False
# Avec threshold = 0.001% → régression
result2 = db.detect_regression("tesseract", threshold=0.001)
assert result2.is_regression is True
# ===========================================================================
# TestGenerateDemoHistory
# ===========================================================================
class TestGenerateDemoHistory:
def test_generate_fills_db(self):
from picarones.core.history import BenchmarkHistory, generate_demo_history
db = BenchmarkHistory(":memory:")
generate_demo_history(db, n_runs=5)
assert db.count() > 0
def test_generate_creates_multiple_engines(self):
from picarones.core.history import BenchmarkHistory, generate_demo_history
db = BenchmarkHistory(":memory:")
generate_demo_history(db, n_runs=4)
engines = db.list_engines()
assert len(engines) >= 2
def test_generate_n_runs(self):
from picarones.core.history import BenchmarkHistory, generate_demo_history
db = BenchmarkHistory(":memory:")
generate_demo_history(db, n_runs=8)
# 8 runs × 3 moteurs = 24 entrées
assert db.count() == 8 * 3
def test_cer_values_in_range(self):
from picarones.core.history import BenchmarkHistory, generate_demo_history
db = BenchmarkHistory(":memory:")
generate_demo_history(db, n_runs=5)
entries = db.query()
for e in entries:
if e.cer_mean is not None:
assert 0.0 <= e.cer_mean <= 1.0
def test_regression_detectable_in_demo(self):
"""La démo inclut une régression simulée au run 5 (tesseract)."""
from picarones.core.history import BenchmarkHistory, generate_demo_history
db = BenchmarkHistory(":memory:")
generate_demo_history(db, n_runs=8, seed=42)
# Vérifier que l'historique a été créé
assert db.count() > 0
# Vérifier que la courbe CER existe pour tesseract
curve = db.get_cer_curve("tesseract")
assert len(curve) > 0
# ===========================================================================
# TestDegradationLevels
# ===========================================================================
class TestDegradationLevels:
def test_import_constants(self):
from picarones.core.robustness import DEGRADATION_LEVELS, ALL_DEGRADATION_TYPES
assert len(DEGRADATION_LEVELS) > 0
assert len(ALL_DEGRADATION_TYPES) > 0
def test_all_types_in_levels(self):
from picarones.core.robustness import DEGRADATION_LEVELS, ALL_DEGRADATION_TYPES
for t in ALL_DEGRADATION_TYPES:
assert t in DEGRADATION_LEVELS
def test_noise_levels(self):
from picarones.core.robustness import DEGRADATION_LEVELS
levels = DEGRADATION_LEVELS["noise"]
assert len(levels) >= 2
assert 0 in levels # niveau original
def test_blur_levels(self):
from picarones.core.robustness import DEGRADATION_LEVELS
levels = DEGRADATION_LEVELS["blur"]
assert 0 in levels
def test_resolution_levels_include_1(self):
from picarones.core.robustness import DEGRADATION_LEVELS
levels = DEGRADATION_LEVELS["resolution"]
assert 1.0 in levels # résolution originale
def test_labels_match_levels(self):
from picarones.core.robustness import DEGRADATION_LEVELS, DEGRADATION_LABELS
for dtype in DEGRADATION_LEVELS:
if dtype in DEGRADATION_LABELS:
assert len(DEGRADATION_LABELS[dtype]) == len(DEGRADATION_LEVELS[dtype])
# ===========================================================================
# TestDegradationFunctions
# ===========================================================================
class TestDegradationFunctions:
def _make_png(self) -> bytes:
"""Génère un PNG minimal valide (10×10 pixels)."""
from picarones.fixtures import _make_placeholder_png
return _make_placeholder_png(40, 30)
def test_degrade_image_bytes_imports(self):
from picarones.core.robustness import degrade_image_bytes
assert callable(degrade_image_bytes)
def test_degrade_noise_returns_bytes(self):
from picarones.core.robustness import degrade_image_bytes
png = self._make_png()
result = degrade_image_bytes(png, "noise", 0)
assert isinstance(result, bytes)
assert len(result) > 0
def test_degrade_blur_returns_bytes(self):
from picarones.core.robustness import degrade_image_bytes
png = self._make_png()
result = degrade_image_bytes(png, "blur", 0)
assert isinstance(result, bytes)
def test_degrade_rotation_returns_bytes(self):
from picarones.core.robustness import degrade_image_bytes
png = self._make_png()
result = degrade_image_bytes(png, "rotation", 0)
assert isinstance(result, bytes)
def test_degrade_resolution_returns_bytes(self):
from picarones.core.robustness import degrade_image_bytes
png = self._make_png()
result = degrade_image_bytes(png, "resolution", 1.0)
assert isinstance(result, bytes)
def test_degrade_binarization_returns_bytes(self):
from picarones.core.robustness import degrade_image_bytes
png = self._make_png()
result = degrade_image_bytes(png, "binarization", 0)
assert isinstance(result, bytes)
def test_degrade_noise_level_5(self):
from picarones.core.robustness import degrade_image_bytes
png = self._make_png()
result = degrade_image_bytes(png, "noise", 5)
assert isinstance(result, bytes)
def test_degrade_blur_level_2(self):
from picarones.core.robustness import degrade_image_bytes
png = self._make_png()
result = degrade_image_bytes(png, "blur", 2)
assert isinstance(result, bytes)
def test_degrade_resolution_half(self):
from picarones.core.robustness import degrade_image_bytes
png = self._make_png()
result = degrade_image_bytes(png, "resolution", 0.5)
assert isinstance(result, bytes)
def test_degrade_rotation_10_degrees(self):
from picarones.core.robustness import degrade_image_bytes
png = self._make_png()
result = degrade_image_bytes(png, "rotation", 10)
assert isinstance(result, bytes)
# ===========================================================================
# TestDegradationCurve
# ===========================================================================
class TestDegradationCurve:
def test_import(self):
from picarones.core.robustness import DegradationCurve
assert DegradationCurve is not None
def test_as_dict_keys(self):
from picarones.core.robustness import DegradationCurve
curve = DegradationCurve(
engine_name="tesseract",
degradation_type="noise",
levels=[0, 5, 15],
labels=["original", "σ=5", "σ=15"],
cer_values=[0.10, 0.15, 0.25],
)
d = curve.as_dict()
assert "engine_name" in d
assert "degradation_type" in d
assert "levels" in d
assert "cer_values" in d
def test_critical_threshold(self):
from picarones.core.robustness import DegradationCurve
curve = DegradationCurve(
engine_name="tesseract",
degradation_type="noise",
levels=[0, 5, 15, 30],
labels=["o", "σ=5", "σ=15", "σ=30"],
cer_values=[0.10, 0.15, 0.22, 0.35],
critical_threshold_level=15,
cer_threshold=0.20,
)
assert curve.critical_threshold_level == 15
def test_none_cer_allowed(self):
from picarones.core.robustness import DegradationCurve
curve = DegradationCurve(
engine_name="e",
degradation_type="blur",
levels=[0, 2],
labels=["o", "r=2"],
cer_values=[None, 0.15],
)
assert curve.cer_values[0] is None
def test_default_cer_threshold(self):
from picarones.core.robustness import DegradationCurve
curve = DegradationCurve("e", "noise", [0], ["o"], [0.1])
assert curve.cer_threshold == 0.20
def test_engine_name_preserved(self):
from picarones.core.robustness import DegradationCurve
curve = DegradationCurve("pero_ocr", "blur", [0, 1], ["o", "r=1"], [0.05, 0.08])
assert curve.engine_name == "pero_ocr"
def test_as_dict_roundtrip(self):
from picarones.core.robustness import DegradationCurve
curve = DegradationCurve(
engine_name="tesseract",
degradation_type="rotation",
levels=[0, 5, 10],
labels=["0°", "5°", "10°"],
cer_values=[0.10, 0.18, 0.30],
critical_threshold_level=10,
)
d = curve.as_dict()
assert d["levels"] == [0, 5, 10]
assert d["cer_values"] == [0.10, 0.18, 0.30]
# ===========================================================================
# TestRobustnessReport
# ===========================================================================
class TestRobustnessReport:
def test_import(self):
from picarones.core.robustness import RobustnessReport
assert RobustnessReport is not None
def test_get_curves_for_engine(self):
from picarones.core.robustness import RobustnessReport, DegradationCurve
c1 = DegradationCurve("tesseract", "noise", [0, 5], ["o", "σ=5"], [0.10, 0.15])
c2 = DegradationCurve("pero_ocr", "noise", [0, 5], ["o", "σ=5"], [0.07, 0.10])
report = RobustnessReport(["tesseract", "pero_ocr"], "C", ["noise"], [c1, c2])
tess_curves = report.get_curves_for_engine("tesseract")
assert len(tess_curves) == 1
assert tess_curves[0].engine_name == "tesseract"
def test_get_curves_for_type(self):
from picarones.core.robustness import RobustnessReport, DegradationCurve
c1 = DegradationCurve("tesseract", "noise", [0, 5], ["o", "σ=5"], [0.10, 0.15])
c2 = DegradationCurve("tesseract", "blur", [0, 2], ["o", "r=2"], [0.10, 0.14])
report = RobustnessReport(["tesseract"], "C", ["noise", "blur"], [c1, c2])
noise_curves = report.get_curves_for_type("noise")
assert len(noise_curves) == 1
assert noise_curves[0].degradation_type == "noise"
def test_as_dict_keys(self):
from picarones.core.robustness import RobustnessReport
report = RobustnessReport(["tesseract"], "C", ["noise"], [])
d = report.as_dict()
assert "engine_names" in d
assert "curves" in d
assert "summary" in d
def test_as_dict_json_serializable(self):
from picarones.core.robustness import RobustnessReport, DegradationCurve
c = DegradationCurve("e", "noise", [0, 5], ["o", "n5"], [0.1, 0.2])
report = RobustnessReport(["e"], "C", ["noise"], [c])
d = report.as_dict()
# Doit être sérialisable en JSON sans erreur
json_str = json.dumps(d)
assert len(json_str) > 0
def test_summary_populated(self):
from picarones.core.robustness import generate_demo_robustness_report
report = generate_demo_robustness_report(engine_names=["tesseract"], seed=1)
assert isinstance(report.summary, dict)
assert len(report.summary) > 0
def test_corpus_name_preserved(self):
from picarones.core.robustness import RobustnessReport
report = RobustnessReport(["e"], "Mon Corpus", ["noise"], [])
assert report.corpus_name == "Mon Corpus"
def test_engine_names_list(self):
from picarones.core.robustness import RobustnessReport
report = RobustnessReport(["tesseract", "pero_ocr"], "C", [], [])
assert "tesseract" in report.engine_names
assert "pero_ocr" in report.engine_names
# ===========================================================================
# TestRobustnessAnalyzer
# ===========================================================================
class TestRobustnessAnalyzer:
def test_import(self):
from picarones.core.robustness import RobustnessAnalyzer
assert RobustnessAnalyzer is not None
def test_init_single_engine(self):
from picarones.core.robustness import RobustnessAnalyzer
mock_engine = type("E", (), {"name": "tesseract"})()
analyzer = RobustnessAnalyzer(mock_engine)
assert len(analyzer.engines) == 1
def test_init_list_engines(self):
from picarones.core.robustness import RobustnessAnalyzer
engines = [
type("E", (), {"name": "tesseract"})(),
type("E", (), {"name": "pero_ocr"})(),
]
analyzer = RobustnessAnalyzer(engines)
assert len(analyzer.engines) == 2
def test_default_degradation_types(self):
from picarones.core.robustness import RobustnessAnalyzer, ALL_DEGRADATION_TYPES
e = type("E", (), {"name": "e"})()
analyzer = RobustnessAnalyzer(e)
assert set(analyzer.degradation_types) == set(ALL_DEGRADATION_TYPES)
def test_custom_degradation_types(self):
from picarones.core.robustness import RobustnessAnalyzer
e = type("E", (), {"name": "e"})()
analyzer = RobustnessAnalyzer(e, degradation_types=["noise", "blur"])
assert analyzer.degradation_types == ["noise", "blur"]
def test_find_critical_level_found(self):
from picarones.core.robustness import RobustnessAnalyzer
levels = [0, 5, 15, 30]
cer_values = [0.10, 0.15, 0.22, 0.35]
critical = RobustnessAnalyzer._find_critical_level(levels, cer_values, 0.20)
assert critical == 15
def test_find_critical_level_none(self):
from picarones.core.robustness import RobustnessAnalyzer
levels = [0, 5, 15]
cer_values = [0.05, 0.10, 0.15]
critical = RobustnessAnalyzer._find_critical_level(levels, cer_values, 0.20)
assert critical is None
def test_build_summary(self):
from picarones.core.robustness import RobustnessAnalyzer, DegradationCurve
curves = [
DegradationCurve("tesseract", "noise", [0, 5], ["o", "n5"], [0.10, 0.20]),
DegradationCurve("pero_ocr", "noise", [0, 5], ["o", "n5"], [0.07, 0.12]),
]
summary = RobustnessAnalyzer._build_summary(curves)
assert "most_robust_noise" in summary
assert summary["most_robust_noise"] == "pero_ocr" # pero_ocr a le CER moyen le plus bas
# ===========================================================================
# TestGenerateDemoRobustness
# ===========================================================================
class TestGenerateDemoRobustness:
def test_import(self):
from picarones.core.robustness import generate_demo_robustness_report
assert callable(generate_demo_robustness_report)
def test_returns_report(self):
from picarones.core.robustness import generate_demo_robustness_report, RobustnessReport
report = generate_demo_robustness_report()
assert isinstance(report, RobustnessReport)
def test_default_engines(self):
from picarones.core.robustness import generate_demo_robustness_report
report = generate_demo_robustness_report()
assert "tesseract" in report.engine_names
assert "pero_ocr" in report.engine_names
def test_custom_engines(self):
from picarones.core.robustness import generate_demo_robustness_report
report = generate_demo_robustness_report(engine_names=["moteur_custom"])
assert "moteur_custom" in report.engine_names
def test_all_degradation_types_present(self):
from picarones.core.robustness import generate_demo_robustness_report, ALL_DEGRADATION_TYPES
report = generate_demo_robustness_report()
types_in_report = {c.degradation_type for c in report.curves}
assert types_in_report == set(ALL_DEGRADATION_TYPES)
def test_cer_values_in_range(self):
from picarones.core.robustness import generate_demo_robustness_report
report = generate_demo_robustness_report(seed=99)
for curve in report.curves:
for cer in curve.cer_values:
if cer is not None:
assert 0.0 <= cer <= 1.0
def test_cer_increases_with_degradation(self):
"""Pour la plupart des types, le CER doit augmenter avec le niveau de dégradation."""
from picarones.core.robustness import generate_demo_robustness_report
report = generate_demo_robustness_report(seed=42)
for curve in report.curves:
valid = [c for c in curve.cer_values if c is not None]
if len(valid) >= 3:
# Au moins le dernier niveau doit être >= le premier
assert valid[-1] >= valid[0], (
f"CER devrait augmenter pour {curve.engine_name}/{curve.degradation_type}: "
f"{valid[0]}{valid[-1]}"
)
def test_reproducible_with_seed(self):
from picarones.core.robustness import generate_demo_robustness_report
r1 = generate_demo_robustness_report(seed=7)
r2 = generate_demo_robustness_report(seed=7)
assert r1.curves[0].cer_values == r2.curves[0].cer_values
def test_summary_contains_most_robust(self):
from picarones.core.robustness import generate_demo_robustness_report
report = generate_demo_robustness_report()
assert any("most_robust" in k for k in report.summary)
def test_json_serializable(self):
from picarones.core.robustness import generate_demo_robustness_report
report = generate_demo_robustness_report()
d = report.as_dict()
json_str = json.dumps(d, ensure_ascii=False)
assert len(json_str) > 0
reparsed = json.loads(json_str)
assert "curves" in reparsed
# ===========================================================================
# TestCLIDemo
# ===========================================================================
class TestCLIDemo:
def test_demo_with_history_flag(self):
from click.testing import CliRunner
from picarones.cli import cli
runner = CliRunner()
result = runner.invoke(cli, ["demo", "--with-history", "--docs", "3"])
assert result.exit_code == 0
assert "longitudinal" in result.output.lower() or "suivi" in result.output.lower() or "CER" in result.output
def test_demo_with_robustness_flag(self):
from click.testing import CliRunner
from picarones.cli import cli
runner = CliRunner()
result = runner.invoke(cli, ["demo", "--with-robustness", "--docs", "3"])
assert result.exit_code == 0
assert "robustesse" in result.output.lower() or "robustness" in result.output.lower() or "bruit" in result.output.lower()
def test_demo_with_both_flags(self):
from click.testing import CliRunner
from picarones.cli import cli
runner = CliRunner()
result = runner.invoke(cli, ["demo", "--with-history", "--with-robustness", "--docs", "3"])
assert result.exit_code == 0
def test_demo_without_flags(self):
from click.testing import CliRunner
from picarones.cli import cli
runner = CliRunner()
result = runner.invoke(cli, ["demo", "--docs", "3"])
assert result.exit_code == 0
def test_demo_generates_html_file(self):
from click.testing import CliRunner
from picarones.cli import cli
import os
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(cli, ["demo", "--docs", "3", "--output", "test_demo.html"])
assert result.exit_code == 0
assert os.path.exists("test_demo.html")