"""Route real UIA notifications through native hooks to a private reader.""" import argparse import os from pathlib import Path import subprocess import sys import time from gi.repository import Gio, GLib from wine_test_environment import add_test_options, run_wine_test, wine_test_directory parser = argparse.ArgumentParser() parser.add_argument("--build-dir", default="build") parser.add_argument("--reader", choices=("cthulhu", "orca"), default="cthulhu") parser.add_argument("--32-bit", dest="is32Bit", action="store_true") parser.add_argument("--session", action="store_true") parser.add_argument("--prism-dll", type=Path) parser.add_argument("--prism-include", type=Path) add_test_options(parser) args = parser.parse_args() if bool(args.prism_dll) != bool(args.prism_include) or (args.prism_dll and args.is32Bit): parser.error("Prism checks require both --prism-dll and --prism-include, and a 64-bit DLL") build = Path(args.build_dir).resolve() if not args.session: sys.exit(run_wine_test(["dbus-run-session", "--", sys.executable, __file__, "--session", "--build-dir", str(build), "--reader", args.reader, *(["--prism-dll", str(args.prism_dll.resolve()), "--prism-include", str(args.prism_include.resolve())] if args.prism_dll else []), *(["--32-bit"] if args.is32Bit else [])], build, "notification_integration", keep=args.keep, keepOnFailure=args.keepOnFailure)) interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service" objectPath = "/" + interface.replace(".", "/") xml = '' for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", ""), ("PresentAccessibleEvent", "sssssssiis")): xml += '' xml += ''.join('' for kind in inputs) xml += '' xml += '' events = [] def record(connection, sender, path, iface, method, parameters, invocation): if method != "PresentAccessibleEvent": events.append((method, 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) with wine_test_directory(prefix="wine-access-notifications-") as temporary: tempPath = Path(temporary) env = os.environ.copy() env.update(WINEPREFIX=str(tempPath / "prefix"), WINEDEBUG="-all", WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1") fixture = tempPath / "notification-fixture.exe" compiler = "i686-w64-mingw32-gcc" if args.is32Bit else "x86_64-w64-mingw32-gcc" subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows", "-o", str(fixture), *(["-DWINE_ACCESS_PRISM_FIXTURE", "-I" + str(args.prism_include.resolve())] if args.prism_dll else []), str(Path(__file__).with_name("notification_fixture.c")), "-luser32", "-lole32", "-loleaut32", "-luiautomationcore", "-lshell32"], check=True) helper = app = None try: subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45) helper = subprocess.Popen([str(build / "wine-access.exe"), "--no-controller", "--reader", args.reader], env=env) time.sleep(.5) app = subprocess.Popen(["wine", str(fixture), *(["Z:" + str(args.prism_dll.resolve()).replace("/", "\\")] if args.prism_dll else [])], env=env) deadline = time.monotonic() + 20 while app.poll() is None and time.monotonic() < deadline: while GLib.MainContext.default().iteration(False): pass time.sleep(.005) while GLib.MainContext.default().iteration(False): pass assert app.poll() == 0, f"notification fixture failed: {app.poll()}" assert helper.poll() is None, "helper exited during notifications" assert events == [ ("SpeakText", ("Menu caf\u00e9 \U0001f600", False)), ("SpeakText", ("Second item", True)), ("CancelSpeech", ()), ("SpeakText", ("x" * 4094, False)), ], events print(f"PASS: {32 if args.is32Bit else 64}-bit UIA text, interruption, cancellation, " f"Unicode, bounded text, and background silence through {args.reader}") finally: for child in (app, helper): if child is not None and child.poll() is None: child.terminate() child.wait(timeout=5) subprocess.run(["wineserver", "-k"], env=env, timeout=5)