Compare commits
3
Commits
f73f84e3ac
...
2dc668f3d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dc668f3d8 | ||
|
|
3a38cb7e83 | ||
|
|
94f6dfeeeb |
No files matched your search
@@ -23,7 +23,7 @@ Du denkst, du tippst nicht. Dein Ergebnis ist ein Plan, den ein Executer ohne R
|
|||||||
- `rot_bei` nennt parametrisierte Tests mit voller Kennung, und zwar nur die Fälle, die der Mutant umkippt. Bei B1 (2026-09-27) wären unter `return False` drei von fünf Fällen grün geblieben.
|
- `rot_bei` nennt parametrisierte Tests mit voller Kennung, und zwar nur die Fälle, die der Mutant umkippt. Bei B1 (2026-09-27) wären unter `return False` drei von fünf Fällen grün geblieben.
|
||||||
- `aus` und `wird` stehen in `'''…'''`, damit Backslashes wörtlich bleiben.
|
- `aus` und `wird` stehen in `'''…'''`, damit Backslashes wörtlich bleiben.
|
||||||
- **Keine Platzhalter.** Kein „analog zu Task 2", kein „hier noch ergänzen", kein Pseudocode. Was im Plan steht, ist tippfertig.
|
- **Keine Platzhalter.** Kein „analog zu Task 2", kein „hier noch ergänzen", kein Pseudocode. Was im Plan steht, ist tippfertig.
|
||||||
- **Blöcke in der Form, die `plan_materialisiere.py` anwendet.** Die Marke steht als letzte nicht leere Zeile vor dem Zaun, ohne Formatierung: `Aus:`, `wird:`, `Die ganze Datei ist neu:` oder `Häng diesen Block ans Ende:`. Die Überschrift (`##` bis `####`) über dem Block endet mit dem Pfad der Zieldatei in Backticks; ein Pfad ohne Schrägstrich und ohne Endung `.py`, `.md`, `.toml`, `.json`, `.ts` oder `.css` steht als `./name` (etwa `./versions.env`). Eine fette Marke oder eine Überschrift, deren letzter Pfad nicht das Ziel ist, übergeht das Werkzeug ohne Fehler. Am 2026-10-11 im Edge-Stack betraf das 8 von 11 Taskdateien, die Plankritik hätte ohne Kopie gearbeitet.
|
- **Blöcke in der Form, die `plan_materialisiere.py` anwendet.** Die Marke steht als letzte nicht leere Zeile vor dem Zaun, ohne Formatierung: `Aus:`, `wird:`, `Die ganze Datei ist neu:` oder `Häng diesen Block ans Ende:`. Die Überschrift (`##` bis `####`) über dem Block endet mit dem Pfad der Zieldatei in Backticks; ein Pfad ohne Schrägstrich und ohne Endung `.py`, `.md`, `.toml`, `.json`, `.ts` oder `.css` steht als `./name` (etwa `./versions.env`). Eine Löschung steht in `## Zieldateien` als ``- `pfad` (gelöscht)``. Eine fette Marke, eine andere Formulierung („mit dem Write-Werkzeug anlegen“, „löschen“) oder eine Überschrift, deren letzter Pfad nicht das Ziel ist, übergeht das Werkzeug ohne Fehler. Am 2026-10-11 im Edge-Stack betraf das 8 von 11 Taskdateien; die Plankritik hätte ohne angewendete Kopie gearbeitet. Prüfskripte im Plan, die Marken zählen, suchen genau diese Form.
|
||||||
- **Nachbesserung nach einer ausgeführten Welle schreibst du als neue Taskdateien, nicht als Neufassung.** Du hast kein Edit-Werkzeug und schreibst jede überarbeitete Datei komplett neu; am 2026-09-23 im Nexhorizons Hub waren dabei 10 von 17 Befunden der Nachprüfung durch das Umschreiben selbst entstanden. Deshalb: neue `task-NN-nachbesserung-<thema>.md` mit `Aus:`/`wird:`-Blöcken gegen den Baum, bestehende Taskdateien bleiben byte-gleich. Was ältere Tasks an den neuen Stand anpassen muss, schreibst du als Folgenliste `nachbesserung-<welle>-folgen.md` im selben `Aus:`/`wird:`-Format; eingesetzt wird sie vom Koordinator oder einem Executer. Für Korrekturen vor der ersten Ausführung gilt das nicht, außer der Auftrag verlangt es.
|
- **Nachbesserung nach einer ausgeführten Welle schreibst du als neue Taskdateien, nicht als Neufassung.** Du hast kein Edit-Werkzeug und schreibst jede überarbeitete Datei komplett neu; am 2026-09-23 im Nexhorizons Hub waren dabei 10 von 17 Befunden der Nachprüfung durch das Umschreiben selbst entstanden. Deshalb: neue `task-NN-nachbesserung-<thema>.md` mit `Aus:`/`wird:`-Blöcken gegen den Baum, bestehende Taskdateien bleiben byte-gleich. Was ältere Tasks an den neuen Stand anpassen muss, schreibst du als Folgenliste `nachbesserung-<welle>-folgen.md` im selben `Aus:`/`wird:`-Format; eingesetzt wird sie vom Koordinator oder einem Executer. Für Korrekturen vor der ersten Ausführung gilt das nicht, außer der Auftrag verlangt es.
|
||||||
- **Offene Entscheidungen benennen, nicht überspielen.** Wo der Auftrag mehrdeutig ist: Annahme treffen, im Abschnitt „Getroffene Annahmen" explizit nennen. Nur abbrechen, wenn jede Annahme den Plan unbrauchbar machen würde.
|
- **Offene Entscheidungen benennen, nicht überspielen.** Wo der Auftrag mehrdeutig ist: Annahme treffen, im Abschnitt „Getroffene Annahmen" explizit nennen. Nur abbrechen, wenn jede Annahme den Plan unbrauchbar machen würde.
|
||||||
|
|
||||||
|
|||||||
@@ -230,12 +230,33 @@ Seit Paket NT bricht `abendkontrolle.py` die Sicherung ab, wenn der Docker-Host
|
|||||||
Was die Kette nicht prüft:
|
Was die Kette nicht prüft:
|
||||||
|
|
||||||
- Ob Exit 255 unter Win32-OpenSSH 9.5p2 auch kommt, wenn auf dem Host ein Signal `abendkontrolle-lesen.sh` beendet. Laut Quelltext von OpenSSH-portable 9.5p1 ja; dann hieße die Zeile bei Summe oder Bestätigung „Docker-Host nicht erreichbar“, obwohl er erreichbar war.
|
- Ob Exit 255 unter Win32-OpenSSH 9.5p2 auch kommt, wenn auf dem Host ein Signal `abendkontrolle-lesen.sh` beendet. Laut Quelltext von OpenSSH-portable 9.5p1 ja; dann hieße die Zeile bei Summe oder Bestätigung „Docker-Host nicht erreichbar“, obwohl er erreichbar war.
|
||||||
- Lässt sich `lauf.log` nicht schreiben, fehlt auch die Zeile in der Notiz; übrig bleibt Exit 1 (kritische Prüfung NT, Befund 3, so seit E9). Ein Fall dazu fehlt, die Abhilfe steht im `todo.md` des Projekts.
|
- Bis 0.39.2 fehlte auch die Zeile in der Notiz, wenn sich `lauf.log` nicht schreiben ließ; übrig blieb Exit 1 (kritische Prüfung NT, Befund 3, so seit E9). Seit 0.39.3 hält das Ergebnisdateien, Zeilen und Sicherung nicht mehr auf, mit sieben Fällen dazu (Abschnitt „Paket NR“ unten).
|
||||||
- Bricht das Archiv der letzten Instanz des letzten Tags ab, weil der Host gerade wegfällt, folgt keine Frage nach einer Summe mehr; die Zeile nennt dann nur „Archiv nicht geholt (… Exit 255 …)“, nicht „Docker-Host nicht erreichbar“.
|
- Bricht das Archiv der letzten Instanz des letzten Tags ab, weil der Host gerade wegfällt, folgt keine Frage nach einer Summe mehr; die Zeile nennt dann nur „Archiv nicht geholt (… Exit 255 …)“, nicht „Docker-Host nicht erreichbar“.
|
||||||
- Bricht das erste Archiv eines Tags ab, bleibt der leere Tagesordner auf D: zurück (so seit E9).
|
- Bricht das erste Archiv eines Tags ab, bleibt der leere Tagesordner auf D: zurück (so seit E9).
|
||||||
- Ohne eigenen Fall: die ASCII-Darstellung des Tags in der Meldung zu `--tag` (ein Mutant `!a` nach `!r` überlebte), und „kein WSL“ bei falschem `--tag` misst der Fall nur mit `--quelle dockerhost`.
|
- Ohne eigenen Fall: die ASCII-Darstellung des Tags in der Meldung zu `--tag` (ein Mutant `!a` nach `!r` überlebte), und „kein WSL“ bei falschem `--tag` misst der Fall nur mit `--quelle dockerhost`.
|
||||||
- Ein Abend mit ausgeschaltetem Host über das echte `ssh.exe`.
|
- Ein Abend mit ausgeschaltetem Host über das echte `ssh.exe`.
|
||||||
|
|
||||||
|
## Paket NR (Trading Plattform 0.39.3): `lauf.log` und Ergebnisdatei halten die Notizzeile nicht auf
|
||||||
|
|
||||||
|
Seit Paket NR hält ein nicht schreibbares `lauf.log` in `abendkontrolle.py` weder Ergebnisdatei noch Notizzeile noch Sicherung auf, und eine nicht schreibbare Ergebnisdatei weder die Zeile ihres Tages noch die folgenden Tage. Plan im Projekt: `docs/plans/2026-10-12-paket-nr-pruefreste/` (Task 743); Codedoku dort in `docs/code/deployment.md`, Unterabschnitt „Sicherung (seit 0.39.0, Paket E9)“, Absatz „Bekannte Grenzen“.
|
||||||
|
|
||||||
|
- **Wirkt ab dem nächsten Start der Windows-Aufgabe,** nicht erst mit einem Abbild: Sie führt `abendkontrolle.py` aus diesem Ordner aus, abends und fünf Minuten nach jeder Anmeldung.
|
||||||
|
- **`lauf.log` nicht schreibbar** (Sperre, Rechte, volle Platte; jede Ausnahme beim Anlegen der Ablage, beim Öffnen oder beim Schreiben): `logge` wirft nicht mehr und merkt sich den Typ der Ausnahme in `LOG_FEHLER`, `main` setzt ihn zu Beginn jedes Laufs zurück. Am Ende, nach der Sicherung, schreibt der Lauf genau eine Zeile in die Notiz von heute, auch wenn `--tag` einen anderen Tag nennt: `Abendkontrolle lauf.log (automatisch): **nicht schreibbar** (<Typ>) #needs-review. Mindestens ein Eintrag dieses Laufs fehlt dort; …`, und endet mit Exit 1. Kein Pfad, kein Inhalt, nur der Typ der letzten Ausnahme. Scheitert auch diese Notiz, endet `main` ohne Ausnahme mit Exit 1.
|
||||||
|
- **Ergebnisdatei nicht schreibbar:** Die Zeile des Tages entsteht trotzdem, mit der Unterzeile `Ergebnisdatei nicht geschrieben (<Typ>) #needs-review. …`; die folgenden Tage des Teils laufen weiter, `lauf.log` bekommt einen Eintrag, Exit 1.
|
||||||
|
- Stand 0.39.3: Der Treiber hat 46 Fälle und 70 Mutanten. Weicht die Zählzeile eines Laufs davon ab, gilt sie.
|
||||||
|
|
||||||
|
| Datei | Was sich ändert | Aufruf |
|
||||||
|
|---|---|---|
|
||||||
|
| `abendkontrolle.py` | `logge` fängt jede Ausnahme und setzt `LOG_FEHLER`; `main` setzt ihn zurück und schreibt am Ende die Zeile mit dem Kopf `LOG_KOPF`; `fahre_teil` fängt einen Fehler beim Schreiben der Ergebnisdatei, hängt die Unterzeile an die Zeile des Tages und macht mit dem nächsten Tag weiter; Kopf-Docstring und ein Kommentar in `main` nachgezogen | `python abendkontrolle.py --quelle dockerhost --quelle-ibkr dockerhost [--tag JJJJ-MM-TT]` |
|
||||||
|
| `abendkontrolle_dockerhost_angriffe.py` | Neue Hilfsfunktionen `lauf_log_sperren` (legt an der Stelle von `lauf.log` einen Ordner an; Öffnen zum Anhängen scheitert dann wie bei einer Sperre, und der Fall misst den Typ der Ausnahme selbst) und `typ_beim_schreiben` (misst den Typ, wenn sich ein Pfad nicht als Datei schreiben lässt). Sieben neue Fälle: `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`, `ein_fehler_in_logge_der_kein_oserror_ist_haelt_den_lauf_nicht_auf`, `eine_ablage_die_sich_nicht_anlegen_laesst_haelt_die_zeilen_nicht_auf` und `eine_nicht_schreibbare_ergebnisdatei_haelt_weder_zeile_noch_folgende_tage_auf`. In `MUTANTEN` dazu E743-01 bis E743-13 | unter Windows `PYTHONIOENCODING=utf-8 python -X utf8 abendkontrolle_dockerhost_angriffe.py [--mutanten]`; Exit 0 ohne Abweichung, sonst 2 |
|
||||||
|
|
||||||
|
Was die Kette nicht prüft:
|
||||||
|
|
||||||
|
- Ein echt gesperrtes `lauf.log` (ein anderes Programm, volle Platte): Nachgestellt ist es als Ordner an der Stelle von `lauf.log`; den Typ der Ausnahme misst der Fall selbst, statt ihn je Betriebssystem zu raten.
|
||||||
|
- Ohne eigenen Mutanten: dass `fahre_teil` die Unterzeile für jeden Tag neu leert. Der Fall zur Ergebnisdatei prüft aber, dass der Folgetag keine Unterzeile trägt.
|
||||||
|
- Eine halb geschriebene Ergebnisdatei: Sie zählt für `faellige_tage` als erledigt (`exists()`), kein Lauf prüft den Tag erneut, und der Lauf räumt nichts weg. Kein Fall dazu; die Unterzeile sagt es.
|
||||||
|
- `rueckweg_aapl.py` schreibt `lauf.log` ungeschützt vor der Notizzeile, `abbau_restbestand.py` schluckt Fehler an `abbau.log` still: derselbe Mechanismus, ohne Änderung in NR. Die Aufgabe „Rueckweg AAPL“ wird in Phase 9 entfernt, die Abbau-Werkzeuge sollen stillgelegt werden (Punkt im `todo.md` des Projekts).
|
||||||
|
|
||||||
## Paket AB (2026-10-07): Abbau des Restbestands nach dem Netzschalter-Abriss
|
## Paket AB (2026-10-07): Abbau des Restbestands nach dem Netzschalter-Abriss
|
||||||
|
|
||||||
Am 2026-10-07 um 20:13 beendete ein Druck auf den Netzschalter beide Paper-Runner mitten im Handel, ohne Glattstellen. Pascal am selben Abend: am 2026-10-08 soweit es geht alles verkaufen, Testläufe bis Dienstag gestoppt. Die Zeitgeber `trading-paper.timer` und `trading-paper-ibkr.timer` sind seitdem aus, die Abendkontrolle ebenfalls; die Meldung dieses Werkzeugs ist in der Pause das einzige Netz. Plan im Projekt: `docs/plans/2026-10-07-paket-ab-abbau-restbestand/`.
|
Am 2026-10-07 um 20:13 beendete ein Druck auf den Netzschalter beide Paper-Runner mitten im Handel, ohne Glattstellen. Pascal am selben Abend: am 2026-10-08 soweit es geht alles verkaufen, Testläufe bis Dienstag gestoppt. Die Zeitgeber `trading-paper.timer` und `trading-paper-ibkr.timer` sind seitdem aus, die Abendkontrolle ebenfalls; die Meldung dieses Werkzeugs ist in der Pause das einzige Netz. Plan im Projekt: `docs/plans/2026-10-07-paket-ab-abbau-restbestand/`.
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ die mit "## Agentenprotokoll" beginnt.
|
|||||||
- Scheitert nur die Notiz (etwa eine Datei, die kein UTF-8 ist), bleibt es bei der Ergebnisdatei und
|
- Scheitert nur die Notiz (etwa eine Datei, die kein UTF-8 ist), bleibt es bei der Ergebnisdatei und
|
||||||
einer Zeile in lauf.log: Die Kontrolle ist gelaufen und hat ihre Meldung schon geschickt; ein zweiter
|
einer Zeile in lauf.log: Die Kontrolle ist gelaufen und hat ihre Meldung schon geschickt; ein zweiter
|
||||||
Lauf schickte sie nur noch einmal.
|
Lauf schickte sie nur noch einmal.
|
||||||
- Ein Tag, der laenger als NACHHOLEN_TAGE scheitert, faellt aus dem Fenster (lauf.log behaelt ihn).
|
- Ein Tag, der laenger als NACHHOLEN_TAGE scheitert, faellt aus dem Fenster. lauf.log behaelt ihn, solange es
|
||||||
|
sich schreiben laesst; sonst bleiben nur seine Zeilen "gescheitert" in der Notiz seines Tags, soweit sie sich
|
||||||
|
schreiben liessen, und die Zeile zu lauf.log in der Notiz von heute (kritische Pruefung NR, Task 746).
|
||||||
Seit Paket IB2 zwei Teile je Tag, nacheinander: alpaca wie oben, dann ibkr (abendkontrolle_wsl.py
|
Seit Paket IB2 zwei Teile je Tag, nacheinander: alpaca wie oben, dann ibkr (abendkontrolle_wsl.py
|
||||||
--teil ibkr) mit eigener Ergebnisdatei <tag>-ibkr.json, eigener Zeile "Abendkontrolle IBKR" in
|
--teil ibkr) mit eigener Ergebnisdatei <tag>-ibkr.json, eigener Zeile "Abendkontrolle IBKR" in
|
||||||
derselben Notiz und eigenem Nachholen ab IBKR_ERSTER_TAG. --teil nimmt nur einen Teil; mit
|
derselben Notiz und eigenem Nachholen ab IBKR_ERSTER_TAG. --teil nimmt nur einen Teil; mit
|
||||||
@@ -39,6 +41,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 +591,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:
|
||||||
|
"""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)
|
ablage.mkdir(parents=True, exist_ok=True)
|
||||||
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as datei:
|
with (ablage / "lauf.log").open("a", encoding="utf-8", newline="\n") as datei:
|
||||||
datei.write(f"{datetime.now().isoformat(timespec='seconds')} {text}\n")
|
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 +641,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 +663,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 +692,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 +746,33 @@ 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 ein spaeterer Lauf ohne --tag den Tag erneut prueft, entscheidet faellige_tage: nur einen Werktag ab
|
||||||
|
# ERSTER_TAG bzw. IBKR_ERSTER_TAG, hoechstens NACHHOLEN_TAGE zurueck, und nur, wenn unter dem Namen der
|
||||||
|
# Datei nichts liegt, auch keine halb geschriebene Datei. Einen Tag, den nur --tag erreicht (Wochenende,
|
||||||
|
# vor dem ersten Tag, aelter als das Fenster), prueft kein Lauf von selbst erneut (kritische Pruefung NR).
|
||||||
|
zusatz = ""
|
||||||
if ergebnis.get("exit") != 2:
|
if ergebnis.get("exit") != 2:
|
||||||
|
try:
|
||||||
(ablage / f"{tag}{endung}.json").write_text(
|
(ablage / f"{tag}{endung}.json").write_text(
|
||||||
json.dumps(ergebnis, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n"
|
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. Ein späterer "
|
||||||
|
"Lauf ohne --tag prüft den Tag nur erneut, wenn er ein Werktag ab dem ersten Prüftag des Teils "
|
||||||
|
f"ist, höchstens {NACHHOLEN_TAGE} Kalendertage zurückliegt und unter dem Namen der Datei nichts "
|
||||||
|
"liegt, auch keine halb geschriebene. Sonst prüft ihn kein Lauf von selbst."
|
||||||
|
)
|
||||||
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,292 @@ 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.
|
||||||
|
|
||||||
|
Seit Task 746 (kritische Pruefung NR) sagt sie nur, was faellige_tage hergibt; 7 ist NACHHOLEN_TAGE.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
f" - Ergebnisdatei nicht geschrieben ({typ}) #needs-review. Ein späterer Lauf ohne --tag prüft den Tag "
|
||||||
|
"nur erneut, wenn er ein Werktag ab dem ersten Prüftag des Teils ist, höchstens 7 Kalendertage "
|
||||||
|
"zurückliegt und unter dem Namen der Datei nichts liegt, auch keine halb geschriebene. Sonst prüft ihn "
|
||||||
|
"kein Lauf von selbst."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 +1617,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