Files
cthulhu/tests/test_cthulhu_modifier_manager.py

911 lines
35 KiB
Python

# Unit tests for cthulhu_modifier_manager.py methods.
#
# 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.
# pylint: disable=wrong-import-position
# pylint: disable=import-outside-toplevel
# pylint: disable=too-many-public-methods
# pylint: disable=too-many-statements
# pylint: disable=protected-access
# pylint: disable=too-many-arguments
# pylint: disable=too-many-positional-arguments
# pylint: disable=too-many-locals
"""Unit tests for cthulhu_modifier_manager.py methods."""
from __future__ import annotations
import subprocess
from typing import TYPE_CHECKING
from unittest.mock import call
import pytest
if TYPE_CHECKING:
from unittest.mock import MagicMock
from cthulhu_test_context import CthulhuTestContext
@pytest.mark.unit
class TestCthulhuModifierManager:
"""Test CthulhuModifierManager class methods."""
def _setup_dependencies(self, test_context: CthulhuTestContext) -> dict[str, MagicMock]:
"""Returns dependencies for cthulhu_modifier_manager module testing."""
additional_modules = ["cthulhu.input_event_manager", "gi.repository"]
essential_modules = test_context.setup_shared_dependencies(additional_modules)
debug_mock = essential_modules["cthulhu.debug"]
debug_mock.print_message = test_context.Mock()
debug_mock.LEVEL_INFO = 800
keybindings_mock = essential_modules["cthulhu.keybindings"]
keybindings_mock.get_keycodes = test_context.Mock(return_value=(0, 0))
input_event_manager_mock = essential_modules["cthulhu.input_event_manager"]
input_manager_instance = test_context.Mock()
input_manager_instance.add_grab_for_modifier = test_context.Mock(return_value=123)
input_manager_instance.remove_grab_for_modifier = test_context.Mock()
input_event_manager_mock.get_manager = test_context.Mock(
return_value=input_manager_instance,
)
gi_repository_mock = essential_modules["gi.repository"]
gdk_mock = test_context.Mock()
display_mock = test_context.Mock()
device_manager_mock = test_context.Mock()
display_mock.get_device_manager = test_context.Mock(return_value=device_manager_mock)
gdk_mock.Display = test_context.Mock()
gdk_mock.Display.get_default = test_context.Mock(return_value=display_mock)
gdk_mock.InputSource = test_context.Mock()
gdk_mock.InputSource.KEYBOARD = 4
gi_repository_mock.Gdk = gdk_mock
atspi_mock = test_context.Mock()
atspi_mock.generate_keyboard_event = test_context.Mock()
atspi_mock.ModifierType = test_context.Mock()
atspi_mock.ModifierType.SHIFTLOCK = 1
atspi_mock.ModifierType.SHIFT = 0
gi_repository_mock.Atspi = atspi_mock
glib_mock = test_context.Mock()
glib_mock.timeout_add = test_context.Mock()
gi_repository_mock.GLib = glib_mock
essential_modules["input_manager_instance"] = input_manager_instance
essential_modules["gdk"] = gdk_mock
essential_modules["atspi"] = atspi_mock
essential_modules["glib"] = glib_mock
from cthulhu import gsettings_registry
gsettings_registry.get_registry().clear_runtime_values()
return essential_modules
def test_init(self, test_context: CthulhuTestContext) -> None:
"""Test CthulhuModifierManager.__init__."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
mock_gdk = test_context.Mock()
test_context.patch("cthulhu.cthulhu_modifier_manager.Gdk", new=mock_gdk)
mock_display = test_context.Mock()
mock_device_manager = test_context.Mock()
mock_display.get_device_manager.return_value = mock_device_manager
mock_gdk.Display.get_default.return_value = mock_display
manager = cthulhu_modifier_manager.CthulhuModifierManager()
assert not manager._grabbed_modifiers
assert manager._is_pressed is False
assert manager._original_xmodmap == b""
assert manager._caps_lock_cleared is False
assert manager._need_to_restore_cthulhu_modifier is False
assert manager is not None
def test_init_no_display(self, test_context: CthulhuTestContext) -> None:
"""Test CthulhuModifierManager.__init__ with no display available."""
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
mock_gdk = test_context.Mock()
test_context.patch("cthulhu.cthulhu_modifier_manager.Gdk", new=mock_gdk)
mock_gdk.Display.get_default.return_value = None
essential_modules["cthulhu.debug"].reset_mock()
manager = cthulhu_modifier_manager.CthulhuModifierManager()
assert not manager._grabbed_modifiers
assert manager._is_pressed is False
assert manager._original_xmodmap == b""
assert manager._caps_lock_cleared is False
assert manager._need_to_restore_cthulhu_modifier is False
@pytest.mark.parametrize(
"case",
[
{
"id": "keyboard_device",
"device_source": 4,
"should_refresh": True,
}, # Gdk.InputSource.KEYBOARD = 4
{
"id": "mouse_device",
"device_source": 0,
"should_refresh": False,
}, # Gdk.InputSource.MOUSE = 0
{
"id": "touchscreen_device",
"device_source": 5,
"should_refresh": False,
}, # Gdk.InputSource.TOUCHSCREEN = 5
],
ids=lambda case: case["id"],
)
def test_on_device_changed(
self,
test_context: CthulhuTestContext,
case: dict,
) -> None:
"""Test CthulhuModifierManager._on_device_changed."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
mock_device = test_context.Mock()
mock_device.get_source.return_value = case["device_source"]
mock_refresh = test_context.Mock()
test_context.patch_object(manager, "refresh_cthulhu_modifiers", new=mock_refresh)
test_context.patch("cthulhu.cthulhu_modifier_manager.Gdk.InputSource.KEYBOARD", new=4)
manager._on_device_changed(None, mock_device)
if case["should_refresh"]:
mock_refresh.assert_called_once_with("Keyboard change detected.")
else:
mock_refresh.assert_not_called()
@pytest.mark.parametrize(
"case",
[
{
"id": "insert_grabbed",
"modifier": "Insert",
"cthulhu_modifier_keys": ["Insert", "KP_Insert"],
"is_grabbed": True,
"expected_result": True,
},
{
"id": "insert_not_grabbed",
"modifier": "Insert",
"cthulhu_modifier_keys": ["Insert", "KP_Insert"],
"is_grabbed": False,
"expected_result": False,
},
{
"id": "kp_insert_grabbed",
"modifier": "KP_Insert",
"cthulhu_modifier_keys": ["Insert", "KP_Insert"],
"is_grabbed": True,
"expected_result": True,
},
{
"id": "kp_insert_not_grabbed",
"modifier": "KP_Insert",
"cthulhu_modifier_keys": ["Insert", "KP_Insert"],
"is_grabbed": False,
"expected_result": False,
},
{
"id": "caps_lock_modifiers_set",
"modifier": "Caps_Lock",
"cthulhu_modifier_keys": ["Caps_Lock"],
"is_grabbed": False,
"modifiers_are_set": True,
"expected_result": True,
},
{
"id": "caps_lock_modifiers_not_set",
"modifier": "Caps_Lock",
"cthulhu_modifier_keys": ["Caps_Lock"],
"is_grabbed": False,
"modifiers_are_set": False,
"expected_result": False,
},
{
"id": "shift_lock_modifiers_set",
"modifier": "Shift_Lock",
"cthulhu_modifier_keys": ["Shift_Lock"],
"is_grabbed": False,
"modifiers_are_set": True,
"expected_result": True,
},
{
"id": "shift_lock_modifiers_not_set",
"modifier": "Shift_Lock",
"cthulhu_modifier_keys": ["Shift_Lock"],
"is_grabbed": False,
"modifiers_are_set": False,
"expected_result": False,
},
{
"id": "not_cthulhu_modifier",
"modifier": "Control_L",
"cthulhu_modifier_keys": ["Insert"],
"is_grabbed": False,
"expected_result": False,
},
],
ids=lambda case: case["id"],
)
def test_is_cthulhu_modifier(
self,
test_context,
case: dict,
) -> None:
"""Test CthulhuModifierManager.is_cthulhu_modifier."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
manager._grabbed_modifiers = {"Insert": 1, "KP_Insert": 2} if case["is_grabbed"] else {}
manager._modifiers_are_set = case.get("modifiers_are_set", False)
manager._modifier_keys_override = case["cthulhu_modifier_keys"]
result = manager.is_cthulhu_modifier(case["modifier"])
assert result == case["expected_result"]
@pytest.mark.parametrize(
"case",
[
{
"id": "get_initial_false",
"test_type": "get",
"initial_state": False,
"new_state": None,
"expected_get": False,
"expected_set": None,
},
{
"id": "get_set_true",
"test_type": "get",
"initial_state": True,
"new_state": None,
"expected_get": True,
"expected_set": None,
},
{
"id": "set_to_true",
"test_type": "set",
"initial_state": None,
"new_state": True,
"expected_get": None,
"expected_set": True,
},
{
"id": "set_to_false",
"test_type": "set",
"initial_state": None,
"new_state": False,
"expected_get": None,
"expected_set": False,
},
],
ids=lambda case: case["id"],
)
def test_pressed_state_operations(self, test_context, case: dict) -> None:
"""Test CthulhuModifierManager get_pressed_state and set_pressed_state."""
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
essential_modules["cthulhu.debug"].reset_mock()
if case["test_type"] == "get":
if case["initial_state"] is not None:
manager._is_pressed = case["initial_state"]
result = manager.get_pressed_state()
assert result == case["expected_get"]
else:
manager.set_pressed_state(case["new_state"])
assert manager._is_pressed == case["expected_set"]
@pytest.mark.parametrize(
"case",
[
{
"id": "modifier_grabbed",
"grabbed_modifiers": {"Insert": 1, "KP_Insert": 2},
"modifier": "Insert",
"expected_result": True,
},
{
"id": "kp_modifier_grabbed",
"grabbed_modifiers": {"Insert": 1, "KP_Insert": 2},
"modifier": "KP_Insert",
"expected_result": True,
},
{
"id": "modifier_not_grabbed",
"grabbed_modifiers": {"Insert": 1},
"modifier": "KP_Insert",
"expected_result": False,
},
{
"id": "no_grabs",
"grabbed_modifiers": {},
"modifier": "Insert",
"expected_result": False,
},
],
ids=lambda case: case["id"],
)
def test_is_modifier_grabbed(
self,
test_context,
case: dict,
) -> None:
"""Test CthulhuModifierManager.is_modifier_grabbed."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
manager._grabbed_modifiers = case["grabbed_modifiers"]
result = manager.is_modifier_grabbed(case["modifier"])
assert result == case["expected_result"]
@pytest.mark.parametrize(
"cthulhu_modifier_keys, expected_calls",
[
pytest.param(["Insert", "KP_Insert"], ["Insert", "KP_Insert"], id="insert_keys"),
pytest.param(["Caps_Lock"], [], id="caps_lock_no_grab"),
pytest.param(
["Insert", "Caps_Lock", "KP_Insert"],
["Insert", "KP_Insert"],
id="mixed_keys",
),
pytest.param([], [], id="no_keys"),
],
)
def test_add_grabs_for_cthulhu_modifiers(
self,
test_context,
cthulhu_modifier_keys,
expected_calls,
) -> None:
"""Test CthulhuModifierManager.add_grabs_for_cthulhu_modifiers."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
manager._modifier_keys_override = cthulhu_modifier_keys
mock_add_grab = test_context.Mock()
test_context.patch_object(manager, "add_modifier_grab", new=mock_add_grab)
manager.add_grabs_for_cthulhu_modifiers()
calls = [call(modifier) for modifier in expected_calls]
mock_add_grab.assert_has_calls(calls, any_order=True)
assert mock_add_grab.call_count == len(expected_calls)
@pytest.mark.parametrize(
"cthulhu_modifier_keys, expected_calls",
[
pytest.param(["Insert", "KP_Insert"], ["Insert", "KP_Insert"], id="insert_keys"),
pytest.param(["Caps_Lock"], [], id="caps_lock_no_ungrab"),
pytest.param(
["Insert", "Caps_Lock", "KP_Insert"],
["Insert", "KP_Insert"],
id="mixed_keys",
),
pytest.param([], [], id="no_keys"),
],
)
def test_remove_grabs_for_cthulhu_modifiers(
self,
test_context,
cthulhu_modifier_keys,
expected_calls,
) -> None:
"""Test CthulhuModifierManager.remove_grabs_for_cthulhu_modifiers."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
manager._modifier_keys_override = cthulhu_modifier_keys
mock_remove_grab = test_context.Mock()
test_context.patch_object(manager, "remove_modifier_grab", new=mock_remove_grab)
manager.remove_grabs_for_cthulhu_modifiers()
calls = [call(modifier) for modifier in expected_calls]
mock_remove_grab.assert_has_calls(calls, any_order=True)
assert mock_remove_grab.call_count == len(expected_calls)
assert manager._is_pressed is False
@pytest.mark.parametrize(
"scenario,has_existing,grab_result,expects_keycodes_call,expects_grab_call,expects_in_dict",
[
pytest.param("new", False, 123, True, True, True, id="new_modifier"),
pytest.param("existing", True, None, False, False, True, id="existing_modifier"),
pytest.param("failed", False, -1, True, True, False, id="failed_grab"),
],
)
def test_add_modifier_grab(
self,
test_context,
scenario: str,
has_existing: bool,
grab_result: int | None,
expects_keycodes_call: bool,
expects_grab_call: bool,
expects_in_dict: bool,
) -> None:
"""Test CthulhuModifierManager.add_modifier_grab with various scenarios."""
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
if has_existing:
manager._grabbed_modifiers["Insert"] = 123
if scenario != "existing":
mock_get_keycodes = test_context.Mock()
test_context.patch(
"cthulhu.cthulhu_modifier_manager.keybindings.get_keycodes",
new=mock_get_keycodes,
)
mock_iem = test_context.Mock()
test_context.patch(
"cthulhu.cthulhu_modifier_manager.input_event_manager.get_manager",
new=mock_iem,
)
mock_get_keycodes.return_value = (65379, 110)
mock_input_manager = test_context.Mock()
mock_input_manager.add_grab_for_modifier.return_value = grab_result
mock_iem.return_value = mock_input_manager
manager.add_modifier_grab("Insert")
if expects_keycodes_call:
mock_get_keycodes.assert_called_once_with("Insert")
elif scenario == "existing":
essential_modules["cthulhu.keybindings"].get_keycodes.assert_not_called()
if expects_grab_call:
mock_input_manager.add_grab_for_modifier.assert_called_once_with("Insert", 65379, 110)
elif scenario == "existing":
essential_modules["input_manager_instance"].add_grab_for_modifier.assert_not_called()
if expects_in_dict:
if scenario in ("new", "existing"):
assert manager._grabbed_modifiers["Insert"] == 123
else:
assert "Insert" not in manager._grabbed_modifiers
@pytest.mark.parametrize(
"has_grabbed,expects_call",
[
pytest.param(True, True, id="grabbed_modifier"),
pytest.param(False, False, id="not_grabbed_modifier"),
],
)
def test_remove_modifier_grab(
self,
test_context,
has_grabbed: bool,
expects_call: bool,
) -> None:
"""Test CthulhuModifierManager.remove_modifier_grab with grabbed and non-grabbed modifiers."""
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
if has_grabbed:
manager._grabbed_modifiers["Insert"] = 123
mock_iem = test_context.Mock()
test_context.patch(
"cthulhu.cthulhu_modifier_manager.input_event_manager.get_manager",
new=mock_iem,
)
mock_input_manager = test_context.Mock()
mock_iem.return_value = mock_input_manager
manager.remove_modifier_grab("Insert")
if expects_call:
mock_input_manager.remove_grab_for_modifier.assert_called_once_with("Insert", 123)
assert "Insert" not in manager._grabbed_modifiers
else:
essential_modules["input_manager_instance"].remove_grab_for_modifier.assert_not_called()
@pytest.mark.parametrize(
"keyval_name, expected_method",
[
pytest.param("Caps_Lock", "_toggle_modifier_lock", id="caps_lock"),
pytest.param("Shift_Lock", "_toggle_modifier_lock", id="shift_lock"),
pytest.param("Insert", "_toggle_modifier_grab", id="insert"),
pytest.param("KP_Insert", "_toggle_modifier_grab", id="kp_insert"),
],
)
def test_toggle_modifier(
self,
test_context,
keyval_name,
expected_method,
) -> None:
"""Test CthulhuModifierManager.toggle_modifier."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
mock_keyboard_event = test_context.Mock()
mock_keyboard_event.keyval_name = keyval_name
mock_keyboard_event.hw_code = 110
mock_keyboard_event.modifiers = 0
mock_keyboard_event.is_pressed_key.return_value = False
mock_method = test_context.Mock()
test_context.patch_object(manager, expected_method, new=mock_method)
manager.toggle_modifier(mock_keyboard_event)
mock_method.assert_called_once_with(mock_keyboard_event)
@pytest.mark.parametrize(
"is_pressed_key, expects_remove_call, expects_timeout_calls",
[
pytest.param(True, False, 0, id="pressed_key_no_action"),
pytest.param(False, True, 2, id="released_key_full_action"),
],
)
def test_toggle_modifier_grab(
self,
test_context: CthulhuTestContext,
is_pressed_key,
expects_remove_call,
expects_timeout_calls,
) -> None:
"""Test CthulhuModifierManager._toggle_modifier_grab with pressed and released keys."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
mock_keyboard_event = test_context.Mock()
mock_keyboard_event.keyval_name = "Insert"
mock_keyboard_event.hw_code = 110
mock_keyboard_event.modifiers = 0
mock_keyboard_event.is_pressed_key.return_value = is_pressed_key
mock_remove = test_context.Mock()
test_context.patch_object(manager, "remove_modifier_grab", new=mock_remove)
mock_add_grab = test_context.Mock()
test_context.patch_object(manager, "add_modifier_grab", new=mock_add_grab)
mock_timeout = test_context.Mock()
test_context.patch("cthulhu.cthulhu_modifier_manager.GLib.timeout_add", new=mock_timeout)
test_context.patch(
"cthulhu.cthulhu_modifier_manager.Atspi.generate_keyboard_event",
side_effect=lambda *args, **kwargs: None,
)
manager._toggle_modifier_grab(mock_keyboard_event)
if expects_remove_call:
mock_remove.assert_called_once_with("Insert")
assert mock_timeout.call_count == expects_timeout_calls
if expects_timeout_calls > 0:
timeout_calls = mock_timeout.call_args_list
assert timeout_calls[0][0][0] == 1
assert timeout_calls[0][0][2] == 110 # hw_code
if expects_timeout_calls > 1:
assert timeout_calls[1][0][0] == 500
assert timeout_calls[1][0][2] == "Insert" # modifier name
else:
mock_remove.assert_not_called()
assert mock_timeout.call_count == expects_timeout_calls
@pytest.mark.parametrize(
"keyval_name, is_pressed, expected_modifier",
[
pytest.param(
"Caps_Lock",
True,
1 << 1,
id="caps_lock_pressed", # 1 << Atspi.ModifierType.SHIFTLOCK
),
pytest.param(
"Shift_Lock",
True,
1 << 0,
id="shift_lock_pressed", # 1 << Atspi.ModifierType.SHIFT
),
pytest.param("Caps_Lock", False, None, id="caps_lock_released"),
pytest.param("Other_Key", True, None, id="other_key"),
],
)
def test_toggle_modifier_lock(
self,
test_context,
keyval_name,
is_pressed,
expected_modifier,
) -> None:
"""Test CthulhuModifierManager._toggle_modifier_lock."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
test_context.patch("cthulhu.cthulhu_modifier_manager.Atspi.ModifierType.SHIFTLOCK", new=1)
test_context.patch("cthulhu.cthulhu_modifier_manager.Atspi.ModifierType.SHIFT", new=0)
manager = cthulhu_modifier_manager.CthulhuModifierManager()
mock_keyboard_event = test_context.Mock()
mock_keyboard_event.keyval_name = keyval_name
mock_keyboard_event.hw_code = 110
mock_keyboard_event.modifiers = 0
mock_keyboard_event.is_pressed_key.return_value = is_pressed
mock_timeout = test_context.Mock()
test_context.patch("cthulhu.cthulhu_modifier_manager.GLib.timeout_add", new=mock_timeout)
manager._toggle_modifier_lock(mock_keyboard_event)
if expected_modifier is not None:
mock_timeout.assert_called_once()
timeout_call = mock_timeout.call_args_list[0]
assert timeout_call[0][0] == 1 # 1ms delay
assert timeout_call[0][2] == 0 # modifiers
assert timeout_call[0][3] == expected_modifier # modifier value
else:
mock_timeout.assert_not_called()
def test_refresh_cthulhu_modifiers(self, test_context: CthulhuTestContext) -> None:
"""Test CthulhuModifierManager.refresh_cthulhu_modifiers."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
test_context.patch("os.environ", new={"DISPLAY": ":0"})
mock_popen = test_context.Mock()
test_context.patch("cthulhu.cthulhu_modifier_manager.subprocess.Popen", new=mock_popen)
mock_process = test_context.Mock()
mock_process.communicate.return_value = (b"xmodmap_content", b"")
mock_context_manager = test_context.Mock()
mock_context_manager.__enter__ = test_context.Mock(return_value=mock_process)
mock_context_manager.__exit__ = test_context.Mock(return_value=None)
mock_popen.return_value = mock_context_manager
mock_restore = test_context.Mock()
test_context.patch_object(manager, "_restore_original_xkbcomp", new=mock_restore)
mock_create = test_context.Mock()
test_context.patch_object(manager, "_create_cthulhu_xmodmap", new=mock_create)
manager.refresh_cthulhu_modifiers("test reason")
mock_restore.assert_called_once()
mock_popen.assert_called_once_with(
["xkbcomp", ":0", "-"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
assert manager._original_xmodmap == b"xmodmap_content"
assert manager._modifiers_are_set is True
mock_create.assert_called_once()
@pytest.mark.parametrize(
"caps_lock_cthulhu, shift_lock_cthulhu, caps_cleared, expected_calls",
[
pytest.param(True, False, False, [("set_caps", True)], id="caps_lock_enable"),
pytest.param(False, True, False, [("set_caps", True)], id="shift_lock_enable"),
pytest.param(True, True, False, [("set_caps", True)], id="both_enable"),
pytest.param(
False,
False,
True,
[("set_caps", False)],
id="disable_previously_cleared",
),
pytest.param(False, False, False, [], id="no_changes"),
],
)
def test_create_cthulhu_xmodmap(
self,
test_context,
caps_lock_cthulhu,
shift_lock_cthulhu,
caps_cleared,
expected_calls,
) -> None:
"""Test CthulhuModifierManager._create_cthulhu_xmodmap."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
manager._caps_lock_cleared = caps_cleared
cthulhu_modifier_keys: list[str] = []
if caps_lock_cthulhu:
cthulhu_modifier_keys.append("Caps_Lock")
if shift_lock_cthulhu:
cthulhu_modifier_keys.append("Shift_Lock")
manager._modifier_keys_override = cthulhu_modifier_keys
mock_set_caps = test_context.Mock()
test_context.patch_object(manager, "set_caps_lock_as_cthulhu_modifier", new=mock_set_caps)
manager._create_cthulhu_xmodmap()
for call_type, enable in expected_calls:
if call_type == "set_caps":
mock_set_caps.assert_called_with(enable)
if caps_lock_cthulhu or shift_lock_cthulhu:
assert manager._caps_lock_cleared is True
elif caps_cleared and not caps_lock_cthulhu and not shift_lock_cthulhu:
assert manager._caps_lock_cleared is False
@pytest.mark.parametrize(
"has_xmodmap, expects_popen_call",
[
pytest.param(True, True, id="with_xmodmap"),
pytest.param(False, False, id="no_xmodmap"),
],
)
def test_unset_cthulhu_modifiers(
self,
test_context: CthulhuTestContext,
has_xmodmap,
expects_popen_call,
) -> None:
"""Test CthulhuModifierManager.unset_cthulhu_modifiers with and without xmodmap."""
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
manager._modifiers_are_set = True
if has_xmodmap:
test_context.patch("os.environ", new={"DISPLAY": ":0"})
manager._original_xmodmap = b"original_xmodmap_content"
else:
manager._original_xmodmap = b""
essential_modules["cthulhu.debug"].reset_mock()
mock_popen = test_context.Mock()
test_context.patch("cthulhu.cthulhu_modifier_manager.subprocess.Popen", new=mock_popen)
mock_unmap = test_context.Mock()
mock_iem = test_context.Mock()
mock_iem.unmap_all_modifiers = mock_unmap
test_context.patch(
"cthulhu.cthulhu_modifier_manager.input_event_manager.get_manager",
return_value=mock_iem,
)
if has_xmodmap:
mock_process = test_context.Mock()
mock_context_manager = test_context.Mock()
mock_context_manager.__enter__ = test_context.Mock(return_value=mock_process)
mock_context_manager.__exit__ = test_context.Mock(return_value=None)
mock_popen.return_value = mock_context_manager
manager.unset_cthulhu_modifiers("test reason" if has_xmodmap else "")
assert manager._modifiers_are_set is False
mock_unmap.assert_called_once()
if expects_popen_call:
mock_popen.assert_called_once_with(
["xkbcomp", "-w0", "-", ":0"],
stdin=subprocess.PIPE,
stdout=None,
stderr=None,
)
mock_process.communicate.assert_called_once_with(b"original_xmodmap_content")
assert manager._caps_lock_cleared is False
else:
mock_popen.assert_not_called()
@pytest.mark.parametrize(
"enable, xmodmap_content, expects_popen_call, expected_content",
[
pytest.param(
True,
"""interpret Caps_Lock+AnyOfOrNone(all) {
action= LockMods(modifiers=Lock);
};""",
True,
b"NoAction()",
id="enable_caps_lock",
),
pytest.param(
False,
"""interpret Caps_Lock+AnyOfOrNone(all) {
action= NoAction();
};""",
True,
b"LockMods(modifiers=Lock)",
id="disable_caps_lock",
),
pytest.param(
True,
"some other xmodmap content",
False,
None,
id="no_changes_needed",
),
],
)
def test_set_caps_lock_as_cthulhu_modifier(
self,
test_context: CthulhuTestContext,
enable,
xmodmap_content,
expects_popen_call,
expected_content,
) -> None:
"""Test CthulhuModifierManager.set_caps_lock_as_cthulhu_modifier with various scenarios."""
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager = cthulhu_modifier_manager.CthulhuModifierManager()
if expects_popen_call:
test_context.patch("os.environ", new={"DISPLAY": ":0"})
manager._original_xmodmap = xmodmap_content.encode("UTF-8")
essential_modules["cthulhu.debug"].reset_mock()
mock_popen = test_context.Mock()
test_context.patch("cthulhu.cthulhu_modifier_manager.subprocess.Popen", new=mock_popen)
if expects_popen_call:
mock_process = test_context.Mock()
mock_context_manager = test_context.Mock()
mock_context_manager.__enter__ = test_context.Mock(return_value=mock_process)
mock_context_manager.__exit__ = test_context.Mock(return_value=None)
mock_popen.return_value = mock_context_manager
manager.set_caps_lock_as_cthulhu_modifier(enable)
if expects_popen_call:
mock_popen.assert_called_once_with(
["xkbcomp", "-w0", "-", ":0"],
stdin=subprocess.PIPE,
stdout=None,
stderr=None,
)
called_data = mock_process.communicate.call_args[0][0]
assert expected_content in called_data
else:
mock_popen.assert_not_called()
def test_get_manager(
self,
test_context,
) -> None:
"""Test cthulhu_modifier_manager.get_manager."""
self._setup_dependencies(test_context)
from cthulhu import cthulhu_modifier_manager
manager1 = cthulhu_modifier_manager.get_manager()
assert manager1 is not None
assert isinstance(manager1, cthulhu_modifier_manager.CthulhuModifierManager)
manager2 = cthulhu_modifier_manager.get_manager()
assert manager2 is manager1