#!/usr/bin/env python3 # Mutationstest.py """ Kapitel Testen: Wer testet die Tests? `test_or_kern.py` besteht aus 31 Tests, und alle sind gruen. Das ist noch kein Ergebnis. Eine Testsuite, die nie rot war, ist keine Pruefung, sondern eine Vermutung - sie koennte aus lauter `assert True` bestehen und saehe genauso aus. Dieses Programm prueft die Pruefung. Es baut nacheinander KLEINE, gezielte Fehler in `or_kern.py` ein - jeder einzelne ist ein Fehler, den man beim Programmieren tatsaechlich macht - und laesst die Testsuite darauf los. Fuer jede Mutation gibt es genau zwei moegliche Ausgaenge: GETOETET Mindestens ein Test wird rot. Die Suite haette diesen Fehler in der Praxis gefunden. UEBERLEBT Alle Tests bleiben gruen. Die Suite haette diesen Fehler DURCHGEHEN LASSEN - hier fehlt ein Test. Ueberlebende Mutationen sind das eigentliche Ergebnis: Sie zeigen die Luecken, und zwar ohne dass man sie erraten muss. Das Verfahren heisst Mutationstest und ist alt; neu ist nur, wie gut es zu Optimierungsmodellen passt. Dort kann man den Zielwert meist nicht gegen einen bekannten Sollwert pruefen - also weiss man ohne so ein Experiment nicht, ob die Ersatzpruefungen (Eigenschaften, Invarianten) ueberhaupt greifen. WICHTIG: Es wird nichts veraendert. Jede Mutation laeuft in einem eigenen temporaeren Verzeichnis mit einer Kopie von or_kern.py. Benoetigt: pytest; im selben Verzeichnis or_kern.py und test_or_kern.py """ from __future__ import annotations import os import shutil import subprocess import sys import tempfile # (Name, gesuchter Text, Ersatz) - jede Zeile ein realistischer Fehler. MUTATIONEN = [ ("Nebenbedingung umgedreht (<= wird >=)", "<= problem.kapazitaeten[ressource], name=ressource)", ">= problem.kapazitaeten[ressource], name=ressource)"), ("Maximierung wird Minimierung", "solver.Maximize(sum(menge[p.name] * p.deckungsbeitrag", "solver.Minimize(sum(menge[p.name] * p.deckungsbeitrag"), ("Vorzeichen der Dualwerte vergessen", "schattenpreise={r: float(-m) for r, m in", "schattenpreise={r: float(m) for r, m in"), ("Abnahmepruefung aufgeweicht (Toleranz 1e-6 -> 1.0)", "toleranz: float = 1e-6) -> list[str]:", "toleranz: float = 1.0) -> list[str]:"), ("Kapazitaetspruefung uebersprungen", "if ist > grenze + toleranz:", "if False:"), ("Pruefung auf doppelte Produktnamen entfernt", "if len(set(namen)) != len(namen):", "if False:"), ("Ganzzahligkeitspruefung entfernt", "if abs(wert - round(wert)) > toleranz:", "if False:"), ("Zielwertabgleich entfernt", "if abs(nachgerechnet - loesung.zielwert) > toleranz * max(1.0, abs(nachgerechnet)):", "if False:"), ("ZEITLIMIT gilt faelschlich als brauchbar", "return self in (SolverStatus.OPTIMAL, SolverStatus.ZULAESSIG)", "return self in (SolverStatus.OPTIMAL, SolverStatus.ZULAESSIG,\n" " SolverStatus.ZEITLIMIT)"), ("Ressourcenreihenfolge nicht mehr stabil", "return sorted(self.kapazitaeten)", "return list(self.kapazitaeten)"), ] def fuehre_suite_aus(verzeichnis: str) -> tuple[int, int, str]: """Laesst pytest im Verzeichnis laufen; liefert (bestanden, fehlgeschlagen).""" ergebnis = subprocess.run( [sys.executable, "-m", "pytest", "test_or_kern.py", "-q", "--tb=no", "-p", "no:cacheprovider"], cwd=verzeichnis, capture_output=True, text=True, timeout=600) letzte = [z for z in ergebnis.stdout.strip().splitlines() if z.strip()] zeile = letzte[-1] if letzte else "" bestanden = fehlgeschlagen = 0 for teil in zeile.replace("=", " ").split(","): for wort in teil.split(): if wort.isdigit(): zahl = int(wort) elif wort.startswith("passed"): bestanden = zahl elif wort.startswith("failed") or wort.startswith("error"): fehlgeschlagen += zahl return bestanden, fehlgeschlagen, zeile def mutiere_und_pruefe(quelle: str, ziel: str, suche: str, ersatz: str) -> tuple[int, int, str] | None: """Legt eine mutierte Kopie an und laesst die Suite darauf laufen.""" with tempfile.TemporaryDirectory() as verzeichnis: text = open(quelle, encoding="utf-8").read() if text.count(suche) != 1: return None # Muster passt nicht (mehr) open(os.path.join(verzeichnis, "or_kern.py"), "w", encoding="utf-8").write(text.replace(suche, ersatz)) shutil.copy(ziel, verzeichnis) return fuehre_suite_aus(verzeichnis) if __name__ == "__main__": hier = os.path.dirname(os.path.abspath(__file__)) quelle = os.path.join(hier, "or_kern.py") suite = os.path.join(hier, "test_or_kern.py") for pfad in (quelle, suite): if not os.path.exists(pfad): raise SystemExit(f"Nicht gefunden: {pfad}\n" f"Beide Dateien muessen im selben Verzeichnis liegen.") print("=" * 84) print(" MUTATIONSTEST: WUERDE DIE SUITE DIESE FEHLER FINDEN?") print("=" * 84) with tempfile.TemporaryDirectory() as v: shutil.copy(quelle, v) shutil.copy(suite, v) bestanden, fehlgeschlagen, _ = fuehre_suite_aus(v) print(f"Ausgangslage: {bestanden} Tests, {fehlgeschlagen} rot.\n") if fehlgeschlagen: raise SystemExit("Die Suite ist schon ohne Mutation rot - erst das reparieren.") print(f"{'eingebauter Fehler':<48} {'rote Tests':>11} {'Urteil':>12}") print("-" * 84) getoetet, ueberlebt = [], [] for name, suche, ersatz in MUTATIONEN: ergebnis = mutiere_und_pruefe(quelle, suite, suche, ersatz) if ergebnis is None: print(f"{name:<48} {'-':>11} {'nicht anwendbar':>12}") continue _, rot, _ = ergebnis urteil = "GETOETET" if rot else "UEBERLEBT" (getoetet if rot else ueberlebt).append(name) print(f"{name:<48} {rot:>11} {urteil:>12}") print("-" * 84) quote = len(getoetet) / max(1, len(getoetet) + len(ueberlebt)) * 100 print(f"Getoetet: {len(getoetet)} von {len(getoetet) + len(ueberlebt)} " f"({quote:.0f} %)") print("\n" + "=" * 84) print(" WAS DAS ERGEBNIS BEDEUTET") print("=" * 84) if ueberlebt: print("Diese Fehler haette die Suite DURCHGEHEN LASSEN:\n") for name in ueberlebt: print(f" * {name}") print("\nJeder ueberlebende Eintrag ist eine Testluecke - und zwar eine") print("gefundene, keine vermutete. Das ist der ganze Zweck des Verfahrens:") print("Es sagt einem, welchen Test man als naechstes schreiben sollte,") print("statt dass man raten muss.") else: print("Alle eingebauten Fehler wurden gefunden. Das heisst nicht, dass die") print("Suite vollstaendig ist - nur, dass sie diese zehn Fehler faengt.") print() print("Die Quote selbst ist keine Kennzahl fuer ein Dashboard. Zehn von Hand") print("gewaehlte Mutationen sind keine Stichprobe aus der Menge aller") print("moeglichen Fehler. Was zaehlt, ist die LISTE der Ueberlebenden.") print("=" * 84)