Fix Orca extension startup and disable lifecycle
An approved and enabled Wine Access extension could remain completely
inactive: no Wine D-Bus endpoint was published and no helper was launched,
leaving even Wine Notepad silent. The extension implemented on_ready(),
on_enabled(), and on_disabled(), but the installed Orca 51 extension loader
does not invoke those callbacks.
Start Wine Access from set_up_commands(), which Orca's loader actually
calls, after preserving the base class setup and checking the disabled
state. Override disable() to remove timers, stop managed helpers, and
unpublish the endpoint before delegating to Orca's base implementation.
Keep on_shutdown() cleanup, the existing delayed bus startup retry, and
the guard against duplicate startup.
Add five lifecycle regression tests using an installed Orca's actual
extension loader, with helper launches, the bus, and timers mocked. Cover
startup and repeated setup, disabled extensions, delayed bus availability,
shutdown, and disabling both active and pending startup. Document how to
run these checks without changing the running reader.
The existing integration harness publishes its own endpoint and starts
the helper directly, while the API check only instantiates the extension
and checks APIs. Neither exercised normal extension startup, which let
this failure escape their coverage.
Validation:
- Before the fix, four of the five lifecycle tests failed; all five pass
with the corrected callbacks:
GSETTINGS_BACKEND=memory python3 orca/tests/test_lifecycle.py
- All 45 existing extension tests passed:
python3 -m pytest orca/tests/test_presenter.py orca/tests/test_manager.py
orca/tests/test_service.py orca/tests/test_review.py
- The installed-Orca API check passed:
GSETTINGS_BACKEND=memory python3 orca/tests/orca_api_check.py
- A private D-Bus probe loaded the approved package through Orca's actual
loader, verified delayed publication of the Wine interface and helper
supervision timer, and verified loader shutdown cleanup.
- Existing Wine integration checks passed on a private display, session
bus, and disposable prefix:
dbus-run-session -- xvfb-run -a python3 orca/tests/integration_test.py
--build-dir build
These covered Notepad editing feedback, menus, save dialog controls and
message, NVDA Controller speech/cancellation/braille calls, and helper
shutdown after the prefix became idle. Recorded output does not prove
physical braille or audible speech.
- After installing the fix, reapproving the extension, and restarting
Orca, the user confirmed successful live use in three applications.
- git diff --check passed.
This commit is contained in:
1 parent
28dd2f02dc
commit
dcc68e87e2
3 files changed
+102
-6
No files matched your search
@@ -117,6 +117,15 @@ and bus, lets the manager start the helper, runs the Notepad and controller
|
||||
fixtures, and checks what Orca would present. To check the extension against
|
||||
an installed Orca, run `orca/tests/orca_api_check.py` with Orca's Python
|
||||
environment; it loads the extension and verifies the Orca APIs it calls.
|
||||
Also run its lifecycle regression checks against the installed Orca:
|
||||
|
||||
```sh
|
||||
GSETTINGS_BACKEND=memory python3 orca/tests/test_lifecycle.py
|
||||
```
|
||||
|
||||
These use Orca's actual extension loader to check startup, delayed D-Bus
|
||||
availability, repeated setup, disabling, and shutdown. Helper launches and
|
||||
timers are mocked; the checks do not change the running reader.
|
||||
|
||||
Recorded presentations do not prove audible speech. For live acceptance,
|
||||
open Wine Notepad with Orca running and check the window title, line,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
"""Exercise startup through an installed Orca's actual extension loader."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
try:
|
||||
from orca import extension_loader
|
||||
except ImportError as error:
|
||||
raise unittest.SkipTest("Lifecycle checks require an installed Orca 51") from error
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from wine_access import POLL_INTERVAL_MS, WineAccess # noqa: E402
|
||||
from wine_access.service import OBJECT_PATH # noqa: E402
|
||||
|
||||
|
||||
class LifecycleTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.manager = Mock(helper="/helper/wine-access.exe")
|
||||
managerPatch = patch("wine_access.HelperManager", return_value=self.manager)
|
||||
managerPatch.start()
|
||||
self.addCleanup(managerPatch.stop)
|
||||
self.extension = WineAccess()
|
||||
self.bus = Mock()
|
||||
busPatch = patch.object(self.extension.controller, "_bus", self.bus)
|
||||
busPatch.start()
|
||||
self.addCleanup(busPatch.stop)
|
||||
self.loader = extension_loader.ExtensionLoader()
|
||||
self.loader._user_extensions = [self.extension]
|
||||
disabledPatch = patch.object(self.loader, "get_disabled_extensions", return_value=[])
|
||||
disabledPatch.start()
|
||||
self.addCleanup(disabledPatch.stop)
|
||||
for name in ("timeout_add", "timeout_add_seconds", "source_remove"):
|
||||
timerPatch = patch(f"wine_access.GLib.{name}", return_value=42)
|
||||
setattr(self, name, timerPatch.start())
|
||||
self.addCleanup(timerPatch.stop)
|
||||
|
||||
def test_loader_starts_once_and_shutdown_cleans_up(self):
|
||||
self.loader.set_up_user_commands()
|
||||
self.loader.set_up_user_commands()
|
||||
self.bus.publish_object.assert_called_once()
|
||||
self.assertEqual(self.bus.publish_object.call_args.args[0], OBJECT_PATH)
|
||||
self.manager.poll.assert_called_once_with()
|
||||
self.timeout_add.assert_called_once_with(POLL_INTERVAL_MS, self.manager.poll)
|
||||
self.extension.on_shutdown()
|
||||
self.manager.shutdown.assert_called_once_with()
|
||||
self.bus.unpublish_object.assert_called_once_with(OBJECT_PATH)
|
||||
self.source_remove.assert_called_once_with(42)
|
||||
|
||||
def test_disabled_extension_does_not_start(self):
|
||||
self.loader.get_disabled_extensions.return_value = [self.extension.module_name]
|
||||
self.loader.set_up_user_commands()
|
||||
self.bus.publish_object.assert_not_called()
|
||||
self.manager.poll.assert_not_called()
|
||||
self.timeout_add.assert_not_called()
|
||||
self.timeout_add_seconds.assert_not_called()
|
||||
|
||||
def test_start_waits_for_controller_bus(self):
|
||||
self.extension.controller._bus = None
|
||||
self.loader.set_up_user_commands()
|
||||
self.timeout_add_seconds.assert_called_once_with(2, self.extension._retry_publish)
|
||||
self.manager.poll.assert_not_called()
|
||||
self.extension.controller._bus = self.bus
|
||||
self.assertFalse(self.extension._retry_publish())
|
||||
self.bus.publish_object.assert_called_once()
|
||||
self.manager.poll.assert_called_once_with()
|
||||
|
||||
def test_disable_stops_active_extension(self):
|
||||
self.loader.set_up_user_commands()
|
||||
self.extension.disable()
|
||||
self.manager.shutdown.assert_called_once_with()
|
||||
self.bus.unpublish_object.assert_called_once_with(OBJECT_PATH)
|
||||
self.source_remove.assert_called_once_with(42)
|
||||
|
||||
def test_disable_cancels_pending_start(self):
|
||||
self.extension.controller._bus = None
|
||||
self.loader.set_up_user_commands()
|
||||
self.extension.disable()
|
||||
self.source_remove.assert_called_once_with(42)
|
||||
self.assertEqual(self.extension._retry_id, 0)
|
||||
self.manager.poll.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -52,14 +52,14 @@ class WineAccess(Extension):
|
||||
|
||||
# Extension lifecycle
|
||||
|
||||
def on_ready(self) -> None:
|
||||
self._start()
|
||||
def set_up_commands(self) -> None:
|
||||
super().set_up_commands()
|
||||
if not self._disabled:
|
||||
self._start()
|
||||
|
||||
def on_enabled(self) -> None:
|
||||
self._start()
|
||||
|
||||
def on_disabled(self) -> None:
|
||||
def disable(self) -> None:
|
||||
self._stop()
|
||||
super().disable()
|
||||
|
||||
def on_shutdown(self) -> None:
|
||||
self._stop()
|
||||
|
||||
Reference in new issue
Block a user