"""Mutantenlauf auf der materialisierten Kopie: Schlaegt jeder neue Test an, wenn sein Gegenstand kaputt ist? Laeuft in WSL; Aufruf: python mutanten_py.py . Je Mutant: Datei als Bytes sichern, genau eine Stelle ersetzen, die genannten Tests fahren, Datei aus der Sicherung zurueckspielen. Erwartet ist ROT; ein gruener Mutant ist ein Befund. Zuerst die Grundlinie: Ohne Mutant muessen dieselben Tests gruen sein. """ import subprocess import sys from pathlib import Path MAT = Path(sys.argv[1]) # WSL-Pfad der Kopie mit dem angewendeten Plan PY = str(Path.home() / "venvs/trading/bin/python") MUTANTEN = [ ( "lies_lauf trennt mit der Zeilentrennung von str", "trading/dashboard/laeufe.py", ' with pfad.open("rb") as strom:\n for roh in strom:\n if not roh.strip():', ' with pfad.open("rb") as strom:\n for roh in [z.encode("utf-8") for z in strom.read().decode("utf-8").splitlines()]:\n if not roh.strip():', ["tests/test_dashboard_laeufe.py::test_die_einzelansicht_trennt_nicht_an_u2028"], ), ( "_ist_protokolldatei laesst alles durch", "trading/dashboard/laeufe.py", " return pfad.is_file() and not pfad.is_symlink()", " return True", [ "tests/test_dashboard_laeufe.py::test_ein_ordner_mit_der_endung_jsonl_ist_kein_lauf", "tests/test_dashboard_laeufe.py::test_eine_symbolische_verknuepfung_ist_kein_lauf", ], ), ( "_ist_protokolldatei folgt Verknuepfungen", "trading/dashboard/laeufe.py", " return pfad.is_file() and not pfad.is_symlink()", " return pfad.is_file()", ["tests/test_dashboard_laeufe.py::test_eine_symbolische_verknuepfung_ist_kein_lauf"], ), ( "letzte_zeilen gibt ein Bruchstueck ohne Trenner zurueck", "trading/dashboard/laeufe.py", " if not trenner:\n return []", " if not trenner:\n return [_bruchstueck]", ["tests/test_dashboard_laeufe.py::test_eine_letzte_zeile_ueber_dem_block_ohne_zeilenende_liefert_nichts"], ), ( "Eimeranfang mit festem Stundenschritt", "trading/dashboard/markt.py", " anfang_min = HANDELSSTART_MIN + nummer * aufloesung_min", " anfang_min = HANDELSSTART_MIN + nummer * 60", ["tests/test_dashboard_markt.py::test_15_und_5_minuten_teilen_den_handelstag_ohne_rest"], ), ( "Tagesachse in der Achsenzone statt in der Handelszone", "trading/dashboard/markt.py", " return zeitpunkt.tz_convert(HANDELSZONE).date().isoformat()", " return zeitpunkt.tz_convert(zone).date().isoformat()", ["tests/test_dashboard_markt.py::test_eine_tageskerze_bekommt_den_new_yorker_kalendertag_als_string"], ), ( "Tagesachse in UTC", "trading/dashboard/markt.py", " return zeitpunkt.tz_convert(HANDELSZONE).date().isoformat()", " return zeitpunkt.date().isoformat()", ["tests/test_dashboard_markt.py::test_eine_tageskerze_bekommt_den_new_yorker_kalendertag_als_string"], ), ( "Handelstag mit 391 Kerzen", "trading/dashboard/markt.py", "KERZEN_JE_HANDELSTAG = 390", "KERZEN_JE_HANDELSTAG = 391", ["tests/test_dashboard_markt.py::test_der_handelstag_hat_390_minuten_und_die_lesespanne_folgt_daraus"], ), ( "Kalenderfaktor 1,5 statt 1,6", "trading/dashboard/markt.py", "KALENDERFAKTOR_MINUTEN = 1.6", "KALENDERFAKTOR_MINUTEN = 1.5", ["tests/test_dashboard_markt.py::test_der_handelstag_hat_390_minuten_und_die_lesespanne_folgt_daraus"], ), ( "lies_kerzen liest die Metadaten ungeschuetzt", "trading/dashboard/markt.py", " meta = _meta(katalog, bar_type)\n bis = _bis(ende, meta)", " meta = katalog.meta(bar_type)\n bis = _bis(ende, meta)", ["tests/test_dashboard_markt.py::test_lies_kerzen_uebersteht_eine_unlesbare_metadatei"], ), ( "katalog() prueft nur die Wurzel", "trading/dashboard/app.py", ' if not (wurzel / "_meta").is_dir():', " if not wurzel.is_dir():", ["tests/test_dashboard_endpunkte.py::test_ein_ordner_ohne_meta_ist_kein_katalog_und_bekommt_keines"], ), ( "_ersetze laesst den alten Snapshot liegen", "trading/dashboard/verteiler.py", " if warteschlange.full():\n try:", " if warteschlange.full():\n return\n if warteschlange.full():\n try:", ["tests/test_dashboard_verteiler.py::test_eine_volle_warteschlange_bekommt_den_neuen_snapshot"], ), ( "killswitch_uebung prueft den Schalter erst nach der Konfiguration", "scripts/killswitch_uebung.py", " if not parse_args().ausfuehren:\n print(OHNE_SCHALTER, file=sys.stderr)\n return 2\n\n limits = lade_limits(\"config/risk.toml\")", " limits = lade_limits(\"config/risk.toml\")\n if not parse_args().ausfuehren:\n print(OHNE_SCHALTER, file=sys.stderr)\n return 2\n", [ "tests/test_skripte_starten.py::test_die_killswitch_uebung_ohne_schalter_endet_mit_exit_2", ], ), ] GRUND = "" def fahre(tests: list[str]) -> bool: """Exitcode allein ist kein Beleg: Ein Mutant mit Syntaxfehler waere genauso rot. Deshalb wird die erste Fehlerzeile mitgeschrieben und in der Ausgabe gezeigt.""" global GRUND ergebnis = subprocess.run( [PY, "-m", "pytest", "-q", "-p", "no:cacheprovider", "-x", "--tb=short", *tests], cwd=MAT, capture_output=True, text=True, stdin=subprocess.DEVNULL, ) zeilen = [z.strip() for z in ergebnis.stdout.splitlines() if z.startswith("E ")] GRUND = " | ".join(zeilen[:2])[:260] return ergebnis.returncode == 0 def main() -> int: befunde = 0 alle = sorted({t for m in MUTANTEN for t in m[4]}) if not fahre(alle): print("BEFUND Grundlinie: ohne Mutant sind nicht alle Tests gruen -- der Lauf misst nichts.") return 2 print(f"Grundlinie gruen ({len(alle)} Tests).") for name, datei, alt, neu, tests in MUTANTEN: pfad = MAT / datei sicherung = pfad.read_bytes() text = sicherung.decode("utf-8") anzahl = text.count(alt) if anzahl != 1: print(f"BEFUND Mutant '{name}': Stelle steht {anzahl}-mal in {datei}") befunde += 1 continue try: pfad.write_bytes(text.replace(alt, neu).encode("utf-8")) gruen = fahre(tests) finally: pfad.write_bytes(sicherung) if gruen: print(f"BEFUND Mutant '{name}' bleibt GRUEN: {tests}") befunde += 1 else: print(f"rot {name}\n {GRUND}") print(f"{befunde} Befund(e).") return 2 if befunde else 0 if __name__ == "__main__": sys.exit(main())