""" Additional coverage tests for main.py module. Tests for parse_args(), main(), and VocalinuxApp class (if present). Covers normal startup, debug mode, version flag, keyboard interrupt, and exception handling. """ import os import sys import unittest from unittest.mock import MagicMock, call, patch import pytest # Autouse fixture to prevent sys.modules pollution between tests @pytest.fixture(autouse=True) def _restore_sys_modules(): saved = dict(sys.modules) yield added = set(sys.modules.keys()) - set(saved.keys()) for k in added: del sys.modules[k] for k, v in saved.items(): if k not in sys.modules or sys.modules[k] is not v: sys.modules[k] = v class TestParseArguments(unittest.TestCase): """Tests for parse_arguments() function.""" def test_parse_args_defaults(self): """Test parsing arguments with defaults.""" with patch.object(sys, "argv", ["vocalinux"]): from vocalinux.main import parse_arguments args = parse_arguments() assert args.debug is False assert args.model is None assert args.language is None assert args.engine is None assert args.wayland is False assert args.start_minimized is False def test_parse_args_debug_flag(self): """Test parsing with debug flag.""" with patch.object(sys, "argv", ["vocalinux", "--debug"]): from vocalinux.main import parse_arguments args = parse_arguments() assert args.debug is True def test_parse_args_model_argument(self): """Test parsing with model argument.""" with patch.object(sys, "argv", ["vocalinux", "--model", "small"]): from vocalinux.main import parse_arguments args = parse_arguments() assert args.model == "small" def test_parse_args_language_argument(self): """Test parsing with language argument.""" with patch.object(sys, "argv", ["vocalinux", "--language", "en-us"]): from vocalinux.main import parse_arguments args = parse_arguments() assert args.language == "en-us" def test_parse_args_engine_argument(self): """Test parsing with engine argument.""" with patch.object(sys, "argv", ["vocalinux", "--engine", "whisper_cpp"]): from vocalinux.main import parse_arguments args = parse_arguments() assert args.engine == "whisper_cpp" def test_parse_args_wayland_flag(self): """Test parsing with wayland flag.""" with patch.object(sys, "argv", ["vocalinux", "--wayland"]): from vocalinux.main import parse_arguments args = parse_arguments() assert args.wayland is True def test_parse_args_start_minimized_flag(self): """Test parsing with start-minimized flag.""" with patch.object(sys, "argv", ["vocalinux", "--start-minimized"]): from vocalinux.main import parse_arguments args = parse_arguments() assert args.start_minimized is True def test_parse_args_multiple_arguments(self): """Test parsing with multiple arguments.""" with patch.object( sys, "argv", [ "vocalinux", "--debug", "--model", "large", "--language", "es", "--engine", "vosk", "--wayland", ], ): from vocalinux.main import parse_arguments args = parse_arguments() assert args.debug is True assert args.model == "large" assert args.language == "es" assert args.engine == "vosk" assert args.wayland is True class TestCheckDependencies(unittest.TestCase): """Tests for check_dependencies() function.""" def test_check_dependencies_success(self): """Test successful dependency check with all deps mocked as available.""" from vocalinux.main import check_dependencies mock_gi = MagicMock() mock_gi_repo = MagicMock() with patch("vocalinux.main.logging"): with patch.dict( sys.modules, { "gi": mock_gi, "gi.repository": mock_gi_repo, "pynput": MagicMock(), "requests": MagicMock(), }, ): result = check_dependencies() assert result is True def test_check_dependencies_missing_gtk(self): """Test dependency check with missing GTK.""" from vocalinux.main import check_dependencies with patch("vocalinux.main.logging"): with patch("builtins.__import__", side_effect=ImportError("No module")): # Just verify the function doesn't crash result = check_dependencies() assert result is False def test_check_dependencies_logs_gnome_extension_guidance(self): from vocalinux.main import check_dependencies with patch("vocalinux.main.logger") as mock_logger: with patch("builtins.__import__", side_effect=ImportError("No module")): result = check_dependencies() assert result is False mock_logger.error.assert_any_call( " NOTE: On GNOME Shell (default on Debian), you also need:" ) mock_logger.error.assert_any_call(" sudo apt install gnome-shell-extension-appindicator") class TestCheckDisplayAvailable(unittest.TestCase): """Tests for check_display_available() function.""" @patch("vocalinux.main.logging") def test_check_display_available_success(self, mock_logging): """Test successful display check - mocks gi to avoid real Gdk calls.""" from vocalinux.main import check_display_available mock_gi = MagicMock() mock_gdk = MagicMock() mock_gdk.Display.get_default.return_value = MagicMock() # display exists mock_gi_repo = MagicMock(Gdk=mock_gdk) with patch.dict(sys.modules, {"gi": mock_gi, "gi.repository": mock_gi_repo}): result = check_display_available() assert result is True @patch("vocalinux.main.logging") def test_check_display_available_no_display(self, mock_logging): """Test display check when no display is available.""" from vocalinux.main import check_display_available mock_gi = MagicMock() mock_gdk = MagicMock() mock_gdk.Display.get_default.return_value = None # no display mock_gi_repo = MagicMock(Gdk=mock_gdk) with patch.dict(sys.modules, {"gi": mock_gi, "gi.repository": mock_gi_repo}): result = check_display_available() assert result is False class TestCheckAppIndicatorSupport(unittest.TestCase): def test_check_appindicator_support_true_when_watcher_present(self): from vocalinux.main import check_appindicator_support mock_proxy = MagicMock() mock_names_variant = MagicMock() mock_names_variant.unpack.return_value = ( ["org.freedesktop.DBus", "org.kde.StatusNotifierWatcher"], ) mock_proxy.call_sync.return_value = mock_names_variant mock_gio = MagicMock() mock_gio.DBusProxy.new_for_bus_sync.return_value = mock_proxy with patch.dict( sys.modules, { "gi": MagicMock(), "gi.repository": MagicMock(Gio=mock_gio), }, ): assert check_appindicator_support() is True def test_check_appindicator_support_false_when_watcher_missing(self): from vocalinux.main import check_appindicator_support mock_proxy = MagicMock() mock_names_variant = MagicMock() mock_names_variant.unpack.return_value = (["org.freedesktop.DBus"],) mock_proxy.call_sync.return_value = mock_names_variant mock_gio = MagicMock() mock_gio.DBusProxy.new_for_bus_sync.return_value = mock_proxy with patch.dict( sys.modules, { "gi": MagicMock(), "gi.repository": MagicMock(Gio=mock_gio), }, ): assert check_appindicator_support() is False def test_check_appindicator_support_true_on_exception(self): from vocalinux.main import check_appindicator_support mock_gio = MagicMock() mock_gio.DBusProxy.new_for_bus_sync.side_effect = RuntimeError("dbus unavailable") with patch.dict( sys.modules, { "gi": MagicMock(), "gi.repository": MagicMock(Gio=mock_gio), }, ): assert check_appindicator_support() is True class TestMainFunction(unittest.TestCase): """Tests for the main() function.""" @patch("vocalinux.main.logging") @patch("vocalinux.main.check_dependencies") @patch("vocalinux.main.check_display_available") @patch("vocalinux.main.parse_arguments") @patch("vocalinux.main.atexit") def test_main_single_instance_already_running( self, mock_atexit, mock_parse_args, mock_check_display, mock_check_deps, mock_logging, ): """Test main() when another instance is already running.""" from vocalinux.main import main mock_single_instance = MagicMock() mock_single_instance.acquire_lock.return_value = False with patch.dict(sys.modules, {"vocalinux.single_instance": mock_single_instance}): with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 1 @patch("vocalinux.main.check_display_available") @patch("vocalinux.main.logging") @patch("vocalinux.main.check_dependencies") @patch("vocalinux.main.parse_arguments") @patch("vocalinux.main.atexit") def test_main_missing_dependencies( self, mock_atexit, mock_parse_args, mock_check_deps, mock_logging, mock_check_display ): """Test main() when dependencies are missing.""" from vocalinux.main import main # Set up mocks mock_check_deps.return_value = False mock_parse_args.return_value = MagicMock(debug=False) # Mock single_instance module since it's imported inside main() mock_single_instance = MagicMock() mock_single_instance.acquire_lock.return_value = True with patch.dict(sys.modules, {"vocalinux.single_instance": mock_single_instance}): # Should exit with status 1 with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 1 @patch("vocalinux.main.logging") @patch("vocalinux.main.check_dependencies") @patch("vocalinux.main.check_display_available") @patch("vocalinux.main.parse_arguments") @patch("vocalinux.main.atexit") def test_main_no_display_available( self, mock_atexit, mock_parse_args, mock_check_display, mock_check_deps, mock_logging ): """Test main() when no display is available.""" from vocalinux.main import main # Set up mocks mock_check_deps.return_value = True mock_check_display.return_value = False mock_parse_args.return_value = MagicMock(debug=False) # Mock single_instance module since it's imported inside main() mock_single_instance = MagicMock() mock_single_instance.acquire_lock.return_value = True with patch.dict(sys.modules, {"vocalinux.single_instance": mock_single_instance}): # Should exit with status 1 with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 1 @patch("vocalinux.main.logging") @patch("vocalinux.main.check_dependencies") @patch("vocalinux.main.check_display_available") @patch("vocalinux.main.parse_arguments") @patch("vocalinux.main.atexit") def test_main_debug_mode_enabled( self, mock_atexit, mock_parse_args, mock_check_display, mock_check_deps, mock_logging, ): """Test main() with debug mode enabled.""" from vocalinux.main import main # Set up mocks mock_check_deps.return_value = True mock_check_display.return_value = True mock_args = MagicMock() mock_args.debug = True mock_args.wayland = False mock_args.start_minimized = False mock_parse_args.return_value = mock_args # Mock single_instance module since it's imported inside main() mock_single_instance = MagicMock() mock_single_instance.acquire_lock.return_value = True # Create proper mock for ConfigManager with get_settings() returning valid data mock_config_manager = MagicMock() mock_config_manager.return_value.get_settings.return_value = { "speech_recognition": { "engine": "whisper", "model_size": "medium", "language": "en-us", }, "audio": {}, "general": {"first_run": False}, } # Mock the imported modules with patch.dict( sys.modules, { "vocalinux.single_instance": mock_single_instance, "vocalinux.common_types": MagicMock(), "vocalinux.speech_recognition": MagicMock( recognition_manager=MagicMock( SpeechRecognitionManager=MagicMock( side_effect=Exception("Test exception to stop execution") ) ) ), "vocalinux.text_injection.text_injector": MagicMock(), "vocalinux.ui.tray_indicator": MagicMock(), "vocalinux.ui.action_handler": MagicMock(), "vocalinux.ui.config_manager": mock_config_manager, "vocalinux.ui.logging_manager": MagicMock(), "vocalinux.text_injection": MagicMock(), "vocalinux.ui.first_run_dialog": MagicMock(), "vocalinux.ui.autostart_manager": MagicMock(), "vocalinux.text_injection.start_ibus_daemon": MagicMock(), }, ): # Mock the config manager and other components with patch("vocalinux.main.logging.getLogger") as mock_get_logger: mock_logger = MagicMock() mock_get_logger.return_value = mock_logger # This would normally start the GTK main loop, which we want to avoid # The initialization should fail as we've set SpeechRecognitionManager to raise an exception with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 1 @patch("vocalinux.main.logging") @patch("vocalinux.main.check_dependencies") @patch("vocalinux.main.check_display_available") @patch("vocalinux.main.check_appindicator_support") @patch("vocalinux.main.parse_arguments") @patch("vocalinux.main.atexit") def test_main_logs_warning_when_appindicator_support_missing( self, mock_atexit, mock_parse_args, mock_check_appindicator, mock_check_display, mock_check_deps, mock_logging, ): from vocalinux.main import main mock_check_deps.return_value = True mock_check_display.return_value = True mock_check_appindicator.return_value = False mock_args = MagicMock() mock_args.debug = False mock_args.wayland = False mock_args.start_minimized = False mock_args.engine = None mock_args.model = None mock_args.language = None mock_parse_args.return_value = mock_args mock_single_instance = MagicMock() mock_single_instance.acquire_lock.return_value = True mock_config_manager = MagicMock() mock_config_manager.return_value.get_settings.return_value = { "speech_recognition": {}, "audio": {}, "general": {"first_run": False}, } mock_speech_manager_ctor = MagicMock(side_effect=Exception("stop")) with patch.dict( sys.modules, { "vocalinux.single_instance": mock_single_instance, "vocalinux.common_types": MagicMock(), "vocalinux.speech_recognition": MagicMock( recognition_manager=MagicMock(SpeechRecognitionManager=mock_speech_manager_ctor) ), "vocalinux.text_injection.text_injector": MagicMock(), "vocalinux.ui.tray_indicator": MagicMock(), "vocalinux.ui.action_handler": MagicMock(), "vocalinux.ui.config_manager": mock_config_manager, "vocalinux.ui.logging_manager": MagicMock(), "vocalinux.text_injection": MagicMock(start_ibus_daemon=MagicMock()), "vocalinux.ui.first_run_dialog": MagicMock(), "vocalinux.ui.autostart_manager": MagicMock(), }, ): with patch("vocalinux.main.logger") as mock_logger: with pytest.raises(SystemExit): main() mock_logger.warning.assert_any_call( "No StatusNotifierWatcher found on D-Bus session bus." ) mock_logger.warning.assert_any_call("The system tray icon may not appear.") mock_speech_manager_ctor.assert_called_once() @patch("vocalinux.main.logging") @patch("vocalinux.main.check_dependencies") @patch("vocalinux.main.check_display_available") @patch("vocalinux.main.parse_arguments") @patch("vocalinux.main.atexit") def test_main_initialization_error( self, mock_atexit, mock_parse_args, mock_check_display, mock_check_deps, mock_logging, ): """Test main() when initialization fails.""" from vocalinux.main import main # Set up mocks mock_check_deps.return_value = True mock_check_display.return_value = True mock_args = MagicMock() mock_args.debug = False mock_args.wayland = False mock_args.start_minimized = False mock_parse_args.return_value = mock_args # Mock single_instance module since it's imported inside main() mock_single_instance = MagicMock() mock_single_instance.acquire_lock.return_value = True # Mock components that will raise an exception with patch("vocalinux.main.logging.getLogger") as mock_get_logger: mock_logger = MagicMock() mock_get_logger.return_value = mock_logger with patch.dict( sys.modules, { "vocalinux.single_instance": mock_single_instance, "vocalinux.common_types": MagicMock(), "vocalinux.speech_recognition": MagicMock( recognition_manager=MagicMock( SpeechRecognitionManager=MagicMock(side_effect=Exception("Init error")) ) ), "vocalinux.text_injection.text_injector": MagicMock(), "vocalinux.ui.tray_indicator": MagicMock(), "vocalinux.ui.action_handler": MagicMock(), "vocalinux.ui.config_manager": MagicMock(), "vocalinux.ui.logging_manager": MagicMock(), "vocalinux.text_injection": MagicMock(), "vocalinux.ui.first_run_dialog": MagicMock(), "vocalinux.ui.autostart_manager": MagicMock(), "vocalinux.text_injection.start_ibus_daemon": MagicMock(), }, ): # Should exit with status 1 with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code == 1 class TestMainEntry(unittest.TestCase): """Tests for the main entry point and script execution.""" @patch("vocalinux.main.main") def test_main_entry_point(self, mock_main): """Test that the script entry point calls main().""" # Import the module and verify the entry point import vocalinux.main as main_module # Check that main function exists assert hasattr(main_module, "main") assert callable(main_module.main) def test_module_can_be_imported(self): """Test that the main module can be imported successfully.""" try: import vocalinux.main # noqa: F401 assert True except Exception as e: pytest.fail(f"Failed to import vocalinux.main: {e}") if __name__ == "__main__": unittest.main()