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 612cd7fb54
commit ff7854cf39
8 files changed
+3457 -19

No files matched your search

@@ -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.