.vault-tools (ohne trading-plattform-pruefung und watermarks-remover), .claude/agents, .claude/skills, .claude/workflows, .claude/settings.json. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1010 lines
45 KiB
Markdown
1010 lines
45 KiB
Markdown
---
|
|
type: plan
|
|
title: "Mutantenläufer: pytest-Konfiguration festnageln, Abbruch von pytest melden"
|
|
status: active
|
|
summary: "Ein Task: mutanten_lauf.py sucht die pytest-Konfiguration wie pytest 9.1.1, aber nie oberhalb der Wurzel, gibt sie mit -c vor und meldet einen Abbruch von pytest (Rückgabewert 3 oder 4) mit dem Ende der Ausgabe statt FEHLT je Test; dazu fünf Treiber-Fälle, Entwurf und LIESMICH."
|
|
tags: [plan, vault-tools, pruefwerkzeuge]
|
|
created: 2026-10-06
|
|
updated: 2026-10-06
|
|
modell: sonnet
|
|
effort: low
|
|
modell_begruendung: "Jeder Block steht wörtlich da, Zieldateien und Verbote sind genannt; die Blöcke für Entwurf und LIESMICH tragen typografische Anführungszeichen, Gedankenstriche und Auslassungspunkte, die Haiku still ersetzt, deshalb sonnet mit effort low."
|
|
---
|
|
|
|
Übersicht: [LIESMICH der Planwerkzeuge](../../LIESMICH.md) · Anlass: Fahrplan C1-Abschluss im Projekt After Install, Schritt 1 (`Code Projekte/active - Linux Härtung Rocky Debian Ubuntu - After Install/docs/plans/2026-10-05-fahrplan-c1-abschluss-c4.md`)
|
|
|
|
# Mutantenläufer: pytest-Konfiguration festnageln, Abbruch von pytest melden
|
|
|
|
## Warum so
|
|
|
|
- **Der Fehler** (Plankritik 2 von C1 im Projekt After Install, `docs/plans/2026-10-05-cis-paket1-system/plankritik-2/befunde.json`, Linse `bezeichner`): Liegt die Wegwerfkopie unter dem Windows-Temp und läuft der Mutantenlauf über `/mnt/c`, findet pytest beim Suchen nach oben `C:/Users/PASCAL~1/AppData/Local/Temp/pyproject.toml`. Deren `addopts = "--new-first --failed-first"` brauchen den Cache, den `-p no:cacheprovider` abschaltet; pytest endet mit Rückgabewert 4. Der Läufer meldete daraufhin alle 80 Tests der Grundlinie als `FEHLT` mit leerer Meldung und verschluckte die Ausgabe von pytest.
|
|
- **Quelle der Suchregeln:** pytest 9.1.1 in den WSL-Venvs `trading` und `mattermost` (Python 3.13.5), `_pytest/config/findpaths.py` mit SHA-256 `535d149e6f3962e44260b6645bb354976e19531f5dd248e14ee7d7fff61c8848` in beiden, nachgelesen am 2026-10-06: `determine_setup`, `locate_config` (Namen `pytest.toml`, `.pytest.toml`, `pytest.ini`, `.pytest.ini`, `pyproject.toml`, `tox.ini`, `setup.cfg`; ohne Treffer die erste `pyproject.toml` überhaupt), `get_common_ancestor`, `load_config_dict_from_file`. Aus `_pytest/config/__init__.py`: `confcutdir` ist ohne Vorgabe der Ordner der Konfigurationsdatei, sonst `rootdir` (Zeilen 1598 bis 1603); `ExitCode` 3 ist `INTERNAL_ERROR`, 4 `USAGE_ERROR`.
|
|
- **Konfiguration der Wurzel bleibt gültig.** Ein bloßes `-c /dev/null` nähme Headscale und Mattermost (`pytest.ini` in der Wurzel) und der Trading Plattform (`pyproject.toml`) ihre Einstellungen; Treiber-Fall 15 (Marker `integration` per `addopts` abgewählt) fiele um. Deshalb sucht der Läufer wie pytest, nur nie oberhalb der Wurzel.
|
|
- **`--confcutdir`.** Mit der leeren Datei aus dem Plugin-Ordner läge `confcutdir` dort, und pytest lüde `conftest.py`-Dateien oberhalb der Wurzel. Ohne fremde Konfiguration war `confcutdir` bisher die Wurzel; `--confcutdir <rootdir>` hält das fest.
|
|
- **Abbruch von pytest.** Bei Rückgabewert 3 oder 4 hält der Läufer die letzten 20 Zeilen der Ausgabe fest. In der Grundlinie ergibt das je Interpreter einen Befund statt einer `FEHLT`-Zeile je Test. Unter einem Mutanten trägt nur ein Test ohne Bericht und ohne Sammelfehler den Text; ein Sammelfehler bleibt `ROT`, denn pytest endet bei einem Knoten in einer nicht sammelbaren Datei ebenfalls mit 4 (`not found`). Eine erste Fassung, die jeden Abbruch als `UNKLAR` zählte, fiel am 2026-10-06 im Treiber-Fall 11 durch.
|
|
- **Gemessen vom Koordinator am 2026-10-06** (Rohdaten unter `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/`, Dateien `02-*`): Mit dem neuen Treiber und dem alten Läufer fallen genau die Fälle 36, 38 und 40; mit dem neuen Läufer fällt keiner. Drei kaputte Varianten des neuen Läufers zeigen die übrigen Fälle rot: Konfiguration der Wurzel nie benutzt → 15, 37, 38, 40; ohne `--confcutdir` → 39; Suche ohne Grenze an der Wurzel → 36.
|
|
|
|
## Zieldateien
|
|
|
|
Pfade relativ zu `D:/Claude/.vault-tools/plan-kopie/`:
|
|
|
|
- `mutanten_lauf.py` (geändert)
|
|
- `mutanten_lauf_treiber.py` (geändert)
|
|
- `mutanten_lauf_ENTWURF.md` (geändert)
|
|
- `LIESMICH.md` (geändert)
|
|
|
|
## Verbote
|
|
|
|
- Nur die vier Zieldateien ändern. `mutanten_lauf_kopie.sh`, `rot_nachweis.py`, `plan_materialisiere.py`, `.claude/workflows/*` und alles andere bleiben unberührt.
|
|
- Kein `git add`, kein `git commit`.
|
|
- Jeden Block byte-genau übernehmen: keine Umformulierung, keine Ersetzung typografischer Zeichen, keine zusätzlichen Leerzeilen. Schreib die Blöcke mit dem Edit-Werkzeug, Aus-Block als `old_string`, wird-Block als `new_string`.
|
|
- Steht ein Aus-Block nicht genau einmal in seiner Datei, oder widerspricht etwas im Plan einem anderen Teil, hältst du an und meldest es. Nichts still zurechtbiegen.
|
|
- Kein Lauf gegen ein echtes Projekt; der Treiber baut alles selbst unter `/tmp` in WSL.
|
|
|
|
## Schritt 1 — Tests zuerst: Treiber
|
|
|
|
Wende alle Blöcke dieses Abschnitts auf `mutanten_lauf_treiber.py` an.
|
|
|
|
### `mutanten_lauf_treiber.py`, Stelle 1 von 4
|
|
|
|
Aus:
|
|
|
|
```python
|
|
#!/usr/bin/env python3
|
|
"""Treiber fuer mutanten_lauf.py: 35 Angriffsfaelle auf einer Wegwerfkopie.
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
#!/usr/bin/env python3
|
|
"""Treiber fuer mutanten_lauf.py: 40 Angriffsfaelle auf einer Wegwerfkopie.
|
|
|
|
```
|
|
|
|
### `mutanten_lauf_treiber.py`, Stelle 2 von 4
|
|
|
|
Aus:
|
|
|
|
```python
|
|
dafuer ein eigenes TMPDIR (tempfile respektiert die Umgebungsvariable).
|
|
"""
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
dafuer ein eigenes TMPDIR (tempfile respektiert die Umgebungsvariable).
|
|
|
|
Faelle 36 bis 40 (2026-10-06) pruefen die pytest-Konfiguration: Die Wurzel
|
|
liegt dafuer als k/ in einem eigenen Rahmen-Ordner, darueber wahlweise eine
|
|
fremde pyproject.toml mit addopts, die zu -p no:cacheprovider nicht passen
|
|
(wie im Windows-Temp bei der Plankritik 2 von C1), oder ein fremdes conftest.py.
|
|
Dazu ein Bedienfehler von pytest in der Grundlinie und unter einem Mutanten.
|
|
"""
|
|
```
|
|
|
|
### `mutanten_lauf_treiber.py`, Stelle 3 von 4
|
|
|
|
Aus:
|
|
|
|
```python
|
|
|
|
FAELLE = [
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
|
|
# --- Faelle 36 bis 40: pytest-Konfiguration ---------------------------------
|
|
|
|
FREMDE_KONFIG = (
|
|
"[tool.pytest.ini_options]\n"
|
|
"addopts = \"--new-first --failed-first\"\n"
|
|
)
|
|
|
|
FREMDES_CONFTEST = "raise RuntimeError(\"fremdes conftest oberhalb der Wurzel geladen\")\n"
|
|
|
|
|
|
def baue_rahmen(eigene_konfig, fremde_konfig, fremdes_conftest):
|
|
rahmen = Path(tempfile.mkdtemp(prefix="mutanten_lauf_treiber_rahmen_"))
|
|
if fremde_konfig:
|
|
(rahmen / "pyproject.toml").write_text(FREMDE_KONFIG, encoding="utf-8")
|
|
if fremdes_conftest:
|
|
(rahmen / "conftest.py").write_text(FREMDES_CONFTEST, encoding="utf-8")
|
|
wurzel = rahmen / "k"
|
|
dateien = baue_projekt(wurzel)
|
|
if not eigene_konfig:
|
|
(wurzel / "pyproject.toml").unlink()
|
|
dateien = [p for p in dateien if p != wurzel / "pyproject.toml"]
|
|
return rahmen, wurzel, dateien
|
|
|
|
|
|
def fall_36(basis, projektpfad):
|
|
rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=False, fremde_konfig=True, fremdes_conftest=False)
|
|
try:
|
|
snap = snapshot(dateien)
|
|
toml = rahmen / "mutanten.toml"
|
|
schreibe_toml(toml, mutant_d1_d2())
|
|
ergebnis = starte_laeufer(wurzel, toml)
|
|
ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout
|
|
melde("Fall 36", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr))
|
|
pruefe_unveraendert(dateien, snap, "Fall 36")
|
|
finally:
|
|
shutil.rmtree(rahmen, ignore_errors=True)
|
|
|
|
|
|
def fall_37(basis, projektpfad):
|
|
rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=True, fremdes_conftest=False)
|
|
try:
|
|
snap = snapshot(dateien)
|
|
toml = rahmen / "mutanten.toml"
|
|
schreibe_toml(toml, mutant_d1_d2())
|
|
sauber = starte_laeufer(wurzel, toml)
|
|
schreibe_toml(toml, kopf() + mutant_luecke("M37", ["tests/test_faelle.py::test_wird_abgewaehlt"]))
|
|
abgewaehlt = starte_laeufer(wurzel, toml)
|
|
ausgabe = abgewaehlt.stdout + abgewaehlt.stderr
|
|
ok = (
|
|
sauber.returncode == 0
|
|
and "D1 OK" in sauber.stdout
|
|
and "D2 OK" in sauber.stdout
|
|
and abgewaehlt.returncode == 2
|
|
and "ABGEWAEHLT" in ausgabe
|
|
)
|
|
melde("Fall 37", ok, "exit={0}/{1}\n{2}{3}\n{4}".format(
|
|
sauber.returncode, abgewaehlt.returncode, sauber.stdout, sauber.stderr, ausgabe))
|
|
pruefe_unveraendert(dateien, snap, "Fall 37")
|
|
finally:
|
|
shutil.rmtree(rahmen, ignore_errors=True)
|
|
|
|
|
|
def fall_38(basis, projektpfad):
|
|
rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False)
|
|
try:
|
|
(wurzel / "pyproject.toml").write_text(
|
|
"[tool.pytest.ini_options]\n"
|
|
"addopts = \"--gibt-es-nicht\"\n",
|
|
encoding="utf-8",
|
|
)
|
|
snap = snapshot(dateien)
|
|
toml = rahmen / "mutanten.toml"
|
|
schreibe_toml(toml, mutant_d1_d2())
|
|
ergebnis = starte_laeufer(wurzel, toml)
|
|
ausgabe = ergebnis.stdout + ergebnis.stderr
|
|
ok = (
|
|
ergebnis.returncode == 2
|
|
and "pytest-Bedienfehler (Rueckgabewert 4)" in ausgabe
|
|
and "--gibt-es-nicht" in ausgabe
|
|
and "ist FEHLT" not in ausgabe
|
|
)
|
|
melde("Fall 38", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe))
|
|
pruefe_unveraendert(dateien, snap, "Fall 38")
|
|
finally:
|
|
shutil.rmtree(rahmen, ignore_errors=True)
|
|
|
|
|
|
def fall_39(basis, projektpfad):
|
|
rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=False, fremde_konfig=False, fremdes_conftest=True)
|
|
try:
|
|
snap = snapshot(dateien)
|
|
toml = rahmen / "mutanten.toml"
|
|
schreibe_toml(toml, mutant_d1_d2())
|
|
ergebnis = starte_laeufer(wurzel, toml)
|
|
ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout
|
|
melde("Fall 39", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr))
|
|
pruefe_unveraendert(dateien, snap, "Fall 39")
|
|
finally:
|
|
shutil.rmtree(rahmen, ignore_errors=True)
|
|
|
|
|
|
def fall_40(basis, projektpfad):
|
|
rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False)
|
|
try:
|
|
snap = snapshot(dateien)
|
|
toml = rahmen / "mutanten.toml"
|
|
schreibe_toml(toml, kopf() + (
|
|
"[[mutant]]\n"
|
|
"id = \"M40\"\n"
|
|
"datei = \"pyproject.toml\"\n"
|
|
"aus = '''\n"
|
|
"addopts = '-m \"not integration\"'\n"
|
|
"'''\n"
|
|
"wird = '''\n"
|
|
"addopts = \"--gibt-es-nicht\"\n"
|
|
"'''\n"
|
|
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
|
|
))
|
|
ergebnis = starte_laeufer(wurzel, toml)
|
|
ok = (
|
|
ergebnis.returncode == 1
|
|
and "M40 UNKLAR" in ergebnis.stdout
|
|
and "pytest-Bedienfehler (Rueckgabewert 4)" in ergebnis.stdout
|
|
)
|
|
melde("Fall 40", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr))
|
|
pruefe_unveraendert(dateien, snap, "Fall 40")
|
|
finally:
|
|
shutil.rmtree(rahmen, ignore_errors=True)
|
|
|
|
|
|
FAELLE = [
|
|
```
|
|
|
|
### `mutanten_lauf_treiber.py`, Stelle 4 von 4
|
|
|
|
Aus:
|
|
|
|
```python
|
|
fall_21, fall_22, fall_23, fall_24, fall_25, fall_26, fall_27, fall_28, fall_29, fall_30,
|
|
fall_31, fall_32, fall_33, fall_34, fall_35,
|
|
]
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
fall_21, fall_22, fall_23, fall_24, fall_25, fall_26, fall_27, fall_28, fall_29, fall_30,
|
|
fall_31, fall_32, fall_33, fall_34, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40,
|
|
]
|
|
```
|
|
|
|
|
|
### Erwarteter Fehlschlag vor der Umsetzung
|
|
|
|
Der neue Treiber läuft gegen den alten Läufer. Lege zuerst in der Git-Bash den Rot-Ordner an; `git show` schreibt dort die Bytes unverändert, die PowerShell würde Zeilenenden umschreiben:
|
|
|
|
```bash
|
|
# alten Laeufer aus dem letzten Commit und den neuen Treiber in einen leeren Rot-Ordner legen
|
|
R=/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot
|
|
rm -rf "$R" && mkdir -p "$R"
|
|
git -C /d/Claude show HEAD:.vault-tools/plan-kopie/mutanten_lauf.py > "$R/mutanten_lauf.py"
|
|
cp /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py "$R/"
|
|
```
|
|
|
|
Dann in der PowerShell:
|
|
|
|
```powershell
|
|
# Treiber in WSL gegen den alten Laeufer fahren
|
|
wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot\ausgabe.txt"
|
|
"exit=$LASTEXITCODE"
|
|
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot\ausgabe.txt" -Pattern "FEHLER:|gerechnete"
|
|
```
|
|
|
|
Erwartet: `exit=1`, und die Treffer von `Select-String` enden genau auf diese vier Zeilen:
|
|
|
|
```
|
|
Fall 36 FEHLER: exit=2
|
|
Fall 38 FEHLER: exit=2
|
|
Fall 40 FEHLER: exit=1
|
|
gerechnete Fehler: 3
|
|
```
|
|
|
|
Weicht das ab, hältst du an und meldest die Ausgabe.
|
|
|
|
## Schritt 2 — Umsetzung
|
|
|
|
Wende alle Blöcke dieses Abschnitts in der Reihenfolge an.
|
|
|
|
### `mutanten_lauf.py`, Stelle 1 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
Geheimnisdateien (der Schluessel "ohne" in mutanten.toml).
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
Geheimnisdateien (der Schluessel "ohne" in mutanten.toml).
|
|
|
|
pytest-Konfiguration (seit 2026-10-06): Fuer jeden pytest-Aufruf bestimmt das
|
|
Skript die Konfigurationsdatei so, wie pytest 9.1.1 sie selbst suchen wuerde
|
|
(_pytest/config/findpaths.py: determine_setup, locate_config,
|
|
get_common_ancestor), sucht aber nie oberhalb der Wurzel, und gibt sie mit -c
|
|
vor. Ohne Konfiguration in der Wurzel bekommt pytest eine leere Datei aus dem
|
|
Plugin-Ordner, dazu --rootdir und --confcutdir. Grund: Unter dem Windows-Temp
|
|
fand pytest eine fremde pyproject.toml, deren addopts nicht zu
|
|
-p no:cacheprovider passten, und jeder Test der Grundlinie stand als FEHLT da
|
|
(Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Benannte Luecken:
|
|
tox.ini und setup.cfg werden zeilenweise nach ihrem Abschnitt durchsucht, nicht
|
|
mit iniconfig; ein setup.cfg mit [pytest] (bei pytest selbst ein Fehler) gilt
|
|
hier als keine Konfiguration.
|
|
|
|
Endet pytest mit Rueckgabewert 3 (interner Fehler) oder 4 (Bedienfehler,
|
|
ExitCode in _pytest/config/__init__.py), steht das Ende seiner Ausgabe in der
|
|
Grundlinie als eigener Befund und unter einem Mutanten in der Meldung der
|
|
Tests ohne Bericht und ohne Sammelfehler. Ein Sammelfehler bleibt ROT, auch
|
|
wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht
|
|
sammeln laesst, ergibt zusaetzlich "not found" (Treiber-Fall 11).
|
|
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 2 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
MUTANT_SCHLUESSEL = {"id", "datei", "aus", "wird", "rot_bei", "hinweis"}
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
MUTANT_SCHLUESSEL = {"id", "datei", "aus", "wird", "rot_bei", "hinweis"}
|
|
|
|
# Reihenfolge wie config_names in locate_config, pytest 9.1.1.
|
|
PYTEST_KONFIG_NAMEN = (
|
|
"pytest.toml",
|
|
".pytest.toml",
|
|
"pytest.ini",
|
|
".pytest.ini",
|
|
"pyproject.toml",
|
|
"tox.ini",
|
|
"setup.cfg",
|
|
)
|
|
LEERE_KONFIG_NAME = "pytest.ini"
|
|
PYTEST_ABBRUCH = {3: "pytest-Abbruch mit internem Fehler", 4: "pytest-Bedienfehler"}
|
|
AUSGABE_ENDE_ZEILEN = 20
|
|
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 3 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
|
|
def umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad):
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
|
|
def _ini_hat_abschnitt(pfad, abschnitt):
|
|
"""Zeilenweise Suche nach [abschnitt]. Eine nicht lesbare Datei zaehlt als
|
|
Treffer, damit pytest sie selbst liest und den Fehler selbst meldet."""
|
|
try:
|
|
text = pfad.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
return True
|
|
kopf = "[" + abschnitt + "]"
|
|
return any(zeile.strip() == kopf for zeile in text.split("\n"))
|
|
|
|
|
|
def _ist_pytest_konfig(pfad):
|
|
"""Nachbau von load_config_dict_from_file(pfad) is not None, pytest 9.1.1."""
|
|
name = pfad.name
|
|
if name in ("pytest.toml", ".pytest.toml", "pytest.ini", ".pytest.ini"):
|
|
return True
|
|
if name == "tox.ini":
|
|
return _ini_hat_abschnitt(pfad, "pytest")
|
|
if name == "setup.cfg":
|
|
return _ini_hat_abschnitt(pfad, "tool:pytest")
|
|
if name == "pyproject.toml":
|
|
try:
|
|
daten = tomllib.loads(pfad.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError):
|
|
return True
|
|
werkzeug = daten.get("tool", {})
|
|
tool_pytest = werkzeug.get("pytest", {}) if isinstance(werkzeug, dict) else {}
|
|
return isinstance(tool_pytest, dict) and bool(tool_pytest)
|
|
return False
|
|
|
|
|
|
def _innerhalb(pfad, wurzel):
|
|
return pfad == wurzel or wurzel in pfad.parents
|
|
|
|
|
|
def _konfig_suchen(startordner, wurzel):
|
|
"""Nachbau von locate_config, pytest 9.1.1; die Suche endet an der Wurzel."""
|
|
erstes_pyproject = None
|
|
for start in startordner:
|
|
for basis in (start, *start.parents):
|
|
if not _innerhalb(basis, wurzel):
|
|
break
|
|
for name in PYTEST_KONFIG_NAMEN:
|
|
pfad = basis / name
|
|
if pfad.is_file():
|
|
if name == "pyproject.toml" and erstes_pyproject is None:
|
|
erstes_pyproject = pfad
|
|
if _ist_pytest_konfig(pfad):
|
|
return pfad
|
|
return erstes_pyproject
|
|
|
|
|
|
def _gemeinsamer_vorfahr(pfade, wurzel):
|
|
"""Nachbau von get_common_ancestor, pytest 9.1.1, mit der Wurzel als Aufrufordner."""
|
|
vorfahr = None
|
|
for pfad in pfade:
|
|
if not pfad.exists():
|
|
continue
|
|
if vorfahr is None:
|
|
vorfahr = pfad
|
|
elif vorfahr in pfad.parents or pfad == vorfahr:
|
|
continue
|
|
elif pfad in vorfahr.parents:
|
|
vorfahr = pfad
|
|
else:
|
|
try:
|
|
vorfahr = Path(os.path.commonpath([str(pfad), str(vorfahr)]))
|
|
except ValueError:
|
|
pass
|
|
if vorfahr is None:
|
|
return wurzel
|
|
if vorfahr.is_file():
|
|
return vorfahr.parent
|
|
return vorfahr
|
|
|
|
|
|
def pytest_konfig_bestimmen(wurzel, knoten_liste):
|
|
"""Konfiguration und rootdir eines Aufrufs, wie determine_setup in pytest
|
|
9.1.1 sie ohne -c bestimmt, aber nie oberhalb der Wurzel gesucht.
|
|
Rueckgabe (konfig, rootdir); konfig ist None, wenn die Wurzel keine hat."""
|
|
wurzel = Path(os.path.abspath(str(wurzel)))
|
|
ordner = []
|
|
for knoten in knoten_liste:
|
|
pfad = wurzel / knoten.split("::", 1)[0]
|
|
if pfad.exists():
|
|
ordner.append(pfad if pfad.is_dir() else pfad.parent)
|
|
vorfahr = _gemeinsamer_vorfahr(ordner, wurzel)
|
|
if not _innerhalb(vorfahr, wurzel):
|
|
vorfahr = wurzel
|
|
konfig = _konfig_suchen([vorfahr], wurzel)
|
|
if konfig is not None:
|
|
return konfig, konfig.parent
|
|
for basis in (vorfahr, *vorfahr.parents):
|
|
if not _innerhalb(basis, wurzel):
|
|
break
|
|
if (basis / "setup.py").is_file():
|
|
return None, basis
|
|
if ordner != [vorfahr]:
|
|
konfig = _konfig_suchen(ordner, wurzel)
|
|
if konfig is not None:
|
|
return konfig, konfig.parent
|
|
return None, wurzel
|
|
|
|
|
|
def pytest_konfig_argumente(wurzel, knoten_liste, leere_konfig):
|
|
"""Die Konfiguration der Wurzel mit -c; sonst die leere Datei mit --rootdir
|
|
und --confcutdir. Ohne --confcutdir laege confcutdir im Ordner der leeren
|
|
Datei, und pytest laede conftest.py-Dateien oberhalb der Wurzel
|
|
(_pytest/config/__init__.py, pytest 9.1.1)."""
|
|
konfig, rootdir = pytest_konfig_bestimmen(wurzel, knoten_liste)
|
|
if konfig is not None:
|
|
return ["-c", str(konfig)]
|
|
return ["-c", str(leere_konfig), "--rootdir", str(rootdir), "--confcutdir", str(rootdir)]
|
|
|
|
|
|
def pytest_konfig_beschreibung(wurzel, knoten_liste):
|
|
konfig, rootdir = pytest_konfig_bestimmen(wurzel, knoten_liste)
|
|
if konfig is not None:
|
|
return "{0} (aus der Wurzel)".format(konfig)
|
|
return "keine in der Wurzel, leere Datei; rootdir {0}".format(rootdir)
|
|
|
|
|
|
def ausgabe_ende(stdout_bytes, stderr_bytes):
|
|
"""Die letzten nicht leeren Zeilen von stdout und stderr, je hoechstens 300 Zeichen."""
|
|
text = (
|
|
(stdout_bytes or b"").decode("utf-8", errors="replace")
|
|
+ "\n"
|
|
+ (stderr_bytes or b"").decode("utf-8", errors="replace")
|
|
)
|
|
zeilen = [zeile.rstrip()[:300] for zeile in text.split("\n") if zeile.strip()]
|
|
return zeilen[-AUSGABE_ENDE_ZEILEN:]
|
|
|
|
|
|
def umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad):
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 4 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
env = umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad)
|
|
cmd = [python_exe, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-p", PLUGIN_NAME] + list(knoten_liste)
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
env = umgebung_abdichten(home_dir, plugin_dir, ergebnis_pfad)
|
|
konfig_argumente = pytest_konfig_argumente(wurzel, knoten_liste, Path(plugin_dir) / LEERE_KONFIG_NAME)
|
|
cmd = (
|
|
[python_exe, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-p", PLUGIN_NAME]
|
|
+ konfig_argumente
|
|
+ list(knoten_liste)
|
|
)
|
|
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 5 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
absturz = None
|
|
proc = None
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
absturz = None
|
|
abbruch = None
|
|
ausgabe = (b"", b"")
|
|
proc = None
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 6 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
try:
|
|
proc.communicate(timeout=zeitlimit)
|
|
except subprocess.TimeoutExpired:
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
try:
|
|
ausgabe = proc.communicate(timeout=zeitlimit)
|
|
except subprocess.TimeoutExpired:
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 7 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
pass
|
|
proc.communicate()
|
|
except BaseException:
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
pass
|
|
ausgabe = proc.communicate()
|
|
except BaseException:
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 8 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
raise
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
raise
|
|
if not zeit_ueberschritten and proc.returncode in PYTEST_ABBRUCH:
|
|
abbruch = "{0} (Rueckgabewert {1}) mit {2}; Ende der Ausgabe:\n {3}".format(
|
|
PYTEST_ABBRUCH[proc.returncode], proc.returncode, python_exe,
|
|
"\n ".join(ausgabe_ende(*ausgabe)) or "(keine Ausgabe)",
|
|
)
|
|
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 9 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
|
|
return zeit_ueberschritten, meldungen, absturz
|
|
finally:
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
|
|
return zeit_ueberschritten, meldungen, absturz, abbruch
|
|
finally:
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 10 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
absturzmeldungen = []
|
|
for python_exe, knoten_liste in gruppen.items():
|
|
zeit, meldungen, absturz = pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimit)
|
|
if zeit:
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
absturzmeldungen = []
|
|
abbrueche = {}
|
|
for python_exe, knoten_liste in gruppen.items():
|
|
zeit, meldungen, absturz, abbruch = pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimit)
|
|
if zeit:
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 11 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
absturzmeldungen.append(absturz)
|
|
for m in meldungen:
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
absturzmeldungen.append(absturz)
|
|
if abbruch:
|
|
abbrueche[python_exe] = abbruch
|
|
for m in meldungen:
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 12 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
deselected.add(m.get("knoten"))
|
|
return reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
deselected.add(m.get("knoten"))
|
|
return reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen, abbrueche
|
|
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 13 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
gruppen = gruppiere_nach_interpreter(alle_tests, kontext["python"], kontext["python_je_datei"])
|
|
reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen = gruppen_ausfuehren(
|
|
gruppen, kontext["wurzel"], home_dir, plugin_dir, zeitlimit,
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
gruppen = gruppiere_nach_interpreter(alle_tests, kontext["python"], kontext["python_je_datei"])
|
|
reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen, abbrueche = gruppen_ausfuehren(
|
|
gruppen, kontext["wurzel"], home_dir, plugin_dir, zeitlimit,
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 14 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
befunde = list(absturzmeldungen)
|
|
ergebnis_je_knoten = {}
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
befunde = list(absturzmeldungen)
|
|
abgebrochen = set()
|
|
for python_exe, knoten_liste in gruppen.items():
|
|
if python_exe in abbrueche:
|
|
befunde.append("Grundlinie: " + abbrueche[python_exe])
|
|
abgebrochen.update(knoten_liste)
|
|
ergebnis_je_knoten = {}
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 15 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
status, meldung = ergebnis_bestimmen(knoten, reports, deselected)
|
|
ergebnis_je_knoten[knoten] = {"ergebnis": status, "meldung": meldung}
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
status, meldung = ergebnis_bestimmen(knoten, reports, deselected)
|
|
if status == "FEHLT" and knoten in abgebrochen:
|
|
ergebnis_je_knoten[knoten] = {"ergebnis": status, "meldung": "pytest abgebrochen, siehe Befunde"}
|
|
continue
|
|
ergebnis_je_knoten[knoten] = {"ergebnis": status, "meldung": meldung}
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 16 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
gruppen = gruppiere_nach_interpreter(knoten_liste, standard_python, python_je_datei)
|
|
reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen = gruppen_ausfuehren(
|
|
gruppen, wurzel, home_dir, plugin_dir, zeitlimit,
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
gruppen = gruppiere_nach_interpreter(knoten_liste, standard_python, python_je_datei)
|
|
reports, deselected, sammelfehler, zeit_ueberschritten, absturzmeldungen, abbrueche = gruppen_ausfuehren(
|
|
gruppen, wurzel, home_dir, plugin_dir, zeitlimit,
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 17 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
unklar = True
|
|
if ergebnis in ("GRUEN", "UEBERSPRUNGEN", "ABGEWAEHLT"):
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
unklar = True
|
|
abbruch = abbrueche.get(python_je_datei.get(knoten.split("::", 1)[0], standard_python))
|
|
if abbruch:
|
|
meldung = abbruch
|
|
if ergebnis in ("GRUEN", "UEBERSPRUNGEN", "ABGEWAEHLT"):
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 18 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
print("python {0}: {1}".format(interpreter, python_version_ermitteln(interpreter)))
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
print("python {0}: {1}".format(interpreter, python_version_ermitteln(interpreter)))
|
|
alle_knoten = []
|
|
for mutant in ausgewaehlte_mutanten:
|
|
for _, knoten in mutant["rot_bei"]:
|
|
if knoten not in alle_knoten:
|
|
alle_knoten.append(knoten)
|
|
print("pytest-konfiguration (Grundlinie): {0}".format(pytest_konfig_beschreibung(kontext["wurzel"], alle_knoten)))
|
|
|
|
```
|
|
|
|
### `mutanten_lauf.py`, Stelle 19 von 19
|
|
|
|
Aus:
|
|
|
|
```python
|
|
(plugin_dir / (PLUGIN_NAME + ".py")).write_text(PLUGIN_QUELLTEXT, encoding="utf-8")
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```python
|
|
(plugin_dir / (PLUGIN_NAME + ".py")).write_text(PLUGIN_QUELLTEXT, encoding="utf-8")
|
|
(plugin_dir / LEERE_KONFIG_NAME).write_text("", encoding="utf-8")
|
|
|
|
```
|
|
|
|
### `mutanten_lauf_ENTWURF.md`, Stelle 1 von 6
|
|
|
|
Aus:
|
|
|
|
```markdown
|
|
created: 2026-09-27
|
|
updated: 2026-09-27
|
|
---
|
|
```
|
|
|
|
wird:
|
|
|
|
```markdown
|
|
created: 2026-09-27
|
|
updated: 2026-10-06
|
|
---
|
|
```
|
|
|
|
### `mutanten_lauf_ENTWURF.md`, Stelle 2 von 6
|
|
|
|
Aus:
|
|
|
|
```markdown
|
|
| Hält die Abdichtung Netz und echte Zugänge fern? | Annahme, aufgelöst als Projektvorgabe: Sie ist ein Fangnetz mit benannten Lücken (Abschnitt 5), kein Nachweis. |
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```markdown
|
|
| Hält die Abdichtung Netz und echte Zugänge fern? | Annahme, aufgelöst als Projektvorgabe: Sie ist ein Fangnetz mit benannten Lücken (Abschnitt 5), kein Nachweis. |
|
|
| Welche pytest-Konfiguration gilt? | Quelle: pytest 9.1.1, `_pytest/config/findpaths.py` (`determine_setup`, `locate_config`, `get_common_ancestor`, `load_config_dict_from_file`; SHA-256 `535d149e6f3962e4…`, in den Venvs `trading` und `mattermost` gleich) und `_pytest/config/__init__.py` (Vorgabe von `confcutdir`, `ExitCode`), nachgelesen am 2026-10-06. Projektvorgabe seit 2026-10-06: gesucht wird wie pytest, aber nie oberhalb der Wurzel (Abschnitt 3, „Konfiguration“). |
|
|
|
|
```
|
|
|
|
### `mutanten_lauf_ENTWURF.md`, Stelle 3 von 6
|
|
|
|
Aus:
|
|
|
|
```markdown
|
|
|
|
**pytest-Aufruf:** `<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`:
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```markdown
|
|
|
|
**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`:
|
|
|
|
```
|
|
|
|
### `mutanten_lauf_ENTWURF.md`, Stelle 4 von 6
|
|
|
|
Aus:
|
|
|
|
```markdown
|
|
Die Meldung ist `longrepr.reprcrash.message`, wenn es das gibt, sonst die erste nicht leere Zeile von `str(longrepr)`, gekürzt auf 300 Zeichen. Das Plugin schreibt jede Zeile sofort und mit `flush`.
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```markdown
|
|
Die Meldung ist `longrepr.reprcrash.message`, wenn es das gibt, sonst die erste nicht leere Zeile von `str(longrepr)`, gekürzt auf 300 Zeichen. Das Plugin schreibt jede Zeile sofort und mit `flush`.
|
|
|
|
**Konfiguration (seit 2026-10-06):** Ohne Vorgabe sucht pytest seine Konfiguration vom gemeinsamen Vorfahren der Testpfade aufwärts bis zur Wurzel des Dateisystems. Liegt die Kopie unter dem Windows-Temp, fand pytest dort eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"`; die Optionen brauchen den Cache, den `-p no:cacheprovider` abschaltet, pytest brach mit Rückgabewert 4 ab, und der Läufer meldete jeden Test der Grundlinie als `FEHLT` (Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Seitdem bestimmt der Läufer je Aufruf, was pytest selbst wählen würde, sucht aber nie oberhalb der Wurzel:
|
|
|
|
- Reihenfolge der Namen und Bedingungen wie in pytest 9.1.1: `pytest.toml`, `.pytest.toml`, `pytest.ini`, `.pytest.ini` immer; `pyproject.toml` mit `[tool.pytest]` oder `[tool.pytest.ini_options]`; `tox.ini` mit `[pytest]`; `setup.cfg` mit `[tool:pytest]`; ohne Treffer die erste `pyproject.toml` überhaupt. Erst vom gemeinsamen Vorfahren aus, ohne Treffer und ohne `setup.py` dazwischen noch einmal von jedem Testordner aus.
|
|
- Gefunden: `-c <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 Kopfzeile `pytest-konfiguration (Grundlinie): …` nennt, was für die Grundlinie gilt.
|
|
- Benannte Lücken: `tox.ini` und `setup.cfg` werden zeilenweise nach ihrem Abschnitt durchsucht, nicht mit `iniconfig`; eine nicht lesbare oder kaputte Datei gilt als Treffer, damit pytest den Fehler selbst meldet; ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration.
|
|
|
|
**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit <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 diesen Text als Meldung; sie machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich „not found“ (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte).
|
|
|
|
```
|
|
|
|
### `mutanten_lauf_ENTWURF.md`, Stelle 5 von 6
|
|
|
|
Aus:
|
|
|
|
```markdown
|
|
|
|
Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testdatei, `pyproject.toml` mit `[tool.pytest.ini_options]`), schreibt je Fall eine `mutanten.toml`, startet den Läufer als Unterprozess und prüft Exit-Code und ein Meldungsstück. **Zuerst die Grundlinie:** Der saubere Fall muss Exit 0 und nur `OK` liefern, sonst misst der Rest nichts. Nach **jedem** Fall prüft der Treiber, dass jede Datei des kleinen Projekts byte-gleich mit dem Ausgangsstand ist.
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```markdown
|
|
|
|
Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testdatei, `pyproject.toml` mit `[tool.pytest.ini_options]`; für die Fälle 36 bis 40 je ein eigenes als Unterordner `k/` eines Rahmen-Ordners), schreibt je Fall eine `mutanten.toml`, startet den Läufer als Unterprozess und prüft Exit-Code und ein Meldungsstück. **Zuerst die Grundlinie:** Der saubere Fall muss Exit 0 und nur `OK` liefern, sonst misst der Rest nichts. Nach **jedem** Fall prüft der Treiber, dass jede Datei des kleinen Projekts byte-gleich mit dem Ausgangsstand ist.
|
|
|
|
```
|
|
|
|
### `mutanten_lauf_ENTWURF.md`, Stelle 6 von 6
|
|
|
|
Aus:
|
|
|
|
```markdown
|
|
| 35 | ein Mutant hängt, der Läufer bekommt nach drei Sekunden SIGTERM (eigenes `TMPDIR`), seit 2026-09-28 | Zeile `ABBRUCH: SIGTERM`, kein Prozess mit `HOME` im Fall-Tmpdir, keine `mutanten_lauf_*`-Reste, Zieldatei byte-gleich |
|
|
|
|
```
|
|
|
|
wird:
|
|
|
|
```markdown
|
|
| 35 | ein Mutant hängt, der Läufer bekommt nach drei Sekunden SIGTERM (eigenes `TMPDIR`), seit 2026-09-28 | Zeile `ABBRUCH: SIGTERM`, kein Prozess mit `HOME` im Fall-Tmpdir, keine `mutanten_lauf_*`-Reste, Zieldatei byte-gleich |
|
|
| 36 | Wurzel ohne eigene Konfiguration, darüber eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"` (der Fall der Plankritik 2 von C1), seit 2026-10-06 | Exit 0, zweimal `OK` |
|
|
| 37 | Wurzel mit eigener `pyproject.toml` (Marker `integration` abgewählt), darüber dieselbe fremde | sauberer Fall Exit 0; Test mit Marker in der Grundlinie Exit 2 und `ABGEWAEHLT`, die eigene Konfiguration gilt also |
|
|
| 38 | eigene Konfiguration mit `addopts = "--gibt-es-nicht"` | Exit 2, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` aus der pytest-Ausgabe, keine Zeile `ist FEHLT` |
|
|
| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` |
|
|
| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1, `UNKLAR`, Meldung mit `pytest-Bedienfehler (Rueckgabewert 4)` |
|
|
|
|
```
|
|
|
|
### `LIESMICH.md`, Stelle 1 von 2
|
|
|
|
Aus:
|
|
|
|
```markdown
|
|
created: 2026-09-17
|
|
updated: 2026-09-27
|
|
---
|
|
```
|
|
|
|
wird:
|
|
|
|
```markdown
|
|
created: 2026-09-17
|
|
updated: 2026-10-06
|
|
---
|
|
```
|
|
|
|
### `LIESMICH.md`, Stelle 2 von 2
|
|
|
|
Aus:
|
|
|
|
```markdown
|
|
|
|
8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel <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`).
|
|
**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“). 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.
|
|
```
|
|
|
|
|
|
## Schritt 3 — Verifikation
|
|
|
|
```bash
|
|
# Kompilieren, nur ASCII in den Python-Dateien, keine CR in allen vier Dateien
|
|
cd /d/Claude/.vault-tools/plan-kopie
|
|
python -m py_compile mutanten_lauf.py mutanten_lauf_treiber.py && echo kompiliert
|
|
LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf.py mutanten_lauf_treiber.py
|
|
for f in mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_ENTWURF.md LIESMICH.md; do printf '%s %s\n' "$f" "$(tr -cd '\r' < "$f" | wc -c)"; done
|
|
```
|
|
|
|
Erwartet: `kompiliert`, je Python-Datei die Zahl `0`, je Datei die Zahl `0`.
|
|
|
|
```powershell
|
|
# Treiber gegen den neuen Laeufer in WSL
|
|
wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot\ausgabe-neu.txt"
|
|
"exit=$LASTEXITCODE"
|
|
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot\ausgabe-neu.txt" -Pattern "FEHLER:|gerechnete"
|
|
```
|
|
|
|
Erwartet: `exit=0` und als einziger Treffer eine Zeile, die auf `gerechnete Fehler: 0` endet.
|
|
|
|
Danach `C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot` löschen.
|
|
|
|
## Bericht
|
|
|
|
Melde: je Zieldatei die Zahl der angewendeten Aus/wird-Paare, die Ausgaben aus Schritt 1 und Schritt 3 im Wortlaut, und jede Abweichung vom Plan oder getroffene Annahme. „Keine Abweichungen“ nur, wenn es keine gab.
|
|
|
|
## Fertig heißt (Koordinator)
|
|
|
|
1. Die vier Zieldateien sind byte-gleich mit der Kopie, auf die dieser Plan per `plan_materialisiere.py` angewendet wurde (`cmp`), und sonst ist unter `.vault-tools/` nichts geändert (`git -C D:/Claude status --porcelain -- .vault-tools`).
|
|
2. Treiber in WSL: Exit 0, `gerechnete Fehler: 0`, selbst gefahren.
|
|
3. Rot-Nachweis: alter Läufer → genau 36, 38, 40; die drei Varianten → 15/37/38/40, 39, 36 (vom Koordinator am 2026-10-06 gefahren, Rohdaten `02-*`).
|
|
4. Gegenprobe an allen sechs `mutanten.toml` im Vault (Headscale Paket A und B, Mattermost Phase 1, Trading Plattform Paket WH, After Install D-40 und C1), je Kopie aus `git archive HEAD` unter `/tmp`, alter gegen neuen Läufer: gleicher Exit-Code, gleiches Ergebnis je Test der Grundlinie, gleicher Status je Mutant. Dazu C1 unter dem Windows-Temp: alter Läufer Exit 2 mit `FEHLT`, neuer wie unter `/tmp`.
|
|
5. Ein `pruefer` hat Läufer und Treiber gegen diesen Plan und die pytest-Quelle gelesen; kein offener blockierender oder wichtiger Befund.
|
|
|
|
## Lauf mit echten Daten
|
|
|
|
Punkt 4 der Liste oben ist der Lauf mit echten Daten. **Auffallen müsste:** ein Plan, dessen Grundlinie mit dem neuen Läufer anders ausgeht als mit dem alten (dann bildet die Suche pytest nicht nach), eine Kopfzeile `pytest-konfiguration (Grundlinie)`, die bei Headscale oder Mattermost nicht `pytest.ini` und bei der Trading Plattform nicht `pyproject.toml` nennt, und bei After Install nicht „keine in der Wurzel“.
|
|
|
|
## Risiken
|
|
|
|
1. **Suche weicht von pytest ab.** `tox.ini` und `setup.cfg` werden zeilenweise gelesen, nicht mit `iniconfig`. Merkmal: Ein Projekt mit Konfiguration in `tox.ini` läuft ohne seine Einstellungen. Heute hat keines der sechs Projekte eine solche Datei.
|
|
2. **Neue pytest-Fassung.** Ändert eine spätere Fassung die Suchregeln, bildet der Läufer die alte nach. Merkmal: Die Kopfzeile nennt eine andere Datei als `pytest --co -q` in der Wurzel ohne `-c`.
|
|
3. **Befundtext statt `FEHLT`.** Wer die Ausgabe der Grundlinie maschinell auf `ist FEHLT` durchsucht, sieht einen Abbruch von pytest jetzt als `pytest-Bedienfehler`. `plankritik.js` wertet nur den Exit-Code aus; der bleibt 2.
|