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_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug, pruef_bilder.py im Rechnungstool schrieb ein eigenes Format. * 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. * Die stummen "except OSError" in api.py, updater.py und einzelinstanz.py sagen jetzt, warum sie schweigen duerfen. Die 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
183 lines
6 KiB
Python
183 lines
6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Pruefstand: geht das Steuerjournal wirklich nur EINMAL auf?
|
|
|
|
Zwei Fenster lesen denselben Rechnungsordner in dieselbe journal.sqlite3. Was
|
|
das eine loescht, steht im anderen noch auf dem Schirm - und wer dort auf
|
|
"Bericht fuers Amt" drueckt, meldet einen Stand, den es nicht mehr gibt.
|
|
|
|
Geprueft wird mit ECHTEN Prozessen, nicht mit einem nachgebauten Aufruf: nur so
|
|
zeigt sich, ob die Sperre wirklich vor dem Datenordner greift und ob sie sich
|
|
beim Beenden auch wieder loest. Das Programm laeuft dabei aus einer Kopie im
|
|
Temp-Ordner - die Sperre gilt trotzdem (sie haengt am Namen, nicht am Ordner),
|
|
das echte Journal wird nicht angefasst.
|
|
|
|
Waehrend des Laufs blitzt kurz ein Fenster auf. Das gehoert dazu.
|
|
"""
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
|
|
HIER = r"C:\claude\beherbergungssteuer"
|
|
sys.path.insert(0, HIER)
|
|
|
|
|
|
laeufer = []
|
|
|
|
|
|
def _reissleine():
|
|
for p in laeufer:
|
|
try:
|
|
p.kill()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog, "
|
|
"auf den niemand klickt")
|
|
os._exit(2)
|
|
|
|
|
|
_wecker = threading.Timer(150, _reissleine)
|
|
_wecker.daemon = True
|
|
_wecker.start()
|
|
|
|
from einzelinstanz import pid_datei # noqa: E402
|
|
from pruefhelfer import fehler, pruefe
|
|
|
|
if os.path.exists(pid_datei()):
|
|
print(f" (Merkzettel: {pid_datei()})")
|
|
|
|
# ---- Kopie des Programms, damit das echte Journal unberuehrt bleibt --------
|
|
tmp = tempfile.mkdtemp(prefix="bst_instanz_")
|
|
prog = os.path.join(tmp, "programm")
|
|
os.makedirs(prog)
|
|
for name in os.listdir(HIER):
|
|
if name.endswith(".py"):
|
|
shutil.copy(os.path.join(HIER, name), os.path.join(prog, name))
|
|
|
|
|
|
# Auch die Beilagen mitnehmen - ohne assets/ faende das Programm sein Logo
|
|
# nicht. Das stuerzt zwar nicht ab, aber ein Pruefstand soll das Programm
|
|
# fahren, wie es ausgeliefert wird, nicht eine abgespeckte Fassung.
|
|
for name in ("assets",):
|
|
quelle = os.path.join(HIER, name)
|
|
if os.path.isdir(quelle):
|
|
shutil.copytree(quelle, os.path.join(prog, name))
|
|
|
|
|
|
# Der Erprobungshinweis wuerde in JEDEM dieser drei Starts ein echtes Fenster
|
|
# oeffnen. Hier laeuft das Programm als eigener Prozess - da laesst sich kein
|
|
# Dialog abfangen, es klickt niemand, und der Pruefstand liefe in die
|
|
# Reissleine. Geprueft wird an dieser Stelle die SPERRE; der Hinweis hat seinen
|
|
# eigenen Pruefstand (pruef_hinweis.py).
|
|
#
|
|
# Also wird die Kenntnisnahme vorher eingetragen - mit genau dem Code, den auch
|
|
# der Klick auf "Ja" ausfuehrt (hinweis.vermerken auf die Einstellungstabelle).
|
|
# Nichts nachgebaut, nichts von Hand ins SQL geschrieben.
|
|
#
|
|
# Das trifft NUR die Kopie im Temp-Ordner: aus der Quelle heraus liegt der
|
|
# Datenordner neben app.py (ablage.daten_ordner()). Im echten Journal des
|
|
# Betriebs entsteht dadurch keine Zustimmung, die niemand gegeben hat.
|
|
def hinweis_vorab_eintragen():
|
|
import db
|
|
import hinweis
|
|
journal = db.Journal(os.path.join(prog, "journal.sqlite3"))
|
|
|
|
class Ablage:
|
|
def get(self, schluessel, standard=""):
|
|
return journal.hole(schluessel, standard)
|
|
|
|
def __setitem__(self, schluessel, wert):
|
|
journal.setze(schluessel, str(wert))
|
|
|
|
hinweis.vermerken(Ablage())
|
|
print(f" (Kenntnisnahme in der Kopie vermerkt, Fassung {hinweis.FASSUNG})")
|
|
|
|
|
|
hinweis_vorab_eintragen()
|
|
|
|
|
|
def starte():
|
|
# --test: kein Erststart-Dialog nach dem gemeinsamen Ordner. Die Sperre
|
|
# selbst greift unabhaengig davon - genau die wird hier geprueft.
|
|
return subprocess.Popen([sys.executable, "app.py", "--test"], cwd=prog,
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
|
|
|
|
def warte_auf_ende(p, sekunden):
|
|
"""Wartet, bis der Prozess von selbst geht. None = er laeuft noch."""
|
|
ende = time.time() + sekunden
|
|
while time.time() < ende:
|
|
if p.poll() is not None:
|
|
return p.returncode
|
|
time.sleep(0.2)
|
|
return None
|
|
|
|
|
|
# ---- 1. Das erste Fenster ------------------------------------------------
|
|
erst = starte()
|
|
laeufer.append(erst)
|
|
time.sleep(6) # Tk braucht einen Moment, bis das Fenster wirklich steht
|
|
pruefe(erst.poll() is None, f"das erste Fenster laeuft (Rueckgabe {erst.poll()})")
|
|
|
|
merkzettel = None
|
|
try:
|
|
with open(pid_datei(), encoding="ascii") as f:
|
|
merkzettel = int(f.read().strip())
|
|
except (OSError, ValueError):
|
|
pass
|
|
pruefe(merkzettel == erst.pid,
|
|
f"der Merkzettel nennt genau diesen Prozess ({merkzettel} / {erst.pid})")
|
|
|
|
# ---- 2. Der zweite Doppelklick -------------------------------------------
|
|
zweit = starte()
|
|
laeufer.append(zweit)
|
|
code = warte_auf_ende(zweit, 25)
|
|
if code is None:
|
|
zweit.kill()
|
|
pruefe(code == 0, f"der zweite Start beendet sich von selbst (Rueckgabe {code})")
|
|
pruefe(erst.poll() is None, "und das erste Fenster laeuft dabei weiter")
|
|
|
|
ausgabe = ""
|
|
try:
|
|
ausgabe = (zweit.stdout.read() or b"").decode("utf-8", "replace")
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
pruefe("Traceback" not in ausgabe, f"ohne Absturz im zweiten Prozess ({ausgabe[-200:]!r})")
|
|
# Die Sperre steht VOR dem Datenordner: der zweite Prozess darf gar nicht erst
|
|
# so weit kommen, dass er das Journal oeffnet.
|
|
pruefe("Journal" not in ausgabe,
|
|
f"der zweite Prozess fasst das Journal nicht an ({ausgabe[-200:]!r})")
|
|
|
|
# ---- 3. Sperre loest sich beim Beenden ------------------------------------
|
|
erst.terminate()
|
|
warte_auf_ende(erst, 15)
|
|
pruefe(erst.poll() is not None, "das erste Fenster laesst sich beenden")
|
|
time.sleep(1.5)
|
|
|
|
dritt = starte()
|
|
laeufer.append(dritt)
|
|
code = warte_auf_ende(dritt, 8)
|
|
pruefe(code is None, f"danach geht das Programm wieder auf (Rueckgabe {code})")
|
|
dritt.terminate()
|
|
warte_auf_ende(dritt, 10)
|
|
if dritt.poll() is None:
|
|
dritt.kill()
|
|
|
|
for p in laeufer:
|
|
if p.poll() is None:
|
|
p.kill()
|
|
try:
|
|
p.stdout.close()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
if not fehler:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
else:
|
|
print(f" (Kopie bleibt zum Nachsehen: {tmp})")
|
|
print("---")
|
|
print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}")
|
|
sys.exit(1 if fehler else 0)
|