Add regression fixtures for blocked Wine MSAA event providers

Add a MinGW fixture and isolated D-Bus runner that stall WM_GETOBJECT or
accessible text-property retrieval for eight seconds. While the provider
is blocked, a separate foreground editor must deliver repeated character
events and native button feedback without changing its text, caret, or
selection. Reject stale focus after the blocked provider returns and verify
normal WM_QUIT shutdown completes before a second provider stall ends.

Cover both 64-bit and 32-bit applications, ordinary property-getter stalls,
background focus events that must not query providers or retire foreground
feedback, and owned nonactivating accessible popups that preserve native
focus. Support the alternate Orca recording endpoint as well as Cthulhu.

Use disposable Wine prefixes and recording services so these checks do not
send speech or braille to the running reader. Compile fixtures with the
matching MinGW C++ compiler and clean up the test processes and prefix's
Wine server when each run finishes.

Validation, run sequentially on a private dummy Xorg display (:94):
- cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=winelib-toolchain.cmake
- cmake --build build --parallel
- ctest --test-dir build --output-on-failure (4/4 passed)
- python3 tests/event_stall_integration.py --build-dir build, repeated with
  no extra option, --32-bit, --getter, --background, --popup, --reader orca
  (all six runs passed)
- python3 tests/menu_integration.py --build-dir build --fixture <fixture>,
  for menu, edit, controls, and list, each with and without --32-bit
  (all eight runs passed)
- git diff --check (passed)

