# -*- coding: utf-8 -*- """Pruefstand: der Updater - vor allem die Faelle, in denen er NICHTS tun darf. Sein Bild (06.09.2026): "der updater schaut beim starten erst: ist auf dem git link eine neue version mit exe? wenn ja, erst laden, ersetzen und dann starten." "einmal pruefen auf vollstaendigkeit." "wir werden den namen mit nummer fuehren - wenn er merkt, eh warte mal, die heisst anders, dann neu machen" Ein Updater ist gefaehrlicher als das Programm, das er aktualisiert: Er entscheidet ueber die Datei, die der Betrieb braucht. Deshalb wird hier weniger geprueft, DASS er aktualisiert, als dass er es in den falschen Faellen NICHT tut - und dass er nie etwas ueberschreibt. Ohne Netz: die Release-Liste wird eingesetzt, nicht geholt. """ import os import shutil import sys import tempfile QUELLE = r"C:\claude\rechnungstool" sys.path.insert(0, QUELLE) import updater as U # noqa: E402 from pruefhelfer import fehler, pruefe EXE = "Rechnungstool.exe" # ---- Versionen vergleichen --------------------------------------------- pruefe(U.zerlegen("v1.6.0") == (1, 6, 0), "v1.6.0 wird zu (1, 6, 0)") pruefe(U.zerlegen("") == (0,), "leere Angabe zaehlt als aelteste") pruefe(U.zerlegen("kaputt") == (0,), "unlesbare Angabe zaehlt als aelteste") pruefe(U.ist_neuer("v1.7.0", "1.6.9"), "1.7.0 ist neuer als 1.6.9 (nicht alphabetisch!)") pruefe(not U.ist_neuer("v1.6.0", "1.6.0"), "Gleichstand ist NICHT neuer") pruefe(U.ist_neuer("v1.10.0", "1.9.0"), "1.10.0 ist neuer als 1.9.0") # ---- Die Nummer im Dateinamen ------------------------------------------ pruefe(U.dateiname(EXE, "v1.6.0") == "Rechnungstool-1.6.0.exe", "aus Name und Tag wird Rechnungstool-1.6.0.exe") pruefe(U.dateiname("Rechnungstool-1.5.0.exe", "v1.6.0") == "Rechnungstool-1.6.0.exe", "eine schon vorhandene Nummer wird ersetzt, nicht angehaengt") pruefe(U.basis_und_endung(EXE) == ("Rechnungstool", ".exe"), "Stamm und Endung sauber getrennt") # ---- Das passende Release finden --------------------------------------- def release(tag, dateien, entwurf=False, vorab=True): return {"tag_name": tag, "draft": entwurf, "prerelease": vorab, "assets": [{"name": n, "size": g, "browser_download_url": f"http://x/{tag}/{n}"} for n, g in dateien]} liste = [release("v1.5.0", [(EXE, 10)]), release("v1.6.0", [(EXE, 20)]), release("v1.5.5", [(EXE, 15)])] treffer = U.passendes_release(liste, EXE) pruefe(treffer and treffer[0]["tag_name"] == "v1.6.0", "aus einer unsortierten Liste wird die neueste genommen") pruefe(U.passendes_release([release("v9.9.9", [("anderes.exe", 5)])], EXE) is None, "ein Release ohne passende Datei zaehlt nicht") pruefe(U.passendes_release([release("v9.9.9", [(EXE, 5)], entwurf=True)], EXE) is None, "ein Entwurf zaehlt nicht") # Vorabversionen MUESSEN zaehlen - alle Fassungen sind so gekennzeichnet. pruefe(U.passendes_release([release("v2.0.0", [(EXE, 30)], vorab=True)], EXE) is not None, "Vorabversionen zaehlen mit (sonst faende der Updater nie etwas)") # ---- Vollstaendigkeit --------------------------------------------------- tmp = tempfile.mkdtemp(prefix="upd_pruef_") probe = os.path.join(tmp, "probe.bin") with open(probe, "wb") as f: f.write(b"A" * 1000) summe = U.sha256(probe) gut, _ = U.vollstaendig(probe, 1000, summe) pruefe(gut, "richtige Groesse und Pruefsumme werden angenommen") gut, grund = U.vollstaendig(probe, 999, summe) pruefe(not gut and "Groesse" in grund, f"falsche Groesse wird abgelehnt ({grund})") gut, grund = U.vollstaendig(probe, 1000, "0" * 64) pruefe(not gut and "Pruefsumme" in grund, f"falsche Pruefsumme wird abgelehnt ({grund})") gut, _ = U.vollstaendig(probe, 1000, "") pruefe(gut, "ohne Pruefsumme entscheidet die Groesse allein") gut, _ = U.vollstaendig(os.path.join(tmp, "gibtsnicht"), 1000, "") pruefe(not gut, "eine fehlende Datei ist nicht vollstaendig") # ---- Was liegt im Ordner? ---------------------------------------------- ordner = os.path.join(tmp, "install") os.makedirs(ordner) for name, inhalt in (("Rechnungstool-1.5.0.exe", b"ALT"), ("Rechnungstool-1.6.0.exe", b"AKTUELL"), ("nichts-zu-tun.txt", b"x")): with open(os.path.join(ordner, name), "wb") as f: f.write(inhalt) fassung, pfad = U.neueste_installierte(ordner, EXE) pruefe(fassung == "1.6.0", f"die neueste vorhandene Fassung wird am Namen erkannt ({fassung})") pruefe(os.path.basename(pfad) == "Rechnungstool-1.6.0.exe", "und der Pfad stimmt") pruefe(len(U.installierte(ordner, EXE)) == 2, "fremde Dateien im Ordner stoeren nicht") # ---- Der ganze Durchlauf, ohne Netz ------------------------------------- meldungen = [] def sagen(t): meldungen.append(str(t)) def alle_da(): return sorted(os.path.basename(p) for _f, p in U.installierte(ordner, EXE)) # 1. gleiche Fassung -> nichts laden, vorhandene starten meldungen.clear() start = U.durchlauf(ordner, "x/y", EXE, sagen=sagen, holen_=lambda _r: [release("v1.6.0", [(EXE, 11)])]) pruefe(os.path.basename(start) == "Rechnungstool-1.6.0.exe", "gleiche Fassung: die vorhandene wird gestartet") pruefe(alle_da() == ["Rechnungstool-1.5.0.exe", "Rechnungstool-1.6.0.exe"], "und im Ordner hat sich nichts geaendert") # 2. aeltere Fassung im Netz -> nichts tun start = U.durchlauf(ordner, "x/y", EXE, sagen=sagen, holen_=lambda _r: [release("v1.5.0", [(EXE, 11)])]) pruefe(os.path.basename(start) == "Rechnungstool-1.6.0.exe", "aeltere Fassung im Netz: es wird nicht aktualisiert") # 3. kein Netz -> vorhandene starten und sagen, warum meldungen.clear() def wirft(_r): raise OSError("kein Netz") start = U.durchlauf(ordner, "x/y", EXE, sagen=sagen, holen_=wirft) pruefe(os.path.basename(start) == "Rechnungstool-1.6.0.exe", "ohne Netz wird die vorhandene Fassung gestartet") pruefe(any("Kein Update" in m for m in meldungen), "und es wird gesagt, warum") # 4. neuere Fassung, aber Groesse passt nicht -> VERWERFEN meldungen.clear() falsch = release("v1.9.0", [(EXE, 999999)]) falsch["assets"][0]["browser_download_url"] = "file:///" + probe.replace("\\", "/") start = U.durchlauf(ordner, "x/y", EXE, sagen=sagen, holen_=lambda _r: [falsch]) pruefe(any("VERWORFEN" in m for m in meldungen), "falsche Groesse: das Update wird verworfen") pruefe(os.path.basename(start) == "Rechnungstool-1.6.0.exe", "und die vorhandene Fassung wird gestartet") pruefe(not os.path.exists(os.path.join(ordner, "Rechnungstool-1.9.0.exe")), "die halbe Datei erreicht den Installationsordner NICHT - der wichtigste Punkt") # 5. neuere Fassung, alles stimmt -> als NEUE Datei dazu meldungen.clear() richtig = release("v1.9.0", [(EXE, 1000)]) richtig["assets"][0]["browser_download_url"] = "file:///" + probe.replace("\\", "/") start = U.durchlauf(ordner, "x/y", EXE, sagen=sagen, holen_=lambda _r: [richtig]) pruefe(os.path.basename(start) == "Rechnungstool-1.9.0.exe", "die neue Fassung wird gestartet") pruefe(os.path.exists(os.path.join(ordner, "Rechnungstool-1.9.0.exe")), "sie liegt als EIGENE Datei im Ordner") pruefe(os.path.exists(os.path.join(ordner, "Rechnungstool-1.6.0.exe")), "die vorige bleibt liegen - sie ist die Rueckfallebene") pruefe(not os.path.exists(os.path.join(ordner, "Rechnungstool-1.5.0.exe")), "die vorvorige wurde aufgeraeumt (es bleiben immer zwei)") buch = U.buch_lesen(ordner) pruefe(buch.get("version") == "1.9.0", f"der Updater fuehrt Buch ueber das, was er installiert hat ({buch})") # 6. gleich noch einmal -> nichts laden, es ist ja jetzt aktuell meldungen.clear() vorher = alle_da() start = U.durchlauf(ordner, "x/y", EXE, sagen=sagen, holen_=lambda _r: [richtig]) pruefe(any("Aktuell" in m for m in meldungen), f"unmittelbar danach gilt es als aktuell ({meldungen})") pruefe(alle_da() == vorher, "und es wird nichts noch einmal geladen") # 7. Buch verloren, Datei aber da -> nicht noch einmal laden # Kommt vor, wenn jemand installiert.json loescht. Der Dateiname allein traegt # die Entscheidung: die 1.9.0 liegt da, also wird sie nur gestartet. meldungen.clear() os.remove(os.path.join(ordner, U.BUCH)) start = U.durchlauf(ordner, "x/y", EXE, sagen=sagen, holen_=lambda _r: [richtig]) pruefe(os.path.basename(start) == "Rechnungstool-1.9.0.exe", "auch ohne Buch wird die richtige Datei gestartet") pruefe(not any("Wird geladen" in m for m in meldungen), f"und nichts noch einmal geladen ({meldungen})") # 8. Ordner nicht beschreibbar -> kein Update, aber das Programm startet # Sein Einwand: "was ist, wenn ein user den ordner aendert?" Liegt die # Installation dann in einem geschuetzten Ordner, darf das Update scheitern - # aber NIE der Start. Ein fehlgeschlagenes Update, nach dem gar nichts mehr # geht, waere schlimmer als kein Update. meldungen.clear() gesperrt = os.path.join(tmp, "gesperrt") os.makedirs(gesperrt) with open(os.path.join(gesperrt, "Rechnungstool-1.6.0.exe"), "wb") as f: f.write(b"INSTALLIERT") echt_beschreibbar = U.beschreibbar U.beschreibbar = lambda _o: False # so, als waere es "Programme" try: start = U.durchlauf(gesperrt, "x/y", EXE, sagen=sagen, holen_=lambda _r: [richtig]) finally: U.beschreibbar = echt_beschreibbar pruefe(os.path.basename(start) == "Rechnungstool-1.6.0.exe", "geschuetzter Ordner: die vorhandene Fassung wird trotzdem gestartet") pruefe(any("nicht geschrieben werden" in m for m in meldungen), f"und es wird gesagt, warum ({meldungen})") pruefe(not os.path.exists(os.path.join(gesperrt, "Rechnungstool-1.9.0.exe")), "es wurde nichts abgelegt") shutil.rmtree(tmp, ignore_errors=True) if fehler: print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt") sys.exit(1) print("GRUEN | Der Updater aktualisiert - und laesst in den falschen Faellen die Finger davon")