Files
voculinux/tests/test_main_args_deps.py
T
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30

650 lines
24 KiB
Python

"""
Additional coverage tests for main.py module.
Tests for parse_args(), main(), and VoculinuxApp 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", ["voculinux"]):
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
assert args.transcribe_file is None
def test_parse_args_debug_flag(self):
"""Test parsing with debug flag."""
with patch.object(sys, "argv", ["voculinux", "--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", ["voculinux", "--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"voculinux {__version__}\n"
def test_parse_args_model_argument(self):
"""Test parsing with model argument."""
with patch.object(sys, "argv", ["voculinux", "--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", ["voculinux", "--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", ["voculinux", "--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", ["voculinux", "--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", ["voculinux", "--start-minimized"]):
from vocalinux.main import parse_arguments
args = parse_arguments()
assert args.start_minimized is True
def test_parse_args_transcribe_file(self):
"""Test parsing with the headless --transcribe-file flag."""
with patch.object(sys, "argv", ["voculinux", "--transcribe-file", "/tmp/a.wav"]):
from vocalinux.main import parse_arguments
args = parse_arguments()
assert args.transcribe_file == "/tmp/a.wav"
def test_run_file_transcription_prints_transcript(self):
"""The headless path prints formatted blocks and skips GTK entirely."""
from vocalinux.main import _run_file_transcription
from vocalinux.speech_recognition.diarization import TranscriptBlock
blocks = [TranscriptBlock(0.0, 1, "Hello there.")]
with patch(
"vocalinux.speech_recognition.diarization.transcribe_audio_file",
return_value=blocks,
):
buffer = StringIO()
with redirect_stdout(buffer):
assert _run_file_transcription("/tmp/a.wav") == 0
assert "Speaker 1: Hello there." in buffer.getvalue()
def test_run_file_transcription_reports_errors_on_stderr(self):
"""Decode/load failures exit non-zero with the error on stderr."""
from vocalinux.main import _run_file_transcription
stderr_buffer = StringIO()
with patch(
"vocalinux.speech_recognition.diarization.transcribe_audio_file",
side_effect=RuntimeError("ffmpeg missing"),
):
with patch.object(sys, "stderr", stderr_buffer):
assert _run_file_transcription("/tmp/a.wav") == 1
assert "ffmpeg missing" in stderr_buffer.getvalue()
def test_parse_args_multiple_arguments(self):
"""Test parsing with multiple arguments."""
with patch.object(
sys,
"argv",
[
"voculinux",
"--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
# Patch the function itself: main() binds the module via the package
# attribute, so a patch.dict(sys.modules, ...) entry is ignored whenever
# an earlier test already imported the real vocalinux.single_instance.
with patch("vocalinux.single_instance.acquire_lock", return_value=False):
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", ["voculinux", "--version"]):
with redirect_stdout(output):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
assert output.getvalue() == f"voculinux {__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()