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:
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())
|
||||
Reference in new issue
Block a user