From c4f5b797ced4396921b45f841b08f33770aae747 Mon Sep 17 00:00:00 2001 From: Sbenazar <127324689+Sbenazar@users.noreply.github.com> Date: Mon, 6 Apr 2026 03:31:35 +0300 Subject: [PATCH] fix(ibus): start engine process before checking registration (fixes #360) (#361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ibus): start engine process before checking registration _setup_engine() gated on is_engine_registered() which queries `ibus list-engine`. On systems where the IBus daemon doesn't scan user-level component directories (~/.local/share/ibus/component/), the engine never appeared in the list, blocking start_engine_process() from ever being called — even though its register_component() D-Bus call is the correct registration path for standalone-launched engines. Reorder _setup_engine() to start the engine process first, relying on register_component() for runtime registration. Remove the XML install step from the setup path since install_ibus_component() triggers `ibus restart` which kills the engine process. Follow-up to PR #304 which added register_component() support. * fix(ibus): remove dead code and add engine readiness check Address review feedback: - Remove install_ibus_component(), is_engine_registered(), is_component_up_to_date(), _get_expected_component_xml() and their tests — no longer called after setup order fix - Add SOCKET_PATH poll after start_engine_process() to verify engine is fully initialized before activation - Rebase onto current main --- src/vocalinux/text_injection/__init__.py | 2 - src/vocalinux/text_injection/ibus_engine.py | 185 +++--------------- tests/test_ibus_engine.py | 196 +------------------- tests/test_ibus_engine_core.py | 23 +-- tests/test_ibus_engine_utils.py | 94 ---------- 5 files changed, 35 insertions(+), 465 deletions(-) diff --git a/src/vocalinux/text_injection/__init__.py b/src/vocalinux/text_injection/__init__.py index 8e52805..bb1ab09 100644 --- a/src/vocalinux/text_injection/__init__.py +++ b/src/vocalinux/text_injection/__init__.py @@ -6,9 +6,7 @@ from . import text_injector # noqa: F401 from .ibus_engine import ( # noqa: F401 IBusSetupError, IBusTextInjector, - install_ibus_component, is_engine_active, - is_engine_registered, is_ibus_active_input_method, is_ibus_available, start_ibus_daemon, diff --git a/src/vocalinux/text_injection/ibus_engine.py b/src/vocalinux/text_injection/ibus_engine.py index 83387a5..d218436 100644 --- a/src/vocalinux/text_injection/ibus_engine.py +++ b/src/vocalinux/text_injection/ibus_engine.py @@ -259,76 +259,6 @@ def _get_exec_command() -> str: return f"{sys.executable} {engine_script} --ibus" -def _get_expected_component_xml() -> str: - """Generate the expected component XML content for the current installation.""" - e = _ENGINE_META - c = _COMPONENT_META - - return f""" - - {COMPONENT_NAME} - {ENGINE_DESCRIPTION} - {_get_exec_command()} - {c['version']} - {c['author']} - {c['license']} - {c['homepage']} - {c['textdomain']} - - - {ENGINE_NAME} - {ENGINE_LONGNAME} - {e['language']} - {e['license']} - {e['author']} - {e['icon']} - {e['layout']} - {ENGINE_DESCRIPTION} - {ENGINE_RANK} - - - -""" - - -def is_component_up_to_date() -> bool: - """ - Check if the installed IBus component XML matches the current installation. - - Returns: - True if component exists and matches expected content, False otherwise - """ - component_file = Path.home() / ".local" / "share" / "ibus" / "component" / "vocalinux.xml" - - if not component_file.exists(): - return False - - try: - installed_content = component_file.read_text() - expected_content = _get_expected_component_xml() - return installed_content.strip() == expected_content.strip() - except Exception as e: - logger.debug(f"Failed to check component XML: {e}") - return False - - -def is_engine_registered() -> bool: - """Check if the Vocalinux IBus engine is registered.""" - if not IBUS_AVAILABLE: - return False - - try: - result = subprocess.run( - ["ibus", "list-engine"], - capture_output=True, - text=True, - timeout=5, - ) - return ENGINE_NAME in result.stdout - except (subprocess.SubprocessError, FileNotFoundError): - return False - - def is_engine_active() -> bool: """Check if the Vocalinux IBus engine is currently active.""" try: @@ -837,11 +767,12 @@ class IBusTextInjector: This class connects to the Vocalinux IBus engine via Unix socket and sends text to be injected. On initialization, it automatically: - 1. Installs the IBus component if not registered - 2. Saves the current engine + 1. Starts the engine process (registers via D-Bus register_component) + 2. Saves the current engine and XKB layout 3. Switches to the Vocalinux engine + 4. Restores the XKB layout - On cleanup (stop), it restores the previous engine. + On cleanup (stop), it restores the previous engine and layout. """ def __init__(self, auto_activate: bool = True): @@ -863,27 +794,28 @@ class IBusTextInjector: def _setup_engine(self) -> None: """Install and activate the IBus engine.""" - # Install or update component if needed - if not is_engine_registered(): - logger.info("IBus engine not registered, installing...") - if not install_ibus_component(system_wide=False): - logger.info("User-level install failed, trying system-wide...") - install_ibus_component(system_wide=True) - elif not is_component_up_to_date(): - logger.info("IBus component XML is outdated, updating...") - install_ibus_component(system_wide=False) - - # Check again after installation - if not is_engine_registered(): - raise IBusSetupError( - "Failed to register IBus engine. " - "Try running 'ibus list-engine' to verify installation." - ) - - # Start the engine process (Vocalinux starts it, not IBus) + # Start the engine process — it calls register_component() via + # D-Bus which makes the engine available even when the IBus daemon + # doesn't scan ~/.local/share/ibus/component/. + # Follow-up to PR #304: register_component() is the reliable path, + # so we no longer gate on is_engine_registered() / ibus list-engine. if not start_engine_process(): raise IBusSetupError("Failed to start IBus engine process. Check logs for details.") + # Verify the engine is fully ready before proceeding. + # start_engine_process() only confirms the subprocess is alive — + # register_component() and socket setup may still be in progress. + for _attempt in range(15): + if SOCKET_PATH.exists(): + logger.debug("Engine socket is ready") + break + time.sleep(0.2) + else: + logger.warning( + "Engine process started but socket not ready after retries; " + "proceeding with activation attempt" + ) + # Capture current XKB layout before switching engines # This is critical for preserving the user's keyboard layout # when IBus engine switching might override it @@ -995,77 +927,6 @@ class IBusTextInjector: return False -def install_ibus_component(system_wide: bool = False) -> bool: - """ - Install the IBus component XML file. - - Args: - system_wide: If True, install to /usr/share/ibus/component/ (requires root). - If False, install to user directory and set IBUS_COMPONENT_PATH. - - Returns: - True if installation was successful, False otherwise - """ - component_xml = _get_expected_component_xml() - - try: - if system_wide: - component_dir = Path("/usr/share/ibus/component") - component_file = component_dir / "vocalinux.xml" - - # Write to temp file and move with sudo - import tempfile - - with tempfile.NamedTemporaryFile(mode="w", suffix=".xml", delete=False) as f: - f.write(component_xml) - temp_path = f.name - - result = subprocess.run( - ["sudo", "cp", temp_path, str(component_file)], - capture_output=True, - text=True, - ) - os.unlink(temp_path) - - if result.returncode != 0: - logger.error(f"Failed to install component: {result.stderr}") - return False - - subprocess.run( - ["sudo", "chmod", "644", str(component_file)], - capture_output=True, - ) - else: - # User-level installation - component_dir = Path.home() / ".local" / "share" / "ibus" / "component" - component_dir.mkdir(parents=True, exist_ok=True) - component_file = component_dir / "vocalinux.xml" - component_file.write_text(component_xml) - logger.info(f"Component installed to {component_file}") - - # Refresh IBus - restart with component path set so it picks up user components - import time - - user_component_dir = Path.home() / ".local" / "share" / "ibus" / "component" - env = os.environ.copy() - env["IBUS_COMPONENT_PATH"] = f"{user_component_dir}:/usr/share/ibus/component" - - subprocess.run(["ibus", "write-cache"], capture_output=True, env=env) - subprocess.run(["ibus", "restart"], capture_output=True, env=env) - time.sleep(1) # Give IBus time to restart - - logger.info( - "IBus component installed successfully.\n" - "Set 'Vocalinux' as your input method in system settings " - "to enable voice dictation." - ) - return True - - except Exception as e: - logger.error(f"Failed to install IBus component: {e}") - return False - - def _get_engines_xml() -> str: """Return engine XML for IBus --xml discovery. diff --git a/tests/test_ibus_engine.py b/tests/test_ibus_engine.py index 1f66315..a670a2e 100644 --- a/tests/test_ibus_engine.py +++ b/tests/test_ibus_engine.py @@ -54,31 +54,9 @@ class TestIBusTextInjectorSetupFailures(unittest.TestCase): @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True) @patch("vocalinux.text_injection.ibus_engine.ensure_ibus_dir") - @patch("vocalinux.text_injection.ibus_engine.install_ibus_component") - @patch("vocalinux.text_injection.ibus_engine.is_engine_registered") - def test_raises_on_registration_failure( - self, mock_is_registered, mock_install, mock_ensure_dir - ): - """Test that IBusSetupError is raised when engine registration fails.""" - mock_is_registered.return_value = False # Registration fails - mock_install.return_value = True - - from vocalinux.text_injection.ibus_engine import IBusSetupError, IBusTextInjector - - with self.assertRaises(IBusSetupError) as context: - IBusTextInjector(auto_activate=True) - - self.assertIn("Failed to register IBus engine", str(context.exception)) - - @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True) - @patch("vocalinux.text_injection.ibus_engine.ensure_ibus_dir") - @patch("vocalinux.text_injection.ibus_engine.is_engine_registered") @patch("vocalinux.text_injection.ibus_engine.start_engine_process") - def test_raises_on_engine_process_failure( - self, mock_start_engine, mock_is_registered, mock_ensure_dir - ): + def test_raises_on_engine_process_failure(self, mock_start_engine, mock_ensure_dir): """Test that IBusSetupError is raised when engine process fails to start.""" - mock_is_registered.return_value = True mock_start_engine.return_value = False # Process start fails from vocalinux.text_injection.ibus_engine import IBusSetupError, IBusTextInjector @@ -90,7 +68,7 @@ class TestIBusTextInjectorSetupFailures(unittest.TestCase): @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True) @patch("vocalinux.text_injection.ibus_engine.ensure_ibus_dir") - @patch("vocalinux.text_injection.ibus_engine.is_engine_registered") + @patch("vocalinux.text_injection.ibus_engine.SOCKET_PATH") @patch("vocalinux.text_injection.ibus_engine.start_engine_process") @patch("vocalinux.text_injection.ibus_engine.is_engine_active") @patch("vocalinux.text_injection.ibus_engine.get_current_engine") @@ -101,12 +79,12 @@ class TestIBusTextInjectorSetupFailures(unittest.TestCase): mock_get_current, mock_is_active, mock_start_engine, - mock_is_registered, + mock_socket_path, mock_ensure_dir, ): """Test that IBusSetupError is raised when engine activation fails.""" - mock_is_registered.return_value = True mock_start_engine.return_value = True + mock_socket_path.exists.return_value = True mock_is_active.return_value = False mock_get_current.return_value = "xkb:us::eng" mock_switch.return_value = False # Activation fails @@ -138,102 +116,6 @@ class TestIBusEngineHelpers(unittest.TestCase): self.assertEqual(is_ibus_available(), IBUS_AVAILABLE) -class TestIsEngineRegistered(unittest.TestCase): - """Tests for is_engine_registered function.""" - - @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True) - @patch("subprocess.run") - def test_engine_registered(self, mock_run): - """Test detection when engine is registered.""" - mock_run.return_value = MagicMock(stdout="vocalinux\nxkb:us::eng\n", returncode=0) - - from vocalinux.text_injection.ibus_engine import is_engine_registered - - result = is_engine_registered() - self.assertTrue(result) - mock_run.assert_called_once() - - @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True) - @patch("subprocess.run") - def test_engine_not_registered(self, mock_run): - """Test detection when engine is not registered.""" - mock_run.return_value = MagicMock(stdout="xkb:us::eng\n", returncode=0) - - from vocalinux.text_injection.ibus_engine import is_engine_registered - - result = is_engine_registered() - self.assertFalse(result) - - @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True) - @patch("subprocess.run") - def test_subprocess_error(self, mock_run): - """Test handling of subprocess errors.""" - import subprocess - - mock_run.side_effect = subprocess.SubprocessError("Command failed") - - from vocalinux.text_injection.ibus_engine import is_engine_registered - - result = is_engine_registered() - self.assertFalse(result) - - @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", False) - def test_ibus_not_available(self): - """Test returns False when IBus is not available.""" - from vocalinux.text_injection.ibus_engine import is_engine_registered - - result = is_engine_registered() - self.assertFalse(result) - - -class TestIsComponentUpToDate(unittest.TestCase): - """Tests for is_component_up_to_date function.""" - - def test_component_missing(self): - """Test returns False when component file doesn't exist.""" - with patch("pathlib.Path.home") as mock_home: - mock_home.return_value = Path("/nonexistent") - from vocalinux.text_injection.ibus_engine import is_component_up_to_date - - result = is_component_up_to_date() - self.assertFalse(result) - - def test_component_matches(self): - """Test returns True when component content matches.""" - with tempfile.TemporaryDirectory() as tmpdir: - component_dir = Path(tmpdir) / ".local" / "share" / "ibus" / "component" - component_dir.mkdir(parents=True) - component_file = component_dir / "vocalinux.xml" - - with patch("pathlib.Path.home", return_value=Path(tmpdir)): - from vocalinux.text_injection.ibus_engine import ( - _get_expected_component_xml, - is_component_up_to_date, - ) - - # Write expected content - component_file.write_text(_get_expected_component_xml()) - - result = is_component_up_to_date() - self.assertTrue(result) - - def test_component_differs(self): - """Test returns False when component content differs.""" - with tempfile.TemporaryDirectory() as tmpdir: - component_dir = Path(tmpdir) / ".local" / "share" / "ibus" / "component" - component_dir.mkdir(parents=True) - component_file = component_dir / "vocalinux.xml" - - with patch("pathlib.Path.home", return_value=Path(tmpdir)): - from vocalinux.text_injection.ibus_engine import is_component_up_to_date - - # Write stale content - component_file.write_text("stale") - - result = is_component_up_to_date() - self.assertFalse(result) - - class TestIsEngineActive(unittest.TestCase): """Tests for is_engine_active function.""" @@ -368,8 +250,7 @@ class TestIBusTextInjector(unittest.TestCase): @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True) @patch("vocalinux.text_injection.ibus_engine.ensure_ibus_dir") - @patch("vocalinux.text_injection.ibus_engine.install_ibus_component") - @patch("vocalinux.text_injection.ibus_engine.is_engine_registered") + @patch("vocalinux.text_injection.ibus_engine.SOCKET_PATH") @patch("vocalinux.text_injection.ibus_engine.is_engine_active") @patch("vocalinux.text_injection.ibus_engine.start_engine_process") @patch("vocalinux.text_injection.ibus_engine.get_current_engine") @@ -380,12 +261,11 @@ class TestIBusTextInjector(unittest.TestCase): mock_get_current, mock_start_engine, mock_is_active, - mock_is_registered, - mock_install, + mock_socket_path, mock_ensure_dir, ): """Test initialization with auto_activate=True.""" - mock_is_registered.return_value = True + mock_socket_path.exists.return_value = True mock_is_active.return_value = False mock_start_engine.return_value = True mock_get_current.return_value = "xkb:us::eng" @@ -487,27 +367,28 @@ class TestIBusTextInjector(unittest.TestCase): @patch("vocalinux.text_injection.ibus_engine.restore_xkb_layout") @patch("vocalinux.text_injection.ibus_engine.get_current_xkb_layout") + @patch("vocalinux.text_injection.ibus_engine.SOCKET_PATH") @patch("vocalinux.text_injection.ibus_engine.start_engine_process", return_value=True) @patch("vocalinux.text_injection.ibus_engine.switch_engine", return_value=True) @patch("vocalinux.text_injection.ibus_engine.get_current_engine", return_value="xkb:us::eng") @patch("vocalinux.text_injection.ibus_engine.is_engine_active", return_value=False) - @patch("vocalinux.text_injection.ibus_engine.is_engine_registered", return_value=True) @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True) @patch("vocalinux.text_injection.ibus_engine.ensure_ibus_dir") def test_setup_engine_captures_and_restores_xkb( self, mock_ensure_dir, - mock_registered, mock_active, mock_get_engine, mock_switch, mock_start_proc, + mock_socket_path, mock_get_xkb, mock_restore_xkb, ): """Test _setup_engine captures XKB layout and restores it after activation (#292).""" from vocalinux.text_injection.ibus_engine import IBusTextInjector + mock_socket_path.exists.return_value = True mock_get_xkb.return_value = ("fr", "azerty", "") injector = IBusTextInjector(auto_activate=False) @@ -621,63 +502,6 @@ class TestIBusTextInjector(unittest.TestCase): self.assertFalse(result) -class TestInstallIBusComponent(unittest.TestCase): - """Tests for install_ibus_component function.""" - - @patch("subprocess.run") - def test_user_install_success(self, mock_run): - """Test successful user-level installation.""" - mock_run.return_value = MagicMock(returncode=0) - - with tempfile.TemporaryDirectory() as temp_dir: - component_dir = Path(temp_dir) - - with patch( - "vocalinux.text_injection.ibus_engine.Path.home", - return_value=Path(temp_dir), - ): - from vocalinux.text_injection.ibus_engine import install_ibus_component - - # Force reload to use patched home - result = install_ibus_component(system_wide=False) - - self.assertTrue(result) - - @patch("subprocess.run") - def test_system_install_needs_sudo(self, mock_run): - """Test system-wide installation calls sudo.""" - mock_run.return_value = MagicMock(returncode=0) - - from vocalinux.text_injection.ibus_engine import install_ibus_component - - with patch("tempfile.NamedTemporaryFile"): - with patch("os.unlink"): - result = install_ibus_component(system_wide=True) - - # Verify sudo was called - sudo_calls = [ - call for call in mock_run.call_args_list if call[0][0] and "sudo" in call[0][0] - ] - self.assertTrue(len(sudo_calls) > 0) - - @patch("subprocess.run") - def test_system_install_failure(self, mock_run): - """Test system-wide installation failure.""" - mock_run.return_value = MagicMock(returncode=1, stderr="Permission denied") - - from vocalinux.text_injection.ibus_engine import install_ibus_component - - with patch("tempfile.NamedTemporaryFile") as mock_tmp: - mock_tmp.return_value.__enter__ = MagicMock() - mock_tmp.return_value.__exit__ = MagicMock() - mock_tmp.return_value.name = "/tmp/test.xml" - mock_tmp.return_value.write = MagicMock() - with patch("os.unlink"): - result = install_ibus_component(system_wide=True) - - self.assertFalse(result) - - class TestTextInjectorWithIBus(unittest.TestCase): """Tests for TextInjector integration with IBus.""" diff --git a/tests/test_ibus_engine_core.py b/tests/test_ibus_engine_core.py index fb48106..f80cd55 100644 --- a/tests/test_ibus_engine_core.py +++ b/tests/test_ibus_engine_core.py @@ -138,24 +138,6 @@ class TestIBusEngineModuleFunctions(unittest.TestCase): result = is_ibus_active_input_method() self.assertFalse(result) - @patch("subprocess.run") - def test_is_engine_registered_success(self, mock_run): - """Test engine registration detection.""" - mock_run.return_value = MagicMock(returncode=0, stdout="vocalinux other-engine") - from vocalinux.text_injection.ibus_engine import is_engine_registered - - result = is_engine_registered() - self.assertTrue(result) - - @patch("subprocess.run") - def test_is_engine_registered_not_found(self, mock_run): - """Test when engine is not registered.""" - mock_run.return_value = MagicMock(returncode=0, stdout="other-engine") - from vocalinux.text_injection.ibus_engine import is_engine_registered - - result = is_engine_registered() - self.assertFalse(result) - @patch("subprocess.run") def test_is_engine_active_success(self, mock_run): """Test when engine is active.""" @@ -498,12 +480,11 @@ class TestIBusTextInjector(unittest.TestCase): self.assertIsNone(injector._previous_engine) @patch("vocalinux.text_injection.ibus_engine.ensure_ibus_dir") - @patch("vocalinux.text_injection.ibus_engine.is_engine_registered", return_value=True) - @patch("vocalinux.text_injection.ibus_engine.is_component_up_to_date", return_value=True) + @patch("vocalinux.text_injection.ibus_engine.SOCKET_PATH") @patch("vocalinux.text_injection.ibus_engine.start_engine_process", return_value=True) @patch("vocalinux.text_injection.ibus_engine.is_engine_active", return_value=True) def test_ibus_text_injector_init_with_auto_activate( - self, mock_active, mock_start, mock_update, mock_registered, mock_ensure_dir + self, mock_active, mock_start, mock_socket_path, mock_ensure_dir ): """Test IBusTextInjector initialization with auto-activation.""" from vocalinux.text_injection.ibus_engine import IBusTextInjector diff --git a/tests/test_ibus_engine_utils.py b/tests/test_ibus_engine_utils.py index 2c6af9a..d71e124 100644 --- a/tests/test_ibus_engine_utils.py +++ b/tests/test_ibus_engine_utils.py @@ -180,100 +180,6 @@ class TestStartIBusDaemon(unittest.TestCase): self.assertFalse(result) -class TestGetExpectedComponentXml(unittest.TestCase): - """Tests for _get_expected_component_xml function.""" - - def test_get_expected_component_xml_returns_string(self): - """Test that function returns valid XML string.""" - from vocalinux.text_injection.ibus_engine import _get_expected_component_xml - - result = _get_expected_component_xml() - self.assertIsInstance(result, str) - self.assertIn("", result) - - def test_get_expected_component_xml_contains_vocalinux(self): - """Test that XML contains Vocalinux references.""" - from vocalinux.text_injection.ibus_engine import _get_expected_component_xml - - result = _get_expected_component_xml() - self.assertIn("vocalinux", result.lower()) - - -class TestIsComponentUpToDate(unittest.TestCase): - """Tests for is_component_up_to_date function.""" - - @patch("pathlib.Path.exists") - def test_is_component_up_to_date_not_exists(self, mock_exists): - """Test when component file doesn't exist.""" - from vocalinux.text_injection.ibus_engine import is_component_up_to_date - - mock_exists.return_value = False - result = is_component_up_to_date() - self.assertFalse(result) - - @patch("vocalinux.text_injection.ibus_engine._get_expected_component_xml") - @patch("pathlib.Path.read_text") - @patch("pathlib.Path.exists") - def test_is_component_up_to_date_matches(self, mock_exists, mock_read, mock_get_xml): - """Test when XML matches.""" - from vocalinux.text_injection.ibus_engine import is_component_up_to_date - - xml_content = "" - mock_exists.return_value = True - mock_read.return_value = xml_content - mock_get_xml.return_value = xml_content - - result = is_component_up_to_date() - self.assertTrue(result) - - @patch("vocalinux.text_injection.ibus_engine._get_expected_component_xml") - @patch("pathlib.Path.read_text") - @patch("pathlib.Path.exists") - def test_is_component_up_to_date_mismatch(self, mock_exists, mock_read, mock_get_xml): - """Test when XML differs.""" - from vocalinux.text_injection.ibus_engine import is_component_up_to_date - - mock_exists.return_value = True - mock_read.return_value = "" - mock_get_xml.return_value = "" - - result = is_component_up_to_date() - self.assertFalse(result) - - -class TestIsEngineRegistered(unittest.TestCase): - """Tests for is_engine_registered function.""" - - @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", False) - def test_is_engine_registered_not_available(self): - """Test when IBus is not available.""" - from vocalinux.text_injection.ibus_engine import is_engine_registered - - result = is_engine_registered() - self.assertFalse(result) - - @patch("subprocess.run") - @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True) - def test_is_engine_registered_true(self, mock_run): - """Test when engine is registered.""" - from vocalinux.text_injection.ibus_engine import ENGINE_NAME, is_engine_registered - - mock_run.return_value.stdout = f"other\n{ENGINE_NAME}\nmore" - result = is_engine_registered() - self.assertTrue(result) - - @patch("subprocess.run") - @patch("vocalinux.text_injection.ibus_engine.IBUS_AVAILABLE", True) - def test_is_engine_registered_false(self, mock_run): - """Test when engine is not registered.""" - from vocalinux.text_injection.ibus_engine import is_engine_registered - - mock_run.return_value.stdout = "other\nengines" - result = is_engine_registered() - self.assertFalse(result) - - class TestIsEngineActive(unittest.TestCase): """Tests for is_engine_active function."""