Files
oto-enterprise-os-dtp/05_deliverables_mvp/qa/acceptance/tests/test_acceptance.py
T

300 lines
12 KiB
Python

"""Tests de la Matrice d'acceptation / traçabilité MVP (stdlib pur · zéro pip).
Couvre : le build réel (couverture bijective vs CI Gitea + registre 4Big), la
conformité au schéma, les 10 familles d'invariants, le déterminisme, et une
batterie d'injections négatives (chaque invariant DOIT rougir quand on le viole).
Les négatifs s'appuient sur une fixture SYNTHÉTIQUE minimale (CI + registre +
spec factices) pour contrôler exactement l'écart injecté, sans dépendre de la
forme du dépôt réel.
"""
from __future__ import annotations
import copy
import os
import sys
import unittest
_HERE = os.path.dirname(os.path.abspath(__file__))
_MOD = os.path.normpath(os.path.join(_HERE, ".."))
sys.path.insert(0, _MOD)
import acceptance_gen as gen # noqa: E402
from acclib import builder, deps # noqa: E402
def _spec() -> dict:
return gen._load(gen._SPEC_PATH)
# --------------------------------------------------------------------------- #
# Fixture synthétique : deux modules gated (a/one=S2, b/two=S3) + la matrice. #
# --------------------------------------------------------------------------- #
_SELF = "qa/acceptance"
def _mini_ci(paths, self_module=_SELF) -> dict:
j2p = {f"{p.replace('/', '-')}-tests": p for p in paths}
j2p["qa-acceptance-tests"] = self_module
return {"job_to_path": j2p, "gate_needs": list(j2p)}
def _mini_audit() -> dict:
return {"a/one": "S2", "b/two": "S3"}
def _mini_spec() -> dict:
return {
"version": "test",
"reference_cadre": "fixture",
"roadmap_ref": "ROADMAP.md",
"self_module": _SELF,
"cible_portage": "recette",
"extra_module_sprint": {},
"sprint_deliverables": [
{"id": "S2", "kind": "sprint_deliverable", "sprint": "S2",
"roadmap_line": 39, "statement": "livrable S2", "status": "in_repo",
"evidence_modules": ["a/one"], "evidence_artifacts": [],
"out_of_scope": []},
{"id": "S3", "kind": "sprint_deliverable", "sprint": "S3",
"roadmap_line": 46, "statement": "livrable S3", "status": "in_repo",
"evidence_modules": ["b/two"], "evidence_artifacts": [],
"out_of_scope": []},
],
"mvp_metrics": [
{"id": "M1", "kind": "mvp_metric", "sprint": None,
"roadmap_line": 81, "statement": "métrique M1", "status": "in_repo",
"evidence_modules": ["a/one"], "evidence_artifacts": [],
"out_of_scope": []},
],
}
def _mini_env():
"""(spec, ci, audit) — la fixture minimale valide."""
return _mini_spec(), _mini_ci(["a/one", "b/two"]), _mini_audit()
def _mini_errors(spec, ci=None, audit=None) -> list:
ci = ci or _mini_ci(["a/one", "b/two"])
audit = audit or _mini_audit()
bundle = builder.build_matrix(spec, ci, audit, deps)
return gen.validate_bundle(bundle, spec, ci, audit)
# --------------------------------------------------------------------------- #
class TestMiniClean(unittest.TestCase):
"""La fixture synthétique doit valider proprement (base des négatifs)."""
def test_mini_validates_clean(self):
self.assertEqual(_mini_errors(*(_mini_env()[:1]),), [],
"la fixture de base doit être verte")
def test_mini_bijective(self):
spec, ci, audit = _mini_env()
b = builder.build_matrix(spec, ci, audit, deps)
cov = b["manifest"]["coverage"]
self.assertTrue(cov["bijective"])
self.assertTrue(cov["partition_ok"])
self.assertTrue(b["verdict"])
# --------------------------------------------------------------------------- #
class TestRealBuild(unittest.TestCase):
"""Build réel contre le CI, le registre 4Big et le spec du dépôt."""
def setUp(self):
self.spec = _spec()
self.ci = deps.parse_ci()
self.audit = deps.audit_sprints()
self.bundle = builder.build_matrix(self.spec, self.ci, self.audit, deps)
def test_build_validates_clean(self):
errs = gen.validate_bundle(self.bundle, self.spec, self.ci, self.audit)
self.assertEqual(errs, [], f"invariants rouges: {errs}")
def test_schema_conformance(self):
schema = gen._load(gen._SCHEMA_PATH)
self.assertEqual(list(deps.validate(self.bundle, schema)), [])
def test_coverage_bijective(self):
cov = self.bundle["manifest"]["coverage"]
self.assertTrue(cov["bijective"])
self.assertEqual(cov["missing_in_matrix"], [])
self.assertEqual(cov["phantom_evidence"], [])
def test_partition_exact_all_sprints(self):
cov = self.bundle["manifest"]["coverage"]
self.assertTrue(cov["partition_ok"])
for p in cov["sprint_partition"]:
self.assertTrue(p["exact"], f"partition {p['sprint']} non exacte")
self.assertEqual(cov["unknown_sprint"], [])
def test_verdict_pass(self):
self.assertTrue(self.bundle["verdict"])
def test_counts_match_roadmap(self):
c = self.bundle["manifest"]["counts"]
self.assertEqual(c["sprint_deliverables"], 8) # S1..S8
self.assertEqual(c["mvp_metrics"], 7) # M1..M7
self.assertEqual(c["gated_modules"], c["cited_modules"])
def test_every_gated_module_cited_once(self):
gated = set(builder.gated_modules(self.ci, self.spec["self_module"]))
cited = set()
for d in builder.all_deliverables(self.spec):
if d["status"] == "in_repo":
cited.update(d["evidence_modules"])
self.assertEqual(cited, gated)
def test_self_module_not_cited(self):
for d in builder.all_deliverables(self.spec):
self.assertNotIn(self.spec["self_module"], d["evidence_modules"])
def test_out_of_scope_all_sourced(self):
for d in builder.all_deliverables(self.spec):
for o in d.get("out_of_scope", []):
self.assertTrue(o.get("source", "").strip(),
f"{d['id']}: hors-périmètre sans source")
def test_evidence_artifacts_exist(self):
for d in builder.all_deliverables(self.spec):
for a in d.get("evidence_artifacts", []):
self.assertTrue(deps.artifact_exists(a), f"{a} absent")
def test_determinism(self):
b2 = builder.build_matrix(self.spec, self.ci, self.audit, deps)
self.assertEqual(self.bundle, b2)
def test_sprint_windows_from_audit_registry(self):
# Toute fenêtre de sprint vient du registre 4Big (sauf l'auditeur, via
# extra) — jamais re-déclarée dans le spec d'acceptation.
ms = builder.module_sprint_map(self.spec, self.audit)
gated = builder.gated_modules(self.ci, self.spec["self_module"])
for m in gated:
self.assertIn(m, ms, f"{m} sans fenêtre de sprint")
def test_audit_4big_declared_via_extra(self):
self.assertIn("qa/audit_4big", self.spec["extra_module_sprint"])
self.assertTrue(
self.spec["extra_module_sprint"]["qa/audit_4big"]["source"].strip())
# --------------------------------------------------------------------------- #
class TestNegativeInjections(unittest.TestCase):
"""Chaque invariant DOIT rougir quand on injecte l'écart correspondant."""
def test_inv2_phantom_module_flagged(self):
spec = _mini_spec()
spec["sprint_deliverables"][0]["evidence_modules"].append("z/ghost")
self.assertTrue(any("non gated" in e or "fantôme" in e
for e in _mini_errors(spec)))
def test_inv3_missing_gated_module(self):
# b/two gated mais retiré de son livrable → couverture non bijective.
spec = _mini_spec()
spec["sprint_deliverables"][1]["evidence_modules"] = []
# laisser un artefact pour ne pas déclencher AUSSI INV7
spec["sprint_deliverables"][1]["evidence_artifacts"] = ["GAP_ANALYSIS_SPRINT1.md"]
errs = _mini_errors(spec)
self.assertTrue(any("non tracés" in e for e in errs), errs)
def test_inv5_partition_wrong_sprint(self):
# Placer b/two (fenêtre S3) dans le livrable S2 → partition fausse.
spec = _mini_spec()
spec["sprint_deliverables"][0]["evidence_modules"] = ["a/one", "b/two"]
spec["sprint_deliverables"][1]["evidence_modules"] = []
spec["sprint_deliverables"][1]["evidence_artifacts"] = ["GAP_ANALYSIS_SPRINT1.md"]
self.assertTrue(any("partition" in e for e in _mini_errors(spec)))
def test_inv4_module_without_sprint(self):
# Registre 4Big ne connaît pas b/two → module gated sans fenêtre.
spec = _mini_spec()
bad_audit = {"a/one": "S2"} # b/two manquant
errs = _mini_errors(spec, audit=bad_audit)
self.assertTrue(any("sans fenêtre" in e for e in errs), errs)
def test_inv4_extra_conflicts_with_registry(self):
spec = _mini_spec()
spec["extra_module_sprint"] = {"a/one": {"sprint": "S9", "source": "x"}}
self.assertTrue(any("conflit" in e for e in _mini_errors(spec)))
def test_inv6_out_of_scope_without_source(self):
spec = _mini_spec()
spec["sprint_deliverables"][0]["out_of_scope"] = [{"item": "x", "source": " "}]
self.assertTrue(any("sans source" in e for e in _mini_errors(spec)))
def test_inv7_in_repo_without_evidence(self):
spec = _mini_spec()
spec["mvp_metrics"][0]["evidence_modules"] = []
self.assertTrue(any("sans aucune preuve" in e for e in _mini_errors(spec)))
def test_inv7_out_of_scope_with_module(self):
spec = _mini_spec()
spec["mvp_metrics"].append({
"id": "M2", "kind": "mvp_metric", "sprint": None, "roadmap_line": 82,
"statement": "m2", "status": "out_of_scope",
"evidence_modules": ["a/one"], "evidence_artifacts": [],
"out_of_scope": [{"item": "x", "source": "s"}]})
self.assertTrue(any("out_of_scope avec preuve" in e for e in _mini_errors(spec)))
def test_inv7_missing_artifact(self):
spec = _mini_spec()
spec["sprint_deliverables"][0]["evidence_artifacts"] = ["does/not/exist.md"]
self.assertTrue(any("inexistant" in e for e in _mini_errors(spec)))
def test_inv8_metric_with_sprint(self):
spec = _mini_spec()
spec["mvp_metrics"][0]["sprint"] = "S4"
self.assertTrue(any("sprint non nul" in e for e in _mini_errors(spec)))
def test_inv8_sprint_deliverable_without_sprint(self):
spec = _mini_spec()
spec["sprint_deliverables"][0]["sprint"] = None
self.assertTrue(any("sans sprint" in e or "sprint" in e
for e in _mini_errors(spec)))
def test_inv9_self_cited(self):
spec = _mini_spec()
# citer la matrice elle-même comme preuve (et la rendre gated)
ci = _mini_ci(["a/one", "b/two"])
ci["job_to_path"]["extra-job"] = _SELF # reste self → exclu du gated
spec["sprint_deliverables"][0]["evidence_modules"] = ["a/one", _SELF]
self.assertTrue(any("elle-même" in e or "non gated" in e
for e in _mini_errors(spec, ci=ci)))
def test_inv1_duplicate_id(self):
spec = _mini_spec()
spec["mvp_metrics"][0]["id"] = "S2" # collision avec un sprint
self.assertTrue(any("double" in e for e in _mini_errors(spec)))
def test_inv3_flags_bijective_false_consistency(self):
# Retirer un module ET son artefact → bijective doit passer à False.
spec = _mini_spec()
spec["sprint_deliverables"][1]["evidence_modules"] = []
spec["sprint_deliverables"][1]["evidence_artifacts"] = ["GAP_ANALYSIS_SPRINT1.md"]
b = builder.build_matrix(spec, _mini_ci(["a/one", "b/two"]), _mini_audit(), deps)
self.assertFalse(b["manifest"]["coverage"]["bijective"])
self.assertFalse(b["verdict"])
# --------------------------------------------------------------------------- #
class TestCLI(unittest.TestCase):
"""Le CLI build/validate produit les artefacts et sort proprement."""
def test_build_writes_outputs(self):
import tempfile
with tempfile.TemporaryDirectory() as tmp:
gen.main(["build", "-o", tmp])
self.assertTrue(os.path.exists(os.path.join(tmp, "acceptance_matrix.json")))
self.assertTrue(os.path.exists(os.path.join(tmp, "MANIFEST.json")))
def test_validate_exits_zero(self):
# cmd_validate ne lève pas sur le dépôt réel (sinon SystemExit).
gen.main(["validate"])
if __name__ == "__main__":
unittest.main()