102 lines
5.1 KiB
Python
102 lines
5.1 KiB
Python
"""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 = '<node><interface name="' + interface + '">'
|
|
for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", ""),
|
|
("PresentAccessibleEvent", "sssssssiis")):
|
|
xml += '<method name="' + method + '">'
|
|
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
|
|
xml += '<arg type="b" direction="out"/></method>'
|
|
xml += '</interface></node>'
|
|
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)
|