#!/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())