"""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", "t@example.invalid") 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())