Files
voculinux/tests/test_main_args_deps.py
T
Anay GarodiaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
eb16a34328 feat(cli): add --version option 🤖🤖🤖 (#563)
* feat(cli): add version flag

* fix(cli): parse args before acquiring single-instance lock so --version works while app is running

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-25 13:04:52 -07:00

613 lines
22 KiB
Python

"""
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 contextlib import redirect_stdout
from io import StringIO
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]
# Also drop the stale attribute from the parent package so that
# mock.patch and a later re-import resolve to the same fresh module
parent, _, child = k.rpartition(".")
if parent and parent in sys.modules:
try:
delattr(sys.modules[parent], child)
except AttributeError:
pass
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_version_flag(self):
"""Test that --version prints the package version and exits."""
from vocalinux.version import __version__
output = StringIO()
with patch.object(sys, "argv", ["vocalinux", "--version"]):
from vocalinux.main import parse_arguments
with redirect_stdout(output), self.assertRaises(SystemExit) as exit_context:
parse_arguments()
assert exit_context.exception.code == 0
assert output.getvalue() == f"vocalinux {__version__}\n"
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
def test_main_version_flag_with_lock_held(self):
"""Test that --version works even when another instance holds the lock."""
from vocalinux.main import main
from vocalinux.version import __version__
mock_single_instance = MagicMock()
mock_single_instance.acquire_lock.return_value = False
output = StringIO()
with patch.dict(sys.modules, {"vocalinux.single_instance": mock_single_instance}):
with patch.object(sys, "argv", ["vocalinux", "--version"]):
with redirect_stdout(output):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
assert output.getvalue() == f"vocalinux {__version__}\n"
mock_single_instance.acquire_lock.assert_not_called()
@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()