rechnungstool/pruef_updater.py
TheMockTv 73042b771c Logo in der Statuszeile, Bau an einer Stelle, Startprobe fuer die EXE
Logo
  Das Programmlogo steht jetzt klein neben "powered by ravokk" - dort sitzt
  die Kennung des Werkzeugs, nicht die des Kunden. Eine eigene Kopfzeile hat
  dieses Programm nicht; oben sind die Reiter.

bauen.py
  Der Weg von der Quelle zur Setup-Datei war eine Folge von Befehlen, die
  jemand von Hand eintippen musste. Wer einen Schritt vergisst, liefert eine
  Setup-Datei mit einer alten EXE aus - und sieht es ihr nicht an. Genau das
  ist heute passiert. Jetzt ist es ein Befehl, die Nummer kommt aus
  version.py, und am Ende stehen die SHA256-Summen fuer den Release-Text.

pruef_start_exe.py
  Prueft, was kein anderer Pruefstand prueft: ob die GEBAUTE EXE ein
  sichtbares Fenster zeigt. Alle uebrigen fahren das Programm aus der Quelle,
  dort trat der Fehler nie auf. "Der Prozess laeuft" beweist dabei nichts -
  er lief ja, waehrend nichts zu sehen war.

Aufgeraeumt
  "Rechnung Campinghof.exe" ist weg; gebaute Dateien gehoeren ins Release,
  nicht ins Repo. Die .spec-Dateien dagegen schon: ohne sie kann niemand die
  EXE nachbauen, die der Updater ausliefert.

Fassung 1.7.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
2026-09-06 20:34:59 +02:00

229 lines
9.9 KiB
Python

# -*- 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
fehler = []
def pruefe(bedingung, text):
zeile = (" OK | " if bedingung else "FEHLER | ") + text
kod = sys.stdout.encoding or "utf-8"
print(zeile.encode(kod, "replace").decode(kod))
if not bedingung:
fehler.append(text)
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")