Spaces:
Sleeping
Sleeping
File size: 9,877 Bytes
2f951ac | 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 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | """Sprint S8.7 β couverture finale des petits fichiers pour
faire passer Codecov patch coverage > 95%.
Cibles :
- ``_workflows._validate_cer_threshold`` (CLI callback validation).
- ``module_policy._is_module_subclass`` AttributeError fallback +
introspection inputs/outputs failure.
- ``history`` router : ``query`` et ``detect_regression`` qui
lΓ¨vent β warning + continue dΓ©gradΓ©.
- ``security.validate_image_safe`` branche ``except Exception``
générique (lignes 239-242) sur erreur Pillow hétérogène.
"""
from __future__ import annotations
import click
import pytest
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# CLI _validate_cer_threshold β validation callback
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestValidateCERThresholdCallback:
"""``--fail-if-cer-above`` doit Γͺtre en fraction β [0, 1].
Avant le fix, l'ancienne sΓ©mantique acceptait des pourcentages
(15.0 = 15 %) ; on Γ©choue maintenant bruyamment sur les
valeurs > 1 pour empΓͺcher la mauvaise interprΓ©tation."""
def _run_callback(self, value):
from picarones.interfaces.cli._workflows import (
_validate_cer_threshold,
)
return _validate_cer_threshold(ctx=None, param=None, value=value)
def test_none_passes_through(self) -> None:
assert self._run_callback(None) is None
def test_valid_fraction_returned(self) -> None:
assert self._run_callback(0.15) == 0.15
def test_zero_accepted(self) -> None:
assert self._run_callback(0.0) == 0.0
def test_one_accepted_at_boundary(self) -> None:
assert self._run_callback(1.0) == 1.0
def test_negative_value_rejected(self) -> None:
with pytest.raises(click.BadParameter, match=">= 0|β₯ 0"):
self._run_callback(-0.1)
def test_legacy_percent_value_rejected(self) -> None:
"""Valeur > 1 (ancienne sΓ©mantique pourcentage) doit lever
avec un message qui explique la migration."""
with pytest.raises(click.BadParameter) as exc_info:
self._run_callback(15.0)
msg = str(exc_info.value)
assert "fraction" in msg
assert "15.0" in msg or "0.15" in msg
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# module_policy β defensive paths
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestModulePolicyDefensive:
def test_is_base_module_no_mro_returns_false(self) -> None:
"""Couvre lignes 220-221 β un objet sans ``__mro__``
accessible doit retourner False sans planter."""
from picarones.evaluation.metrics.module_policy import (
_is_base_module,
)
class TrulyBroken:
@property
def __mro__(self): # type: ignore[override]
raise AttributeError("simulated absent __mro__")
# ``_is_base_module`` accΓ¨de Γ ``cls.__mro__`` directement β
# on doit lui passer une instance dont l'accès lève.
result = _is_base_module(TrulyBroken())
assert result is False
def test_audit_module_introspection_failure_falls_back(
self, caplog,
) -> None:
"""Couvre lignes 284-292 β si l'accΓ¨s Γ
``output_types`` lève (manifest custom property qui plante),
``audit_module`` retombe sur listes vides + log debug."""
from picarones.evaluation.metrics.module_policy import (
ModuleManifest, audit_module,
)
class BadManifestModule:
input_types = "this-should-be-iterable-but-isnt-an-iterable-of-types"
@property
def output_types(self):
# ``getattr(cls, "output_types", None)`` cΓ΄tΓ© audit
# accΓ¨de au descriptor β property.__get__ avec cls=None
# ne lève pas, mais l'itération ``for t in attr_out``
# plus tard plantera (str pas itΓ©rable de types).
raise RuntimeError("manifest cassΓ© simulΓ©")
manifest = ModuleManifest(
name="bad", version="1.0", author="t", license="MIT",
description="test bad",
input_types=[], output_types=[],
)
with caplog.at_level("DEBUG"):
result = audit_module(BadManifestModule, manifest)
# ``audit_module`` retourne un ``AuditResult`` mΓͺme avec un
# manifest cassΓ© β c'est tout l'intΓ©rΓͺt de la dΓ©fense.
assert result is not None
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# history router β dΓ©gradation gracieuse
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestHistoryRouterDegraded:
def _app(self):
from fastapi import FastAPI
from picarones.interfaces.web.routers import history as h
app = FastAPI()
app.include_router(h.router)
return app
def test_query_failure_returns_empty_targets_with_warning(
self, monkeypatch, caplog,
) -> None:
"""Quand ``BenchmarkHistory.query`` lève (DB corrompue,
schΓ©ma migrΓ©), on log un warning et on retourne une liste
vide de rΓ©gressions plutΓ΄t que de planter en 500. Couvre
lignes 52-56."""
from fastapi.testclient import TestClient
from picarones.evaluation.metrics import history as eval_history
# Mock BenchmarkHistory.query pour lever.
def raising_query(*args, **kwargs):
raise RuntimeError("DB schema mismatch simulΓ©")
monkeypatch.setattr(
eval_history.BenchmarkHistory, "query", raising_query,
)
app = self._app()
with caplog.at_level("WARNING"):
with TestClient(app) as client:
r = client.get("/api/history/regressions")
assert r.status_code == 200, r.text
# Sans moteur explicite + query qui plante β liste vide.
assert r.json()["regressions"] == []
# Warning Γ©mis.
assert any(
"Γ©numΓ©ration" in rec.message.lower() or "moteurs" in rec.message.lower()
for rec in caplog.records
)
def test_detect_regression_failure_continues_to_next_engine(
self, monkeypatch, caplog,
) -> None:
"""Quand ``detect_regression`` lève pour un moteur, on log
un warning et on continue avec les suivants. Couvre
lignes 62-66."""
from fastapi.testclient import TestClient
from picarones.evaluation.metrics import history as eval_history
def raising_detect(self, *, engine, threshold):
raise RuntimeError(f"detect_regression KO pour {engine}")
monkeypatch.setattr(
eval_history.BenchmarkHistory,
"detect_regression",
raising_detect,
)
app = self._app()
with caplog.at_level("WARNING"):
with TestClient(app) as client:
r = client.get(
"/api/history/regressions",
params={"engine": "tesseract"},
)
assert r.status_code == 200, r.text
# detect a plantΓ© β pas de rΓ©sultat dans la liste.
assert r.json()["regressions"] == []
assert any(
"detect_regression" in rec.message or "tesseract" in rec.message
for rec in caplog.records
)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# security.validate_image_safe β branche Exception gΓ©nΓ©rique
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestValidateImageGenericException:
"""Pillow lève un panel d'exceptions hétérogènes (SyntaxError
sur GIF malformΓ©, OSError sur TIFF corrompu, AttributeError
interne, etc.) β toutes doivent Γͺtre transformΓ©es en
``ValueError`` propre via la branche ``except Exception``.
Couvre lignes 239-242."""
def test_generic_pillow_failure_wrapped_in_value_error(
self, monkeypatch,
) -> None:
from PIL import Image
from picarones.interfaces.web.security import validate_image_safe
# Mock Image.open pour retourner un objet dont ``verify()``
# lève une OSError (typique TIFF corrompu).
class FakeImg:
def __enter__(self):
return self
def __exit__(self, *args):
pass
def verify(self):
raise OSError("simulated corrupt TIFF")
monkeypatch.setattr(Image, "open", lambda *args, **kwargs: FakeImg())
with pytest.raises(ValueError, match="OSError|erreur de dΓ©codage"):
validate_image_safe(b"any-bytes", filename="corrupt.tiff")
|