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
252 lines
11 KiB
Python
252 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Générateur de la matrice de régression exhaustive · Sprint 8 · QA.
|
|
|
|
Roadmap Sprint 8 · QA : « Regression tests exhaustifs ». Harnais de MÉTA-NIVEAU :
|
|
il agrège l'exécution de TOUTES les suites de tests gated du mandat en une matrice
|
|
unique + un verdict PASS/FAIL, et fournit le compte agrégé faisant autorité
|
|
(« N tests verts ») — celui que les rapports citaient jusqu'ici à la main.
|
|
|
|
Le périmètre « exhaustif » est PROUVÉ, pas déclaré : les suites sont DÉRIVÉES de
|
|
`.gitea/workflows/ci.yml` (réutilise `q4lib/registry.parse_ci` · zéro duplication
|
|
· #5), jamais listées à la main. Le harnais s'exclut lui-même (séparation des
|
|
pouvoirs · ISA 315 · évite la récursion).
|
|
|
|
ANTI-INVENTION (#6) : le `build` (plan) ne contient AUCUN compteur de résultat —
|
|
seuls des faits de disque (fichiers/méthodes `test_*`) et de CI (job, gate). Les
|
|
compteurs verts/rouges sont produits UNIQUEMENT par `run`, en parsant la sortie
|
|
réelle de unittest.
|
|
|
|
Sous-commandes :
|
|
build [-o OUT] → écrit regression_plan.json + MANIFEST.json (déterministe)
|
|
validate → (re)génère le plan en mémoire, valide schéma + invariants ;
|
|
sort en erreur si la couverture n'est pas prouvée ou si une
|
|
suite passe sous les planchers structurels.
|
|
run [-o OUT] → exécute réellement toutes les suites, agrège la matrice
|
|
live + verdict ; sort en erreur (code ≠ 0) si une suite est
|
|
rouge. Écrit `regression_run.json` : bien qu'il porte les
|
|
compteurs réellement exécutés, il ne contient AUCUN
|
|
horodatage/hôte/chemin absolu, ET chaque suite tourne sous
|
|
`python -S` (oracle tiers optionnel `jsonschema` neutralisé,
|
|
comme sur le runner pip-less · cf. `reglib/runner.py`) →
|
|
deux `run` sont byte-identiques quel que soit l'environnement,
|
|
donc l'artefact EST commité et byte-gaté par
|
|
`ci/check_regression.sh` (toute dérive = CI rouge).
|
|
|
|
Toutes les sous-commandes productrices sont déterministes (tri stable, aucun
|
|
horodatage) → artefacts diffables + commités + byte-gatés : `regression_plan.json`
|
|
par `ci/check_artifacts.sh`, `regression_run.json` par `ci/check_regression.sh`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, _HERE)
|
|
|
|
from reglib import builder, discovery # noqa: E402
|
|
from reglib.deps import validate as maison_validate # noqa: E402
|
|
|
|
_SPEC_PATH = os.path.join(_HERE, "regression_spec.json")
|
|
_SCHEMA_PATH = os.path.join(_HERE, "regression.schema.json")
|
|
_DEFAULT_OUT = os.path.join(_HERE, "out")
|
|
|
|
|
|
def _eprint(*args) -> None:
|
|
print(*args, file=sys.stderr)
|
|
|
|
|
|
def _load(path: str) -> dict:
|
|
with open(path, encoding="utf-8") as fh:
|
|
return json.load(fh)
|
|
|
|
|
|
def _write_json(path: str, data) -> None:
|
|
with open(path, "w", encoding="utf-8") as fh:
|
|
json.dump(data, fh, ensure_ascii=False, indent=2)
|
|
fh.write("\n")
|
|
|
|
|
|
def _manifest(spec: dict, plan: dict) -> dict:
|
|
return {
|
|
"generated_from": "regression_spec.json",
|
|
"generator": "regression_gen.py",
|
|
"matrix": plan["matrix"],
|
|
"version": plan["version"],
|
|
"verdict": plan["verdict"],
|
|
"suites": plan["totals"]["suites"],
|
|
"test_methods": plan["totals"]["test_methods"],
|
|
"coverage_ok": plan["coverage"]["ok"],
|
|
"artifacts": ["regression_plan.json"],
|
|
"roadmap": "Sprint 8 · QA · Regression tests exhaustifs",
|
|
"hors_perimetre_worker": (
|
|
"Exécution planifiée de `run` sur le runner CI/VPS + publication du "
|
|
"compte agrégé dans le desk ERPNext / le pipeline de release → agent "
|
|
"QA / DevOps (#8) : hors périmètre car planifiée sur le runner/VPS, "
|
|
"non parce que la sortie dériverait — `regression_run.json` est "
|
|
"byte-déterministe · commité · gaté par `ci/check_regression.sh`."
|
|
),
|
|
}
|
|
|
|
|
|
def check_invariants(plan: dict, spec: dict) -> list[str]:
|
|
"""Invariants de cohérence de la matrice. Retourne la liste des violations."""
|
|
errs: list[str] = []
|
|
|
|
# 1. Schéma de sortie.
|
|
schema = _load(_SCHEMA_PATH)
|
|
errs += [f"schema: {e}" for e in maison_validate(plan, schema)]
|
|
|
|
# 2. Anti-invention : le plan ne DOIT contenir aucun compteur de résultat.
|
|
# (verdict vert/rouge, tests passés) — ceux-ci n'existent qu'en mode run.
|
|
forbidden = {"green", "red", "passed", "ran", "failures", "errors"}
|
|
leaked = forbidden & set(plan["totals"].keys())
|
|
if leaked:
|
|
errs.append(f"INV2 compteurs de résultat dans le plan (invention ?) : "
|
|
f"{sorted(leaked)}")
|
|
|
|
# 3. Le harnais ne s'auto-recense pas (séparation des pouvoirs · récursion).
|
|
self_mod = spec["self_module"]
|
|
if any(s["path"] == self_mod for s in plan["suites"]):
|
|
errs.append(f"INV3 harnais {self_mod} présent dans les suites exécutées")
|
|
|
|
# 4. Couverture prouvée (disque + gate + self gated + self exclu).
|
|
cov = plan["coverage"]
|
|
if not cov["ok"]:
|
|
errs.append(
|
|
f"INV4 couverture non prouvée : missing_tests_dir="
|
|
f"{cov['missing_tests_dir']} not_in_gate={cov['not_in_gate']} "
|
|
f"orphan_tests_dirs={cov['orphan_tests_dirs']} "
|
|
f"self_module_gated={cov['self_module_gated']}")
|
|
if cov["self_module_excluded"] != self_mod:
|
|
errs.append("INV4 self_module_excluded incohérent avec le spec")
|
|
|
|
# 5. Recompute des suites depuis le disque (aucun compte figé/fabriqué).
|
|
fresh = discovery.discover_suites(spec)
|
|
if [s["id"] for s in fresh] != [s["id"] for s in plan["suites"]]:
|
|
errs.append("INV5 ensemble/ordre des suites non reproductible")
|
|
fresh_by_id = {s["id"]: s for s in fresh}
|
|
for s in plan["suites"]:
|
|
f = fresh_by_id.get(s["id"])
|
|
if f and (f["test_files"], f["test_methods"]) != (
|
|
s["test_files"], s["test_methods"]):
|
|
errs.append(f"INV5 {s['id']} comptes disque {s['test_files']}/"
|
|
f"{s['test_methods']} ≠ recomputés {f['test_files']}/"
|
|
f"{f['test_methods']} (compte figé ?)")
|
|
|
|
# 6. Planchers structurels : chaque suite ≥ seuils, cohérent avec la liste.
|
|
th = spec["thresholds"]
|
|
under = [s["path"] for s in plan["suites"]
|
|
if s["test_files"] < th["min_test_files"]
|
|
or s["test_methods"] < th["min_methods_per_suite"]]
|
|
if under != plan["under_threshold"]:
|
|
errs.append(f"INV6 under_threshold {plan['under_threshold']} ≠ "
|
|
f"recomputé {under}")
|
|
|
|
# 7. Totaux cohérents (recomputés depuis les suites).
|
|
t = plan["totals"]
|
|
suites = plan["suites"]
|
|
exp = {
|
|
"suites": len(suites),
|
|
"test_files": sum(s["test_files"] for s in suites),
|
|
"test_methods": sum(s["test_methods"] for s in suites),
|
|
"min_methods": min((s["test_methods"] for s in suites), default=0),
|
|
"under_threshold": len(under),
|
|
}
|
|
for k, v in exp.items():
|
|
if t.get(k) != v:
|
|
errs.append(f"INV7 totals[{k}]={t.get(k)} ≠ recomputé {v}")
|
|
|
|
# 8. Verdict global cohérent (GATE : couverture prouvée ET zéro sous-plancher).
|
|
expect = "PASS" if (cov["ok"] and not under) else "FAIL"
|
|
if plan["verdict"] != expect:
|
|
errs.append(f"INV8 verdict {plan['verdict']} ≠ attendu {expect}")
|
|
|
|
return errs
|
|
|
|
|
|
def cmd_build(args: argparse.Namespace) -> int:
|
|
spec = _load(_SPEC_PATH)
|
|
plan = builder.build_plan(spec)
|
|
errs = check_invariants(plan, spec)
|
|
if errs:
|
|
_eprint("❌ Invariants violés — build refusé :")
|
|
for e in errs:
|
|
_eprint(f" · {e}")
|
|
return 1
|
|
out_dir = args.out or _DEFAULT_OUT
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
_write_json(os.path.join(out_dir, "regression_plan.json"), plan)
|
|
_write_json(os.path.join(out_dir, "MANIFEST.json"), _manifest(spec, plan))
|
|
print(f"✅ Plan de régression écrit dans {out_dir}/ — verdict "
|
|
f"{plan['verdict']} · {plan['totals']['suites']} suites · "
|
|
f"{plan['totals']['test_methods']} méthodes test_* recensées.")
|
|
return 0
|
|
|
|
|
|
def cmd_validate(args: argparse.Namespace) -> int:
|
|
spec = _load(_SPEC_PATH)
|
|
plan = builder.build_plan(spec)
|
|
errs = check_invariants(plan, spec)
|
|
if errs:
|
|
_eprint("❌ Validation échouée :")
|
|
for e in errs:
|
|
_eprint(f" · {e}")
|
|
return 1
|
|
if plan["verdict"] != "PASS":
|
|
_eprint(f"❌ Verdict régression = {plan['verdict']} "
|
|
f"(couverture={plan['coverage']['ok']} · "
|
|
f"sous-plancher={plan['under_threshold']})")
|
|
return 1
|
|
print(f"✅ Matrice de régression valide — {plan['totals']['suites']} suites "
|
|
f"gated · {plan['totals']['test_methods']} méthodes recensées · "
|
|
f"couverture prouvée.")
|
|
return 0
|
|
|
|
|
|
def cmd_run(args: argparse.Namespace) -> int:
|
|
spec = _load(_SPEC_PATH)
|
|
print("▶ Exécution exhaustive des suites gated (peut prendre un moment)…",
|
|
file=sys.stderr)
|
|
matrix = builder.run_matrix(spec)
|
|
out_dir = args.out or _DEFAULT_OUT
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
_write_json(os.path.join(out_dir, "regression_run.json"), matrix)
|
|
t = matrix["totals"]
|
|
for r in matrix["suites"]:
|
|
mark = "✅" if r["ok"] else "❌"
|
|
print(f" {mark} {r['path']:32s} ran={r['ran']:3d} "
|
|
f"fail={r['failures']} err={r['errors']} skip={r['skipped']}")
|
|
print(f"\n{'✅' if matrix['verdict'] == 'PASS' else '❌'} Régression "
|
|
f"{matrix['verdict']} — {t['green']}/{t['suites']} suites vertes · "
|
|
f"{t['passed']} tests passés · {t['failures']} échecs · "
|
|
f"{t['errors']} erreurs.")
|
|
return 0 if matrix["verdict"] == "PASS" else 1
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
ap = argparse.ArgumentParser(description="Matrice de régression exhaustive "
|
|
"(Sprint 8 · QA).")
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
b = sub.add_parser("build", help="écrit le plan déterministe + MANIFEST")
|
|
b.add_argument("-o", "--out", help="répertoire de sortie")
|
|
b.set_defaults(func=cmd_build)
|
|
v = sub.add_parser("validate", help="valide schéma + invariants (gate)")
|
|
v.set_defaults(func=cmd_validate)
|
|
r = sub.add_parser("run", help="exécute réellement toutes les suites (live)")
|
|
r.add_argument("-o", "--out", help="répertoire de sortie")
|
|
r.set_defaults(func=cmd_run)
|
|
return ap
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
return args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|