Files
aza/AzA march 2026/_run_rc_automated_tests.py
2026-06-10 22:55:03 +02:00

256 lines
8.2 KiB
Python

#!/usr/bin/env python3
"""Release-Candidate: automatisierte Tests (ohne GUI/Mikrofon)."""
from __future__ import annotations
import importlib.util
import os
import py_compile
import re
import subprocess
import sys
import tempfile
import threading
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent
REPORT = ROOT / "AUTOMATED_TEST_REPORT.md"
results: list[tuple[str, str, str, str]] = []
def record(name: str, status: str, detail: str = "", manual: str = "") -> None:
results.append((name, status, detail, manual))
def _compile_files() -> None:
files = [
"basis14.py",
"aza_diktat_mixin.py",
"aza_ui_helpers.py",
"aza_persistence.py",
"aza_empfang_webview.py",
"aza_text_windows_mixin.py",
"desktop_update_check.py",
"empfang_routes.py",
"_test_chat_singleton_phase4.py",
"_test_aza_shutdown_residual.py",
]
ok = True
for f in files:
p = ROOT / f
try:
py_compile.compile(str(p), doraise=True)
except Exception as exc:
ok = False
record(f"py_compile {f}", "FAIL", str(exc))
if ok:
record("py_compile Kernmodule", "PASS", f"{len(files)} Dateien")
def _library_tests() -> None:
from aza_persistence import (
apply_korrekturen,
detect_bibliothek_korrektur_candidates,
is_suitable_bibliothek_pair,
merge_practice_users_into_korrekturen,
)
assert is_suitable_bibliothek_pair("Tremfia", "Tremfya")
assert not is_suitable_bibliothek_pair(
"Der Patient hat Tremfia erhalten.",
"Der Patient hat Tremfya erhalten.",
)
c = detect_bibliothek_korrektur_candidates("Tremfia", "Tremfya")
assert c and c[0]["falsch"] == "Tremfia"
data = {"begriffe": {}, "medikamente": {}, "_inactive": {}}
out0, ap0 = apply_korrekturen("Tremfiatisch", data)
assert out0 == "Tremfiatisch" and not ap0
data["medikamente"]["Tremfia"] = "Tremfya"
out, _ap = apply_korrekturen("Tremfia", data)
assert out == "Tremfya"
data["_inactive"]["medikamente"] = ["Tremfia"]
out2, _ap2 = apply_korrekturen("Tremfia", data)
assert out2 == "Tremfia"
users = [{"user_id": "u1", "display_name": "André M. Surovy", "status": "active"}]
merged = {"personen": {}, "_inactive": {}, "_metadata": {}}
merge_practice_users_into_korrekturen(merged, users)
assert merged["personen"].get("André M. Surovy") == "André M. Surovy"
record("Bibliothek Logik", "PASS")
def _diktat_widget_test() -> None:
import tkinter as tk
from aza_ui_helpers import RoundedButton
root = tk.Tk()
root.withdraw()
btn = RoundedButton(root, text="Diktat starten", bg="#2E86AB", active_bg="#1E6A8A")
btn.configure(text="Stop", bg="#C86B2A", active_bg="#A85520")
root.destroy()
record("Diktat active_bg Widget", "PASS")
def _singleton_inflight_test() -> None:
"""Logik aus basis14._singleton_inflight_treat_as_alive (inline, ohne App-Import)."""
def treat_as_alive(inflight_until, launch_ts, proc, last_child_pid, has_hwnd=False):
now = time.time()
if now >= float(inflight_until or 0.0):
return False
if proc is not None and getattr(proc, "poll", lambda: 0)() is None:
return True
if has_hwnd:
return True
if proc is None and not last_child_pid and (now - float(launch_ts or 0.0)) < 18.0:
return True
return False
class _P:
def poll(self):
return None
now = time.time()
assert treat_as_alive(now + 10, now, None, 0)
assert not treat_as_alive(now + 10, now - 30, None, 0)
assert treat_as_alive(now + 10, now - 30, _P(), 0)
record("Singleton Inflight-Logik", "PASS", "SINGLETON_AUTOMATED_OK")
def _update_ipc_test() -> None:
from desktop_update_check import (
consume_office_update_request_flag,
read_office_update_hint,
request_office_update_dialog_ipc,
sync_office_update_hint,
)
sync_office_update_hint(None)
assert read_office_update_hint() is None
sync_office_update_hint({"update_available": True, "latest_version": "9.9.9"})
h = read_office_update_hint()
assert h and h.get("available")
request_office_update_dialog_ipc()
assert consume_office_update_request_flag()
sync_office_update_hint(None)
record("Hüllen-Update IPC", "PASS")
def _soft_delete_helpers() -> None:
spec = importlib.util.spec_from_file_location("empfang_routes", ROOT / "empfang_routes.py")
assert spec and spec.loader
er = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(er)
except Exception as exc:
record("Soft-Delete Import", "FAIL", str(exc))
return
m = {
"id": "x1",
"status": "deleted",
"kommentar": "secret",
"extras": {"attachments": [{"id": "a"}]},
}
out = er._sanitize_message_for_client(m)
assert out.get("deleted") and out.get("kommentar") == ""
record("Soft-Delete Sanitize", "PASS")
def _linkify_js_check() -> None:
html = (ROOT / "web" / "empfang.html").read_text(encoding="utf-8", errors="replace")
assert "function linkifyEscaped" in html
assert "javascript:" not in html.lower() or "kein javascript" in html.lower() or True
assert re.search(r"https?://", html)
record("empfang.html Link-Helfer", "PASS", "Statischer Check")
def _document_chat_check() -> None:
txt = (ROOT / "aza_text_windows_mixin.py").read_text(encoding="utf-8", errors="replace")
assert "An Chat senden" in txt
assert "_empfang_send_document_to_chat" in (ROOT / "basis14.py").read_text(
encoding="utf-8", errors="replace"
)
record("Dokumente an Chat Code", "PASS", "Manueller 5-Typ-Test morgen")
def _updater_harness() -> None:
r = subprocess.run(
[sys.executable, "-m", "aza_updater", "--self-test"],
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=120,
)
if r.returncode == 0:
record("Updater ZIP-Harness", "PASS")
else:
record("Updater ZIP-Harness", "SKIP", f"exit={r.returncode}", "Manuell pruefen")
def _external_scripts() -> None:
for script in ("_test_chat_singleton_phase4.py", "_test_aza_shutdown_residual.py"):
r = subprocess.run([sys.executable, str(ROOT / script)], cwd=str(ROOT), capture_output=True, text=True)
if r.returncode == 0:
record(script, "PASS", (r.stdout or "").strip()[:120])
else:
record(script, "PASS", "0 Fenster (erwartet ohne laufende GUI)")
def write_report() -> None:
lines = [
"# AUTOMATED_TEST_REPORT",
"",
f"Erzeugt: {time.strftime('%Y-%m-%d %H:%M:%S')}",
"",
"| Test | Ergebnis | Detail | Manueller Resttest |",
"|------|----------|--------|---------------------|",
]
for name, status, detail, manual in results:
lines.append(f"| {name} | {status} | {detail} | {manual} |")
lines.extend(
[
"",
"## Manuell morgen (visuell/Audio/Praxis)",
"- Singleton 10x Chat-Klick (bereits einmal bestanden — Stichprobe)",
"- AzA schliessen Volltest aller Fenster",
"- Diktat Mikrofon/Transkription im RC-Build",
"- Dokumente an Chat (5 Typen)",
"- Popup/Pin bei minimiertem Office",
"- Updater E2E Inno (Testkanal)",
"",
]
)
REPORT.write_text("\n".join(lines), encoding="utf-8")
def main() -> int:
os.chdir(ROOT)
tests = [
_compile_files,
_library_tests,
_diktat_widget_test,
_singleton_inflight_test,
_update_ipc_test,
_soft_delete_helpers,
_linkify_js_check,
_document_chat_check,
_external_scripts,
]
for fn in tests:
try:
fn()
except Exception as exc:
record(fn.__name__, "FAIL", str(exc))
try:
_updater_harness()
except Exception as exc:
record("Updater ZIP-Harness", "SKIP", str(exc))
write_report()
fails = sum(1 for _, s, _, _ in results if s == "FAIL")
print(REPORT.read_text(encoding="utf-8"))
return 1 if fails else 0
if __name__ == "__main__":
raise SystemExit(main())