Make the process of adding an orca extension easier.
This commit is contained in:
1 parent
a8118c12cf
commit
417ee40dda
6 files changed
+199
-35
No files matched your search
@@ -102,12 +102,29 @@ The controller test drives the helper through a native Windows NVDA RPC client.
|
||||
It verifies speech, cancellation, braille, SSML status forwarding, reader loss,
|
||||
invalid reader values, and startup refusal when only the unselected service
|
||||
exists. Both service names are owned while the selected reader handles requests.
|
||||
It also rejects invalid endpoint overrides, missing objects/interfaces,
|
||||
missing required methods and incorrect input/output signatures.
|
||||
The menu/editor and review tests also accept `--reader cthulhu`; omit the option
|
||||
to check existing default behavior. Review checks reject callers even when they
|
||||
own the unselected service, then replace the selected service owner and verify
|
||||
registration and capture access move to the new owner. Its provider object path
|
||||
and interface remain the same for both readers.
|
||||
|
||||
To check an Orca extension endpoint, add `--extension-endpoint`:
|
||||
|
||||
```sh
|
||||
xvfb-run -a python3 tests/controller_integration.py --build-dir build --reader orca --extension-endpoint
|
||||
xvfb-run -a python3 tests/menu_integration.py --build-dir build --reader orca --extension-endpoint
|
||||
xvfb-run -a python3 tests/menu_integration.py --build-dir build --reader orca --extension-endpoint --fixture edit
|
||||
xvfb-run -a python3 tests/review_integration.py --build-dir build --reader orca --extension-endpoint
|
||||
```
|
||||
|
||||
These publish `/org/gnome/Orca1/Service/WineAccess` with interface
|
||||
`org.gnome.Orca1.WineAccess`, while retaining the `org.gnome.Orca1.Service`
|
||||
bus name. Controller checks put overrides before reader selection; control
|
||||
and review checks put them after it. Review checks still reject unrelated
|
||||
callers and transfer capture access only when the selected bus owner changes.
|
||||
|
||||
These tests supply recording services implementing the Wine protocol; they do
|
||||
not run Orca or prove audible speech or physical braille. Stock Orca still needs
|
||||
the reader-side methods and helper lifecycle described in the
|
||||
|
||||
@@ -12,17 +12,21 @@ from gi.repository import Gio, GLib
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
||||
parser.add_argument("--extension-endpoint", action="store_true")
|
||||
parser.add_argument("--session", 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),
|
||||
*(["--extension-endpoint"] if args.extension_endpoint else []),
|
||||
*(["--reader", args.reader] if args.reader else [])]))
|
||||
|
||||
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||
busName = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||
interface = busName.rsplit(".", 1)[0] + ".WineAccess" if args.extension_endpoint else busName
|
||||
otherInterface = "org.stormux.Cthulhu1.Service" if args.reader == "orca" else "org.gnome.Orca1.Service"
|
||||
objectPath = "/" + interface.replace(".", "/")
|
||||
objectPath = "/" + busName.replace(".", "/") + ("/WineAccess" if args.extension_endpoint else "")
|
||||
endpointArguments = ["--reader-path", objectPath, "--reader-interface", interface] if args.extension_endpoint else []
|
||||
methods = {"SpeakText": ("sb", "b"), "CancelSpeech": ("", "b"),
|
||||
"BrailleMessage": ("s", "b"), "SpeakSsml": ("ss", "i")}
|
||||
xml = '<node><interface name="' + interface + '">'
|
||||
@@ -41,7 +45,7 @@ def record(connection, sender, path, iface, method, parameters, invocation):
|
||||
|
||||
|
||||
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
||||
registration = connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
||||
|
||||
|
||||
def name_call(method, name):
|
||||
@@ -51,13 +55,13 @@ def name_call(method, name):
|
||||
Gio.DBusCallFlags.NONE, 1000, None).unpack()[0]
|
||||
|
||||
|
||||
def wait_for_client(client):
|
||||
def wait_for_client(client, expected=0):
|
||||
deadline = time.monotonic() + 10
|
||||
while client.poll() is None and time.monotonic() < deadline:
|
||||
while GLib.MainContext.default().iteration(False):
|
||||
pass
|
||||
time.sleep(.005)
|
||||
assert client.poll() == 0, f"controller client failed or timed out: {client.poll()}"
|
||||
assert client.poll() == expected, f"client failed or timed out: {client.poll()}"
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
||||
@@ -80,6 +84,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
||||
helper = None
|
||||
client = None
|
||||
helperCommand = [str(build / "cthulhu-wine-access.exe"), "--no-dialog-reader",
|
||||
*endpointArguments,
|
||||
*(["--reader", args.reader] if args.reader else [])]
|
||||
try:
|
||||
subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45)
|
||||
@@ -87,11 +92,43 @@ with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
||||
assert name_call("RequestName", otherInterface) == 1
|
||||
result = subprocess.run(helperCommand, env=env, timeout=5)
|
||||
assert result.returncode == 1, "helper accepted an unselected reader"
|
||||
for readerArgs in (["--reader"], ["--reader", "unknown"]):
|
||||
for readerArgs in (["--reader"], ["--reader", "unknown"],
|
||||
["--reader-path"], ["--reader-path", ""],
|
||||
["--reader-path", "relative/path"], ["--reader-path", "/bad-path"],
|
||||
["--reader-interface"], ["--reader-interface", ""],
|
||||
["--reader-interface", "WineAccess"],
|
||||
["--reader-interface", "org.example.Bad-Interface"]):
|
||||
result = subprocess.run([str(build / "cthulhu-wine-access.exe"), *readerArgs],
|
||||
env=env, timeout=5)
|
||||
assert result.returncode == 1, "invalid reader argument was accepted"
|
||||
assert name_call("RequestName", interface) == 1
|
||||
assert result.returncode == 1, "invalid endpoint argument was accepted"
|
||||
assert name_call("RequestName", busName) == 1
|
||||
# An owned reader name is insufficient: refuse missing objects,
|
||||
# interfaces, methods and incompatible signatures before starting RPC.
|
||||
def reject(command):
|
||||
rejected = subprocess.Popen(command, env=env, stderr=subprocess.PIPE)
|
||||
try:
|
||||
wait_for_client(rejected, 1)
|
||||
return rejected.communicate()[1].decode()
|
||||
finally:
|
||||
if rejected.poll() is None:
|
||||
rejected.kill()
|
||||
rejected.wait()
|
||||
diagnostic = reject([*helperCommand, "--reader-path", "/Missing"])
|
||||
# Gio can introspect an absent object as an empty node.
|
||||
assert "reader object" in diagnostic or "reader interface" in diagnostic, diagnostic
|
||||
assert "reader interface" in reject([*helperCommand, "--reader-interface", "org.example.Missing"])
|
||||
connection.unregister_object(registration)
|
||||
for brokenXml in (
|
||||
xml.replace('<method name="CancelSpeech"><arg type="b" direction="out"/></method>', ''),
|
||||
xml.replace('<arg type="i" direction="out"/>', '<arg type="b" direction="out"/>'),
|
||||
xml.replace('<arg type="b" direction="in"/>', '<arg type="s" direction="in"/>')):
|
||||
broken = connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(brokenXml).interfaces[0], record, None, None)
|
||||
assert "reader method" in reject(helperCommand)
|
||||
connection.unregister_object(broken)
|
||||
registration = connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
||||
# Controller-only interfaces must not enable control reading.
|
||||
assert "PresentAccessibleEvent" in reject([part for part in helperCommand if part != "--no-dialog-reader"])
|
||||
print("PASS: endpoint validation and required method signatures")
|
||||
helper = subprocess.Popen(helperCommand, env=env)
|
||||
# The fixture waits for the RPC server to listen before making calls.
|
||||
client = subprocess.Popen(["wine", str(clientPath)], env=env)
|
||||
@@ -103,7 +140,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
||||
("BrailleMessage", ("Controller braille",)),
|
||||
("SpeakSsml", ("wine-nvda-controller", "<speak>Controller SSML</speak>")),
|
||||
], events
|
||||
assert name_call("ReleaseName", interface) == 1
|
||||
assert name_call("ReleaseName", busName) == 1
|
||||
client = subprocess.Popen(["wine", str(clientPath), "unavailable"], env=env)
|
||||
wait_for_client(client)
|
||||
print(f"PASS: {interface} controller routing, SSML status, service loss, and reader validation")
|
||||
|
||||
@@ -18,6 +18,7 @@ parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--session", action="store_true")
|
||||
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
||||
parser.add_argument("--extension-endpoint", action="store_true")
|
||||
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "listbox", "delphi", "winforms", "controls", "scintilla", "goldwave"), default="menu")
|
||||
parser.add_argument("--notepadpp-exe", type=Path)
|
||||
parser.add_argument("--goldwave-exe", type=Path)
|
||||
@@ -38,6 +39,7 @@ if not args.session:
|
||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
|
||||
__file__, "--session", "--build-dir", str(build),
|
||||
"--fixture", args.fixture,
|
||||
*(["--extension-endpoint"] if args.extension_endpoint else []),
|
||||
*(["--reader", args.reader] if args.reader else []),
|
||||
*(["--notepadpp-exe", str(args.notepadpp_exe.resolve())] if args.notepadpp_exe else []),
|
||||
*(["--goldwave-exe", str(args.goldwave_exe.resolve())] if args.goldwave_exe else []),
|
||||
@@ -45,9 +47,12 @@ if not args.session:
|
||||
*(["--late-helper"] if args.late_helper else []),
|
||||
*(["--restart-helper"] if args.restart_helper else [])]))
|
||||
|
||||
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||
object_path = "/" + interface.replace(".", "/")
|
||||
busName = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||
interface = busName.rsplit(".", 1)[0] + ".WineAccess" if args.extension_endpoint else busName
|
||||
object_path = "/" + busName.replace(".", "/") + ("/WineAccess" if args.extension_endpoint else "")
|
||||
readerArguments = ["--reader", args.reader] if args.reader else []
|
||||
if args.extension_endpoint:
|
||||
readerArguments += ["--reader-path", object_path, "--reader-interface", interface]
|
||||
xml = '<node><interface name="' + interface + '"><method name="PresentAccessibleEvent">'
|
||||
for index, kind in enumerate(["s"] * 7 + ["i", "i", "s"]):
|
||||
xml += f'<arg name="arg{index}" type="{kind}" direction="in"/>'
|
||||
@@ -62,7 +67,7 @@ connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
connection.register_object(object_path, 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,
|
||||
GLib.Variant("(su)", (busName, 0)), None,
|
||||
Gio.DBusCallFlags.NONE, 1000, None)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
|
||||
@@ -13,16 +13,21 @@ parser.add_argument('--build-dir', default='build')
|
||||
parser.add_argument('--32-bit', dest='is32', action='store_true')
|
||||
parser.add_argument('--session', action='store_true')
|
||||
parser.add_argument('--reader', choices=('cthulhu', 'orca'))
|
||||
parser.add_argument('--extension-endpoint', 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),
|
||||
*(['--extension-endpoint'] if args.extension_endpoint else []),
|
||||
*(['--reader', args.reader] if args.reader else []),
|
||||
*(['--32-bit'] if args.is32 else [])]))
|
||||
iface = 'org.gnome.Orca1.Service' if args.reader == 'orca' else 'org.stormux.Cthulhu1.Service'
|
||||
path = '/' + iface.replace('.', '/')
|
||||
busName = 'org.gnome.Orca1.Service' if args.reader == 'orca' else 'org.stormux.Cthulhu1.Service'
|
||||
iface = busName.rsplit('.', 1)[0] + '.WineAccess' if args.extension_endpoint else busName
|
||||
path = '/' + busName.replace('.', '/') + ('/WineAccess' if args.extension_endpoint else '')
|
||||
readerArguments = ['--reader', args.reader] if args.reader else []
|
||||
if args.extension_endpoint:
|
||||
readerArguments += ['--reader-path', path, '--reader-interface', iface]
|
||||
capture_iface = 'org.stormux.Cthulhu1.WineReview'
|
||||
capture_path = '/org/stormux/Cthulhu1/WineReview'
|
||||
|
||||
@@ -50,7 +55,7 @@ def record(conn, sender, object_path, interface, method, params, invocation):
|
||||
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
registration = connection.register_object(path, Gio.DBusNodeInfo.new_for_xml(interface_xml()).interfaces[0], record, None, None)
|
||||
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
||||
GLib.Variant('(su)',(iface,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
GLib.Variant('(su)',(busName,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
|
||||
def pump_until(predicate, seconds=5):
|
||||
end = time.monotonic()+seconds
|
||||
@@ -127,9 +132,9 @@ with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
||||
other.register_object(path, Gio.DBusNodeInfo.new_for_xml(interface_xml()).interfaces[0],record,None,None)
|
||||
providers.clear(); focuses.clear()
|
||||
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','ReleaseName',
|
||||
GLib.Variant('(s)',(iface,)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
GLib.Variant('(s)',(busName,)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
other.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
||||
GLib.Variant('(su)',(iface,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
GLib.Variant('(su)',(busName,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
pump_until(lambda:sender in providers and sender in focuses)
|
||||
focus=focuses[sender]
|
||||
wrong=request(connection,sender,focus); pump_until(lambda:bool(wrong),2)
|
||||
@@ -139,9 +144,9 @@ with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
||||
assert accepted[0][0].unpack()[0]['status']=='ok'
|
||||
providers.clear(); focuses.clear()
|
||||
other.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','ReleaseName',
|
||||
GLib.Variant('(s)',(iface,)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
GLib.Variant('(s)',(busName,)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
connection.call_sync('org.freedesktop.DBus','/org/freedesktop/DBus','org.freedesktop.DBus','RequestName',
|
||||
GLib.Variant('(su)',(iface,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
GLib.Variant('(su)',(busName,0)),None,Gio.DBusCallFlags.NONE,1000,None)
|
||||
pump_until(lambda:sender in providers and sender in focuses)
|
||||
other.close_sync(None)
|
||||
print('PASS: test_reader_owner_replacement')
|
||||
|
||||
Reference in new issue
Block a user