Trading-Plattform-Pruefung: lauf.log und Ergebnisdatei halten die Notizzeile nicht auf (Paket NR, Task 743)

abendkontrolle.py: logge wirft nicht mehr; scheiterte lauf.log in einem Lauf, steht
am Ende eine Zeile in der Notiz von heute und der Lauf endet mit Exit 1. Laesst sich
die Ergebnisdatei eines Tages nicht schreiben, bekommt seine Notizzeile eine
Unterzeile mit dem Typ, die folgenden Tage laufen weiter, Exit 1. Treiber
abendkontrolle_dockerhost_angriffe.py: sieben neue Faelle (46), Mutanten E743-01
bis E743-13 (70, keiner ueberlebt). Kritische Pruefung NT, Befund 3, und Plankritik NR.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0126RDfXiECscXipzf67uypJ
This commit is contained in:
Pascal MichelandClaude Opus 5.5 committed 2026-10-12 22:13:36 +02:00
1 parent f73f84e3ac
commit 94f6dfeeeb
2 files changed
+403 -3

No files matched your search

@@ -39,6 +39,12 @@ Seit Paket NT (Quorum 4): Endet die Frage nach einer Summe oder die Bestaetigung
im Zeitlimit, ist der Docker-Host nicht erreichbar, und die Sicherung hoert sofort auf: eine Zeile in der
Notiz von heute, Exit 1. Bricht nur das Archiv so ab, scheitert allein diese Instanz. --tag ausser der
Form JJJJ-MM-TT oder ohne gueltiges Datum endet gleich nach dem Zerlegen mit Exit 2, ohne Notiz und ssh.
Seit Paket NR (kritische Pruefung NT, Befund 3): Laesst sich lauf.log nicht schreiben (Sperre, Rechte, volle
Platte), haelt das weder Ergebnisdatei noch Notizzeile noch Sicherung auf. Der Lauf schreibt am Ende eine
Zeile "Abendkontrolle lauf.log (automatisch): **nicht schreibbar** (<Typ>)" in die Notiz von heute und endet
mit Exit 1. Seit der Plankritik NR: Laesst sich die Ergebnisdatei eines Tages nicht schreiben, bekommt seine
Zeile in der Notiz eine Unterzeile mit dem Typ der Ausnahme, die folgenden Tage des Teils laufen weiter, und
der Lauf endet mit Exit 1.
Aufruf von Hand: python abendkontrolle.py [--tag JJJJ-MM-TT] [--vault D:\\Claude] [--ohne-wsl <json-datei>]
[--quelle wsl|dockerhost] [--quelle-ibkr wsl|dockerhost] [--sicherung-ziel <ordner>]
"""
@@ -583,10 +589,27 @@ def schreibe_notiz(vault: Path, tag: str, zeile: str) -> None:
os.replace(neu, notiz)
# Seit Paket NR (kritische Pruefung NT, Befund 3): der Typ der letzten Ausnahme, an der lauf.log in diesem
# Lauf scheiterte; None, solange es ging. main setzt ihn zu Beginn zurueck, nur logge setzt ihn.
LOG_FEHLER: str | None = None
LOG_KOPF = "Abendkontrolle lauf.log (automatisch)"
def logge(ablage: Path, text: str) -> None:
"""Eine Zeile in lauf.log. Wirft seit Paket NR nicht mehr.
Vorher warf ein gesperrtes oder nicht schreibbares lauf.log in main vor fahre_teil, in fahre_teil vor
der Zeile in der Notiz und in der Sicherung vor der Zeile zum unerreichbaren Host; uebrig blieb nur
Exit 1. Jetzt merkt sich logge den Typ der Ausnahme in LOG_FEHLER, und main schreibt dazu am Ende
eine Zeile in die Notiz von heute.
"""
global LOG_FEHLER
try:
ablage.mkdir(parents=True, exist_ok=True)
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as datei:
datei.write(f"{datetime.now().isoformat(timespec='seconds')} {text}\n")
except Exception as ausnahme:
LOG_FEHLER = type(ausnahme).__name__
def main(argv=None) -> int:
@@ -616,6 +639,10 @@ def main(argv=None) -> int:
if argumente.tag is not None and not tag_gueltig(argumente.tag):
print(f"abendkontrolle: --tag {argumente.tag!a} ist kein Datum JJJJ-MM-TT", file=sys.stderr)
return 2
# Seit Paket NR: Jeder Lauf beginnt ohne Fehler an lauf.log. Die Angriffsfaelle rufen main mehrmals im
# selben Prozess; ohne das Zuruecksetzen truege ein Lauf den Fehler des vorigen weiter.
global LOG_FEHLER
LOG_FEHLER = None
# Seit Paket E9: Das Zeitbudget der Sicherung zaehlt ab hier, also mit der Zeit fuer die
# Ergebnisse (bis zu zwoelf Abfragen mit je SSH_TIMEOUT_S; Plankritik Runde 2, Befund 1).
frist = monotonic() + SICHERUNG_BUDGET_S
@@ -634,8 +661,9 @@ def main(argv=None) -> int:
else:
tage = faellige_tage(jetzt, ablage)
vorsatz = "" if teil == "alpaca" else f"{teil} "
# Scheitert ein Teil ausserhalb seiner eigenen Fehlerbehandlung (Ergebnisdatei nicht
# schreibbar, lauf.log gesperrt), laeuft der naechste trotzdem (Pruefung IB2).
# Scheitert ein Teil ausserhalb seiner eigenen Fehlerbehandlung, laeuft der naechste trotzdem
# (Pruefung IB2). Seit Paket NR werfen weder ein gesperrtes lauf.log (logge) noch eine nicht
# schreibbare Ergebnisdatei (fahre_teil).
try:
logge(ablage, f"{vorsatz}start, faellig: {tage}")
# Seit Paket DH: die Quelle je Teil; die IBKR-Instanz zieht an einem eigenen Abend um.
@@ -662,6 +690,19 @@ def main(argv=None) -> int:
logge(ablage, f"sicherung abgebrochen: {type(ausnahme).__name__}: {ausnahme}")
except Exception:
pass
# Seit Paket NR (kritische Pruefung NT, Befund 3): Scheiterte lauf.log in diesem Lauf, einmal eine Zeile
# in der Notiz von heute mit dem Typ der Ausnahme, ohne Pfad und ohne Inhalt, und Exit 1. Scheitert
# auch die Notiz, bleibt es bei Exit 1.
if LOG_FEHLER is not None:
code = 1
log_zeile = (
f"- {jetzt:%H:%M} Uhr — {LOG_KOPF}: **nicht schreibbar** ({LOG_FEHLER}) #needs-review. Mindestens "
"ein Eintrag dieses Laufs fehlt dort; Ergebnisdateien, Notizzeilen und Sicherung hängen nicht davon ab."
)
try:
schreibe_notiz(vault, jetzt.date().isoformat(), log_zeile)
except Exception:
pass
return code
@@ -703,15 +744,30 @@ def fahre_teil(teil: str, tage: list[str], jetzt: datetime, vault: Path, ablage:
logge(ablage, f"{vorsatz}{tag}: auch die Notiz scheiterte {zweite!r}")
continue
# Ein Abbruch im WSL-Teil (Exit 2) wird wiederholt; alles andere gilt als gelaufen.
# Seit Paket NR (Plankritik NR): Liess sich die Ergebnisdatei nicht schreiben, flog die Ausnahme vorher
# aus fahre_teil, und es fehlten die Zeile dieses Tages und die aller folgenden Tage des Teils. Jetzt
# bekommt die Zeile eine Unterzeile mit dem Typ der Ausnahme, die folgenden Tage laufen weiter, Exit 1.
# Ob der naechste Lauf den Tag erneut prueft, entscheidet faellige_tage: nur im Fenster und nur, wenn
# unter dem Namen der Datei nichts liegt, auch keine halb geschriebene Datei.
zusatz = ""
if ergebnis.get("exit") != 2:
try:
(ablage / f"{tag}{endung}.json").write_text(
json.dumps(ergebnis, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n"
)
except Exception as ausnahme:
code = 1
logge(ablage, f"{vorsatz}{tag}: Ergebnisdatei nicht geschrieben {ausnahme!r}")
zusatz = (
f"\n - Ergebnisdatei nicht geschrieben ({type(ausnahme).__name__}) #needs-review. Der nächste "
"Lauf prüft den Tag erneut, wenn er dann noch im Nachholfenster ist und unter dem Namen der "
"Datei nichts liegt, auch keine halb geschriebene."
)
else:
code = 1
logge(ablage, f"{vorsatz}{tag}: {len(ergebnis['befunde'])} Befund(e), exit {ergebnis.get('exit')}")
try:
schreibe_notiz(vault, tag, zeile_von(ergebnis, uhrzeit))
schreibe_notiz(vault, tag, zeile_von(ergebnis, uhrzeit) + zusatz)
except Exception as ausnahme:
code = 1
logge(ablage, f"{vorsatz}{tag}: Notiz nicht geschrieben {ausnahme!r}")
@@ -18,6 +18,10 @@ Nichts landet unter D:\\Trading-Sicherung.
Seit Paket NT (Quorum 4): ANGRIFF_HOST_WEG=<n> laesst die Attrappe jeden Aufruf nach den ersten n mit
Exit 255 enden wie ssh ohne Host; ANGRIFF_SICHERUNG=langsam haelt jeden Sicherungsbefehl 5 s auf,
ANGRIFF_SICHERUNG=archiv_unlesbar endet beim Archiv wie abendkontrolle-lesen.sh ohne Leserecht.
Seit Paket NR (kritische Pruefung NT, Befund 3): lauf_log_sperren legt an der Stelle von lauf.log einen
Ordner an, und vier Faelle zeigen, dass Ergebnis, Notizzeilen und Sicherung nicht daran haengen. Seit der
Plankritik NR drei weitere: ein Fehler in logge, der kein OSError ist, eine Ablage, die sich nicht anlegen
laesst, und eine Ergebnisdatei, die sich nicht schreiben laesst.
Aufruf: python abendkontrolle_dockerhost_angriffe.py [--mutanten]
Exit 0 ohne Abweichung, 2 mit Abweichung. --mutanten faehrt MUTANTEN auf Kopien von
abendkontrolle.py und MUTANTEN_AUFGABE auf Kopien von abendkontrolle_einrichten.ps1: Jeder genannte
@@ -1191,6 +1195,288 @@ def kein_kommentar_beginnt_mit_s3():
assert [z for z in zeilen if z.startswith("# (S3)")] == [], "Kommentar beginnt mit (S3)."
# --- Seit Paket NR (kritische Pruefung NT, Befund 3): lauf.log nicht schreibbar ---------------------
LOG_KOPF = "Abendkontrolle lauf.log (automatisch)"
def lauf_log_sperren(modul, wurzel):
"""Legt an der Stelle von lauf.log einen Ordner an; liefert die Ablage und den Typ der Ausnahme.
Oeffnen zum Anhaengen scheitert dann wie bei einer Sperre oder fehlenden Rechten, und die Ablage
daneben bleibt schreibbar. Den Typ misst der Fall hier selbst, statt ihn je Betriebssystem zu raten.
"""
ablage = wurzel / "vault" / modul.ABLAGE_TEIL
gesperrt = ablage / "lauf.log"
gesperrt.mkdir(parents=True)
try:
with gesperrt.open("a", encoding="utf-8"):
pass
except OSError as fehler:
return ablage, type(fehler).__name__
raise AssertionError("lauf.log als Ordner liess sich zum Anhaengen oeffnen")
def log_zeile(uhrzeit, typ):
"""Die eine Zeile, die ein Lauf mit nicht schreibbarem lauf.log in die Notiz von heute schreibt."""
return (
f"- {uhrzeit} Uhr — {LOG_KOPF}: **nicht schreibbar** ({typ}) #needs-review. Mindestens ein Eintrag "
"dieses Laufs fehlt dort; Ergebnisdateien, Notizzeilen und Sicherung hängen nicht davon ab."
)
def log_zeilen(notiz):
"""Die Zeilen der Notizen, die lauf.log nennen."""
return [zeile for zeile in notiz.splitlines() if LOG_KOPF in zeile]
@fall
def ein_gesperrtes_lauf_log_haelt_ergebnis_notiz_und_sicherung_nicht_auf():
# Kritische Pruefung NT, Befund 3: Vorher warf logge schon beim Start jedes Teils, beide Teile fielen
# aus, die Sicherung brach nach dem ersten Archiv ab, und in der Notiz stand nichts; uebrig blieb
# Exit 1. Jetzt laufen Ergebnis, Zeilen und Sicherung beider Teile (erster und letzter Teil), und
# genau eine Zeile von heute nennt lauf.log, mit Exit 1. Ein zweiter Lauf mit demselben Modul und
# schreibbarem lauf.log endet mit 0 und ohne neue Zeile: Der Fehler gilt je Lauf.
with tempfile.TemporaryDirectory() as ordner:
wurzel = Path(ordner)
host = host_ablage(wurzel)
modul = lade()
ablage, typ = lauf_log_sperren(modul, wurzel)
code, notiz, _, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", modul=modul)
ergebnisse = sorted(p.name for p in ablage.glob("*.json"))
assert ergebnisse == [f"{TAG}-ibkr.json", f"{TAG}.json"], "Ergebnisdateien fehlen"
assert "Abendkontrolle Trading Plattform (automatisch): ohne Befund" in notiz, notiz
assert "Abendkontrolle IBKR (automatisch): ohne Befund" in notiz, notiz
ziel = wurzel / "ziel" / TAG
assert namen(ziel) == ["alpaca.tar", "alpaca.tar.sha256", "ibkr.tar", "ibkr.tar.sha256"], namen(ziel)
assert namen(host / "bestaetigt") == [f"{TAG}-alpaca", f"{TAG}-ibkr"], namen(host / "bestaetigt")
assert "**gescheitert**" not in einzige_zeile(notiz), notiz
assert log_zeilen(notiz) == [log_zeile("23:30", typ)], notiz
assert code == 1, code
assert namen(ablage / "lauf.log") == [], namen(ablage / "lauf.log")
# Zweiter Lauf mit demselben Modul; lauf.log ist wieder schreibbar.
(ablage / "lauf.log").rmdir()
code, notiz, _, _ = sicherungslauf(wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:45", modul=modul)
assert code == 0, (code, notiz)
assert len(log_zeilen(notiz)) == 1, notiz
assert (ablage / "lauf.log").is_file(), "lauf.log nach dem zweiten Lauf keine Datei"
@fall
def ein_gesperrtes_lauf_log_verschluckt_die_zeile_zum_unerreichbaren_host_nicht():
# Kritische Pruefung NT, Befund 3: Im Zweig "Docker-Host nicht erreichbar" stand logge vor der Zeile in
# der Notiz; warf es, fehlte die Zeile, und uebrig blieb Exit 1. Jetzt stehen in der Notiz von heute
# die Zeile zum Host und die zu lauf.log, je genau einmal.
with tempfile.TemporaryDirectory() as ordner:
wurzel = Path(ordner)
modul = lade()
ergebnisse_liegen(modul, wurzel)
_, typ = lauf_log_sperren(modul, wurzel)
code, notiz, befehle, _ = sicherungslauf(
wurzel, "--jetzt", "2026-10-12T23:30", modul=modul, erster_tag=date(2026, 10, 12), host_weg="0"
)
assert len(sicherungszeilen(notiz)) == 1, "Zeile zum unerreichbaren Host fehlt"
assert einzige_zeile(notiz) == (
f"- 23:30 Uhr — {KOPF}: **gescheitert** #needs-review. Docker-Host nicht erreichbar "
f"(sicherung-summe 2026-10-12-alpaca: Exit 255; {HOST_WEG}), Sicherung abgebrochen. "
"Der nächste Lauf versucht es erneut."
), notiz
assert log_zeilen(notiz) == [log_zeile("23:30", typ)], notiz
assert namen(wurzel / "vault" / "Inbox") == ["2026-10-12.md"], namen(wurzel / "vault" / "Inbox")
assert befehle == ["sicherung-summe 2026-10-12-alpaca"], befehle
assert code == 1, code
@fall
def ein_gesperrtes_lauf_log_verschluckt_die_zeilen_gescheiterter_teile_nicht():
# Kritische Pruefung NT, Befund 3: Scheiterte ein Teil, stand logge vor seiner Zeile in der Notiz. Hier
# liefert der Host fuer beide Teile kein Ergebnis (erster und letzter Teil), und --tag ist nicht heute:
# Die Zeilen der Teile stehen in der Notiz des Tags, die Zeile zu lauf.log nur in der von heute.
with tempfile.TemporaryDirectory() as ordner:
wurzel = Path(ordner)
modul = lade()
_, typ = lauf_log_sperren(modul, wurzel)
def kein_ergebnis(tag, teil="alpaca"):
raise RuntimeError(f"auf dem Docker-Host liegt fuer {tag} ({teil}) noch kein Ergebnis")
modul.fahre_dockerhost = kein_ergebnis
code, _, befehle, _ = sicherungslauf(
wurzel, "--tag", TAG, "--jetzt", "2026-10-06T23:30", modul=modul, erster_tag=None
)
inbox = wurzel / "vault" / "Inbox"
assert namen(inbox) == [f"{TAG}.md", "2026-10-06.md"], f"Notizen {namen(inbox)}"
tagnotiz = (inbox / f"{TAG}.md").read_text(encoding="utf-8")
heute = (inbox / "2026-10-06.md").read_text(encoding="utf-8")
for kopf in ("Abendkontrolle Trading Plattform (automatisch)", "Abendkontrolle IBKR (automatisch)"):
assert f"- 2026-10-06 23:30 Uhr — {kopf}: **gescheitert** (RuntimeError: " in tagnotiz, tagnotiz
assert log_zeilen(tagnotiz) == [], tagnotiz
assert log_zeilen(heute) == [log_zeile("23:30", typ)], heute
assert befehle == [], befehle
assert code == 1, code
@fall
def eine_gesperrte_notiz_neben_gesperrtem_lauf_log_bricht_main_nicht_ab():
# Paket NR: Laesst sich neben lauf.log auch die Notiz von heute nicht schreiben (ihre Nachbardatei
# ist ein Ordner), endet main ohne Ausnahme mit Exit 1, und die Ergebnisdateien beider Teile liegen.
with tempfile.TemporaryDirectory() as ordner:
wurzel = Path(ordner)
modul = lade()
ablage, _ = lauf_log_sperren(modul, wurzel)
(wurzel / "vault" / "Inbox" / f"{TAG}.md.abendkontrolle.tmp").mkdir(parents=True)
code, _, befehle, _ = sicherungslauf(
wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", modul=modul, erster_tag=None
)
ergebnisse = sorted(p.name for p in ablage.glob("*.json"))
assert ergebnisse == [f"{TAG}-ibkr.json", f"{TAG}.json"], "Ergebnisdateien fehlen"
assert namen(wurzel / "vault" / "Inbox") == [f"{TAG}.md.abendkontrolle.tmp"], namen(wurzel / "vault" / "Inbox")
assert befehle == [], befehle
assert code == 1, code
# --- Seit der Plankritik NR: kein OSError in logge, Ablage nicht anlegbar, Ergebnisdatei nicht schreibbar ---
def typ_beim_schreiben(pfad):
"""Der Typ der Ausnahme, wenn pfad sich nicht als Datei schreiben laesst; der Fall misst ihn selbst."""
try:
pfad.write_text("{}", encoding="utf-8")
except OSError as fehler:
return type(fehler).__name__
raise AssertionError("an dieser Stelle liess sich eine Datei schreiben")
def ergebnis_zusatz(typ):
"""Die Unterzeile unter der Zeile eines Tages, dessen Ergebnisdatei sich nicht schreiben liess."""
return (
f" - Ergebnisdatei nicht geschrieben ({typ}) #needs-review. Der nächste Lauf prüft den Tag erneut, "
"wenn er dann noch im Nachholfenster ist und unter dem Namen der Datei nichts liegt, auch keine halb "
"geschriebene."
)
def zeile_mit_folge(notiz, anfang):
"""Die eine Zeile der Notizen, die mit anfang beginnt, und die Zeile danach ("" am Ende)."""
zeilen = notiz.splitlines()
treffer = [i for i, zeile in enumerate(zeilen) if zeile.startswith(anfang)]
assert len(treffer) == 1, (anfang, notiz)
i = treffer[0]
return zeilen[i], (zeilen[i + 1] if i + 1 < len(zeilen) else "")
@fall
def ein_fehler_in_logge_der_kein_oserror_ist_haelt_den_lauf_nicht_auf():
# Plankritik NR: logge faengt jede Ausnahme, nicht nur OSError (Entscheidungsliste Task 743, Nr. 1).
# Hier wirft die Uhr in logge einen RuntimeError; die Attrappe gilt nur im geladenen Modul, und --jetzt
# liest dieselbe Klasse heil. Ergebnis und Zeilen beider Teile entstehen, die Zeile zu lauf.log nennt
# den Typ, Exit 1.
with tempfile.TemporaryDirectory() as ordner:
wurzel = Path(ordner)
modul = lade()
class KaputteUhr(datetime):
@classmethod
def now(cls, tz=None):
raise RuntimeError("Uhr der Attrappe")
modul.datetime = KaputteUhr
code, notiz, befehle, _ = sicherungslauf(
wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", modul=modul, erster_tag=None
)
ablage = wurzel / "vault" / modul.ABLAGE_TEIL
ergebnisse = sorted(p.name for p in ablage.glob("*.json"))
assert ergebnisse == [f"{TAG}-ibkr.json", f"{TAG}.json"], "Ergebnisdateien fehlen"
assert "Abendkontrolle Trading Plattform (automatisch): ohne Befund" in notiz, notiz
assert "Abendkontrolle IBKR (automatisch): ohne Befund" in notiz, notiz
assert log_zeilen(notiz) == [log_zeile("23:30", "RuntimeError")], notiz
assert befehle == [], befehle
assert code == 1, code
@fall
def eine_ablage_die_sich_nicht_anlegen_laesst_haelt_die_zeilen_nicht_auf():
# Plankritik NR: logge legt die Ablage im try an (Entscheidungsliste Task 743, Nr. 1). Hier liegt an
# ihrer Stelle eine Datei; weder lauf.log noch die Ergebnisdateien lassen sich schreiben. Mit --tag
# liest der Lauf die Ablage vorher nicht (faellige_tage). Die Zeilen beider Teile stehen mit der
# Unterzeile zur Ergebnisdatei in der Notiz, dazu die Zeile zu lauf.log, Exit 1.
with tempfile.TemporaryDirectory() as ordner:
wurzel = Path(ordner)
modul = lade()
ablage = wurzel / "vault" / modul.ABLAGE_TEIL
ablage.parent.mkdir(parents=True)
ablage.write_bytes(b"keine Ablage\n")
try:
ablage.mkdir(parents=True, exist_ok=True)
except OSError as fehler:
typ_log = type(fehler).__name__
else:
raise AssertionError("die Ablage liess sich anlegen")
typ_ergebnis = typ_beim_schreiben(ablage / f"{TAG}.json")
code, notiz, befehle, _ = sicherungslauf(
wurzel, "--tag", TAG, "--jetzt", "2026-10-05T23:30", modul=modul, erster_tag=None
)
inbox = wurzel / "vault" / "Inbox"
assert namen(inbox) == [f"{TAG}.md"], f"Notizen {namen(inbox)}"
for teil in ("alpaca", "ibkr"):
zeile, folge = zeile_mit_folge(notiz, f"- 23:30 Uhr — {modul.KOEPFE[teil]}: ohne Befund.")
assert folge == ergebnis_zusatz(typ_ergebnis), (zeile, folge)
assert log_zeilen(notiz) == [log_zeile("23:30", typ_log)], notiz
assert ablage.read_bytes() == b"keine Ablage\n"
assert befehle == [], befehle
assert code == 1, code
@fall
def eine_nicht_schreibbare_ergebnisdatei_haelt_weder_zeile_noch_folgende_tage_auf():
# Plankritik NR: Die Ergebnisdatei stand in fahre_teil ohne try vor der Zeile in der Notiz. Liess sie
# sich nicht schreiben, flog die Ausnahme aus fahre_teil: Es fehlten die Zeile dieses Tages und die
# aller folgenden Tage des Teils, uebrig blieben "abgebrochen" in lauf.log und Exit 1. Hier liegt an
# ihrer Stelle ein Ordner, bei alpaca am ersten, bei ibkr am letzten faelligen Tag. Die Attrappe legt
# ihn erst nach der Frage an den Host an; vorher zaehlte faellige_tage den Tag als erledigt.
with tempfile.TemporaryDirectory() as ordner:
wurzel = Path(ordner)
modul = lade()
ablage = wurzel / "vault" / modul.ABLAGE_TEIL
ablage.mkdir(parents=True)
for tag in ("2026-09-25", "2026-09-28", "2026-09-29", "2026-09-30"):
(ablage / f"{tag}.json").write_text("{}", encoding="utf-8")
(ablage / f"{tag}-ibkr.json").write_text("{}", encoding="utf-8")
gesperrt = {
("2026-10-01", "alpaca"): ablage / "2026-10-01.json",
("2026-10-02", "ibkr"): ablage / "2026-10-02-ibkr.json",
}
echt = modul.fahre_dockerhost
def fahre_dockerhost(tag, teil="alpaca"):
ergebnis = echt(tag, teil)
if (tag, teil) in gesperrt:
gesperrt[(tag, teil)].mkdir()
return ergebnis
modul.fahre_dockerhost = fahre_dockerhost
code, notiz, befehle, _ = sicherungslauf(
wurzel, "--jetzt", "2026-10-02T23:30", modul=modul, erster_tag=None
)
inbox = wurzel / "vault" / "Inbox"
assert namen(inbox) == ["2026-10-01.md", "2026-10-02.md"], f"Notizen {namen(inbox)}"
zusatz = ergebnis_zusatz(typ_beim_schreiben(gesperrt[("2026-10-01", "alpaca")]))
for uhrzeit, teil, gesperrt_hier in (
("2026-10-02 23:30", "alpaca", True),
("2026-10-02 23:30", "ibkr", False),
("23:30", "alpaca", False),
("23:30", "ibkr", True),
):
_, folge = zeile_mit_folge(notiz, f"- {uhrzeit} Uhr — {modul.KOEPFE[teil]}: ohne Befund.")
assert (folge == zusatz) == gesperrt_hier, (uhrzeit, teil, folge)
assert notiz.count("Ergebnisdatei nicht geschrieben") == 2, notiz
assert (ablage / "2026-10-02.json").is_file() and (ablage / "2026-10-01-ibkr.json").is_file(), namen(ablage)
assert all(pfad.is_dir() for pfad in gesperrt.values()), namen(ablage)
assert log_zeilen(notiz) == [], notiz
assert befehle == [], befehle
assert code == 1, code
# Mutanten fuer --mutanten: Name, Text in abendkontrolle.py (genau einmal), Ersatz und die Faelle,
# die davon rot werden muessen. MUTANTEN_AUFGABE (unten) tut dasselbe fuer abendkontrolle_einrichten.ps1.
# Ein Mutant, der das Verhalten nicht aendert, gehoert nicht hierher
@@ -1327,6 +1613,64 @@ MUTANTEN = [
' try:\n lauf = ssh_lauf(f"sicherung-bestaetigt {name} {summe}", SSH_TIMEOUT_S)\n'
' except HostNichtErreichbar as abbruch:\n raise RuntimeError(f"nicht bestaetigt ({abbruch})") from None',
["eine_bestaetigung_ohne_host_bricht_die_sicherung_ab"]),
# Seit Paket NR (kritische Pruefung NT, Befund 3): lauf.log nicht schreibbar.
("E743-01 lauf.log wirft wieder",
" except Exception as ausnahme:\n LOG_FEHLER = type(ausnahme).__name__",
" except ZeroDivisionError as ausnahme:\n LOG_FEHLER = type(ausnahme).__name__",
["ein_gesperrtes_lauf_log_haelt_ergebnis_notiz_und_sicherung_nicht_auf",
"ein_gesperrtes_lauf_log_verschluckt_die_zeile_zum_unerreichbaren_host_nicht",
"ein_gesperrtes_lauf_log_verschluckt_die_zeilen_gescheiterter_teile_nicht",
"eine_gesperrte_notiz_neben_gesperrtem_lauf_log_bricht_main_nicht_ab"]),
("E743-02 keine Zeile zu lauf.log", " if LOG_FEHLER is not None:\n code = 1",
" if False:\n code = 1",
["ein_gesperrtes_lauf_log_haelt_ergebnis_notiz_und_sicherung_nicht_auf",
"ein_gesperrtes_lauf_log_verschluckt_die_zeile_zum_unerreichbaren_host_nicht",
"ein_gesperrtes_lauf_log_verschluckt_die_zeilen_gescheiterter_teile_nicht"]),
("E743-03 Exit 0 trotz lauf.log", " if LOG_FEHLER is not None:\n code = 1",
" if LOG_FEHLER is not None:\n pass",
["ein_gesperrtes_lauf_log_haelt_ergebnis_notiz_und_sicherung_nicht_auf"]),
("E743-04 Fehler des vorigen Laufs bleibt", " LOG_FEHLER = None", " pass",
["ein_gesperrtes_lauf_log_haelt_ergebnis_notiz_und_sicherung_nicht_auf"]),
("E743-05 Zeile in der Notiz des Tags",
" schreibe_notiz(vault, jetzt.date().isoformat(), log_zeile)",
" schreibe_notiz(vault, argumente.tag or jetzt.date().isoformat(), log_zeile)",
["ein_gesperrtes_lauf_log_verschluckt_die_zeilen_gescheiterter_teile_nicht"]),
("E743-06 Zeile ohne Typ", "**nicht schreibbar** ({LOG_FEHLER}) #needs-review", "**nicht schreibbar** #needs-review",
["ein_gesperrtes_lauf_log_haelt_ergebnis_notiz_und_sicherung_nicht_auf",
"ein_gesperrtes_lauf_log_verschluckt_die_zeile_zum_unerreichbaren_host_nicht",
"ein_gesperrtes_lauf_log_verschluckt_die_zeilen_gescheiterter_teile_nicht"]),
("E743-07 gesperrte Notiz bricht main ab",
" schreibe_notiz(vault, jetzt.date().isoformat(), log_zeile)\n except Exception:\n pass",
" schreibe_notiz(vault, jetzt.date().isoformat(), log_zeile)\n except ZeroDivisionError:\n pass",
["eine_gesperrte_notiz_neben_gesperrtem_lauf_log_bricht_main_nicht_ab"]),
# Seit der Plankritik NR: logge faengt jede Ausnahme und legt die Ablage im try an; eine nicht
# schreibbare Ergebnisdatei haelt weder die Zeile ihres Tages noch die folgenden Tage auf.
("E743-08 lauf.log faengt nur OSError",
" except Exception as ausnahme:\n LOG_FEHLER = type(ausnahme).__name__",
" except OSError as ausnahme:\n LOG_FEHLER = type(ausnahme).__name__",
["ein_fehler_in_logge_der_kein_oserror_ist_haelt_den_lauf_nicht_auf"]),
("E743-09 Ablage vor dem try angelegt",
" try:\n ablage.mkdir(parents=True, exist_ok=True)\n",
" ablage.mkdir(parents=True, exist_ok=True)\n try:\n",
["eine_ablage_die_sich_nicht_anlegen_laesst_haelt_die_zeilen_nicht_auf"]),
("E743-10 Ergebnisdatei reisst den Teil ab",
' except Exception as ausnahme:\n code = 1\n logge(ablage, f"{vorsatz}{tag}: Ergebnisdatei',
' except ZeroDivisionError as ausnahme:\n code = 1\n logge(ablage, f"{vorsatz}{tag}: Ergebnisdatei',
["eine_nicht_schreibbare_ergebnisdatei_haelt_weder_zeile_noch_folgende_tage_auf",
"eine_ablage_die_sich_nicht_anlegen_laesst_haelt_die_zeilen_nicht_auf"]),
("E743-11 Exit 0 trotz fehlender Ergebnisdatei",
' code = 1\n logge(ablage, f"{vorsatz}{tag}: Ergebnisdatei',
' pass\n logge(ablage, f"{vorsatz}{tag}: Ergebnisdatei',
["eine_nicht_schreibbare_ergebnisdatei_haelt_weder_zeile_noch_folgende_tage_auf"]),
("E743-12 Zeile ohne Unterzeile zur Ergebnisdatei",
" schreibe_notiz(vault, tag, zeile_von(ergebnis, uhrzeit) + zusatz)",
" schreibe_notiz(vault, tag, zeile_von(ergebnis, uhrzeit))",
["eine_nicht_schreibbare_ergebnisdatei_haelt_weder_zeile_noch_folgende_tage_auf",
"eine_ablage_die_sich_nicht_anlegen_laesst_haelt_die_zeilen_nicht_auf"]),
("E743-13 Unterzeile ohne Typ", "Ergebnisdatei nicht geschrieben ({type(ausnahme).__name__}) #needs-review",
"Ergebnisdatei nicht geschrieben #needs-review",
["eine_nicht_schreibbare_ergebnisdatei_haelt_weder_zeile_noch_folgende_tage_auf",
"eine_ablage_die_sich_nicht_anlegen_laesst_haelt_die_zeilen_nicht_auf"]),
("Name der Summendatei beim Host egal", ' if treffer is None or treffer.group(2) != f"{name}.tar":',
" if treffer is None:", ["eine_summendatei_des_hosts_in_falscher_form_holt_kein_archiv"]),
("Grossbuchstaben in der Summe", r'SUMMENZEILE = re.compile(r"([0-9a-f]{64}) (\S+)\n")',