9d30939db9
CI / Contraintes NON-NÉGOCIABLES (CLAUDE.md) (push) Has been cancelled
CI / Validation JSON (schémas Faisabilité) (push) Has been cancelled
CI / Qualité documentaire (liens + 4Big) (push) Has been cancelled
CI / Reproductibilité des artefacts out/ (build == commité) (push) Has been cancelled
CI / Fraîcheur matrice de régression (run == commité) (push) Has been cancelled
CI / Intégrité du câblage CI (gate agrège tout · gates statiques verrouillés) (push) Has been cancelled
CI / Intégrité des chiffres du README (valeur == artefact cité · (push) Has been cancelled
CI / Intégrité mobile-build.yml (gating portable · activation différée · (push) Has been cancelled
CI / Publiciste · parser + schéma + generator (unittest) (push) Has been cancelled
CI / RBAC · 50 rôles + schéma (unittest) (push) Has been cancelled
CI / Faisabilité · générateur 4 volets + round-trip (unittest) (push) Has been cancelled
CI / RBAC · fixtures ERPNext (Role + Custom DocPerm) (push) Has been cancelled
CI / RBAC · plan User Permission (row-level) (push) Has been cancelled
CI / RBAC · Role Profile (bundles par portail) (push) Has been cancelled
CI / RBAC · run-book d'application unifié (agrégat 3 volets) (push) Has been cancelled
CI / Faisabilité · dossier bancable trilingue FR/EN/ES (push) Has been cancelled
CI / CRM · workflow vente ERPNext (lead → CONFOTUR) (push) Has been cancelled
CI / CRM · DocType porteur OTO Dossier Vente (push) Has been cancelled
CI / CRM · barème commissions vendeurs (push) Has been cancelled
CI / CRM · Financement Bancaire (gate hypothécaire RD) (push) Has been cancelled
CI / Fiscal · e-CF DGII (Compupar) (push) Has been cancelled
CI / Frontend · Workspaces 5 portails rôle (push) Has been cancelled
CI / Legal · DocType CONFOTUR Application (push) Has been cancelled
CI / QA · Audit 5D conformité (push) Has been cancelled
CI / SEO · mots-clés trilingues + schema.org + hreflang (push) Has been cancelled
CI / Chat OTOIA · montage par portail (Custom Block) (push) Has been cancelled
CI / QA · Audit 4Big (95+/100 sur 100% deliverables) (push) Has been cancelled
CI / Démo · Scénarios (run-sheet P07 banquier / P05 client) (push) Has been cancelled
CI / QA · Matrice de régression exhaustive (Sprint 8) (push) Has been cancelled
CI / DevOps · Run-book de déploiement VPS unifié (Sprint 8) (push) Has been cancelled
CI / QA · Matrice d'acceptation / traçabilité MVP (Sprint 8) (push) Has been cancelled
CI / Mobile · config app Expo/EAS (navigation par rôle) (push) Has been cancelled
CI / PIE · manifest de dépendances (Annexe 12 · V10.1) (push) Has been cancelled
CI / E2E baseline Playwright (manuel) (push) Has been cancelled
Mobile Build (EAS) / Préflight config EAS + état secrets (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
Mobile Build (EAS) / EAS build iOS (App Store (push) Has been cancelled
Mobile Build (EAS) / EAS build Android (Play Store) (push) Has been cancelled
110 lines
4.8 KiB
Python
110 lines
4.8 KiB
Python
"""Exécution réelle des suites + parseur PUR de la sortie unittest.
|
|
|
|
C'est le cœur de la régression EXÉCUTÉE (mode `run`) : on lance chaque suite dans
|
|
un sous-processus isolé (`python -m unittest discover -s tests`, exactement comme
|
|
le CI Gitea), on parse sa sortie réelle, et on agrège les compteurs. Aucun chiffre
|
|
n'est fabriqué — tout provient de la sortie de unittest (#6 anti-invention).
|
|
|
|
Isolation par subprocess (et non import in-process) : les modules du mandat ont
|
|
des fichiers `tests/test_*.py` aux noms qui se recouvrent → un discover global
|
|
in-process provoquerait des collisions de modules. Le subprocess reproduit
|
|
fidèlement l'environnement du CI.
|
|
|
|
Déterminisme cross-environnement (`-S`) : chaque suite est lancée avec l'option
|
|
`-S` (interpréteur SANS `site` → aucun `site-packages`/`dist-packages` sur
|
|
`sys.path`). C'est le contrat exact du runner Gitea (`.gitea/workflows/ci.yml` :
|
|
« stdlib pur, zéro pip » — « L'oracle jsonschema est utilisé s'il est présent,
|
|
sinon ignoré »). Sans `-S`, le seul paquet tiers du corpus — l'oracle OPTIONNEL
|
|
`jsonschema`, toujours gardé par `skipUnless`/`try…except ImportError` — ferait
|
|
diverger la matrice : présent sur un poste dev (`skipped=0`), absent sur le
|
|
runner pip-less (`skipped=N`). L'artefact `regression_run.json` commité — que
|
|
`ci/check_regression.sh` exige byte-identique au run frais — devenait alors
|
|
un FAUX-VERT : vert en local, ROUGE sur le runner. `-S` neutralise l'oracle
|
|
partout → la matrice reflète la baseline garantie (stdlib + validateurs maison)
|
|
et redevient byte-identique quel que soit ce qui est `pip install`é localement.
|
|
|
|
`parse_unittest_output` est PUR (str → dict) donc testable sans exécuter quoi que
|
|
ce soit — on lui injecte des sorties unittest synthétiques (OK / FAILED / skipped).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
_RAN_RE = None # compilé paresseusement pour rester importable sans `re` en tête
|
|
|
|
|
|
def _res():
|
|
import re
|
|
return (
|
|
re.compile(r"Ran (\d+) tests? in"),
|
|
re.compile(r"failures=(\d+)"),
|
|
re.compile(r"errors=(\d+)"),
|
|
re.compile(r"skipped=(\d+)"),
|
|
)
|
|
|
|
|
|
def parse_unittest_output(text: str, returncode: int) -> dict:
|
|
"""Extrait les compteurs de la sortie texte de unittest.
|
|
|
|
unittest écrit son résumé sur stderr :
|
|
« Ran 12 tests in 0.03s » puis « OK » | « OK (skipped=2) »
|
|
| « FAILED (failures=1, errors=2, skipped=3) ».
|
|
`ok` reflète le code de retour du processus (source de vérité), pas un parse
|
|
fragile du mot « OK »."""
|
|
ran_re, fail_re, err_re, skip_re = _res()
|
|
ran_m = ran_re.search(text)
|
|
fail_m = fail_re.search(text)
|
|
err_m = err_re.search(text)
|
|
skip_m = skip_re.search(text)
|
|
ran = int(ran_m.group(1)) if ran_m else 0
|
|
failures = int(fail_m.group(1)) if fail_m else 0
|
|
errors = int(err_m.group(1)) if err_m else 0
|
|
skipped = int(skip_m.group(1)) if skip_m else 0
|
|
passed = max(ran - failures - errors - skipped, 0)
|
|
return {
|
|
"ran": ran,
|
|
"passed": passed,
|
|
"failures": failures,
|
|
"errors": errors,
|
|
"skipped": skipped,
|
|
"ok": returncode == 0 and ran > 0,
|
|
"returncode": returncode,
|
|
}
|
|
|
|
|
|
def run_suite(abs_path: str, *, python: str | None = None,
|
|
discover_cmd: list[str] | None = None,
|
|
timeout: int = 600) -> dict:
|
|
"""Exécute une suite dans son propre répertoire et renvoie ses compteurs.
|
|
|
|
`abs_path` = répertoire du module (contenant `tests/`). Aucune sortie n'est
|
|
imprimée ici — l'agrégateur décide de l'affichage."""
|
|
py = python or sys.executable
|
|
# `-S` : interpréteur sans `site` → pas de `site-packages` → l'oracle tiers
|
|
# optionnel `jsonschema` est absent, exactement comme sur le runner Gitea
|
|
# pip-less. Garantit une matrice byte-déterministe indépendante de ce qui est
|
|
# `pip install`é en local (cf. docstring du module).
|
|
cmd = ["-S", *list(discover_cmd or ["-m", "unittest", "discover", "-s", "tests"])]
|
|
tests_dir = os.path.join(abs_path, "tests")
|
|
if not os.path.isdir(tests_dir):
|
|
return {
|
|
"ran": 0, "passed": 0, "failures": 0, "errors": 0, "skipped": 0,
|
|
"ok": False, "returncode": -1, "note": "tests/ absent",
|
|
}
|
|
try:
|
|
proc = subprocess.run(
|
|
[py, *cmd], cwd=abs_path, capture_output=True, text=True,
|
|
timeout=timeout,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return {
|
|
"ran": 0, "passed": 0, "failures": 0, "errors": 0, "skipped": 0,
|
|
"ok": False, "returncode": -2, "note": f"timeout {timeout}s",
|
|
}
|
|
# unittest écrit son résumé sur stderr ; on garde stdout en secours.
|
|
return parse_unittest_output((proc.stderr or "") + (proc.stdout or ""),
|
|
proc.returncode)
|