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

@@ -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())