Spaces:
Running
Running
File size: 5,103 Bytes
da31b89 | 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 | """Phase 4.4 audit code-quality — interdit les ``pytest.skip("dep
non installée")`` sur des dépendances déclarées **obligatoires**
dans ``pyproject.toml``.
Pattern zombie typique :
.. code-block:: python
try:
import click
except ImportError:
pytest.skip("click non installé")
Si ``click`` est dans ``[project.dependencies]`` (pas dans
``[project.optional-dependencies]``), cet ``ImportError`` ne peut
jamais se déclencher → le skip est vacuement vrai et le test
n'est jamais exécuté. L'audit code-quality (2026-05) en a trouvé
**7 occurrences** dans ``tests/integration/test_chantier{4,5}.py``,
toutes sur ``click``.
Ce test scanne ``tests/`` à la recherche de skips qui mentionnent
une dep obligatoire et échoue avec un message clair indiquant
quel test transformer en exécution franche.
"""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
TESTS_DIR = REPO_ROOT / "tests"
#: Liste de noms de packages déclarés en dep obligatoire
#: ``[project.dependencies]``. Source de vérité :
#: ``pyproject.toml``. À synchroniser si la liste évolue (rare
#: — les deps obligatoires sont stables par construction).
MANDATORY_DEPS: frozenset[str] = frozenset({
"click",
"pydantic",
"fastapi",
"uvicorn",
"lxml",
"defusedxml",
"rapidfuzz",
"jiwer",
"numpy",
"pyyaml",
"annotated_types",
"typing_extensions",
})
#: ``pytest.skip("<package> non installé")`` ou variantes. Capture
#: le nom du package à l'intérieur de la chaîne pour le rapporter.
_SKIP_RE = re.compile(
r"pytest\.skip\s*\(\s*[fr]?[\"']([^\"']*?)\b"
r"(?P<pkg>[a-zA-Z_][\w\-]*)\b[^\"']*?non installé",
re.IGNORECASE,
)
def _scan_zombie_skips() -> list[tuple[Path, int, str]]:
"""Scan AST plutôt que regex pour ignorer commentaires et docstrings."""
import ast
findings: list[tuple[Path, int, str]] = []
for path in sorted(TESTS_DIR.rglob("test_*.py")):
# On ignore ce test lui-même (sinon il se signale).
if path == Path(__file__):
continue
try:
tree = ast.parse(path.read_text(encoding="utf-8"))
except SyntaxError:
continue
for node in ast.walk(tree):
# Cherche les appels ``pytest.skip("...")``.
if not isinstance(node, ast.Call):
continue
func = node.func
is_pytest_skip = (
isinstance(func, ast.Attribute)
and func.attr == "skip"
and isinstance(func.value, ast.Name)
and func.value.id == "pytest"
)
if not is_pytest_skip or not node.args:
continue
first = node.args[0]
if not isinstance(first, ast.Constant) or not isinstance(first.value, str):
continue
msg = first.value
m = _SKIP_RE.search(f'pytest.skip("{msg}")')
if not m:
continue
pkg = m.group("pkg").lower()
if pkg in MANDATORY_DEPS:
findings.append((path, node.lineno, pkg))
return findings
def test_no_skip_on_mandatory_dependency() -> None:
"""Aucun ``pytest.skip("<dep> non installé")`` ne doit cibler
une dep obligatoire.
Si une dep apparaît dans le scan, deux options :
1. **Recommandée** — la dep est vraiment obligatoire : retirer
le ``try/except ImportError`` et faire un ``import`` direct.
Le test plantera franchement si l'environnement est cassé,
ce qui est le comportement correct (signal opérationnel).
2. **Exceptionnelle** — la dep est en fait optionnelle (a déménagé
vers ``[project.optional-dependencies]``) : retirer le nom
de :data:`MANDATORY_DEPS` ci-dessus.
"""
zombies = _scan_zombie_skips()
if zombies:
lines = "\n".join(
f" {p.relative_to(REPO_ROOT)}:{ln} → skip '{pkg} non installé'"
for p, ln, pkg in zombies
)
raise AssertionError(
"Skips zombies détectés (dep obligatoire = ImportError "
"impossible) :\n" + lines
+ "\n\nRemplacer le ``try/except ImportError → pytest.skip`` "
"par un import direct, ou retirer la dep de MANDATORY_DEPS "
"si elle est devenue optionnelle."
)
def test_scanner_catches_obvious_zombie_pattern(tmp_path: Path) -> None:
"""Méta-test : le scanner détecte effectivement le pattern.
Garde-fou contre un regex trop laxiste qui passerait à côté.
"""
sample = tmp_path / "test_sample.py"
sample.write_text(
"import pytest\n"
"\n"
"def test_x():\n"
" try:\n"
" import click\n"
" except ImportError:\n"
" pytest.skip('click non installé')\n",
encoding="utf-8",
)
matches = list(_SKIP_RE.finditer(sample.read_text(encoding="utf-8")))
assert len(matches) == 1
assert matches[0].group("pkg").lower() == "click"
|