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:
1 parent
f73f84e3ac
commit
94f6dfeeeb
2 files changed
+409
-9
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
|
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
|
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.
|
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>]
|
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>]
|
[--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)
|
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:
|
def logge(ablage: Path, text: str) -> None:
|
||||||
ablage.mkdir(parents=True, exist_ok=True)
|
"""Eine Zeile in lauf.log. Wirft seit Paket NR nicht mehr.
|
||||||
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as datei:
|
|
||||||
datei.write(f"{datetime.now().isoformat(timespec='seconds')} {text}\n")
|
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:
|
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):
|
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)
|
print(f"abendkontrolle: --tag {argumente.tag!a} ist kein Datum JJJJ-MM-TT", file=sys.stderr)
|
||||||
return 2
|
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
|
# 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).
|
# Ergebnisse (bis zu zwoelf Abfragen mit je SSH_TIMEOUT_S; Plankritik Runde 2, Befund 1).
|
||||||
frist = monotonic() + SICHERUNG_BUDGET_S
|
frist = monotonic() + SICHERUNG_BUDGET_S
|
||||||
@@ -634,8 +661,9 @@ def main(argv=None) -> int:
|
|||||||
else:
|
else:
|
||||||
tage = faellige_tage(jetzt, ablage)
|
tage = faellige_tage(jetzt, ablage)
|
||||||
vorsatz = "" if teil == "alpaca" else f"{teil} "
|
vorsatz = "" if teil == "alpaca" else f"{teil} "
|
||||||
# Scheitert ein Teil ausserhalb seiner eigenen Fehlerbehandlung (Ergebnisdatei nicht
|
# Scheitert ein Teil ausserhalb seiner eigenen Fehlerbehandlung, laeuft der naechste trotzdem
|
||||||
# schreibbar, lauf.log gesperrt), laeuft der naechste trotzdem (Pruefung IB2).
|
# (Pruefung IB2). Seit Paket NR werfen weder ein gesperrtes lauf.log (logge) noch eine nicht
|
||||||
|
# schreibbare Ergebnisdatei (fahre_teil).
|
||||||
try:
|
try:
|
||||||
logge(ablage, f"{vorsatz}start, faellig: {tage}")
|
logge(ablage, f"{vorsatz}start, faellig: {tage}")
|
||||||
# Seit Paket DH: die Quelle je Teil; die IBKR-Instanz zieht an einem eigenen Abend um.
|
# 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}")
|
logge(ablage, f"sicherung abgebrochen: {type(ausnahme).__name__}: {ausnahme}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
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}")
|
logge(ablage, f"{vorsatz}{tag}: auch die Notiz scheiterte {zweite!r}")
|
||||||
continue
|
continue
|
||||||
# Ein Abbruch im WSL-Teil (Exit 2) wird wiederholt; alles andere gilt als gelaufen.
|
# 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:
|
if ergebnis.get("exit") != 2:
|
||||||
(ablage / f"{tag}{endung}.json").write_text(
|
try:
|
||||||
json.dumps(ergebnis, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n"
|
(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:
|
else:
|
||||||
code = 1
|
code = 1
|
||||||
logge(ablage, f"{vorsatz}{tag}: {len(ergebnis['befunde'])} Befund(e), exit {ergebnis.get('exit')}")
|
logge(ablage, f"{vorsatz}{tag}: {len(ergebnis['befunde'])} Befund(e), exit {ergebnis.get('exit')}")
|
||||||
try:
|
try:
|
||||||
schreibe_notiz(vault, tag, zeile_von(ergebnis, uhrzeit))
|
schreibe_notiz(vault, tag, zeile_von(ergebnis, uhrzeit) + zusatz)
|
||||||
except Exception as ausnahme:
|
except Exception as ausnahme:
|
||||||
code = 1
|
code = 1
|
||||||
logge(ablage, f"{vorsatz}{tag}: Notiz nicht geschrieben {ausnahme!r}")
|
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
|
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,
|
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.
|
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]
|
Aufruf: python abendkontrolle_dockerhost_angriffe.py [--mutanten]
|
||||||
Exit 0 ohne Abweichung, 2 mit Abweichung. --mutanten faehrt MUTANTEN auf Kopien von
|
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
|
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)."
|
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,
|
# 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.
|
# die davon rot werden muessen. MUTANTEN_AUFGABE (unten) tut dasselbe fuer abendkontrolle_einrichten.ps1.
|
||||||
# Ein Mutant, der das Verhalten nicht aendert, gehoert nicht hierher
|
# 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'
|
' 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',
|
' except HostNichtErreichbar as abbruch:\n raise RuntimeError(f"nicht bestaetigt ({abbruch})") from None',
|
||||||
["eine_bestaetigung_ohne_host_bricht_die_sicherung_ab"]),
|
["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":',
|
("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"]),
|
" 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")',
|
("Grossbuchstaben in der Summe", r'SUMMENZEILE = re.compile(r"([0-9a-f]{64}) (\S+)\n")',
|
||||||
|
|||||||
Reference in new issue
Block a user