dbde8fac46
Le fix 89eb8e6 (session 053701) avait corrigé regression_gen.py docstring +
README, mais la même contre-vérité subsistait dans 3 surfaces manquées :
· reglib/builder.py:10-13 (docstring module — pointait vers .gitignore comme
autorité alors que le .gitignore affirme l'INVERSE)
· reglib/builder.py:71 (docstring run_matrix)
· regression_gen.py:82-85 (champ hors_perimetre_worker → coule dans MANIFEST.json
commité/gaté ; contredisait le docstring corrigé 12 lignes plus haut)
Ground-truth (4 sources) : git ls-files (commité) · .gitignore (commité+byte-
déterministe) · ci/check_regression.sh (exige byte-identité d'un run frais) ·
docstring regression_gen.py:26 déjà réaligné. MANIFEST.json régénéré (build),
plan + regression_run.json inchangés. Consommateur aval : aucun. run_ci.sh 30
PASS 0 FAIL. Classe prose-facts-vs-numeric-drift / docstring-vs-code, gate-neutre.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
4.1 KiB
Python
110 lines
4.1 KiB
Python
"""Assemblage : plan déterministe (`build`) et matrice live (`run`).
|
|
|
|
Deux artefacts, deux natures :
|
|
|
|
build → PLAN (out/regression_plan.json) : recensement EXHAUSTIF et
|
|
DÉTERMINISTE des suites gated (chemin, jobs, gate, fichiers/méthodes
|
|
test_*). Aucun compteur de résultat → diffable, re-générable, commité.
|
|
Verdict PASS ssi couverture prouvée + chaque suite ≥ planchers.
|
|
|
|
run → MATRICE live : exécute réellement chaque suite et agrège les compteurs
|
|
(ran/passed/failures/errors/skipped) + verdict global. Aucun horodatage /
|
|
hôte / chemin absolu → deux `run` sont byte-identiques : l'artefact
|
|
`regression_run.json` EST commité et byte-gaté par `ci/check_regression.sh`
|
|
(voir .gitignore), c'est la sortie qui fait autorité sur « N tests verts ».
|
|
|
|
Aucune date/horodatage → build reproductible (gate CI stable)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
from . import discovery, runner
|
|
from .deps import DELIVERABLES_ROOT
|
|
|
|
MATRIX_NAME = "OTO QA · Matrice de régression exhaustive"
|
|
|
|
|
|
def build_plan(spec: dict[str, Any]) -> dict[str, Any]:
|
|
"""Plan déterministe des suites gated (sans exécution)."""
|
|
suites = discovery.discover_suites(spec)
|
|
coverage = discovery.coverage_report(spec, suites)
|
|
th = spec["thresholds"]
|
|
min_files = th["min_test_files"]
|
|
min_methods = th["min_methods_per_suite"]
|
|
|
|
under = [
|
|
s["path"] for s in suites
|
|
if s["test_files"] < min_files or s["test_methods"] < min_methods
|
|
]
|
|
totals = {
|
|
"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),
|
|
}
|
|
verdict = "PASS" if (coverage["ok"] and not under) else "FAIL"
|
|
|
|
return {
|
|
"matrix": MATRIX_NAME,
|
|
"version": spec["version"],
|
|
"reference_cadre": spec["reference_cadre"],
|
|
"self_module": spec["self_module"],
|
|
"thresholds": {
|
|
"min_test_files": min_files,
|
|
"min_methods_per_suite": min_methods,
|
|
},
|
|
"discover_cmd": list(spec["discover_cmd"]),
|
|
"coverage": coverage,
|
|
"suites": suites,
|
|
"under_threshold": under,
|
|
"totals": totals,
|
|
"verdict": verdict,
|
|
"notes": spec.get("notes", []),
|
|
}
|
|
|
|
|
|
def run_matrix(spec: dict[str, Any], *, python: str | None = None) -> dict[str, Any]:
|
|
"""Exécute réellement toutes les suites du plan et agrège les compteurs.
|
|
|
|
Byte-déterministe (aucun horodatage/hôte/chemin absolu) → l'artefact
|
|
`regression_run.json` EST commité et byte-gaté par `ci/check_regression.sh`."""
|
|
suites = discovery.discover_suites(spec)
|
|
discover_cmd = list(spec["discover_cmd"])
|
|
# `python3 -m unittest ...` → on retire le binaire, on garde les args.
|
|
cmd_args = discover_cmd[1:] if discover_cmd and discover_cmd[0].startswith("python") \
|
|
else discover_cmd
|
|
|
|
results: list[dict] = []
|
|
for s in suites:
|
|
abs_path = os.path.join(DELIVERABLES_ROOT, s["path"])
|
|
res = runner.run_suite(abs_path, python=python, discover_cmd=cmd_args)
|
|
results.append({
|
|
"id": s["id"],
|
|
"path": s["path"],
|
|
"expected_methods": s["test_methods"],
|
|
**res,
|
|
})
|
|
|
|
totals = {
|
|
"suites": len(results),
|
|
"green": sum(1 for r in results if r["ok"]),
|
|
"red": sum(1 for r in results if not r["ok"]),
|
|
"ran": sum(r["ran"] for r in results),
|
|
"passed": sum(r["passed"] for r in results),
|
|
"failures": sum(r["failures"] for r in results),
|
|
"errors": sum(r["errors"] for r in results),
|
|
"skipped": sum(r["skipped"] for r in results),
|
|
}
|
|
verdict = "PASS" if totals["red"] == 0 and totals["suites"] > 0 else "FAIL"
|
|
return {
|
|
"matrix": MATRIX_NAME,
|
|
"version": spec["version"],
|
|
"self_module": spec["self_module"],
|
|
"suites": results,
|
|
"totals": totals,
|
|
"verdict": verdict,
|
|
}
|