116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
"""Garde-fous du workflow CI Gitea (BUG-082, BUG-083).
|
|
|
|
Sans dépendance (pas de PyYAML) : analyse ligne à ligne de
|
|
`.gitea/workflows/ci.yml`, suffisante pour les conventions de ce fichier.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
CI_YML = Path(__file__).resolve().parent.parent / ".gitea" / "workflows" / "ci.yml"
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def _run_bodies() -> list[tuple[int, str]]:
|
|
"""Toutes les lignes shell de chaque bloc `run:` → [(n° ligne, code)]."""
|
|
lines = CI_YML.read_text(encoding="utf-8").splitlines()
|
|
bodies: list[tuple[int, str]] = []
|
|
i = 0
|
|
while i < len(lines):
|
|
m = re.match(r"^(\s*)run:(?:\s*\|\s*)?$", lines[i])
|
|
inline = re.match(r"^(\s*)run:\s+(\S.*)$", lines[i])
|
|
if m:
|
|
base = len(m.group(1))
|
|
i += 1
|
|
while i < len(lines):
|
|
cur = lines[i]
|
|
if not cur.strip():
|
|
i += 1
|
|
continue
|
|
if len(cur) - len(cur.lstrip()) <= base:
|
|
break
|
|
bodies.append((i + 1, cur.strip()))
|
|
i += 1
|
|
elif inline:
|
|
bodies.append((i + 1, inline.group(2).strip()))
|
|
i += 1
|
|
else:
|
|
i += 1
|
|
return bodies
|
|
|
|
|
|
class TestRunnerProofScripts:
|
|
def test_no_hash_inside_run_bodies(self):
|
|
"""BUG-083 : aucun `#` dans le code shell des `run:`.
|
|
|
|
Le runner Gitea Act tronque naïvement au premier `#` (même entre
|
|
guillemets) : `echo "... see #87)"` devenait une citation non
|
|
fermée → `unexpected EOF while looking for matching '"'` (job
|
|
`security` rouge). Les lignes-commentaires shell (`# ...`) restent
|
|
autorisées : leur troncature est sémantiquement neutre.
|
|
"""
|
|
offenders = [
|
|
f"L{n}: {code}"
|
|
for n, code in _run_bodies()
|
|
if not code.startswith("#") and "#" in code
|
|
]
|
|
assert not offenders, (
|
|
"BUG-083 : `#` interdit dans le code des `run:` "
|
|
f"(tronqué par le runner) :\n" + "\n".join(offenders)
|
|
)
|
|
|
|
|
|
class TestSemgrepStep:
|
|
def test_semgrep_local_rules_enforced(self):
|
|
"""#87 T7 : semgrep bloquant sur règles locales (aucun registre)."""
|
|
text = CI_YML.read_text(encoding="utf-8")
|
|
assert "semgrep --config semgrep-rules/ backend/" in text, (
|
|
"#87 T7 : étape semgrep locale attendue dans le job security"
|
|
)
|
|
rules = REPO_ROOT / "semgrep-rules" / "obsigate-python.yaml"
|
|
assert rules.exists(), "ruleset semgrep manquant"
|
|
|
|
|
|
class TestFrontendStepsHaveTheirDeps:
|
|
@staticmethod
|
|
def _root_step_files() -> list[str]:
|
|
"""Fichiers `node tests/frontend/<f>` de l'étape racine (sans jsdom)."""
|
|
text = CI_YML.read_text(encoding="utf-8")
|
|
root_part = text.split("Frontend JSDOM tests", 1)[0]
|
|
root_steps = root_part.split("Frontend unit tests", 1)[1]
|
|
return re.findall(r"node tests/frontend/(\S+\.mjs)", root_steps)
|
|
|
|
@staticmethod
|
|
def _has_static_jsdom_import(rel: str) -> bool:
|
|
path = REPO_ROOT / "tests" / "frontend" / rel
|
|
return any(
|
|
re.match(r"^\s*import\b.*\bfrom\s+['\"]jsdom['\"]", line)
|
|
or re.match(r"""\brequire\(\s*['"]jsdom['"]\s*\)""", line)
|
|
for line in path.read_text(encoding="utf-8").splitlines()
|
|
)
|
|
|
|
def test_root_step_files_need_no_jsdom(self):
|
|
"""BUG-082 : l'étape racine tourne sans `tests/frontend/node_modules`
|
|
(installé seulement par l'étape JSDOM) : aucun de ses fichiers ne
|
|
doit importer `jsdom` statiquement — sinon `ERR_MODULE_NOT_FOUND`
|
|
et `lint` rouge (cas `upload.test.mjs`, puis `config-ai-keys.test.mjs`).
|
|
"""
|
|
offenders = [f for f in self._root_step_files() if self._has_static_jsdom_import(f)]
|
|
assert not offenders, (
|
|
"BUG-082 : ces fichiers importent `jsdom` mais tournent dans "
|
|
"l'étape racine (sans node_modules) — les déplacer dans l'étape "
|
|
f"JSDOM :\n" + "\n".join(offenders)
|
|
)
|
|
|
|
def test_jsdom_dependent_tests_run_in_jsdom_step(self):
|
|
"""BUG-082 : les suites à import statique `jsdom` tournent bien dans
|
|
l'étape JSDOM (les deux branches)."""
|
|
text = CI_YML.read_text(encoding="utf-8")
|
|
jsdom_part = text.split("Frontend JSDOM tests", 1)[1]
|
|
for suite in ("node upload.test.mjs", "node config-ai-keys.test.mjs"):
|
|
assert jsdom_part.count(suite) >= 2, (
|
|
f"BUG-082 : `{suite}` attendu dans les deux branches de "
|
|
"l'étape JSDOM"
|
|
)
|