Automatischer Push: 11 Datei(en) geändert

- .claude/agents/planer.md
- .claude/workflows/plankritik.js
- .vault-tools/plan-kopie/LIESMICH.md
- .vault-tools/plan-kopie/mutanten_lauf.py
- .vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md
- .vault-tools/plan-kopie/mutanten_lauf_kopie.sh
- .vault-tools/plan-kopie/mutanten_lauf_treiber.py
- .vault-tools/workflow-pruefung/faelle_workflows.mjs
- .vault-tools/workflow-pruefung/mutanten_workflows.py
- AI/Regelwerk/codeworkflow.md
- CLAUDE.md

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-09-28 04:00:02 +02:00
1 parent 3b7128c9e1
commit 1be4f913aa
11 files changed
+2214 -10

No files matched your search

+16 -1
View File
@@ -5,7 +5,7 @@ title: "Planwerkzeuge: Plan auf eine Kopie anwenden und jede Welle dagegen vergl
summary: "Skripte für die Standardschleife bei Plänen mit ausgeschriebenem Code — schlanke Kopie bauen, Plan auf die Kopie anwenden, je Welle Menge und Bytes vergleichen, Aus-wird-Blöcke prüfen, Codeblöcke auf unsichtbare Zeichen absuchen, Escape-Folgen per Codepunkt setzen. Entstanden 2026-09-17 in der Trading Plattform (Teil 3b)."
tags: [vault-tools, codeworkflow]
created: 2026-09-17
updated: 2026-09-24
updated: 2026-09-27
---
# Planwerkzeuge für die Kopie mit angewendetem Plan
@@ -23,8 +23,23 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko
7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test] [--venv <wsl-pfad>] [--frontend <ordner>] [--projektname <ordnername>]`. **Für ein anderes Projekt als die Trading Plattform braucht es die drei letzten Schalter (seit 2026-09-24):** Ihre Vorgaben sind die der Trading Plattform (`~/venvs/trading`, `dashboard`, `trading-plattform`). Jedes andere Projekt nennt seine Werte, für den Nexhorizons Hub `--venv '~/venvs/hub' --frontend frontend --projektname 'active - Nexhorizons Hub'`, das Venv in einfachen Anführungszeichen, sonst macht die Git-Bash aus `~` einen Windows-Pfad. Das Skript druckt die benutzten Werte in der ersten Zeile. Die Vorgabe `trading-plattform` passt nur auf Pläne mit Arbeitsbaum-Pfaden; ältere Trading-Pläne mit absoluten Pfaden unter `active - Trading Plattform` brauchen `--projektname 'active - Trading Plattform'`, sonst endet der Lauf mit einem `ValueError`. Weiter fest verdrahtet bleiben die Distribution `Debian`, die Ordner `tests/` und `tests/fixtures/`, die Endungen `.test.ts`/`.test.tsx` und `npx.cmd vitest`. **Vorbedingung:** Das Projekt ist im Venv nicht editierbar installiert (`pip install -e`); sonst löst der Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung läuft grün. `tests/conftest.py` gilt dabei als Produktivseite und wird zurückgesetzt; nur wenn die neue Fixture eines Tasks dort liegt, lässt `--conftest-als-test` sie stehen (Beleg unten, 2026-09-22). `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich.
8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. Es fährt zuerst die Grundlinie, dann je Mutant nur die genannten Tests, setzt die Datei zurück und prüft das per SHA-256. Je Test gibt es die Fehlermeldung mit aus; ob ein Test an der gemeinten Stelle fällt, liest man dort. `--nur-muster` prüft nur, ob jedes Muster genau einmal dasteht, ohne pytest. Die Kopie legt man in WSL unter `/tmp` an (`cp -a`, danach `__pycache__` löschen). Auf `/mnt/d` läuft es auch, nur langsamer. **Aus der Git-Bash ein Skript nie mit `wsl … bash /mnt/d/…` starten:** Die Git-Bash macht aus dem Pfad einen Windows-Pfad. Aus der PowerShell starten oder `MSYS_NO_PATHCONV=1` davorsetzen. **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27):** Liegt `mutanten.toml` im Planordner, startet `plankritik.js` in der Vorbereitung `mutanten_lauf_kopie.sh`. Das Skript kopiert die Kopie mit angewendetem Plan nach `/tmp` in WSL, fährt dort den Läufer und gibt die Ausgabe an die Linse `arithmetik`. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`. Der ganze Lauf hat 540 s, weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. B1 mit 74 Mutanten brauchte 147 s. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes.
## Was belegt ist
Seit 2026-09-27 gibt es `mutanten_lauf.py` und `mutanten_lauf_treiber.py` (Punkt 3 der Arbeitsweise vom 2026-09-26). Der Treiber fährt die 34 Angriffsfälle aus dem Entwurf auf einem kleinen Projekt unter `/tmp` und prüft nach jedem Fall, dass das Projekt byte-gleich zurückgesetzt ist. Am selben Tag hat der Koordinator neun Fehler in eine Kopie des Läufers eingebaut, und der Treiber hat jeden an seinem Fall gefangen:
- „passed“ zählt als rot
- kein Zurücksetzen
- keine `.git`-Prüfung
- `aus` gleich `wird` erlaubt
- HOME nicht abgedichtet
- Sammelfehler nicht rot
- Grundlinie lässt Übersprungenes durch
- Zeitlimit zählt als OK
- eine Teilzeile zählt als Treffer
Das Skript dazu liegt außerhalb des Vaults und ist nicht Teil der Werkzeuge. Erster echter Einsatz war am selben Tag B1 der Trading Plattform: 74 von 74 Mutanten getötet, alle Fehlschläge Zusicherungen, die Kopie danach unverändert. Eine Lehre aus der Übertragung: Nennt ein Plan einen parametrisierten Test ohne Fall, gehören nur die Fälle in `rot_bei`, die der Mutant umkippt. Bei D1 wären drei von fünf Fällen unter `return False` grün geblieben.
Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17.
Seit 2026-09-24 hat `rot_nachweis.py` die Schalter `--venv`, `--frontend` und `--projektname`; vorher waren `~/venvs/trading`, `dashboard` und `trading-plattform/` fest verdrahtet. Ein Hub-Lauf im Trading-Venv wäre für die Tasks mit `argon2-cffi` und `pyotp` rot aus einem fehlenden Paket gewesen, nicht aus der fehlenden Umsetzung. Fehlt das Venv in WSL, steht beim Task jetzt `VORAUSSETZUNG FEHLT` statt `ROT`, und die Schlusszeile zählt diese Läufe getrennt. Belegt am selben Tag: Der Vorgabe-Befehl ist byte-gleich mit der alten Fassung, falsche Schalterwerte enden mit Exit 3, und der erste echte Lauf über 38 Tasks des Hub meldete keinen grün ohne Umsetzung. Im selben Lauf zeigte sich eine zweite Lücke: Beim Zurücksetzen blieben Ordner stehen, die erst der Task angelegt hatte. Ein leeres `hub/auth/` liest Python als Namespace-Paket, und die Fehlerzeile nannte `hub.auth.passwort` statt `hub.auth`. Jetzt gehen solche Ordner mit, sobald sie leer sind; derselbe Lauf für die Tasks 7, 9 und 11 zeigt danach genau die Zeile aus dem Plan. Die unabhängige Prüfung desselben Tages fand eine dritte Lücke: Ein vergessenes `--venv` fiel still auf das Trading-Venv zurück, und Task 7 stand als `ROT … No module named 'argon2'` da. Seitdem wird eine erste Fehlerzeile, die ein Modul außerhalb des Projekts und außerhalb der Zieldateien vermisst, als `VORAUSSETZUNG FEHLT` gemeldet. Das trifft auch den Ende-zu-Ende-Task des Hub, dessen Tests `playwright` brauchen. Mit den Vorgaben lieferte das Skript für die sieben Tasks des Trading-Pakets K vor und nach allen Änderungen dieselben Zeilen. Zwei weitere Lücken desselben Tages, beide ein ROT, das nichts misst: **Ein Arbeitsordner mit Windows-Kurznamen** (`C:/Users/PASCAL~1/…`) ließ vitest dort starten, die Dateien aber über den langen Namen auflösen, und jede Oberflächen-Testdatei endete als `Cannot find module '/src/…'`. Das Skript löst den Arbeitsordner jetzt auf den langen Namen auf; danach zeigte Trading-Task 395 wieder `9 failed | 52 passed` statt `no tests`, und die Hub-Tasks 24, 25, 29 und 37 die Zeile aus dem Plan (`Error: Failed to resolve import "./kontrast" …`). Die erste `Error:`-Zeile steht seitdem mit in der Ergebniszeile, weil die `FAIL`-Zeile allein den Grund nicht zeigt. **Ein Test, der über Vite eine Datei aus `node_modules` lädt** (`?raw`), scheitert hinter der Junction mit `Denied ID`, weil Vite nichts außerhalb seines Wurzelordners ausliefert; das Skript meldet das als `VORAUSSETZUNG FEHLT`. Für solche Tasks läuft der Nachweis in einer Kopie mit echtem `node_modules` (242 MB, gut 20 Sekunden).
+853
View File
@@ -0,0 +1,853 @@
#!/usr/bin/env python3
"""Allgemeiner Mutantenlaeufer aus einer mutanten.toml.
Zweck: Fuer eine Liste von Mutanten (Textersetzungen in einer Datei) prueft
dieses Skript, ob die dazu genannten Tests unter jedem Mutanten wie erwartet
rot werden ("getoetet"). Es liest dazu genau eine Datei im Format aus dem
Entwurf (Abschnitt 2), nicht Prosa aus einem Plan.
Aufruf (in WSL, Python ab 3.11, nur Standardbibliothek):
python mutanten_lauf.py --wurzel <kopie> --mutanten <mutanten.toml>
[--nur ID,ID,...] [--nur-muster] [--bericht <datei.json>]
[--zeitlimit <sekunden>]
Geltungsbereich: Dieses Werkzeug ist ein Fangnetz, kein Nachweis. Es sagt,
ob die genannten Tests unter einer Aenderung fallen - nicht, ob sie an der
gemeinten Stelle fallen. Das zeigt nur die mitgelieferte Fehlermeldung, das
Urteil bleibt beim Menschen.
Benannte Luecken der Abdichtung (Entwurf, Abschnitt 5): Ein Unterprozess,
den ein Test mit absolutem Pfad startet, ist nicht abgedichtet. Ein Socket
ohne Proxy-Nutzung ist nicht abgedichtet. Ein HTTP-Client mit
trust_env=False oder eigenem Transport ist nicht abgedichtet. Dateien mit
absolutem Pfad ausserhalb von HOME sind nicht abgedichtet. Die eigentliche
Sperre bleibt die Netzsperre der Testdateien selbst und eine Kopie ohne
Geheimnisdateien (der Schluessel "ohne" in mutanten.toml).
Spezifikation: .vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md
Entscheidungen, die der Entwurf offen liess (siehe Bericht des Executers):
Verhalten bei einer Zieldatei, die kein gueltiges UTF-8 ist, wird wie ein
Formatbefund behandelt (Abbruch mit Exit 2), weil das Skript sonst keine
verlaessliche Aussage ueber die Trefferzahl treffen kann.
"""
import argparse
import hashlib
import json
import os
import re
import shutil
import signal
import subprocess
import sys
import tempfile
import tomllib
import traceback
from pathlib import Path
PLUGIN_NAME = "_mutanten_lauf_plugin"
PLUGIN_QUELLTEXT = '''"""Pytest-Plugin des Mutantenlaeufers, wird in einen Temp-Ordner geschrieben.
Schreibt je Bericht eine JSON-Zeile in die Datei aus der Umgebungsvariable
MUTANTEN_LAUF_ERGEBNIS. Siehe mutanten_lauf_ENTWURF.md, Abschnitt 3.
"""
import json
import os
def _ergebnis_pfad():
return os.environ.get("MUTANTEN_LAUF_ERGEBNIS")
def _schreiben(datensatz):
pfad = _ergebnis_pfad()
if not pfad:
return
with open(pfad, "a", encoding="utf-8") as datei:
datei.write(json.dumps(datensatz, ensure_ascii=True))
datei.write("\\n")
datei.flush()
def _erste_zeile(text):
for zeile in str(text).splitlines():
zeile = zeile.strip()
if zeile:
return zeile[:300]
return ""
def _meldung_aus_longrepr(longrepr):
if longrepr is None:
return ""
crash = getattr(longrepr, "reprcrash", None)
meldung = getattr(crash, "message", None)
if meldung:
return str(meldung)[:300]
return _erste_zeile(longrepr)
def pytest_runtest_logreport(report):
meldung = ""
if report.outcome == "failed":
meldung = _meldung_aus_longrepr(getattr(report, "longrepr", None))
datensatz = {
"art": "test",
"knoten": report.nodeid,
"phase": report.when,
"ergebnis": report.outcome,
"xfail": hasattr(report, "wasxfail"),
"meldung": meldung,
}
_schreiben(datensatz)
def pytest_collectreport(report):
if report.outcome == "failed":
meldung = _meldung_aus_longrepr(getattr(report, "longrepr", None))
_schreiben({"art": "sammeln", "knoten": report.nodeid, "meldung": meldung})
def pytest_deselected(items):
for item in items:
_schreiben({"art": "abgewaehlt", "knoten": item.nodeid})
'''
TOP_LEVEL_SCHLUESSEL = {"format", "python", "ohne", "kuerzel", "python_je_datei", "mutant"}
MUTANT_SCHLUESSEL = {"id", "datei", "aus", "wird", "rot_bei", "hinweis"}
class RuecksetzenGescheitert(Exception):
def __init__(self, mutant_id, datei):
super().__init__("Ruecksetzen gescheitert bei Mutant {0} ({1})".format(mutant_id, datei))
def ist_relativer_pfad(pfad):
if not isinstance(pfad, str) or pfad == "":
return False
if pfad.startswith("/") or pfad.startswith("~"):
return False
if len(pfad) >= 2 and pfad[1] == ":":
return False
teile = pfad.split("/")
if ".." in teile:
return False
return True
def zeilen_aus_text(text):
if text == "":
return []
if text.endswith("\n"):
text = text[:-1]
return text.split("\n")
def positionen_finden(datei_zeilen, muster_zeilen):
treffer = []
n = len(muster_zeilen)
if n == 0:
return treffer
obergrenze = len(datei_zeilen) - n + 1
for i in range(max(obergrenze, 0)):
if datei_zeilen[i:i + n] == muster_zeilen:
treffer.append(i)
return treffer
def kuerzel_aufloesen(eintrag, kuerzel_tabelle):
if "::" not in eintrag:
return None
vorderteil, rest = eintrag.split("::", 1)
if vorderteil in kuerzel_tabelle:
return kuerzel_tabelle[vorderteil] + "::" + rest
if ist_relativer_pfad(vorderteil) and vorderteil.endswith(".py"):
return vorderteil + "::" + rest
return None
def interpreter_gueltig(pfad):
return isinstance(pfad, str) and pfad != "" and "~" not in pfad and pfad.startswith("/") \
and os.path.isfile(pfad) and os.access(pfad, os.X_OK)
def lade_und_validiere(args):
"""Schritt 1 des Ablaufs: Vorbedingungen und Format (Entwurf, Abschnitt 3.1)."""
befunde = []
wurzel = Path(args.wurzel)
wurzel_ok = wurzel.exists() and wurzel.is_dir()
if not wurzel_ok:
befunde.append("Wurzel fehlt oder ist kein Ordner: {0}".format(wurzel))
if wurzel_ok:
git_pfad = wurzel / ".git"
if git_pfad.exists() or git_pfad.is_symlink():
befunde.append("Wurzel enthaelt .git: {0}".format(git_pfad))
mutanten_pfad = Path(args.mutanten)
daten = None
try:
with open(mutanten_pfad, "rb") as datei:
daten = tomllib.load(datei)
except FileNotFoundError:
befunde.append("Mutanten-Datei fehlt: {0}".format(mutanten_pfad))
except tomllib.TOMLDecodeError as exc:
befunde.append("TOML nicht lesbar: {0}".format(exc))
except OSError as exc:
befunde.append("TOML nicht lesbar: {0}".format(exc))
kontext = {
"wurzel": wurzel,
"mutanten_pfad": mutanten_pfad,
"python": None,
"ohne": [],
"kuerzel": {},
"python_je_datei": {},
"mutanten": [],
}
if daten is None:
return befunde, kontext
if not isinstance(daten, dict):
befunde.append("Mutanten-Datei ist auf oberster Ebene keine Tabelle")
return befunde, kontext
unbekannt = set(daten.keys()) - TOP_LEVEL_SCHLUESSEL
for schluessel in sorted(unbekannt):
befunde.append("unbekannter Schluessel auf oberster Ebene: {0}".format(schluessel))
if "format" not in daten:
befunde.append("format fehlt")
else:
format_wert = daten.get("format")
if not isinstance(format_wert, int) or isinstance(format_wert, bool) or format_wert != 1:
befunde.append("format muss die Ganzzahl 1 sein, ist {0!r}".format(format_wert))
if "python" not in daten:
befunde.append("python fehlt")
else:
python_wert = daten.get("python")
if not isinstance(python_wert, str) or python_wert == "":
befunde.append("python ist kein nicht-leerer Text")
elif "~" in python_wert:
befunde.append("python enthaelt ~: {0}".format(python_wert))
elif not python_wert.startswith("/"):
befunde.append("python ist kein absoluter Pfad: {0}".format(python_wert))
elif not (os.path.isfile(python_wert) and os.access(python_wert, os.X_OK)):
befunde.append("python fehlt oder ist nicht ausfuehrbar: {0}".format(python_wert))
else:
kontext["python"] = python_wert
ohne_wert = []
if "ohne" in daten:
roh = daten.get("ohne")
if not isinstance(roh, list):
befunde.append("ohne ist keine Liste")
else:
for eintrag in roh:
if not ist_relativer_pfad(eintrag):
befunde.append("ohne enthaelt keinen relativen Pfad: {0!r}".format(eintrag))
else:
ohne_wert.append(eintrag)
kontext["ohne"] = ohne_wert
if wurzel_ok:
for eintrag in ohne_wert:
ziel = wurzel / eintrag
if ziel.exists() or ziel.is_symlink():
befunde.append("Pfad aus ohne existiert: {0}".format(eintrag))
kuerzel_wert = {}
if "kuerzel" in daten:
roh = daten.get("kuerzel")
if not isinstance(roh, dict):
befunde.append("kuerzel ist keine Tabelle")
else:
for name, pfad in roh.items():
if not ist_relativer_pfad(pfad) or not str(pfad).endswith(".py"):
befunde.append("kuerzel {0} zeigt auf keinen gueltigen relativen Pfad: {1!r}".format(name, pfad))
else:
kuerzel_wert[name] = pfad
kontext["kuerzel"] = kuerzel_wert
pjd_wert = {}
if "python_je_datei" in daten:
roh = daten.get("python_je_datei")
if not isinstance(roh, dict):
befunde.append("python_je_datei ist keine Tabelle")
else:
for pfad, interpreter in roh.items():
if not ist_relativer_pfad(pfad) or not str(pfad).endswith(".py"):
befunde.append("python_je_datei-Schluessel ist kein gueltiger relativer Pfad: {0!r}".format(pfad))
continue
if not interpreter_gueltig(interpreter):
befunde.append("Interpreter in python_je_datei fehlt oder ist nicht ausfuehrbar: {0!r}".format(interpreter))
continue
pjd_wert[pfad] = interpreter
kontext["python_je_datei"] = pjd_wert
if "mutant" not in daten:
befunde.append("mutant fehlt")
mutant_liste_roh = []
else:
mutant_liste_roh = daten.get("mutant")
if not isinstance(mutant_liste_roh, list) or len(mutant_liste_roh) == 0:
befunde.append("mutant ist keine nicht-leere Liste")
mutant_liste_roh = []
gesehene_ids = set()
mutanten_normalisiert = []
for index, eintrag in enumerate(mutant_liste_roh):
vorspann = "Mutant Nr. {0}".format(index + 1)
if not isinstance(eintrag, dict):
befunde.append("{0} ist keine Tabelle".format(vorspann))
continue
unbekannt = set(eintrag.keys()) - MUTANT_SCHLUESSEL
for schluessel in sorted(unbekannt):
befunde.append("{0}: unbekannter Schluessel {1}".format(vorspann, schluessel))
mid = eintrag.get("id")
if not isinstance(mid, str) or mid == "":
befunde.append("{0}: id fehlt oder ist leer".format(vorspann))
mid = None
elif mid in gesehene_ids:
befunde.append("doppelte id: {0}".format(mid))
else:
gesehene_ids.add(mid)
bezeichner = mid if mid else vorspann
datei = eintrag.get("datei")
datei_ok = False
if not isinstance(datei, str) or datei == "":
befunde.append("{0}: datei fehlt oder ist leer".format(bezeichner))
elif not ist_relativer_pfad(datei):
befunde.append("{0}: datei ist kein gueltiger relativer Pfad: {1!r}".format(bezeichner, datei))
elif wurzel_ok:
ziel = wurzel / datei
if ziel.is_symlink():
befunde.append("{0}: datei ist eine Verknuepfung: {1}".format(bezeichner, datei))
elif not ziel.is_file():
befunde.append("{0}: datei existiert nicht: {1}".format(bezeichner, datei))
else:
datei_ok = True
aus = eintrag.get("aus")
if not isinstance(aus, str) or aus == "":
befunde.append("{0}: aus fehlt oder ist leer".format(bezeichner))
aus = None
wird = eintrag.get("wird")
if not isinstance(wird, str):
befunde.append("{0}: wird fehlt oder ist kein Text".format(bezeichner))
wird = None
if aus is not None and wird is not None and aus == wird:
befunde.append("{0}: aus ist gleich wird (wirkungslos)".format(bezeichner))
rot_bei_roh = eintrag.get("rot_bei")
rot_bei = []
if not isinstance(rot_bei_roh, list) or len(rot_bei_roh) == 0:
befunde.append("{0}: rot_bei ist keine nicht-leere Liste".format(bezeichner))
else:
for roheintrag in rot_bei_roh:
if not isinstance(roheintrag, str) or roheintrag == "":
befunde.append("{0}: rot_bei enthaelt keinen nicht-leeren Text: {1!r}".format(bezeichner, roheintrag))
continue
aufgeloest = kuerzel_aufloesen(roheintrag, kuerzel_wert)
if aufgeloest is None:
befunde.append("{0}: unbekanntes Kuerzel oder ungueltiger Eintrag in rot_bei: {1!r}".format(bezeichner, roheintrag))
else:
rot_bei.append((roheintrag, aufgeloest))
hinweis = eintrag.get("hinweis")
if "hinweis" in eintrag and not isinstance(hinweis, str):
befunde.append("{0}: hinweis ist kein Text".format(bezeichner))
hinweis = None
if mid is not None and datei_ok and aus is not None and wird is not None and rot_bei:
mutanten_normalisiert.append({
"id": mid,
"datei": datei,
"aus": aus,
"wird": wird,
"rot_bei": rot_bei,
"hinweis": hinweis,
})
kontext["mutanten"] = mutanten_normalisiert
return befunde, kontext
def muster_pruefen(mutant, wurzel):
"""Schritt 2 des Ablaufs (Entwurf, Abschnitt 3.2)."""
ziel = wurzel / mutant["datei"]
rohbytes = ziel.read_bytes()
try:
text = rohbytes.decode("utf-8", errors="strict")
except UnicodeDecodeError as exc:
return {"kodierungsfehler": str(exc)}
datei_zeilen = text.split("\n")
aus_zeilen = zeilen_aus_text(mutant["aus"])
wird_zeilen = zeilen_aus_text(mutant["wird"])
treffer_positionen = positionen_finden(datei_zeilen, aus_zeilen)
return {
"kodierungsfehler": None,
"treffer": len(treffer_positionen),
"start": treffer_positionen[0] if len(treffer_positionen) == 1 else None,
"aus_zeilen": aus_zeilen,
"wird_zeilen": wird_zeilen,
"cr_in_datei": "\r" in text,
}
def gruppiere_nach_interpreter(knoten_liste, standard_python, python_je_datei):
gruppen = {}
for knoten in knoten_liste:
dateipfad = knoten.split("::", 1)[0]
interpreter = python_je_datei.get(dateipfad, standard_python)
gruppen.setdefault(interpreter, []).append(knoten)
return gruppen
def umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad):
"""Abdichtung nach Entwurf, Abschnitt 5. Siehe Kopfkommentar fuer die Luecken."""
env = os.environ.copy()
env["HOME"] = str(home_dir)
for schluessel in ("http_proxy", "https_proxy", "all_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"):
env[schluessel] = "http://127.0.0.1:9"
for schluessel in ("no_proxy", "NO_PROXY"):
env.pop(schluessel, None)
env["PYTHONDONTWRITEBYTECODE"] = "1"
env["COLUMNS"] = "300"
env["MUTANTEN_LAUF_ERGEBNIS"] = ergebnis_pfad
vorhanden = env.get("PYTHONPATH", "")
env["PYTHONPATH"] = str(plugin_dir) + (os.pathsep + vorhanden if vorhanden else "")
return env
def pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimit):
ergebnis_datei = tempfile.NamedTemporaryFile(prefix="mutanten_lauf_ergebnis_", suffix=".jsonl", delete=False)
ergebnis_datei.close()
ergebnis_pfad = ergebnis_datei.name
env = umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad)
cmd = [python_exe, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-p", PLUGIN_NAME] + list(knoten_liste)
zeit_ueberschritten = False
absturz = None
try:
proc = subprocess.Popen(
cmd, cwd=str(wurzel), env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
start_new_session=True,
)
try:
proc.communicate(timeout=zeitlimit)
except subprocess.TimeoutExpired:
zeit_ueberschritten = True
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except (ProcessLookupError, PermissionError):
pass
proc.communicate()
except OSError as exc:
absturz = "pytest-Aufruf gescheitert ({0}): {1}".format(python_exe, exc)
meldungen = []
try:
with open(ergebnis_pfad, "r", encoding="utf-8") as datei:
for zeile in datei:
zeile = zeile.strip()
if not zeile:
continue
try:
meldungen.append(json.loads(zeile))
except ValueError:
pass
except OSError:
pass
finally:
try:
os.remove(ergebnis_pfad)
except OSError:
pass
return zeit_ueberschritten, meldungen, absturz
def gruppen_ausfuehren(gruppen, wurzel, home_dir, plugin_dir, zeitlimit):
reports = {}
deselected = set()
sammelfehler = {}
zeit_ueberschritten = False
absturzmeldungen = []
for python_exe, knoten_liste in gruppen.items():
zeit, meldungen, absturz = pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimit)
if zeit:
zeit_ueberschritten = True
if absturz:
absturzmeldungen.append(absturz)
for m in meldungen:
art = m.get("art")
if art == "test":
reports.setdefault(m.get("knoten"), []).append({
"phase": m.get("phase"),
"ergebnis": m.get("ergebnis"),
"xfail": bool(m.get("xfail")),
"meldung": m.get("meldung", ""),
})
elif art == "sammeln":
sammelfehler[m.get("knoten")] = m.get("meldung", "")
elif art == "abgewaehlt":
deselected.add(m.get("knoten"))
return reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen
def ergebnis_bestimmen(knoten, reports, deselected):
if knoten in deselected:
return "ABGEWAEHLT", ""
phasen = reports.get(knoten, [])
for p in phasen:
if p["ergebnis"] == "failed" and not p["xfail"]:
return "ROT", p["meldung"]
for p in phasen:
if p["ergebnis"] == "skipped" or p["xfail"]:
return "UEBERSPRUNGEN", p["meldung"]
for p in phasen:
if p["phase"] == "call" and p["ergebnis"] == "passed":
return "GRUEN", ""
return "FEHLT", ""
def sammelfehler_fuer(knoten, sammelfehler_tabelle):
dateipfad = knoten.split("::", 1)[0]
for cf_knoten, meldung in sammelfehler_tabelle.items():
cf_datei = cf_knoten.split("::", 1)[0]
if cf_knoten == dateipfad or cf_datei == dateipfad:
return meldung
if dateipfad == cf_knoten or dateipfad.startswith(cf_knoten + "::") or dateipfad.startswith(cf_knoten + "/"):
return meldung
return None
def baseline_pruefen(kontext, ausgewaehlte_mutanten, home_dir, plugin_dir, zeitlimit):
"""Schritt 3 des Ablaufs (Entwurf, Abschnitt 3.3)."""
alle_tests = []
gesehen = set()
for mutant in ausgewaehlte_mutanten:
for _, knoten in mutant["rot_bei"]:
if knoten not in gesehen:
gesehen.add(knoten)
alle_tests.append(knoten)
gruppen = gruppiere_nach_interpreter(alle_tests, kontext["python"], kontext["python_je_datei"])
reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen = gruppen_ausfuehren(
gruppen, kontext["wurzel"], home_dir, plugin_dir, zeitlimit,
)
befunde = list(absturzmeldungen)
ergebnis_je_knoten = {}
for knoten in alle_tests:
if zeit_ueberschritten:
status, meldung = "ZEIT", "Zeitlimit in der Grundlinie ueberschritten"
else:
status, meldung = ergebnis_bestimmen(knoten, reports, deselected)
ergebnis_je_knoten[knoten] = {"ergebnis": status, "meldung": meldung}
if status != "GRUEN":
zusatz = " ({0})".format(meldung) if meldung else ""
befunde.append("Grundlinie: {0} ist {1}{2}".format(knoten, status, zusatz))
return befunde, ergebnis_je_knoten
def mutant_anwenden_und_pruefen(mutant, treffer_start, aus_zeilen, wird_zeilen, wurzel,
home_dir, plugin_dir, python_je_datei, standard_python, zeitlimit):
"""Schritt 4 des Ablaufs, ein Mutant (Entwurf, Abschnitt 3.4)."""
ziel = wurzel / mutant["datei"]
original_bytes = ziel.read_bytes()
sha_vorher = hashlib.sha256(original_bytes).hexdigest()
datei_zeilen = original_bytes.decode("utf-8").split("\n")
neue_zeilen = datei_zeilen[:treffer_start] + wird_zeilen + datei_zeilen[treffer_start + len(aus_zeilen):]
neuer_inhalt = "\n".join(neue_zeilen).encode("utf-8")
status = "UNKLAR"
test_ergebnisse = []
try:
ziel.write_bytes(neuer_inhalt)
knoten_liste = [knoten for _, knoten in mutant["rot_bei"]]
gruppen = gruppiere_nach_interpreter(knoten_liste, standard_python, python_je_datei)
reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen = gruppen_ausfuehren(
gruppen, wurzel, home_dir, plugin_dir, zeitlimit,
)
if zeit_ueberschritten:
status = "ZEIT"
for eintrag, knoten in mutant["rot_bei"]:
test_ergebnisse.append({"eintrag": eintrag, "knoten": knoten, "ergebnis": "ZEIT", "meldung": ""})
else:
unklar = bool(absturzmeldungen)
luecke = False
if not reports and not sammelfehler and not deselected and not absturzmeldungen:
unklar = True
for eintrag, knoten in mutant["rot_bei"]:
ergebnis, meldung = ergebnis_bestimmen(knoten, reports, deselected)
if ergebnis == "FEHLT":
sammel_meldung = sammelfehler_fuer(knoten, sammelfehler)
if sammel_meldung is not None:
ergebnis = "ROT"
meldung = "Sammelfehler: " + sammel_meldung
else:
unklar = True
if ergebnis in ("GRUEN", "UEBERSPRUNGEN", "ABGEWAEHLT"):
luecke = True
test_ergebnisse.append({"eintrag": eintrag, "knoten": knoten, "ergebnis": ergebnis, "meldung": meldung})
if unklar:
status = "UNKLAR"
elif luecke:
status = "LUECKE"
else:
status = "OK"
finally:
ziel.write_bytes(original_bytes)
sha_nachher = hashlib.sha256(ziel.read_bytes()).hexdigest()
if sha_nachher != sha_vorher:
raise RuecksetzenGescheitert(mutant["id"], mutant["datei"])
return status, test_ergebnisse
def format_meldung(meldung, laenge):
meldung = (meldung or "").replace("\n", " ")
if len(meldung) > laenge:
meldung = meldung[:max(laenge - 3, 0)] + "..."
return meldung
def format_mutant_zeile(mutant, status, test_ergebnisse, muster_ergebnis):
stuecke = []
for eintrag in test_ergebnisse:
stuecke.append("{0}={1}({2})".format(eintrag["eintrag"], eintrag["ergebnis"], format_meldung(eintrag["meldung"], 120)))
if status == "MUSTER":
stuecke.append("Trefferzahl={0}".format(muster_ergebnis["treffer"]))
if muster_ergebnis.get("cr_in_datei"):
stuecke.append("Datei hat CR")
zeile = "{0} {1} {2}".format(mutant["id"], status, " ".join(stuecke))
return zeile.rstrip()
def python_version_ermitteln(interpreter):
try:
ergebnis = subprocess.run([interpreter, "--version"], capture_output=True, text=True, timeout=30)
text = (ergebnis.stdout or "") + (ergebnis.stderr or "")
return text.strip()
except OSError as exc:
return "nicht ermittelbar ({0})".format(exc)
def kopfzeilen_ausgeben(kontext, ausgewaehlte_mutanten):
print("wurzel: {0}".format(kontext["wurzel"]))
print("mutanten-datei: {0}".format(kontext["mutanten_pfad"]))
print("mutanten gesamt: {0}, ausgewaehlt: {1}".format(len(kontext["mutanten"]), len(ausgewaehlte_mutanten)))
interpreter_menge = {kontext["python"]}
interpreter_menge.update(kontext["python_je_datei"].values())
for interpreter in sorted(interpreter_menge):
print("python {0}: {1}".format(interpreter, python_version_ermitteln(interpreter)))
def zusammenfassung_rechnen(mutant_liste):
zaehler = {"OK": 0, "LUECKE": 0, "UNKLAR": 0, "ZEIT": 0, "MUSTER": 0}
for m in mutant_liste:
if m["status"] in zaehler:
zaehler[m["status"]] += 1
zaehler["gesamt"] = len(mutant_liste)
return zaehler
def zusammenfassung_zeile(z):
return "ergebnis: {0} Mutanten, {1} OK, {2} LUECKE, {3} UNKLAR, {4} ZEIT, {5} MUSTER".format(
z["gesamt"], z["OK"], z["LUECKE"], z["UNKLAR"], z["ZEIT"], z["MUSTER"],
)
def parse_args(argv=None):
parser = argparse.ArgumentParser(description="Allgemeiner Mutantenlaeufer aus mutanten.toml")
parser.add_argument("--wurzel", required=True)
parser.add_argument("--mutanten", required=True)
parser.add_argument("--nur")
parser.add_argument("--nur-muster", dest="nur_muster", action="store_true")
parser.add_argument("--bericht")
parser.add_argument("--zeitlimit", type=float, default=900.0)
return parser.parse_args(argv)
def ablauf(args, bericht):
befunde, kontext = lade_und_validiere(args)
bericht["wurzel"] = str(kontext["wurzel"])
alle_ids = [m["id"] for m in kontext["mutanten"]]
ausgewaehlte_ids = None
if args.nur:
ausgewaehlte_ids = [x.strip() for x in args.nur.split(",") if x.strip() != ""]
for x in ausgewaehlte_ids:
if x not in alle_ids:
befunde.append("--nur nennt eine unbekannte id: {0}".format(x))
bericht["befunde"] = list(befunde)
if befunde:
return 2
if ausgewaehlte_ids is not None:
ausgewaehlte_mutanten = [m for m in kontext["mutanten"] if m["id"] in ausgewaehlte_ids]
else:
ausgewaehlte_mutanten = list(kontext["mutanten"])
wurzel = kontext["wurzel"]
kopfzeilen_ausgeben(kontext, ausgewaehlte_mutanten)
muster_ergebnisse = {}
for mutant in ausgewaehlte_mutanten:
ergebnis = muster_pruefen(mutant, wurzel)
muster_ergebnisse[mutant["id"]] = ergebnis
if ergebnis.get("kodierungsfehler"):
befunde.append("{0}: Zieldatei ist kein UTF-8: {1}".format(mutant["id"], ergebnis["kodierungsfehler"]))
bericht["befunde"] = list(befunde)
if befunde:
return 2
if args.nur_muster:
mutant_ergebnisliste = []
alle_ok = True
for mutant in ausgewaehlte_mutanten:
ergebnis = muster_ergebnisse[mutant["id"]]
if ergebnis["treffer"] == 1:
status = "OK"
else:
status = "MUSTER"
alle_ok = False
eintrag = {
"id": mutant["id"],
"datei": mutant["datei"],
"status": status,
"hinweis": mutant.get("hinweis"),
"treffer": ergebnis["treffer"],
"tests": [],
}
mutant_ergebnisliste.append(eintrag)
bericht["mutanten"] = mutant_ergebnisliste
print(format_mutant_zeile(mutant, status, [], ergebnis))
if mutant.get("hinweis"):
print(" " + mutant["hinweis"])
zusammenfassung = zusammenfassung_rechnen(mutant_ergebnisliste)
bericht["zusammenfassung"] = zusammenfassung
print(zusammenfassung_zeile(zusammenfassung))
return 0 if alle_ok else 1
plugin_dir = Path(tempfile.mkdtemp(prefix="mutanten_lauf_plugin_"))
home_dir = Path(tempfile.mkdtemp(prefix="mutanten_lauf_home_"))
try:
(plugin_dir / (PLUGIN_NAME + ".py")).write_text(PLUGIN_QUELLTEXT, encoding="utf-8")
baseline_befunde, baseline_ergebnisse = baseline_pruefen(
kontext, ausgewaehlte_mutanten, home_dir, plugin_dir, args.zeitlimit,
)
bericht["grundlinie"] = baseline_ergebnisse
if baseline_befunde:
bericht["befunde"] = list(befunde) + baseline_befunde
return 2
bericht["mutanten"] = []
for mutant in ausgewaehlte_mutanten:
muster = muster_ergebnisse[mutant["id"]]
if muster["treffer"] != 1:
eintrag = {
"id": mutant["id"],
"datei": mutant["datei"],
"status": "MUSTER",
"hinweis": mutant.get("hinweis"),
"treffer": muster["treffer"],
"tests": [],
}
bericht["mutanten"].append(eintrag)
print(format_mutant_zeile(mutant, "MUSTER", [], muster))
if mutant.get("hinweis"):
print(" " + mutant["hinweis"])
continue
status, test_ergebnisse = mutant_anwenden_und_pruefen(
mutant, muster["start"], muster["aus_zeilen"], muster["wird_zeilen"],
wurzel, home_dir, plugin_dir,
kontext["python_je_datei"], kontext["python"], args.zeitlimit,
)
eintrag = {
"id": mutant["id"],
"datei": mutant["datei"],
"status": status,
"hinweis": mutant.get("hinweis"),
"treffer": muster["treffer"],
"tests": test_ergebnisse,
}
bericht["mutanten"].append(eintrag)
print(format_mutant_zeile(mutant, status, test_ergebnisse, muster))
if mutant.get("hinweis"):
print(" " + mutant["hinweis"])
zusammenfassung = zusammenfassung_rechnen(bericht["mutanten"])
bericht["zusammenfassung"] = zusammenfassung
print(zusammenfassung_zeile(zusammenfassung))
alle_ok = all(m["status"] == "OK" for m in bericht["mutanten"])
return 0 if alle_ok else 1
except RuecksetzenGescheitert as exc:
bericht["befunde"] = list(befunde) + [str(exc)]
return 2
finally:
shutil.rmtree(plugin_dir, ignore_errors=True)
shutil.rmtree(home_dir, ignore_errors=True)
def ausgabe_schreiben(bericht, args):
if args.bericht:
try:
with open(args.bericht, "w", encoding="utf-8") as datei:
json.dump(bericht, datei, ensure_ascii=True, indent=2)
except OSError as exc:
print("Bericht konnte nicht geschrieben werden: {0}".format(exc), file=sys.stderr)
if bericht.get("befunde"):
print("befunde:")
for b in bericht["befunde"]:
print(" - {0}".format(b))
def main(argv=None):
args = parse_args(argv)
bericht = {
"wurzel": args.wurzel,
"mutanten_datei": args.mutanten,
"befunde": [],
"grundlinie": {},
"mutanten": [],
"zusammenfassung": {},
}
try:
exit_code = ablauf(args, bericht)
except Exception:
bericht["befunde"] = bericht.get("befunde", []) + ["unerwarteter Abbruch:\n" + traceback.format_exc()]
exit_code = 3
ausgabe_schreiben(bericht, args)
return exit_code
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,198 @@
---
type: resource
status: active
title: "Entwurf: mutanten_lauf.py — allgemeiner Mutantenläufer aus der Plantabelle"
summary: "Entscheidungsliste, Dateiformat mutanten.toml, Ablauf, Status und Exit-Codes, Abdichtung und Angriffsliste für den allgemeinen Mutantenläufer. Punkt 3 der Arbeitsweise vom 2026-09-26."
tags: [vault-tools, codeworkflow, pruefwerkzeuge]
created: 2026-09-27
updated: 2026-09-27
---
# Entwurf: `mutanten_lauf.py`
Bisher hatte jeder Plan seinen eigenen Mutantenläufer. `mutlauf.py` für L1a las die Mutanten per Regex aus dem Plantext, `mutanten_py.py` hielt sie als Python-Liste. Die Mutanten von B1 stehen in einer dritten Form im Plan, die keiner der beiden liest. Der allgemeine Läufer liest stattdessen **eine Datei in einem festen Format**, die im Planordner neben der Übersicht liegt: `mutanten.toml`. Prosa im Plan wird nicht geparst.
Das Werkzeug ist ein Fangnetz, kein Nachweis. Es sagt, ob die genannten Tests unter einer bestimmten Änderung fallen. Ob sie **an der gemeinten Stelle** fallen, zeigt es nur über die Fehlermeldung, die es mit ausgibt. Das Urteil darüber bleibt beim Menschen.
## 1 Entscheidungsliste
| Frage | Herkunft |
|---|---|
| Steht das Muster eines Mutanten genau einmal als ganze Zeilenfolge in seiner Datei? | Projektvorgabe (Pläne L1a und B1: „jedes Muster steht als ganze Zeile genau einmal in seiner Datei“) |
| Sind alle genannten Tests ohne Mutant grün (Grundlinie)? | Projektvorgabe ([[AI/Regelwerk/pruefwerkzeuge\|pruefwerkzeuge.md]], Abschnitt 3: zuerst die Grundlinie) |
| Fällt unter dem Mutanten **jeder** genannte Test? | Projektvorgabe (Pläne: „‚Rot bei‘ nennt Tests, die fallen müssen“) |
| Was heißt „fällt“? | Projektvorgabe: Der Test hat in einer Phase (setup, call, teardown) das Ergebnis `failed`, oder er fehlt, weil das Einsammeln seiner Datei scheitert. `skipped` und `xfail` sind nicht rot. |
| Ist die Datei nach jedem Mutanten wieder byte-gleich? | Projektvorgabe: SHA-256 vorher und nachher |
| Läuft das auf einer Wegwerfkopie? | Projektvorgabe: Die Wurzel darf kein `.git` haben (Ordner oder Datei), und keiner der Pfade aus `ohne` darf existieren |
| Welche Tests und Ergebnisse meldet pytest? | Quelle: pytest-Hooks `pytest_runtest_logreport`, `pytest_collectreport`, `pytest_deselected` (pytest 9.1.1 im Venv `trading`, nachgesehen 2026-09-27). Die Knotennamen kommen aus `report.nodeid`, nicht aus einer JUnit-Klassenbezeichnung. |
| Hält die Abdichtung Netz und echte Zugänge fern? | Annahme, aufgelöst als Projektvorgabe: Sie ist ein Fangnetz mit benannten Lücken (Abschnitt 5), kein Nachweis. |
## 2 Dateiformat `mutanten.toml`, Format 1
TOML, gelesen mit `tomllib`. Literale Zeichenketten in `'''…'''` nehmen Backslashes wörtlich; deshalb dieses Format und nicht JSON.
```toml
format = 1
python = "/home/claude/venvs/trading/bin/python"
ohne = ["config/secrets.toml", "config/zugang.toml"]
[kuerzel]
DA = "tests/test_bewertung_daten.py"
U = "tests/test_bewertung_urteil.py"
[python_je_datei]
"tests/test_broker_ibkr.py" = "/home/claude/venvs/trading-ibkr/bin/python"
[[mutant]]
id = "D1"
datei = "trading/bewertung/daten.py"
aus = '''
return ort.weekday() < 5 and SPERRE_VON <= ort.time() < SPERRE_BIS
'''
wird = '''
return False
'''
rot_bei = ["DA::test_montag_bis_freitag_von_neun_bis_halb_fuenf_new_york_wird_nicht_abgerufen"]
hinweis = "frei, wird nur ausgegeben"
```
**Oberste Ebene**, erlaubt sind genau diese Schlüssel:
| Schlüssel | Pflicht | Typ | Bedeutung |
|---|---|---|---|
| `format` | ja | Ganzzahl, genau `1` | Formatfassung |
| `python` | ja | absoluter Pfad | Interpreter für pytest; `~` ist ein Befund |
| `ohne` | nein | Liste von relativen Pfaden | darf es in der Wurzel nicht geben; `.git` gilt immer zusätzlich |
| `kuerzel` | nein | Tabelle Name → relativer Pfad einer Testdatei | Abkürzung in `rot_bei` |
| `python_je_datei` | nein | Tabelle relativer Pfad → absoluter Pfad | anderer Interpreter für diese Testdatei |
| `mutant` | ja | Liste von Tabellen, nicht leer | die Mutanten |
**Je Mutant**, erlaubt sind genau diese Schlüssel:
| Schlüssel | Pflicht | Typ | Bedeutung |
|---|---|---|---|
| `id` | ja | nicht leere Zeichenkette, eindeutig | Name in der Ausgabe und in `--nur` |
| `datei` | ja | relativer Pfad | Datei in der Wurzel; kein `..`, nicht absolut, keine Verknüpfung, muss existieren |
| `aus` | ja | nicht leere Zeichenkette | ganze Zeilen, die ersetzt werden |
| `wird` | ja | Zeichenkette | ganze Zeilen, die dafür eingesetzt werden; leer heißt: Zeilen entfernen |
| `rot_bei` | ja | nicht leere Liste von Zeichenketten | Tests, die fallen müssen |
| `hinweis` | nein | Zeichenkette | wird in Ausgabe und Bericht mitgeführt |
Jeder unbekannte Schlüssel ist ein Befund (Positivliste). So fällt `rotbei` statt `rot_bei` auf.
**Zeilen aus `aus` und `wird`:** Ein einzelnes `\n` am Ende wird entfernt (die schließenden `'''` auf eigener Zeile). Eine leere Zeichenkette ergibt null Zeilen. Sonst wird an `\n` getrennt. Ein `\r` bleibt Teil der Zeile.
**Einträge in `rot_bei`:** `K::name` mit `K` aus `kuerzel` wird zu `<pfad>::name`. Sonst muss der Eintrag mit einem relativen Pfad auf `.py` beginnen, gefolgt von `::`. Alles andere ist ein Befund (unbekanntes Kürzel). Parametrisierte Tests stehen mit ihrer vollen Kennung, etwa `U::test_knapp_hinter_jeder_grenze_faellt_genau_dieses_kriterium[max_drawdown-0.1501-max_drawdown]`.
## 3 Ablauf
Aufruf in WSL, mit einem Python ab 3.11 (nur Standardbibliothek):
```
python mutanten_lauf.py --wurzel <kopie> --mutanten <mutanten.toml> [--nur ID,ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <sekunden>]
```
`--zeitlimit` gilt je pytest-Aufruf, Vorgabe 900. `--nur-muster` fährt nur die Schritte 1 und 2.
1. **Vorbedingungen und Format.** Alle Befunde sammeln, nicht beim ersten abbrechen: Wurzel fehlt oder ist kein Ordner; `.git` in der Wurzel; ein Pfad aus `ohne` existiert; TOML nicht lesbar; jede Formatregel aus Abschnitt 2; ein Interpreter fehlt oder ist nicht ausführbar; `--nur` nennt eine unbekannte ID. Gibt es einen Befund, endet der Lauf hier mit Exit 2.
2. **Muster.** Für jeden ausgewählten Mutanten: Datei als Bytes lesen, streng als UTF-8 dekodieren (sonst Befund), an `\n` trennen, die Zeilenfolge aus `aus` an jeder Startposition suchen (überlappende Treffer zählen mit). Genau ein Treffer ist gut. Sonst Status `MUSTER` mit der Trefferzahl; enthält die Datei ein `\r`, steht der Hinweis „Datei hat CR“ dabei. `aus` gleich `wird` ist ein Formatbefund aus Schritt 1 („wirkungslos“).
3. **Grundlinie.** Alle genannten Tests der ausgewählten Mutanten ohne Mutant fahren, gruppiert nach Interpreter. Jeder muss `GRUEN` sein. Fehlt einer, ist er abgewählt (etwa durch den Marker `integration`), übersprungen oder rot, ist das ein Befund mit der Meldung, und der Lauf endet mit Exit 2, bevor ein Mutant läuft.
4. **Mutanten** in der Reihenfolge der Datei, `MUSTER`-Fälle ausgelassen. Je Mutant:
- SHA-256 der Originalbytes merken, die Zeilen ersetzen, mit `\n` zusammenfügen, als UTF-8-Bytes schreiben.
- Die genannten Tests fahren, gruppiert nach Interpreter.
- Im `finally` die Originalbytes zurückschreiben und den SHA-256 neu lesen. Weicht er ab, endet der ganze Lauf sofort mit dem Befund „Rücksetzen gescheitert“ und Exit 2.
5. **Zusammenfassung** und Exit-Code.
**pytest-Aufruf:** `<python> -m pytest -q -p no:cacheprovider -p _mutanten_lauf_plugin <knoten…>` mit `cwd` = Wurzel. Das Plugin ist eine kleine Datei, die das Skript in einen eigenen Temp-Ordner schreibt; der Ordner kommt vorn in `PYTHONPATH`. Das Plugin schreibt je Bericht eine JSON-Zeile in die Datei aus der Umgebungsvariable `MUTANTEN_LAUF_ERGEBNIS`:
- `pytest_runtest_logreport`: `{"art": "test", "knoten": nodeid, "phase": when, "ergebnis": outcome, "xfail": hat wasxfail, "meldung": erste Zeile}`
- `pytest_collectreport` nur bei `failed`: `{"art": "sammeln", "knoten": nodeid, "meldung": erste Zeile}`
- `pytest_deselected`: je Element `{"art": "abgewaehlt", "knoten": nodeid}`
Die Meldung ist `longrepr.reprcrash.message`, wenn es das gibt, sonst die erste nicht leere Zeile von `str(longrepr)`, gekürzt auf 300 Zeichen. Das Plugin schreibt jede Zeile sofort und mit `flush`.
**Auswertung je Test:** eine Phase `failed` → `ROT` (bei `xfail` nicht); sonst `skipped` in einer Phase oder `xfail` → `UEBERSPRUNGEN`; sonst `call` mit `passed` → `GRUEN`; abgewählt → `ABGEWAEHLT`; gar kein Bericht → `FEHLT`. Unter einem Mutanten zählt `FEHLT` als `ROT` mit der Meldung „Sammelfehler: …“, wenn es für seine Testdatei (oder einen übergeordneten Knoten) einen `sammeln`-Bericht gibt. In der Grundlinie ist jedes `FEHLT` ein Befund.
**Zeitlimit:** Prozess mit `start_new_session=True` starten; bei Überschreitung die ganze Prozessgruppe mit `SIGKILL` beenden. Der Mutant bekommt `ZEIT`, das Rücksetzen läuft trotzdem.
## 4 Status, Ausgabe, Exit-Codes
| Status je Mutant | Bedeutung |
|---|---|
| `OK` | jeder genannte Test `ROT` |
| `LUECKE` | mindestens ein genannter Test `GRUEN` oder `UEBERSPRUNGEN` |
| `UNKLAR` | ein Test `FEHLT` ohne Sammelfehler, oder pytest endete ohne auswertbare Ergebnisdatei |
| `ZEIT` | Zeitlimit überschritten |
| `MUSTER` | Muster nicht genau einmal gefunden, nichts gefahren |
Ausgabe auf stdout, UTF-8, je Mutant eine Zeile: `D1 OK DA::test_x=ROT(<meldung, 120 Zeichen>) …`. Tests erscheinen in der Kurzform aus `rot_bei`. Ein vorhandener `hinweis` folgt in der nächsten Zeile, eingerückt. Oben stehen Wurzel, Mutantendatei, Anzahl gesamt und ausgewählt und je Interpreter die Python-Version. Unten eine gerechnete Zusammenfassung: `ergebnis: <n> Mutanten, <a> OK, <b> LUECKE, <c> UNKLAR, <d> ZEIT, <e> MUSTER`.
`--bericht` schreibt dasselbe als JSON: `{"wurzel", "mutanten_datei", "befunde": [...], "grundlinie": {knoten: {ergebnis, meldung}}, "mutanten": [{"id", "datei", "status", "hinweis", "treffer", "tests": [{"eintrag", "knoten", "ergebnis", "meldung"}]}], "zusammenfassung": {...}}`. Der Bericht wird auch bei einem Abbruch geschrieben, mit allem, was bis dahin feststand.
| Exit | Bedeutung |
|---|---|
| 0 | Grundlinie grün, jeder ausgewählte Mutant `OK` (mit `--nur-muster`: jedes Muster genau einmal) |
| 1 | mindestens ein Mutant nicht `OK` |
| 2 | Vorbedingung, Format, Grundlinie oder Rücksetzen gescheitert |
| 3 | unerwarteter Abbruch; die bis dahin gesammelten Befunde und Ergebnisse werden trotzdem ausgegeben |
Der Hauptablauf steht in `try`. Ein Traceback ist ein Befund und wird als solcher gemeldet ([[AI/Regelwerk/pruefwerkzeuge|pruefwerkzeuge.md]], Abschnitt 4).
## 5 Abdichtung
Jeder pytest-Aufruf bekommt eine Kopie der Umgebung mit diesen Änderungen:
- `HOME` zeigt auf einen frischen, leeren Temp-Ordner. Zugänge, die unter `~/.config` liegen (Scharfschalter, IBKR-Live-Zugang, Scalable), sind damit nicht erreichbar.
- `http_proxy`, `https_proxy`, `all_proxy` und dieselben in Großschrift zeigen auf `http://127.0.0.1:9`; `no_proxy` und `NO_PROXY` werden entfernt. Bibliotheken, die Proxy-Variablen lesen, laufen damit gegen einen geschlossenen Port.
- `PYTHONDONTWRITEBYTECODE=1`, `COLUMNS=300`, `MUTANTEN_LAUF_ERGEBNIS=<temp-datei>`, `PYTHONPATH` mit dem Plugin-Ordner vorn.
**Was das nicht abdichtet:** einen Unterprozess mit absolutem Pfad (so kam der Vorfall vom 2026-09-26 zustande), einen Socket ohne Proxy, einen Client mit `trust_env=False` oder mit eigenem Transport, Dateien mit absolutem Pfad außerhalb von `HOME`. Die eigentliche Sperre bleibt die Netzsperre der Testdateien selbst und eine Kopie ohne Geheimnisdateien (`ohne`).
Alle Temp-Ordner liegen außerhalb der Wurzel und werden am Ende entfernt, auch nach einem Abbruch.
## 6 Angriffsliste für den Treiber `mutanten_lauf_treiber.py`
Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testdatei, `pyproject.toml` mit `[tool.pytest.ini_options]`), schreibt je Fall eine `mutanten.toml`, startet den Läufer als Unterprozess und prüft Exit-Code und ein Meldungsstück. **Zuerst die Grundlinie:** Der saubere Fall muss Exit 0 und nur `OK` liefern, sonst misst der Rest nichts. Nach **jedem** Fall prüft der Treiber, dass jede Datei des kleinen Projekts byte-gleich mit dem Ausgangsstand ist.
| Nr | Fall | erwartet |
|---|---|---|
| 1 | sauber, zwei Mutanten, beide getötet | Exit 0, zweimal `OK` |
| 2 | ein Test bleibt grün, als erster Eintrag in `rot_bei` | Exit 1, `LUECKE` |
| 3 | dasselbe als letzter Eintrag | Exit 1, `LUECKE` |
| 4 | der lückenhafte Mutant an erster und an letzter Position der Datei | je Exit 1, `LUECKE` beim richtigen Mutanten |
| 5 | `aus` kommt nicht vor | Exit 1, `MUSTER` mit Zahl 0 |
| 6 | `aus` kommt zweimal vor | Exit 1, `MUSTER` mit Zahl 2 |
| 7 | `aus` ist nur ein Teil einer Zeile (andere Einrückung) | Exit 1, `MUSTER` mit Zahl 0 |
| 8 | Zieldatei mit CRLF | Exit 1, `MUSTER` mit Hinweis auf CR |
| 9 | `aus` über zwei Zeilen, getötet | Exit 0, `OK` |
| 10 | `wird = ""` entfernt eine Zeile, getötet | Exit 0, `OK` |
| 11 | Mutant erzeugt einen Syntaxfehler, das Einsammeln scheitert | Exit 0, `OK` mit „Sammelfehler“ |
| 12 | Mutant erzeugt eine Endlosschleife, `--zeitlimit 5` | Exit 1, `ZEIT`, Datei danach byte-gleich |
| 13 | ein Test ist in der Grundlinie rot | Exit 2 |
| 14 | ein Test ist in der Grundlinie übersprungen | Exit 2 |
| 15 | ein Test ist per Marker abgewählt | Exit 2, Meldung nennt „abgewählt“ oder `ABGEWAEHLT` |
| 16 | ein Testname existiert nicht | Exit 2 |
| 17 | `.git` als Ordner in der Wurzel; dasselbe als Datei | je Exit 2 |
| 18 | ein Pfad aus `ohne` existiert | Exit 2 |
| 19 | unbekannter Schlüssel im Mutanten (`rotbei`) und auf oberster Ebene | je Exit 2 |
| 20 | doppelte `id` | Exit 2 |
| 21 | `rot_bei` leer; `rot_bei` als Zeichenkette statt Liste | je Exit 2 |
| 22 | `datei` mit `..`; absolut; fehlt | je Exit 2 |
| 23 | unbekanntes Kürzel in `rot_bei` | Exit 2 |
| 24 | `aus` gleich `wird` | Exit 2 |
| 25 | `aus` leer; `format = 2`; `format` fehlt; `python` relativ; `python` existiert nicht | je Exit 2 |
| 26 | TOML-Syntaxfehler | Exit 2 |
| 27 | keine Mutanten | Exit 2 |
| 28 | `--nur` mit unbekannter ID | Exit 2 |
| 29 | `--nur` wählt einen von zwei Mutanten | Exit 0, nur dieser läuft |
| 30 | `--nur-muster` bei sauberem Fall; bei Fall 6 | Exit 0 ohne pytest-Lauf; Exit 1 |
| 31 | Zieldatei ist kein UTF-8 | Exit 1 oder 2, Meldung nennt die Kodierung |
| 32 | Abdichtung: ein Test prüft, dass `HOME` nicht das echte ist und `https_proxy` gesetzt ist | Grundlinie grün, Exit 0 |
| 33 | `--bericht` schreibt gültiges JSON mit den Status aus Fall 2 | Datei lesbar, Status stimmt |
| 34 | `python_je_datei` setzt für eine Testdatei einen zweiten Interpreter (derselbe Pfad über einen anderen Namen, etwa eine Verknüpfung im Temp) | Exit 0 |
Der Treiber gibt je Fall eine Zeile aus (`Fall 5 ok` oder `Fall 5 FEHLER: erwartet …, bekommen …`) und am Ende die gerechnete Zahl der Fehler. Exit 0 nur, wenn alle Fälle wie erwartet ausgehen und jedes Mal byte-gleich zurückgesetzt wurde.
## 7 Abbruchbedingung
Eine Runde mit dem Prüfer endet, wenn die Befunde nur noch Formulierungen betreffen. Der erste echte Einsatz ist B1 (74 Mutanten auf einer Kopie von `b1-mat-kopie`).
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Mutantenlauf auf einer Kopie der Kopie, gedacht fuer den Hilfsagenten der Plankritik
# (.claude/workflows/plankritik.js). Nie in der uebergebenen Kopie selbst arbeiten: Die Linsen
# fahren dort gleichzeitig Tests, und mutanten_lauf.py setzt Dateien zwischenzeitlich zurueck.
#
# Aufruf: bash mutanten_lauf_kopie.sh <kopie als Windows-Pfad> <planordner als Windows-Pfad> <modus>
# modus ist "voll" oder "muster". Der ganze Lauf ist auf BUDGET_S Sekunden begrenzt (Vorgabe 540),
# weil das Bash-Werkzeug eines Hilfsagenten nach hoechstens zehn Minuten endet; eine ueberschrittene
# Grenze meldet die Zeile ZEITGRENZE, die bis dahin gefahrenen Mutanten stehen in der Ausgabe.
# Beide Pfade sind Windows-Pfade (auch Kurznamen wie PASCAL~1),
# die dieses Skript selbst mit wslpath in WSL-Pfade umwandelt.
set -u
KOPIE_WIN="${1:-}"
PLANORDNER_WIN="${2:-}"
MODUS="${3:-}"
if [ -z "$KOPIE_WIN" ] || [ -z "$PLANORDNER_WIN" ] || [ -z "$MODUS" ]; then
echo "MUTANTEN_LAUF_FEHLER: Aufruf braucht <kopie> <planordner> <modus>"
exit 2
fi
if [ "$MODUS" != "voll" ] && [ "$MODUS" != "muster" ]; then
echo "MUTANTEN_LAUF_FEHLER: modus muss voll oder muster sein, nicht $MODUS"
exit 2
fi
BUDGET_S="${BUDGET_S:-540}"
KOPIE=$(wslpath -u "$KOPIE_WIN")
PLANORDNER=$(wslpath -u "$PLANORDNER_WIN")
if [ ! -d "$KOPIE" ]; then
echo "MUTANTEN_LAUF_FEHLER: Kopie fehlt: $KOPIE"
exit 2
fi
if [ ! -d "$PLANORDNER" ]; then
echo "MUTANTEN_LAUF_FEHLER: Planordner fehlt: $PLANORDNER"
exit 2
fi
MUTANTEN_TOML="$PLANORDNER/mutanten.toml"
if [ ! -f "$MUTANTEN_TOML" ]; then
echo "KEINE_MUTANTEN_TOML"
exit 0
fi
TMPKOPIE=$(mktemp -d /tmp/plankritik-mut-XXXXXX)
trap 'rm -rf "$TMPKOPIE"' EXIT
cp -a "$KOPIE/." "$TMPKOPIE/"
find "$TMPKOPIE" -name __pycache__ -type d -prune -exec rm -rf {} +
MUSTER_SCHALTER=""
if [ "$MODUS" = "muster" ]; then
MUSTER_SCHALTER="--nur-muster"
fi
timeout "$BUDGET_S" python3 /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py --wurzel "$TMPKOPIE" --mutanten "$MUTANTEN_TOML" $MUSTER_SCHALTER
EXITCODE=$?
if [ "$EXITCODE" -eq 124 ]; then
echo "ZEITGRENZE: Mutantenlauf nach $BUDGET_S s abgebrochen; Mutanten ohne Zeile oben sind nicht gefahren"
fi
echo "EXIT mutanten_lauf: $EXITCODE"
exit $EXITCODE
@@ -0,0 +1,964 @@
#!/usr/bin/env python3
"""Treiber fuer mutanten_lauf.py: 34 Angriffsfaelle auf einer Wegwerfkopie.
Zweck: Baut in einem Temp-Ordner unter /tmp ein kleines Testprojekt, fuehrt
mutanten_lauf.py je Fall mit einer eigenen mutanten.toml aus und vergleicht
Exit-Code und ein Meldungsstueck gegen die Erwartung aus dem Entwurf,
Abschnitt 6. Nach jedem Fall wird geprueft, dass jede Datei des kleinen
Projekts wieder byte-gleich mit dem Ausgangsstand ist.
Aufruf: python mutanten_lauf_treiber.py
Baut nichts ausserhalb von /tmp, geht nicht ins Netz, raeumt am Ende auf.
Spezifikation: .vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md, Abschnitt 6.
"""
import hashlib
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
HIER = Path(__file__).resolve().parent
LAEUFER = HIER / "mutanten_lauf.py"
INTERPRETER = "/home/claude/venvs/trading/bin/python"
FEHLERLISTE = []
def melde(fallname, ok, zusatz=""):
if ok:
print("{0} ok".format(fallname))
else:
print("{0} FEHLER: {1}".format(fallname, zusatz))
FEHLERLISTE.append(fallname)
def sha_von(pfad):
return hashlib.sha256(pfad.read_bytes()).hexdigest()
def snapshot(dateien):
return {p: sha_von(p) for p in dateien}
def pruefe_unveraendert(dateien, snap, fallname):
for p in dateien:
if not p.exists():
melde(fallname + " (rueckstellung)", False, "Datei fehlt nach dem Lauf: {0}".format(p))
return
if sha_von(p) != snap[p]:
melde(fallname + " (rueckstellung)", False, "Datei nicht byte-gleich zurueckgesetzt: {0}".format(p))
return
def starte_laeufer(basis, toml_pfad, zusatzargumente=None, bericht_pfad=None, zeitlimit=None):
cmd = [INTERPRETER, str(LAEUFER), "--wurzel", str(basis), "--mutanten", str(toml_pfad)]
if zeitlimit is not None:
cmd += ["--zeitlimit", str(zeitlimit)]
if bericht_pfad is not None:
cmd += ["--bericht", str(bericht_pfad)]
if zusatzargumente:
cmd += zusatzargumente
return subprocess.run(cmd, capture_output=True, text=True, timeout=120)
def schreibe_toml(pfad, text):
pfad.write_text(text, encoding="utf-8")
def enthaelt(text, teilstueck):
return teilstueck in (text or "")
# --- Projektaufbau ----------------------------------------------------
def baue_projekt(basis):
(basis / "paket").mkdir(parents=True)
(basis / "tests").mkdir(parents=True)
(basis / "paket" / "__init__.py").write_text("", encoding="utf-8")
(basis / "paket" / "modul.py").write_text(
"def f1(x):\n"
" if x > 0:\n"
" return True\n"
" return False\n"
"\n"
"\n"
"def f2(y):\n"
" if y >= 10:\n"
" return \"gross\"\n"
" return \"klein\"\n"
"\n"
"\n"
"def f3(z):\n"
" if z == 0:\n"
" pass # platzhalter\n"
" return z\n"
"\n"
"\n"
"def f4(w):\n"
" if w == 1:\n"
" pass # platzhalter\n"
" return w\n"
"\n"
"\n"
"def f5(a, b):\n"
" resultat = a + b\n"
" return resultat\n",
encoding="utf-8",
)
(basis / "paket" / "modul_syntax.py").write_text(
"def f6(n):\n"
" return n * 2\n",
encoding="utf-8",
)
(basis / "paket" / "modul_zeit.py").write_text(
"def f7(n):\n"
" return n + 1\n",
encoding="utf-8",
)
(basis / "paket" / "modul_crlf.py").write_bytes(
b"def f8(n):\r\n return n\r\n",
)
(basis / "paket" / "modul_binaer.py").write_bytes(
b"\xff\xfe nicht utf-8 \x80\x81\n",
)
(basis / "tests" / "__init__.py").write_text("", encoding="utf-8")
(basis / "tests" / "test_modul.py").write_text(
"from paket.modul import f1, f2, f5\n"
"\n"
"\n"
"def test_f1_positiv():\n"
" assert f1(5) is True\n"
"\n"
"\n"
"def test_f1_negativ():\n"
" assert f1(-3) is False\n"
"\n"
"\n"
"def test_f2_gross():\n"
" assert f2(15) == \"gross\"\n"
"\n"
"\n"
"def test_f2_klein():\n"
" assert f2(1) == \"klein\"\n"
"\n"
"\n"
"def test_f5_summe():\n"
" assert f5(2, 3) == 5\n",
encoding="utf-8",
)
(basis / "tests" / "test_modul_syntax.py").write_text(
"from paket.modul_syntax import f6\n"
"\n"
"\n"
"def test_f6():\n"
" assert f6(3) == 6\n",
encoding="utf-8",
)
(basis / "tests" / "test_zeit.py").write_text(
"from paket.modul_zeit import f7\n"
"\n"
"\n"
"def test_f7():\n"
" assert f7(1) == 2\n",
encoding="utf-8",
)
(basis / "tests" / "test_faelle.py").write_text(
"import pytest\n"
"\n"
"\n"
"def test_immer_rot():\n"
" assert False\n"
"\n"
"\n"
"def test_immer_uebersprungen():\n"
" pytest.skip(\"fuer den Treiber-Fall 14\")\n"
"\n"
"\n"
"@pytest.mark.integration\n"
"def test_wird_abgewaehlt():\n"
" assert True\n",
encoding="utf-8",
)
(basis / "tests" / "test_abdichtung.py").write_text(
"import os\n"
"\n"
"\n"
"def test_abdichtung_home_und_proxy():\n"
" home = os.environ.get(\"HOME\", \"\")\n"
" proxy = os.environ.get(\"https_proxy\", \"\")\n"
" assert \"mutanten_lauf_home_\" in home and proxy == \"http://127.0.0.1:9\"\n",
encoding="utf-8",
)
(basis / "pyproject.toml").write_text(
"[tool.pytest.ini_options]\n"
"addopts = '-m \"not integration\"'\n"
"markers = [\n"
" \"integration: wird vom Treiber absichtlich abgewaehlt\",\n"
"]\n",
encoding="utf-8",
)
getrackte_dateien = [
basis / "pyproject.toml",
basis / "paket" / "__init__.py",
basis / "paket" / "modul.py",
basis / "paket" / "modul_syntax.py",
basis / "paket" / "modul_zeit.py",
basis / "paket" / "modul_crlf.py",
basis / "paket" / "modul_binaer.py",
basis / "tests" / "__init__.py",
basis / "tests" / "test_modul.py",
basis / "tests" / "test_modul_syntax.py",
basis / "tests" / "test_zeit.py",
basis / "tests" / "test_faelle.py",
basis / "tests" / "test_abdichtung.py",
]
return getrackte_dateien
# --- TOML-Bausteine -----------------------------------------------------
def kopf(python=INTERPRETER, zusatz=""):
return "format = 1\npython = \"{0}\"\n{1}\n".format(python, zusatz)
def mutant_d1_d2():
return (
kopf() +
"[[mutant]]\n"
"id = \"D1\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return True\n"
"'''\n"
"wird = '''\n"
" return False\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
"\n"
"[[mutant]]\n"
"id = \"D2\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return \"gross\"\n"
"'''\n"
"wird = '''\n"
" return \"klein\"\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f2_gross\"]\n"
)
def mutant_luecke(id_, rot_bei_liste):
rot_bei_text = ", ".join('"{0}"'.format(x) for x in rot_bei_liste)
return (
"[[mutant]]\n"
"id = \"{0}\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return True\n"
"'''\n"
"wird = '''\n"
" return False\n"
"'''\n"
"rot_bei = [{1}]\n"
).format(id_, rot_bei_text)
def mutant_korrekt(id_):
return (
"[[mutant]]\n"
"id = \"{0}\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return \"gross\"\n"
"'''\n"
"wird = '''\n"
" return \"klein\"\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f2_gross\"]\n"
).format(id_)
# --- Faelle ---------------------------------------------------------------
def fall_01(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, mutant_d1_d2())
ergebnis = starte_laeufer(basis, toml)
ok = (
ergebnis.returncode == 0
and ergebnis.stdout.count(" OK ") >= 2
and "D1 OK" in ergebnis.stdout
and "D2 OK" in ergebnis.stdout
)
melde("Fall 1", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_02(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, kopf() + mutant_luecke("L1", ["tests/test_modul.py::test_f2_gross", "tests/test_modul.py::test_f1_positiv"]))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 1 and "L1 LUECKE" in ergebnis.stdout
melde("Fall 2", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_03(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, kopf() + mutant_luecke("L2", ["tests/test_modul.py::test_f1_positiv", "tests/test_modul.py::test_f2_gross"]))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 1 and "L2 LUECKE" in ergebnis.stdout
melde("Fall 3", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_04(basis, projektpfad):
toml_a = projektpfad / "mutanten_4a.toml"
schreibe_toml(toml_a, kopf() + mutant_luecke("L3", ["tests/test_modul.py::test_f2_gross", "tests/test_modul.py::test_f1_positiv"]) + "\n" + mutant_korrekt("K3"))
ergebnis_a = starte_laeufer(basis, toml_a)
ok_a = (
ergebnis_a.returncode == 1
and "L3 LUECKE" in ergebnis_a.stdout
and "K3 OK" in ergebnis_a.stdout
)
toml_b = projektpfad / "mutanten_4b.toml"
schreibe_toml(toml_b, kopf() + mutant_korrekt("K4") + "\n" + mutant_luecke("L4", ["tests/test_modul.py::test_f2_gross", "tests/test_modul.py::test_f1_positiv"]))
ergebnis_b = starte_laeufer(basis, toml_b)
ok_b = (
ergebnis_b.returncode == 1
and "L4 LUECKE" in ergebnis_b.stdout
and "K4 OK" in ergebnis_b.stdout
)
melde("Fall 4", ok_a and ok_b, "a: exit={0}\n{1}\nb: exit={2}\n{3}".format(
ergebnis_a.returncode, ergebnis_a.stdout, ergebnis_b.returncode, ergebnis_b.stdout))
def fall_05(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M5\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return None\n"
"'''\n"
"wird = '''\n"
" return False\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 1 and "M5 MUSTER" in ergebnis.stdout and "Trefferzahl=0" in ergebnis.stdout
melde("Fall 5", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_06(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M6\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" pass # platzhalter\n"
"'''\n"
"wird = '''\n"
" pass\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 1 and "M6 MUSTER" in ergebnis.stdout and "Trefferzahl=2" in ergebnis.stdout
melde("Fall 6", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_07(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M7\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return True\n"
"'''\n"
"wird = '''\n"
" return False\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 1 and "M7 MUSTER" in ergebnis.stdout and "Trefferzahl=0" in ergebnis.stdout
melde("Fall 7", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_08(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M8\"\n"
"datei = \"paket/modul_crlf.py\"\n"
"aus = '''\n"
" return n\n"
"'''\n"
"wird = '''\n"
" return 0\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 1 and "M8 MUSTER" in ergebnis.stdout and "Datei hat CR" in ergebnis.stdout
melde("Fall 8", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_09(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M9\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" if y >= 10:\n"
" return \"gross\"\n"
"'''\n"
"wird = '''\n"
" if y >= 10:\n"
" return \"mittel\"\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f2_gross\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 0 and "M9 OK" in ergebnis.stdout
melde("Fall 9", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_10(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M10\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" resultat = a + b\n"
"'''\n"
"wird = \"\"\n"
"rot_bei = [\"tests/test_modul.py::test_f5_summe\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 0 and "M10 OK" in ergebnis.stdout
melde("Fall 10", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_11(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M11\"\n"
"datei = \"paket/modul_syntax.py\"\n"
"aus = '''\n"
" return n * 2\n"
"'''\n"
"wird = '''\n"
" return n *\n"
"'''\n"
"rot_bei = [\"tests/test_modul_syntax.py::test_f6\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ok = (
ergebnis.returncode == 0
and "M11 OK" in ergebnis.stdout
and "Sammelfehler" in ergebnis.stdout
)
melde("Fall 11", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_12(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M12\"\n"
"datei = \"paket/modul_zeit.py\"\n"
"aus = '''\n"
" return n + 1\n"
"'''\n"
"wird = '''\n"
" while True:\n"
" pass\n"
"'''\n"
"rot_bei = [\"tests/test_zeit.py::test_f7\"]\n"
))
ergebnis = starte_laeufer(basis, toml, zeitlimit=5)
ok = ergebnis.returncode == 1 and "M12 ZEIT" in ergebnis.stdout
melde("Fall 12", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_13(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, mutant_luecke("M13", ["tests/test_faelle.py::test_immer_rot"]))
schreibe_toml(toml, kopf() + mutant_luecke("M13", ["tests/test_faelle.py::test_immer_rot"]))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 2
melde("Fall 13", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_14(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, kopf() + mutant_luecke("M14", ["tests/test_faelle.py::test_immer_uebersprungen"]))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 2
melde("Fall 14", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_15(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, kopf() + mutant_luecke("M15", ["tests/test_faelle.py::test_wird_abgewaehlt"]))
ergebnis = starte_laeufer(basis, toml)
ausgabe = ergebnis.stdout + ergebnis.stderr
ok = ergebnis.returncode == 2 and ("abgew" in ausgabe.lower() or "ABGEWAEHLT" in ausgabe)
melde("Fall 15", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe))
def fall_16(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, kopf() + mutant_luecke("M16", ["tests/test_modul.py::test_gibt_es_nicht"]))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 2
melde("Fall 16", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_17(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, mutant_d1_d2())
git_ordner = basis / ".git"
git_ordner.mkdir()
ergebnis_ordner = starte_laeufer(basis, toml)
git_ordner.rmdir()
ok_ordner = ergebnis_ordner.returncode == 2
git_datei = basis / ".git"
git_datei.write_text("kein echtes repo", encoding="utf-8")
ergebnis_datei = starte_laeufer(basis, toml)
git_datei.unlink()
ok_datei = ergebnis_datei.returncode == 2
melde("Fall 17", ok_ordner and ok_datei, "ordner exit={0}, datei exit={1}".format(
ergebnis_ordner.returncode, ergebnis_datei.returncode))
def fall_18(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, kopf(zusatz="ohne = [\"geheim/geheimnis.toml\"]\n") + mutant_d1_d2().split("\n", 2)[-1])
geheim_ordner = basis / "geheim"
geheim_ordner.mkdir()
(geheim_ordner / "geheimnis.toml").write_text("x = 1\n", encoding="utf-8")
ergebnis = starte_laeufer(basis, toml)
shutil.rmtree(geheim_ordner)
ok = ergebnis.returncode == 2
melde("Fall 18", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_19(basis, projektpfad):
toml_a = projektpfad / "mutanten_19a.toml"
schreibe_toml(toml_a, (
kopf() +
"[[mutant]]\n"
"id = \"M19a\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return True\n"
"'''\n"
"wird = '''\n"
" return False\n"
"'''\n"
"rotbei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
))
ergebnis_a = starte_laeufer(basis, toml_a)
ok_a = ergebnis_a.returncode == 2
toml_b = projektpfad / "mutanten_19b.toml"
schreibe_toml(toml_b, "unbekannt = 1\n" + mutant_d1_d2())
ergebnis_b = starte_laeufer(basis, toml_b)
ok_b = ergebnis_b.returncode == 2
melde("Fall 19", ok_a and ok_b, "a exit={0}, b exit={1}".format(ergebnis_a.returncode, ergebnis_b.returncode))
def fall_20(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, kopf() + mutant_korrekt("GLEICH") + "\n" + mutant_korrekt("GLEICH"))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 2
melde("Fall 20", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_21(basis, projektpfad):
toml_a = projektpfad / "mutanten_21a.toml"
schreibe_toml(toml_a, (
kopf() +
"[[mutant]]\n"
"id = \"M21a\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return True\n"
"'''\n"
"wird = '''\n"
" return False\n"
"'''\n"
"rot_bei = []\n"
))
ergebnis_a = starte_laeufer(basis, toml_a)
ok_a = ergebnis_a.returncode == 2
toml_b = projektpfad / "mutanten_21b.toml"
schreibe_toml(toml_b, (
kopf() +
"[[mutant]]\n"
"id = \"M21b\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return True\n"
"'''\n"
"wird = '''\n"
" return False\n"
"'''\n"
"rot_bei = \"tests/test_modul.py::test_f1_positiv\"\n"
))
ergebnis_b = starte_laeufer(basis, toml_b)
ok_b = ergebnis_b.returncode == 2
melde("Fall 21", ok_a and ok_b, "a exit={0}, b exit={1}".format(ergebnis_a.returncode, ergebnis_b.returncode))
def fall_22(basis, projektpfad):
varianten = {
"22a (..)": "paket/../paket/modul.py",
"22b (absolut)": "/etc/passwd",
"22c (fehlt)": "paket/existiert_nicht.py",
}
alle_ok = True
zusatz = []
for name, datei in varianten.items():
toml = projektpfad / "mutanten_{0}.toml".format(name.split()[0])
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M22\"\n"
"datei = \"{0}\"\n"
"aus = '''\n"
" return True\n"
"'''\n"
"wird = '''\n"
" return False\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
).format(datei))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 2
alle_ok = alle_ok and ok
zusatz.append("{0}: exit={1}".format(name, ergebnis.returncode))
melde("Fall 22", alle_ok, "; ".join(zusatz))
def fall_23(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M23\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return True\n"
"'''\n"
"wird = '''\n"
" return False\n"
"'''\n"
"rot_bei = [\"UNBEKANNT::test_f1_positiv\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 2
melde("Fall 23", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_24(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M24\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" return True\n"
"'''\n"
"wird = '''\n"
" return True\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 2
melde("Fall 24", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_25(basis, projektpfad):
zusatz = []
alle_ok = True
toml = projektpfad / "mutanten_25a.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M25a\"\n"
"datei = \"paket/modul.py\"\n"
"aus = \"\"\n"
"wird = '''\n"
" return False\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
))
e = starte_laeufer(basis, toml)
ok = e.returncode == 2
alle_ok = alle_ok and ok
zusatz.append("aus leer: exit={0}".format(e.returncode))
toml = projektpfad / "mutanten_25b.toml"
schreibe_toml(toml, "format = 2\npython = \"{0}\"\n\n".format(INTERPRETER) + mutant_korrekt("M25b"))
e = starte_laeufer(basis, toml)
ok = e.returncode == 2
alle_ok = alle_ok and ok
zusatz.append("format=2: exit={0}".format(e.returncode))
toml = projektpfad / "mutanten_25c.toml"
schreibe_toml(toml, "python = \"{0}\"\n\n".format(INTERPRETER) + mutant_korrekt("M25c"))
e = starte_laeufer(basis, toml)
ok = e.returncode == 2
alle_ok = alle_ok and ok
zusatz.append("format fehlt: exit={0}".format(e.returncode))
toml = projektpfad / "mutanten_25d.toml"
schreibe_toml(toml, "format = 1\npython = \"bin/python\"\n\n" + mutant_korrekt("M25d"))
e = starte_laeufer(basis, toml)
ok = e.returncode == 2
alle_ok = alle_ok and ok
zusatz.append("python relativ: exit={0}".format(e.returncode))
toml = projektpfad / "mutanten_25e.toml"
schreibe_toml(toml, "format = 1\npython = \"/nicht/vorhanden/python\"\n\n" + mutant_korrekt("M25e"))
e = starte_laeufer(basis, toml)
ok = e.returncode == 2
alle_ok = alle_ok and ok
zusatz.append("python existiert nicht: exit={0}".format(e.returncode))
melde("Fall 25", alle_ok, "; ".join(zusatz))
def fall_26(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, "format = 1\npython = \"{0}\"\n[[mutant\n".format(INTERPRETER))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 2
melde("Fall 26", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_27(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, kopf() + "mutant = []\n")
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 2
melde("Fall 27", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_28(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, mutant_d1_d2())
ergebnis = starte_laeufer(basis, toml, zusatzargumente=["--nur", "UNBEKANNTEID"])
ok = ergebnis.returncode == 2
melde("Fall 28", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_29(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, mutant_d1_d2())
ergebnis = starte_laeufer(basis, toml, zusatzargumente=["--nur", "D1"])
ok = (
ergebnis.returncode == 0
and "D1 OK" in ergebnis.stdout
and "D2" not in ergebnis.stdout.replace("mutanten gesamt: 2, ausgewaehlt: 1", "")
)
melde("Fall 29", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_30(basis, projektpfad):
toml_sauber = projektpfad / "mutanten.toml"
schreibe_toml(toml_sauber, mutant_d1_d2())
e1 = starte_laeufer(basis, toml_sauber, zusatzargumente=["--nur-muster"])
ok1 = e1.returncode == 0
toml_luecke = projektpfad / "mutanten_6.toml"
schreibe_toml(toml_luecke, (
kopf() +
"[[mutant]]\n"
"id = \"M6\"\n"
"datei = \"paket/modul.py\"\n"
"aus = '''\n"
" pass # platzhalter\n"
"'''\n"
"wird = '''\n"
" pass\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
))
e2 = starte_laeufer(basis, toml_luecke, zusatzargumente=["--nur-muster"])
ok2 = e2.returncode == 1
melde("Fall 30", ok1 and ok2, "sauber exit={0}, fall6 exit={1}".format(e1.returncode, e2.returncode))
def fall_31(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M31\"\n"
"datei = \"paket/modul_binaer.py\"\n"
"aus = '''\n"
" irrelevant\n"
"'''\n"
"wird = '''\n"
" irrelevant2\n"
"'''\n"
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ausgabe = ergebnis.stdout + ergebnis.stderr
ok = ergebnis.returncode in (1, 2) and ("UTF-8" in ausgabe or "utf-8" in ausgabe)
melde("Fall 31", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe))
def fall_32(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M32\"\n"
"datei = \"tests/test_abdichtung.py\"\n"
"aus = '''\n"
" assert \"mutanten_lauf_home_\" in home and proxy == \"http://127.0.0.1:9\"\n"
"'''\n"
"wird = '''\n"
" assert False\n"
"'''\n"
"rot_bei = [\"tests/test_abdichtung.py::test_abdichtung_home_und_proxy\"]\n"
))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 0 and "M32 OK" in ergebnis.stdout
melde("Fall 32", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
def fall_33(basis, projektpfad):
import json as jsonmod
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, mutant_luecke("L1", ["tests/test_modul.py::test_f2_gross", "tests/test_modul.py::test_f1_positiv"]))
schreibe_toml(toml, kopf() + mutant_luecke("L1", ["tests/test_modul.py::test_f2_gross", "tests/test_modul.py::test_f1_positiv"]))
bericht_pfad = projektpfad / "bericht_33.json"
ergebnis = starte_laeufer(basis, toml, bericht_pfad=bericht_pfad)
ok = ergebnis.returncode == 1
if ok:
try:
daten = jsonmod.loads(bericht_pfad.read_text(encoding="utf-8"))
ok = (
isinstance(daten, dict)
and any(m.get("id") == "L1" and m.get("status") == "LUECKE" for m in daten.get("mutanten", []))
)
except (ValueError, OSError) as exc:
ok = False
ergebnis = subprocess.CompletedProcess(ergebnis.args, ergebnis.returncode, ergebnis.stdout + "\nJSON-Fehler: " + str(exc), ergebnis.stderr)
melde("Fall 33", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
try:
bericht_pfad.unlink()
except OSError:
pass
def fall_34(basis, projektpfad):
# Ein blosser Symlink auf die Python-Binaerdatei bricht die
# Venv-Erkennung (site.py sucht pyvenv.cfg im Verzeichnis des
# Symlinks, nicht im Zielverzeichnis) - pytest waere dann nicht
# importierbar. Deshalb wird das ganze Venv-Verzeichnis verlinkt.
alt_python_dir = Path(tempfile.mkdtemp(prefix="mutanten_lauf_fall34_"))
venv_verzeichnis = Path(INTERPRETER).parent.parent
venv_link = alt_python_dir / "venv_alt"
alt_python = venv_link / "bin" / "python"
try:
os.symlink(venv_verzeichnis, venv_link)
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, (
kopf(zusatz="\n[python_je_datei]\n\"tests/test_modul.py\" = \"{0}\"\n".format(alt_python)) +
mutant_d1_d2().split("\n", 2)[-1]
))
ergebnis = starte_laeufer(basis, toml)
ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout
melde("Fall 34", ok, "exit={0}\n{1}".format(ergebnis.returncode, ergebnis.stdout))
finally:
shutil.rmtree(alt_python_dir, ignore_errors=True)
FAELLE = [
fall_01, fall_02, fall_03, fall_04, fall_05, fall_06, fall_07, fall_08, fall_09, fall_10,
fall_11, fall_12, fall_13, fall_14, fall_15, fall_16, fall_17, fall_18, fall_19, fall_20,
fall_21, fall_22, fall_23, fall_24, fall_25, fall_26, fall_27, fall_28, fall_29, fall_30,
fall_31, fall_32, fall_33, fall_34,
]
def main():
basis = Path(tempfile.mkdtemp(prefix="mutanten_lauf_treiber_"))
projektpfad = basis
try:
getrackte_dateien = baue_projekt(basis)
snap = snapshot(getrackte_dateien)
for funktion in FAELLE:
vor_lauf = len(FEHLERLISTE)
try:
funktion(basis, projektpfad)
except Exception as exc:
melde(funktion.__name__, False, "Ausnahme: {0}".format(exc))
pruefe_unveraendert(getrackte_dateien, snap, funktion.__name__)
anzahl_fehler = len(FEHLERLISTE)
print("gerechnete Fehler: {0}".format(anzahl_fehler))
return 0 if anzahl_fehler == 0 else 1
finally:
shutil.rmtree(basis, ignore_errors=True)
if __name__ == "__main__":
sys.exit(main())