From ff7854cf39eb76c1c50c692d7a8aa23879467ce5 Mon Sep 17 00:00:00 2001 From: Pascal Michel Date: Tue, 6 Oct 2026 17:41:05 +0200 Subject: [PATCH] Mutantenlaeufer: pytest-Konfiguration wie pytest, nie oberhalb der Wurzel; Abbruch von pytest mit Ursache statt FEHLT; Gleichwertigkeitsprobe, Treiber 46 Faelle; zwei Pruefrunden Anlass: Plankritik 2 von C1 (After Install), fremde pyproject.toml im Windows-Temp. Gegenprobe an sechs mutanten.toml im Vault: gleiche Ergebnisse, C1 unter Windows-Temp jetzt 69 OK statt 101 FEHLT. Co-Authored-By: Claude Opus 5.5 --- .vault-tools/plan-kopie/LIESMICH.md | 10 +- ...06-pytest-konfiguration-nachbesserung-1.md | 1023 +++++++++++++++++ ...06-pytest-konfiguration-nachbesserung-2.md | 634 ++++++++++ .../plans/2026-10-06-pytest-konfiguration.md | 1009 ++++++++++++++++ .vault-tools/plan-kopie/mutanten_lauf.py | 273 ++++- .../plan-kopie/mutanten_lauf_ENTWURF.md | 36 +- .../plan-kopie/mutanten_lauf_probe_konfig.py | 182 +++ .../plan-kopie/mutanten_lauf_treiber.py | 309 ++++- 8 files changed, 3457 insertions(+), 19 deletions(-) create mode 100644 .vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration-nachbesserung-1.md create mode 100644 .vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration-nachbesserung-2.md create mode 100644 .vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration.md create mode 100644 .vault-tools/plan-kopie/mutanten_lauf_probe_konfig.py diff --git a/.vault-tools/plan-kopie/LIESMICH.md b/.vault-tools/plan-kopie/LIESMICH.md index acc33ed..bd3ce48 100644 --- a/.vault-tools/plan-kopie/LIESMICH.md +++ b/.vault-tools/plan-kopie/LIESMICH.md @@ -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-27 +updated: 2026-10-06 --- # Planwerkzeuge für die Kopie mit angewendetem Plan @@ -23,7 +23,7 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko 7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py [--fixturen ] [--node-modules ] [--conftest-als-test] [--venv ] [--frontend ] [--projektname ]`. **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 --mutanten /mutanten.toml [--nur ID,…] [--nur-muster] [--bericht ] [--zeitlimit ]`, 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. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`). +8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel --mutanten /mutanten.toml [--nur ID,…] [--nur-muster] [--bericht ] [--zeitlimit ]`, 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`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Ob die Suche pytest selbst entspricht, prüft `mutanten_lauf_probe_konfig.py ` mit dem Python eines Venvs, in dem pytest liegt (26 Aufbauten, Exit 0 ohne Abweichung). 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. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`). **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** 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 in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes. ## Was belegt ist @@ -43,6 +43,12 @@ Das Skript dazu liegt außerhalb des Vaults und ist nicht Teil der Werkzeuge. Er Nachbesserung vom 2026-09-28 (Prüfbericht desselben Tages): `mutanten_lauf_kopie.sh` läuft jetzt unter `unshare --map-current-user --net`, mit `python3 -u` und einem Zeitlimit je Mutant (`ZEITLIMIT_S`, kleiner als `BUDGET_S`), schreibt vor dem Lauf eine Modus-Zeile und räumt bei SIGTERM auf (`timeout -k 15`, eigener Handler in `mutanten_lauf.py`). `plankritik.js` gibt zusätzlich `mutanten_status` und `mutanten_modus` zurück und erklärt der Linse `arithmetik` Modus `muster` und Exit 2/3. Treiber-Fall 35 (SIGTERM mitten in einem hängenden Mutanten) und die zusätzlichen Fälle in `faelle_workflows.mjs`/`mutanten_workflows.py` sind gefahren und grün; die Rot-Ausgaben davor stehen im Bericht des Executers vom selben Tag. +Nachbesserung vom 2026-10-06 (Plankritik 2 von C1 im Projekt After Install, Plan `docs/plans/2026-10-06-pytest-konfiguration.md` und seine Nachbesserung): pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel; Abbruch von pytest mit Ursache statt `FEHLT`. Belegt am selben Tag: +- Treiber 45 von 45 grün. Die neuen Fälle 36 bis 45 waren je mindestens einmal rot: gegen den Läufer vor der Änderung (36, 38, 40), gegen den Stand vor der Nachbesserung (40, 41, 43, 45) und gegen kaputte Varianten (37 ohne eigene Konfiguration, 39 ohne `--confcutdir`, 42 ohne Rückgabewert 3, 43 ohne `setup.py`-Zweig oder ohne zweite Suche, 44 nach dem ersten Abbruch ohne weitere Gruppe). +- Gegenprobe an allen sechs `mutanten.toml` im Vault, alter gegen neuer Läufer auf Kopien aus `git archive HEAD` unter `/tmp`: Headscale A, Mattermost, After Install C1 und D-40 gleich (175, 130, 69 und 41 Mutanten `OK`); Headscale B und Trading Plattform WH enden bei beiden mit Exit 2, weil ihr Stand nicht zum Plan passt. Unter dem Windows-Temp endete C1 mit dem alten Läufer mit 101 × `FEHLT`, mit dem neuen mit 69 `OK`. +- Ein `pruefer` fand in der ersten Fassung den Symlink-Fall und fehlende Fälle; die Nachbesserung behob beides. Ein zweiter `pruefer` fand danach keinen Verhaltensfehler mehr, nur eine Aufräumliste (Kopfzeile je Interpreter ohne eigenen Fall, Kurzfassung bei Rückgabewert 3, `;` und Seitenvorschub in `tox.ini`, Selbstprüfung der Probe, lange Fassung im Bericht, zwei Lücken benennen); sie ist in Nachbesserung 2 erledigt, Treiber 46 von 46. +- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`, `08-gegenprobe2/` mit dem nachgebesserten Läufer, `09-*` für Nachbesserung 2). + 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). diff --git a/.vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration-nachbesserung-1.md b/.vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration-nachbesserung-1.md new file mode 100644 index 0000000..1ea91da --- /dev/null +++ b/.vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration-nachbesserung-1.md @@ -0,0 +1,1023 @@ +--- +type: plan +title: "Mutantenläufer: Nachbesserung 1 nach der Prüfung vom 2026-10-06" +status: active +summary: "Delta zum Plan 2026-10-06-pytest-konfiguration: Wurzel über realpath, Abschnittszeile wie iniconfig, kein Absturz bei überlangem Knoten, Ursache vorn in der Mutantenzeile, Kopfzeile je Interpreter; neue Gleichwertigkeitsprobe gegen pytest und Treiber-Fälle 41 bis 45; Entwurf und LIESMICH nachgezogen." +tags: [plan, vault-tools, pruefwerkzeuge] +created: 2026-10-06 +updated: 2026-10-06 +modell: sonnet +effort: low +modell_begruendung: "Jeder Block steht wörtlich da, Zieldateien und Verbote sind genannt; Entwurf und LIESMICH tragen typografische Anführungszeichen, Gedankenstriche und Auslassungspunkte, die Haiku still ersetzt." +--- + +Übersicht: [LIESMICH der Planwerkzeuge](../../LIESMICH.md) · Vorgänger: [Plan 2026-10-06-pytest-konfiguration](2026-10-06-pytest-konfiguration.md) + +# Mutantenläufer: Nachbesserung 1 + +## Warum so + +Ein `pruefer` hat den umgesetzten Plan am 2026-10-06 geprüft (Treiber selbst gefahren, fünf eigene Varianten, Gleichwertigkeitsprobe über 25 Aufbauten gegen `determine_setup` und echte `pytest --co`-Läufe, Gegenprobe an drei Projekten). Seine Befunde und was dieser Plan daraus macht: + +| Befund | Schwere | Hier | +|---|---|---| +| Wurzel über einen Symlink: `os.path.abspath` löst nicht auf, pytest rechnet aber vom aufgelösten Arbeitsordner aus (`main.py:828`); mit `-c /…` wurden Knoten zu `::test_…`, die Grundlinie zu `FEHLT` | wichtig | `os.path.realpath`; Treiber-Fall 41 | +| Neuer Code ohne eigenen Fall: Rückgabewert 3, `setup.py`, zweite Suche, Konfiguration im Unterordner, Abbruch in nur einer Interpretergruppe an zwei Stellen, Symlink, Kopfzeile | wichtig | Fälle 41 bis 45, Probe `mutanten_lauf_probe_konfig.py` als Fall 43, Kopfzeile in 36 und 37 | +| Ursache des Abbruchs in der Mutantenzeile abgeschnitten (120 Zeichen, Präfix und Interpreterpfad zuerst) | klein | Kurzfassung mit der Ursache vorn; Fall 40 prüft die Zeile selbst | +| Absturz bei Dateinamen über 255 Zeichen (`OSError` aus `Path.exists`) | klein | `_existiert` wie `safe_exists`; Fall 45 | +| `tox.ini` mit `[pytest] # Kommentar` gilt bei pytest, beim Läufer nicht; Satz zu kaputten Dateien stimmt nur für `pyproject.toml` | klein | Abschnittszeile wie `_parseline` in iniconfig 2.3.0 (`[` in Spalte 0, Kommentare nach `#` und `;` entfernt, `]` am Ende); Entwurf berichtigt | +| pytest meldet bei einer nicht sammelbaren Datei `found no collectors for`, nicht „not found“ | klein | Kopfkommentar und Entwurf | +| Entwurf Schritt 3 und „Auswertung je Test“ widersprechen dem Abbruch-Absatz | klein | Ausnahme dort genannt | +| Kopfzeile kann bei mehreren Interpretern eine nicht benutzte Konfiguration nennen | klein | eine Kopfzeile je Interpreter | +| `config.inipath` ist ohne eigene Konfiguration die leere Datei; `paths`-Werte aus `-o`/`PYTEST_ADDOPTS` gelten dann relativ zum Plugin-Ordner | klein | als benannte Lücke im Kopfkommentar und Entwurf | + +- **Quelle der Abschnittszeile:** iniconfig 2.3.0 im WSL-Venv `trading`, `iniconfig/_parse.py`, `_parseline` (Zeilen 102 bis 127), nachgelesen am 2026-10-06 (`COMMENTCHARS = "#;"`, Abschnitt nur bei `line[0] == "["`, Kommentare per `split`, Name `line[1:-1]`). +- **Die Probe des Prüfers** hatte im `conftest.py`-Text ein `'\n'` innerhalb einer Zeichenkette; im geschriebenen `conftest.py` wurde daraus ein echter Zeilenumbruch mitten im Literal, die Datei war nicht ladbar. Die neue Probe schreibt den Zeilenumbruch mit `chr(10)`. +- **Gemessen vom Koordinator am 2026-10-06** (Rohdaten `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/07-*`): neuer Stand 45 von 45; der Stand vor dieser Nachbesserung rot bei 40, 41, 43 und 45; Varianten rot genau an ihrem Fall (ohne Rückgabewert 3 → 42; ohne `setup.py`-Zweig → 43 mit `setup_py_unterordner`; ohne zweite Suche → 43 mit drei Aufbauten; nach dem ersten Abbruch keine weitere Gruppe → 44 an erster Stelle). + +## Zieldateien + +Pfade relativ zu `D:/Claude/.vault-tools/plan-kopie/`: + +- `mutanten_lauf.py` (geändert) +- `mutanten_lauf_treiber.py` (geändert) +- `mutanten_lauf_probe_konfig.py` (neu) +- `mutanten_lauf_ENTWURF.md` (geändert) +- `LIESMICH.md` (geändert) + +## Verbote + +- Nur die fünf Zieldateien anlegen oder ändern. `mutanten_lauf_kopie.sh`, `rot_nachweis.py`, die Planwerkzeuge und `.claude/workflows/*` bleiben unberührt. +- Kein `git add`, kein `git commit`. +- Jeden Block byte-genau übernehmen: keine Umformulierung, keine Ersetzung typografischer Zeichen, keine zusätzlichen Leerzeilen. Aus-Block als `old_string`, wird-Block als `new_string` im Edit-Werkzeug; die neue Datei mit dem Write-Werkzeug. +- Steht ein Aus-Block nicht genau einmal in seiner Datei, oder weicht eine Ausgabe von der Erwartung ab, hältst du an und meldest es. Nichts still zurechtbiegen. +- `python -m py_compile` legt `__pycache__` an; lösch den Ordner danach wieder. + +## Schritt 1 — Tests zuerst: Treiber und Probe + +Wende alle Blöcke dieses Abschnitts an. + +### `mutanten_lauf_treiber.py`, Stelle 1 von 8 + +Aus: + +```python +#!/usr/bin/env python3 +"""Treiber fuer mutanten_lauf.py: 40 Angriffsfaelle auf einer Wegwerfkopie. + +``` + +wird: + +```python +#!/usr/bin/env python3 +"""Treiber fuer mutanten_lauf.py: 45 Angriffsfaelle auf einer Wegwerfkopie. + +``` + +### `mutanten_lauf_treiber.py`, Stelle 2 von 8 + +Aus: + +```python +Dazu ein Bedienfehler von pytest in der Grundlinie und unter einem Mutanten. +""" +``` + +wird: + +```python +Dazu ein Bedienfehler von pytest in der Grundlinie und unter einem Mutanten. + +Faelle 41 bis 45 (Nachbesserung nach der Pruefung vom 2026-10-06): Wurzel ueber +einen Symlink, Rueckgabewert 3, die Gleichwertigkeitsprobe gegen pytest selbst +(mutanten_lauf_probe_konfig.py), ein Abbruch in nur einer von zwei +Interpretergruppen an erster und an letzter Stelle, ein ueberlanger Knoten. +""" +``` + +### `mutanten_lauf_treiber.py`, Stelle 3 von 8 + +Aus: + +```python +LAEUFER = HIER / "mutanten_lauf.py" +INTERPRETER = "/home/claude/venvs/trading/bin/python" +``` + +wird: + +```python +LAEUFER = HIER / "mutanten_lauf.py" +PROBE = HIER / "mutanten_lauf_probe_konfig.py" +INTERPRETER = "/home/claude/venvs/trading/bin/python" +``` + +### `mutanten_lauf_treiber.py`, Stelle 4 von 8 + +Aus: + +```python + ergebnis = starte_laeufer(wurzel, toml) + ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout + melde("Fall 36", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) +``` + +wird: + +```python + ergebnis = starte_laeufer(wurzel, toml) + ok = ( + ergebnis.returncode == 0 + and "D1 OK" in ergebnis.stdout + and "D2 OK" in ergebnis.stdout + and "keine in der Wurzel, leere Datei" in ergebnis.stdout + ) + melde("Fall 36", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) +``` + +### `mutanten_lauf_treiber.py`, Stelle 5 von 8 + +Aus: + +```python + and "D2 OK" in sauber.stdout + and abgewaehlt.returncode == 2 +``` + +wird: + +```python + and "D2 OK" in sauber.stdout + and "k/pyproject.toml (aus der Wurzel)" in sauber.stdout + and abgewaehlt.returncode == 2 +``` + +### `mutanten_lauf_treiber.py`, Stelle 6 von 8 + +Aus: + +```python + ergebnis = starte_laeufer(wurzel, toml) + ok = ( + ergebnis.returncode == 1 + and "M40 UNKLAR" in ergebnis.stdout + and "pytest-Bedienfehler (Rueckgabewert 4)" in ergebnis.stdout + ) +``` + +wird: + +```python + ergebnis = starte_laeufer(wurzel, toml) + mutantenzeile = [z for z in ergebnis.stdout.split("\n") if z.startswith("M40 ")] + ok = ( + ergebnis.returncode == 1 + and len(mutantenzeile) == 1 + and "M40 UNKLAR" in mutantenzeile[0] + and "pytest-Bedienfehler (Rueckgabewert 4)" in mutantenzeile[0] + and "--gibt-es-nicht" in mutantenzeile[0] + ) +``` + +### `mutanten_lauf_treiber.py`, Stelle 7 von 8 + +Aus: + +```python + +FAELLE = [ +``` + +wird: + +```python + +def fall_41(basis, projektpfad): + rahmen = Path(tempfile.mkdtemp(prefix="mutanten_lauf_treiber_symlink_")) + try: + dateien = baue_projekt(rahmen / "echt" / "k") + os.symlink(rahmen / "echt", rahmen / "link") + wurzel = rahmen / "link" / "k" + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + ergebnis = starte_laeufer(wurzel, toml) + ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout + melde("Fall 41", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + pruefe_unveraendert(dateien, snap, "Fall 41") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_42(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False) + try: + (wurzel / "conftest.py").write_text( + "def pytest_collection_modifyitems(items):\n" + " raise RuntimeError(\"interner Fehler fuer Fall 42\")\n", + encoding="utf-8", + ) + dateien = dateien + [wurzel / "conftest.py"] + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + ergebnis = starte_laeufer(wurzel, toml) + ausgabe = ergebnis.stdout + ergebnis.stderr + ok = ( + ergebnis.returncode == 2 + and "pytest-Abbruch mit internem Fehler (Rueckgabewert 3)" in ausgabe + and "interner Fehler fuer Fall 42" in ausgabe + and "ist FEHLT" not in ausgabe + ) + melde("Fall 42", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe)) + pruefe_unveraendert(dateien, snap, "Fall 42") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_43(basis, projektpfad): + arbeit = Path(tempfile.mkdtemp(prefix="mutanten_lauf_treiber_probe_")) + try: + ergebnis = subprocess.run( + [INTERPRETER, str(PROBE), str(LAEUFER), str(arbeit)], + capture_output=True, text=True, timeout=900, + ) + ok = ergebnis.returncode == 0 and ", abweichungen: 0" in ergebnis.stdout + melde("Fall 43", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + finally: + shutil.rmtree(arbeit, ignore_errors=True) + + +def fall_44(basis, projektpfad): + zweit_dir = Path(tempfile.mkdtemp(prefix="mutanten_lauf_fall44_")) + venv_link = zweit_dir / "venv_zwei" + zweit_python = venv_link / "bin" / "python" + try: + os.symlink(Path(INTERPRETER).parent.parent, venv_link) + toml = projektpfad / "mutanten.toml" + zusatz = "\n[python_je_datei]\n\"tests/test_zeit.py\" = \"{0}\"\n".format(zweit_python) + befund_zwei = "Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit {0};".format(zweit_python) + befund_eins = "Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit {0};".format(INTERPRETER) + reihenfolgen = [ + ["tests/test_zeit.py::test_gibt_es_nicht", "tests/test_modul.py::test_f1_positiv"], + ["tests/test_modul.py::test_f1_positiv", "tests/test_zeit.py::test_gibt_es_nicht"], + ] + for stelle, rot_bei in zip(("erste", "letzte"), reihenfolgen): + schreibe_toml(toml, kopf(zusatz=zusatz) + mutant_luecke("M44", rot_bei)) + ergebnis = starte_laeufer(basis, toml) + ausgabe = ergebnis.stdout + ergebnis.stderr + ok = ( + ergebnis.returncode == 2 + and ausgabe.count(befund_zwei) == 1 + and befund_eins not in ausgabe + and "ist FEHLT" not in ausgabe + and "test_f1_positiv ist" not in ausgabe + ) + melde("Fall 44 ({0} Stelle)".format(stelle), ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe)) + finally: + shutil.rmtree(zweit_dir, ignore_errors=True) + + +def fall_45(basis, projektpfad): + toml = projektpfad / "mutanten.toml" + schreibe_toml(toml, kopf() + mutant_luecke("M45", ["tests/" + "a" * 300 + ".py::test_x"])) + ergebnis = starte_laeufer(basis, toml) + ausgabe = ergebnis.stdout + ergebnis.stderr + ok = ( + ergebnis.returncode == 2 + and "Grundlinie: pytest-" in ausgabe + and "unerwarteter Abbruch" not in ausgabe + ) + melde("Fall 45", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe[-2000:])) + + +FAELLE = [ +``` + +### `mutanten_lauf_treiber.py`, Stelle 8 von 8 + +Aus: + +```python + fall_31, fall_32, fall_33, fall_34, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40, +] +``` + +wird: + +```python + fall_31, fall_32, fall_33, fall_34, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40, + fall_41, fall_42, fall_43, fall_44, fall_45, +] +``` + +### `mutanten_lauf_probe_konfig.py` + +Die ganze Datei ist neu. + +```python +#!/usr/bin/env python3 +"""Gleichwertigkeitsprobe fuer die Konfigurationssuche von mutanten_lauf.py. + +Zweck: Je Aufbau zwei Vergleiche. Erstens pytest_konfig_bestimmen aus +mutanten_lauf.py gegen determine_setup aus pytest selbst (Konfigurationsdatei +und rootdir). Zweitens ein echtes "pytest --co" ohne -c in einem sauberen +Rahmen gegen einen Lauf mit den Argumenten des Laeufers in einem Rahmen, ueber +dem eine fremde pyproject.toml, ein fremdes conftest.py und ein fremdes +setup.py liegen. Verglichen werden Rueckgabewert, Knotennamen und die Liste +der geladenen conftest.py-Dateien. + +Aufruf (in WSL, mit dem Python eines Venvs, in dem pytest liegt): + python mutanten_lauf_probe_konfig.py +Exit 0 ohne Abweichung, 1 mit Abweichung, 2 bei falschem Aufruf. + +Geltungsbereich: ein Fangnetz fuer die Aufbauten unten, kein Nachweis fuer +jeden denkbaren Baum. Ein Knoten mit ".." fehlt, weil mutanten_lauf.py ihn +schon beim Lesen der mutanten.toml abweist. Eine Wurzel ueber einen Symlink +prueft Treiber-Fall 41. + +Grundlage: Probe des Pruefers vom 2026-10-06 (25 Aufbauten). Das conftest.py +der Probe schreibt den Zeilenumbruch hier mit chr(10), damit kein Backslash +durch die Zeichenkette muss. +""" + +import importlib.util +import os +import subprocess +import sys +from pathlib import Path + +TEST = "def test_a():\n pass\n\n\ndef test_b():\n pass\n" +CONFTEST = "import os\nopen(os.environ['CFLOG'], 'a').write(__file__ + chr(10))\n" +INI = "[pytest]\n" + +AUFBAUTEN = { + "wurzel_pytest_ini": ({"pytest.ini": INI, "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "unterordner_ini": ( + {"tests/pytest.ini": INI, "tests/test_a.py": TEST, "conftest.py": CONFTEST, "tests/conftest.py": CONFTEST}, + ["tests/test_a.py::test_a"], + ), + "zwei_ordner_ini_im_ersten": ( + {"tests/a/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST}, + ["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"], + ), + "zwei_ordner_ini_im_zweiten": ( + {"tests/b/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST}, + ["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"], + ), + "setup_py_wurzel": ({"setup.py": "", "tests/test_a.py": TEST, "conftest.py": CONFTEST}, ["tests/test_a.py::test_a"]), + "setup_py_unterordner": ( + {"sub/setup.py": "", "sub/tests/test_a.py": TEST, "conftest.py": CONFTEST, "sub/conftest.py": CONFTEST}, + ["sub/tests/test_a.py::test_a"], + ), + "setup_py_neben_ini": ( + {"tests/a/setup.py": "", "tests/b/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST}, + ["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"], + ), + "pyproject_ohne_abschnitt": ( + {"pyproject.toml": "[project]\nname = 'x'\n", "tests/test_a.py": TEST, "conftest.py": CONFTEST}, + ["tests/test_a.py::test_a"], + ), + "pyproject_ohne_unten_ini_oben": ( + {"tests/pyproject.toml": "[project]\nname = 'x'\n", "pytest.ini": INI, "tests/test_a.py": TEST}, + ["tests/test_a.py::test_a"], + ), + "pyproject_ohne_oben_ini_unten": ( + {"pyproject.toml": "[project]\nname = 'x'\n", "tests/pytest.ini": INI, "tests/test_a.py": TEST}, + ["tests/test_a.py::test_a"], + ), + "pyproject_ini_options_leer": ({"pyproject.toml": "[tool.pytest.ini_options]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "pyproject_tool_pytest_leer": ({"pyproject.toml": "[tool.pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "pyproject_toml_modus": ({"pyproject.toml": "[tool.pytest]\nminversion = '8'\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "tox_mit_abschnitt": ({"tox.ini": "[tox]\n[pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "tox_ohne_abschnitt": ({"tox.ini": "[tox]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "tox_abschnitt_mit_kommentar": ({"tox.ini": "[pytest] # k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "setup_cfg_tool_pytest": ({"setup.cfg": "[tool:pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "pytest_toml_leer": ({"pytest.toml": "", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "punkt_pytest_ini_unterordner": ({"tests/.pytest.ini": "", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "knoten_fehlt_teilweise": ( + {"tests/pytest.ini": INI, "tests/test_a.py": TEST}, + ["tests/test_a.py::test_a", "gibtsnicht/test_x.py::test_a"], + ), + "knoten_fehlen_alle": ({"tests/pytest.ini": INI, "tests/test_a.py": TEST}, ["gibtsnicht/test_x.py::test_a"]), + "ordner_als_knoten": ({"tests/pytest.ini": INI, "tests/test_a.py": TEST}, ["tests"]), + "testdatei_in_der_wurzel": ({"test_a.py": TEST, "conftest.py": CONFTEST}, ["test_a.py::test_a"]), + "ohne_konfiguration": ( + {"tests/test_a.py": TEST, "conftest.py": CONFTEST, "tests/conftest.py": CONFTEST}, + ["tests/test_a.py::test_a"], + ), +} + + +def baue(rahmen, dateien, fremd): + wurzel = rahmen / "k" + for rel, inhalt in dateien.items(): + pfad = wurzel / rel + pfad.parent.mkdir(parents=True, exist_ok=True) + pfad.write_text(inhalt, encoding="utf-8") + if fremd: + (rahmen / "pyproject.toml").write_text("[tool.pytest.ini_options]\naddopts = '--new-first'\n", encoding="utf-8") + (rahmen / "conftest.py").write_text(CONFTEST, encoding="utf-8") + (rahmen / "setup.py").write_text("", encoding="utf-8") + return wurzel + + +def sammeln(wurzel, zusatz, knoten, cflog): + umgebung = dict(os.environ, CFLOG=str(cflog), PYTHONDONTWRITEBYTECODE="1") + lauf = subprocess.run( + [sys.executable, "-m", "pytest", "--co", "-q", "-p", "no:cacheprovider"] + zusatz + knoten, + cwd=str(wurzel), env=umgebung, capture_output=True, text=True, timeout=120, + ) + knotennamen = sorted(zeile for zeile in lauf.stdout.splitlines() if "::" in zeile) + geladen = [] + if cflog.exists(): + geladen = sorted(os.path.relpath(z, str(wurzel)) for z in cflog.read_text(encoding="utf-8").split("\n") if z) + cflog.unlink() + return lauf.returncode, knotennamen, geladen + + +def main(): + if len(sys.argv) != 3: + print("Aufruf: mutanten_lauf_probe_konfig.py ") + return 2 + spec = importlib.util.spec_from_file_location("mutanten_lauf_unter_probe", sys.argv[1]) + laeufer = importlib.util.module_from_spec(spec) + spec.loader.exec_module(laeufer) + from _pytest.config.findpaths import determine_setup + + arbeit = Path(sys.argv[2]) + leere_konfig = arbeit / "plugin" / "pytest.ini" + leere_konfig.parent.mkdir(parents=True, exist_ok=True) + leere_konfig.write_text("", encoding="utf-8") + cflog = arbeit / "cflog" + abweichungen = 0 + for name, (dateien, knoten) in AUFBAUTEN.items(): + sauber = baue(arbeit / name / "sauber", dateien, False) + fremd = baue(arbeit / name / "fremd", dateien, True) + vorher = os.getcwd() + os.chdir(sauber) + try: + rootdir, inipath, _, _ = determine_setup( + inifile=None, override_ini=None, args=knoten, rootdir_cmd_arg=None, invocation_dir=sauber, + ) + erwartet = (str(inipath) if inipath else None, str(rootdir)) + except Exception as exc: + erwartet = ("Ausnahme " + type(exc).__name__, None) + finally: + os.chdir(vorher) + konfig, rootdir_l = laeufer.pytest_konfig_bestimmen(sauber, knoten) + bekommen = (str(konfig) if konfig else None, str(rootdir_l)) + ohne_c = sammeln(sauber, [], knoten, cflog) + mit_c = sammeln(fremd, laeufer.pytest_konfig_argumente(fremd, knoten, leere_konfig), knoten, cflog) + gleich_suche = erwartet == bekommen + gleich_lauf = ohne_c == mit_c + if not (gleich_suche and gleich_lauf): + abweichungen += 1 + print("{0:34s} suche={1} lauf={2}".format( + name, "gleich" if gleich_suche else "ABWEICHUNG", "gleich" if gleich_lauf else "ABWEICHUNG")) + if not gleich_suche: + print(" pytest: {0}".format(erwartet)) + print(" laeufer: {0}".format(bekommen)) + if not gleich_lauf: + print(" ohne -c, sauber: {0}".format(ohne_c)) + print(" laeufer, fremd: {0}".format(mit_c)) + print("aufbauten: {0}, abweichungen: {1}".format(len(AUFBAUTEN), abweichungen)) + return 0 if abweichungen == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) +``` + + +### Erwarteter Fehlschlag vor der Umsetzung + +Der neue Treiber und die neue Probe laufen gegen den Läufer, wie er jetzt im Baum steht (vor Schritt 2). In der Git-Bash: + +```bash +# Laeufer im jetzigen Stand, neuer Treiber und neue Probe in einen leeren Rot-Ordner +R=/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot2 +rm -rf "$R" && mkdir -p "$R" +cp /d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_probe_konfig.py "$R/" +``` + +Dann in der PowerShell: + +```powershell +# Treiber in WSL gegen den Laeufer vor der Nachbesserung +wsl -d Debian -- timeout 1500 /home/claude/venvs/trading/bin/python /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot2/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot2\ausgabe.txt" +"exit=$LASTEXITCODE" +Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot2\ausgabe.txt" -Pattern "FEHLER:|gerechnete" +``` + +Erwartet: `exit=1`, und die Treffer enden genau auf diese fünf Zeilen: + +``` +Fall 40 FEHLER: exit=1 +Fall 41 FEHLER: exit=2 +Fall 43 FEHLER: exit=1 +Fall 45 FEHLER: exit=3 +gerechnete Fehler: 4 +``` + +## Schritt 2 — Umsetzung + +Wende alle Blöcke dieses Abschnitts in der Reihenfolge an. + +### `mutanten_lauf.py`, Stelle 1 von 13 + +Aus: + +```python +-p no:cacheprovider passten, und jeder Test der Grundlinie stand als FEHLT da +(Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Benannte Luecken: +tox.ini und setup.cfg werden zeilenweise nach ihrem Abschnitt durchsucht, nicht +mit iniconfig; ein setup.cfg mit [pytest] (bei pytest selbst ein Fehler) gilt +hier als keine Konfiguration. + +``` + +wird: + +```python +-p no:cacheprovider passten, und jeder Test der Grundlinie stand als FEHLT da +(Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Die Wurzel wird +vorher mit realpath aufgeloest: pytest rechnet vom Arbeitsordner aus, den der +Kernel ohne Verknuepfungen meldet; ueber einen Symlink angegeben, passten die +Knotennamen sonst nicht (Pruefung vom 2026-10-06). Benannte Luecken: tox.ini +und setup.cfg werden zeilenweise nach ihrem Abschnitt durchsucht wie in +iniconfig 2.3.0 (_parse.py, _parseline), eine Datei, die iniconfig nicht lesen +kann, uebergeht der Laeufer, pytest bricht dort mit 4 ab; ein setup.cfg mit +[pytest] (bei pytest selbst ein Fehler) gilt hier als keine Konfiguration; ohne +Konfiguration in der Wurzel ist config.inipath die leere Datei, ini-Werte vom +Typ paths aus -o oder PYTEST_ADDOPTS gelten dann relativ zum Plugin-Ordner. + +``` + +### `mutanten_lauf.py`, Stelle 2 von 13 + +Aus: + +```python +ExitCode in _pytest/config/__init__.py), steht das Ende seiner Ausgabe in der +Grundlinie als eigener Befund und unter einem Mutanten in der Meldung der +Tests ohne Bericht und ohne Sammelfehler. Ein Sammelfehler bleibt ROT, auch +wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht +sammeln laesst, ergibt zusaetzlich "not found" (Treiber-Fall 11). + +``` + +wird: + +```python +ExitCode in _pytest/config/__init__.py), steht das Ende seiner Ausgabe in der +Grundlinie als eigener Befund je Interpreter und unter einem Mutanten, als +eine Zeile mit der Ursache vorn, in der Meldung der Tests ohne Bericht und +ohne Sammelfehler. Ein Sammelfehler bleibt ROT, auch wenn pytest dabei mit 4 +endet: Ein Knoten in einer Datei, die sich nicht sammeln laesst, ergibt +zusaetzlich "found no collectors for" (Treiber-Fall 11). + +``` + +### `mutanten_lauf.py`, Stelle 3 von 13 + +Aus: + +```python +def _ini_hat_abschnitt(pfad, abschnitt): + """Zeilenweise Suche nach [abschnitt]. Eine nicht lesbare Datei zaehlt als + Treffer, damit pytest sie selbst liest und den Fehler selbst meldet.""" + try: +``` + +wird: + +```python +def _ini_hat_abschnitt(pfad, abschnitt): + """Suche nach der Abschnittszeile wie _parseline in iniconfig 2.3.0: "[" in + Spalte 0, Kommentare nach # und ; entfernt, dann "]" am Ende. Eine nicht + lesbare Datei zaehlt als Treffer, damit pytest den Fehler selbst meldet.""" + try: +``` + +### `mutanten_lauf.py`, Stelle 4 von 13 + +Aus: + +```python + return True + kopf = "[" + abschnitt + "]" + return any(zeile.strip() == kopf for zeile in text.split("\n")) + +``` + +wird: + +```python + return True + for zeile in text.split("\n"): + zeile = zeile.rstrip() + if not zeile.startswith("["): + continue + for zeichen in "#;": + zeile = zeile.split(zeichen)[0].rstrip() + if zeile.endswith("]") and zeile[1:-1] == abschnitt: + return True + return False + +``` + +### `mutanten_lauf.py`, Stelle 5 von 13 + +Aus: + +```python + +def _konfig_suchen(startordner, wurzel): +``` + +wird: + +```python + +def _existiert(pfad): + """Wie safe_exists in pytest 9.1.1: ein zu langer Name ist kein Absturz.""" + try: + return pfad.exists() + except OSError: + return False + + +def _konfig_suchen(startordner, wurzel): +``` + +### `mutanten_lauf.py`, Stelle 6 von 13 + +Aus: + +```python + for pfad in pfade: + if not pfad.exists(): + continue +``` + +wird: + +```python + for pfad in pfade: + if not _existiert(pfad): + continue +``` + +### `mutanten_lauf.py`, Stelle 7 von 13 + +Aus: + +```python + Rueckgabe (konfig, rootdir); konfig ist None, wenn die Wurzel keine hat.""" + wurzel = Path(os.path.abspath(str(wurzel))) + ordner = [] +``` + +wird: + +```python + Rueckgabe (konfig, rootdir); konfig ist None, wenn die Wurzel keine hat.""" + wurzel = Path(os.path.realpath(str(wurzel))) + ordner = [] +``` + +### `mutanten_lauf.py`, Stelle 8 von 13 + +Aus: + +```python + pfad = wurzel / knoten.split("::", 1)[0] + if pfad.exists(): + ordner.append(pfad if pfad.is_dir() else pfad.parent) +``` + +wird: + +```python + pfad = wurzel / knoten.split("::", 1)[0] + if _existiert(pfad): + ordner.append(pfad if pfad.is_dir() else pfad.parent) +``` + +### `mutanten_lauf.py`, Stelle 9 von 13 + +Aus: + +```python + return zeilen[-AUSGABE_ENDE_ZEILEN:] + +``` + +wird: + +```python + return zeilen[-AUSGABE_ENDE_ZEILEN:] + + +def kernzeile(ende, rueckgabe): + """Die Zeile, die die Ursache nennt: bei 4 die erste mit "error" ausser der + Gebrauchszeile von argparse, bei 3 und ohne Treffer die letzte.""" + if rueckgabe == 4: + for zeile in ende: + if "error" in zeile.lower() and not zeile.startswith("ERROR: usage:"): + return zeile.strip() + return ende[-1].strip() if ende else "(keine Ausgabe)" + +``` + +### `mutanten_lauf.py`, Stelle 10 von 13 + +Aus: + +```python + if not zeit_ueberschritten and proc.returncode in PYTEST_ABBRUCH: + abbruch = "{0} (Rueckgabewert {1}) mit {2}; Ende der Ausgabe:\n {3}".format( + PYTEST_ABBRUCH[proc.returncode], proc.returncode, python_exe, + "\n ".join(ausgabe_ende(*ausgabe)) or "(keine Ausgabe)", + ) + +``` + +wird: + +```python + if not zeit_ueberschritten and proc.returncode in PYTEST_ABBRUCH: + ende = ausgabe_ende(*ausgabe) + abbruch = { + "lang": "{0} (Rueckgabewert {1}) mit {2}; Ende der Ausgabe:\n {3}".format( + PYTEST_ABBRUCH[proc.returncode], proc.returncode, python_exe, + "\n ".join(ende) or "(keine Ausgabe)", + ), + "kurz": "{0} (Rueckgabewert {1}): {2}".format( + PYTEST_ABBRUCH[proc.returncode], proc.returncode, kernzeile(ende, proc.returncode), + ), + } + +``` + +### `mutanten_lauf.py`, Stelle 11 von 13 + +Aus: + +```python + if python_exe in abbrueche: + befunde.append("Grundlinie: " + abbrueche[python_exe]) + abgebrochen.update(knoten_liste) +``` + +wird: + +```python + if python_exe in abbrueche: + befunde.append("Grundlinie: " + abbrueche[python_exe]["lang"]) + abgebrochen.update(knoten_liste) +``` + +### `mutanten_lauf.py`, Stelle 12 von 13 + +Aus: + +```python + if abbruch: + meldung = abbruch + if ergebnis in ("GRUEN", "UEBERSPRUNGEN", "ABGEWAEHLT"): +``` + +wird: + +```python + if abbruch: + meldung = abbruch["kurz"] + if ergebnis in ("GRUEN", "UEBERSPRUNGEN", "ABGEWAEHLT"): +``` + +### `mutanten_lauf.py`, Stelle 13 von 13 + +Aus: + +```python + alle_knoten.append(knoten) + print("pytest-konfiguration (Grundlinie): {0}".format(pytest_konfig_beschreibung(kontext["wurzel"], alle_knoten))) + +``` + +wird: + +```python + alle_knoten.append(knoten) + gruppen = gruppiere_nach_interpreter(alle_knoten, kontext["python"], kontext["python_je_datei"]) + for interpreter, knoten_liste in gruppen.items(): + print("pytest-konfiguration (Grundlinie, {0}): {1}".format( + interpreter, pytest_konfig_beschreibung(kontext["wurzel"], knoten_liste))) + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 1 von 5 + +Aus: + +```markdown +| 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. | +| Welche pytest-Konfiguration gilt? | Quelle: pytest 9.1.1, `_pytest/config/findpaths.py` (`determine_setup`, `locate_config`, `get_common_ancestor`, `load_config_dict_from_file`; SHA-256 `535d149e6f3962e4…`, in den Venvs `trading` und `mattermost` gleich) und `_pytest/config/__init__.py` (Vorgabe von `confcutdir`, `ExitCode`), nachgelesen am 2026-10-06. Projektvorgabe seit 2026-10-06: gesucht wird wie pytest, aber nie oberhalb der Wurzel (Abschnitt 3, „Konfiguration“). | + +``` + +wird: + +```markdown +| 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. | +| Welche pytest-Konfiguration gilt? | Quelle: pytest 9.1.1, `_pytest/config/findpaths.py` (`determine_setup`, `locate_config`, `get_common_ancestor`, `load_config_dict_from_file`; SHA-256 `535d149e6f3962e4…`, in den Venvs `trading` und `mattermost` gleich) und `_pytest/config/__init__.py` (Vorgabe von `confcutdir`, `ExitCode`), nachgelesen am 2026-10-06; die Abschnittszeile in `tox.ini` und `setup.cfg` aus iniconfig 2.3.0 (`_parse.py`, `_parseline`) im selben Venv. Projektvorgabe seit 2026-10-06: gesucht wird wie pytest, aber nie oberhalb der Wurzel (Abschnitt 3, „Konfiguration“). | + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 2 von 5 + +Aus: + +```markdown +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: +``` + +wird: + +```markdown +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. Bricht pytest für einen Interpreter ab (Rückgabewert 3 oder 4), steht statt der Zeilen je Test ein Befund für diesen Interpreter (unten, „Abbruch von pytest“). +4. **Mutanten** in der Reihenfolge der Datei, `MUSTER`-Fälle ausgelassen. Je Mutant: +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 3 von 5 + +Aus: + +```markdown + +- Reihenfolge der Namen und Bedingungen wie in pytest 9.1.1: `pytest.toml`, `.pytest.toml`, `pytest.ini`, `.pytest.ini` immer; `pyproject.toml` mit `[tool.pytest]` oder `[tool.pytest.ini_options]`; `tox.ini` mit `[pytest]`; `setup.cfg` mit `[tool:pytest]`; ohne Treffer die erste `pyproject.toml` überhaupt. Erst vom gemeinsamen Vorfahren aus, ohne Treffer und ohne `setup.py` dazwischen noch einmal von jedem Testordner aus. +- Gefunden: `-c `. pytest setzt `rootdir` und `confcutdir` dann selbst auf ihren Ordner, wie bei der eigenen Suche. +- Nicht gefunden: `-c --rootdir --confcutdir `, `r` ist der Ordner mit `setup.py` oder sonst die Wurzel. Ohne `--confcutdir` läge `confcutdir` im Plugin-Ordner, und pytest lüde `conftest.py`-Dateien oberhalb der Wurzel. +- Die Kopfzeile `pytest-konfiguration (Grundlinie): …` nennt, was für die Grundlinie gilt. +- Benannte Lücken: `tox.ini` und `setup.cfg` werden zeilenweise nach ihrem Abschnitt durchsucht, nicht mit `iniconfig`; eine nicht lesbare oder kaputte Datei gilt als Treffer, damit pytest den Fehler selbst meldet; ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration. + +**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit ; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler diesen Text als Meldung; sie machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich „not found“ (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte). + +**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. + +``` + +wird: + +```markdown + +- Die Wurzel wird zuerst mit `realpath` aufgelöst. pytest rechnet vom Arbeitsordner aus, den der Kernel ohne Verknüpfungen meldet; über einen Symlink angegeben, wurden die Knotennamen sonst zu `::test_…` (Prüfung vom 2026-10-06, Treiber-Fall 41). +- Reihenfolge der Namen und Bedingungen wie in pytest 9.1.1: `pytest.toml`, `.pytest.toml`, `pytest.ini`, `.pytest.ini` immer; `pyproject.toml` mit `[tool.pytest]` oder `[tool.pytest.ini_options]`; `tox.ini` mit `[pytest]`; `setup.cfg` mit `[tool:pytest]`; ohne Treffer die erste `pyproject.toml` überhaupt. Erst vom gemeinsamen Vorfahren aus, ohne Treffer und ohne `setup.py` dazwischen noch einmal von jedem Testordner aus. Die Abschnittszeile in `tox.ini` und `setup.cfg` erkennt der Läufer wie `_parseline` in iniconfig 2.3.0: `[` in Spalte 0, Kommentare nach `#` und `;` entfernt, dann `]` am Ende. Ein Pfad, dessen Name zu lang ist, gilt wie bei `safe_exists` in pytest als nicht vorhanden. +- Gefunden: `-c `. pytest setzt `rootdir` und `confcutdir` dann selbst auf ihren Ordner, wie bei der eigenen Suche. +- Nicht gefunden: `-c --rootdir --confcutdir `, `r` ist der Ordner mit `setup.py` oder sonst die Wurzel. Ohne `--confcutdir` läge `confcutdir` im Plugin-Ordner, und pytest lüde `conftest.py`-Dateien oberhalb der Wurzel. +- Die Kopfzeilen `pytest-konfiguration (Grundlinie, ): …` nennen je Interpreter, was für seinen Aufruf in der Grundlinie gilt. +- Gegen pytest selbst geprüft wird das mit `mutanten_lauf_probe_konfig.py` (Treiber-Fall 43): 24 Aufbauten, je `pytest_konfig_bestimmen` gegen `determine_setup` und ein echtes `pytest --co` ohne `-c` gegen einen Lauf mit den Argumenten des Läufers unter fremder Konfiguration. +- Benannte Lücken: Eine `tox.ini` oder ein `setup.cfg`, die iniconfig nicht lesen kann, übergeht der Läufer, pytest bricht dort mit 4 ab; eine nicht lesbare `pyproject.toml` oder `pytest.toml` gilt dagegen als Treffer, damit pytest den Fehler selbst meldet. Ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration. Ohne Konfiguration in der Wurzel ist `config.inipath` die leere Datei; ini-Werte vom Typ `paths`, die über `-o` oder `PYTEST_ADDOPTS` kommen, gelten dann relativ zum Plugin-Ordner. + +**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit ; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler eine kurze Fassung als Meldung, die Ursache vorn, damit sie in die 120 Zeichen der Mutantenzeile passt: `pytest-Bedienfehler (Rueckgabewert 4): `. Die Zeile ist bei 4 die erste mit „error“ außer der Gebrauchszeile `ERROR: usage:` von argparse, bei 3 die letzte. Diese Tests machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich `found no collectors for …` (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte). + +**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, außer pytest ist für diesen Interpreter abgebrochen; dann steht der eine Befund aus „Abbruch von pytest“. + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 4 von 5 + +Aus: + +```markdown + +Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testdatei, `pyproject.toml` mit `[tool.pytest.ini_options]`; für die Fälle 36 bis 40 je ein eigenes als Unterordner `k/` eines Rahmen-Ordners), 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. + +``` + +wird: + +```markdown + +Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testdatei, `pyproject.toml` mit `[tool.pytest.ini_options]`; für die Fälle 36 bis 42 je ein eigenes als Unterordner `k/` eines Rahmen-Ordners), 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. + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 5 von 5 + +Aus: + +```markdown +| 35 | ein Mutant hängt, der Läufer bekommt nach drei Sekunden SIGTERM (eigenes `TMPDIR`), seit 2026-09-28 | Zeile `ABBRUCH: SIGTERM`, kein Prozess mit `HOME` im Fall-Tmpdir, keine `mutanten_lauf_*`-Reste, Zieldatei byte-gleich | +| 36 | Wurzel ohne eigene Konfiguration, darüber eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"` (der Fall der Plankritik 2 von C1), seit 2026-10-06 | Exit 0, zweimal `OK` | +| 37 | Wurzel mit eigener `pyproject.toml` (Marker `integration` abgewählt), darüber dieselbe fremde | sauberer Fall Exit 0; Test mit Marker in der Grundlinie Exit 2 und `ABGEWAEHLT`, die eigene Konfiguration gilt also | +| 38 | eigene Konfiguration mit `addopts = "--gibt-es-nicht"` | Exit 2, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` aus der pytest-Ausgabe, keine Zeile `ist FEHLT` | +| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` | +| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1, `UNKLAR`, Meldung mit `pytest-Bedienfehler (Rueckgabewert 4)` | + +``` + +wird: + +```markdown +| 35 | ein Mutant hängt, der Läufer bekommt nach drei Sekunden SIGTERM (eigenes `TMPDIR`), seit 2026-09-28 | Zeile `ABBRUCH: SIGTERM`, kein Prozess mit `HOME` im Fall-Tmpdir, keine `mutanten_lauf_*`-Reste, Zieldatei byte-gleich | +| 36 | Wurzel ohne eigene Konfiguration, darüber eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"` (der Fall der Plankritik 2 von C1), seit 2026-10-06 | Exit 0, zweimal `OK`, Kopfzeile „keine in der Wurzel, leere Datei“ | +| 37 | Wurzel mit eigener `pyproject.toml` (Marker `integration` abgewählt), darüber dieselbe fremde | sauberer Fall Exit 0 mit Kopfzeile `k/pyproject.toml (aus der Wurzel)`; Test mit Marker in der Grundlinie Exit 2 und `ABGEWAEHLT`, die eigene Konfiguration gilt also | +| 38 | eigene Konfiguration mit `addopts = "--gibt-es-nicht"` | Exit 2, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` aus der pytest-Ausgabe, keine Zeile `ist FEHLT` | +| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` | +| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1; die Mutantenzeile selbst trägt `UNKLAR`, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` | +| 41 | Wurzel über einen Symlink angegeben (`link/k` → `echt/k`), seit der Nachbesserung vom 2026-10-06 | Exit 0, zweimal `OK` | +| 42 | ein `conftest.py` der Wurzel wirft in `pytest_collection_modifyitems` | Exit 2, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3)` und der Text der Ausnahme, keine Zeile `ist FEHLT` | +| 43 | `mutanten_lauf_probe_konfig.py`: 24 Aufbauten gegen `determine_setup` und gegen echte `pytest --co`-Läufe | Exit 0, `abweichungen: 0` | +| 44 | zwei Interpretergruppen (`python_je_datei`), nur in einer ein Knoten, den es nicht gibt; diese Gruppe einmal an erster, einmal an letzter Stelle | je Exit 2, genau ein Befund mit dem Interpreter dieser Gruppe, keiner für die andere, keine Zeile `ist FEHLT` | +| 45 | ein Knoten mit einem Dateinamen über 255 Zeichen | Exit 2 mit einem Befund `Grundlinie: pytest-…`, kein `unerwarteter Abbruch` | + +``` + +### `LIESMICH.md`, Stelle 1 von 2 + +Aus: + +```markdown + +8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel --mutanten /mutanten.toml [--nur ID,…] [--nur-muster] [--bericht ] [--zeitlimit ]`, 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`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). 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. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`). + **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** 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 in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes. +``` + +wird: + +```markdown + +8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel --mutanten /mutanten.toml [--nur ID,…] [--nur-muster] [--bericht ] [--zeitlimit ]`, 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`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Ob die Suche pytest selbst entspricht, prüft `mutanten_lauf_probe_konfig.py ` mit dem Python eines Venvs, in dem pytest liegt (24 Aufbauten, Exit 0 ohne Abweichung). 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. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`). + **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** 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 in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes. +``` + +### `LIESMICH.md`, Stelle 2 von 2 + +Aus: + +```markdown + +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. +``` + +wird: + +```markdown + +Nachbesserung vom 2026-10-06 (Plankritik 2 von C1 im Projekt After Install, Plan `docs/plans/2026-10-06-pytest-konfiguration.md` und seine Nachbesserung): pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel; Abbruch von pytest mit Ursache statt `FEHLT`. Belegt am selben Tag: +- Treiber 45 von 45 grün. Die neuen Fälle 36 bis 45 waren je mindestens einmal rot: gegen den Läufer vor der Änderung (36, 38, 40), gegen den Stand vor der Nachbesserung (40, 41, 43, 45) und gegen kaputte Varianten (37 ohne eigene Konfiguration, 39 ohne `--confcutdir`, 42 ohne Rückgabewert 3, 43 ohne `setup.py`-Zweig oder ohne zweite Suche, 44 nach dem ersten Abbruch ohne weitere Gruppe). +- Gegenprobe an allen sechs `mutanten.toml` im Vault, alter gegen neuer Läufer auf Kopien aus `git archive HEAD` unter `/tmp`: Headscale A, Mattermost, After Install C1 und D-40 gleich (175, 130, 69 und 41 Mutanten `OK`); Headscale B und Trading Plattform WH enden bei beiden mit Exit 2, weil ihr Stand nicht zum Plan passt. Unter dem Windows-Temp endete C1 mit dem alten Läufer mit 101 × `FEHLT`, mit dem neuen mit 69 `OK`. +- Ein `pruefer` fand in der ersten Fassung den Symlink-Fall und die fehlenden Fälle; beides ist in der Nachbesserung behoben. +- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`). + +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. +``` + + +## Schritt 3 — Verifikation + +```bash +# Kompilieren, nur ASCII in den Python-Dateien, keine CR in allen fuenf Dateien, Cache wieder weg +cd /d/Claude/.vault-tools/plan-kopie +python -m py_compile mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py && echo kompiliert +rm -rf __pycache__ +LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py +for f in mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py mutanten_lauf_ENTWURF.md LIESMICH.md; do printf '%s %s\n' "$f" "$(tr -cd '\r' < "$f" | wc -c)"; done +``` + +Erwartet: `kompiliert`, je Python-Datei die Zahl `0`, je Datei die Zahl `0`. + +```powershell +# Treiber gegen den nachgebesserten Laeufer in WSL +wsl -d Debian -- timeout 1500 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot2\ausgabe-neu.txt" +"exit=$LASTEXITCODE" +Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot2\ausgabe-neu.txt" -Pattern "FEHLER:|gerechnete" +``` + +Erwartet: `exit=0` und als einziger Treffer eine Zeile, die auf `gerechnete Fehler: 0` endet. + +Danach `C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot2` löschen. + +## Bericht + +Melde: je Zieldatei die Zahl der angewendeten Aus/wird-Paare (und für die neue Datei „neu“), die Ausgaben aus Schritt 1 und Schritt 3 im Wortlaut, und jede Abweichung vom Plan oder getroffene Annahme. „Keine Abweichungen“ nur, wenn es keine gab. + +## Fertig heißt (Koordinator) + +1. Die fünf Zieldateien sind byte-gleich mit der Kopie, auf die dieser Plan angewendet wurde (`cmp`), und sonst ist unter `.vault-tools/` nichts geändert außer den Plänen unter `plan-kopie/docs/`. +2. Treiber in WSL: Exit 0, `gerechnete Fehler: 0`, selbst gefahren. +3. Rot-Nachweis wie unter „Gemessen“ oben. +4. Gegenprobe an den sechs `mutanten.toml` mit dem nachgebesserten Läufer, alter gegen neuer Läufer, wie beim Vorgänger: gleicher Exit-Code, gleiches Ergebnis je Test der Grundlinie, gleicher Status je Mutant; C1 unter dem Windows-Temp mit dem neuen Läufer wie unter `/tmp`. +5. Ein `pruefer` hat die Nachbesserung gelesen; er sagt, ob eine weitere Runde noch Substanz hat. + +## Risiken + +1. **Die Probe hängt an pytest-Interna.** `determine_setup` ist keine öffentliche Schnittstelle. Ändert eine spätere pytest-Fassung die Signatur, scheitert Fall 43 mit einer Ausnahme. Merkmal: `Fall 43 FEHLER` mit einem `TypeError` im Text; dann die Probe an die neue Fassung anpassen, nicht den Läufer. +2. **Laufzeit des Treibers.** Fall 43 fährt 48 `pytest --co`-Läufe. Merkmal: Der Treiber braucht deutlich länger als vorher; das Zeitlimit im Befehl oben ist dafür auf 1500 Sekunden gesetzt. diff --git a/.vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration-nachbesserung-2.md b/.vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration-nachbesserung-2.md new file mode 100644 index 0000000..d04aec9 --- /dev/null +++ b/.vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration-nachbesserung-2.md @@ -0,0 +1,634 @@ +--- +type: plan +title: "Mutantenläufer: Nachbesserung 2 (Aufräumliste der zweiten Prüfung)" +status: active +summary: "Delta zur Nachbesserung 1: Kopfzeile je Interpreter mit eigenem Fall, Kurzfassung bei Rückgabewert 3 aus der letzten INTERNALERROR-Zeile, lange Fassung im Bericht, Abschnittszeile mit splitlines, Probe mit Selbstprüfung und zwei neuen Aufbauten, Treiber-Fall 46, zwei Lücken benannt." +tags: [plan, vault-tools, pruefwerkzeuge] +created: 2026-10-06 +updated: 2026-10-06 +modell: sonnet +effort: low +modell_begruendung: "Jeder Block steht wörtlich da; Entwurf und LIESMICH tragen typografische Zeichen, die Haiku still ersetzt." +--- + +Übersicht: [LIESMICH der Planwerkzeuge](../../LIESMICH.md) · Vorgänger: [Nachbesserung 1](2026-10-06-pytest-konfiguration-nachbesserung-1.md) + +# Mutantenläufer: Nachbesserung 2 + +## Warum so + +Der zweite `pruefer` fand am 2026-10-06 keinen Verhaltensfehler mit Wirkung auf die Projekte, nur eine Aufräumliste, und hielt eine weitere volle Prüfrunde für entbehrlich. Umgesetzt: + +| Befund | Hier | +|---|---| +| Kopfzeile je Interpreter ohne eigenen Fall (wichtig) | Fall 44 zählt genau zwei Kopfzeilen, je eine je Interpreter | +| Kurzfassung bei Rückgabewert 3 ohne Fall | Fall 46 (Mutant lässt `conftest.py` werfen). Dabei gefunden: „letzte Zeile“ war die Summenzeile `no tests ran …`; jetzt die letzte `INTERNALERROR>`-Zeile | +| `;` als Kommentarzeichen ohne Fall | Aufbau `tox_abschnitt_mit_semikolon` in der Probe | +| iniconfig trennt Zeilen mit `splitlines` | Läufer auch; Aufbau `tox_seitenvorschub_vor_abschnitt` | +| Lange Fassung unter einem Mutanten verloren | Bericht trägt beim Test `ausgabe`; Fälle 40 und 46 prüfen es | +| Probe prüft ihre Voraussetzung nicht | Selbstprüfung: ohne `-c` Rückgabewert 0 (außer `knoten_fehl*`), mit `conftest.py` im Aufbau mindestens eines geladen | +| `rootdir` unter der Wurzel, Zeilentrenner | als Lücken benannt | +| `safe_exists` fängt auch `ValueError` | `_existiert` ebenso | +| LIESMICH: Rohdaten `08-gegenprobe2/`, Satz zur Nachbesserung | nachgezogen | + +**Gemessen vom Koordinator am 2026-10-06** (Rohdaten `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/09-*`): neuer Stand 46 von 46; Stand nach Nachbesserung 1 rot bei 40, 43, 46; Varianten rot genau an ihrem Fall (eine Kopfzeile → 44 an beiden Stellen; erste statt letzte `INTERNALERROR>`-Zeile → 46; ohne den Zweig für 3 → 46; kaputtes `conftest.py` der ersten Probe → 43; ohne `;` → 43). + +## Zieldateien + +Pfade relativ zu `D:/Claude/.vault-tools/plan-kopie/`: + +- `mutanten_lauf.py` (geändert) +- `mutanten_lauf_treiber.py` (geändert) +- `mutanten_lauf_probe_konfig.py` (geändert) +- `mutanten_lauf_ENTWURF.md` (geändert) +- `LIESMICH.md` (geändert) + +## Verbote + +- Nur die fünf Zieldateien ändern. Kein `git add`, kein `git commit`. +- Jeden Block byte-genau übernehmen; der ganze Aus-Block als `old_string`, der ganze wird-Block als `new_string`. Steht ein Aus-Block nicht genau einmal da oder weicht eine Ausgabe ab: anhalten und melden. +- `python -m py_compile` legt `__pycache__` an; den Ordner danach löschen. + +## Schritt 1 — Tests zuerst: Treiber und Probe + +### `mutanten_lauf_treiber.py`, Stelle 1 von 9 + +Aus: + +```python +#!/usr/bin/env python3 +"""Treiber fuer mutanten_lauf.py: 45 Angriffsfaelle auf einer Wegwerfkopie. + +``` + +wird: + +```python +#!/usr/bin/env python3 +"""Treiber fuer mutanten_lauf.py: 46 Angriffsfaelle auf einer Wegwerfkopie. + +``` + +### `mutanten_lauf_treiber.py`, Stelle 2 von 9 + +Aus: + +```python +Interpretergruppen an erster und an letzter Stelle, ein ueberlanger Knoten. +""" +``` + +wird: + +```python +Interpretergruppen an erster und an letzter Stelle, ein ueberlanger Knoten. +Fall 46 (Nachbesserung 2): Rueckgabewert 3 unter einem Mutanten, Kurzfassung in +der Mutantenzeile und lange Fassung im Bericht. +""" +``` + +### `mutanten_lauf_treiber.py`, Stelle 3 von 9 + +Aus: + +```python +import hashlib +import os +``` + +wird: + +```python +import hashlib +import json +import os +``` + +### `mutanten_lauf_treiber.py`, Stelle 4 von 9 + +Aus: + +```python + )) + ergebnis = starte_laeufer(wurzel, toml) + mutantenzeile = [z for z in ergebnis.stdout.split("\n") if z.startswith("M40 ")] + ok = ( +``` + +wird: + +```python + )) + bericht = rahmen / "bericht40.json" + ergebnis = starte_laeufer(wurzel, toml, bericht_pfad=bericht) + mutantenzeile = [z for z in ergebnis.stdout.split("\n") if z.startswith("M40 ")] + try: + ausgabe_bericht = json.loads(bericht.read_text(encoding="utf-8"))["mutanten"][0]["tests"][0].get("ausgabe", "") + except (OSError, ValueError, KeyError, IndexError): + ausgabe_bericht = "" + ok = ( +``` + +### `mutanten_lauf_treiber.py`, Stelle 5 von 9 + +Aus: + +```python + and "--gibt-es-nicht" in mutantenzeile[0] + ) +``` + +wird: + +```python + and "--gibt-es-nicht" in mutantenzeile[0] + and "Ende der Ausgabe:" in ausgabe_bericht + ) +``` + +### `mutanten_lauf_treiber.py`, Stelle 6 von 9 + +Aus: + +```python + ausgabe = ergebnis.stdout + ergebnis.stderr + ok = ( +``` + +wird: + +```python + ausgabe = ergebnis.stdout + ergebnis.stderr + kopfzeilen = [z for z in ergebnis.stdout.split("\n") if z.startswith("pytest-konfiguration (Grundlinie, ")] + ok = ( +``` + +### `mutanten_lauf_treiber.py`, Stelle 7 von 9 + +Aus: + +```python + and "test_f1_positiv ist" not in ausgabe + ) +``` + +wird: + +```python + and "test_f1_positiv ist" not in ausgabe + and len(kopfzeilen) == 2 + and sum(1 for z in kopfzeilen if z.startswith("pytest-konfiguration (Grundlinie, {0}): ".format(INTERPRETER))) == 1 + and sum(1 for z in kopfzeilen if z.startswith("pytest-konfiguration (Grundlinie, {0}): ".format(zweit_python))) == 1 + ) +``` + +### `mutanten_lauf_treiber.py`, Stelle 8 von 9 + +Aus: + +```python + +FAELLE = [ +``` + +wird: + +```python + +def fall_46(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False) + try: + (wurzel / "conftest.py").write_text("X = 1\n", encoding="utf-8") + dateien = dateien + [wurzel / "conftest.py"] + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, kopf() + ( + "[[mutant]]\n" + "id = \"M46\"\n" + "datei = \"conftest.py\"\n" + "aus = '''\n" + "X = 1\n" + "'''\n" + "wird = '''\n" + "def pytest_collection_modifyitems(items):\n" + " raise RuntimeError(\"intern Fall 46\")\n" + "'''\n" + "rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n" + )) + bericht = rahmen / "bericht46.json" + ergebnis = starte_laeufer(wurzel, toml, bericht_pfad=bericht) + mutantenzeile = [z for z in ergebnis.stdout.split("\n") if z.startswith("M46 ")] + try: + ausgabe_bericht = json.loads(bericht.read_text(encoding="utf-8"))["mutanten"][0]["tests"][0].get("ausgabe", "") + except (OSError, ValueError, KeyError, IndexError): + ausgabe_bericht = "" + ok = ( + ergebnis.returncode == 1 + and len(mutantenzeile) == 1 + and "M46 UNKLAR" in mutantenzeile[0] + and "pytest-Abbruch mit internem Fehler (Rueckgabewert 3): " in mutantenzeile[0] + and "intern Fall 46" in mutantenzeile[0] + and "Ende der Ausgabe:" in ausgabe_bericht + ) + melde("Fall 46", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + pruefe_unveraendert(dateien, snap, "Fall 46") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +FAELLE = [ +``` + +### `mutanten_lauf_treiber.py`, Stelle 9 von 9 + +Aus: + +```python + fall_31, fall_32, fall_33, fall_34, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40, + fall_41, fall_42, fall_43, fall_44, fall_45, +] +``` + +wird: + +```python + fall_31, fall_32, fall_33, fall_34, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40, + fall_41, fall_42, fall_43, fall_44, fall_45, fall_46, +] +``` + +### `mutanten_lauf_probe_konfig.py`, Stelle 1 von 3 + +Aus: + +```python +prueft Treiber-Fall 41. + +``` + +wird: + +```python +prueft Treiber-Fall 41. + +Die Probe prueft ihre eigene Voraussetzung mit: Ohne -c im sauberen Rahmen +muss pytest mit 0 enden (ausser bei den Aufbauten knoten_fehl*), und wo der +Aufbau ein conftest.py hat, muss eines geladen sein. Sonst endeten beide +Laeufe gleich kaputt, und "gleich" hiesse nichts. + +``` + +### `mutanten_lauf_probe_konfig.py`, Stelle 2 von 3 + +Aus: + +```python + "tox_abschnitt_mit_kommentar": ({"tox.ini": "[pytest] # k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "setup_cfg_tool_pytest": ({"setup.cfg": "[tool:pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), +``` + +wird: + +```python + "tox_abschnitt_mit_kommentar": ({"tox.ini": "[pytest] # k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "tox_abschnitt_mit_semikolon": ({"tox.ini": "[pytest] ; k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "tox_seitenvorschub_vor_abschnitt": ({"tox.ini": "[tox]" + chr(12) + "[pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "setup_cfg_tool_pytest": ({"setup.cfg": "[tool:pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), +``` + +### `mutanten_lauf_probe_konfig.py`, Stelle 3 von 3 + +Aus: + +```python + gleich_lauf = ohne_c == mit_c + if not (gleich_suche and gleich_lauf): + abweichungen += 1 + print("{0:34s} suche={1} lauf={2}".format( + name, "gleich" if gleich_suche else "ABWEICHUNG", "gleich" if gleich_lauf else "ABWEICHUNG")) + if not gleich_suche: +``` + +wird: + +```python + gleich_lauf = ohne_c == mit_c + voraussetzung = name.startswith("knoten_fehl") or ohne_c[0] == 0 + if any(rel.endswith("conftest.py") for rel in dateien) and not ohne_c[2]: + voraussetzung = False + if not (gleich_suche and gleich_lauf and voraussetzung): + abweichungen += 1 + print("{0:34s} suche={1} lauf={2} voraussetzung={3}".format( + name, "gleich" if gleich_suche else "ABWEICHUNG", "gleich" if gleich_lauf else "ABWEICHUNG", + "ok" if voraussetzung else "VERLETZT")) + if not gleich_suche: +``` + + +### Erwarteter Fehlschlag vor der Umsetzung + +In der Git-Bash: + +```bash +# Laeufer im jetzigen Stand, neuer Treiber und neue Probe in einen leeren Rot-Ordner +R=/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot3 +rm -rf "$R" && mkdir -p "$R" +cp /d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_probe_konfig.py "$R/" +``` + +In der PowerShell: + +```powershell +# Treiber in WSL gegen den Laeufer vor Nachbesserung 2 +wsl -d Debian -- timeout 1500 /home/claude/venvs/trading/bin/python /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot3/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot3\ausgabe.txt" +"exit=$LASTEXITCODE" +Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot3\ausgabe.txt" -Pattern "FEHLER:|gerechnete" +``` + +Erwartet: `exit=1`, und die Treffer enden genau auf diese vier Zeilen: + +``` +Fall 40 FEHLER: exit=1 +Fall 43 FEHLER: exit=1 +Fall 46 FEHLER: exit=1 +gerechnete Fehler: 3 +``` + +## Schritt 2 — Umsetzung + +### `mutanten_lauf.py`, Stelle 1 von 7 + +Aus: + +```python +Konfiguration in der Wurzel ist config.inipath die leere Datei, ini-Werte vom +Typ paths aus -o oder PYTEST_ADDOPTS gelten dann relativ zum Plugin-Ordner. + +``` + +wird: + +```python +Konfiguration in der Wurzel ist config.inipath die leere Datei, ini-Werte vom +Typ paths aus -o oder PYTEST_ADDOPTS gelten dann relativ zum Plugin-Ordner; +iniconfig trennt Zeilen mit splitlines, der Laeufer auch. Liegt rootdir unter +der Wurzel (Konfiguration oder setup.py in einem Unterordner der Tests), +bildet pytest die Knotennamen relativ dazu, und rot_bei passt nicht mehr: Die +Grundlinie meldet FEHLT, die Kopfzeile nennt die Datei. + +``` + +### `mutanten_lauf.py`, Stelle 2 von 7 + +Aus: + +```python + return True + for zeile in text.split("\n"): + zeile = zeile.rstrip() +``` + +wird: + +```python + return True + for zeile in text.splitlines(): + zeile = zeile.rstrip() +``` + +### `mutanten_lauf.py`, Stelle 3 von 7 + +Aus: + +```python + return pfad.exists() + except OSError: + return False +``` + +wird: + +```python + return pfad.exists() + except (OSError, ValueError): + return False +``` + +### `mutanten_lauf.py`, Stelle 4 von 7 + +Aus: + +```python + """Die Zeile, die die Ursache nennt: bei 4 die erste mit "error" ausser der + Gebrauchszeile von argparse, bei 3 und ohne Treffer die letzte.""" + if rueckgabe == 4: +``` + +wird: + +```python + """Die Zeile, die die Ursache nennt: bei 4 die erste mit "error" ausser der + Gebrauchszeile von argparse, bei 3 die letzte mit INTERNALERROR> (danach + folgt noch die Summenzeile, Treiber-Fall 46), ohne Treffer die letzte.""" + if rueckgabe == 4: +``` + +### `mutanten_lauf.py`, Stelle 5 von 7 + +Aus: + +```python + return zeile.strip() + return ende[-1].strip() if ende else "(keine Ausgabe)" +``` + +wird: + +```python + return zeile.strip() + if rueckgabe == 3: + interne = [zeile for zeile in ende if zeile.startswith("INTERNALERROR>")] + if interne: + return interne[-1].strip() + return ende[-1].strip() if ende else "(keine Ausgabe)" +``` + +### `mutanten_lauf.py`, Stelle 6 von 7 + +Aus: + +```python + ergebnis, meldung = ergebnis_bestimmen(knoten, reports, deselected) + if ergebnis == "FEHLT": +``` + +wird: + +```python + ergebnis, meldung = ergebnis_bestimmen(knoten, reports, deselected) + abbruch = None + if ergebnis == "FEHLT": +``` + +### `mutanten_lauf.py`, Stelle 7 von 7 + +Aus: + +```python + luecke = True + test_ergebnisse.append({"eintrag": eintrag, "knoten": knoten, "ergebnis": ergebnis, "meldung": meldung}) + +``` + +wird: + +```python + luecke = True + eintrag_bericht = {"eintrag": eintrag, "knoten": knoten, "ergebnis": ergebnis, "meldung": meldung} + if ergebnis == "FEHLT" and abbruch: + eintrag_bericht["ausgabe"] = abbruch["lang"] + test_ergebnisse.append(eintrag_bericht) + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 1 von 3 + +Aus: + +```markdown +- Die Kopfzeilen `pytest-konfiguration (Grundlinie, ): …` nennen je Interpreter, was für seinen Aufruf in der Grundlinie gilt. +- Gegen pytest selbst geprüft wird das mit `mutanten_lauf_probe_konfig.py` (Treiber-Fall 43): 24 Aufbauten, je `pytest_konfig_bestimmen` gegen `determine_setup` und ein echtes `pytest --co` ohne `-c` gegen einen Lauf mit den Argumenten des Läufers unter fremder Konfiguration. +- Benannte Lücken: Eine `tox.ini` oder ein `setup.cfg`, die iniconfig nicht lesen kann, übergeht der Läufer, pytest bricht dort mit 4 ab; eine nicht lesbare `pyproject.toml` oder `pytest.toml` gilt dagegen als Treffer, damit pytest den Fehler selbst meldet. Ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration. Ohne Konfiguration in der Wurzel ist `config.inipath` die leere Datei; ini-Werte vom Typ `paths`, die über `-o` oder `PYTEST_ADDOPTS` kommen, gelten dann relativ zum Plugin-Ordner. + +**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit ; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler eine kurze Fassung als Meldung, die Ursache vorn, damit sie in die 120 Zeichen der Mutantenzeile passt: `pytest-Bedienfehler (Rueckgabewert 4): `. Die Zeile ist bei 4 die erste mit „error“ außer der Gebrauchszeile `ERROR: usage:` von argparse, bei 3 die letzte. Diese Tests machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich `found no collectors for …` (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte). + +``` + +wird: + +```markdown +- Die Kopfzeilen `pytest-konfiguration (Grundlinie, ): …` nennen je Interpreter, was für seinen Aufruf in der Grundlinie gilt. +- Gegen pytest selbst geprüft wird das mit `mutanten_lauf_probe_konfig.py` (Treiber-Fall 43): 26 Aufbauten, je `pytest_konfig_bestimmen` gegen `determine_setup` und ein echtes `pytest --co` ohne `-c` gegen einen Lauf mit den Argumenten des Läufers unter fremder Konfiguration. Die Probe sichert ihre Voraussetzung mit: Ohne `-c` endet pytest mit 0 (außer bei fehlenden Knoten), und wo der Aufbau ein `conftest.py` hat, ist eines geladen. +- Benannte Lücken: Eine `tox.ini` oder ein `setup.cfg`, die iniconfig nicht lesen kann, übergeht der Läufer, pytest bricht dort mit 4 ab; eine nicht lesbare `pyproject.toml` oder `pytest.toml` gilt dagegen als Treffer, damit pytest den Fehler selbst meldet. Ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration. Ohne Konfiguration in der Wurzel ist `config.inipath` die leere Datei; ini-Werte vom Typ `paths`, die über `-o` oder `PYTEST_ADDOPTS` kommen, gelten dann relativ zum Plugin-Ordner. Liegt `rootdir` unter der Wurzel (Konfiguration oder `setup.py` in einem Unterordner der Tests), bildet pytest die Knotennamen relativ dazu, und `rot_bei` passt nicht mehr: Die Grundlinie meldet `FEHLT`, die Kopfzeile nennt die gefundene Datei; heute betrifft das kein Projekt im Vault. Zeilen trennt der Läufer wie iniconfig mit `splitlines`, also auch an Seitenvorschub und Unicode-Zeilentrennern. + +**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit ; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler eine kurze Fassung als Meldung, die Ursache vorn, damit sie in die 120 Zeichen der Mutantenzeile passt: `pytest-Bedienfehler (Rueckgabewert 4): `. Die Zeile ist bei 4 die erste mit „error“ außer der Gebrauchszeile `ERROR: usage:` von argparse, bei 3 die letzte mit `INTERNALERROR>` (danach steht noch die Summenzeile `no tests ran …`, Treiber-Fall 46), ohne Treffer die letzte überhaupt. Die lange Fassung steht im Bericht beim Test unter `ausgabe`. Diese Tests machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich `found no collectors for …` (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte). + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 2 von 3 + +Aus: + +```markdown + +`--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. + +``` + +wird: + +```markdown + +`--bericht` schreibt dasselbe als JSON: `{"wurzel", "mutanten_datei", "befunde": [...], "grundlinie": {knoten: {ergebnis, meldung}}, "mutanten": [{"id", "datei", "status", "hinweis", "treffer", "tests": [{"eintrag", "knoten", "ergebnis", "meldung", "ausgabe" (nur bei einem Abbruch von pytest)}]}], "zusammenfassung": {...}}`. Der Bericht wird auch bei einem Abbruch geschrieben, mit allem, was bis dahin feststand. + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 3 von 3 + +Aus: + +```markdown +| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` | +| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1; die Mutantenzeile selbst trägt `UNKLAR`, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` | +| 41 | Wurzel über einen Symlink angegeben (`link/k` → `echt/k`), seit der Nachbesserung vom 2026-10-06 | Exit 0, zweimal `OK` | +| 42 | ein `conftest.py` der Wurzel wirft in `pytest_collection_modifyitems` | Exit 2, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3)` und der Text der Ausnahme, keine Zeile `ist FEHLT` | +| 43 | `mutanten_lauf_probe_konfig.py`: 24 Aufbauten gegen `determine_setup` und gegen echte `pytest --co`-Läufe | Exit 0, `abweichungen: 0` | +| 44 | zwei Interpretergruppen (`python_je_datei`), nur in einer ein Knoten, den es nicht gibt; diese Gruppe einmal an erster, einmal an letzter Stelle | je Exit 2, genau ein Befund mit dem Interpreter dieser Gruppe, keiner für die andere, keine Zeile `ist FEHLT` | +| 45 | ein Knoten mit einem Dateinamen über 255 Zeichen | Exit 2 mit einem Befund `Grundlinie: pytest-…`, kein `unerwarteter Abbruch` | + +``` + +wird: + +```markdown +| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` | +| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1; die Mutantenzeile selbst trägt `UNKLAR`, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht`; im Bericht steht beim Test `ausgabe` mit „Ende der Ausgabe:“ | +| 41 | Wurzel über einen Symlink angegeben (`link/k` → `echt/k`), seit der Nachbesserung vom 2026-10-06 | Exit 0, zweimal `OK` | +| 42 | ein `conftest.py` der Wurzel wirft in `pytest_collection_modifyitems` | Exit 2, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3)` und der Text der Ausnahme, keine Zeile `ist FEHLT` | +| 43 | `mutanten_lauf_probe_konfig.py`: 26 Aufbauten gegen `determine_setup` und gegen echte `pytest --co`-Läufe, mit Selbstprüfung der Voraussetzung | Exit 0, `abweichungen: 0` | +| 44 | zwei Interpretergruppen (`python_je_datei`), nur in einer ein Knoten, den es nicht gibt; diese Gruppe einmal an erster, einmal an letzter Stelle | je Exit 2, genau ein Befund mit dem Interpreter dieser Gruppe, keiner für die andere, keine Zeile `ist FEHLT`; genau zwei Kopfzeilen, je eine je Interpreter | +| 45 | ein Knoten mit einem Dateinamen über 255 Zeichen | Exit 2 mit einem Befund `Grundlinie: pytest-…`, kein `unerwarteter Abbruch` | +| 46 | ein Mutant lässt ein `conftest.py` der Wurzel in `pytest_collection_modifyitems` werfen | Exit 1, die Mutantenzeile trägt `UNKLAR`, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3): ` und den Text der Ausnahme; im Bericht `ausgabe` mit „Ende der Ausgabe:“ | + +``` + +### `LIESMICH.md`, Stelle 1 von 2 + +Aus: + +```markdown + +8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel --mutanten /mutanten.toml [--nur ID,…] [--nur-muster] [--bericht ] [--zeitlimit ]`, 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`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Ob die Suche pytest selbst entspricht, prüft `mutanten_lauf_probe_konfig.py ` mit dem Python eines Venvs, in dem pytest liegt (24 Aufbauten, Exit 0 ohne Abweichung). 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. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`). + **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** 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 in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes. +``` + +wird: + +```markdown + +8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel --mutanten /mutanten.toml [--nur ID,…] [--nur-muster] [--bericht ] [--zeitlimit ]`, 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`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Ob die Suche pytest selbst entspricht, prüft `mutanten_lauf_probe_konfig.py ` mit dem Python eines Venvs, in dem pytest liegt (26 Aufbauten, Exit 0 ohne Abweichung). 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. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`). + **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** 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 in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes. +``` + +### `LIESMICH.md`, Stelle 2 von 2 + +Aus: + +```markdown +- Gegenprobe an allen sechs `mutanten.toml` im Vault, alter gegen neuer Läufer auf Kopien aus `git archive HEAD` unter `/tmp`: Headscale A, Mattermost, After Install C1 und D-40 gleich (175, 130, 69 und 41 Mutanten `OK`); Headscale B und Trading Plattform WH enden bei beiden mit Exit 2, weil ihr Stand nicht zum Plan passt. Unter dem Windows-Temp endete C1 mit dem alten Läufer mit 101 × `FEHLT`, mit dem neuen mit 69 `OK`. +- Ein `pruefer` fand in der ersten Fassung den Symlink-Fall und die fehlenden Fälle; beides ist in der Nachbesserung behoben. +- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`). + +``` + +wird: + +```markdown +- Gegenprobe an allen sechs `mutanten.toml` im Vault, alter gegen neuer Läufer auf Kopien aus `git archive HEAD` unter `/tmp`: Headscale A, Mattermost, After Install C1 und D-40 gleich (175, 130, 69 und 41 Mutanten `OK`); Headscale B und Trading Plattform WH enden bei beiden mit Exit 2, weil ihr Stand nicht zum Plan passt. Unter dem Windows-Temp endete C1 mit dem alten Läufer mit 101 × `FEHLT`, mit dem neuen mit 69 `OK`. +- Ein `pruefer` fand in der ersten Fassung den Symlink-Fall und fehlende Fälle; die Nachbesserung behob beides. Ein zweiter `pruefer` fand danach keinen Verhaltensfehler mehr, nur eine Aufräumliste (Kopfzeile je Interpreter ohne eigenen Fall, Kurzfassung bei Rückgabewert 3, `;` und Seitenvorschub in `tox.ini`, Selbstprüfung der Probe, lange Fassung im Bericht, zwei Lücken benennen); sie ist in Nachbesserung 2 erledigt, Treiber 46 von 46. +- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`, `08-gegenprobe2/` mit dem nachgebesserten Läufer, `09-*` für Nachbesserung 2). + +``` + + +## Schritt 3 — Verifikation + +```bash +# Kompilieren, ASCII, keine CR, Cache wieder weg +cd /d/Claude/.vault-tools/plan-kopie +python -m py_compile mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py && echo kompiliert +rm -rf __pycache__ +LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py +for f in mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py mutanten_lauf_ENTWURF.md LIESMICH.md; do printf '%s %s\n' "$f" "$(tr -cd '\r' < "$f" | wc -c)"; done +``` + +Erwartet: `kompiliert`, überall `0`. + +```powershell +# Treiber gegen den Laeufer nach Nachbesserung 2 +wsl -d Debian -- timeout 1500 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot3\ausgabe-neu.txt" +"exit=$LASTEXITCODE" +Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot3\ausgabe-neu.txt" -Pattern "FEHLER:|gerechnete" +``` + +Erwartet: `exit=0` und als einziger Treffer eine Zeile, die auf `gerechnete Fehler: 0` endet. Danach den Rot-Ordner und ein von WSL angelegtes `__pycache__` unter `plan-kopie/` löschen. + +## Bericht + +Je Zieldatei die Zahl der angewendeten Paare, die Ausgaben aus Schritt 1 und 3 im Wortlaut, jede Abweichung und Annahme. + +## Fertig heißt (Koordinator) + +1. Die fünf Zieldateien byte-gleich mit der Kopie mit angewendetem Plan (`cmp`). +2. Treiber 46 von 46, selbst gefahren. +3. Rot-Nachweis wie unter „Gemessen“. diff --git a/.vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration.md b/.vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration.md new file mode 100644 index 0000000..5c6f923 --- /dev/null +++ b/.vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration.md @@ -0,0 +1,1009 @@ +--- +type: plan +title: "Mutantenläufer: pytest-Konfiguration festnageln, Abbruch von pytest melden" +status: active +summary: "Ein Task: mutanten_lauf.py sucht die pytest-Konfiguration wie pytest 9.1.1, aber nie oberhalb der Wurzel, gibt sie mit -c vor und meldet einen Abbruch von pytest (Rückgabewert 3 oder 4) mit dem Ende der Ausgabe statt FEHLT je Test; dazu fünf Treiber-Fälle, Entwurf und LIESMICH." +tags: [plan, vault-tools, pruefwerkzeuge] +created: 2026-10-06 +updated: 2026-10-06 +modell: sonnet +effort: low +modell_begruendung: "Jeder Block steht wörtlich da, Zieldateien und Verbote sind genannt; die Blöcke für Entwurf und LIESMICH tragen typografische Anführungszeichen, Gedankenstriche und Auslassungspunkte, die Haiku still ersetzt, deshalb sonnet mit effort low." +--- + +Übersicht: [LIESMICH der Planwerkzeuge](../../LIESMICH.md) · Anlass: Fahrplan C1-Abschluss im Projekt After Install, Schritt 1 (`Code Projekte/active - Linux Härtung Rocky Debian Ubuntu - After Install/docs/plans/2026-10-05-fahrplan-c1-abschluss-c4.md`) + +# Mutantenläufer: pytest-Konfiguration festnageln, Abbruch von pytest melden + +## Warum so + +- **Der Fehler** (Plankritik 2 von C1 im Projekt After Install, `docs/plans/2026-10-05-cis-paket1-system/plankritik-2/befunde.json`, Linse `bezeichner`): Liegt die Wegwerfkopie unter dem Windows-Temp und läuft der Mutantenlauf über `/mnt/c`, findet pytest beim Suchen nach oben `C:/Users/PASCAL~1/AppData/Local/Temp/pyproject.toml`. Deren `addopts = "--new-first --failed-first"` brauchen den Cache, den `-p no:cacheprovider` abschaltet; pytest endet mit Rückgabewert 4. Der Läufer meldete daraufhin alle 80 Tests der Grundlinie als `FEHLT` mit leerer Meldung und verschluckte die Ausgabe von pytest. +- **Quelle der Suchregeln:** pytest 9.1.1 in den WSL-Venvs `trading` und `mattermost` (Python 3.13.5), `_pytest/config/findpaths.py` mit SHA-256 `535d149e6f3962e44260b6645bb354976e19531f5dd248e14ee7d7fff61c8848` in beiden, nachgelesen am 2026-10-06: `determine_setup`, `locate_config` (Namen `pytest.toml`, `.pytest.toml`, `pytest.ini`, `.pytest.ini`, `pyproject.toml`, `tox.ini`, `setup.cfg`; ohne Treffer die erste `pyproject.toml` überhaupt), `get_common_ancestor`, `load_config_dict_from_file`. Aus `_pytest/config/__init__.py`: `confcutdir` ist ohne Vorgabe der Ordner der Konfigurationsdatei, sonst `rootdir` (Zeilen 1598 bis 1603); `ExitCode` 3 ist `INTERNAL_ERROR`, 4 `USAGE_ERROR`. +- **Konfiguration der Wurzel bleibt gültig.** Ein bloßes `-c /dev/null` nähme Headscale und Mattermost (`pytest.ini` in der Wurzel) und der Trading Plattform (`pyproject.toml`) ihre Einstellungen; Treiber-Fall 15 (Marker `integration` per `addopts` abgewählt) fiele um. Deshalb sucht der Läufer wie pytest, nur nie oberhalb der Wurzel. +- **`--confcutdir`.** Mit der leeren Datei aus dem Plugin-Ordner läge `confcutdir` dort, und pytest lüde `conftest.py`-Dateien oberhalb der Wurzel. Ohne fremde Konfiguration war `confcutdir` bisher die Wurzel; `--confcutdir ` hält das fest. +- **Abbruch von pytest.** Bei Rückgabewert 3 oder 4 hält der Läufer die letzten 20 Zeilen der Ausgabe fest. In der Grundlinie ergibt das je Interpreter einen Befund statt einer `FEHLT`-Zeile je Test. Unter einem Mutanten trägt nur ein Test ohne Bericht und ohne Sammelfehler den Text; ein Sammelfehler bleibt `ROT`, denn pytest endet bei einem Knoten in einer nicht sammelbaren Datei ebenfalls mit 4 (`not found`). Eine erste Fassung, die jeden Abbruch als `UNKLAR` zählte, fiel am 2026-10-06 im Treiber-Fall 11 durch. +- **Gemessen vom Koordinator am 2026-10-06** (Rohdaten unter `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/`, Dateien `02-*`): Mit dem neuen Treiber und dem alten Läufer fallen genau die Fälle 36, 38 und 40; mit dem neuen Läufer fällt keiner. Drei kaputte Varianten des neuen Läufers zeigen die übrigen Fälle rot: Konfiguration der Wurzel nie benutzt → 15, 37, 38, 40; ohne `--confcutdir` → 39; Suche ohne Grenze an der Wurzel → 36. + +## Zieldateien + +Pfade relativ zu `D:/Claude/.vault-tools/plan-kopie/`: + +- `mutanten_lauf.py` (geändert) +- `mutanten_lauf_treiber.py` (geändert) +- `mutanten_lauf_ENTWURF.md` (geändert) +- `LIESMICH.md` (geändert) + +## Verbote + +- Nur die vier Zieldateien ändern. `mutanten_lauf_kopie.sh`, `rot_nachweis.py`, `plan_materialisiere.py`, `.claude/workflows/*` und alles andere bleiben unberührt. +- Kein `git add`, kein `git commit`. +- Jeden Block byte-genau übernehmen: keine Umformulierung, keine Ersetzung typografischer Zeichen, keine zusätzlichen Leerzeilen. Schreib die Blöcke mit dem Edit-Werkzeug, Aus-Block als `old_string`, wird-Block als `new_string`. +- Steht ein Aus-Block nicht genau einmal in seiner Datei, oder widerspricht etwas im Plan einem anderen Teil, hältst du an und meldest es. Nichts still zurechtbiegen. +- Kein Lauf gegen ein echtes Projekt; der Treiber baut alles selbst unter `/tmp` in WSL. + +## Schritt 1 — Tests zuerst: Treiber + +Wende alle Blöcke dieses Abschnitts auf `mutanten_lauf_treiber.py` an. + +### `mutanten_lauf_treiber.py`, Stelle 1 von 4 + +Aus: + +```python +#!/usr/bin/env python3 +"""Treiber fuer mutanten_lauf.py: 35 Angriffsfaelle auf einer Wegwerfkopie. + +``` + +wird: + +```python +#!/usr/bin/env python3 +"""Treiber fuer mutanten_lauf.py: 40 Angriffsfaelle auf einer Wegwerfkopie. + +``` + +### `mutanten_lauf_treiber.py`, Stelle 2 von 4 + +Aus: + +```python +dafuer ein eigenes TMPDIR (tempfile respektiert die Umgebungsvariable). +""" +``` + +wird: + +```python +dafuer ein eigenes TMPDIR (tempfile respektiert die Umgebungsvariable). + +Faelle 36 bis 40 (2026-10-06) pruefen die pytest-Konfiguration: Die Wurzel +liegt dafuer als k/ in einem eigenen Rahmen-Ordner, darueber wahlweise eine +fremde pyproject.toml mit addopts, die zu -p no:cacheprovider nicht passen +(wie im Windows-Temp bei der Plankritik 2 von C1), oder ein fremdes conftest.py. +Dazu ein Bedienfehler von pytest in der Grundlinie und unter einem Mutanten. +""" +``` + +### `mutanten_lauf_treiber.py`, Stelle 3 von 4 + +Aus: + +```python + +FAELLE = [ +``` + +wird: + +```python + +# --- Faelle 36 bis 40: pytest-Konfiguration --------------------------------- + +FREMDE_KONFIG = ( + "[tool.pytest.ini_options]\n" + "addopts = \"--new-first --failed-first\"\n" +) + +FREMDES_CONFTEST = "raise RuntimeError(\"fremdes conftest oberhalb der Wurzel geladen\")\n" + + +def baue_rahmen(eigene_konfig, fremde_konfig, fremdes_conftest): + rahmen = Path(tempfile.mkdtemp(prefix="mutanten_lauf_treiber_rahmen_")) + if fremde_konfig: + (rahmen / "pyproject.toml").write_text(FREMDE_KONFIG, encoding="utf-8") + if fremdes_conftest: + (rahmen / "conftest.py").write_text(FREMDES_CONFTEST, encoding="utf-8") + wurzel = rahmen / "k" + dateien = baue_projekt(wurzel) + if not eigene_konfig: + (wurzel / "pyproject.toml").unlink() + dateien = [p for p in dateien if p != wurzel / "pyproject.toml"] + return rahmen, wurzel, dateien + + +def fall_36(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=False, fremde_konfig=True, fremdes_conftest=False) + try: + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + ergebnis = starte_laeufer(wurzel, toml) + ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout + melde("Fall 36", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + pruefe_unveraendert(dateien, snap, "Fall 36") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_37(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=True, fremdes_conftest=False) + try: + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + sauber = starte_laeufer(wurzel, toml) + schreibe_toml(toml, kopf() + mutant_luecke("M37", ["tests/test_faelle.py::test_wird_abgewaehlt"])) + abgewaehlt = starte_laeufer(wurzel, toml) + ausgabe = abgewaehlt.stdout + abgewaehlt.stderr + ok = ( + sauber.returncode == 0 + and "D1 OK" in sauber.stdout + and "D2 OK" in sauber.stdout + and abgewaehlt.returncode == 2 + and "ABGEWAEHLT" in ausgabe + ) + melde("Fall 37", ok, "exit={0}/{1}\n{2}{3}\n{4}".format( + sauber.returncode, abgewaehlt.returncode, sauber.stdout, sauber.stderr, ausgabe)) + pruefe_unveraendert(dateien, snap, "Fall 37") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_38(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False) + try: + (wurzel / "pyproject.toml").write_text( + "[tool.pytest.ini_options]\n" + "addopts = \"--gibt-es-nicht\"\n", + encoding="utf-8", + ) + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + ergebnis = starte_laeufer(wurzel, toml) + ausgabe = ergebnis.stdout + ergebnis.stderr + ok = ( + ergebnis.returncode == 2 + and "pytest-Bedienfehler (Rueckgabewert 4)" in ausgabe + and "--gibt-es-nicht" in ausgabe + and "ist FEHLT" not in ausgabe + ) + melde("Fall 38", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe)) + pruefe_unveraendert(dateien, snap, "Fall 38") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_39(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=False, fremde_konfig=False, fremdes_conftest=True) + try: + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + ergebnis = starte_laeufer(wurzel, toml) + ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout + melde("Fall 39", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + pruefe_unveraendert(dateien, snap, "Fall 39") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_40(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False) + try: + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, kopf() + ( + "[[mutant]]\n" + "id = \"M40\"\n" + "datei = \"pyproject.toml\"\n" + "aus = '''\n" + "addopts = '-m \"not integration\"'\n" + "'''\n" + "wird = '''\n" + "addopts = \"--gibt-es-nicht\"\n" + "'''\n" + "rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n" + )) + ergebnis = starte_laeufer(wurzel, toml) + ok = ( + ergebnis.returncode == 1 + and "M40 UNKLAR" in ergebnis.stdout + and "pytest-Bedienfehler (Rueckgabewert 4)" in ergebnis.stdout + ) + melde("Fall 40", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + pruefe_unveraendert(dateien, snap, "Fall 40") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +FAELLE = [ +``` + +### `mutanten_lauf_treiber.py`, Stelle 4 von 4 + +Aus: + +```python + 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, fall_35, +] +``` + +wird: + +```python + 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, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40, +] +``` + + +### Erwarteter Fehlschlag vor der Umsetzung + +Der neue Treiber läuft gegen den alten Läufer. Lege zuerst in der Git-Bash den Rot-Ordner an; `git show` schreibt dort die Bytes unverändert, die PowerShell würde Zeilenenden umschreiben: + +```bash +# alten Laeufer aus dem letzten Commit und den neuen Treiber in einen leeren Rot-Ordner legen +R=/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot +rm -rf "$R" && mkdir -p "$R" +git -C /d/Claude show HEAD:.vault-tools/plan-kopie/mutanten_lauf.py > "$R/mutanten_lauf.py" +cp /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py "$R/" +``` + +Dann in der PowerShell: + +```powershell +# Treiber in WSL gegen den alten Laeufer fahren +wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot\ausgabe.txt" +"exit=$LASTEXITCODE" +Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot\ausgabe.txt" -Pattern "FEHLER:|gerechnete" +``` + +Erwartet: `exit=1`, und die Treffer von `Select-String` enden genau auf diese vier Zeilen: + +``` +Fall 36 FEHLER: exit=2 +Fall 38 FEHLER: exit=2 +Fall 40 FEHLER: exit=1 +gerechnete Fehler: 3 +``` + +Weicht das ab, hältst du an und meldest die Ausgabe. + +## Schritt 2 — Umsetzung + +Wende alle Blöcke dieses Abschnitts in der Reihenfolge an. + +### `mutanten_lauf.py`, Stelle 1 von 19 + +Aus: + +```python +Geheimnisdateien (der Schluessel "ohne" in mutanten.toml). + +``` + +wird: + +```python +Geheimnisdateien (der Schluessel "ohne" in mutanten.toml). + +pytest-Konfiguration (seit 2026-10-06): Fuer jeden pytest-Aufruf bestimmt das +Skript die Konfigurationsdatei so, wie pytest 9.1.1 sie selbst suchen wuerde +(_pytest/config/findpaths.py: determine_setup, locate_config, +get_common_ancestor), sucht aber nie oberhalb der Wurzel, und gibt sie mit -c +vor. Ohne Konfiguration in der Wurzel bekommt pytest eine leere Datei aus dem +Plugin-Ordner, dazu --rootdir und --confcutdir. Grund: Unter dem Windows-Temp +fand pytest eine fremde pyproject.toml, deren addopts nicht zu +-p no:cacheprovider passten, und jeder Test der Grundlinie stand als FEHLT da +(Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Benannte Luecken: +tox.ini und setup.cfg werden zeilenweise nach ihrem Abschnitt durchsucht, nicht +mit iniconfig; ein setup.cfg mit [pytest] (bei pytest selbst ein Fehler) gilt +hier als keine Konfiguration. + +Endet pytest mit Rueckgabewert 3 (interner Fehler) oder 4 (Bedienfehler, +ExitCode in _pytest/config/__init__.py), steht das Ende seiner Ausgabe in der +Grundlinie als eigener Befund und unter einem Mutanten in der Meldung der +Tests ohne Bericht und ohne Sammelfehler. Ein Sammelfehler bleibt ROT, auch +wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht +sammeln laesst, ergibt zusaetzlich "not found" (Treiber-Fall 11). + +``` + +### `mutanten_lauf.py`, Stelle 2 von 19 + +Aus: + +```python +MUTANT_SCHLUESSEL = {"id", "datei", "aus", "wird", "rot_bei", "hinweis"} + +``` + +wird: + +```python +MUTANT_SCHLUESSEL = {"id", "datei", "aus", "wird", "rot_bei", "hinweis"} + +# Reihenfolge wie config_names in locate_config, pytest 9.1.1. +PYTEST_KONFIG_NAMEN = ( + "pytest.toml", + ".pytest.toml", + "pytest.ini", + ".pytest.ini", + "pyproject.toml", + "tox.ini", + "setup.cfg", +) +LEERE_KONFIG_NAME = "pytest.ini" +PYTEST_ABBRUCH = {3: "pytest-Abbruch mit internem Fehler", 4: "pytest-Bedienfehler"} +AUSGABE_ENDE_ZEILEN = 20 + +``` + +### `mutanten_lauf.py`, Stelle 3 von 19 + +Aus: + +```python + +def umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad): +``` + +wird: + +```python + +def _ini_hat_abschnitt(pfad, abschnitt): + """Zeilenweise Suche nach [abschnitt]. Eine nicht lesbare Datei zaehlt als + Treffer, damit pytest sie selbst liest und den Fehler selbst meldet.""" + try: + text = pfad.read_text(encoding="utf-8", errors="replace") + except OSError: + return True + kopf = "[" + abschnitt + "]" + return any(zeile.strip() == kopf for zeile in text.split("\n")) + + +def _ist_pytest_konfig(pfad): + """Nachbau von load_config_dict_from_file(pfad) is not None, pytest 9.1.1.""" + name = pfad.name + if name in ("pytest.toml", ".pytest.toml", "pytest.ini", ".pytest.ini"): + return True + if name == "tox.ini": + return _ini_hat_abschnitt(pfad, "pytest") + if name == "setup.cfg": + return _ini_hat_abschnitt(pfad, "tool:pytest") + if name == "pyproject.toml": + try: + daten = tomllib.loads(pfad.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError): + return True + werkzeug = daten.get("tool", {}) + tool_pytest = werkzeug.get("pytest", {}) if isinstance(werkzeug, dict) else {} + return isinstance(tool_pytest, dict) and bool(tool_pytest) + return False + + +def _innerhalb(pfad, wurzel): + return pfad == wurzel or wurzel in pfad.parents + + +def _konfig_suchen(startordner, wurzel): + """Nachbau von locate_config, pytest 9.1.1; die Suche endet an der Wurzel.""" + erstes_pyproject = None + for start in startordner: + for basis in (start, *start.parents): + if not _innerhalb(basis, wurzel): + break + for name in PYTEST_KONFIG_NAMEN: + pfad = basis / name + if pfad.is_file(): + if name == "pyproject.toml" and erstes_pyproject is None: + erstes_pyproject = pfad + if _ist_pytest_konfig(pfad): + return pfad + return erstes_pyproject + + +def _gemeinsamer_vorfahr(pfade, wurzel): + """Nachbau von get_common_ancestor, pytest 9.1.1, mit der Wurzel als Aufrufordner.""" + vorfahr = None + for pfad in pfade: + if not pfad.exists(): + continue + if vorfahr is None: + vorfahr = pfad + elif vorfahr in pfad.parents or pfad == vorfahr: + continue + elif pfad in vorfahr.parents: + vorfahr = pfad + else: + try: + vorfahr = Path(os.path.commonpath([str(pfad), str(vorfahr)])) + except ValueError: + pass + if vorfahr is None: + return wurzel + if vorfahr.is_file(): + return vorfahr.parent + return vorfahr + + +def pytest_konfig_bestimmen(wurzel, knoten_liste): + """Konfiguration und rootdir eines Aufrufs, wie determine_setup in pytest + 9.1.1 sie ohne -c bestimmt, aber nie oberhalb der Wurzel gesucht. + Rueckgabe (konfig, rootdir); konfig ist None, wenn die Wurzel keine hat.""" + wurzel = Path(os.path.abspath(str(wurzel))) + ordner = [] + for knoten in knoten_liste: + pfad = wurzel / knoten.split("::", 1)[0] + if pfad.exists(): + ordner.append(pfad if pfad.is_dir() else pfad.parent) + vorfahr = _gemeinsamer_vorfahr(ordner, wurzel) + if not _innerhalb(vorfahr, wurzel): + vorfahr = wurzel + konfig = _konfig_suchen([vorfahr], wurzel) + if konfig is not None: + return konfig, konfig.parent + for basis in (vorfahr, *vorfahr.parents): + if not _innerhalb(basis, wurzel): + break + if (basis / "setup.py").is_file(): + return None, basis + if ordner != [vorfahr]: + konfig = _konfig_suchen(ordner, wurzel) + if konfig is not None: + return konfig, konfig.parent + return None, wurzel + + +def pytest_konfig_argumente(wurzel, knoten_liste, leere_konfig): + """Die Konfiguration der Wurzel mit -c; sonst die leere Datei mit --rootdir + und --confcutdir. Ohne --confcutdir laege confcutdir im Ordner der leeren + Datei, und pytest laede conftest.py-Dateien oberhalb der Wurzel + (_pytest/config/__init__.py, pytest 9.1.1).""" + konfig, rootdir = pytest_konfig_bestimmen(wurzel, knoten_liste) + if konfig is not None: + return ["-c", str(konfig)] + return ["-c", str(leere_konfig), "--rootdir", str(rootdir), "--confcutdir", str(rootdir)] + + +def pytest_konfig_beschreibung(wurzel, knoten_liste): + konfig, rootdir = pytest_konfig_bestimmen(wurzel, knoten_liste) + if konfig is not None: + return "{0} (aus der Wurzel)".format(konfig) + return "keine in der Wurzel, leere Datei; rootdir {0}".format(rootdir) + + +def ausgabe_ende(stdout_bytes, stderr_bytes): + """Die letzten nicht leeren Zeilen von stdout und stderr, je hoechstens 300 Zeichen.""" + text = ( + (stdout_bytes or b"").decode("utf-8", errors="replace") + + "\n" + + (stderr_bytes or b"").decode("utf-8", errors="replace") + ) + zeilen = [zeile.rstrip()[:300] for zeile in text.split("\n") if zeile.strip()] + return zeilen[-AUSGABE_ENDE_ZEILEN:] + + +def umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad): +``` + +### `mutanten_lauf.py`, Stelle 4 von 19 + +Aus: + +```python + env = umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad) + cmd = [python_exe, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-p", PLUGIN_NAME] + list(knoten_liste) + +``` + +wird: + +```python + env = umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad) + konfig_argumente = pytest_konfig_argumente(wurzel, knoten_liste, Path(plugin_dir) / LEERE_KONFIG_NAME) + cmd = ( + [python_exe, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-p", PLUGIN_NAME] + + konfig_argumente + + list(knoten_liste) + ) + +``` + +### `mutanten_lauf.py`, Stelle 5 von 19 + +Aus: + +```python + absturz = None + proc = None +``` + +wird: + +```python + absturz = None + abbruch = None + ausgabe = (b"", b"") + proc = None +``` + +### `mutanten_lauf.py`, Stelle 6 von 19 + +Aus: + +```python + try: + proc.communicate(timeout=zeitlimit) + except subprocess.TimeoutExpired: +``` + +wird: + +```python + try: + ausgabe = proc.communicate(timeout=zeitlimit) + except subprocess.TimeoutExpired: +``` + +### `mutanten_lauf.py`, Stelle 7 von 19 + +Aus: + +```python + pass + proc.communicate() + except BaseException: +``` + +wird: + +```python + pass + ausgabe = proc.communicate() + except BaseException: +``` + +### `mutanten_lauf.py`, Stelle 8 von 19 + +Aus: + +```python + raise + +``` + +wird: + +```python + raise + if not zeit_ueberschritten and proc.returncode in PYTEST_ABBRUCH: + abbruch = "{0} (Rueckgabewert {1}) mit {2}; Ende der Ausgabe:\n {3}".format( + PYTEST_ABBRUCH[proc.returncode], proc.returncode, python_exe, + "\n ".join(ausgabe_ende(*ausgabe)) or "(keine Ausgabe)", + ) + +``` + +### `mutanten_lauf.py`, Stelle 9 von 19 + +Aus: + +```python + + return zeit_ueberschritten, meldungen, absturz + finally: +``` + +wird: + +```python + + return zeit_ueberschritten, meldungen, absturz, abbruch + finally: +``` + +### `mutanten_lauf.py`, Stelle 10 von 19 + +Aus: + +```python + 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: +``` + +wird: + +```python + absturzmeldungen = [] + abbrueche = {} + for python_exe, knoten_liste in gruppen.items(): + zeit, meldungen, absturz, abbruch = pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimit) + if zeit: +``` + +### `mutanten_lauf.py`, Stelle 11 von 19 + +Aus: + +```python + absturzmeldungen.append(absturz) + for m in meldungen: +``` + +wird: + +```python + absturzmeldungen.append(absturz) + if abbruch: + abbrueche[python_exe] = abbruch + for m in meldungen: +``` + +### `mutanten_lauf.py`, Stelle 12 von 19 + +Aus: + +```python + deselected.add(m.get("knoten")) + return reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen + +``` + +wird: + +```python + deselected.add(m.get("knoten")) + return reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen, abbrueche + +``` + +### `mutanten_lauf.py`, Stelle 13 von 19 + +Aus: + +```python + 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, +``` + +wird: + +```python + gruppen = gruppiere_nach_interpreter(alle_tests, kontext["python"], kontext["python_je_datei"]) + reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen, abbrueche = gruppen_ausfuehren( + gruppen, kontext["wurzel"], home_dir, plugin_dir, zeitlimit, +``` + +### `mutanten_lauf.py`, Stelle 14 von 19 + +Aus: + +```python + befunde = list(absturzmeldungen) + ergebnis_je_knoten = {} +``` + +wird: + +```python + befunde = list(absturzmeldungen) + abgebrochen = set() + for python_exe, knoten_liste in gruppen.items(): + if python_exe in abbrueche: + befunde.append("Grundlinie: " + abbrueche[python_exe]) + abgebrochen.update(knoten_liste) + ergebnis_je_knoten = {} +``` + +### `mutanten_lauf.py`, Stelle 15 von 19 + +Aus: + +```python + status, meldung = ergebnis_bestimmen(knoten, reports, deselected) + ergebnis_je_knoten[knoten] = {"ergebnis": status, "meldung": meldung} +``` + +wird: + +```python + status, meldung = ergebnis_bestimmen(knoten, reports, deselected) + if status == "FEHLT" and knoten in abgebrochen: + ergebnis_je_knoten[knoten] = {"ergebnis": status, "meldung": "pytest abgebrochen, siehe Befunde"} + continue + ergebnis_je_knoten[knoten] = {"ergebnis": status, "meldung": meldung} +``` + +### `mutanten_lauf.py`, Stelle 16 von 19 + +Aus: + +```python + 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, +``` + +wird: + +```python + gruppen = gruppiere_nach_interpreter(knoten_liste, standard_python, python_je_datei) + reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen, abbrueche = gruppen_ausfuehren( + gruppen, wurzel, home_dir, plugin_dir, zeitlimit, +``` + +### `mutanten_lauf.py`, Stelle 17 von 19 + +Aus: + +```python + unklar = True + if ergebnis in ("GRUEN", "UEBERSPRUNGEN", "ABGEWAEHLT"): +``` + +wird: + +```python + unklar = True + abbruch = abbrueche.get(python_je_datei.get(knoten.split("::", 1)[0], standard_python)) + if abbruch: + meldung = abbruch + if ergebnis in ("GRUEN", "UEBERSPRUNGEN", "ABGEWAEHLT"): +``` + +### `mutanten_lauf.py`, Stelle 18 von 19 + +Aus: + +```python + print("python {0}: {1}".format(interpreter, python_version_ermitteln(interpreter))) + +``` + +wird: + +```python + print("python {0}: {1}".format(interpreter, python_version_ermitteln(interpreter))) + alle_knoten = [] + for mutant in ausgewaehlte_mutanten: + for _, knoten in mutant["rot_bei"]: + if knoten not in alle_knoten: + alle_knoten.append(knoten) + print("pytest-konfiguration (Grundlinie): {0}".format(pytest_konfig_beschreibung(kontext["wurzel"], alle_knoten))) + +``` + +### `mutanten_lauf.py`, Stelle 19 von 19 + +Aus: + +```python + (plugin_dir / (PLUGIN_NAME + ".py")).write_text(PLUGIN_QUELLTEXT, encoding="utf-8") + +``` + +wird: + +```python + (plugin_dir / (PLUGIN_NAME + ".py")).write_text(PLUGIN_QUELLTEXT, encoding="utf-8") + (plugin_dir / LEERE_KONFIG_NAME).write_text("", encoding="utf-8") + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 1 von 6 + +Aus: + +```markdown +created: 2026-09-27 +updated: 2026-09-27 +--- +``` + +wird: + +```markdown +created: 2026-09-27 +updated: 2026-10-06 +--- +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 2 von 6 + +Aus: + +```markdown +| 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. | + +``` + +wird: + +```markdown +| 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. | +| Welche pytest-Konfiguration gilt? | Quelle: pytest 9.1.1, `_pytest/config/findpaths.py` (`determine_setup`, `locate_config`, `get_common_ancestor`, `load_config_dict_from_file`; SHA-256 `535d149e6f3962e4…`, in den Venvs `trading` und `mattermost` gleich) und `_pytest/config/__init__.py` (Vorgabe von `confcutdir`, `ExitCode`), nachgelesen am 2026-10-06. Projektvorgabe seit 2026-10-06: gesucht wird wie pytest, aber nie oberhalb der Wurzel (Abschnitt 3, „Konfiguration“). | + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 3 von 6 + +Aus: + +```markdown + +**pytest-Aufruf:** ` -m pytest -q -p no:cacheprovider -p _mutanten_lauf_plugin ` 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`: + +``` + +wird: + +```markdown + +**pytest-Aufruf:** ` -m pytest -q -p no:cacheprovider -p _mutanten_lauf_plugin ` 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`: + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 4 von 6 + +Aus: + +```markdown +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`. + +``` + +wird: + +```markdown +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`. + +**Konfiguration (seit 2026-10-06):** Ohne Vorgabe sucht pytest seine Konfiguration vom gemeinsamen Vorfahren der Testpfade aufwärts bis zur Wurzel des Dateisystems. Liegt die Kopie unter dem Windows-Temp, fand pytest dort eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"`; die Optionen brauchen den Cache, den `-p no:cacheprovider` abschaltet, pytest brach mit Rückgabewert 4 ab, und der Läufer meldete jeden Test der Grundlinie als `FEHLT` (Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Seitdem bestimmt der Läufer je Aufruf, was pytest selbst wählen würde, sucht aber nie oberhalb der Wurzel: + +- Reihenfolge der Namen und Bedingungen wie in pytest 9.1.1: `pytest.toml`, `.pytest.toml`, `pytest.ini`, `.pytest.ini` immer; `pyproject.toml` mit `[tool.pytest]` oder `[tool.pytest.ini_options]`; `tox.ini` mit `[pytest]`; `setup.cfg` mit `[tool:pytest]`; ohne Treffer die erste `pyproject.toml` überhaupt. Erst vom gemeinsamen Vorfahren aus, ohne Treffer und ohne `setup.py` dazwischen noch einmal von jedem Testordner aus. +- Gefunden: `-c `. pytest setzt `rootdir` und `confcutdir` dann selbst auf ihren Ordner, wie bei der eigenen Suche. +- Nicht gefunden: `-c --rootdir --confcutdir `, `r` ist der Ordner mit `setup.py` oder sonst die Wurzel. Ohne `--confcutdir` läge `confcutdir` im Plugin-Ordner, und pytest lüde `conftest.py`-Dateien oberhalb der Wurzel. +- Die Kopfzeile `pytest-konfiguration (Grundlinie): …` nennt, was für die Grundlinie gilt. +- Benannte Lücken: `tox.ini` und `setup.cfg` werden zeilenweise nach ihrem Abschnitt durchsucht, nicht mit `iniconfig`; eine nicht lesbare oder kaputte Datei gilt als Treffer, damit pytest den Fehler selbst meldet; ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration. + +**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit ; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler diesen Text als Meldung; sie machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich „not found“ (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte). + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 5 von 6 + +Aus: + +```markdown + +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. + +``` + +wird: + +```markdown + +Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testdatei, `pyproject.toml` mit `[tool.pytest.ini_options]`; für die Fälle 36 bis 40 je ein eigenes als Unterordner `k/` eines Rahmen-Ordners), 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. + +``` + +### `mutanten_lauf_ENTWURF.md`, Stelle 6 von 6 + +Aus: + +```markdown +| 35 | ein Mutant hängt, der Läufer bekommt nach drei Sekunden SIGTERM (eigenes `TMPDIR`), seit 2026-09-28 | Zeile `ABBRUCH: SIGTERM`, kein Prozess mit `HOME` im Fall-Tmpdir, keine `mutanten_lauf_*`-Reste, Zieldatei byte-gleich | + +``` + +wird: + +```markdown +| 35 | ein Mutant hängt, der Läufer bekommt nach drei Sekunden SIGTERM (eigenes `TMPDIR`), seit 2026-09-28 | Zeile `ABBRUCH: SIGTERM`, kein Prozess mit `HOME` im Fall-Tmpdir, keine `mutanten_lauf_*`-Reste, Zieldatei byte-gleich | +| 36 | Wurzel ohne eigene Konfiguration, darüber eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"` (der Fall der Plankritik 2 von C1), seit 2026-10-06 | Exit 0, zweimal `OK` | +| 37 | Wurzel mit eigener `pyproject.toml` (Marker `integration` abgewählt), darüber dieselbe fremde | sauberer Fall Exit 0; Test mit Marker in der Grundlinie Exit 2 und `ABGEWAEHLT`, die eigene Konfiguration gilt also | +| 38 | eigene Konfiguration mit `addopts = "--gibt-es-nicht"` | Exit 2, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` aus der pytest-Ausgabe, keine Zeile `ist FEHLT` | +| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` | +| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1, `UNKLAR`, Meldung mit `pytest-Bedienfehler (Rueckgabewert 4)` | + +``` + +### `LIESMICH.md`, Stelle 1 von 2 + +Aus: + +```markdown +created: 2026-09-17 +updated: 2026-09-27 +--- +``` + +wird: + +```markdown +created: 2026-09-17 +updated: 2026-10-06 +--- +``` + +### `LIESMICH.md`, Stelle 2 von 2 + +Aus: + +```markdown + +8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel --mutanten /mutanten.toml [--nur ID,…] [--nur-muster] [--bericht ] [--zeitlimit ]`, 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. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`). + **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** 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 in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes. +``` + +wird: + +```markdown + +8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel --mutanten /mutanten.toml [--nur ID,…] [--nur-muster] [--bericht ] [--zeitlimit ]`, 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`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). 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. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`). + **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** 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 in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes. +``` + + +## Schritt 3 — Verifikation + +```bash +# Kompilieren, nur ASCII in den Python-Dateien, keine CR in allen vier Dateien +cd /d/Claude/.vault-tools/plan-kopie +python -m py_compile mutanten_lauf.py mutanten_lauf_treiber.py && echo kompiliert +LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf.py mutanten_lauf_treiber.py +for f in mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_ENTWURF.md LIESMICH.md; do printf '%s %s\n' "$f" "$(tr -cd '\r' < "$f" | wc -c)"; done +``` + +Erwartet: `kompiliert`, je Python-Datei die Zahl `0`, je Datei die Zahl `0`. + +```powershell +# Treiber gegen den neuen Laeufer in WSL +wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot\ausgabe-neu.txt" +"exit=$LASTEXITCODE" +Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot\ausgabe-neu.txt" -Pattern "FEHLER:|gerechnete" +``` + +Erwartet: `exit=0` und als einziger Treffer eine Zeile, die auf `gerechnete Fehler: 0` endet. + +Danach `C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot` löschen. + +## Bericht + +Melde: je Zieldatei die Zahl der angewendeten Aus/wird-Paare, die Ausgaben aus Schritt 1 und Schritt 3 im Wortlaut, und jede Abweichung vom Plan oder getroffene Annahme. „Keine Abweichungen“ nur, wenn es keine gab. + +## Fertig heißt (Koordinator) + +1. Die vier Zieldateien sind byte-gleich mit der Kopie, auf die dieser Plan per `plan_materialisiere.py` angewendet wurde (`cmp`), und sonst ist unter `.vault-tools/` nichts geändert (`git -C D:/Claude status --porcelain -- .vault-tools`). +2. Treiber in WSL: Exit 0, `gerechnete Fehler: 0`, selbst gefahren. +3. Rot-Nachweis: alter Läufer → genau 36, 38, 40; die drei Varianten → 15/37/38/40, 39, 36 (vom Koordinator am 2026-10-06 gefahren, Rohdaten `02-*`). +4. Gegenprobe an allen sechs `mutanten.toml` im Vault (Headscale Paket A und B, Mattermost Phase 1, Trading Plattform Paket WH, After Install D-40 und C1), je Kopie aus `git archive HEAD` unter `/tmp`, alter gegen neuen Läufer: gleicher Exit-Code, gleiches Ergebnis je Test der Grundlinie, gleicher Status je Mutant. Dazu C1 unter dem Windows-Temp: alter Läufer Exit 2 mit `FEHLT`, neuer wie unter `/tmp`. +5. Ein `pruefer` hat Läufer und Treiber gegen diesen Plan und die pytest-Quelle gelesen; kein offener blockierender oder wichtiger Befund. + +## Lauf mit echten Daten + +Punkt 4 der Liste oben ist der Lauf mit echten Daten. **Auffallen müsste:** ein Plan, dessen Grundlinie mit dem neuen Läufer anders ausgeht als mit dem alten (dann bildet die Suche pytest nicht nach), eine Kopfzeile `pytest-konfiguration (Grundlinie)`, die bei Headscale oder Mattermost nicht `pytest.ini` und bei der Trading Plattform nicht `pyproject.toml` nennt, und bei After Install nicht „keine in der Wurzel“. + +## Risiken + +1. **Suche weicht von pytest ab.** `tox.ini` und `setup.cfg` werden zeilenweise gelesen, nicht mit `iniconfig`. Merkmal: Ein Projekt mit Konfiguration in `tox.ini` läuft ohne seine Einstellungen. Heute hat keines der sechs Projekte eine solche Datei. +2. **Neue pytest-Fassung.** Ändert eine spätere Fassung die Suchregeln, bildet der Läufer die alte nach. Merkmal: Die Kopfzeile nennt eine andere Datei als `pytest --co -q` in der Wurzel ohne `-c`. +3. **Befundtext statt `FEHLT`.** Wer die Ausgabe der Grundlinie maschinell auf `ist FEHLT` durchsucht, sieht einen Abbruch von pytest jetzt als `pytest-Bedienfehler`. `plankritik.js` wertet nur den Exit-Code aus; der bleibt 2. diff --git a/.vault-tools/plan-kopie/mutanten_lauf.py b/.vault-tools/plan-kopie/mutanten_lauf.py index 254703f..b327562 100644 --- a/.vault-tools/plan-kopie/mutanten_lauf.py +++ b/.vault-tools/plan-kopie/mutanten_lauf.py @@ -24,6 +24,37 @@ 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). +pytest-Konfiguration (seit 2026-10-06): Fuer jeden pytest-Aufruf bestimmt das +Skript die Konfigurationsdatei so, wie pytest 9.1.1 sie selbst suchen wuerde +(_pytest/config/findpaths.py: determine_setup, locate_config, +get_common_ancestor), sucht aber nie oberhalb der Wurzel, und gibt sie mit -c +vor. Ohne Konfiguration in der Wurzel bekommt pytest eine leere Datei aus dem +Plugin-Ordner, dazu --rootdir und --confcutdir. Grund: Unter dem Windows-Temp +fand pytest eine fremde pyproject.toml, deren addopts nicht zu +-p no:cacheprovider passten, und jeder Test der Grundlinie stand als FEHLT da +(Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Die Wurzel wird +vorher mit realpath aufgeloest: pytest rechnet vom Arbeitsordner aus, den der +Kernel ohne Verknuepfungen meldet; ueber einen Symlink angegeben, passten die +Knotennamen sonst nicht (Pruefung vom 2026-10-06). Benannte Luecken: tox.ini +und setup.cfg werden zeilenweise nach ihrem Abschnitt durchsucht wie in +iniconfig 2.3.0 (_parse.py, _parseline), eine Datei, die iniconfig nicht lesen +kann, uebergeht der Laeufer, pytest bricht dort mit 4 ab; ein setup.cfg mit +[pytest] (bei pytest selbst ein Fehler) gilt hier als keine Konfiguration; ohne +Konfiguration in der Wurzel ist config.inipath die leere Datei, ini-Werte vom +Typ paths aus -o oder PYTEST_ADDOPTS gelten dann relativ zum Plugin-Ordner; +iniconfig trennt Zeilen mit splitlines, der Laeufer auch. Liegt rootdir unter +der Wurzel (Konfiguration oder setup.py in einem Unterordner der Tests), +bildet pytest die Knotennamen relativ dazu, und rot_bei passt nicht mehr: Die +Grundlinie meldet FEHLT, die Kopfzeile nennt die Datei. + +Endet pytest mit Rueckgabewert 3 (interner Fehler) oder 4 (Bedienfehler, +ExitCode in _pytest/config/__init__.py), steht das Ende seiner Ausgabe in der +Grundlinie als eigener Befund je Interpreter und unter einem Mutanten, als +eine Zeile mit der Ursache vorn, in der Meldung der Tests ohne Bericht und +ohne Sammelfehler. Ein Sammelfehler bleibt ROT, auch wenn pytest dabei mit 4 +endet: Ein Knoten in einer Datei, die sich nicht sammeln laesst, ergibt +zusaetzlich "found no collectors for" (Treiber-Fall 11). + Spezifikation: .vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md Entscheidungen, die der Entwurf offen liess (siehe Bericht des Executers): @@ -119,6 +150,20 @@ def pytest_deselected(items): TOP_LEVEL_SCHLUESSEL = {"format", "python", "ohne", "kuerzel", "python_je_datei", "mutant"} MUTANT_SCHLUESSEL = {"id", "datei", "aus", "wird", "rot_bei", "hinweis"} +# Reihenfolge wie config_names in locate_config, pytest 9.1.1. +PYTEST_KONFIG_NAMEN = ( + "pytest.toml", + ".pytest.toml", + "pytest.ini", + ".pytest.ini", + "pyproject.toml", + "tox.ini", + "setup.cfg", +) +LEERE_KONFIG_NAME = "pytest.ini" +PYTEST_ABBRUCH = {3: "pytest-Abbruch mit internem Fehler", 4: "pytest-Bedienfehler"} +AUSGABE_ENDE_ZEILEN = 20 + class RuecksetzenGescheitert(Exception): def __init__(self, mutant_id, datei): @@ -427,6 +472,170 @@ def gruppiere_nach_interpreter(knoten_liste, standard_python, python_je_datei): return gruppen +def _ini_hat_abschnitt(pfad, abschnitt): + """Suche nach der Abschnittszeile wie _parseline in iniconfig 2.3.0: "[" in + Spalte 0, Kommentare nach # und ; entfernt, dann "]" am Ende. Eine nicht + lesbare Datei zaehlt als Treffer, damit pytest den Fehler selbst meldet.""" + try: + text = pfad.read_text(encoding="utf-8", errors="replace") + except OSError: + return True + for zeile in text.splitlines(): + zeile = zeile.rstrip() + if not zeile.startswith("["): + continue + for zeichen in "#;": + zeile = zeile.split(zeichen)[0].rstrip() + if zeile.endswith("]") and zeile[1:-1] == abschnitt: + return True + return False + + +def _ist_pytest_konfig(pfad): + """Nachbau von load_config_dict_from_file(pfad) is not None, pytest 9.1.1.""" + name = pfad.name + if name in ("pytest.toml", ".pytest.toml", "pytest.ini", ".pytest.ini"): + return True + if name == "tox.ini": + return _ini_hat_abschnitt(pfad, "pytest") + if name == "setup.cfg": + return _ini_hat_abschnitt(pfad, "tool:pytest") + if name == "pyproject.toml": + try: + daten = tomllib.loads(pfad.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError): + return True + werkzeug = daten.get("tool", {}) + tool_pytest = werkzeug.get("pytest", {}) if isinstance(werkzeug, dict) else {} + return isinstance(tool_pytest, dict) and bool(tool_pytest) + return False + + +def _innerhalb(pfad, wurzel): + return pfad == wurzel or wurzel in pfad.parents + + +def _existiert(pfad): + """Wie safe_exists in pytest 9.1.1: ein zu langer Name ist kein Absturz.""" + try: + return pfad.exists() + except (OSError, ValueError): + return False + + +def _konfig_suchen(startordner, wurzel): + """Nachbau von locate_config, pytest 9.1.1; die Suche endet an der Wurzel.""" + erstes_pyproject = None + for start in startordner: + for basis in (start, *start.parents): + if not _innerhalb(basis, wurzel): + break + for name in PYTEST_KONFIG_NAMEN: + pfad = basis / name + if pfad.is_file(): + if name == "pyproject.toml" and erstes_pyproject is None: + erstes_pyproject = pfad + if _ist_pytest_konfig(pfad): + return pfad + return erstes_pyproject + + +def _gemeinsamer_vorfahr(pfade, wurzel): + """Nachbau von get_common_ancestor, pytest 9.1.1, mit der Wurzel als Aufrufordner.""" + vorfahr = None + for pfad in pfade: + if not _existiert(pfad): + continue + if vorfahr is None: + vorfahr = pfad + elif vorfahr in pfad.parents or pfad == vorfahr: + continue + elif pfad in vorfahr.parents: + vorfahr = pfad + else: + try: + vorfahr = Path(os.path.commonpath([str(pfad), str(vorfahr)])) + except ValueError: + pass + if vorfahr is None: + return wurzel + if vorfahr.is_file(): + return vorfahr.parent + return vorfahr + + +def pytest_konfig_bestimmen(wurzel, knoten_liste): + """Konfiguration und rootdir eines Aufrufs, wie determine_setup in pytest + 9.1.1 sie ohne -c bestimmt, aber nie oberhalb der Wurzel gesucht. + Rueckgabe (konfig, rootdir); konfig ist None, wenn die Wurzel keine hat.""" + wurzel = Path(os.path.realpath(str(wurzel))) + ordner = [] + for knoten in knoten_liste: + pfad = wurzel / knoten.split("::", 1)[0] + if _existiert(pfad): + ordner.append(pfad if pfad.is_dir() else pfad.parent) + vorfahr = _gemeinsamer_vorfahr(ordner, wurzel) + if not _innerhalb(vorfahr, wurzel): + vorfahr = wurzel + konfig = _konfig_suchen([vorfahr], wurzel) + if konfig is not None: + return konfig, konfig.parent + for basis in (vorfahr, *vorfahr.parents): + if not _innerhalb(basis, wurzel): + break + if (basis / "setup.py").is_file(): + return None, basis + if ordner != [vorfahr]: + konfig = _konfig_suchen(ordner, wurzel) + if konfig is not None: + return konfig, konfig.parent + return None, wurzel + + +def pytest_konfig_argumente(wurzel, knoten_liste, leere_konfig): + """Die Konfiguration der Wurzel mit -c; sonst die leere Datei mit --rootdir + und --confcutdir. Ohne --confcutdir laege confcutdir im Ordner der leeren + Datei, und pytest laede conftest.py-Dateien oberhalb der Wurzel + (_pytest/config/__init__.py, pytest 9.1.1).""" + konfig, rootdir = pytest_konfig_bestimmen(wurzel, knoten_liste) + if konfig is not None: + return ["-c", str(konfig)] + return ["-c", str(leere_konfig), "--rootdir", str(rootdir), "--confcutdir", str(rootdir)] + + +def pytest_konfig_beschreibung(wurzel, knoten_liste): + konfig, rootdir = pytest_konfig_bestimmen(wurzel, knoten_liste) + if konfig is not None: + return "{0} (aus der Wurzel)".format(konfig) + return "keine in der Wurzel, leere Datei; rootdir {0}".format(rootdir) + + +def ausgabe_ende(stdout_bytes, stderr_bytes): + """Die letzten nicht leeren Zeilen von stdout und stderr, je hoechstens 300 Zeichen.""" + text = ( + (stdout_bytes or b"").decode("utf-8", errors="replace") + + "\n" + + (stderr_bytes or b"").decode("utf-8", errors="replace") + ) + zeilen = [zeile.rstrip()[:300] for zeile in text.split("\n") if zeile.strip()] + return zeilen[-AUSGABE_ENDE_ZEILEN:] + + +def kernzeile(ende, rueckgabe): + """Die Zeile, die die Ursache nennt: bei 4 die erste mit "error" ausser der + Gebrauchszeile von argparse, bei 3 die letzte mit INTERNALERROR> (danach + folgt noch die Summenzeile, Treiber-Fall 46), ohne Treffer die letzte.""" + if rueckgabe == 4: + for zeile in ende: + if "error" in zeile.lower() and not zeile.startswith("ERROR: usage:"): + return zeile.strip() + if rueckgabe == 3: + interne = [zeile for zeile in ende if zeile.startswith("INTERNALERROR>")] + if interne: + return interne[-1].strip() + return ende[-1].strip() if ende else "(keine Ausgabe)" + + def umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad): """Abdichtung nach Entwurf, Abschnitt 5. Siehe Kopfkommentar fuer die Luecken.""" env = os.environ.copy() @@ -448,10 +657,17 @@ def pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimi 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) + konfig_argumente = pytest_konfig_argumente(wurzel, knoten_liste, Path(plugin_dir) / LEERE_KONFIG_NAME) + cmd = ( + [python_exe, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-p", PLUGIN_NAME] + + konfig_argumente + + list(knoten_liste) + ) zeit_ueberschritten = False absturz = None + abbruch = None + ausgabe = (b"", b"") proc = None try: try: @@ -465,14 +681,14 @@ def pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimi if proc is not None: try: - proc.communicate(timeout=zeitlimit) + ausgabe = 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() + ausgabe = proc.communicate() except BaseException: # Auch ein SIGTERM (SigtermUnterbrechung) darf keinen pytest-Prozess # zurueck lassen: Prozessgruppe beenden, dann weiterwerfen, damit die @@ -487,6 +703,17 @@ def pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimi except Exception: pass raise + if not zeit_ueberschritten and proc.returncode in PYTEST_ABBRUCH: + ende = ausgabe_ende(*ausgabe) + abbruch = { + "lang": "{0} (Rueckgabewert {1}) mit {2}; Ende der Ausgabe:\n {3}".format( + PYTEST_ABBRUCH[proc.returncode], proc.returncode, python_exe, + "\n ".join(ende) or "(keine Ausgabe)", + ), + "kurz": "{0} (Rueckgabewert {1}): {2}".format( + PYTEST_ABBRUCH[proc.returncode], proc.returncode, kernzeile(ende, proc.returncode), + ), + } meldungen = [] try: @@ -502,7 +729,7 @@ def pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimi except OSError: pass - return zeit_ueberschritten, meldungen, absturz + return zeit_ueberschritten, meldungen, absturz, abbruch finally: # Muss auch bei einem SIGTERM mitten im Lauf greifen (N4): sonst bleibt die # Ergebnisdatei liegen, obwohl der pytest-Prozess schon beendet ist. @@ -518,12 +745,15 @@ def gruppen_ausfuehren(gruppen, wurzel, home_dir, plugin_dir, zeitlimit): sammelfehler = {} zeit_ueberschritten = False absturzmeldungen = [] + abbrueche = {} for python_exe, knoten_liste in gruppen.items(): - zeit, meldungen, absturz = pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimit) + zeit, meldungen, absturz, abbruch = pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimit) if zeit: zeit_ueberschritten = True if absturz: absturzmeldungen.append(absturz) + if abbruch: + abbrueche[python_exe] = abbruch for m in meldungen: art = m.get("art") if art == "test": @@ -537,7 +767,7 @@ def gruppen_ausfuehren(gruppen, wurzel, home_dir, plugin_dir, zeitlimit): sammelfehler[m.get("knoten")] = m.get("meldung", "") elif art == "abgewaehlt": deselected.add(m.get("knoten")) - return reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen + return reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen, abbrueche def ergebnis_bestimmen(knoten, reports, deselected): @@ -578,17 +808,25 @@ def baseline_pruefen(kontext, ausgewaehlte_mutanten, home_dir, plugin_dir, zeitl alle_tests.append(knoten) gruppen = gruppiere_nach_interpreter(alle_tests, kontext["python"], kontext["python_je_datei"]) - reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen = gruppen_ausfuehren( + reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen, abbrueche = gruppen_ausfuehren( gruppen, kontext["wurzel"], home_dir, plugin_dir, zeitlimit, ) befunde = list(absturzmeldungen) + abgebrochen = set() + for python_exe, knoten_liste in gruppen.items(): + if python_exe in abbrueche: + befunde.append("Grundlinie: " + abbrueche[python_exe]["lang"]) + abgebrochen.update(knoten_liste) 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) + if status == "FEHLT" and knoten in abgebrochen: + ergebnis_je_knoten[knoten] = {"ergebnis": status, "meldung": "pytest abgebrochen, siehe Befunde"} + continue ergebnis_je_knoten[knoten] = {"ergebnis": status, "meldung": meldung} if status != "GRUEN": zusatz = " ({0})".format(meldung) if meldung else "" @@ -615,7 +853,7 @@ def mutant_anwenden_und_pruefen(mutant, treffer_start, aus_zeilen, wird_zeilen, 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( + reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen, abbrueche = gruppen_ausfuehren( gruppen, wurzel, home_dir, plugin_dir, zeitlimit, ) @@ -630,6 +868,7 @@ def mutant_anwenden_und_pruefen(mutant, treffer_start, aus_zeilen, wird_zeilen, unklar = True for eintrag, knoten in mutant["rot_bei"]: ergebnis, meldung = ergebnis_bestimmen(knoten, reports, deselected) + abbruch = None if ergebnis == "FEHLT": sammel_meldung = sammelfehler_fuer(knoten, sammelfehler) if sammel_meldung is not None: @@ -637,9 +876,15 @@ def mutant_anwenden_und_pruefen(mutant, treffer_start, aus_zeilen, wird_zeilen, meldung = "Sammelfehler: " + sammel_meldung else: unklar = True + abbruch = abbrueche.get(python_je_datei.get(knoten.split("::", 1)[0], standard_python)) + if abbruch: + meldung = abbruch["kurz"] if ergebnis in ("GRUEN", "UEBERSPRUNGEN", "ABGEWAEHLT"): luecke = True - test_ergebnisse.append({"eintrag": eintrag, "knoten": knoten, "ergebnis": ergebnis, "meldung": meldung}) + eintrag_bericht = {"eintrag": eintrag, "knoten": knoten, "ergebnis": ergebnis, "meldung": meldung} + if ergebnis == "FEHLT" and abbruch: + eintrag_bericht["ausgabe"] = abbruch["lang"] + test_ergebnisse.append(eintrag_bericht) if unklar: status = "UNKLAR" @@ -692,6 +937,15 @@ def kopfzeilen_ausgeben(kontext, ausgewaehlte_mutanten): interpreter_menge.update(kontext["python_je_datei"].values()) for interpreter in sorted(interpreter_menge): print("python {0}: {1}".format(interpreter, python_version_ermitteln(interpreter))) + alle_knoten = [] + for mutant in ausgewaehlte_mutanten: + for _, knoten in mutant["rot_bei"]: + if knoten not in alle_knoten: + alle_knoten.append(knoten) + gruppen = gruppiere_nach_interpreter(alle_knoten, kontext["python"], kontext["python_je_datei"]) + for interpreter, knoten_liste in gruppen.items(): + print("pytest-konfiguration (Grundlinie, {0}): {1}".format( + interpreter, pytest_konfig_beschreibung(kontext["wurzel"], knoten_liste))) def zusammenfassung_rechnen(mutant_liste): @@ -788,6 +1042,7 @@ def ablauf(args, bericht): aktueller_mutant_id = None try: (plugin_dir / (PLUGIN_NAME + ".py")).write_text(PLUGIN_QUELLTEXT, encoding="utf-8") + (plugin_dir / LEERE_KONFIG_NAME).write_text("", encoding="utf-8") baseline_befunde, baseline_ergebnisse = baseline_pruefen( kontext, ausgewaehlte_mutanten, home_dir, plugin_dir, args.zeitlimit, diff --git a/.vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md b/.vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md index 3580152..c4b26f9 100644 --- a/.vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md +++ b/.vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md @@ -5,7 +5,7 @@ title: "Entwurf: mutanten_lauf.py — allgemeiner Mutantenläufer aus der Planta 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 +updated: 2026-10-06 --- # Entwurf: `mutanten_lauf.py` @@ -26,6 +26,7 @@ Das Werkzeug ist ein Fangnetz, kein Nachweis. Es sagt, ob die genannten Tests un | 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. | +| Welche pytest-Konfiguration gilt? | Quelle: pytest 9.1.1, `_pytest/config/findpaths.py` (`determine_setup`, `locate_config`, `get_common_ancestor`, `load_config_dict_from_file`; SHA-256 `535d149e6f3962e4…`, in den Venvs `trading` und `mattermost` gleich) und `_pytest/config/__init__.py` (Vorgabe von `confcutdir`, `ExitCode`), nachgelesen am 2026-10-06; die Abschnittszeile in `tox.ini` und `setup.cfg` aus iniconfig 2.3.0 (`_parse.py`, `_parseline`) im selben Venv. Projektvorgabe seit 2026-10-06: gesucht wird wie pytest, aber nie oberhalb der Wurzel (Abschnitt 3, „Konfiguration“). | ## 2 Dateiformat `mutanten.toml`, Format 1 @@ -96,14 +97,14 @@ python mutanten_lauf.py --wurzel --mutanten [--nur ID,ID 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. +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. Bricht pytest für einen Interpreter ab (Rückgabewert 3 oder 4), steht statt der Zeilen je Test ein Befund für diesen Interpreter (unten, „Abbruch von pytest“). 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:** ` -m pytest -q -p no:cacheprovider -p _mutanten_lauf_plugin ` 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-Aufruf:** ` -m pytest -q -p no:cacheprovider -p _mutanten_lauf_plugin ` 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}` @@ -111,7 +112,19 @@ python mutanten_lauf.py --wurzel --mutanten [--nur ID,ID 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. +**Konfiguration (seit 2026-10-06):** Ohne Vorgabe sucht pytest seine Konfiguration vom gemeinsamen Vorfahren der Testpfade aufwärts bis zur Wurzel des Dateisystems. Liegt die Kopie unter dem Windows-Temp, fand pytest dort eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"`; die Optionen brauchen den Cache, den `-p no:cacheprovider` abschaltet, pytest brach mit Rückgabewert 4 ab, und der Läufer meldete jeden Test der Grundlinie als `FEHLT` (Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Seitdem bestimmt der Läufer je Aufruf, was pytest selbst wählen würde, sucht aber nie oberhalb der Wurzel: + +- Die Wurzel wird zuerst mit `realpath` aufgelöst. pytest rechnet vom Arbeitsordner aus, den der Kernel ohne Verknüpfungen meldet; über einen Symlink angegeben, wurden die Knotennamen sonst zu `::test_…` (Prüfung vom 2026-10-06, Treiber-Fall 41). +- Reihenfolge der Namen und Bedingungen wie in pytest 9.1.1: `pytest.toml`, `.pytest.toml`, `pytest.ini`, `.pytest.ini` immer; `pyproject.toml` mit `[tool.pytest]` oder `[tool.pytest.ini_options]`; `tox.ini` mit `[pytest]`; `setup.cfg` mit `[tool:pytest]`; ohne Treffer die erste `pyproject.toml` überhaupt. Erst vom gemeinsamen Vorfahren aus, ohne Treffer und ohne `setup.py` dazwischen noch einmal von jedem Testordner aus. Die Abschnittszeile in `tox.ini` und `setup.cfg` erkennt der Läufer wie `_parseline` in iniconfig 2.3.0: `[` in Spalte 0, Kommentare nach `#` und `;` entfernt, dann `]` am Ende. Ein Pfad, dessen Name zu lang ist, gilt wie bei `safe_exists` in pytest als nicht vorhanden. +- Gefunden: `-c `. pytest setzt `rootdir` und `confcutdir` dann selbst auf ihren Ordner, wie bei der eigenen Suche. +- Nicht gefunden: `-c --rootdir --confcutdir `, `r` ist der Ordner mit `setup.py` oder sonst die Wurzel. Ohne `--confcutdir` läge `confcutdir` im Plugin-Ordner, und pytest lüde `conftest.py`-Dateien oberhalb der Wurzel. +- Die Kopfzeilen `pytest-konfiguration (Grundlinie, ): …` nennen je Interpreter, was für seinen Aufruf in der Grundlinie gilt. +- Gegen pytest selbst geprüft wird das mit `mutanten_lauf_probe_konfig.py` (Treiber-Fall 43): 26 Aufbauten, je `pytest_konfig_bestimmen` gegen `determine_setup` und ein echtes `pytest --co` ohne `-c` gegen einen Lauf mit den Argumenten des Läufers unter fremder Konfiguration. Die Probe sichert ihre Voraussetzung mit: Ohne `-c` endet pytest mit 0 (außer bei fehlenden Knoten), und wo der Aufbau ein `conftest.py` hat, ist eines geladen. +- Benannte Lücken: Eine `tox.ini` oder ein `setup.cfg`, die iniconfig nicht lesen kann, übergeht der Läufer, pytest bricht dort mit 4 ab; eine nicht lesbare `pyproject.toml` oder `pytest.toml` gilt dagegen als Treffer, damit pytest den Fehler selbst meldet. Ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration. Ohne Konfiguration in der Wurzel ist `config.inipath` die leere Datei; ini-Werte vom Typ `paths`, die über `-o` oder `PYTEST_ADDOPTS` kommen, gelten dann relativ zum Plugin-Ordner. Liegt `rootdir` unter der Wurzel (Konfiguration oder `setup.py` in einem Unterordner der Tests), bildet pytest die Knotennamen relativ dazu, und `rot_bei` passt nicht mehr: Die Grundlinie meldet `FEHLT`, die Kopfzeile nennt die gefundene Datei; heute betrifft das kein Projekt im Vault. Zeilen trennt der Läufer wie iniconfig mit `splitlines`, also auch an Seitenvorschub und Unicode-Zeilentrennern. + +**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit ; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler eine kurze Fassung als Meldung, die Ursache vorn, damit sie in die 120 Zeichen der Mutantenzeile passt: `pytest-Bedienfehler (Rueckgabewert 4): `. Die Zeile ist bei 4 die erste mit „error“ außer der Gebrauchszeile `ERROR: usage:` von argparse, bei 3 die letzte mit `INTERNALERROR>` (danach steht noch die Summenzeile `no tests ran …`, Treiber-Fall 46), ohne Treffer die letzte überhaupt. Die lange Fassung steht im Bericht beim Test unter `ausgabe`. Diese Tests machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich `found no collectors for …` (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte). + +**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, außer pytest ist für diesen Interpreter abgebrochen; dann steht der eine Befund aus „Abbruch von pytest“. **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. @@ -131,7 +144,7 @@ Die Meldung ist `longrepr.reprcrash.message`, wenn es das gibt, sonst die erste Ausgabe auf stdout, UTF-8, je Mutant eine Zeile: `D1 OK DA::test_x=ROT() …`. 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: Mutanten, OK, LUECKE, UNKLAR, ZEIT, 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. +`--bericht` schreibt dasselbe als JSON: `{"wurzel", "mutanten_datei", "befunde": [...], "grundlinie": {knoten: {ergebnis, meldung}}, "mutanten": [{"id", "datei", "status", "hinweis", "treffer", "tests": [{"eintrag", "knoten", "ergebnis", "meldung", "ausgabe" (nur bei einem Abbruch von pytest)}]}], "zusammenfassung": {...}}`. Der Bericht wird auch bei einem Abbruch geschrieben, mit allem, was bis dahin feststand. | Exit | Bedeutung | |---|---| @@ -158,7 +171,7 @@ Alle Temp-Ordner liegen außerhalb der Wurzel und werden am Ende entfernt, auch ## 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. +Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testdatei, `pyproject.toml` mit `[tool.pytest.ini_options]`; für die Fälle 36 bis 42 je ein eigenes als Unterordner `k/` eines Rahmen-Ordners), 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 | |---|---|---| @@ -197,6 +210,17 @@ Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testd | 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 | | 35 | ein Mutant hängt, der Läufer bekommt nach drei Sekunden SIGTERM (eigenes `TMPDIR`), seit 2026-09-28 | Zeile `ABBRUCH: SIGTERM`, kein Prozess mit `HOME` im Fall-Tmpdir, keine `mutanten_lauf_*`-Reste, Zieldatei byte-gleich | +| 36 | Wurzel ohne eigene Konfiguration, darüber eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"` (der Fall der Plankritik 2 von C1), seit 2026-10-06 | Exit 0, zweimal `OK`, Kopfzeile „keine in der Wurzel, leere Datei“ | +| 37 | Wurzel mit eigener `pyproject.toml` (Marker `integration` abgewählt), darüber dieselbe fremde | sauberer Fall Exit 0 mit Kopfzeile `k/pyproject.toml (aus der Wurzel)`; Test mit Marker in der Grundlinie Exit 2 und `ABGEWAEHLT`, die eigene Konfiguration gilt also | +| 38 | eigene Konfiguration mit `addopts = "--gibt-es-nicht"` | Exit 2, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` aus der pytest-Ausgabe, keine Zeile `ist FEHLT` | +| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` | +| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1; die Mutantenzeile selbst trägt `UNKLAR`, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht`; im Bericht steht beim Test `ausgabe` mit „Ende der Ausgabe:“ | +| 41 | Wurzel über einen Symlink angegeben (`link/k` → `echt/k`), seit der Nachbesserung vom 2026-10-06 | Exit 0, zweimal `OK` | +| 42 | ein `conftest.py` der Wurzel wirft in `pytest_collection_modifyitems` | Exit 2, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3)` und der Text der Ausnahme, keine Zeile `ist FEHLT` | +| 43 | `mutanten_lauf_probe_konfig.py`: 26 Aufbauten gegen `determine_setup` und gegen echte `pytest --co`-Läufe, mit Selbstprüfung der Voraussetzung | Exit 0, `abweichungen: 0` | +| 44 | zwei Interpretergruppen (`python_je_datei`), nur in einer ein Knoten, den es nicht gibt; diese Gruppe einmal an erster, einmal an letzter Stelle | je Exit 2, genau ein Befund mit dem Interpreter dieser Gruppe, keiner für die andere, keine Zeile `ist FEHLT`; genau zwei Kopfzeilen, je eine je Interpreter | +| 45 | ein Knoten mit einem Dateinamen über 255 Zeichen | Exit 2 mit einem Befund `Grundlinie: pytest-…`, kein `unerwarteter Abbruch` | +| 46 | ein Mutant lässt ein `conftest.py` der Wurzel in `pytest_collection_modifyitems` werfen | Exit 1, die Mutantenzeile trägt `UNKLAR`, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3): ` und den Text der Ausnahme; im Bericht `ausgabe` mit „Ende der Ausgabe:“ | 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. diff --git a/.vault-tools/plan-kopie/mutanten_lauf_probe_konfig.py b/.vault-tools/plan-kopie/mutanten_lauf_probe_konfig.py new file mode 100644 index 0000000..bc12e3e --- /dev/null +++ b/.vault-tools/plan-kopie/mutanten_lauf_probe_konfig.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Gleichwertigkeitsprobe fuer die Konfigurationssuche von mutanten_lauf.py. + +Zweck: Je Aufbau zwei Vergleiche. Erstens pytest_konfig_bestimmen aus +mutanten_lauf.py gegen determine_setup aus pytest selbst (Konfigurationsdatei +und rootdir). Zweitens ein echtes "pytest --co" ohne -c in einem sauberen +Rahmen gegen einen Lauf mit den Argumenten des Laeufers in einem Rahmen, ueber +dem eine fremde pyproject.toml, ein fremdes conftest.py und ein fremdes +setup.py liegen. Verglichen werden Rueckgabewert, Knotennamen und die Liste +der geladenen conftest.py-Dateien. + +Aufruf (in WSL, mit dem Python eines Venvs, in dem pytest liegt): + python mutanten_lauf_probe_konfig.py +Exit 0 ohne Abweichung, 1 mit Abweichung, 2 bei falschem Aufruf. + +Geltungsbereich: ein Fangnetz fuer die Aufbauten unten, kein Nachweis fuer +jeden denkbaren Baum. Ein Knoten mit ".." fehlt, weil mutanten_lauf.py ihn +schon beim Lesen der mutanten.toml abweist. Eine Wurzel ueber einen Symlink +prueft Treiber-Fall 41. + +Die Probe prueft ihre eigene Voraussetzung mit: Ohne -c im sauberen Rahmen +muss pytest mit 0 enden (ausser bei den Aufbauten knoten_fehl*), und wo der +Aufbau ein conftest.py hat, muss eines geladen sein. Sonst endeten beide +Laeufe gleich kaputt, und "gleich" hiesse nichts. + +Grundlage: Probe des Pruefers vom 2026-10-06 (25 Aufbauten). Das conftest.py +der Probe schreibt den Zeilenumbruch hier mit chr(10), damit kein Backslash +durch die Zeichenkette muss. +""" + +import importlib.util +import os +import subprocess +import sys +from pathlib import Path + +TEST = "def test_a():\n pass\n\n\ndef test_b():\n pass\n" +CONFTEST = "import os\nopen(os.environ['CFLOG'], 'a').write(__file__ + chr(10))\n" +INI = "[pytest]\n" + +AUFBAUTEN = { + "wurzel_pytest_ini": ({"pytest.ini": INI, "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "unterordner_ini": ( + {"tests/pytest.ini": INI, "tests/test_a.py": TEST, "conftest.py": CONFTEST, "tests/conftest.py": CONFTEST}, + ["tests/test_a.py::test_a"], + ), + "zwei_ordner_ini_im_ersten": ( + {"tests/a/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST}, + ["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"], + ), + "zwei_ordner_ini_im_zweiten": ( + {"tests/b/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST}, + ["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"], + ), + "setup_py_wurzel": ({"setup.py": "", "tests/test_a.py": TEST, "conftest.py": CONFTEST}, ["tests/test_a.py::test_a"]), + "setup_py_unterordner": ( + {"sub/setup.py": "", "sub/tests/test_a.py": TEST, "conftest.py": CONFTEST, "sub/conftest.py": CONFTEST}, + ["sub/tests/test_a.py::test_a"], + ), + "setup_py_neben_ini": ( + {"tests/a/setup.py": "", "tests/b/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST}, + ["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"], + ), + "pyproject_ohne_abschnitt": ( + {"pyproject.toml": "[project]\nname = 'x'\n", "tests/test_a.py": TEST, "conftest.py": CONFTEST}, + ["tests/test_a.py::test_a"], + ), + "pyproject_ohne_unten_ini_oben": ( + {"tests/pyproject.toml": "[project]\nname = 'x'\n", "pytest.ini": INI, "tests/test_a.py": TEST}, + ["tests/test_a.py::test_a"], + ), + "pyproject_ohne_oben_ini_unten": ( + {"pyproject.toml": "[project]\nname = 'x'\n", "tests/pytest.ini": INI, "tests/test_a.py": TEST}, + ["tests/test_a.py::test_a"], + ), + "pyproject_ini_options_leer": ({"pyproject.toml": "[tool.pytest.ini_options]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "pyproject_tool_pytest_leer": ({"pyproject.toml": "[tool.pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "pyproject_toml_modus": ({"pyproject.toml": "[tool.pytest]\nminversion = '8'\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "tox_mit_abschnitt": ({"tox.ini": "[tox]\n[pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "tox_ohne_abschnitt": ({"tox.ini": "[tox]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "tox_abschnitt_mit_kommentar": ({"tox.ini": "[pytest] # k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "tox_abschnitt_mit_semikolon": ({"tox.ini": "[pytest] ; k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "tox_seitenvorschub_vor_abschnitt": ({"tox.ini": "[tox]" + chr(12) + "[pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "setup_cfg_tool_pytest": ({"setup.cfg": "[tool:pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "pytest_toml_leer": ({"pytest.toml": "", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "punkt_pytest_ini_unterordner": ({"tests/.pytest.ini": "", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]), + "knoten_fehlt_teilweise": ( + {"tests/pytest.ini": INI, "tests/test_a.py": TEST}, + ["tests/test_a.py::test_a", "gibtsnicht/test_x.py::test_a"], + ), + "knoten_fehlen_alle": ({"tests/pytest.ini": INI, "tests/test_a.py": TEST}, ["gibtsnicht/test_x.py::test_a"]), + "ordner_als_knoten": ({"tests/pytest.ini": INI, "tests/test_a.py": TEST}, ["tests"]), + "testdatei_in_der_wurzel": ({"test_a.py": TEST, "conftest.py": CONFTEST}, ["test_a.py::test_a"]), + "ohne_konfiguration": ( + {"tests/test_a.py": TEST, "conftest.py": CONFTEST, "tests/conftest.py": CONFTEST}, + ["tests/test_a.py::test_a"], + ), +} + + +def baue(rahmen, dateien, fremd): + wurzel = rahmen / "k" + for rel, inhalt in dateien.items(): + pfad = wurzel / rel + pfad.parent.mkdir(parents=True, exist_ok=True) + pfad.write_text(inhalt, encoding="utf-8") + if fremd: + (rahmen / "pyproject.toml").write_text("[tool.pytest.ini_options]\naddopts = '--new-first'\n", encoding="utf-8") + (rahmen / "conftest.py").write_text(CONFTEST, encoding="utf-8") + (rahmen / "setup.py").write_text("", encoding="utf-8") + return wurzel + + +def sammeln(wurzel, zusatz, knoten, cflog): + umgebung = dict(os.environ, CFLOG=str(cflog), PYTHONDONTWRITEBYTECODE="1") + lauf = subprocess.run( + [sys.executable, "-m", "pytest", "--co", "-q", "-p", "no:cacheprovider"] + zusatz + knoten, + cwd=str(wurzel), env=umgebung, capture_output=True, text=True, timeout=120, + ) + knotennamen = sorted(zeile for zeile in lauf.stdout.splitlines() if "::" in zeile) + geladen = [] + if cflog.exists(): + geladen = sorted(os.path.relpath(z, str(wurzel)) for z in cflog.read_text(encoding="utf-8").split("\n") if z) + cflog.unlink() + return lauf.returncode, knotennamen, geladen + + +def main(): + if len(sys.argv) != 3: + print("Aufruf: mutanten_lauf_probe_konfig.py ") + return 2 + spec = importlib.util.spec_from_file_location("mutanten_lauf_unter_probe", sys.argv[1]) + laeufer = importlib.util.module_from_spec(spec) + spec.loader.exec_module(laeufer) + from _pytest.config.findpaths import determine_setup + + arbeit = Path(sys.argv[2]) + leere_konfig = arbeit / "plugin" / "pytest.ini" + leere_konfig.parent.mkdir(parents=True, exist_ok=True) + leere_konfig.write_text("", encoding="utf-8") + cflog = arbeit / "cflog" + abweichungen = 0 + for name, (dateien, knoten) in AUFBAUTEN.items(): + sauber = baue(arbeit / name / "sauber", dateien, False) + fremd = baue(arbeit / name / "fremd", dateien, True) + vorher = os.getcwd() + os.chdir(sauber) + try: + rootdir, inipath, _, _ = determine_setup( + inifile=None, override_ini=None, args=knoten, rootdir_cmd_arg=None, invocation_dir=sauber, + ) + erwartet = (str(inipath) if inipath else None, str(rootdir)) + except Exception as exc: + erwartet = ("Ausnahme " + type(exc).__name__, None) + finally: + os.chdir(vorher) + konfig, rootdir_l = laeufer.pytest_konfig_bestimmen(sauber, knoten) + bekommen = (str(konfig) if konfig else None, str(rootdir_l)) + ohne_c = sammeln(sauber, [], knoten, cflog) + mit_c = sammeln(fremd, laeufer.pytest_konfig_argumente(fremd, knoten, leere_konfig), knoten, cflog) + gleich_suche = erwartet == bekommen + gleich_lauf = ohne_c == mit_c + voraussetzung = name.startswith("knoten_fehl") or ohne_c[0] == 0 + if any(rel.endswith("conftest.py") for rel in dateien) and not ohne_c[2]: + voraussetzung = False + if not (gleich_suche and gleich_lauf and voraussetzung): + abweichungen += 1 + print("{0:34s} suche={1} lauf={2} voraussetzung={3}".format( + name, "gleich" if gleich_suche else "ABWEICHUNG", "gleich" if gleich_lauf else "ABWEICHUNG", + "ok" if voraussetzung else "VERLETZT")) + if not gleich_suche: + print(" pytest: {0}".format(erwartet)) + print(" laeufer: {0}".format(bekommen)) + if not gleich_lauf: + print(" ohne -c, sauber: {0}".format(ohne_c)) + print(" laeufer, fremd: {0}".format(mit_c)) + print("aufbauten: {0}, abweichungen: {1}".format(len(AUFBAUTEN), abweichungen)) + return 0 if abweichungen == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.vault-tools/plan-kopie/mutanten_lauf_treiber.py b/.vault-tools/plan-kopie/mutanten_lauf_treiber.py index ab7a861..058ba8b 100644 --- a/.vault-tools/plan-kopie/mutanten_lauf_treiber.py +++ b/.vault-tools/plan-kopie/mutanten_lauf_treiber.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Treiber fuer mutanten_lauf.py: 35 Angriffsfaelle auf einer Wegwerfkopie. +"""Treiber fuer mutanten_lauf.py: 46 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 @@ -16,9 +16,23 @@ Fall 35 (Nachbesserung 2026-09-28, N11) prueft das Aufraeumen bei SIGTERM: Ein Mutant haengt, der Laeufer bekommt nach wenigen Sekunden selbst ein SIGTERM. Damit die Pruefung nur die Reste dieses einen Falls sieht, bekommt der Laeufer dafuer ein eigenes TMPDIR (tempfile respektiert die Umgebungsvariable). + +Faelle 36 bis 40 (2026-10-06) pruefen die pytest-Konfiguration: Die Wurzel +liegt dafuer als k/ in einem eigenen Rahmen-Ordner, darueber wahlweise eine +fremde pyproject.toml mit addopts, die zu -p no:cacheprovider nicht passen +(wie im Windows-Temp bei der Plankritik 2 von C1), oder ein fremdes conftest.py. +Dazu ein Bedienfehler von pytest in der Grundlinie und unter einem Mutanten. + +Faelle 41 bis 45 (Nachbesserung nach der Pruefung vom 2026-10-06): Wurzel ueber +einen Symlink, Rueckgabewert 3, die Gleichwertigkeitsprobe gegen pytest selbst +(mutanten_lauf_probe_konfig.py), ein Abbruch in nur einer von zwei +Interpretergruppen an erster und an letzter Stelle, ein ueberlanger Knoten. +Fall 46 (Nachbesserung 2): Rueckgabewert 3 unter einem Mutanten, Kurzfassung in +der Mutantenzeile und lange Fassung im Bericht. """ import hashlib +import json import os import shutil import signal @@ -30,6 +44,7 @@ from pathlib import Path HIER = Path(__file__).resolve().parent LAEUFER = HIER / "mutanten_lauf.py" +PROBE = HIER / "mutanten_lauf_probe_konfig.py" INTERPRETER = "/home/claude/venvs/trading/bin/python" FEHLERLISTE = [] @@ -1010,11 +1025,301 @@ def fall_35(basis, projektpfad): shutil.rmtree(eigenes_tmpdir, ignore_errors=True) +# --- Faelle 36 bis 40: pytest-Konfiguration --------------------------------- + +FREMDE_KONFIG = ( + "[tool.pytest.ini_options]\n" + "addopts = \"--new-first --failed-first\"\n" +) + +FREMDES_CONFTEST = "raise RuntimeError(\"fremdes conftest oberhalb der Wurzel geladen\")\n" + + +def baue_rahmen(eigene_konfig, fremde_konfig, fremdes_conftest): + rahmen = Path(tempfile.mkdtemp(prefix="mutanten_lauf_treiber_rahmen_")) + if fremde_konfig: + (rahmen / "pyproject.toml").write_text(FREMDE_KONFIG, encoding="utf-8") + if fremdes_conftest: + (rahmen / "conftest.py").write_text(FREMDES_CONFTEST, encoding="utf-8") + wurzel = rahmen / "k" + dateien = baue_projekt(wurzel) + if not eigene_konfig: + (wurzel / "pyproject.toml").unlink() + dateien = [p for p in dateien if p != wurzel / "pyproject.toml"] + return rahmen, wurzel, dateien + + +def fall_36(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=False, fremde_konfig=True, fremdes_conftest=False) + try: + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + ergebnis = starte_laeufer(wurzel, toml) + ok = ( + ergebnis.returncode == 0 + and "D1 OK" in ergebnis.stdout + and "D2 OK" in ergebnis.stdout + and "keine in der Wurzel, leere Datei" in ergebnis.stdout + ) + melde("Fall 36", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + pruefe_unveraendert(dateien, snap, "Fall 36") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_37(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=True, fremdes_conftest=False) + try: + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + sauber = starte_laeufer(wurzel, toml) + schreibe_toml(toml, kopf() + mutant_luecke("M37", ["tests/test_faelle.py::test_wird_abgewaehlt"])) + abgewaehlt = starte_laeufer(wurzel, toml) + ausgabe = abgewaehlt.stdout + abgewaehlt.stderr + ok = ( + sauber.returncode == 0 + and "D1 OK" in sauber.stdout + and "D2 OK" in sauber.stdout + and "k/pyproject.toml (aus der Wurzel)" in sauber.stdout + and abgewaehlt.returncode == 2 + and "ABGEWAEHLT" in ausgabe + ) + melde("Fall 37", ok, "exit={0}/{1}\n{2}{3}\n{4}".format( + sauber.returncode, abgewaehlt.returncode, sauber.stdout, sauber.stderr, ausgabe)) + pruefe_unveraendert(dateien, snap, "Fall 37") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_38(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False) + try: + (wurzel / "pyproject.toml").write_text( + "[tool.pytest.ini_options]\n" + "addopts = \"--gibt-es-nicht\"\n", + encoding="utf-8", + ) + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + ergebnis = starte_laeufer(wurzel, toml) + ausgabe = ergebnis.stdout + ergebnis.stderr + ok = ( + ergebnis.returncode == 2 + and "pytest-Bedienfehler (Rueckgabewert 4)" in ausgabe + and "--gibt-es-nicht" in ausgabe + and "ist FEHLT" not in ausgabe + ) + melde("Fall 38", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe)) + pruefe_unveraendert(dateien, snap, "Fall 38") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_39(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=False, fremde_konfig=False, fremdes_conftest=True) + try: + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + ergebnis = starte_laeufer(wurzel, toml) + ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout + melde("Fall 39", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + pruefe_unveraendert(dateien, snap, "Fall 39") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_40(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False) + try: + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, kopf() + ( + "[[mutant]]\n" + "id = \"M40\"\n" + "datei = \"pyproject.toml\"\n" + "aus = '''\n" + "addopts = '-m \"not integration\"'\n" + "'''\n" + "wird = '''\n" + "addopts = \"--gibt-es-nicht\"\n" + "'''\n" + "rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n" + )) + bericht = rahmen / "bericht40.json" + ergebnis = starte_laeufer(wurzel, toml, bericht_pfad=bericht) + mutantenzeile = [z for z in ergebnis.stdout.split("\n") if z.startswith("M40 ")] + try: + ausgabe_bericht = json.loads(bericht.read_text(encoding="utf-8"))["mutanten"][0]["tests"][0].get("ausgabe", "") + except (OSError, ValueError, KeyError, IndexError): + ausgabe_bericht = "" + ok = ( + ergebnis.returncode == 1 + and len(mutantenzeile) == 1 + and "M40 UNKLAR" in mutantenzeile[0] + and "pytest-Bedienfehler (Rueckgabewert 4)" in mutantenzeile[0] + and "--gibt-es-nicht" in mutantenzeile[0] + and "Ende der Ausgabe:" in ausgabe_bericht + ) + melde("Fall 40", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + pruefe_unveraendert(dateien, snap, "Fall 40") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_41(basis, projektpfad): + rahmen = Path(tempfile.mkdtemp(prefix="mutanten_lauf_treiber_symlink_")) + try: + dateien = baue_projekt(rahmen / "echt" / "k") + os.symlink(rahmen / "echt", rahmen / "link") + wurzel = rahmen / "link" / "k" + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + ergebnis = starte_laeufer(wurzel, toml) + ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout + melde("Fall 41", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + pruefe_unveraendert(dateien, snap, "Fall 41") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_42(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False) + try: + (wurzel / "conftest.py").write_text( + "def pytest_collection_modifyitems(items):\n" + " raise RuntimeError(\"interner Fehler fuer Fall 42\")\n", + encoding="utf-8", + ) + dateien = dateien + [wurzel / "conftest.py"] + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, mutant_d1_d2()) + ergebnis = starte_laeufer(wurzel, toml) + ausgabe = ergebnis.stdout + ergebnis.stderr + ok = ( + ergebnis.returncode == 2 + and "pytest-Abbruch mit internem Fehler (Rueckgabewert 3)" in ausgabe + and "interner Fehler fuer Fall 42" in ausgabe + and "ist FEHLT" not in ausgabe + ) + melde("Fall 42", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe)) + pruefe_unveraendert(dateien, snap, "Fall 42") + finally: + shutil.rmtree(rahmen, ignore_errors=True) + + +def fall_43(basis, projektpfad): + arbeit = Path(tempfile.mkdtemp(prefix="mutanten_lauf_treiber_probe_")) + try: + ergebnis = subprocess.run( + [INTERPRETER, str(PROBE), str(LAEUFER), str(arbeit)], + capture_output=True, text=True, timeout=900, + ) + ok = ergebnis.returncode == 0 and ", abweichungen: 0" in ergebnis.stdout + melde("Fall 43", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + finally: + shutil.rmtree(arbeit, ignore_errors=True) + + +def fall_44(basis, projektpfad): + zweit_dir = Path(tempfile.mkdtemp(prefix="mutanten_lauf_fall44_")) + venv_link = zweit_dir / "venv_zwei" + zweit_python = venv_link / "bin" / "python" + try: + os.symlink(Path(INTERPRETER).parent.parent, venv_link) + toml = projektpfad / "mutanten.toml" + zusatz = "\n[python_je_datei]\n\"tests/test_zeit.py\" = \"{0}\"\n".format(zweit_python) + befund_zwei = "Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit {0};".format(zweit_python) + befund_eins = "Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit {0};".format(INTERPRETER) + reihenfolgen = [ + ["tests/test_zeit.py::test_gibt_es_nicht", "tests/test_modul.py::test_f1_positiv"], + ["tests/test_modul.py::test_f1_positiv", "tests/test_zeit.py::test_gibt_es_nicht"], + ] + for stelle, rot_bei in zip(("erste", "letzte"), reihenfolgen): + schreibe_toml(toml, kopf(zusatz=zusatz) + mutant_luecke("M44", rot_bei)) + ergebnis = starte_laeufer(basis, toml) + ausgabe = ergebnis.stdout + ergebnis.stderr + kopfzeilen = [z for z in ergebnis.stdout.split("\n") if z.startswith("pytest-konfiguration (Grundlinie, ")] + ok = ( + ergebnis.returncode == 2 + and ausgabe.count(befund_zwei) == 1 + and befund_eins not in ausgabe + and "ist FEHLT" not in ausgabe + and "test_f1_positiv ist" not in ausgabe + and len(kopfzeilen) == 2 + and sum(1 for z in kopfzeilen if z.startswith("pytest-konfiguration (Grundlinie, {0}): ".format(INTERPRETER))) == 1 + and sum(1 for z in kopfzeilen if z.startswith("pytest-konfiguration (Grundlinie, {0}): ".format(zweit_python))) == 1 + ) + melde("Fall 44 ({0} Stelle)".format(stelle), ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe)) + finally: + shutil.rmtree(zweit_dir, ignore_errors=True) + + +def fall_45(basis, projektpfad): + toml = projektpfad / "mutanten.toml" + schreibe_toml(toml, kopf() + mutant_luecke("M45", ["tests/" + "a" * 300 + ".py::test_x"])) + ergebnis = starte_laeufer(basis, toml) + ausgabe = ergebnis.stdout + ergebnis.stderr + ok = ( + ergebnis.returncode == 2 + and "Grundlinie: pytest-" in ausgabe + and "unerwarteter Abbruch" not in ausgabe + ) + melde("Fall 45", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe[-2000:])) + + +def fall_46(basis, projektpfad): + rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False) + try: + (wurzel / "conftest.py").write_text("X = 1\n", encoding="utf-8") + dateien = dateien + [wurzel / "conftest.py"] + snap = snapshot(dateien) + toml = rahmen / "mutanten.toml" + schreibe_toml(toml, kopf() + ( + "[[mutant]]\n" + "id = \"M46\"\n" + "datei = \"conftest.py\"\n" + "aus = '''\n" + "X = 1\n" + "'''\n" + "wird = '''\n" + "def pytest_collection_modifyitems(items):\n" + " raise RuntimeError(\"intern Fall 46\")\n" + "'''\n" + "rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n" + )) + bericht = rahmen / "bericht46.json" + ergebnis = starte_laeufer(wurzel, toml, bericht_pfad=bericht) + mutantenzeile = [z for z in ergebnis.stdout.split("\n") if z.startswith("M46 ")] + try: + ausgabe_bericht = json.loads(bericht.read_text(encoding="utf-8"))["mutanten"][0]["tests"][0].get("ausgabe", "") + except (OSError, ValueError, KeyError, IndexError): + ausgabe_bericht = "" + ok = ( + ergebnis.returncode == 1 + and len(mutantenzeile) == 1 + and "M46 UNKLAR" in mutantenzeile[0] + and "pytest-Abbruch mit internem Fehler (Rueckgabewert 3): " in mutantenzeile[0] + and "intern Fall 46" in mutantenzeile[0] + and "Ende der Ausgabe:" in ausgabe_bericht + ) + melde("Fall 46", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)) + pruefe_unveraendert(dateien, snap, "Fall 46") + finally: + shutil.rmtree(rahmen, 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, fall_35, + fall_31, fall_32, fall_33, fall_34, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40, + fall_41, fall_42, fall_43, fall_44, fall_45, fall_46, ]