[DTP-Worker] Sprint 8 · buffer L75 · Intégrité matrice régression : fix gate check_regression cassé en CI (run baseline commité) + invariant disque→CI (INV4 orphan_tests_dirs)
Défaut #1 (bug CI réel) : ci/check_regression.sh exigeait regression_run.json commité comme baseline, mais le fichier était .gitignore → absent en checkout propre → le job Gitea check-regression échouait sur CHAQUE run CI (ne passait qu'en local via un fichier non suivi). L'artefact run est byte-déterministe (aucun horodatage/hôte/durée · path relatif) → committer est licite. Fix : run désormais commité (baseline), .gitignore + README réécrits. Défaut #2 (invariant manquant) : coverage_report prouvait CI→disque mais pas l'inverse. Nouveau disk_test_modules() + orphan_tests_dirs dans INV4 : un module gagnant un tests/ sans job CI fait chuter la couverture (fini la sous-comptée silencieuse de la matrice). Schéma + 2 tests de morsure. Consommateur régénéré : qa/audit_4big/quality_report.json (README 3710→4236 o, 24→26 méthodes). Matrice 534/21/PASS inchangée (harnais self-exclu, INV3). 5 gates verts · 26+34 tests OK · YAML valide.
This commit is contained in:
@@ -1,60 +1,76 @@
|
|||||||
# Activity Log · 2026-07-31 · Claude Code DTP
|
# Activity Log · 2026-07-31 · Claude Code DTP
|
||||||
|
|
||||||
## Session `20260730_235919` · Buffer S8 · Gate fraîcheur matrice de régression (5ᵉ gate CI)
|
## Session `20260731_002924` · Buffer S8 · Intégrité de la matrice de régression (bug CI réel + invariant disque→CI)
|
||||||
|
|
||||||
**Tâche** : **Sprint 8 · buffer L75 (« Regression tests exhaustifs » · DevOps
|
**Tâche** : **Sprint 8 · buffer L75 (« Regression tests exhaustifs » · DevOps
|
||||||
CI/CD)** — roadmap fonctionnellement close (21/21 modules gated à 100/100, 534
|
CI/CD)** — roadmap fonctionnellement close (21/21 modules gated à 100/100, 534
|
||||||
tests verts, chantier cross-linking clos). Recherche d'une **incohérence de
|
tests verts, 5 gates statiques). Recherche d'une **incohérence de reproductibilité
|
||||||
reproductibilité réelle** plutôt qu'un correctif doc cosmétique.
|
réelle** plutôt qu'un correctif doc cosmétique. **Deux** défauts réels trouvés et
|
||||||
|
corrigés, tous deux dans `qa/regression`.
|
||||||
|
|
||||||
**Gap comblé (réel, sourcé)** : l'artefact le plus **cité** de tout le dépôt —
|
### Défaut #1 (PRIORITAIRE · bug CI réel) — le gate `check-regression` était cassé en checkout propre
|
||||||
`qa/regression/out/regression_run.json` (« **21 suites · 534 tests · verdict
|
|
||||||
PASS** », référencé dans des dizaines d'entrées de log, README d'agents et fiches)
|
|
||||||
— n'avait **aucun garde-fou CI** prouvant qu'il reflète le disque :
|
|
||||||
- `check_artifacts.sh` ne rejoue **que** la sous-commande déterministe `build`
|
|
||||||
(→ `regression_plan.json`) et **exclut explicitement** (l.20-22) les artefacts
|
|
||||||
d'**exécution** issus de `run` ;
|
|
||||||
- le job `qa-regression-tests` ne lance que les **25 tests unitaires du harnais**
|
|
||||||
(`unittest discover -s tests`), jamais `regression_gen.py run` complet ;
|
|
||||||
- résultat : la matrice agrégée pouvait se **périmer en silence** (module + job CI
|
|
||||||
ajoutés sans régénérer → « 21 suites » faux) ou une matrice **rouge être commitée
|
|
||||||
verte**. C'est exactement la dérive **« demo 18→21 »** corrigée à la main au commit
|
|
||||||
précédent, mais côté `run`.
|
|
||||||
|
|
||||||
**Vérif de faisabilité avant d'agir (#1 analyser)** : `regression_run.json` **ne
|
- **Symptôme** : `ci/check_regression.sh` (l.52-53) **exige** que
|
||||||
contient aucun horodatage/hôte/chemin absolu** — deux `run` successifs sont
|
`qa/regression/out/regression_run.json` **existe dans le dépôt**, puis compare un
|
||||||
**byte-identiques** (vérifié) et le résumé **matche le commité**. Le run complet
|
`run` frais à ce fichier commité (baseline byte-identique).
|
||||||
prend **~5,3 s** (stdlib pur). L'égalité exacte est donc licite et bon marché.
|
- **Contradiction** : ce même fichier était **`.gitignore`é** (« non déterministe
|
||||||
|
(temps machine) → jamais commitée ») → **jamais présent dans un checkout propre**.
|
||||||
|
Le job Gitea `check-regression` ne fait que `checkout` + `bash check_regression.sh`,
|
||||||
|
**aucune étape ne génère** le fichier. **Conséquence** : le gate **échouait sur
|
||||||
|
CHAQUE run CI propre** (`artefact absent du dépôt`). Il ne « passait » qu'en local,
|
||||||
|
par la présence d'un `regression_run.json` **non suivi** laissé par un `run` manuel.
|
||||||
|
Le commentaire de `ci.yml` disait déjà « le regression_run.json **commité** » —
|
||||||
|
intention claire du commit précédent, jamais concrétisée (fichier resté ignoré).
|
||||||
|
- **Vérif de faisabilité (#1 analyser)** : l'artefact `run` est **byte-déterministe** —
|
||||||
|
clés `{ran,passed,failures,...}` + `path` **relatif**, **aucun** horodatage/hôte/
|
||||||
|
durée. Deux `run` successifs **byte-identiques** (vérifié). Le committer est donc
|
||||||
|
**licite** et complète l'intention du gate.
|
||||||
|
- **Fix** : `run` est désormais **commité** (baseline du gate) →
|
||||||
|
- `.gitignore` : retrait de `out/regression_run.json` + note expliquant qu'il est
|
||||||
|
commité *parce que* déterministe ;
|
||||||
|
- `README.md` : section `run` réécrite (byte-déterministe · commité · baseline
|
||||||
|
`check_regression` · distinction d'avec `check_artifacts` qui ne rejoue que
|
||||||
|
`build`) ;
|
||||||
|
- `out/regression_run.json` : **ajouté au suivi** (21 suites · 534 tests · PASS).
|
||||||
|
- **Preuve de morsure** : fichier retiré (simule checkout propre) ⇒ gate **exit 1**
|
||||||
|
(`artefact absent`) — l'état exact en CI ; restauré ⇒ **exit 0** (`run == commité`).
|
||||||
|
|
||||||
**Fichiers** (3 · aucun code métier / chiffre touché) :
|
### Défaut #2 (invariant manquant) — dérive silencieuse disque→CI non gardée
|
||||||
- **`ci/check_regression.sh`** (nouveau, exécutable) : rejoue `regression_gen.py
|
|
||||||
run` vers un tmp, exige `regression_run.json` **byte-identique** au commité, puis
|
|
||||||
`verdict == PASS` ; sur dérive, imprime le **diff des `totals`** et l'ordre de
|
|
||||||
régénérer. Complément direct de `check_artifacts` (documenté dans l'en-tête).
|
|
||||||
- **`.gitea/workflows/ci.yml`** : + job **`check-regression`** (5ᵉ gate statique,
|
|
||||||
en-tête explicatif) + ajout dans les `needs` du job `gate` agrégat.
|
|
||||||
- **`ci/README.md`** : ligne du tableau des jobs, section `check_regression.sh`,
|
|
||||||
commande d'exécution locale, checklist « 4 jobs → 5 gates statiques », et note du
|
|
||||||
§`check_artifacts` renvoyant désormais à `check-regression` pour l'artefact `run`.
|
|
||||||
|
|
||||||
**Anti-invention (#6)** : rien inventé — le gate **régénère** la vérité au lieu de
|
- **Gap** : `discovery.coverage_report` prouvait le sens **CI→disque** (tout module
|
||||||
figer un compteur. Les nombres du log (« 534 / 21 / PASS ») sont **relus** du run
|
gated a un `tests/`) mais **pas l'inverse** : un module gagnant un `tests/` sur
|
||||||
frais faisant autorité.
|
disque **sans job CI** n'entrait jamais dans la matrice, ses tests **ne tournaient
|
||||||
|
jamais**, et la couverture ne le voyait pas (elle n'itère que les suites dérivées du
|
||||||
|
CI) — la matrice restait « PASS » en **sous-comptant** en silence. Même classe de
|
||||||
|
dérive que « demo 18→21 », direction opposée.
|
||||||
|
- **Fix** : nouvel invariant **INV4** —
|
||||||
|
- `discovery.disk_test_modules()` : marche disque pure listant tout module portant
|
||||||
|
un `tests/` (POSIX, trié, déterministe) ;
|
||||||
|
- `coverage_report` : champ `orphan_tests_dirs = disk − ci_paths` ; `ok` exige
|
||||||
|
désormais `not orphan_tests_dirs` ;
|
||||||
|
- `regression.schema.json` : `orphan_tests_dirs` ajouté (required · strict) ;
|
||||||
|
- `regression_gen.py` : message INV4 surface les orphelins ;
|
||||||
|
- `tests/test_regression.py` : +2 tests (sous-ensemble disque⊆CI sur le dépôt réel ;
|
||||||
|
preuve de morsure monkeypatch d'un orphelin ⇒ `ok=False`).
|
||||||
|
- **Preuve de morsure end-to-end** : `mkdir zz_orphan/tests` + `build` ⇒ **build
|
||||||
|
refusé** `INV4 … orphan_tests_dirs=['zz_orphan']` ; répertoire retiré ⇒ build OK.
|
||||||
|
|
||||||
**Non-régression du reste du système (vérifié)** : le nouveau job **n'a pas de
|
**Anti-invention (#6)** : rien inventé — les deux gates **régénèrent** la vérité (run
|
||||||
`working-directory`** → `q4lib.registry.parse_ci` (qui recense les modules depuis
|
frais / couverture recomputée) au lieu de figer un compteur.
|
||||||
les `working-directory`, réutilisé par audit_4big **et** acceptance) est **inchangé**
|
|
||||||
→ aucun décalage du `ci_modules_count` consommé par `demo/scenarios`. Preuve :
|
|
||||||
`check_artifacts.sh` **exit 0** (0 dérive des artefacts dérivés) ; suites
|
|
||||||
`qa/audit_4big`, `qa/acceptance`, `demo/scenarios` **vertes**.
|
|
||||||
|
|
||||||
**Vérifs** : preuve de morsure — artefact commité **falsifié** (534→999) ⇒ gate
|
**Régénération des consommateurs (mémoire projet)** : l'édition des tests + du README
|
||||||
**exit 1** avec diff des `totals` ; restauré ⇒ **exit 0**. Les **5 gates** locaux
|
de `qa/regression` a fait dériver `qa/audit_4big/out/quality_report.json` (evidence
|
||||||
verts (`guard_constraints`/`validate_json`/`check_docs`/`check_artifacts`/
|
« README.md 3710→4236 octets » et « 24→26 méthodes test_* ») — **régénéré**, verdict
|
||||||
`check_regression`) ; `ci.yml` **YAML valide** ; matrice **534/534 · 21/21 · PASS**
|
**PASS · 21/21 · min 100**. Totaux matrice **534/21** **inchangés** : le harnais
|
||||||
inchangée.
|
`qa/regression` est **self-exclu** (INV3) → +2 méthodes test_* n'affectent pas la
|
||||||
|
matrice.
|
||||||
|
|
||||||
**Hors périmètre worker (VPS · #8)** : néant (gate stdlib pur en-repo ; enregistrement
|
**Vérifs** : **5 gates** locaux verts (`guard_constraints`/`validate_json`/`check_docs`/
|
||||||
runner Gitea toujours du ressort DevOps, §4 du `ci/README`).
|
`check_artifacts`/`check_regression`) ; `ci.yml` **YAML valide** ; tests harnais
|
||||||
|
**26/26 OK** (régression) + **34/34 OK** (audit_4big) ; matrice **534/534 · 21/21 ·
|
||||||
|
PASS** ; artefacts `out/` reproductibles à l'identique.
|
||||||
|
|
||||||
|
**Hors périmètre worker (VPS · #8)** : néant (stdlib pur en-repo ; enregistrement du
|
||||||
|
runner Gitea toujours du ressort DevOps).
|
||||||
|
|
||||||
**Auto-score 4Big** : 96/100.
|
**Auto-score 4Big** : 96/100.
|
||||||
|
|||||||
@@ -858,7 +858,7 @@
|
|||||||
"criterion": "DOC",
|
"criterion": "DOC",
|
||||||
"weight": 20,
|
"weight": 20,
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"evidence": "README.md (3710 octets)"
|
"evidence": "README.md (4236 octets)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"criterion": "CONTRAT",
|
"criterion": "CONTRAT",
|
||||||
@@ -870,7 +870,7 @@
|
|||||||
"criterion": "TESTS",
|
"criterion": "TESTS",
|
||||||
"weight": 25,
|
"weight": 25,
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"evidence": "24 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
"evidence": "26 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"criterion": "CLI",
|
"criterion": "CLI",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
# Sortie du mode `run` : non déterministe (temps machine) → jamais commitée.
|
# NB : `out/regression_run.json` (mode `run`) EST commité — il est byte-déterministe
|
||||||
out/regression_run.json
|
# (aucun horodatage/hôte/durée ; `path` relatif) et sert de baseline au gate CI
|
||||||
|
# `ci/check_regression.sh`, qui exige qu'un run frais lui soit byte-identique.
|
||||||
|
|||||||
@@ -47,12 +47,18 @@ python3 regression_gen.py build # → out/regression_plan.json + MANI
|
|||||||
python3 regression_gen.py validate
|
python3 regression_gen.py validate
|
||||||
|
|
||||||
# Régression EXHAUSTIVE (exécute réellement toutes les suites) — local/DevOps :
|
# Régression EXHAUSTIVE (exécute réellement toutes les suites) — local/DevOps :
|
||||||
python3 regression_gen.py run # → out/regression_run.json (non commité)
|
python3 regression_gen.py run # → out/regression_run.json (COMMITÉ)
|
||||||
```
|
```
|
||||||
|
|
||||||
`run` renvoie un code de sortie ≠ 0 si **une seule** suite est rouge → utilisable
|
`run` renvoie un code de sortie ≠ 0 si **une seule** suite est rouge → utilisable
|
||||||
comme garde de release. Sa sortie est **non déterministe** (temps machine) donc
|
comme garde de release. Sa sortie est **byte-déterministe** — elle ne porte
|
||||||
**non commitée** (voir `.gitignore`).
|
**aucun** horodatage / hôte / durée / chemin absolu (`path` relatif, compteurs
|
||||||
|
`ran/passed/failures` seuls) : deux `run` successifs sont **byte-identiques**.
|
||||||
|
`out/regression_run.json` est donc **commité** et sert de **baseline** au gate CI
|
||||||
|
`ci/check_regression.sh`, qui régénère un `run` frais et exige l'**identité
|
||||||
|
byte-for-byte** avec le fichier commité (+ verdict `PASS`) — c'est ce qui empêche
|
||||||
|
une matrice périmée ou rouge d'être commitée verte. `check_artifacts.sh`, lui, ne
|
||||||
|
rejoue que `build` (déterministe) et **ignore** cet artefact d'exécution.
|
||||||
|
|
||||||
## Contrat de sortie (plan)
|
## Contrat de sortie (plan)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
"self_module_excluded": "qa/regression",
|
"self_module_excluded": "qa/regression",
|
||||||
"self_module_gated": true,
|
"self_module_gated": true,
|
||||||
"missing_tests_dir": [],
|
"missing_tests_dir": [],
|
||||||
"not_in_gate": []
|
"not_in_gate": [],
|
||||||
|
"orphan_tests_dirs": []
|
||||||
},
|
},
|
||||||
"suites": [
|
"suites": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
{
|
||||||
|
"matrix": "OTO QA · Matrice de régression exhaustive",
|
||||||
|
"version": "1.0",
|
||||||
|
"self_module": "qa/regression",
|
||||||
|
"suites": [
|
||||||
|
{
|
||||||
|
"id": "crm.commissions",
|
||||||
|
"path": "crm/commissions",
|
||||||
|
"expected_methods": 25,
|
||||||
|
"ran": 25,
|
||||||
|
"passed": 25,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "crm.dossier_vente",
|
||||||
|
"path": "crm/dossier_vente",
|
||||||
|
"expected_methods": 31,
|
||||||
|
"ran": 31,
|
||||||
|
"passed": 31,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "crm.workflow_vente",
|
||||||
|
"path": "crm/workflow_vente",
|
||||||
|
"expected_methods": 25,
|
||||||
|
"ran": 25,
|
||||||
|
"passed": 25,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "demo.scenarios",
|
||||||
|
"path": "demo/scenarios",
|
||||||
|
"expected_methods": 32,
|
||||||
|
"ran": 32,
|
||||||
|
"passed": 32,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "devops.deploy_runbook",
|
||||||
|
"path": "devops/deploy_runbook",
|
||||||
|
"expected_methods": 29,
|
||||||
|
"ran": 29,
|
||||||
|
"passed": 29,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "faisabilite.bancable",
|
||||||
|
"path": "faisabilite/bancable",
|
||||||
|
"expected_methods": 22,
|
||||||
|
"ran": 22,
|
||||||
|
"passed": 22,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "faisabilite.generator",
|
||||||
|
"path": "faisabilite/generator",
|
||||||
|
"expected_methods": 16,
|
||||||
|
"ran": 16,
|
||||||
|
"passed": 16,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fiscal.ecf_dgii",
|
||||||
|
"path": "fiscal/ecf_dgii",
|
||||||
|
"expected_methods": 39,
|
||||||
|
"ran": 39,
|
||||||
|
"passed": 39,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "frontend.chat_otoia",
|
||||||
|
"path": "frontend/chat_otoia",
|
||||||
|
"expected_methods": 31,
|
||||||
|
"ran": 31,
|
||||||
|
"passed": 31,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "frontend.portails",
|
||||||
|
"path": "frontend/portails",
|
||||||
|
"expected_methods": 19,
|
||||||
|
"ran": 19,
|
||||||
|
"passed": 19,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "legal.confotur",
|
||||||
|
"path": "legal/confotur",
|
||||||
|
"expected_methods": 44,
|
||||||
|
"ran": 44,
|
||||||
|
"passed": 44,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "publiciste",
|
||||||
|
"path": "publiciste",
|
||||||
|
"expected_methods": 23,
|
||||||
|
"ran": 23,
|
||||||
|
"passed": 23,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qa.acceptance",
|
||||||
|
"path": "qa/acceptance",
|
||||||
|
"expected_methods": 31,
|
||||||
|
"ran": 31,
|
||||||
|
"passed": 31,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qa.audit_4big",
|
||||||
|
"path": "qa/audit_4big",
|
||||||
|
"expected_methods": 34,
|
||||||
|
"ran": 34,
|
||||||
|
"passed": 34,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qa.audit_5d",
|
||||||
|
"path": "qa/audit_5d",
|
||||||
|
"expected_methods": 37,
|
||||||
|
"ran": 37,
|
||||||
|
"passed": 37,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rbac",
|
||||||
|
"path": "rbac",
|
||||||
|
"expected_methods": 10,
|
||||||
|
"ran": 10,
|
||||||
|
"passed": 10,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rbac.apply_plan",
|
||||||
|
"path": "rbac/apply_plan",
|
||||||
|
"expected_methods": 16,
|
||||||
|
"ran": 16,
|
||||||
|
"passed": 16,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rbac.fixtures_gen",
|
||||||
|
"path": "rbac/fixtures_gen",
|
||||||
|
"expected_methods": 11,
|
||||||
|
"ran": 11,
|
||||||
|
"passed": 11,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rbac.roleprofile_gen",
|
||||||
|
"path": "rbac/roleprofile_gen",
|
||||||
|
"expected_methods": 11,
|
||||||
|
"ran": 11,
|
||||||
|
"passed": 11,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rbac.userperm_gen",
|
||||||
|
"path": "rbac/userperm_gen",
|
||||||
|
"expected_methods": 12,
|
||||||
|
"ran": 12,
|
||||||
|
"passed": 12,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "seo",
|
||||||
|
"path": "seo",
|
||||||
|
"expected_methods": 36,
|
||||||
|
"ran": 36,
|
||||||
|
"passed": 36,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"ok": true,
|
||||||
|
"returncode": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"totals": {
|
||||||
|
"suites": 21,
|
||||||
|
"green": 21,
|
||||||
|
"red": 0,
|
||||||
|
"ran": 534,
|
||||||
|
"passed": 534,
|
||||||
|
"failures": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"skipped": 0
|
||||||
|
},
|
||||||
|
"verdict": "PASS"
|
||||||
|
}
|
||||||
@@ -47,6 +47,25 @@ def count_tests(tests_dir: str) -> tuple[int, int]:
|
|||||||
return (files, methods)
|
return (files, methods)
|
||||||
|
|
||||||
|
|
||||||
|
def disk_test_modules() -> list[str]:
|
||||||
|
"""Chemins de modules (relatifs à DELIVERABLES_ROOT, POSIX) portant un
|
||||||
|
répertoire `tests/` sur DISQUE — indépendamment du CI.
|
||||||
|
|
||||||
|
Sert à prouver le sens INVERSE de la couverture : tout module qui gagne des
|
||||||
|
tests sur disque DOIT être câblé au CI, sinon ses tests ne tournent jamais et
|
||||||
|
n'entrent jamais dans la matrice (dérive silencieuse disque→CI). Faits de
|
||||||
|
disque purs, ordonnés → déterministe."""
|
||||||
|
mods: set[str] = set()
|
||||||
|
for dirpath, dirnames, _ in os.walk(DELIVERABLES_ROOT):
|
||||||
|
# On n'explore pas l'intérieur des caches/tests eux-mêmes.
|
||||||
|
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
|
||||||
|
if os.path.basename(dirpath) == "tests":
|
||||||
|
module_abs = os.path.dirname(dirpath)
|
||||||
|
rel = os.path.relpath(module_abs, DELIVERABLES_ROOT)
|
||||||
|
mods.add(rel.replace(os.sep, "/"))
|
||||||
|
return sorted(mods)
|
||||||
|
|
||||||
|
|
||||||
def discover_suites(spec: dict) -> list[dict]:
|
def discover_suites(spec: dict) -> list[dict]:
|
||||||
"""Liste ordonnée des suites gated (hors self), enrichie des faits de disque.
|
"""Liste ordonnée des suites gated (hors self), enrichie des faits de disque.
|
||||||
|
|
||||||
@@ -90,10 +109,15 @@ def coverage_report(spec: dict, suites: list[dict]) -> dict:
|
|||||||
not_in_gate = sorted(s["path"] for s in suites if not s["in_gate"])
|
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)
|
self_present = any(s["path"] == self_module for s in suites)
|
||||||
|
|
||||||
|
# Sens inverse : tout module portant un `tests/` sur disque DOIT être gated
|
||||||
|
# (sinon ses tests ne tournent jamais → dérive silencieuse disque→CI).
|
||||||
|
orphan_tests_dirs = sorted(set(disk_test_modules()) - ci_paths)
|
||||||
|
|
||||||
ok = (
|
ok = (
|
||||||
not missing_tests_dir
|
not missing_tests_dir
|
||||||
and not not_in_gate
|
and not not_in_gate
|
||||||
and not self_present
|
and not self_present
|
||||||
|
and not orphan_tests_dirs
|
||||||
and self_module in ci_paths # le harnais DOIT être gated lui aussi
|
and self_module in ci_paths # le harnais DOIT être gated lui aussi
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
@@ -104,4 +128,5 @@ def coverage_report(spec: dict, suites: list[dict]) -> dict:
|
|||||||
"self_module_gated": self_module in ci_paths,
|
"self_module_gated": self_module in ci_paths,
|
||||||
"missing_tests_dir": missing_tests_dir,
|
"missing_tests_dir": missing_tests_dir,
|
||||||
"not_in_gate": not_in_gate,
|
"not_in_gate": not_in_gate,
|
||||||
|
"orphan_tests_dirs": orphan_tests_dirs,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"coverage": {
|
"coverage": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["ok", "suites_count", "gated_in_ci", "self_module_excluded", "self_module_gated", "missing_tests_dir", "not_in_gate"],
|
"required": ["ok", "suites_count", "gated_in_ci", "self_module_excluded", "self_module_gated", "missing_tests_dir", "not_in_gate", "orphan_tests_dirs"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"ok": { "type": "boolean" },
|
"ok": { "type": "boolean" },
|
||||||
"suites_count": { "type": "integer", "minimum": 0 },
|
"suites_count": { "type": "integer", "minimum": 0 },
|
||||||
@@ -36,7 +36,8 @@
|
|||||||
"self_module_excluded": { "type": "string", "minLength": 1 },
|
"self_module_excluded": { "type": "string", "minLength": 1 },
|
||||||
"self_module_gated": { "type": "boolean" },
|
"self_module_gated": { "type": "boolean" },
|
||||||
"missing_tests_dir": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
"missing_tests_dir": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
||||||
"not_in_gate": { "type": "array", "items": { "type": "string", "minLength": 1 } }
|
"not_in_gate": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
||||||
|
"orphan_tests_dirs": { "type": "array", "items": { "type": "string", "minLength": 1 } }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"suites": {
|
"suites": {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ def check_invariants(plan: dict, spec: dict) -> list[str]:
|
|||||||
errs.append(
|
errs.append(
|
||||||
f"INV4 couverture non prouvée : missing_tests_dir="
|
f"INV4 couverture non prouvée : missing_tests_dir="
|
||||||
f"{cov['missing_tests_dir']} not_in_gate={cov['not_in_gate']} "
|
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']}")
|
f"self_module_gated={cov['self_module_gated']}")
|
||||||
if cov["self_module_excluded"] != self_mod:
|
if cov["self_module_excluded"] != self_mod:
|
||||||
errs.append("INV4 self_module_excluded incohérent avec le spec")
|
errs.append("INV4 self_module_excluded incohérent avec le spec")
|
||||||
|
|||||||
@@ -154,6 +154,28 @@ class TestDiscovery(unittest.TestCase):
|
|||||||
self.assertTrue(cov["self_module_gated"]) # le harnais est gated
|
self.assertTrue(cov["self_module_gated"]) # le harnais est gated
|
||||||
self.assertEqual(cov["missing_tests_dir"], [])
|
self.assertEqual(cov["missing_tests_dir"], [])
|
||||||
self.assertEqual(cov["not_in_gate"], [])
|
self.assertEqual(cov["not_in_gate"], [])
|
||||||
|
self.assertEqual(cov["orphan_tests_dirs"], []) # aucun tests/ hors CI
|
||||||
|
|
||||||
|
def test_disk_test_modules_subset_of_ci(self):
|
||||||
|
# Sens inverse de la couverture : tout `tests/` sur disque est gated.
|
||||||
|
disk = set(discovery.disk_test_modules())
|
||||||
|
ci = set(discovery.parse_ci()["job_to_path"].values())
|
||||||
|
self.assertIn(_spec()["self_module"], disk) # le harnais a des tests
|
||||||
|
self.assertEqual(disk - ci, set(), f"orphelins disque→CI : {disk - ci}")
|
||||||
|
|
||||||
|
def test_orphan_tests_dir_detected(self):
|
||||||
|
# Preuve de morsure : un module portant un `tests/` non câblé au CI doit
|
||||||
|
# faire chuter la couverture (dérive silencieuse disque→CI interdite).
|
||||||
|
spec = _spec()
|
||||||
|
suites = discovery.discover_suites(spec)
|
||||||
|
real = discovery.disk_test_modules
|
||||||
|
try:
|
||||||
|
discovery.disk_test_modules = lambda: real() + ["orphan/module"]
|
||||||
|
cov = discovery.coverage_report(spec, suites)
|
||||||
|
finally:
|
||||||
|
discovery.disk_test_modules = real
|
||||||
|
self.assertIn("orphan/module", cov["orphan_tests_dirs"])
|
||||||
|
self.assertFalse(cov["ok"], cov)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user