Picarones / tests /architecture /test_live_test_markers.py
Claude
feat(audit): Phase 3 partielle — câblage des features inachevées (S2, S4, S6)
bd5c812 unverified
Raw
History Blame
3.33 kB
"""Phase 3.5 audit code-quality — les tests dans
``tests/integration/live/`` doivent porter le marker
``@pytest.mark.live`` sur **chacune** de leurs fonctions de test.
Contexte : ``pyproject.toml`` déclare le marker ``live`` comme
« tests d'intégration contre vraie API/binaire (Tesseract,
Anthropic, OpenAI, Mistral) ; exclus par défaut, opt-in via
``pytest -m live`` ». Le filtre ``addopts = '-m "not live and not
network"'`` les déselectionne au runner par défaut.
Si une fonction dans ``tests/integration/live/`` oublie le marker,
elle s'exécute lors du ``pytest tests/`` standard et :
- échoue sur les runners sans la dep cloud → faux échec CI ;
- consomme du quota API (clé en CI = facture surprise) ;
- introduit une dépendance réseau non documentée.
L'agent d'audit avait flaggé ``test_tesseract_live.py`` comme
« skip top-level inconditionnel ». Vérification : le skip est en
fait **conditionnel** (``if shutil.which("tesseract") is None``),
ce qui est légitime — un test live qui peut s'exécuter seulement
si le binaire est présent. Mais le garde-fou ci-dessous évite
qu'une nouvelle fonction de test oublie le marker.
"""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
LIVE_DIR = Path(__file__).resolve().parents[1] / "integration" / "live"
def _test_functions(path: Path) -> list[tuple[str, ast.FunctionDef | ast.AsyncFunctionDef]]:
"""Liste les fonctions ``test_*`` au top-level d'un fichier."""
tree = ast.parse(path.read_text(encoding="utf-8"))
out: list[tuple[str, ast.FunctionDef | ast.AsyncFunctionDef]] = []
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name.startswith("test_"):
out.append((node.name, node))
return out
def _has_live_marker(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
for deco in fn.decorator_list:
# ``@pytest.mark.live`` ou ``@pytest.mark.live(reason=...)``
if isinstance(deco, ast.Attribute) and deco.attr == "live":
return True
if isinstance(deco, ast.Call) and isinstance(deco.func, ast.Attribute) and deco.func.attr == "live":
return True
return False
def _live_test_files() -> list[Path]:
if not LIVE_DIR.exists():
return []
return [
p for p in sorted(LIVE_DIR.glob("test_*.py"))
if p.name != "__init__.py" and p.name != "conftest.py"
]
@pytest.mark.parametrize("path", _live_test_files(), ids=lambda p: p.name)
def test_every_function_in_live_dir_has_live_marker(path: Path) -> None:
"""Chaque ``test_*`` dans ``tests/integration/live/`` porte ``@pytest.mark.live``.
Sinon le test peut s'exécuter en CI standard et casser sur
l'absence de clé API / binaire externe.
"""
missing: list[str] = []
for name, fn in _test_functions(path):
if not _has_live_marker(fn):
missing.append(f" {path.name}:{fn.lineno} :: {name}")
assert not missing, (
f"Fonctions dans {LIVE_DIR.name}/ sans ``@pytest.mark.live`` :\n"
+ "\n".join(missing)
+ "\n\nAjouter ``@pytest.mark.live`` au-dessus de chaque test "
"qui hit une API/un binaire externe — sinon le test "
"s'exécute sans opt-in et peut faire échouer le CI standard."
)