#!/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 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 ") 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())