327 lines
15 KiB
Python
327 lines
15 KiB
Python
# 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 importlib
|
|
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."""
|
|
|
|
self._import_patch_target_parent(target)
|
|
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)
|
|
|
|
@staticmethod
|
|
def _import_patch_target_parent(target: str) -> None:
|
|
"""Import the deepest module prefix for string patch targets."""
|
|
|
|
parts = target.split(".")
|
|
for index in range(len(parts) - 1, 0, -1):
|
|
module_name = ".".join(parts[:index])
|
|
try:
|
|
module = importlib.import_module(module_name)
|
|
except ImportError:
|
|
continue
|
|
if "." in module_name:
|
|
package_name, attr_name = module_name.rsplit(".", 1)
|
|
package = sys.modules.get(package_name)
|
|
if package is not None:
|
|
setattr(package, attr_name, module)
|
|
break
|
|
|
|
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()
|