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