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 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5.5 committed 2026-10-06 17:41:05 +02:00
1 parent ed55848b00
commit 30ed04a643
8 files changed
+3457 -19

No files matched your search

+8 -2
View File
@@ -5,7 +5,7 @@ title: "Planwerkzeuge: Plan auf eine Kopie anwenden und jede Welle dagegen vergl
summary: "Skripte für die Standardschleife bei Plänen mit ausgeschriebenem Code — schlanke Kopie bauen, Plan auf die Kopie anwenden, je Welle Menge und Bytes vergleichen, Aus-wird-Blöcke prüfen, Codeblöcke auf unsichtbare Zeichen absuchen, Escape-Folgen per Codepunkt setzen. Entstanden 2026-09-17 in der Trading Plattform (Teil 3b)."
tags: [vault-tools, codeworkflow]
created: 2026-09-17
updated: 2026-09-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 <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test] [--venv <wsl-pfad>] [--frontend <ordner>] [--projektname <ordnername>]`. **Für ein anderes Projekt als die Trading Plattform braucht es die drei letzten Schalter (seit 2026-09-24):** Ihre Vorgaben sind die der Trading Plattform (`~/venvs/trading`, `dashboard`, `trading-plattform`). Jedes andere Projekt nennt seine Werte, für den Nexhorizons Hub `--venv '~/venvs/hub' --frontend frontend --projektname 'active - Nexhorizons Hub'`, das Venv in einfachen Anführungszeichen, sonst macht die Git-Bash aus `~` einen Windows-Pfad. Das Skript druckt die benutzten Werte in der ersten Zeile. Die Vorgabe `trading-plattform` passt nur auf Pläne mit Arbeitsbaum-Pfaden; ältere Trading-Pläne mit absoluten Pfaden unter `active - Trading Plattform` brauchen `--projektname 'active - Trading Plattform'`, sonst endet der Lauf mit einem `ValueError`. Weiter fest verdrahtet bleiben die Distribution `Debian`, die Ordner `tests/` und `tests/fixtures/`, die Endungen `.test.ts`/`.test.tsx` und `npx.cmd vitest`. **Vorbedingung:** Das Projekt ist im Venv nicht editierbar installiert (`pip install -e`); sonst löst der Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung läuft grün. `tests/conftest.py` gilt dabei als Produktivseite und wird zurückgesetzt; nur wenn die neue Fixture eines Tasks dort liegt, lässt `--conftest-als-test` sie stehen (Beleg unten, 2026-09-22). `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich.
8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. Es fährt zuerst die Grundlinie, dann je Mutant nur die genannten Tests, setzt die Datei zurück und prüft das per SHA-256. Je Test gibt es die Fehlermeldung mit aus; ob ein Test an der gemeinten Stelle fällt, liest man dort. `--nur-muster` prüft nur, ob jedes Muster genau einmal dasteht, ohne pytest. Die Kopie legt man in WSL unter `/tmp` an (`cp -a`, danach `__pycache__` löschen). Auf `/mnt/d` läuft es auch, nur langsamer. **Aus der Git-Bash ein Skript nie mit `wsl … bash /mnt/d/…` starten:** Die Git-Bash macht aus dem Pfad einen Windows-Pfad. Aus der PowerShell starten oder `MSYS_NO_PATHCONV=1` davorsetzen. 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 <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. 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 <mutanten_lauf.py> <arbeitsordner>` 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_<n>`) 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).
@@ -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, <python>): …` 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 <python>; 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): <zeile>`. 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, <python>): …` 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 <python>; 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): <zeile>`. 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 <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. 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 <mutanten_lauf.py> <arbeitsordner>` 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_<n>`) 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 <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. 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 <mutanten_lauf.py> <arbeitsordner>` 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_<n>`) 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“.
File diff suppressed because it is too large. Load diff
+264 -9
View File
@@ -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,
@@ -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 <kopie> --mutanten <mutanten.toml> [--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:** `<python> -m pytest -q -p no:cacheprovider -p _mutanten_lauf_plugin <knoten…>` mit `cwd` = Wurzel. Das Plugin ist eine kleine Datei, die das Skript in einen eigenen Temp-Ordner schreibt; der Ordner kommt vorn in `PYTHONPATH`. Das Plugin schreibt je Bericht eine JSON-Zeile in die Datei aus der Umgebungsvariable `MUTANTEN_LAUF_ERGEBNIS`:
**pytest-Aufruf:** `<python> -m pytest -q -p no:cacheprovider -p _mutanten_lauf_plugin <konfiguration…> <knoten…>` mit `cwd` = Wurzel. Das Plugin ist eine kleine Datei, die das Skript in einen eigenen Temp-Ordner schreibt; der Ordner kommt vorn in `PYTHONPATH`. Das Plugin schreibt je Bericht eine JSON-Zeile in die Datei aus der Umgebungsvariable `MUTANTEN_LAUF_ERGEBNIS`:
- `pytest_runtest_logreport`: `{"art": "test", "knoten": nodeid, "phase": when, "ergebnis": outcome, "xfail": hat wasxfail, "meldung": erste Zeile}`
- `pytest_collectreport` nur bei `failed`: `{"art": "sammeln", "knoten": nodeid, "meldung": erste Zeile}`
@@ -111,7 +112,19 @@ python mutanten_lauf.py --wurzel <kopie> --mutanten <mutanten.toml> [--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 <datei>`. pytest setzt `rootdir` und `confcutdir` dann selbst auf ihren Ordner, wie bei der eigenen Suche.
- Nicht gefunden: `-c <leere pytest.ini im Plugin-Ordner> --rootdir <r> --confcutdir <r>`, `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, <python>): …` 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 <python>; 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): <zeile>`. 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(<meldung, 120 Zeichen>) …`. Tests erscheinen in der Kurzform aus `rot_bei`. Ein vorhandener `hinweis` folgt in der nächsten Zeile, eingerückt. Oben stehen Wurzel, Mutantendatei, Anzahl gesamt und ausgewählt und je Interpreter die Python-Version. Unten eine gerechnete Zusammenfassung: `ergebnis: <n> Mutanten, <a> OK, <b> LUECKE, <c> UNKLAR, <d> ZEIT, <e> MUSTER`.
`--bericht` schreibt dasselbe als JSON: `{"wurzel", "mutanten_datei", "befunde": [...], "grundlinie": {knoten: {ergebnis, meldung}}, "mutanten": [{"id", "datei", "status", "hinweis", "treffer", "tests": [{"eintrag", "knoten", "ergebnis", "meldung"}]}], "zusammenfassung": {...}}`. Der Bericht wird auch bei einem Abbruch geschrieben, mit allem, was bis dahin feststand.
`--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.
@@ -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 <mutanten_lauf.py> <arbeitsordner>
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 <mutanten_lauf.py> <arbeitsordner>")
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())
@@ -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,
]