Files
claude-werkzeuge/.vault-tools/plan-kopie/welle_vergleich_treiber.py
T
Pascal Michel 4d90e6be60 Abgleich mit dem Vault-Stand a201514
16 Datei(en) geändert.
Erzeugt von .vault-tools/auto-push/werkzeuge_spiegel.py.
2026-10-18 04:00:03 +02:00

280 lines
12 KiB
Python

"""Regressionsnachweis fuer welle_vergleich.py: Grundlinie zuerst, dann jeder Angriffsfall einzeln auf
einer frischen Wegwerfkopie. Je Fall werden Exit-Code und ausloesende Meldung geprueft.
Aufruf: python welle_vergleich_treiber.py Exit 0, wenn jeder Fall so ausgeht wie erwartet.
"""
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
WERKZEUG = Path(__file__).parent / "welle_vergleich.py"
def git(ordner: Path, *args: str) -> None:
subprocess.run(["git", "-C", str(ordner), *args], check=True, capture_output=True, stdin=subprocess.DEVNULL)
def schreibe(pfad: Path, text: str) -> None:
pfad.parent.mkdir(parents=True, exist_ok=True)
pfad.write_bytes(text.encode("utf-8"))
def baue(wurzel: Path) -> tuple[Path, Path, Path]:
"""Repo mit drei committeten Dateien, danach die 'Welle': a.py geaendert, sub/neu.py neu, weg.py geloescht,
doku.md geaendert (Prosa). Die Kopie traegt denselben Stand."""
projekt, mat = wurzel / "projekt", wurzel / "mat"
projekt.mkdir()
git(projekt, "init", "-q")
git(projekt, "config", "user.email", "[email protected]")
git(projekt, "config", "user.name", "Treiber")
git(projekt, "config", "core.autocrlf", "false")
schreibe(projekt / "a.py", "eins\nzwei\ndrei\n")
schreibe(projekt / "b.py", "bleibt\n")
schreibe(projekt / "weg.py", "weg\n")
schreibe(projekt / "doku.md", "alt\n")
git(projekt, "add", "-A")
git(projekt, "commit", "-q", "-m", "basis")
schreibe(projekt / "a.py", "EINS\nzwei\nDREI\n")
schreibe(projekt / "sub" / "neu.py", "neu eins\nneu zwei\n")
(projekt / "weg.py").unlink()
schreibe(projekt / "doku.md", "neue Prosa des Executers\n")
schreibe(mat / "a.py", "EINS\nzwei\nDREI\n")
schreibe(mat / "b.py", "bleibt\n")
schreibe(mat / "sub" / "neu.py", "neu eins\nneu zwei\n")
schreibe(mat / "doku.md", "anderer Stand in der Kopie\n")
task = wurzel / "task-01.md"
schreibe(
task,
"# Task\n\n## Zieldateien\n\n"
f"- `{projekt}\\a.py` (geändert)\n"
f"- `{projekt}\\sub\\neu.py` (neu)\n"
f"- `{projekt}\\weg.py` (gelöscht)\n"
f"- `{projekt}\\doku.md` (geändert)\n\n"
"Keine andere Datei wird angefasst.\n\n## Regeln\n\n- `nicht/gemeint.py` (kein Ziel)\n",
)
return projekt, mat, task
def fall_grundlinie(projekt, mat, task, wurzel):
pass
def fall_fremde_datei(projekt, mat, task, wurzel):
schreibe(projekt / "fremd.py", "x\n")
def fall_ziel_unveraendert(projekt, mat, task, wurzel):
schreibe(projekt / "a.py", "eins\nzwei\ndrei\n")
def fall_abweichung_erste_zeile(projekt, mat, task, wurzel):
schreibe(projekt / "a.py", "Eins\nzwei\nDREI\n")
def fall_abweichung_letzte_zeile(projekt, mat, task, wurzel):
schreibe(projekt / "a.py", "EINS\nzwei\nDrei\n")
def fall_abweichung_letzte_zieldatei(projekt, mat, task, wurzel):
schreibe(projekt / "sub" / "neu.py", "neu eins\nneu ZWEI\n")
def fall_crlf(projekt, mat, task, wurzel):
schreibe(projekt / "a.py", "EINS\r\nzwei\r\nDREI\r\n")
def fall_fehlt_in_kopie(projekt, mat, task, wurzel):
(mat / "sub" / "neu.py").unlink()
def fall_ohne_zieldateien(projekt, mat, task, wurzel):
schreibe(task, "# Task\n\n## Regeln\n\nnichts\n")
def fall_kopie_fehlt(projekt, mat, task, wurzel):
shutil.rmtree(mat)
def fall_nicht_geloescht(projekt, mat, task, wurzel):
schreibe(projekt / "weg.py", "weg\n")
def fall_prosa_unveraendert(projekt, mat, task, wurzel):
schreibe(projekt / "doku.md", "alt\n")
def fall_ziel_ausserhalb(projekt, mat, task, wurzel):
text = task.read_text(encoding="utf-8")
schreibe(task, text.replace("## Regeln", "") .replace("Keine andere Datei", "- `C:\\anderswo\\x.py` (neu)\n\nKeine andere Datei"))
def fall_laengere_datei(projekt, mat, task, wurzel):
schreibe(projekt / "a.py", "EINS\nzwei\nDREI\nvier\n")
def _relativ(task, projekt):
"""Schreibt die Zieldateien der Taskdatei relativ zum Projekt, eine davon mit Schraegstrich."""
text = task.read_text(encoding="utf-8")
schreibe(task, text.replace(f"{projekt}\\", "").replace("sub\\neu.py", "sub/neu.py"))
def fall_relativ_grundlinie(projekt, mat, task, wurzel):
_relativ(task, projekt)
def fall_relativ_abweichung(projekt, mat, task, wurzel):
_relativ(task, projekt)
schreibe(projekt / "sub" / "neu.py", "neu eins\nneu ZWEI\n")
def _ohne_klammer(task):
"""Nimmt den Zieldateien die Klammer (ausser der Loeschmarke) und haengt einer Zeile Text an."""
text = task.read_text(encoding="utf-8")
text = text.replace("a.py` (geändert)", "a.py` — **nur die erste und die letzte Zeile**")
schreibe(task, text.replace(" (neu)", "").replace("doku.md` (geändert)", "doku.md`"))
def fall_ohne_klammer_grundlinie(projekt, mat, task, wurzel):
_ohne_klammer(task)
def fall_ohne_klammer_abweichung(projekt, mat, task, wurzel):
_ohne_klammer(task)
schreibe(projekt / "a.py", "Eins\nzwei\nDREI\n")
def fall_relativ_nach_aussen(projekt, mat, task, wurzel):
_relativ(task, projekt)
text = task.read_text(encoding="utf-8")
schreibe(task, text.replace("Keine andere Datei", "- `../anderswo/x.py` (neu)\n\nKeine andere Datei"))
def _ignoriert(projekt, mat, task, erst="neu", letzt="neu"):
"""Seit 2026-10-17: Zieldateien unter einem Pfad, den .gitignore ausblendet (Paket R1 im Edge-Stack: Prüfdateien
unter tests/.work/). .gitignore kommt in den Basis-Commit; work/erst.py steht als erste, work/letzt.py als letzte
Zieldatei in der Liste, beide in Baum und Kopie gleich. erst/letzt: "neu" oder "gelöscht" (dann in keinem Baum)."""
schreibe(projekt / ".gitignore", "work/\n")
git(projekt, "add", ".gitignore")
git(projekt, "commit", "-q", "-m", "ignoriert")
for name, art in (("erst.py", erst), ("letzt.py", letzt)):
if art == "neu":
schreibe(projekt / "work" / name, f"{name} eins\n{name} zwei\n")
schreibe(mat / "work" / name, f"{name} eins\n{name} zwei\n")
text = task.read_text(encoding="utf-8")
text = text.replace("## Zieldateien\n\n", f"## Zieldateien\n\n- `work/erst.py` ({erst})\n", 1)
text = text.replace("\n\nKeine andere Datei", f"\n- `work/letzt.py` ({letzt})\n\nKeine andere Datei", 1)
schreibe(task, text)
def fall_ignoriert_gleich(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task)
def fall_ignoriert_erste_weicht_ab(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task)
schreibe(projekt / "work" / "erst.py", "erst.py EINS\nerst.py zwei\n")
def fall_ignoriert_letzte_weicht_ab(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task)
schreibe(projekt / "work" / "letzt.py", "letzt.py eins\nletzt.py ZWEI\n")
def fall_ignoriert_fehlt_im_baum(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task)
(projekt / "work" / "letzt.py").unlink()
def fall_ignoriert_fehlt_in_kopie(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task)
(mat / "work" / "erst.py").unlink()
def fall_ignoriert_geloescht(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task, letzt="gelöscht")
def fall_ignoriert_nicht_geloescht(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task, letzt="gelöscht")
schreibe(projekt / "work" / "letzt.py", "bleibt\n")
def fall_ignoriert_getrackt(projekt, mat, task, wurzel):
"""Eine getrackte Datei unter einem ignorierten Muster (git add -f) ist kein ignoriertes Ziel: Unveraendert
gegenueber HEAD bleibt ein Befund."""
_ignoriert(projekt, mat, task)
git(projekt, "add", "-f", "work/erst.py")
git(projekt, "commit", "-q", "-m", "erzwungen")
FAELLE = [
("Grundlinie", fall_grundlinie, 0, "0 Befund(e)."),
("fremde Datei im Baum", fall_fremde_datei, 2, "fremd.py: geaendert"),
("Zieldatei unveraendert", fall_ziel_unveraendert, 2, "a.py: Zieldatei ist unveraendert"),
("Abweichung in der ersten Zeile", fall_abweichung_erste_zeile, 2, "a.py: weicht vom Plan ab -- Zeile 1"),
("Abweichung in der letzten Zeile", fall_abweichung_letzte_zeile, 2, "a.py: weicht vom Plan ab -- Zeile 3"),
("Abweichung in der letzten Zieldatei", fall_abweichung_letzte_zieldatei, 2, "sub/neu.py: weicht vom Plan ab -- Zeile 2"),
("CRLF statt LF", fall_crlf, 2, "nur die Zeilenenden weichen ab"),
("Datei fehlt in der Kopie", fall_fehlt_in_kopie, 2, "sub/neu.py: fehlt in der Kopie"),
("Taskdatei ohne Zieldateien", fall_ohne_zieldateien, 2, "keine Zieldateien gefunden"),
("Kopie fehlt", fall_kopie_fehlt, 2, "fehlt -- ohne sie gibt es keinen Byte-Vergleich"),
("Loeschung nicht ausgefuehrt", fall_nicht_geloescht, 2, "weg.py: soll geloescht sein, steht aber noch im Baum"),
("Prosa-Datei unveraendert", fall_prosa_unveraendert, 2, "doku.md: Zieldatei ist unveraendert"),
("Zieldatei ausserhalb des Projekts", fall_ziel_ausserhalb, 2, "liegt nicht unter dem Projekt"),
# Die angehaengte Zeile steht der leeren Restzeile der Kopie gegenueber, deshalb "Zeile 4" und nicht "Laenge".
("Datei laenger als der Plan", fall_laengere_datei, 2, "a.py: weicht vom Plan ab -- Zeile 4: Baum b'vier'"),
# Seit 2026-09-18: Zieldateien relativ zum Projekt (Plaene fuer einen Git-Worktree).
("Zieldateien relativ zum Projekt", fall_relativ_grundlinie, 0, "0 Befund(e)."),
("relative Zieldatei weicht ab", fall_relativ_abweichung, 2, "sub/neu.py: weicht vom Plan ab -- Zeile 2"),
("relative Zieldatei fuehrt nach aussen", fall_relativ_nach_aussen, 2, "liegt nicht unter dem Projekt"),
# Seit 2026-09-18: Die Klammer hinter dem Pfad ist wahlfrei, Text dahinter erlaubt.
("Zieldateien ohne Klammer", fall_ohne_klammer_grundlinie, 0, "0 Befund(e)."),
("Zieldatei ohne Klammer weicht ab", fall_ohne_klammer_abweichung, 2, "a.py: weicht vom Plan ab -- Zeile 1"),
# Seit 2026-10-17: Zieldateien, die git ignoriert, werden byte-genau verglichen statt als unveraendert gemeldet.
("ignorierte Zieldateien gleich", fall_ignoriert_gleich, 0, "4 byte-gleich mit der Kopie, 1 nur als Menge geprueft, 2 von git ignoriert."),
("ignorierte erste Zieldatei weicht ab", fall_ignoriert_erste_weicht_ab, 2, "work/erst.py: weicht vom Plan ab -- Zeile 1"),
("ignorierte letzte Zieldatei weicht ab", fall_ignoriert_letzte_weicht_ab, 2, "work/letzt.py: weicht vom Plan ab -- Zeile 2"),
("ignorierte Zieldatei fehlt im Baum", fall_ignoriert_fehlt_im_baum, 2, "work/letzt.py: Zieldatei fehlt im Baum"),
("ignorierte Zieldatei fehlt in der Kopie", fall_ignoriert_fehlt_in_kopie, 2, "work/erst.py: fehlt in der Kopie"),
("ignorierte Zieldatei geloescht", fall_ignoriert_geloescht, 0, "0 Befund(e)."),
("ignorierte Loeschung nicht ausgefuehrt", fall_ignoriert_nicht_geloescht, 2, "work/letzt.py: soll geloescht sein, steht aber noch im Baum"),
("getrackte Datei unter ignoriertem Muster unveraendert", fall_ignoriert_getrackt, 2, "work/erst.py: Zieldatei ist unveraendert"),
]
def main() -> int:
fehler = 0
for name, angriff, exit_erwartet, meldung in FAELLE:
wurzel = Path(tempfile.mkdtemp(prefix="wv_"))
try:
projekt, mat, task = baue(wurzel)
angriff(projekt, mat, task, wurzel)
lauf = subprocess.run(
[sys.executable, "-X", "utf8", str(WERKZEUG), "--projekt", str(projekt), "--mat", str(mat),
"--prosa", "doku.md", "--", str(task)],
capture_output=True, text=True, encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL,
)
ausgabe = lauf.stdout + lauf.stderr
gut = lauf.returncode == exit_erwartet and meldung in ausgabe
if name != "Grundlinie" and gut:
# Genau ein Befund je Fall, ausser wo der Angriff zwangslaeufig zwei ausloest.
pass
print(f"{'ok ' if gut else 'FEHLER'} {name}: Exit {lauf.returncode} (erwartet {exit_erwartet})")
if not gut:
fehler += 1
print(" erwartete Meldung: " + meldung)
print(" " + ausgabe.strip().replace("\n", "\n "))
if name == "Grundlinie":
print("Die Grundlinie ist nicht sauber -- der Rest misst nichts.")
return 2
finally:
shutil.rmtree(wurzel, ignore_errors=True)
print(f"{fehler} Fall/Faelle mit falschem Ausgang.")
return 2 if fehler else 0
if __name__ == "__main__":
sys.exit(main())