"""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, }