Xvfb is not installed, so use the installed unprivileged Xorg binary and
dummy driver for isolation. These checks record D-Bus output and native
fixture state; installed-helper MUSHclient, physical input, audible speech,
and physical braille acceptance remain separate live checks.
This commit is contained in:
deedra committed 2026-10-12 23:31:03 -07:00
1 parent 4b58fc1cdc
commit 0c69564780
2 files changed
+272

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
"""Check event feedback and normal shutdown while a Wine MSAA provider stalls.
Run on a private X display. Uses a disposable prefix and a recording D-Bus
service; no speech reaches the user's reader or physical braille display.
"""
import argparse
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
from gi.repository import Gio, GLib
parser = argparse.ArgumentParser()
parser.add_argument("--build-dir", default="build")
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
parser.add_argument("--reader", choices=("cthulhu", "orca"), default="cthulhu")
parser.add_argument("--session", action="store_true")
parser.add_argument("--getter", action="store_true")
parser.add_argument("--background", action="store_true")
parser.add_argument("--popup", action="store_true")
args = parser.parse_args()
build = Path(args.build_dir).resolve()
if not args.session:
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable, __file__,
"--session", "--build-dir", str(build), "--reader", args.reader,
*(["--32-bit"] if args.is32Bit else []),
*(["--getter"] if args.getter else []),
*(["--background"] if args.background else []),
*(["--popup"] if args.popup else [])]))
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
objectPath = "/" + interface.replace(".", "/")
xml = '<node><interface name="' + interface + '"><method name="PresentAccessibleEvent">'
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in ["s"] * 7 + ["i", "i", "s"])
xml += '<arg type="b" direction="out"/></method></interface></node>'
events = []
def record(connection, sender, path, iface, method, parameters, invocation):
events.append(parameters.unpack())
invocation.return_value(GLib.Variant("(b)", (True,)))
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus",
"RequestName", GLib.Variant("(su)", (interface, 0)), None,
Gio.DBusCallFlags.NONE, 1000, None)
def pump_until(predicate, seconds):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
while GLib.MainContext.default().iteration(False):
pass
if predicate():
return
time.sleep(.005)
raise AssertionError(f"condition timed out; events: {events}")
with tempfile.TemporaryDirectory(prefix="wine-event-stall-") as temporary:
root = Path(temporary)
env = os.environ.copy()
env.update(WINEPREFIX=str(root / "prefix"), WINEDEBUG="-all",
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
ready, done, checked = root / "ready", root / "done", root / "checked"
for key, path in (("WINE_ACCESS_STALL_READY", ready), ("WINE_ACCESS_STALL_DONE", done),
("WINE_ACCESS_EDITOR_CHECKED", checked)):
env[key] = "Z:" + str(path).replace("/", "\\")
fixture = root / "fixture.exe"
compiler = "i686-w64-mingw32-g++" if args.is32Bit else "x86_64-w64-mingw32-g++"
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows", "-static", "-o", str(fixture),
str(Path(__file__).with_name("event_stall_fixture.cpp")), "-luser32", "-loleacc", "-lole32", "-loleaut32", "-luuid"], check=True)
children = []
try:
subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45)
helper = subprocess.Popen([str(build / "cthulhu-wine-access.exe"), "--no-controller",
"--reader", args.reader], env=env)
children.append(helper)
time.sleep(1)
modes = ["getter"] if args.getter else []
if args.background: modes.append("background")
if args.background or args.popup:
editor = subprocess.Popen(["wine", str(fixture), "editor", *(["popup"] if args.popup else [])], env=env)
children.append(editor)
if args.popup:
pump_until(lambda: editor.poll() is not None or any(e[2] == "Owned popup provider" for e in events), 3)
assert editor.poll() is None, f"popup fixture failed: {editor.returncode}"
assert any(e[2] == "Owned popup provider" for e in events)
print("PASS: accessible owned popup preserves foreground and native focus", flush=True)
sys.exit(0)
pump_until(lambda: any(e[1] == "text" and e[2] == "a" for e in events), 3)
stall = subprocess.Popen(["wine", str(fixture), *modes], env=env)
children.append(stall)
deadline = time.monotonic() + 2
while time.monotonic() < deadline:
while GLib.MainContext.default().iteration(False):
pass
time.sleep(.005)
assert not ready.exists(), "background provider was queried"
assert any(e[1] == "focus" and e[2] == "Responsive button" and not e[0].endswith(":standard") for e in events), "background event starved foreground MSAA"
print("PASS: background provider not queried; ordinary foreground MSAA feedback retained", flush=True)
sys.exit(0)
stall = subprocess.Popen(["wine", str(fixture), *modes], env=env)
children.append(stall)
pump_until(ready.exists, 10)
events.clear()
started = time.monotonic()
editor = subprocess.Popen(["wine", str(fixture), "editor"], env=env)
children.append(editor)
pump_until(lambda: sum(e[1] == "text" and e[2] == "a" for e in events) >= 2, 2)
elapsed = time.monotonic() - started
assert not done.exists(), "typing waited for the blocked provider"
assert helper.poll() is None, "helper exited"
print(f"PASS: repeated typing while MSAA stalled ({elapsed:.3f} s)", flush=True)
pump_until(lambda: any(e[1] == "focus" and e[2] == "Responsive button" for e in events), 2)
assert checked.exists(), "native text, caret or selection changed"
assert not done.exists(), "native button feedback waited for the blocked provider"
print("PASS: native button fallback; unchanged native text/caret/selection", flush=True)
pump_until(done.exists, 10)
# Drain enough time for an incorrectly queued late result to surface.
deadline = time.monotonic() + .5
while time.monotonic() < deadline:
while GLib.MainContext.default().iteration(False):
pass
time.sleep(.005)
assert not any(e[9] == "Stalled provider fixture" for e in events), "late stale focus escaped"
print("PASS: stale MSAA focus suppressed", flush=True)
ready.unlink(); done.unlink()
second = subprocess.Popen(["wine", str(fixture), *modes], env=env)
children.append(second)
pump_until(ready.exists, 10)
started = time.monotonic()
subprocess.run(["wine", str(fixture), "quit-helper"], env=env, check=True, timeout=3)
pump_until(lambda: helper.poll() is not None, 2)
assert helper.returncode == 0, f"normal helper shutdown failed: {helper.returncode}"
assert not done.exists(), "normal shutdown waited for the provider"
print(f"PASS: normal shutdown during MSAA stall ({time.monotonic() - started:.3f} s)", flush=True)
finally:
for child in reversed(children):
if child.poll() is None:
child.terminate()
try:
child.wait(timeout=3)
except subprocess.TimeoutExpired:
child.kill(); child.wait(timeout=3)
subprocess.run(["wineserver", "-k"], env=env, timeout=5)