Code optimization and bug fixes.

This commit is contained in:
ak47 committed 2026-10-14 20:55:28 -04:00
1 parent 4c47cc5fc4
commit b16c6968ae
11 files changed
+124 -28

No files matched your search

+5
View File
@@ -104,6 +104,8 @@ 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.
UIA speech and cancellation methods are checked even with the RPC controller
disabled; missing methods and incompatible input/output signatures must fail startup.
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
@@ -308,6 +310,9 @@ input rejected at the text limit. Command-history checks consume queued Up/Down
before dispatch, then replace the text and move the caret as MUSHclient does.
They require ordered, single announcements for equal-length and rapid recalls
and empty input, with no password, background or unrelated programmatic output.
An unchanged history boundary is followed by a worker-thread text replacement
before another queued message is removed. That replacement must stay silent,
and subsequent navigation must read from the updated text snapshot.
Both 32-bit and 64-bit fixtures use the same assertions. Lifecycle variants
attach after the control has focus or restart the helper without closing it.
Every text action must have an established
+30 -6
View File
@@ -29,12 +29,18 @@ objectPath = "/" + busName.replace(".", "/") + ("/WineAccess" if args.extension_
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 + '">'
for method, (inputs, output) in methods.items():
xml += '<method name="' + method + '">'
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
xml += '<arg type="' + output + '" direction="out"/></method>'
xml += '</interface></node>'
def interface_xml(methods):
xml = '<node><interface name="' + interface + '">'
for method, (inputs, output) in methods.items():
xml += '<method name="' + method + '">'
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
xml += '<arg type="' + output + '" direction="out"/></method>'
return xml + '</interface></node>'
xml = interface_xml(methods)
events = []
@@ -125,6 +131,24 @@ with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
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)
# UIA still uses direct speech when the NVDA RPC controller is disabled.
dialogMethods = {"SpeakText": ("sb", "b"), "CancelSpeech": ("", "b"),
"PresentAccessibleEvent": ("sssssssiis", "b")}
dialogCommand = ["--no-controller" if part == "--no-dialog-reader" else part
for part in helperCommand]
for method in ("SpeakText", "CancelSpeech"):
for signature in (None, ("s", "b"), (dialogMethods[method][0], "i")):
brokenMethods = dialogMethods.copy()
if signature is None:
del brokenMethods[method]
else:
brokenMethods[method] = signature
broken = connection.register_object(objectPath,
Gio.DBusNodeInfo.new_for_xml(interface_xml(brokenMethods)).interfaces[0],
record, None, None)
assert "reader method " + method in reject(dialogCommand)
connection.unregister_object(broken)
print("PASS: UIA speech methods required without the controller")
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"])
+32 -2
View File
@@ -22,6 +22,29 @@ static HWND editor, password, history;
static int step;
static int history_position;
static DWORD WINAPI update_history_from_worker(void *window) {
SendMessageW((HWND)window, WM_SETTEXT, 0, (LPARAM)L"unrelated worker update");
return 0;
}
static void unrelated_history_update(void) {
HANDLE thread = CreateThread(NULL, 0, update_history_from_worker, history, 0, NULL);
if (!thread) ExitProcess(2);
// Dispatch sent messages without removing a queued message, as GetMessage
// does while waiting for input. Do not rely on worker/timer scheduling.
for (;;) {
DWORD result = MsgWaitForMultipleObjects(1, &thread, FALSE, 2000, QS_SENDMESSAGE);
if (result == WAIT_OBJECT_0) break;
if (result != WAIT_OBJECT_0 + 1) ExitProcess(2);
MSG message;
PeekMessageW(&message, NULL, 0, 0, PM_NOREMOVE);
}
CloseHandle(thread);
WCHAR text[64];
GetWindowTextW(history, text, 64);
if (lstrcmpW(text, L"unrelated worker update")) ExitProcess(2);
}
// Model MUSHclient's PreTranslateMessage: consume queued arrows before the
// Edit procedure, then use SetWindowText and SetSel to recall a command.
static BOOL history_input(const MSG *message) {
@@ -29,7 +52,10 @@ static BOOL history_input(const MSG *message) {
(message->wParam != VK_UP && message->wParam != VK_DOWN)) return FALSE;
if (message->wParam == VK_UP && history_position < 2) ++history_position;
else if (message->wParam == VK_DOWN && history_position > 0) --history_position;
else return TRUE;
else {
if (step == 49) unrelated_history_update();
return TRUE;
}
const WCHAR *commands[] = {L"", L"history north", L"history south"};
SetWindowTextW(history, commands[history_position]);
int length = GetWindowTextLengthW(history);
@@ -239,10 +265,14 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); // no history change
break;
case 50:
// The silent worker update must still refresh the navigation snapshot.
PostMessageW(history, WM_KEYDOWN, VK_RIGHT, 1);
break;
case 51:
SendMessageW(history, EM_SETPASSWORDCHAR, L'*', 0);
PostMessageW(history, WM_KEYDOWN, VK_UP, 1);
break;
case 51:
case 52:
SetFocus(password);
SendMessageW(history, EM_SETPASSWORDCHAR, 0, 0);
PostMessageW(history, WM_KEYDOWN, VK_UP, 1); // background recall
+6 -1
View File
@@ -35,7 +35,12 @@ interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux
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>'
xml += '<arg type="b" direction="out"/></method>'
for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", "")):
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 = []
+7 -2
View File
@@ -56,7 +56,12 @@ if args.extension_endpoint:
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"/>'
xml += '<arg type="b" direction="out"/></method></interface></node>'
xml += '<arg type="b" direction="out"/></method>'
for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", "")):
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):
@@ -317,7 +322,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
history_actions = [e[2] for e in text_events
if e[0] == history_focus and e[1] == "text"]
assert history_actions == ["history north", "history south", "history north", "blank",
"history north", "history south", "history north", "h", "blank", "blank"], \
"history north", "history south", "history north", "h", "blank", "blank", "n"], \
f"command history missing, reordered, duplicated or leaked: {history_actions!r}"
if args.fixture == "winforms":
assert any(e[2] == "Email" and e[3] == "edit" and e[4] == "alpha beta"
+6 -1
View File
@@ -31,7 +31,12 @@ interface = "org.stormux.Cthulhu1.Service"
object_path = "/org/stormux/Cthulhu1/Service"
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>'
xml += '<arg type="b" direction="out"/></method>'
for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", "")):
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, held = [], []
+2 -1
View File
@@ -32,7 +32,8 @@ capture_iface = 'org.stormux.Cthulhu1.WineReview'
capture_path = '/org/stormux/Cthulhu1/WineReview'
def interface_xml(review=True):
methods = {'PresentAccessibleEvent': ['s']*7+['i','i','s']}
methods = {'PresentAccessibleEvent': ['s']*7+['i','i','s'],
'SpeakText': ['s','b'], 'CancelSpeech': []}
if review:
methods.update(RegisterWineReviewProvider=['o','u'], PresentWineReviewFocus=['s','s','s','t','s','s','b','(iiii)'])
return '<node><interface name="'+iface+'">'+''.join(