[DTP-Worker] Sprint 8 · Générateur Matrice de régression exhaustive (19 suites · 474 tests · gate méta-niveau) (QA · roadmap L74)

Harnais méta-niveau : agrège l'exécution de toutes les suites gated en une
matrice + verdict PASS/FAIL et fournit le compte agrégé faisant autorité
(N tests verts). Périmètre dérivé du CI (réutilise q4lib/registry.parse_ci ·
zéro duplication) ; anti-invention (#6) : le plan ne contient aucun compteur de
résultat, recomputé à la validation. Enregistré dans l'audit 4Big (18→19
modules · PASS 19/19). run exhaustif : 19/19 suites vertes · 474 tests passés.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-07-30 11:09:08 +00:00
parent cac65386ab
commit 4b0752906d
20 changed files with 1503 additions and 8 deletions
@@ -0,0 +1,107 @@
"""Découverte des suites de tests + preuve de couverture (faits, pas déclaration).
Les suites de la matrice sont DÉRIVÉES du CI Gitea (`parse_ci`), jamais listées à
la main : le périmètre « exhaustif » est donc auto-prouvé — tout job de test
ajouté au CI entre dans la matrice, toute suite retirée en sort. Pour chaque suite
on lit ensuite des FAITS de disque (fichiers `test_*.py`, méthodes `def test_`) —
aucun compteur de résultat n'est inventé ici (#6).
La couverture est PROUVÉE : chaque suite gated doit (a) exister sur disque avec un
répertoire `tests/`, (b) alimenter le job d'agrégat `gate`. Le harnais lui-même
(`self_module`) est exclu — séparation des pouvoirs (ISA 315), et il ne
s'auto-exécute pas (évite la récursion).
"""
from __future__ import annotations
import os
import re
from .deps import DELIVERABLES_ROOT, parse_ci
_TEST_FILE_RE = re.compile(r"^test_.*\.py$")
_TEST_METHOD_RE = re.compile(r"^\s*def\s+(test_\w+)\s*\(")
def _slug(path: str) -> str:
"""Identifiant stable d'une suite depuis son chemin (déterministe)."""
return path.replace("/", ".")
def count_tests(tests_dir: str) -> tuple[int, int]:
"""(nb fichiers test_*.py, nb méthodes def test_*) dans `tests_dir`.
Faits de disque purs — base du recensement exhaustif. Zéro exécution."""
if not os.path.isdir(tests_dir):
return (0, 0)
files = 0
methods = 0
for name in os.listdir(tests_dir):
if not _TEST_FILE_RE.match(name):
continue
files += 1
with open(os.path.join(tests_dir, name), encoding="utf-8") as fh:
for line in fh:
if _TEST_METHOD_RE.match(line):
methods += 1
return (files, methods)
def discover_suites(spec: dict) -> list[dict]:
"""Liste ordonnée des suites gated (hors self), enrichie des faits de disque.
Ordonné par `path` → sortie déterministe et diffable."""
self_module = spec["self_module"]
ci = parse_ci()
job_to_path = ci["job_to_path"]
gate_needs = set(ci["gate_needs"])
path_to_jobs: dict[str, list[str]] = {}
for job, path in job_to_path.items():
path_to_jobs.setdefault(path, []).append(job)
suites: list[dict] = []
for path in sorted(p for p in job_to_path.values() if p != self_module):
jobs = sorted(path_to_jobs[path])
abs_path = os.path.join(DELIVERABLES_ROOT, path)
tests_dir = os.path.join(abs_path, "tests")
files, methods = count_tests(tests_dir)
suites.append({
"id": _slug(path),
"path": path,
"jobs": jobs,
"in_gate": all(j in gate_needs for j in jobs),
"has_tests_dir": os.path.isdir(tests_dir),
"test_files": files,
"test_methods": methods,
})
return suites
def coverage_report(spec: dict, suites: list[dict]) -> dict:
"""Diagnostic de couverture sérialisable. `ok=True` ssi chaque suite gated a
un répertoire `tests/` sur disque ET alimente le gate, et que le harnais
lui-même est bien exclu du périmètre."""
self_module = spec["self_module"]
ci = parse_ci()
ci_paths = set(ci["job_to_path"].values())
missing_tests_dir = sorted(s["path"] for s in suites if not s["has_tests_dir"])
not_in_gate = sorted(s["path"] for s in suites if not s["in_gate"])
self_present = any(s["path"] == self_module for s in suites)
ok = (
not missing_tests_dir
and not not_in_gate
and not self_present
and self_module in ci_paths # le harnais DOIT être gated lui aussi
)
return {
"ok": ok,
"suites_count": len(suites),
"gated_in_ci": len(ci_paths),
"self_module_excluded": self_module,
"self_module_gated": self_module in ci_paths,
"missing_tests_dir": missing_tests_dir,
"not_in_gate": not_in_gate,
}