Automatischer Push: 11 Datei(en) geändert

- .claude/agents/planer.md
- .claude/workflows/plankritik.js
- .claude/workflows/quorum.js
- .claude/workflows/welle-mit-pruefung.js
- .vault-tools/plan-kopie/LIESMICH.md
- .vault-tools/plan-kopie/plan_bloecke.py
- .vault-tools/plan-kopie/plan_materialisiere.py
- .vault-tools/plan-kopie/plan_zaun_treiber.py
- .vault-tools/workflow-pruefung/LIESMICH.md
- .vault-tools/workflow-pruefung/faelle_workflows.mjs
- .vault-tools/workflow-pruefung/mutanten_workflows.py

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-11-03 03:00:01 +01:00
1 parent 7f484f9be5
commit 22761eb186
11 files changed
+1520 -65

No files matched your search

@@ -0,0 +1,377 @@
"""Regressionsnachweis fuer die Zaunpruefung von plan_materialisiere.py und plan_bloecke.py.
Anlass 2026-11-01 (Kette 050 der Trading Plattform, Auftrag 13): In task-1020 klebte der schliessende Zaun
zweimal an der Codezeile. Der Block lief weiter, schluckte das naechste Aus/wird-Paar ohne eigene Meldung, und
das Werkzeug sagte nur "Aus-Block steht 0-mal". Unter "Die ganze Datei ist neu" schrieb es Planprosa in die
Zieldatei und meldete 0 Befunde. Die Faelle unten bauen diese Form nach, an erster und an letzter Stelle, je
schreibender Marke, im eingerueckten und im laengeren Block, mit CRLF, ueber zwei Taskdateien, dazu den Block,
der nie schliesst, und den fehlenden Zaun vor dem naechsten Block.
Die Angriffe aus paar() haben ein geheiltes Gegenstueck, das sich nur im Zaun unterscheidet und schreiben muss:
Sonst waere "nichts geschrieben" auch dann gruen, wenn die Fixture von vornherein nichts anwendet. Die Gegenfaelle
ohne Befund sind echte Inhalte: Markdown-Vorlagen in laengeren Zaeunen mit ##-Ueberschriften, eingerueckte Bloecke,
Inline-Code, Zeichenketten mit Zaunfolge. Faelle mit "Luecke:" im Namen halten fest, was das Werkzeug bewusst nicht
sieht: Wird einer rot, ist die Luecke zu und der Kopfkommentar von plan_materialisiere.py nachzuziehen.
Grundlinie zuerst, dann jeder Fall einzeln in einem frischen Wegwerfordner. Je Fall werden Exit-Code, jede
erwartete Befundzeile samt Zeilennummer (aus dem Text der Taskdatei gerechnet), die Zahl der BEFUND-Zeilen, die
Summenzeile und bei plan_materialisiere.py jede Datei der Kopie geprueft.
--mutanten baut die Fehler aus MUTANTEN je in eine Kopie der Werkzeuge ein und verlangt, dass mindestens ein Fall
rot wird; ein Mutant, dessen Zeile nicht genau einmal dasteht, ist selbst ein Fehler.
Aufruf: python plan_zaun_treiber.py [--werkzeuge <ordner>] [--mutanten] Vorgabe: der Ordner dieses Skripts.
Exit 0, wenn jeder Fall so ausgeht wie erwartet und jeder Mutant gefangen ist, sonst 2.
"""
import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
BASIS = {
"tests/a.sh": "eins\nzwei\ndrei\n",
"tests/b.sh": "vier\nfuenf\n",
"tests/c.py": "@pytest.mark.parametrize(\"pfad\", [\n pytest.param(pfad)\n])\n",
"README.md": "# Projekt\n",
}
Z = "```"
KLEBT = "Zaun klebt"
FEHLT = "nicht geschlossen"
OFFEN = "schliesst nie"
LESEN = "nicht lesbar"
KOPF = "# Task 01\n\n## Prüfstand\n\n"
A = "#### Stelle 1 — `tests/a.sh`"
B = "#### Stelle 2 — `tests/b.sh`"
C = "#### Stelle 1 — `tests/c.py`"
N = "#### Stelle 1 — `docs/neu.md`"
NICHTS = {}
GANZ_A = {"tests/a.sh": "eins\nZWEI\ndrei\n"}
GANZ_B = {"tests/b.sh": "VIER\nfuenf\n"}
def b(wort: str, wortlaut: str | None, welche: int = 1, datei: str = "task-01.md") -> tuple:
"""Erwarteter Befund: Wort, Text der Zeile (welche-tes Vorkommen, -1 das letzte), Taskdatei."""
return wort, wortlaut, welche, datei
def aus_wird(ueberschrift: str, alt: str, neu: str, schluss_aus: str = "\n" + Z, schluss_wird: str = "\n" + Z) -> str:
return f"{ueberschrift}\n\nAus:\n\n{Z}text\n{alt}{schluss_aus}\n\nwird:\n\n{Z}text\n{neu}{schluss_wird}\n\n"
def ganz(ueberschrift: str, text: str, schluss: str = "\n" + Z) -> str:
return f"{ueberschrift}\n\nDie ganze Datei ist neu:\n\n{Z}text\n{text}{schluss}\n\n"
def anhaengen(ueberschrift: str, text: str, schluss: str = "\n" + Z) -> str:
return f"{ueberschrift}\n\nHäng diesen Block ans Ende der Datei:\n\n{Z}text\n{text}{schluss}\n\n"
def befehl(text: str, schluss: str = "\n" + Z) -> str:
return f"Aufruf:\n\n{Z}bash\n{text}{schluss}\n\n"
STELLE_A = aus_wird(A, "zwei", "ZWEI")
STELLE_B = aus_wird(B, "vier", "VIER")
# Name, Art (mat, vor, nach), Taskdatei(en), Exit, erwartete Befunde (Wort, Text der Zeile), Dateien der Kopie
# (nur mat; nur Abweichungen von BASIS, None heisst geloescht)
FAELLE = [
("Grundlinie", "mat", KOPF + STELLE_A + STELLE_B, 0, [], {**GANZ_A, **GANZ_B}),
("bloecke: Grundlinie", "vor", KOPF + STELLE_A + STELLE_B, 0, [], None),
]
def paar(name: str, vorlage: str, klebt: str, heil: str, erwartet: list, dateien: dict, bloecke: bool = True) -> None:
"""Haengt den Angriff und sein Gegenstueck an; {S} in der Vorlage ist die Stelle des Zauns."""
FAELLE.append((name, "mat", vorlage.replace("{S}", klebt), 2, erwartet, NICHTS))
FAELLE.append((f"{name}: geheilt", "mat", vorlage.replace("{S}", heil), 0, [], dateien))
if bloecke:
FAELLE.append((f"bloecke vor: {name}", "vor", vorlage.replace("{S}", klebt), 2, erwartet, None))
FAELLE.append((f"bloecke nach: {name}", "nach", vorlage.replace("{S}", klebt), 2, erwartet, None))
NAH = "\n" + Z
# Die Form aus task-1020: beide Zaeune kleben an "])", das naechste Paar waere geschluckt
paar("T27-Form: Aus und wird kleben", KOPF + aus_wird(C, " pytest.param(pfad)\n])", " pytest.param(PFAD)\n])", "{S}", "{S}") + STELLE_B,
Z, NAH, [b(KLEBT, "])" + Z), b(KLEBT, "])" + Z, 2)], {"tests/c.py": BASIS["tests/c.py"].replace("(pfad)", "(PFAD)"), **GANZ_B})
paar("nur Aus klebt, erste Stelle", KOPF + aus_wird(A, "zwei", "ZWEI", "{S}") + STELLE_B, Z, NAH, [b(KLEBT, "zwei" + Z)], {**GANZ_A, **GANZ_B})
paar("nur wird klebt, Folgepaar", KOPF + aus_wird(A, "zwei", "ZWEI", NAH, "{S}") + STELLE_B, Z, NAH, [b(KLEBT, "ZWEI" + Z)], {**GANZ_A, **GANZ_B})
paar("nur wird klebt, letzte Stelle", KOPF + STELLE_B + aus_wird(A, "zwei", "ZWEI", NAH, "{S}"), Z, NAH, [b(KLEBT, "ZWEI" + Z)], {**GANZ_A, **GANZ_B})
paar("ganze Datei klebt, Folgepaar", KOPF + ganz(N, "neu", "{S}") + STELLE_A, Z, NAH, [b(KLEBT, "neu" + Z)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
paar("ganze Datei klebt, letzte Stelle", KOPF + STELLE_A + ganz(N, "neu", "{S}"), Z, NAH, [b(KLEBT, "neu" + Z)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
paar("anhaengen klebt", KOPF + STELLE_A + anhaengen(B, "sechs", "{S}") + "Schluss.\n", Z, NAH, [b(KLEBT, "sechs" + Z)],
{**GANZ_A, "tests/b.sh": "vier\nfuenf\nsechs\n"}, bloecke=False)
paar("Befehlsblock klebt, Folgepaar", KOPF + befehl("ls", "{S}") + STELLE_A, Z, NAH, [b(KLEBT, "ls" + Z)], GANZ_A)
paar("eingerueckter Block klebt", KOPF + f"1. Schritt:\n\n {Z}bash\n ls{{S}}\n\n" + STELLE_A, Z, "\n " + Z, [b(KLEBT, " ls" + Z)], GANZ_A)
paar("Leerzeichen hinter dem klebenden Zaun", KOPF + STELLE_B + ganz(N, "neu", "{S}"), Z + " ", NAH, [b(KLEBT, "neu" + Z + " ")], {"docs/neu.md": "neu\n", **GANZ_B}, bloecke=False)
paar("klebender Zaun laenger als der Block", KOPF + aus_wird(A, "zwei", "ZWEI", "{S}") + STELLE_B, Z + "``", NAH, [b(KLEBT, "zwei" + Z + "``")], {**GANZ_A, **GANZ_B})
VIER = Z + "`"
paar("Vierer-Zaun klebt", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\nneu{{S}}\n\n" + STELLE_A, VIER, "\n" + VIER, [b(KLEBT, "neu" + VIER)],
{"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
# Bewusst entschieden: Auch ein Text, der auf einen Zaun endet, ist ein Befund; Abhilfe ist der laengere Zaun (Fall unten)
FAELLE.append(("Zeichenkette im Test endet auf den Zaun", "mat",
KOPF + ganz(N, f'TEXT = """Aus:\nzwei{Z}\n"""') + STELLE_A, 2, [b(KLEBT, "zwei" + Z), b(FEHLT, Z + "text", 2)], NICHTS))
# Block, der nie schliesst: letzte, einzige und allerletzte Zeile, Schlusszaun zu kurz; die heile Form schreibt
paar("Block schliesst nie, letzte Stelle", KOPF + STELLE_A + f"{N}\n\nDie ganze Datei ist neu:\n\n{Z}text\nneu\n{{S}}", "", Z + "\n",
[b(OFFEN, Z + "text", -1)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
paar("Block schliesst nie, einziger Block", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{Z}text\nneu\n\nProsa.\n{{S}}", "", Z + "\n",
[b(OFFEN, Z + "text")], {"docs/neu.md": "neu\n\nProsa.\n"}, bloecke=False)
paar("Aus-Block schliesst nie", KOPF + f"{A}\n\nAus:\n\n{Z}text\nzwei\n{{S}}", "", Z + "\n\nwird:\n\n" + Z + "text\nZWEI\n" + Z + "\n", [b(OFFEN, Z + "text")], GANZ_A)
paar("Zaun auf der allerletzten Zeile", KOPF + STELLE_A + "Aufruf:\n\n" + Z + "{S}", "", "bash\nls\n" + Z, [b(OFFEN, Z, -1)], GANZ_A)
paar("Schlusszaun kuerzer als der Oeffner", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}text\nneu\n{{S}}\n\nProsa.\n", Z, VIER, [b(OFFEN, VIER + "text")], {"docs/neu.md": "neu\n"}, bloecke=False)
# Zaun fehlt, der naechste Block oeffnet: Der Block schloesse mit dem Zaun des naechsten
paar("Zaun fehlt, Folgepaar", KOPF + ganz(N, "neu", "{S}") + STELLE_A, "", NAH, [b(FEHLT, Z + "text", 2)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
paar("Zaun fehlt im Aus-Block", KOPF + aus_wird(A, "zwei", "ZWEI", "{S}") + STELLE_B, "", NAH, [b(FEHLT, Z + "text", 2)], {**GANZ_A, **GANZ_B})
paar("Zaun fehlt, naechster Block ist laenger", KOPF + ganz(N, "neu", "{S}") + f"{A}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\n{Z}text\nx\n{Z}\n{VIER}\n", "", NAH,
[b(FEHLT, VIER + "markdown")], {"docs/neu.md": "neu\n", "tests/a.sh": f"{Z}text\nx\n{Z}\n"}, bloecke=False)
# Zwei Fehler in einer Datei: beide stehen da, keiner verdeckt den anderen
FAELLE.append(("klebt und schliesst nie", "mat", KOPF + aus_wird(A, "zwei", "ZWEI", Z) + ganz(N, "neu", ""), 2,
[b(KLEBT, "zwei" + Z), b(OFFEN, Z + "text", -1)], NICHTS))
paar("CRLF: beide kleben", (KOPF + aus_wird(A, "zwei", "ZWEI", "{S}", "{S}") + STELLE_B).replace("\n", "\r\n"), Z, "\r\n" + Z,
[b(KLEBT, "zwei" + Z), b(KLEBT, "ZWEI" + Z)], {**GANZ_A, **GANZ_B})
# Mehrere Taskdateien: Der ganze Lauf schreibt nichts, auch die heile Datei davor nicht
ZWEI_DATEIEN = [("zweite Taskdatei klebt", {"task-01.md": KOPF + STELLE_A, "task-02.md": KOPF + ganz(N, "neu", Z)}, [b(KLEBT, "neu" + Z, 1, "task-02.md")]),
("erste Taskdatei klebt", {"task-01.md": KOPF + ganz(N, "neu", Z), "task-02.md": KOPF + STELLE_A}, [b(KLEBT, "neu" + Z)])]
for name, dateien, erwartet in ZWEI_DATEIEN:
FAELLE.append((name, "mat", dateien, 2, erwartet, NICHTS))
FAELLE.append(("zwei Taskdateien: geheilt", "mat", {"task-01.md": KOPF + STELLE_A, "task-02.md": KOPF + ganz(N, "neu")}, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}))
# Loeschung unter Zieldateien bleibt aus, solange die Taskdatei einen Zaunbefund traegt
LOESCHEN = "# Task 01\n\n## Zieldateien\n\n- `tests/b.sh` (gelöscht)\n\n## Prüfstand\n\n" + aus_wird(A, "zwei", "ZWEI", "{S}")
paar("Loeschung unter Zieldateien", LOESCHEN, Z, NAH, [b(KLEBT, "zwei" + Z)], {**GANZ_A, "tests/b.sh": None}, bloecke=False)
# Unlesbar: ein Befund, kein Traceback, auch die heile Datei davor bleibt ungeschrieben
FAELLE.append(("Taskdatei fehlt", "mat", {"task-01.md": KOPF + STELLE_A, "task-02.md": None}, 2, [b(LESEN, None, 1, "task-02.md")], NICHTS))
FAELLE.append(("Taskdatei ist kein UTF-8", "mat", {"task-01.md": KOPF + STELLE_A, "task-02.md": b"\xff\xfe\x00kaputt"}, 2, [b(LESEN, None, 1, "task-02.md")], NICHTS))
FAELLE.append(("bloecke: Taskdatei fehlt", "vor", {"task-01.md": KOPF + STELLE_A, "task-02.md": None}, 2, [b(LESEN, None, 1, "task-02.md")], None))
# Nachbesserung nach der Pruefung vom 2026-11-02: Einzug, zwei Zaeune auf einer Zeile, Zeile beginnt mit dem Zaun
TIEF = " " + Z
ZEICHENKETTE = f'def test():\n t = """\n{TIEF}\n code\n{TIEF}\n """'
FAELLE.extend([
("Zaun vier Spalten tiefer ist Inhalt, kein Schluss", "mat", KOPF + ganz(N, ZEICHENKETTE) + STELLE_A, 2, [b(KLEBT, TIEF)], NICHTS),
("Zaun vier Spalten tiefer im laengeren Zaun", "mat", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}python\n{ZEICHENKETTE}\n{VIER}\n\n" + STELLE_A, 0, [],
{"docs/neu.md": ZEICHENKETTE + "\n", **GANZ_A}),
("bloecke: Zaun vier Spalten tiefer im Aus-Block", "vor", KOPF + aus_wird(A, f"eins\n{TIEF}\n zwei\n{TIEF}\ndrei", "X") + STELLE_B, 2, [b(KLEBT, TIEF)], None),
("Zaun drei Spalten tiefer schliesst", "mat", KOPF + ganz(N, "neu", "\n " + Z) + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
("eingerueckter Block: Schlusszaun drei Spalten tiefer", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n" + STELLE_A, 0, [], GANZ_A),
("eingerueckter Block: Zaun vier Spalten tiefer", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n {Z}\n\n" + STELLE_A, 2,
[b(KLEBT, " " + Z), b(FEHLT, Z + "text")], NICHTS),
("zwei Zaeune auf einer Zeile schliessen nicht", "mat", KOPF + ganz(N, "neu", f"\n{Z} {Z}") + STELLE_A, 2, [b(KLEBT, f"{Z} {Z}")], NICHTS),
("Blockzeile beginnt mit dem Zaun und endet anders", "mat", KOPF + ganz(N, f"neu\n{Z} {Z}text\nzwei") + STELLE_A, 2, [b(FEHLT, f"{Z} {Z}text")], NICHTS),
("eingerueckter Oeffner im Block", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n\n2. Danach:\n\n {Z}text\n pwd\n {Z}\n\n" + STELLE_A, 2, [b(FEHLT, " " + Z + "text")], NICHTS),
("zwei Bloecke ohne Schluss", "mat", KOPF + ganz(N, "neu", "") + ganz(A, "x", ""), 2, [b(FEHLT, Z + "text", 2), b(OFFEN, Z + "text", 2)], NICHTS),
("Meldung nennt den Block des klebenden Zauns", "mat", f"Prosa.\n\n{Z}text\nzwei{Z}\n", 2, [b("Block ab Zeile 3", "zwei" + Z)], NICHTS),
("Meldung nennt je den offenen Block", "mat", f"{Z}text\na\n{Z}text\nb\n{Z}text\nc\n{Z}\n", 2, [b("Block ab Zeile 1", Z + "text", 2), b("Block ab Zeile 3", Z + "text", 3)], NICHTS),
("Prosa beginnt mit zwei Backticks", "mat", KOPF + "``nur zwei\n\n" + STELLE_A, 0, [], GANZ_A),
("Zaun fehlt, naechster Block tiefer eingerueckt", "mat", KOPF + ganz(N, "neu", "") + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n" + STELLE_A, 2,
[b(FEHLT, " " + Z + "bash")], NICHTS),
# plan_bloecke.py: Der Schlusszaun des eingerueckten Blocks muss schliessen, sonst ginge das Aus danach still verloren
("bloecke: eingerueckter Block, Schlusszaun drei Spalten tiefer", "vor", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n" + aus_wird(A, "gibt es nicht", "x"), 2,
[b("Aus-Block steht nicht", Z + "text")], None),
# Bewusst nicht gesehen: Ein nackter Zaun in Spalte 0 schliesst, die Zahl der Zaeune geht auf, der Rest ist Prosa
("Luecke: nackter Zaun als Zeile im Text", "mat", KOPF + ganz(N, f't = """\n{Z}\ncode\n{Z}\n"""') + STELLE_A, 0, [], {"docs/neu.md": 't = """\n', **GANZ_A}),
])
# Echte Inhalte: kein Befund, und der Text kommt unveraendert an
VORLAGE =f"## Abschnitt\n\n### `tests/b.sh`\n\nProsa mit `Code`.\n\n{Z}text\nx\n{Z}\n\n{Z}bash\ny\n{Z}"
FAELLE.extend([
("Markdown-Vorlage im Vierer-Zaun, ##-Ueberschriften aendern kein Ziel", "mat",
KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\n{VORLAGE}\n{VIER}\n\n" + STELLE_A, 0, [], {"docs/neu.md": VORLAGE + "\n", **GANZ_A}),
("Vorlage: letzte Stelle", "mat", KOPF + STELLE_A + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\n{VORLAGE}\n{VIER}\n", 0, [], {"docs/neu.md": VORLAGE + "\n", **GANZ_A}),
("Eingerueckter Befehl in einer Liste", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n2. Danach:\n\n {Z}\n pwd\n {Z}\n\n" + STELLE_A, 0, [], GANZ_A),
("Spalte-0-Block mit eingeruecktem Schlusszaun", "mat", KOPF + ganz(N, "neu", "\n " + Z) + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
("Inline-Code am Ende einer Blockzeile", "mat", KOPF + ganz(N, "- `pfad`\n- ``doppelt``\n- ende``") + STELLE_A, 0, [],
{"docs/neu.md": "- `pfad`\n- ``doppelt``\n- ende``\n", **GANZ_A}),
("Zaunfolge am Zeilenende im Vierer-Zaun", "mat", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}text\nzwei{Z}\n{VIER}\n\n" + STELLE_A, 0, [],
{"docs/neu.md": f"zwei{Z}\n", **GANZ_A}),
("Zeichenkette im Test im laengeren Zaun", "mat", KOPF + f'{N}\n\nDie ganze Datei ist neu:\n\n{VIER}python\nTEXT = """Aus:\nzwei{Z}\n{Z}text\n"""\n{VIER}\n\n' + STELLE_A, 0, [],
{"docs/neu.md": f'TEXT = """Aus:\nzwei{Z}\n{Z}text\n"""\n', **GANZ_A}),
("Zaunfolge in Anfuehrungszeichen", "mat", KOPF + ganz(N, f'Z = "{Z}"\nzaun = f"{{Z}}text"\nassert x == "{Z}"') + STELLE_A, 0, [],
{"docs/neu.md": f'Z = "{Z}"\nzaun = f"{{Z}}text"\nassert x == "{Z}"\n', **GANZ_A}),
("Zaunfolge mitten in der Zeile", "mat", KOPF + ganz(N, f"a {Z} b\n{Z[:2]}x") + STELLE_A, 0, [], {"docs/neu.md": f"a {Z} b\n{Z[:2]}x\n", **GANZ_A}),
("Prosa endet auf Zaunfolge", "mat", KOPF + f"Der Zaun besteht aus {Z}\n\n" + STELLE_A, 0, [], GANZ_A),
("Inline-Code am Zeilenanfang oeffnet nichts", "mat", KOPF + f"{Z}foo{Z} ist Inline-Code.\n\n" + STELLE_A + STELLE_B, 0, [], {**GANZ_A, **GANZ_B}),
("Inline-Code am Zeilenanfang, letzte Zeile", "mat", KOPF + STELLE_A + f"{Z}foo{Z} ist Inline-Code.\n", 0, [], GANZ_A),
("Schlusszaun mit Leerzeichen dahinter", "mat", KOPF + ganz(N, "neu", "\n" + Z + " ") + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
("Schlusszaun laenger als der Oeffner", "mat", KOPF + ganz(N, "neu", "\n" + VIER) + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
("leerer Block", "mat", KOPF + f"Aufruf:\n\n{Z}\n{Z}\n\n" + STELLE_A, 0, [], GANZ_A),
("Taskdatei ohne Block", "mat", KOPF + "Nur Prosa.\n", 0, [], NICHTS),
("leere Taskdatei", "mat", "", 0, [], NICHTS),
# plan_bloecke.py: dieselbe Blockstruktur wie plan_materialisiere.py
("bloecke: Vorlage als Aus-Block im Vierer-Zaun", "vor", KOPF + STELLE_A + f"{A}\n\nAus:\n\n{VIER}text\nzwei\ndrei\n{VIER}\n\nwird:\n\n{VIER}markdown\n{VORLAGE}\n{VIER}\n\n", 0, [], None),
("bloecke: Inline-Code am Zeilenanfang oeffnet nichts", "vor", KOPF + f"{Z}foo{Z} ist Inline-Code.\n\n" + STELLE_A + STELLE_B, 0, [], None),
("bloecke: eingerueckter Block nach Aus zaehlt nicht", "vor", KOPF + f"{A}\n\nAus:\n\n {Z}text\n gibt es nicht\n {Z}\n\n" + STELLE_A, 0, [], None),
("bloecke: eingerueckter Schlusszaun schliesst", "vor", KOPF + aus_wird(A, "zwei", "ZWEI", "\n " + Z) + STELLE_B, 0, [], None),
# Die heile Datei daneben wird weiter geprueft; ihr Befund steht neben dem Zaunbefund
("bloecke: zwei Taskdateien, die heile wird geprueft", "vor",
{"task-01.md": KOPF + aus_wird(A, "zwei", "ZWEI", Z), "task-02.md": KOPF + aus_wird(A, "gibt es nicht", "x")}, 2,
[b(KLEBT, "zwei" + Z), b("Aus-Block steht nicht", Z + "text", 1, "task-02.md")], None),
])
MAT, BLO = "plan_materialisiere.py", "plan_bloecke.py"
FEHLT_ZEILE = ' if kern.startswith("`" * laenge):'
NEUBEGINN = ' start, laenge, spalte = i, len(kern) - len(kern.lstrip("`")), len(zeilen[i]) - len(kern)'
SCHLUSS = ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 3'
# Name, Datei, Text (ganze Zeilen, genau einmal in der Datei), Ersatz, Fall, an dem der Mutant rot werden muss
MUTANTEN = [
("klebt: Pruefung aus", MAT, ' if zeilen[i].rstrip().endswith("`" * laenge):', " if False:", "T27-Form: Aus und wird kleben"),
("klebt: Leerzeichen dahinter uebersehen", MAT, ' if zeilen[i].rstrip().endswith("`" * laenge):', ' if zeilen[i].endswith("`" * laenge):', "Leerzeichen hinter dem klebenden Zaun"),
("klebt: feste Laenge drei", MAT, ' if zeilen[i].rstrip().endswith("`" * laenge):', f' if zeilen[i].rstrip().endswith("{Z}"):', "Zaunfolge am Zeilenende im Vierer-Zaun"),
("klebt: Block endet nicht am Zaun", MAT, 'in einen laengeren Zaun"))\n break\n', 'in einen laengeren Zaun"))\n', "T27-Form: Aus und wird kleben"),
("offen: Pruefung aus", MAT, " if i >= len(zeilen):", " if False:", "Block schliesst nie, letzte Stelle"),
("fehlt: Pruefung aus", MAT, FEHLT_ZEILE, " if False:", "Zaun fehlt, Folgepaar"),
("fehlt: nur laengere Zaeune", MAT, FEHLT_ZEILE, ' if kern.startswith("`" * (laenge + 1)):', "Zaun fehlt, Folgepaar"),
("fehlt: nur gleich lange Zaeune", MAT, FEHLT_ZEILE, ' if len(kern) - len(kern.lstrip("`")) == laenge:', "Zaun fehlt, naechster Block ist laenger"),
("fehlt: nur Zaeune mit Sprache", MAT, FEHLT_ZEILE, " if zaun(zeilen[i]) >= laenge:", "Blockzeile beginnt mit dem Zaun und endet anders"),
("fehlt: kein neuer Block", MAT, NEUBEGINN, " pass", "Zaun fehlt, naechster Block ist laenger"),
("fehlt: Zeile des Blocks bleibt", MAT, NEUBEGINN, ' laenge, spalte = len(kern) - len(kern.lstrip("`")), len(zeilen[i]) - len(kern)', "zwei Bloecke ohne Schluss"),
("fehlt: Zaunlaenge bleibt", MAT, NEUBEGINN, " start, spalte = i, len(zeilen[i]) - len(kern)", "Zaun fehlt, naechster Block ist laenger"),
("fehlt: Einzug bleibt", MAT, NEUBEGINN, ' start, laenge = i, len(kern) - len(kern.lstrip("`"))', "Zaun fehlt, naechster Block tiefer eingerueckt"),
("main: Vorpruefung aus", MAT, " for zeile, meldung in zaunbefunde(plan / name):", " for zeile, meldung in []:", "ganze Datei klebt, Folgepaar"),
("main: Befund ohne Abbruch", MAT, ' if befunde:\n print(f"{befunde} Befund(e).")\n return 2\n', "", "ganze Datei klebt, Folgepaar"),
("main: nur die erste Taskdatei", MAT, " for name in sys.argv[3:]:\n for zeile, meldung", " for name in sys.argv[3:4]:\n for zeile, meldung", "zweite Taskdatei klebt"),
("Inline-Code oeffnet einen Block", MAT, ' return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0', " return laenge if laenge >= 3 else 0", "Inline-Code am Zeilenanfang oeffnet nichts"),
("Zaun schon ab zwei Backticks", MAT, ' return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0', ' return laenge if laenge >= 2 and "`" not in kern[laenge:] else 0', "Prosa beginnt mit zwei Backticks"),
("Zaun erst ab vier Backticks", MAT, ' return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0', ' return laenge if laenge >= 4 and "`" not in kern[laenge:] else 0', "Grundlinie"),
("Schlusszaun nur in Spalte 0", MAT, ' kern = zeile.lstrip()\n return kern.startswith("`" * laenge)', ' kern = zeile\n return kern.startswith("`" * laenge)', "Eingerueckter Befehl in einer Liste"),
("Schlusszaun mit Text dahinter", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and len(zeile) - len(kern) - einzug <= 3', "Zaun fehlt, Folgepaar"),
("Schlusszaun: Leerzeichen dahinter schliessen nicht", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.strip("`") and len(zeile) - len(kern) - einzug <= 3', "Schlusszaun mit Leerzeichen dahinter"),
("Schlusszaun: zwei Zaeune auf einer Zeile", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.strip("`").strip() and len(zeile) - len(kern) - einzug <= 3', "zwei Zaeune auf einer Zeile schliessen nicht"),
("Schlusszaun: vier Spalten tiefer schliesst", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 4', "Zaun vier Spalten tiefer ist Inhalt, kein Schluss"),
("Schlusszaun: drei Spalten tiefer schliesst nicht", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 2', "Zaun drei Spalten tiefer schliesst"),
("Schlusszaun: Einzug des Oeffners zaehlt nicht", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) <= 3', "eingerueckter Block: Schlusszaun drei Spalten tiefer"),
("Suche: Einzug des Blocks fehlt", MAT, " spalte = len(zeilen[start]) - len(zeilen[start].lstrip())", " spalte = 0", "eingerueckter Block: Schlusszaun drei Spalten tiefer"),
("bloecke(): Einzug des Blocks fehlt", MAT, " while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):\n inhalt.append(zeilen[i][einzug:]", " while i < len(zeilen) and not schliesst(zeilen[i], laenge, 0):\n inhalt.append(zeilen[i][einzug:]", "eingerueckter Block: Schlusszaun drei Spalten tiefer"),
("lesen: UTF-8-Fehler nicht abgefangen", MAT, " except (OSError, UnicodeDecodeError) as fehler:", " except OSError as fehler:", "Taskdatei ist kein UTF-8"),
("lesen: fehlende Datei nicht abgefangen", MAT, " except (OSError, UnicodeDecodeError) as fehler:", " except UnicodeDecodeError as fehler:", "Taskdatei fehlt"),
("bloecke(): eigene Suche nach dem Oeffner", MAT, " laenge = zaun(zeile)\n if laenge:\n einzug", ' laenge = len(zeile.lstrip()) - len(zeile.lstrip().lstrip("`"))\n if laenge >= 3:\n einzug', "Inline-Code am Zeilenanfang oeffnet nichts"),
("plan_bloecke: Pruefung aus", BLO, " kaputt = zaunbefunde(taskdatei)", " kaputt = []", "bloecke vor: T27-Form: Aus und wird kleben"),
("plan_bloecke: Summe zaehlt je Datei", BLO, " befunde += len(kaputt)", " befunde += 1", "bloecke vor: T27-Form: Aus und wird kleben"),
("plan_bloecke: Einzug des Blocks fehlt", BLO, " while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):", " while i < len(zeilen) and not schliesst(zeilen[i], laenge, 0):", "bloecke: eingerueckter Block, Schlusszaun drei Spalten tiefer"),
("plan_bloecke: prueft trotz Befund", BLO, " zaunfehler = True\n continue\n", " zaunfehler = True\n", "bloecke vor: T27-Form: Aus und wird kleben"),
("plan_bloecke: Exit 3 trotz Zaunbefund", BLO, " if geprueft == 0 and not zaunfehler:", " if geprueft == 0:", "bloecke vor: T27-Form: Aus und wird kleben"),
("plan_bloecke: eingerueckter Block zaehlt", BLO, ' if marke in ("Aus:", "wird:") and zeile.startswith("`"):', ' if marke in ("Aus:", "wird:"):', "bloecke: eingerueckter Block nach Aus zaehlt nicht"),
("plan_bloecke: eigene Suche nach dem Oeffner", BLO, " laenge = zaun(zeile)\n if laenge:\n start = i", f' laenge = len(zeile) - len(zeile.lstrip("`"))\n if zeile.startswith("{Z}"):\n start = i', "bloecke: Inline-Code am Zeilenanfang oeffnet nichts"),
("plan_bloecke: Schlusszaun nur in Spalte 0", BLO, " while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):", ' while i < len(zeilen) and not (zeilen[i].startswith("`" * laenge) and not zeilen[i].strip("`").strip()):', "bloecke: eingerueckter Schlusszaun schliesst"),
]
def schreibe(pfad: Path, text) -> None:
pfad.parent.mkdir(parents=True, exist_ok=True)
pfad.write_bytes(text if isinstance(text, bytes) else text.encode("utf-8"))
def zeile_von(text, wortlaut: str, welche: int = 1) -> int:
"""Zeilennummer des welche-ten Vorkommens von wortlaut als ganze Zeile; -1 zaehlt von hinten, wie bei Listen."""
zeilen = text.replace("\r\n", "\n").split("\n")
treffer = [nr for nr, zeile in enumerate(zeilen, start=1) if zeile == wortlaut]
return treffer[welche - 1 if welche > 0 else welche]
def fahre(werkzeuge: Path, art: str, wurzel: Path, plan: Path, namen: list[str]) -> subprocess.CompletedProcess:
if art == "mat":
befehl_ = [str(werkzeuge / "plan_materialisiere.py"), str(plan), str(wurzel / "kopie"), *namen]
else:
befehl_ = [str(werkzeuge / "plan_bloecke.py"), art, *[str(plan / n) for n in namen], "--projekt", str(wurzel / "kopie")]
return subprocess.run([sys.executable, "-X", "utf8", "-B", *befehl_], capture_output=True, text=True,
encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL)
def alle_dateien(ordner: Path) -> dict[str, bytes]:
return {p.relative_to(ordner).as_posix(): p.read_bytes() for p in ordner.rglob("*") if p.is_file()}
def fall(werkzeuge: Path, art: str, task, exit_erwartet: int, erwartet: list, dateien: dict | None) -> tuple[list[str], str]:
"""Faehrt einen Fall in einem frischen Wegwerfordner; Rueckgabe (Gruende fuer Rot, Ausgabe)."""
wurzel = Path(tempfile.mkdtemp(prefix="pz_"))
try:
for rel, text in BASIS.items():
schreibe(wurzel / "kopie" / rel, text)
tasks = task if isinstance(task, dict) else {"task-01.md": task}
for name, text in tasks.items():
if text is not None:
schreibe(wurzel / "plan" / name, text)
lauf = fahre(werkzeuge, art, wurzel, wurzel / "plan", list(tasks))
ausgabe = lauf.stdout + lauf.stderr
gruende = []
if lauf.returncode != exit_erwartet:
gruende.append(f"Exit {lauf.returncode}, erwartet {exit_erwartet}")
if "Traceback" in ausgabe:
gruende.append("Traceback in der Ausgabe")
for wort, wortlaut, welche, datei in erwartet:
kopf = f"BEFUND {datei}:"
if wortlaut is not None:
kopf += f"{zeile_von(tasks[datei], wortlaut, welche)}:"
if not any(z.startswith(kopf) and wort in z for z in ausgabe.split("\n")):
gruende.append(f"Meldung fehlt: {kopf} ... {wort}")
if ausgabe.count("BEFUND ") != len(erwartet):
gruende.append(f"{ausgabe.count('BEFUND ')} BEFUND-Zeile(n), erwartet {len(erwartet)}")
if f"{len(erwartet)} Befund(e)." not in ausgabe.split("\n"):
gruende.append(f"Summenzeile fehlt: {len(erwartet)} Befund(e).")
if dateien is not None:
soll = {rel: text.encode("utf-8") for rel, text in {**BASIS, **dateien}.items() if text is not None}
ist = alle_dateien(wurzel / "kopie")
for rel in sorted(set(soll) | set(ist)):
if soll.get(rel) != ist.get(rel):
gruende.append(f"{rel}: {ist.get(rel)!r}, erwartet {soll.get(rel)!r}")
return gruende, ausgabe
finally:
shutil.rmtree(wurzel, ignore_errors=True)
def alle_faelle(werkzeuge: Path, laut: bool) -> list[str]:
"""Faehrt alle Faelle, Grundlinie zuerst; Rueckgabe: Namen der roten Faelle."""
rot = []
for name, art, task, exit_erwartet, erwartet, dateien in FAELLE:
gruende, ausgabe = fall(werkzeuge, art, task, exit_erwartet, erwartet, dateien)
if laut:
print(f"{'ok ' if not gruende else 'FEHLER'} {name}")
if gruende:
rot.append(name)
if laut:
for grund in gruende:
print(" " + grund[:300])
print(" " + ausgabe.strip().replace("\n", "\n ")[:900])
if name == "Grundlinie":
if laut:
print("Die Grundlinie ist nicht sauber -- der Rest misst nichts.")
return rot
return rot
def mutanten_lauf(werkzeuge: Path) -> int:
"""Baut je Mutant einen Fehler in eine Kopie der Werkzeuge ein; Rueckgabe: Mutanten ohne Rot an ihrem Fall."""
fehler = 0
for name, datei, alt, neu, soll in MUTANTEN:
ordner = Path(tempfile.mkdtemp(prefix="pm_"))
try:
shutil.copy(werkzeuge / MAT, ordner / MAT)
shutil.copy(werkzeuge / BLO, ordner / BLO)
text = (ordner / datei).read_text(encoding="utf-8")
if text.count(alt) != 1:
print(f"FEHLER {name}: der Text steht {text.count(alt)}-mal in {datei}")
fehler += 1
continue
(ordner / datei).write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
rot = alle_faelle(ordner, False)
print(f"{'gefangen ' if soll in rot else 'UEBERLEBT'} {name}: {len(rot)} rot, erwartet an: {soll}")
fehler += 0 if soll in rot else 1
finally:
shutil.rmtree(ordner, ignore_errors=True)
print(f"{fehler} Mutant(en) nicht an ihrem Fall gefangen, von {len(MUTANTEN)}.")
return fehler
def main() -> int:
zerleger = argparse.ArgumentParser()
zerleger.add_argument("--werkzeuge", default=str(Path(__file__).resolve().parent))
zerleger.add_argument("--mutanten", action="store_true")
argumente = zerleger.parse_args()
werkzeuge = Path(argumente.werkzeuge)
rot = alle_faelle(werkzeuge, True)
print(f"{len(rot)} Fall/Faelle mit falschem Ausgang von {len(FAELLE)}.")
if rot:
return 2
return 2 if argumente.mutanten and mutanten_lauf(werkzeuge) else 0
if __name__ == "__main__":
sys.exit(main())