Add native Wine accessibility bridge

This commit is contained in:
Storm Dragon
2026-07-17 18:17:23 -04:00
parent 0576b6f79f
commit c9f56cb7ed
24 changed files with 1188 additions and 8 deletions
@@ -119,6 +119,45 @@ class NativeRemoteControllerTest(unittest.TestCase):
self.assertEqual(dbus_service.CthulhuRemoteController.SERVICE_NAME, "org.stormux.Cthulhu1.Service")
self.assertEqual(dbus_service.CthulhuRemoteController.OBJECT_PATH, "/org/stormux/Cthulhu1/Service")
@mock.patch("cthulhu.speech.speak")
def test_speak_text_uses_direct_speech_output(self, speak):
interface = dbus_service.CthulhuDBusServiceInterface()
self.assertTrue(interface.SpeakText("hello", True))
speak.assert_called_once_with("hello", interrupt=True)
@mock.patch("cthulhu.braille.displayMessage")
def test_braille_message_uses_persistent_flash(self, display_message):
interface = dbus_service.CthulhuDBusServiceInterface()
self.assertTrue(interface.BrailleMessage("hello"))
display_message.assert_called_once_with("hello", flashTime=-1)
@mock.patch("cthulhu.speech.stop")
def test_cancel_speech_stops_output(self, stop):
interface = dbus_service.CthulhuDBusServiceInterface()
self.assertTrue(interface.CancelSpeech())
stop.assert_called_once_with()
def test_ssml_fails_closed(self):
interface = dbus_service.CthulhuDBusServiceInterface()
self.assertEqual(interface.SpeakSsml("request", "<speak>Hello</speak>"), 50)
self.assertEqual(interface.SpeakSsml("request", "not xml"), 87)
@mock.patch.object(dbus_service.CthulhuDBusServiceInterface, "PresentMessage")
def test_accessible_event_normalizes_useful_fields(self, present_message):
present_message.return_value = True
interface = dbus_service.CthulhuDBusServiceInterface()
result = interface.PresentAccessibleEvent(
"source", "focus", "Open", "button", "", "focused", "", 2, 4, "Settings"
)
self.assertTrue(result)
present_message.assert_called_once_with("Open, button, 2 of 4, Settings")
if __name__ == "__main__":
unittest.main()
+134
View File
@@ -0,0 +1,134 @@
import unittest
from unittest import mock
from cthulhu.wine_access_manager import PrefixProcess, WineAccessManager
class WineAccessManagerTest(unittest.TestCase):
def test_prefix_prefers_explicit_wineprefix(self):
environment = {
"WINEPREFIX": "/tmp/custom-prefix",
"STEAM_COMPAT_DATA_PATH": "/tmp/compat",
}
self.assertEqual(
WineAccessManager._prefix_from_environ(environment), "/tmp/custom-prefix"
)
def test_prefix_uses_proton_pfx(self):
environment = {"STEAM_COMPAT_DATA_PATH": "/tmp/compat"}
self.assertEqual(WineAccessManager._prefix_from_environ(environment), "/tmp/compat/pfx")
@mock.patch("cthulhu.wine_access_manager.os.access", return_value=True)
@mock.patch(
"cthulhu.wine_access_manager.os.readlink",
return_value="/opt/proton/files/bin/wine64-preloader",
)
def test_preloader_resolves_to_runtime_loader(self, _readlink, _access):
self.assertEqual(
WineAccessManager._loader_for_process(100, {}),
"/opt/proton/files/bin/wine64",
)
@mock.patch("cthulhu.wine_access_manager.subprocess.Popen")
def test_launch_preserves_prefix_and_runtime_loader(self, popen):
process = mock.Mock()
process.poll.return_value = None
popen.return_value = process
manager = WineAccessManager("/usr/bin/cthulhu-wine-access.exe")
state = PrefixProcess()
manager._ensure_running(
"/tmp/prefix",
state,
{"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus"},
"/opt/proton/files/bin/wine64",
10.0,
)
environment = popen.call_args.kwargs["env"]
self.assertEqual(environment["WINEPREFIX"], "/tmp/prefix")
self.assertEqual(environment["WINELOADER"], "/opt/proton/files/bin/wine64")
self.assertEqual(environment[manager.MANAGED_ENVIRONMENT_KEY], "1")
self.assertEqual(
environment["DBUS_SESSION_BUS_ADDRESS"], "unix:path=/run/user/1000/bus"
)
self.assertEqual(popen.call_args.args[0], ["/usr/bin/cthulhu-wine-access.exe"])
@mock.patch("cthulhu.wine_access_manager.subprocess.Popen")
def test_disabled_surfaces_are_passed_to_helper(self, popen):
process = mock.Mock()
process.poll.return_value = None
popen.return_value = process
manager = WineAccessManager("/usr/bin/cthulhu-wine-access.exe")
manager.dialogReaderEnabled = False
manager.controllerEnabled = False
manager._ensure_running("/tmp/prefix", PrefixProcess(), {}, None, 10.0)
self.assertEqual(
popen.call_args.args[0],
[
"/usr/bin/cthulhu-wine-access.exe",
"--no-dialog-reader",
"--no-controller",
],
)
def test_short_lived_helper_gets_restart_backoff(self):
manager = WineAccessManager("/usr/bin/cthulhu-wine-access.exe")
state = PrefixProcess()
state.process = mock.Mock()
state.process.poll.return_value = 0
state.startedAt = 9.0
manager._record_exit(state, 10.0)
self.assertIsNone(state.process)
self.assertEqual(state.failures, 1)
self.assertEqual(state.nextStart, 12.0)
@mock.patch.object(WineAccessManager, "_loader_for_process", return_value=None)
@mock.patch.object(
WineAccessManager,
"_read_environ",
return_value={"WINEPREFIX": "/tmp/exported-prefix"},
)
@mock.patch("cthulhu.wine_access_manager.Path.iterdir")
@mock.patch("cthulhu.wine_access_manager.os.getuid", return_value=1000)
def test_discovery_ignores_non_wine_processes(
self, _getuid, iterdir, _read_environ, _loader
):
entry = mock.Mock()
entry.name = "123"
entry.stat.return_value.st_uid = 1000
iterdir.return_value = [entry]
self.assertEqual(WineAccessManager("/tmp/helper").discover_prefixes(), {})
@mock.patch.object(WineAccessManager, "_loader_for_process")
@mock.patch.object(
WineAccessManager,
"_read_environ",
return_value={
"WINEPREFIX": "/tmp/prefix",
WineAccessManager.MANAGED_ENVIRONMENT_KEY: "1",
},
)
@mock.patch("cthulhu.wine_access_manager.Path.iterdir")
@mock.patch("cthulhu.wine_access_manager.os.getuid", return_value=1000)
def test_discovery_ignores_managed_helper_processes(
self, _getuid, iterdir, _read_environ, loader
):
entry = mock.Mock()
entry.name = "123"
entry.stat.return_value.st_uid = 1000
iterdir.return_value = [entry]
self.assertEqual(WineAccessManager("/tmp/helper").discover_prefixes(), {})
loader.assert_not_called()
if __name__ == "__main__":
unittest.main()