Files
voculinux/tests/test_ibus_engine_utils.py
T
c59fe8b577 fix: skip unbridged IBus on KDE Plasma Wayland (#577)
* fix(injection): skip unbridged IBus on KDE Plasma Wayland

Do not auto-start XIM ibus-daemon on Wayland (circular viability check).
On KDE, require KWin VirtualKeyboard enabled before selecting IBus.

Fixes #574

* refactor(injection): tighten KDE VirtualKeyboard bridge check

Ponytail cleanup: shorter logs and fewer failure branches, same behavior.

* style: black-format ibus daemon Wayland test for CI

* fix(injection): parse KWin VirtualKeyboard gdbus variants strictly

Match <<true>>/<<false>> wrappers instead of bare substrings so ambiguous
gdbus output cannot flip the #574 IBus bridge decision.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-07-26 23:06:26 -07:00

325 lines
12 KiB
Python

"""
Final comprehensive tests for IBus engine.
These tests exercise code paths to improve coverage without platform-specific issues.
"""
import subprocess
import sys
import unittest
from unittest.mock import MagicMock, patch
# Mock GI imports before importing any vocalinux modules that use gi.
# On CI, real gi/IBus may be installed; importing ibus_engine without mocks
# would set IBUS_AVAILABLE=True and VocalinuxEngine would inherit from real
# IBus.Engine, causing hangs when super().__init__() tries to connect to IBus.
_mock_gi = MagicMock()
_mock_ibus = MagicMock()
_mock_glib = MagicMock()
_mock_gobject = MagicMock()
_mock_ibus.Engine = MagicMock
_mock_glib.MainLoop = MagicMock
_mock_gi_repo = MagicMock()
_mock_gi_repo.IBus = _mock_ibus
_mock_gi_repo.GLib = _mock_glib
_mock_gi_repo.GObject = _mock_gobject
sys.modules["gi"] = _mock_gi
sys.modules["gi.repository"] = _mock_gi_repo
# Force reload ibus_engine so it picks up mocks (in case it was already imported)
for _key in list(sys.modules.keys()):
if "vocalinux" in _key and "ibus_engine" in _key:
del sys.modules[_key]
class TestIBusSetupError(unittest.TestCase):
"""Tests for IBusSetupError exception."""
def test_ibus_setup_error_is_runtime_error(self):
"""Test that IBusSetupError is a RuntimeError."""
from vocalinux.text_injection.ibus_engine import IBusSetupError
self.assertTrue(issubclass(IBusSetupError, RuntimeError))
def test_ibus_setup_error_accepts_message(self):
"""Test that IBusSetupError accepts a message."""
from vocalinux.text_injection.ibus_engine import IBusSetupError
msg = "Test error message"
error = IBusSetupError(msg)
self.assertEqual(str(error), msg)
class TestEnsureIBusDir(unittest.TestCase):
"""Tests for ensure_ibus_dir function."""
@patch("pathlib.Path.mkdir")
@patch("pathlib.Path.chmod")
def test_ensure_ibus_dir_creates_and_secures(self, mock_chmod, mock_mkdir):
"""Test that ensure_ibus_dir creates directory with proper permissions."""
from vocalinux.text_injection.ibus_engine import ensure_ibus_dir
ensure_ibus_dir()
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
mock_chmod.assert_called_once_with(0o700)
class TestIsIBusAvailable(unittest.TestCase):
"""Tests for is_ibus_available function."""
@patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True)
def test_is_ibus_available_true(self):
"""Test is_ibus_available when IBus is available."""
from vocalinux.text_injection.ibus_engine import is_ibus_available
result = is_ibus_available()
# Verify the function returns True when IBUS_AVAILABLE flag is True
self.assertTrue(result)
# Verify it's checking the flag, not just always returning a value
self.assertEqual(result, True)
@patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", False)
def test_is_ibus_available_false(self):
"""Test is_ibus_available when IBus is not available."""
from vocalinux.text_injection.ibus_engine import is_ibus_available
result = is_ibus_available()
self.assertFalse(result)
class TestIsIBusDaemonRunning(unittest.TestCase):
"""Tests for is_ibus_daemon_running function."""
@patch("subprocess.run")
def test_is_ibus_daemon_running_true(self, mock_run):
"""Test is_ibus_daemon_running returns True when daemon is running."""
from vocalinux.text_injection.ibus_engine import is_ibus_daemon_running
mock_run.return_value.returncode = 0
result = is_ibus_daemon_running()
self.assertTrue(result)
@patch("subprocess.run")
def test_is_ibus_daemon_running_false(self, mock_run):
"""Test is_ibus_daemon_running returns False when daemon is not running."""
from vocalinux.text_injection.ibus_engine import is_ibus_daemon_running
mock_run.return_value.returncode = 1
result = is_ibus_daemon_running()
self.assertFalse(result)
@patch("subprocess.run", side_effect=FileNotFoundError("pgrep not found"))
def test_is_ibus_daemon_running_file_not_found(self, mock_run):
"""Test is_ibus_daemon_running handles FileNotFoundError."""
from vocalinux.text_injection.ibus_engine import is_ibus_daemon_running
result = is_ibus_daemon_running()
self.assertFalse(result)
class TestIsIBusActiveInputMethod(unittest.TestCase):
"""Tests for is_ibus_active_input_method function."""
@patch.dict("os.environ", {"GTK_IM_MODULE": "ibus"})
def test_is_ibus_active_gtk_im_module(self):
"""Test detection via GTK_IM_MODULE."""
from vocalinux.text_injection.ibus_engine import is_ibus_active_input_method
result = is_ibus_active_input_method()
self.assertTrue(result)
@patch.dict("os.environ", {"QT_IM_MODULE": "ibus"}, clear=True)
def test_is_ibus_active_qt_im_module(self):
"""Test detection via QT_IM_MODULE."""
from vocalinux.text_injection.ibus_engine import is_ibus_active_input_method
result = is_ibus_active_input_method()
self.assertTrue(result)
@patch.dict("os.environ", {"XMODIFIERS": "@im=ibus"}, clear=True)
def test_is_ibus_active_xmodifiers(self):
"""Test detection via XMODIFIERS."""
from vocalinux.text_injection.ibus_engine import is_ibus_active_input_method
result = is_ibus_active_input_method()
self.assertTrue(result)
@patch.dict("os.environ", {}, clear=True)
def test_is_ibus_active_not_active(self):
"""Test when IBus is not the active input method."""
from vocalinux.text_injection.ibus_engine import is_ibus_active_input_method
result = is_ibus_active_input_method()
self.assertFalse(result)
@patch("vocalinux.text_injection.ibus_engine.is_ibus_daemon_running", return_value=False)
@patch.dict("os.environ", {"XMODIFIERS": "@im=none"}, clear=True)
def test_xmodifiers_none_is_not_a_competing_input_method(self, mock_daemon):
"""Treat the XIM 'none' sentinel like an unset input method."""
from vocalinux.text_injection.ibus_engine import is_ibus_active_input_method
self.assertFalse(is_ibus_active_input_method())
mock_daemon.assert_called_once_with()
class TestStartIBusDaemon(unittest.TestCase):
"""Tests for start_ibus_daemon function."""
@patch("vocalinux.text_injection.ibus_engine.is_ibus_daemon_running")
def test_start_ibus_daemon_already_running(self, mock_is_running):
"""Test when daemon is already running."""
from vocalinux.text_injection.ibus_engine import start_ibus_daemon
mock_is_running.return_value = True
result = start_ibus_daemon()
self.assertTrue(result)
@patch("vocalinux.text_injection.ibus_engine.is_ibus_available")
@patch("vocalinux.text_injection.ibus_engine.is_ibus_daemon_running")
def test_start_ibus_daemon_not_available(self, mock_is_running, mock_available):
"""Test when IBus is not available."""
from vocalinux.text_injection.ibus_engine import start_ibus_daemon
mock_is_running.return_value = False
mock_available.return_value = False
result = start_ibus_daemon()
self.assertFalse(result)
@patch("vocalinux.text_injection.ibus_engine.subprocess.Popen")
@patch("vocalinux.text_injection.ibus_engine.is_ibus_available", return_value=True)
@patch("vocalinux.text_injection.ibus_engine.is_ibus_daemon_running", return_value=False)
def test_start_ibus_daemon_noop_on_wayland(self, mock_is_running, mock_available, mock_popen):
"""On Wayland, never spawn XIM ibus-daemon -x -d -r (#574)."""
from vocalinux.text_injection.ibus_engine import start_ibus_daemon
with patch.dict("os.environ", {"XDG_SESSION_TYPE": "wayland"}):
result = start_ibus_daemon()
self.assertFalse(result)
mock_popen.assert_not_called()
@patch("vocalinux.text_injection.ibus_engine.time.sleep")
@patch("vocalinux.text_injection.ibus_engine.subprocess.Popen")
@patch("vocalinux.text_injection.ibus_engine.is_ibus_available", return_value=True)
def test_start_ibus_daemon_still_starts_on_x11(self, mock_available, mock_popen, mock_sleep):
"""X11 sessions may still auto-start ibus-daemon with XIM flags."""
from vocalinux.text_injection.ibus_engine import start_ibus_daemon
with patch(
"vocalinux.text_injection.ibus_engine.is_ibus_daemon_running",
side_effect=[False, True],
):
with patch.dict("os.environ", {"XDG_SESSION_TYPE": "x11"}, clear=False):
result = start_ibus_daemon()
self.assertTrue(result)
mock_popen.assert_called_once()
self.assertEqual(mock_popen.call_args[0][0], ["ibus-daemon", "-x", "-d", "-r"])
class TestIsEngineActive(unittest.TestCase):
"""Tests for is_engine_active function."""
@patch("subprocess.run")
def test_is_engine_active_true(self, mock_run):
"""Test when engine is active."""
from vocalinux.text_injection.ibus_engine import ENGINE_NAME, is_engine_active
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = ENGINE_NAME
result = is_engine_active()
self.assertTrue(result)
@patch("subprocess.run")
def test_is_engine_active_false(self, mock_run):
"""Test when engine is not active."""
from vocalinux.text_injection.ibus_engine import is_engine_active
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = "other"
result = is_engine_active()
self.assertFalse(result)
class TestGetCurrentEngine(unittest.TestCase):
"""Tests for get_current_engine function."""
@patch("subprocess.run")
def test_get_current_engine_success(self, mock_run):
"""Test successfully retrieving current engine."""
from vocalinux.text_injection.ibus_engine import get_current_engine
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = "test-engine\n"
result = get_current_engine()
self.assertEqual(result, "test-engine")
@patch("subprocess.run", side_effect=FileNotFoundError("ibus not found"))
def test_get_current_engine_not_found(self, mock_run):
"""Test when ibus command is not found."""
from vocalinux.text_injection.ibus_engine import get_current_engine
result = get_current_engine()
self.assertIsNone(result)
class TestSwitchEngine(unittest.TestCase):
"""Tests for switch_engine function."""
@patch("vocalinux.text_injection.ibus_engine.get_current_engine")
@patch("time.sleep")
@patch("subprocess.run")
def test_switch_engine_success(self, mock_run, mock_sleep, mock_get_engine):
"""Test successfully switching engines."""
from vocalinux.text_injection.ibus_engine import switch_engine
mock_get_engine.return_value = "vocalinux"
result = switch_engine("vocalinux")
self.assertTrue(result)
@patch("vocalinux.text_injection.ibus_engine.get_current_engine")
@patch("time.sleep")
@patch("subprocess.run")
def test_switch_engine_failure(self, mock_run, mock_sleep, mock_get_engine):
"""Test when engine switch fails."""
from vocalinux.text_injection.ibus_engine import switch_engine
mock_get_engine.return_value = "other"
result = switch_engine("vocalinux")
self.assertFalse(result)
class TestIsEngineProcessRunning(unittest.TestCase):
"""Tests for is_engine_process_running function."""
@patch("pathlib.Path.exists")
def test_is_engine_process_running_no_pid_file(self, mock_exists):
"""Test when PID file doesn't exist."""
from vocalinux.text_injection.ibus_engine import is_engine_process_running
mock_exists.return_value = False
result = is_engine_process_running()
self.assertFalse(result)
class TestStopEngineProcess(unittest.TestCase):
"""Tests for stop_engine_process function."""
@patch("pathlib.Path.exists")
def test_stop_engine_process_no_pid_file(self, mock_exists):
"""Test when no PID file exists."""
from vocalinux.text_injection.ibus_engine import stop_engine_process
mock_exists.return_value = False
# Should not raise any exception
result = stop_engine_process()
# Verify the function handles missing PID file gracefully
self.assertIsNone(result)
if __name__ == "__main__":
unittest.main()