Fehler: War ein Popup offen und man klickte in ein anderes Programm, ließ sich das Popup über Hauptfenster, Taskleiste oder Alt+Tab nicht mehr nach vorn holen. Ursache: Rahmenlose Tk-Fenster mit transient() legt Tk unter Windows als Kindfenster des Desktops (WS_CHILD) an – ohne Besitzer, daher kam beim Zurückholen nur das (gesperrte) Hauptfenster nach vorn. - Popups (Dialoge, Rückfragen, Rundgang) ohne transient(), stattdessen Windows-Besitzer + Sperre des aufrufenden Fensters wie bei echten modalen Windows-Dialogen (gui/win32.py) - Rundgang: neue Sprechblase erscheint vor dem Entfernen der alten (sonst springt Windows weg) - Alt+F4 und Modalsperre robust gegen Timing (Sperre wird bei Bedarf nachgeholt) - Tests (nur unter Windows/Wine): Besitzer, Sperre, Stapelreihenfolge nach „woanders klicken“ und Zurückholen, verschachtelte Rückfragen, Rundgang - Tipp des Tages mit freigestelltem Büroklammer-Bild (build/make_tipp_bild.py), in der .exe enthalten und vom Selbsttest geprüft Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
183 lines
7.6 KiB
Python
183 lines
7.6 KiB
Python
"""Selbsttest der fertigen Programmdatei: `Provisionsmanager95.exe --selbsttest ergebnis.txt`
|
|
|
|
Läuft im Build (Wine + virtueller Bildschirm) gegen die tatsächlich ausgelieferte .exe. Startet die
|
|
echte Oberfläche in einem Wegwerf-Profil, spielt kritische Abläufe durch und schreibt „OK“ bzw. die
|
|
gefundenen Fehler in die Ergebnisdatei (Exit-Code 0 = bestanden).
|
|
|
|
Geprüft wird u. a. der Fall „Programm schließen, während der Einrichtungs-Assistent offen ist“
|
|
sowie Import und Export (openpyxl/xlrd müssen in der .exe enthalten sein).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import tkinter as tk
|
|
import traceback
|
|
from pathlib import Path
|
|
|
|
ZEITLIMIT = 60 # Sekunden je Szenario
|
|
|
|
|
|
class _Szenario:
|
|
def __init__(self, name: str, home: Path, einstellungen: str | None = None):
|
|
self.name, self.home, self.fehler = name, home, []
|
|
home.mkdir(parents=True, exist_ok=True)
|
|
os.environ["PROVTOOL_HOME"] = str(home)
|
|
if einstellungen:
|
|
(home / "einstellungen.json").write_text(einstellungen, encoding="utf-8")
|
|
from provtool.gui.app import App
|
|
self.root = tk.Tk()
|
|
self.app = App(self.root)
|
|
echt = self.root.report_callback_exception
|
|
|
|
def melden(exc, val, tb):
|
|
self.fehler.append("".join(traceback.format_exception(exc, val, tb)))
|
|
try:
|
|
echt(exc, val, tb)
|
|
except Exception:
|
|
self.fehler.append("Fehler-Handler ist abgestürzt:\n" + traceback.format_exc())
|
|
self.root.report_callback_exception = melden
|
|
|
|
def lebt(self) -> bool:
|
|
try:
|
|
return bool(self.root.winfo_exists())
|
|
except tk.TclError:
|
|
return False
|
|
|
|
def dialog(self, typ):
|
|
from provtool.gui import win95 as w
|
|
gefunden = []
|
|
|
|
def suche(widget):
|
|
for kind in widget.winfo_children():
|
|
if isinstance(kind, w.Dialog) and isinstance(kind, typ) and kind.winfo_exists():
|
|
gefunden.append(kind)
|
|
suche(kind)
|
|
if self.lebt():
|
|
suche(self.root)
|
|
return gefunden[0] if gefunden else None
|
|
|
|
def ablauf(self, schritte):
|
|
offen, start = list(schritte), time.time()
|
|
|
|
def poll():
|
|
if not self.lebt():
|
|
return
|
|
if time.time() - start > ZEITLIMIT:
|
|
self.fehler.append(f"Zeitüberschreitung bei: {offen[0][2] if offen else 'Ende'}")
|
|
try:
|
|
self.root.destroy()
|
|
except tk.TclError:
|
|
pass
|
|
return
|
|
if offen:
|
|
try:
|
|
bereit = offen[0][0]()
|
|
except Exception:
|
|
bereit = False
|
|
if bereit:
|
|
_, aktion, _ = offen.pop(0)
|
|
|
|
def ausfuehren(a=aktion):
|
|
try:
|
|
a()
|
|
except Exception:
|
|
self.fehler.append(traceback.format_exc())
|
|
self.root.after(0, ausfuehren)
|
|
self.root.after(20, poll)
|
|
|
|
self.root.after(20, poll)
|
|
self.root.mainloop()
|
|
if offen:
|
|
self.fehler.append("Nicht ausgeführt: " + ", ".join(s[2] for s in offen))
|
|
log = self.home / "fehler.log"
|
|
if log.exists() and log.read_text(encoding="utf-8").strip():
|
|
self.fehler.append("fehler.log:\n" + log.read_text(encoding="utf-8"))
|
|
return [f"[{self.name}] {f}" for f in self.fehler]
|
|
|
|
|
|
def _beispiel_csv(pfad: Path):
|
|
kopf = ("Zeitraum;Verguetungsart;Haendlernummer;Unterhaendler;Vertragsnummer;Aktiv;Deaktiv;Rufnummer;"
|
|
"Kartennummer;IMEI_SN;Geraetename;Kundenvorname;Kundename;Tarif-Kurz;Tarif;Prozent;Betrag")
|
|
zeilen = [kopf,
|
|
"2026.10.01-2026.10.07;SIM-Karten;1;111;1;05.10.2026;;1601111111;;;;;Muster GmbH;;Allnet;0;8",
|
|
"2026.10.01-2026.10.07;Sonderbonus Aktion;1;111;2;06.10.2026;;1602222222;;;;;Beispiel KG;;Classic;0;170"]
|
|
pfad.write_bytes(("\r\n".join(zeilen) + "\r\n").encode("cp1252"))
|
|
|
|
|
|
def ausfuehren(ergebnisdatei: str) -> int:
|
|
from provtool.gui.assistent import Einrichtungsassistent
|
|
basis = Path(tempfile.mkdtemp(prefix="pm95-selbsttest-"))
|
|
fehler: list[str] = []
|
|
immer = lambda: True # noqa: E731
|
|
try:
|
|
# 1) Gemeldeter Absturz: Schließen, während der Einrichtungs-Assistent offen ist
|
|
s = _Szenario("Schließen bei offenem Assistenten", basis / "a")
|
|
cmd = s.root.tk.call("wm", "protocol", s.root._w, "WM_DELETE_WINDOW")
|
|
fehler += s.ablauf([(lambda: s.dialog(Einrichtungsassistent) is not None, lambda: None, "Assistent erscheint"),
|
|
(immer, lambda: s.root.tk.call(cmd), "Fenster schließen")])
|
|
|
|
# 2) Assistent komplett, Import, Export, Rundgang, Beenden
|
|
csv = basis / "freenet.csv"
|
|
_beispiel_csv(csv)
|
|
s = _Szenario("Assistent, Import, Export", basis / "b")
|
|
wiz = lambda: s.dialog(Einrichtungsassistent) # noqa: E731
|
|
seite = lambda n: (lambda: wiz() is not None and wiz().nr == n) # noqa: E731
|
|
|
|
def haendler():
|
|
wiz().v_name.set("Selbsttest-Händler")
|
|
wiz().v_fn.set("111")
|
|
wiz()._haendler_dazu()
|
|
|
|
def abrechnung():
|
|
wiz().dateien.append(str(csv))
|
|
wiz().v_rundgang.set(False)
|
|
wiz()._weiter()
|
|
|
|
def import_bericht_zu():
|
|
from provtool.gui.dialogs import ImportBericht
|
|
s.dialog(ImportBericht).abbrechen()
|
|
|
|
def export():
|
|
from provtool import export as exp
|
|
app = s.app
|
|
pos = [p for p in app.positionen if p["haendler_id"] > 0]
|
|
dateien = exp.exportiere_haendler(basis / "export", "Selbsttest", pos, "Test")
|
|
if len(pos) != 2 or len(dateien) != 1 or not dateien[0].exists():
|
|
s.fehler.append(f"Export fehlerhaft: {len(pos)} Positionen, Dateien {dateien}")
|
|
|
|
from provtool.gui.assistent import TippDesTages
|
|
from provtool.gui.dialogs import ImportBericht
|
|
|
|
def tipp_pruefen():
|
|
d = s.dialog(TippDesTages)
|
|
if not d.bild_ok:
|
|
s.fehler.append("Bild für den Tipp des Tages fehlt in der .exe (provtool/gui/bilder/tipp.png)")
|
|
d.abbrechen()
|
|
|
|
fehler += s.ablauf([
|
|
(seite(0), lambda: wiz()._weiter(), "Willkommen"),
|
|
(seite(1), lambda: wiz()._weiter(), "Speicherort"),
|
|
(seite(2), haendler, "Händler anlegen"),
|
|
(lambda: len(s.app.db.haendler_liste()) == 1, lambda: wiz()._weiter(), "Händler → weiter"),
|
|
(seite(3), abrechnung, "Abrechnung mit Datei"),
|
|
(seite(4), lambda: wiz()._weiter(), "Fertig stellen"),
|
|
(lambda: s.dialog(ImportBericht) is not None, import_bericht_zu, "Import-Bericht schließen"),
|
|
(lambda: len(s.app.positionen) == 2, export, "Export"),
|
|
(immer, s.app.tipp_des_tages, "Tipp des Tages öffnen"),
|
|
(lambda: s.dialog(TippDesTages) is not None, tipp_pruefen, "Tipp-Bild in der .exe"),
|
|
(lambda: s.dialog(TippDesTages) is None, s.app.rundgang, "Rundgang"),
|
|
(lambda: getattr(s.app, "_rundgang", None) is not None, s.app.beenden, "Beenden während Rundgang"),
|
|
])
|
|
except Exception:
|
|
fehler.append(traceback.format_exc())
|
|
|
|
Path(ergebnisdatei).write_text("OK\n" if not fehler else "FEHLER\n\n" + "\n\n".join(fehler), encoding="utf-8")
|
|
return 0 if not fehler else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(ausfuehren(sys.argv[1] if len(sys.argv) > 1 else "selbsttest.txt"))
|