"""Mutantenlauf gegen quorum_werkzeug.py: je Mutant genau eine Ersetzung in einer Kopie, dann faelle_quorum_werkzeug.py. Jede Ersetzung muss genau einmal vorkommen, sonst bricht der Lauf mit Exit 2 ab (veralteter Mutant). Aufruf: python mutanten_quorum_werkzeug.py Exit 0, wenn jeder Mutant gefangen wird, 1 bei einem Ueberlebenden, 2 bei einem veralteten Muster. """ import shutil import subprocess import sys import tempfile from pathlib import Path HIER = Path(__file__).resolve().parent MUTANTEN = [ ("Ausschluss docs/quorum fehlt", 'AUSSCHLUSS = ":(exclude)docs/quorum"', 'AUSSCHLUSS = "."'), ("erster statt hoechster Bericht", 'return max(berichte, key=lambda b: b["nummer"])', "return berichte[0]"), ("letzter statt hoechster Bericht", 'return max(berichte, key=lambda b: b["nummer"])', "return berichte[-1]"), ("unsauberer Arbeitsbaum erlaubt", " if unsauber:\n", " if False:\n"), ("unversionierte Dateien zusammengefasst", '"--porcelain", "--untracked-files=all", "--"', '"--porcelain", "--"'), ("erstes Quorum ohne basis nimmt HEAD", ' raise Befund("erstes Quorum in diesem Projekt: --basis angeben, etwa den Commit der letzten abgenommenen Version")', " basis = stand"), ("leer ohne Vorbericht erlaubt", " if not dateien and not vorher:\n", " if False:\n"), ("zwei gleiche Nummern erlaubt", " if doppelt:\n", " if False:\n"), ("nummer nicht hochgezaehlt", '"nummer": (vorher["nummer"] if vorher else 0) + 1', '"nummer": (vorher["nummer"] if vorher else 0)'), ("Berichtspfad ueberschrieben", " while pfad.exists():\n", " while False:\n"), ("CHANGELOG mit entfernten Zeilen", 'if z.startswith("+") and not z.startswith("+++")', 'if z.startswith(("+", "-")) and not z.startswith(("+++", "---"))'), ("Suite-Datei uebergangen", " suite_kurz, suite_zeilen = lies_suite(a.suite_datei)", ' suite_kurz, suite_zeilen = "ohne", []'), ("Kontext ungeprueft", " kontext = pruefe_kontext(a.kontext or [])", " kontext = [str(k) for k in (a.kontext or [])]"), ("geaenderte Datei im Satz uebersehen", " elif alt[rel] != summe:", " elif False:"), ("entfernte Datei uebersehen", ' dateien.append({"pfad": rel, "art": "entfernt"})', " pass"), ("Pfad ausserhalb des Ordners erlaubt", ' if rel.anchor or ".." in rel.parts:', " if False:"), ("Lesemarke im Codex-Auftrag", 'wurzel=teil["wurzel"].as_posix(), anfang=', 'wurzel=teil["wurzel"].as_posix() + " " + marke, anfang='), ("usage limit nur klein geschrieben", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.M)'), ("usage limit ueberall im Log gezaehlt", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', 'USAGE_LIMIT = re.compile(r"usage limit", re.I)'), ("usage limit ohne Zeitstempel am Zeilenanfang", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', 'USAGE_LIMIT = re.compile(r"Codex error: .*usage limit", re.I | re.M)'), ("usage limit nur in der ersten Zeile", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I)'), ("erster statt letzter Block", "roh.rfind(MARKE_ANFANG)", "roh.find(MARKE_ANFANG)"), ("Schwere ungeprueft", ' if felder["schwere"] not in SCHWEREN:\n', " if False:\n"), ("Befund ohne beleg-Feld gilt", " leer = [k for k, v in felder.items() if not isinstance(v, str)]", " leer = []"), ("Suite: erste statt letzte EXIT-Zeile", "exits[-1][len(", "exits[0][len("), ("Suite: 81 statt 80 Zeilen", "SUITE_ZEILEN = 80", "SUITE_ZEILEN = 81"), ("Suite: Leerzeile am Ende mitgezaehlt", ' if zeilen[-1] == "":\n', " if False:\n"), ("Umbenennung als geaendert", '"diff", "--name-status", "--no-renames", basis', '"diff", "--name-status", basis'), ("Hinweis basis weicht fehlt", ' if vorher and vorher["stand"] != basis:\n', " if False:\n"), ("Hinweis kein Vorfahr fehlt", ' if git(projekt, "merge-base", "--is-ancestor", basis, stand, pruefen=False).returncode != 0:\n', " if False:\n"), ("Stand-Liste unsortiert in der Pruefsumme", r'"\n".join(sorted(stand_liste))', r'"\n".join(stand_liste)'), ("dateien in der Zeile", " zeile = {k: ergebnis[k] for k in PAKET_ZEILE}", " zeile = dict(ergebnis)"), ("paket_summe ueber paket.md", 'hashlib.sha256((ziel / "paket.json").read_bytes())', 'hashlib.sha256((ziel / "paket.md").read_bytes())'), ("ablegen: paket.json unlesbar ohne Befund", 'raise Befund(f"paket.json nicht lesbar in {ordner.as_posix()} ({type(fehler).__name__})")', "raise"), ("ablegen: Ziel ueberschrieben", " if ziel.exists():\n", " if False:\n"), ("ablegen: Frontmatter ungeprueft", ' if zeilen is None:\n raise Befund("bericht ohne Frontmatter")\n', ' if False:\n raise Befund("bericht ohne Frontmatter")\n'), ("ablegen: nummer ungeprueft", ' if werte.get("nummer") != str(daten["nummer"]):\n', " if False:\n"), ("ablegen: basis ungeprueft", ' if werte.get("basis") != str(daten["basis"]):\n', " if False:\n"), ("ablegen: stand PAKET im Rumpf mitgezaehlt", ' treffer = [i for i, z in enumerate(zeilen) if z == "stand: PAKET"]', r' treffer = [i - 1 for i, z in enumerate(text.split("\n")) if z == "stand: PAKET"]'), ("ablegen: stand PAKET zweimal erlaubt", " if len(treffer) != 1:\n", " if not treffer:\n"), ("ablegen: fehlendes stand PAKET erlaubt", " if len(treffer) != 1:\n", " if len(treffer) > 1:\n"), ("ablegen: Kundendokument bekommt die Pruefsumme", ' if daten["art"] == "doku":\n', " if False:\n"), ("ablegen: Ordner nicht angelegt", " ziel.parent.mkdir(parents=True, exist_ok=True)\n", ""), ("git ohne GIT_OPTIONAL_LOCKS=0", "*argumente], env=umgebung,", "*argumente],"), ("ablegen: --summe ungeprueft", " if gemessen != a.summe:\n", " if False:\n"), ("ablegen: --summe ueber paket.md", 'gemessen = hashlib.sha256((ordner / "paket.json").read_bytes())', 'gemessen = hashlib.sha256((ordner / "paket.md").read_bytes())'), ] # Mutanten, die bei heutigem git kein Verhalten aendern koennten, stehen hier mit Grund; ein # Ueberlebender ausserhalb dieser Liste ist ein Befund. WIRKUNGSLOS = {} def main() -> int: ueberlebt, veraltet = [], [] for name, alt, neu in MUTANTEN: with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp: ziel = Path(tmp) / "q" ziel.mkdir() for datei in ("quorum_werkzeug.py", "faelle_quorum_werkzeug.py"): shutil.copy2(HIER / datei, ziel / datei) pfad = ziel / "quorum_werkzeug.py" text = pfad.read_text(encoding="utf-8") if text.count(alt) != 1: veraltet.append(name) print(f"VERALTET {name} (Muster {text.count(alt)}-mal)") continue pfad.write_text(text.replace(alt, neu), encoding="utf-8", newline="\n") lauf = subprocess.run([sys.executable, str(ziel / "faelle_quorum_werkzeug.py")], capture_output=True, text=True, encoding="utf-8", errors="replace") rot = [z for z in lauf.stdout.splitlines() if z.startswith("FEHLER")] if lauf.returncode == 0: grund = WIRKUNGSLOS.get(name) if grund: print(f"WIRKUNGSLOS {name} ({grund})") else: ueberlebt.append(name) print(f"UEBERLEBT {name}") else: print(f"GEFANGEN {name} ({rot[0][7:90] if rot else 'Absturz'})") print(f"{len(MUTANTEN)} Mutanten, {len(ueberlebt)} ueberlebt, {len(veraltet)} veraltet") return 2 if veraltet else 1 if ueberlebt else 0 if __name__ == "__main__": sys.stdout.reconfigure(errors="replace") sys.exit(main())