S1005403_RisCC/tests/conftest.py

559 lines
14 KiB
Python

"""
Test conftest to provide headless-safe tkinter/ttk stubs so GUI tests can run in CI
without a real Tcl/Tk installation. This file is intentionally minimal: it implements
only the methods and attributes used by the project test-suite.
"""
from types import SimpleNamespace
import builtins
import tkinter as tk
from tkinter import ttk
class DummyVar:
def __init__(self, value=None):
self._value = value
self._trace_callbacks = []
def get(self):
return self._value
def set(self, v):
self._value = v
# invoke trace callbacks registered via trace_add ('write' mode)
try:
for cb in list(self._trace_callbacks):
try:
cb()
except Exception:
pass
except Exception:
pass
def trace_add(self, mode, callback):
# minimal shim: accept registration and return a token
try:
if callable(callback):
self._trace_callbacks.append(callback)
except Exception:
pass
return f"trace-{len(self._trace_callbacks)}"
def trace_remove(self, mode, token):
# best-effort removal by index token format
try:
if token and token.startswith("trace-"):
idx = int(token.split("-", 1)[1]) - 1
if 0 <= idx < len(self._trace_callbacks):
self._trace_callbacks.pop(idx)
except Exception:
pass
class DummyWidget:
def __init__(self, master=None, **kwargs):
self.master = master
self.children = {}
self._config = dict(kwargs)
# Option database placeholder used by some GUI code (kept as method _options)
# expose a tk interpreter object for tkinter internals
try:
self.tk = getattr(master, "tk")
except Exception:
# Provide a tiny interpreter facade used by some widgets/PIL backends
class DummyInterp:
def __init__(self):
self._commands = {}
def createcommand(self, name, func):
self._commands[name] = func
return name
def deletecommand(self, name):
self._commands.pop(name, None)
def call(self, *args, **kwargs):
return None
self.tk = DummyInterp()
# default identifiers used by tkinter internals
self._last_child_ids = {}
parent_w = getattr(master, "_w", ".")
self._w = parent_w + f".{id(self)}"
# geometry managers
def pack(self, *a, **k):
return None
def grid(self, *a, **k):
return None
def place(self, *a, **k):
return None
def pack_forget(self):
return None
def grid_remove(self):
# mirror tkinter widget api used by some code paths
return None
def pack_propagate(self, flag=None):
# emulate tkinter.Frame.pack_propagate; tests only set False to disable
# geometry propagation. We store it for introspection if needed.
if not hasattr(self, "_pack_propagate"):
self._pack_propagate = True
if flag is not None:
self._pack_propagate = bool(flag)
return None
def config(self, **kwargs):
self._config.update(kwargs)
# tkinter widgets often expose 'configure' as an alias
def configure(self, **kwargs):
return self.config(**kwargs)
def bind(self, *a, **k):
return None
def destroy(self):
return None
def grab_set(self):
return None
def grab_release(self):
return None
def after(self, ms, func=None, *args):
# return a fake id
return f"after-{id(self)}-{ms}"
def after_idle(self, func, *args):
# matplotlib backend expects after_idle to schedule an idle call.
# For headless tests call immediately and return an id-like token.
try:
func(*args)
except Exception:
# ignore exceptions raised by idle work in tests
pass
return f"after-idle-{id(self)}"
def after_cancel(self, id_):
return None
def see(self, *a, **k):
return None
def focus(self, *a, **k):
return None
def focus_set(self):
return None
def selection_set(self, *a, **k):
return None
def __setitem__(self, key, value):
# allow widget[key] = child assignment used by some windows
self.children[key] = value
def __getitem__(self, key):
return self.children.get(key)
def selection(self):
return []
# window-like helpers expected on Toplevel/Tk
def title(self, *a, **k):
return None
def geometry(self, *a, **k):
return None
def transient(self, master):
return None
def resizable(self, a, b):
return None
def protocol(self, *a, **k):
return None
def minsize(self, *a, **k):
return None
def rowconfigure(self, index, **kwargs):
if not hasattr(self, "_row_config"):
self._row_config = {}
self._row_config[index] = kwargs
return None
def columnconfigure(self, index, **kwargs):
if not hasattr(self, "_col_config"):
self._col_config = {}
self._col_config[index] = kwargs
return None
# aliases some code expects
def grid_rowconfigure(self, index, **kwargs):
return self.rowconfigure(index, **kwargs)
def grid_columnconfigure(self, index, **kwargs):
return self.columnconfigure(index, **kwargs)
def set(self, *a, **k):
# scrollbar.set compatibility
return None
def create_image(self, *a, **k):
return f"img-{id(self)}"
def create_rectangle(self, *a, **k):
return f"rect-{id(self)}"
def create_line(self, *a, **k):
return f"line-{id(self)}"
def create_oval(self, *a, **k):
return f"oval-{id(self)}"
def delete(self, *a, **k):
# generic delete for canvas or widgets
return None
def itemconfig(self, *a, **k):
return None
def move(self, *a, **k):
return None
def create_text(self, *a, **k):
return f"text-{id(self)}"
def create_window(self, *a, **k):
return f"window-{id(self)}"
def cget(self, key):
return self._config.get(key)
def winfo_exists(self):
return True
def winfo_width(self):
return getattr(self, "_width", 100)
def winfo_height(self):
return getattr(self, "_height", 100)
def winfo_children(self):
return list(self.children.values())
def pack_info(self):
return {}
def grid_info(self):
return {}
def winfo_toplevel(self):
# walk up to the top-level
node = self
while getattr(node, "master", None) is not None:
node = node.master
return node
def winfo_rootx(self):
return 0
def winfo_rooty(self):
return 0
def winfo_x(self):
return 0
def winfo_y(self):
return 0
def wait_window(self, widget=None):
# non-blocking stub for tests
return None
def _options(self, options: dict):
"""Convert an options dict into the flattened list expected by
tk interpreter calls (e.g. ['-title', 'MyTitle', '-message', '...']).
Tests only need that this is callable and returns a sequence.
"""
res = []
for k, v in (options or {}).items():
# tkinter option names are prefixed with '-' in the interpreter
res.append(f"-{k}")
res.append(v)
return res
# Minimal text buffer API used by scrolledtext.ScrolledText in the GUI
def insert(self, index, text, tag=None):
if not hasattr(self, "_text"):
self._text = ""
# simple append semantics for tests
self._text = (self._text or "") + str(text)
def delete(self, start, end=None):
# clear buffer entirely for simplicity
self._text = ""
def get(self, start, end=None):
return getattr(self, "_text", "")
def tag_config(self, *a, **k):
return None
def index(self, idx):
# return a dummy index
return "1.0"
def yview(self, *a, **k):
# report full-range view (user at bottom) to allow auto-scroll logic
return (0.0, 1.0)
class DummyTreeview(DummyWidget):
def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self._items = {}
def delete(self, *items):
for it in items:
if it in self._items:
del self._items[it]
def insert(self, parent, pos, iid=None, values=None):
if iid is None:
# generate a deterministic id
iid = f"item-{len(self._items)+1}"
self._items[iid] = values
return iid
def get_children(self):
return list(self._items.keys())
def heading(self, *a, **k):
return None
def column(self, *a, **k):
return None
def configure(self, **kwargs):
# accept yscroll or other widget bindings
self._config.update(kwargs)
return None
def yview(self, *a, **k):
# Treeview-specific yview may be used by some code; keep full-range
return (0.0, 1.0)
def item(self, iid, **kwargs):
# emulate getting/setting an item
if kwargs:
# set values if provided
if "values" in kwargs:
self._items[iid] = kwargs["values"]
return None
return {"values": self._items.get(iid)}
def set(self, item, column=None, value=None):
# behave like setting a cell value
if item in self._items:
vals = list(self._items[item]) if self._items[item] is not None else []
# try to set by index
try:
idx = int(column)
# expand if needed
while len(vals) <= idx:
vals.append(None)
vals[idx] = value
self._items[item] = tuple(vals)
except Exception:
# fallback: ignore
pass
def selection(self):
# return empty selection by default
return []
class DummyTk(DummyWidget):
def __init__(self, *a, **k):
super().__init__(None)
# tk attribute used by tkinter internals: provide a fake interpreter
class _Interp:
def __init__(self):
self._commands = {}
def createcommand(self, name, func):
self._commands[name] = func
return name
def deletecommand(self, name):
self._commands.pop(name, None)
def call(self, *args, **kwargs):
return None
self.tk = _Interp()
self._last_child_ids = {}
self._w = "."
def after(self, ms, func=None, *args):
# For logger tests which schedule via after
return f"after-{ms}"
def title(self, *a, **k):
return None
def geometry(self, *a, **k):
return None
def protocol(self, *a, **k):
return None
def withdraw(self):
return None
def wait_window(self, widget=None):
return None
def focus_set(self):
return None
def winfo_x(self):
return 0
self._w = "."
# emulate option database expected by some GUI code
self._options = {}
def winfo_y(self):
return 0
# Apply the lightweight stubs to tkinter and ttk used names
tk.Tk = DummyTk
tk.Toplevel = DummyWidget
tk.StringVar = DummyVar
tk.BooleanVar = DummyVar
tk.DoubleVar = DummyVar
tk.IntVar = DummyVar
ttk.Frame = DummyWidget
ttk.LabelFrame = DummyWidget
ttk.Scrollbar = DummyWidget
ttk.Button = DummyWidget
ttk.Label = DummyWidget
ttk.Scale = DummyWidget
ttk.Combobox = DummyWidget
ttk.Checkbutton = DummyWidget
ttk.Treeview = DummyTreeview
# Replace tk.Canvas as matplotlib backend sometimes creates it
tk.Canvas = DummyWidget
# Ensure scrolledtext.ScrolledText uses our DummyWidget so get/insert/delete work
try:
import tkinter.scrolledtext as scrolledtext
scrolledtext.ScrolledText = DummyWidget
except Exception:
pass
class DummyMenu(DummyWidget):
def add_cascade(self, label=None, menu=None, **kwargs):
return None
def add_command(self, label=None, command=None, **kwargs):
return None
def add_separator(self):
return None
tk.Menu = DummyMenu
# Add set to scrollbar widget
def _scrollbar_set(*a, **k):
return None
ttk.Scrollbar.set = staticmethod(lambda *a, **k: None)
# Some modules import names at top-level; also set them on builtins to be safe
builtins._DUMMY_TK_INSTALLED_FOR_TESTS = True
# PhotoImage stub used by some GUI code
class DummyPhotoImage:
def __init__(self, *a, **k):
self._size = k.get("size", (1, 1))
# Provide a .tk attribute similar to real PhotoImage which holds the tk interpreter
try:
self.tk = getattr(tk, "_default_root").tk
except Exception:
self.tk = SimpleNamespace(call=lambda *a, **k: None)
def subsample(self, *a, **k):
return self
def zoom(self, *a, **k):
return self
tk.PhotoImage = DummyPhotoImage
# Minimal ttk.Style stub
class DummyStyle:
def __init__(self):
self._styles = {}
def configure(self, style_name, **kwargs):
self._styles[style_name] = kwargs
def lookup(self, style_name, option, default=None):
return self._styles.get(style_name, {}).get(option, default)
ttk.Style = DummyStyle
# If PIL.ImageTk is available, ensure its PhotoImage is replaced by our dummy to avoid
# AttributeError when Pillow's PhotoImage expects a real tk interpreter in headless tests.
try:
from PIL import ImageTk
ImageTk.PhotoImage = DummyPhotoImage
except Exception:
pass
# provide a default root used by some libraries
_DEFAULT_ROOT = DummyTk()
tk._default_root = _DEFAULT_ROOT
# ensure Scrollbar is available on tk and ttk
tk.Scrollbar = DummyWidget
ttk.Scrollbar = DummyWidget
# add update stubs used by some widgets/backends
def _noop_update(self=None):
return None
DummyWidget.update = _noop_update
DummyWidget.update_idletasks = _noop_update
DummyTk.update = _noop_update
DummyTk.update_idletasks = _noop_update