Prepare test harness for Orca 50 rebase

This commit is contained in:
2026-04-11 01:59:13 -04:00
parent 3100228a17
commit ff9bea7749
5 changed files with 431 additions and 6 deletions
+10 -5
View File
@@ -6,15 +6,15 @@ project('cthulhu',
python = import('python')
i18n = import('i18n')
python_minimum_version = '3.9'
python_minimum_version = '3.10'
python3 = python.find_installation('python3', required: true)
if not python3.language_version().version_compare(f'>= @python_minimum_version@')
error(f'Python @python_minimum_version@ or newer is required.')
endif
# Hard dependencies (checked via pkg-config)
dependency('atspi-2', version: '>= 2.52.0')
dependency('atk-bridge-2.0', version: '>= 2.26.0')
dependency('atspi-2', version: '>= 2.56.0')
dependency('atk-bridge-2.0', version: '>= 2.56.0')
dependency('pygobject-3.0', version: '>= 3.18')
# Hard Python module dependencies
@@ -33,14 +33,19 @@ if not pluggy_result.found()
error('pluggy module is required')
endif
dasbus_result = python.find_installation('python3', modules:['dasbus'], required: false)
if not dasbus_result.found()
error('dasbus is required for D-Bus remote controller interface')
endif
# End users might not have the Gtk development libraries installed, making pkg-config fail.
# Therefore, check this dependency via python.
gtk_major_version = '3'
gtk_minor_version = '0'
gtk_minor_version = '24'
gtk_command = ' '.join([
'import gi; gi.require_version("Gtk", "3.0"); from gi.repository import Gtk;',
'print(f"{Gtk.get_major_version()}.{Gtk.get_minor_version()}.{Gtk.get_micro_version()}");',
f'failed = Gtk.get_major_version() != @gtk_major_version@;',
f'failed = Gtk.get_major_version() != @gtk_major_version@ or Gtk.get_minor_version() < @gtk_minor_version@;',
'exit(failed)'
])
gtk_test = run_command(python3, '-c', gtk_command, check: false)
+2 -1
View File
@@ -7,12 +7,13 @@ name = "cthulhu"
dynamic = ["version"]
description = "Fork of the Orca screen reader"
readme = "README.md"
requires-python = ">=3.9"
requires-python = ">=3.10"
license = { text = "LGPL-2.1-or-later" }
dependencies = [
"pygobject>=3.18",
"pluggy",
"tomlkit",
"dasbus",
"brlapi; extra == 'braille'",
"python-speechd; extra == 'speech'",
"piper-tts; extra == 'piper'",
+28
View File
@@ -1,8 +1,12 @@
import importlib.util
import os
import sys
import types
from pathlib import Path
import pytest
os.environ["GSETTINGS_BACKEND"] = "memory"
REPO_ROOT = Path(__file__).resolve().parents[1]
SRC_ROOT = REPO_ROOT / "src"
@@ -63,3 +67,27 @@ for generated_module in ("cthulhu_i18n", "cthulhu_platform"):
loaded_module = _load_generated_module(generated_module)
sys.modules[f"cthulhu.{generated_module}"] = loaded_module
setattr(cthulhu, generated_module, loaded_module)
from cthulhu_test_fixtures import test_context # noqa: E402,F401
def clean_all_cthulhu_modules() -> None:
modules_to_remove = [
module_name
for module_name in sys.modules
if module_name.startswith("cthulhu.")
and module_name not in {"cthulhu.cthulhu_i18n", "cthulhu.cthulhu_platform"}
]
for module_name in modules_to_remove:
sys.modules.pop(module_name, None)
def pytest_configure(config: pytest.Config) -> None:
config.addinivalue_line("markers", "unit: marks tests as unit tests")
def pytest_runtest_setup(item: pytest.Item) -> None:
clean_all_cthulhu_modules()
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))
+306
View File
@@ -0,0 +1,306 @@
# Orca Test Context - Test Isolation Framework
#
# Copyright 2025 Igalia, S.L.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
"""Test isolation framework for Cthulhu screen reader tests."""
from __future__ import annotations
import os
import sys
from typing import TYPE_CHECKING, Any
import gi
gi.require_version("Atspi", "2.0")
gi.require_version("Gio", "2.0")
from gi.repository import Atspi, Gio, GLib # pylint: disable=wrong-import-position
if TYPE_CHECKING:
from unittest.mock import MagicMock
from _pytest.monkeypatch import MonkeyPatch
from pytest_mock import MockerFixture
class CthulhuTestContext:
"""Test isolation framework for Cthulhu tests."""
def __init__(self, mocker: MockerFixture, monkeypatch: MonkeyPatch):
"""Initialize the test context."""
self.mocker: MockerFixture = mocker
self.monkeypatch: MonkeyPatch = monkeypatch
self.patches: dict[str, Any] = {}
self.mocks: dict[str, MagicMock] = {}
def patch(self, target: str, **kwargs) -> MagicMock:
"""Convenience method for creating patches."""
return self.mocker.patch(target, **kwargs)
def patch_object(self, target: object, attribute: str, **kwargs) -> MagicMock:
"""Convenience method for patching object attributes."""
return self.mocker.patch.object(target, attribute, **kwargs)
def Mock(self, **kwargs) -> MagicMock: # pylint: disable=invalid-name
"""Convenience method for creating Mock objects."""
return self.mocker.Mock(**kwargs)
def patch_env(
self,
env_vars: dict[str, str],
remove_vars: list[str] | None = None,
) -> MagicMock | None:
"""Convenience method for patching environment variables."""
if remove_vars:
for var in remove_vars:
if var in os.environ:
del os.environ[var]
if env_vars:
return self.mocker.patch.dict(os.environ, env_vars)
return None
def patch_module(self, module_name: str, mock_module: Any) -> MagicMock:
"""Convenience method for patching sys.modules entries."""
return self.mocker.patch.dict(sys.modules, {module_name: mock_module})
def patch_modules(self, modules: dict[str, Any]) -> MagicMock:
"""Convenience method for patching multiple sys.modules entries."""
return self.mocker.patch.dict(sys.modules, modules)
def _setup_required_imports(self) -> None:
"""Sets up commonly required module imports that real modules need."""
required_modules = [
"cthulhu.cthulhu_i18n",
"cthulhu.cmdnames",
"cthulhu.input_event",
"cthulhu.keybindings",
"cthulhu.messages",
"cthulhu.text_attribute_names",
]
for module_name in required_modules:
if module_name not in self.mocks:
mock_module = self.mocker.patch(module_name, create=True)
if module_name == "cthulhu.cthulhu_i18n":
mock_module._ = lambda x: x
self.mocks[module_name] = mock_module
def _setup_essential_modules(self, module_names: list[str]) -> dict[str, MagicMock]:
"""Returns dictionary mapping module names to mock objects."""
essential_modules = {}
for module_name in module_names:
mock_module = self.mocker.Mock()
self.patch_module(module_name, mock_module)
essential_modules[module_name] = mock_module
return essential_modules
def setup_shared_dependencies(
self,
additional_modules: list[str] | None = None,
) -> dict[str, MagicMock]:
"""Returns common/shared dependencies used across most Orca test modules."""
core_modules = [
"cthulhu.debug",
"cthulhu.messages",
"cthulhu.input_event",
"cthulhu.keybindings",
"cthulhu.cmdnames",
"cthulhu.ax_object",
"cthulhu.dbus_service",
"cthulhu.script_manager",
"cthulhu.cthulhu_i18n",
"cthulhu.guilabels",
"cthulhu.text_attribute_names",
"cthulhu.focus_manager",
"cthulhu.braille",
"cthulhu.cthulhu_platform",
]
if additional_modules:
core_modules.extend(additional_modules)
essential_modules = self._setup_essential_modules(core_modules)
self.configure_shared_module_behaviors(essential_modules)
return essential_modules
# pylint: disable-next=too-many-locals, too-many-statements, too-many-branches
def configure_shared_module_behaviors(self, essential_modules: dict[str, MagicMock]) -> None:
"""Configure standard behaviors for shared modules to reduce duplication."""
if "cthulhu.cthulhu_i18n" in essential_modules:
i18n_mock = essential_modules["cthulhu.cthulhu_i18n"]
i18n_mock._ = lambda x: x
i18n_mock.C_ = lambda c, x: x
i18n_mock.ngettext = lambda s, p, n: s if n == 1 else p
if "cthulhu.debug" in essential_modules:
debug_mock = essential_modules["cthulhu.debug"]
debug_mock.LEVEL_INFO = 800
debug_mock.LEVEL_SEVERE = 1000
debug_mock.print_message = self.mocker.Mock()
debug_mock.print_tokens = self.mocker.Mock()
debug_mock.println = self.mocker.Mock()
if "cthulhu.keybindings" in essential_modules:
keybindings_mock = essential_modules["cthulhu.keybindings"]
bindings_instance = self.mocker.Mock()
bindings_instance.is_empty = self.mocker.Mock(return_value=True)
bindings_instance.add = self.mocker.Mock()
keybindings_mock.KeyBindings = self.mocker.Mock(return_value=bindings_instance)
keybindings_mock.KeyBinding = self.mocker.Mock(return_value=self.mocker.Mock())
keybindings_mock.DEFAULT_MODIFIER_MASK = 1
keybindings_mock.CTHULHU_SHIFT_MODIFIER_MASK = 2
if "cthulhu.focus_manager" in essential_modules:
focus_manager_mock = essential_modules["cthulhu.focus_manager"]
manager_instance = self.mocker.Mock()
manager_instance.get_locus_of_focus = self.mocker.Mock(return_value=None)
manager_instance.set_locus_of_focus = self.mocker.Mock()
manager_instance.in_say_all = self.mocker.Mock(return_value=False)
manager_instance.is_in_preferences_window = self.mocker.Mock(return_value=False)
manager_instance.get_active_mode_and_object_of_interest = self.mocker.Mock(
return_value=(None, None),
)
focus_manager_mock.get_manager = self.mocker.Mock(return_value=manager_instance)
focus_manager_mock.OBJECT_NAVIGATOR = "object-navigator"
essential_modules["focus_manager_instance"] = manager_instance
if "cthulhu.dbus_service" in essential_modules:
dbus_service_mock = essential_modules["cthulhu.dbus_service"]
controller_mock = self.mocker.Mock()
controller_mock.register_decorated_module = self.mocker.Mock()
dbus_service_mock.get_remote_controller = self.mocker.Mock(return_value=controller_mock)
dbus_service_mock.command = lambda func: func
dbus_service_mock.getter = lambda func: func
dbus_service_mock.setter = lambda func: func
dbus_service_mock.parameterized_command = lambda func: func
if "cthulhu.script_manager" in essential_modules:
script_manager_mock = essential_modules["cthulhu.script_manager"]
manager_instance = self.mocker.Mock()
script_instance = self.mocker.Mock()
script_instance.present_message = self.mocker.Mock()
script_instance.present_object = self.mocker.Mock()
script_instance.speak_message = self.mocker.Mock()
script_instance.update_braille = self.mocker.Mock()
speech_gen = self.mocker.Mock()
braille_gen = self.mocker.Mock()
script_instance.get_speech_generator = self.mocker.Mock(return_value=speech_gen)
script_instance.get_braille_generator = self.mocker.Mock(return_value=braille_gen)
manager_instance.get_active_script = self.mocker.Mock(return_value=script_instance)
manager_instance.get_script = self.mocker.Mock(return_value=script_instance)
manager_instance.get_manager = self.mocker.Mock(return_value=manager_instance)
script_manager_mock.get_manager = self.mocker.Mock(return_value=manager_instance)
if "cthulhu.ax_object" in essential_modules:
ax_object_mock = essential_modules["cthulhu.ax_object"]
ax_object_class_mock = self.mocker.Mock()
ax_object_class_mock.is_valid = self.mocker.Mock(return_value=True)
ax_object_class_mock.is_dead = self.mocker.Mock(return_value=False)
ax_object_class_mock.get_name = self.mocker.Mock(return_value="")
ax_object_class_mock.get_role = self.mocker.Mock(return_value=Atspi.Role.PANEL)
ax_object_class_mock.get_parent = self.mocker.Mock(return_value=None)
ax_object_class_mock.find_ancestor = self.mocker.Mock(return_value=None)
ax_object_class_mock.clear_cache = self.mocker.Mock()
ax_object_mock.AXObject = ax_object_class_mock
if "cthulhu.ax_utilities" in essential_modules:
ax_utilities_mock = essential_modules["cthulhu.ax_utilities"]
ax_utilities_class_mock = self.mocker.Mock()
ax_utilities_class_mock.is_focused = self.mocker.Mock(return_value=True)
ax_utilities_class_mock.is_table_cell_or_header = self.mocker.Mock(return_value=False)
ax_utilities_class_mock.is_list_item = self.mocker.Mock(return_value=False)
ax_utilities_class_mock.is_layout_only = self.mocker.Mock(return_value=False)
ax_utilities_class_mock.get_status_bar = self.mocker.Mock(return_value=None)
ax_utilities_class_mock.get_info_bar = self.mocker.Mock(return_value=None)
ax_utilities_class_mock.is_showing = self.mocker.Mock(return_value=True)
ax_utilities_class_mock.is_visible = self.mocker.Mock(return_value=True)
ax_utilities_class_mock.is_sensitive = self.mocker.Mock(return_value=True)
ax_utilities_mock.AXUtilities = ax_utilities_class_mock
if "cthulhu.input_event" in essential_modules:
input_event_mock = essential_modules["cthulhu.input_event"]
input_event_mock.KEY_PRESSED_EVENT = "key-pressed"
input_event_mock.KEY_RELEASED_EVENT = "key-released"
input_event_mock.MOUSE_BUTTON_CLICKED_EVENT = "mouse-clicked"
if "cthulhu.cthulhu_platform" in essential_modules:
cthulhu_platform_mock = essential_modules["cthulhu.cthulhu_platform"]
cthulhu_platform_mock.tablesdir = "/usr/share/liblouis/tables"
if "cthulhu.braille_presenter" in essential_modules:
braille_presenter_mock = essential_modules["cthulhu.braille_presenter"]
presenter_instance = self.mocker.Mock()
presenter_instance.use_braille = self.mocker.Mock(return_value=False)
presenter_instance.get_braille_is_enabled = self.mocker.Mock(return_value=False)
presenter_instance.get_flash_messages_are_enabled = self.mocker.Mock(return_value=False)
presenter_instance.get_flash_messages_are_detailed = self.mocker.Mock(
return_value=False,
)
presenter_instance.get_flashtime_from_settings = self.mocker.Mock(return_value=5000)
braille_presenter_mock.get_presenter = self.mocker.Mock(return_value=presenter_instance)
if "cthulhu.presentation_manager" in essential_modules:
presentation_manager_mock = essential_modules["cthulhu.presentation_manager"]
manager_instance = self.mocker.Mock()
manager_instance.interrupt_presentation = self.mocker.Mock()
manager_instance.present_message = self.mocker.Mock()
manager_instance.speak_message = self.mocker.Mock()
manager_instance.speak_character = self.mocker.Mock()
manager_instance.present_braille_message = self.mocker.Mock()
manager_instance.spell_item = self.mocker.Mock()
manager_instance.spell_phonetically = self.mocker.Mock()
manager_instance.play_sound = self.mocker.Mock()
manager_instance.speak_contents = self.mocker.Mock()
manager_instance.display_contents = self.mocker.Mock()
presentation_manager_mock.get_manager = self.mocker.Mock(return_value=manager_instance)
if "gi.repository" in essential_modules:
gi_repo_mock = essential_modules["gi.repository"]
gi_repo_mock.Gio = Gio
gi_repo_mock.GLib = GLib
def get_mock(self, name: str) -> MagicMock | None:
"""Returns mock object if it exists, None otherwise."""
return self.mocks.get(name)
def __enter__(self) -> CthulhuTestContext: # noqa: PYI034
"""Enter the test context."""
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""Exit the test context and clean up all patches."""
for patch_obj in self.patches.values():
patch_obj.stop()
self.patches.clear()
self.mocks.clear()
+85
View File
@@ -0,0 +1,85 @@
# Orca Test Fixtures - Pytest Integration
#
# Copyright 2025 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
"""Pytest fixtures for Cthulhu screen reader tests.
This module provides pytest fixtures that integrate the CthulhuTestContext
with pytest's fixture system, providing clean, isolated test environments.
The fixtures are designed to be simple to use while providing complete
test isolation and preventing cross-test contamination.
Usage:
def test_presenter(orca_test):
cthulhu_test.setup_where_am_i_presenter_dependencies()
from cthulhu.where_am_i_presenter import WhereAmIPresenter
presenter = WhereAmIPresenter()
result = presenter.some_method()
assert result is True
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from cthulhu_test_context import CthulhuTestContext
if TYPE_CHECKING:
from collections.abc import Generator
from pytest_mock import MockerFixture
@pytest.fixture
def test_context(mocker: MockerFixture, monkeypatch) -> Generator[CthulhuTestContext, None, None]:
"""Provides clean, isolated Orca test environment.
This is the primary fixture for Cthulhu tests. It provides a complete
test isolation context that prevents cross-test contamination.
Usage:
class TestMyModule:
def _setup_dependencies(self, cthulhu_test):
# Set up only what your module needs
return cthulhu_test.setup_shared_dependencies(["cthulhu.debug"])
def test_my_functionality(self, cthulhu_test):
self._setup_dependencies(orca_test)
from cthulhu.my_module import MyClass
# Your test code here
instance = MyClass()
result = instance.some_method()
assert result is True
Args:
mocker: pytest-mock mocker fixture (automatically injected)
monkeypatch: pytest monkeypatch fixture (automatically injected)
Yields:
CthulhuTestContext instance with clean isolation
"""
with CthulhuTestContext(mocker, monkeypatch) as context:
yield context