Spaces:
Running
Running
File size: 7,430 Bytes
6c882f0 bb9f9b6 ff6d2d5 bb9f9b6 6c882f0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | """Chantier 1 (audit prod) — dé-sprintage des noms de fichiers de test.
Phase 1.0 = livrable revu AVANT tout renommage : ce script porte la
**règle** de dé-sprintage + la **table d'overrides curée** (collisions
arbitrées, fichiers à supprimer plutôt que renommer, refs externes à
patcher en lockstep). ``--check`` n'écrit RIEN (revue de stratégie).
``--apply <dir>`` exécute le renommage ``git mv`` d'UN dossier +
patche les refs externes connues le concernant (Phase 1.1..1.N).
Principe : la revue porte sur la RÈGLE + la petite table d'overrides
(≈3 cas sur 184), pas sur 184 lignes générées.
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
TESTS = REPO / "tests"
# Règle : retire le 1er token sprint après ``test_``.
# Formes : sprint<N> | sprint<N>_... | sprint_<lettre><N>[_s<N>] |
# sprint_<alnum> | s<N>.
_DESPRINT = re.compile(
r"^test_(?:sprint_[a-z]?[0-9]+(?:_s[0-9]+)?|sprint[0-9]+|"
r"sprint_[a-z0-9]+|s[0-9]+)_(?P<rest>.+)$",
)
def desprint(name: str) -> str | None:
m = _DESPRINT.match(name)
return f"test_{m.group('rest')}" if m else None
def is_sprint_named(name: str) -> bool:
# ``test_sprint*`` ou ``test_s<digit>*`` ; exclut ``test_s<lettre>``
# (test_scientific_audit_2026, test_storage_keys…, test_specs…).
if name.startswith("test_sprint"):
return True
return bool(re.match(r"^test_s[0-9]", name))
# ── Overrides curés (Phase 1.0, arbitrés manuellement) ──────────────
# Collision : 2 fichiers du même dossier dé-sprintent vers le même nom.
# tests/adapters/vlm/ : la série A14 est la suite canonique
# post-rewrite → garde le nom court ; l'ancienne S4 est suffixée.
OVERRIDES: dict[str, str] = {
"tests/adapters/vlm/test_sprint_a14_s45_vlm_adapters.py":
"test_vlm_adapters.py",
"tests/adapters/vlm/test_s4_vlm_adapters.py":
"test_vlm_adapters_coverage.py",
}
# À SUPPRIMER (pas renommer) en Phase 1.final : ce test AUDITE la
# convention de numérotation sprint elle-même — rendu obsolète par le
# garde-fou anti-régression ``test_no_sprint_named_tests.py``.
DELETE_IN_FINAL: list[str] = [
"tests/docs/test_sprint_numbering.py",
]
# Refs externes à patcher EN LOCKSTEP avec le lot concerné.
# (chemin source, "ancien" → "nouveau" calculé)
EXTERNAL_REF_FILES = [
"CLAUDE.md",
"Makefile",
]
# Docs : nombreuses réfs ``test_s*`` — patchées par lot via grep
# ciblé au moment du renommage du dossier correspondant (cf. --apply).
def build_map() -> dict[str, str]:
"""Retourne {chemin_relatif_ancien: nouveau_basename}."""
out: dict[str, str] = {}
for p in sorted(TESTS.rglob("test_*.py")):
rel = p.relative_to(REPO).as_posix()
if not is_sprint_named(p.name):
continue
if rel in {*DELETE_IN_FINAL}:
continue # supprimé en final, pas renommé
if rel in OVERRIDES:
out[rel] = OVERRIDES[rel]
continue
nw = desprint(p.name)
if nw is None:
raise SystemExit(f"NON DÉSPRINTABLE (étendre la règle) : {rel}")
out[rel] = nw
return out
def check() -> int:
m = build_map()
# Collisions résiduelles (intra-dossier) post-overrides.
seen: dict[str, str] = {}
collisions = []
for old, nw in m.items():
d = str(Path(old).parent)
key = f"{d}/{nw}"
if key in seen:
collisions.append((seen[key], old, key))
else:
seen[key] = old
# Cible déjà existante non-sprint dans le dossier ?
tgt = REPO / d / nw
if tgt.exists() and (REPO / old).name != nw:
collisions.append(("<existant>", old, key))
bydir: dict[str, int] = {}
for old in m:
bydir[str(Path(old).parent)] = bydir.get(str(Path(old).parent), 0) + 1
print(f"Fichiers à renommer : {len(m)}")
print(f"À supprimer en Phase 1.final : {len(DELETE_IN_FINAL)} "
f"({', '.join(DELETE_IN_FINAL)})")
print(f"Overrides curés (collisions arbitrées) : {len(OVERRIDES)}")
for k, v in OVERRIDES.items():
print(f" {k} -> {v}")
print(f"Collisions résiduelles : {len(collisions)}")
for a, b, k in collisions:
print(f" !! {a} ⨯ {b} -> {k}")
print("Répartition par dossier (ordre d'application conseillé "
"= concentrique) :")
for d in sorted(bydir):
print(f" {d:45s} {bydir[d]:3d}")
return 1 if collisions else 0
def apply_dir(target_dir: str) -> int:
"""Phase 1.1..1.N — renomme UN dossier (git mv) + patche les refs
externes le concernant. Idempotent, vert exigé après."""
m = {o: n for o, n in build_map().items()
if str(Path(o).parent) == target_dir.rstrip("/")}
if not m:
print(f"Aucun fichier sprint dans {target_dir}")
return 0
renamed: list[tuple[str, str]] = []
for old, nw in m.items():
new = str(Path(old).parent / nw)
subprocess.run(["git", "mv", old, new], check=True, cwd=REPO)
renamed.append((Path(old).name, nw))
print(f"git mv {old} -> {new}")
# Patch refs externes (CLAUDE.md, Makefile, docs/) pour ces fichiers.
for src in EXTERNAL_REF_FILES + _docs_files():
sp = REPO / src
if not sp.exists():
continue
txt = sp.read_text(encoding="utf-8")
orig = txt
for old_name, new_name in renamed:
txt = txt.replace(old_name, new_name)
if txt != orig:
sp.write_text(txt, encoding="utf-8")
print(f"patché refs : {src}")
# Sweep GÉNÉRIQUE des imports inter-tests : tout module renommé
# dans ce lot, référencé en dotted-path depuis n'importe quel
# fichier de ``tests/`` (``from tests.x.y.<old_stem> import`` ou
# ``import tests.x.y.<old_stem>``), est repointé vers le nouveau
# stem. Remplace l'ancien cas hardcodé fragile (ordre-dépendant).
stem_map = {
Path(old_name).stem: Path(new_name).stem
for old_name, new_name in renamed
}
for tp in TESTS.rglob("*.py"):
t = tp.read_text(encoding="utf-8")
orig = t
for old_stem, new_stem in stem_map.items():
# Couvre l'import DOTTED (``from tests.x.y.<stem> import``)
# ET l'import BARE (``from <stem> import`` / ``import
# <stem>`` — style pytest rootdir, sans point). Borné par
# ``\b`` + lookahead pour ne pas matcher un préfixe d'un
# nom plus long.
t = re.sub(
rf"\b{re.escape(old_stem)}\b(?=\s*(?:import|$|\.|\n))",
new_stem, t,
)
if t != orig:
tp.write_text(t, encoding="utf-8")
print(f"patché import inter-tests : "
f"{tp.relative_to(REPO).as_posix()}")
return 0
def _docs_files() -> list[str]:
return [p.relative_to(REPO).as_posix()
for p in (REPO / "docs").rglob("*.md")]
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--check", action="store_true")
ap.add_argument("--apply", metavar="DIR",
help="renomme un dossier (git mv) + patche refs")
a = ap.parse_args()
if a.apply:
sys.exit(apply_dir(a.apply))
sys.exit(check())
|