Die Runde vom 07.09.2026 hat neben den zwei echten Fehlern eine Reihe Wiederholungen und stumme Stellen gemeldet. Abgearbeitet in Etappen, jede fuer sich pruefbar - nach jeder liefen beide Pruefketten gruen. * Menueleiste: der Wechsel von einem Titel zum anderen brauchte ZWEI Klicks. Der grab_set der offenen Liste schluckt den ersten, die <Button-1>-Bindung des Titels feuert gar nicht. Das native tk.Menu, das diese Leiste ersetzt hat, wechselte schon beim blossen Ueberfahren - zwei Klicks waren also eine Verschlechterung gegenueber dem, was vorher da war. Die Klappliste wird ausserdem jetzt wie jede andere Position ueber theme.auf_bildschirm() begrenzt; am unteren Fensterrand lief sie vorher hinaus. Neu: pruef_menueleiste.py misst beides ueber echte Klickereignisse. * %LOCALAPPDATA%/ravokk wurde an drei Stellen einzeln ausgerechnet. Jetzt fragen alle gemeinsam.standard_ordner(). Laufen die je auseinander, fuehren die beiden Programme zwei Nummernbuecher - und das faellt erst auf, wenn eine Rechnungsnummer zum zweiten Mal vergeben ist (§ 14 Abs. 4 Nr. 4 UStG). * pruefe() stand 35-mal im Quelltext, in sechs Fassungen -> pruefhelfer.py. Die zwei Ausreisser sind mitgezogen: pruef_bilder.py schrieb ein eigenes Format, pruef_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug. * dlg_darstellung() und die drei Handgriffe des Umschaltens standen in beiden app.py fast wortgleich -> ThemeMixin in theme.py. Beide trugen inzwischen denselben langen Kommentar zu demselben Fehler; das war der Beweis, dass es eine Sache ist. Programmspezifisch bleiben der Hinweistext im Dialog (R31) und die Frage, ob die Einstellungen ausdruecklich gespeichert werden muessen. * pruef_gemeinsam_automatisch.py sagte im Text "genau ein Buch" und prueft ">= 1" mit einem any() - eine Pruefung, die nicht rot werden kann. Jetzt == 1, und der Dateiname wird mitgeprueft. Dazu neu: die neun Dateien, die im Kopf zusagen, sie laegen in beiden Programmen gleich, werden byteweise verglichen. Bei hinweis.py stimmte die Zusage seit dem 06.09. nicht mehr - gleicher Inhalt, CRLF gegen LF. * .gitattributes, damit die Zeilenenden nicht von der Maschine abhaengen. Ohne das meldet genau diese Pruefung nach einem frischen Checkout einen Unterschied, den es im Repository gar nicht gibt. * Sechs stumme "except OSError" sagen jetzt, warum sie schweigen duerfen, und einer meldet statt zu schweigen: schlaegt die Uebernahme einer alten config.json fehl, faengt das Programm ohne Firmendaten, Katalog und Zaehler neu an - und der Erfolgsfall schrieb eine Zeile, der Fehlerfall nicht. Die 24 stummen tk.TclError bleiben: dort wird ein Widget angefasst, das gerade zerstoert wurde, und eine Meldung waere Rauschen. * Tote Parameter und Variablen aus dem Umbau des Vortags: symbol_setzen(dunkel=), kachel(grund=, radius=), mit_trennlinie, sechs x/y-Berechnungen, die von theme.mittig() sofort ueberschrieben wurden, vier lokale "import ctypes". 39 Pruefstaende, 0 rot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
220 lines
9.7 KiB
Python
220 lines
9.7 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
|
|
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")
|