prole/tests/conftest.py
chrisfu 396cea60e7 installer: stabilize retries + improve k3s inventory detection
- Reuse namespace from existing prole.cfg unless env overrides

- Support k3s_hosts children groups and prefer k3s_servers as default

- Add headless pytest tkinter stubs and blocked-cluster reconciliation tests

- Update registry mirror endpoint, port mappings, and prole-db manifests
2026-03-07 23:57:21 -08:00

150 lines
4.1 KiB
Python

"""Global pytest configuration.
CI/autobuild must run unattended and non-interactive.
We do **not** globally replace the whole `tkinter` module because some tests
exercise real widget layout, but we *do* stub popup-oriented modules
(`tkinter.messagebox`, `tkinter.filedialog`) so no test can block on a dialog.
"""
from __future__ import annotations
import sys
import types
from unittest.mock import MagicMock
import pytest
try:
import tkinter as tk
except Exception: # pragma: no cover
tk = None # type: ignore[assignment]
def _configure_messagebox(msg: MagicMock) -> None:
# Confirmation-style prompts: auto-accept.
for fn in (
"askyesno",
"askokcancel",
"askretrycancel",
"askyesnocancel",
):
getattr(msg, fn).return_value = True
# Informational prompts: no-op.
for fn in (
"showinfo",
"showwarning",
"showerror",
):
getattr(msg, fn).return_value = None
def _install_popup_stubs() -> dict[str, MagicMock]:
messagebox = MagicMock(name="tkinter.messagebox")
filedialog = MagicMock(name="tkinter.filedialog")
_configure_messagebox(messagebox)
# Seed sys.modules so `from tkinter import messagebox, filedialog` picks up
# these stubs without requiring a real UI.
sys.modules["tkinter.messagebox"] = messagebox
sys.modules["tkinter.filedialog"] = filedialog
# If tkinter is already imported, ensure it points at our stub submodules.
tk_mod = sys.modules.get("tkinter")
if tk_mod is not None:
try:
setattr(tk_mod, "messagebox", messagebox)
setattr(tk_mod, "filedialog", filedialog)
except Exception:
pass
return {
"tkinter.messagebox": messagebox,
"tkinter.filedialog": filedialog,
}
_POPUP_STUBS = _install_popup_stubs()
@pytest.fixture(scope="session", autouse=True)
def _tk_default_root_session():
"""Ensure a default Tkinter root exists for tests that create variables.
Some UI logic uses `tkinter.StringVar`/`BooleanVar` without explicitly
providing a master, which requires a default root. In headless contexts we
prefer a `Tcl()` interpreter fallback.
"""
if tk is None:
yield
return
existing = getattr(tk, "_default_root", None)
if existing is not None:
yield
return
root = None
try:
root = tk.Tk()
try:
root.withdraw()
except Exception:
pass
except Exception:
try:
root = tk.Tcl()
try:
tk._default_root = root # type: ignore[attr-defined]
except Exception:
pass
except Exception:
root = None
try:
yield
finally:
try:
if root is not None and hasattr(root, "destroy"):
root.destroy()
except Exception:
pass
try:
tk._default_root = None # type: ignore[attr-defined]
except Exception:
pass
def _stub_module(name: str, *, is_package: bool = False) -> types.ModuleType:
m = types.ModuleType(name)
if is_package:
# Mark as a package so submodule imports can succeed.
m.__path__ = [] # type: ignore[attr-defined]
return m
def _ensure_importable_or_stub(name: str, *, is_package: bool = False) -> None:
if name in sys.modules:
return
try:
__import__(name)
except Exception:
sys.modules[name] = _stub_module(name, is_package=is_package)
# Optional UI/OS modules: only stub when unavailable (keeps real deps when present).
_ensure_importable_or_stub("Foundation")
_ensure_importable_or_stub("AppKit")
_ensure_importable_or_stub("PIL", is_package=True)
_ensure_importable_or_stub("PIL.Image")
_ensure_importable_or_stub("PIL.ImageTk")
@pytest.fixture(autouse=True)
def _reset_tkinter_stubs_between_tests():
# Avoid test-order coupling by resetting call history between tests.
for mod in _POPUP_STUBS.values():
mod.reset_mock()
_configure_messagebox(_POPUP_STUBS["tkinter.messagebox"])
yield