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

@@ -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,
]