* feat: prepare for v0.2.0-alpha release * feat: prepare for v0.2.0-alpha release * chore: remove CHANGELOG.md, use GitHub Releases instead * refactor: use existing About dialog instead of Settings section * style: enhance About dialog UI readability * style: fix formatting with black and isort * feat: show VAD and silence timeout settings for Whisper engine - Renamed 'VOSK Settings' to 'Recognition Settings' - VAD Sensitivity and Silence Timeout now apply to both engines - Added tooltips explaining each setting - VOSK-specific model info only shown when VOSK selected * fix: resolve overlapping UI elements in settings dialog * fix: save VAD and silence timeout settings for both engines Previously these settings were only saved when VOSK was selected. Now they persist correctly for Whisper as well. * fix: show download progress when selecting undownloaded model Previously when auto-applying settings, models would download silently in the background without showing the progress dialog. Now: - Download progress dialog appears for undownloaded models - Model icons update correctly after download completes * feat: add download progress, speed, ETA and cancel button - Show real download progress percentage for VOSK models - Display download speed (MB/s) and estimated time remaining - Add Cancel button to abort downloads - Update status during extraction phase - Whisper downloads still pulse (uses internal downloader) * fix: defer model download until needed, don't block startup - App now starts instantly even if models aren't downloaded - Models are downloaded on-demand when selected in Settings - If user tries to dictate without a model, plays error sound - Added model_ready property to check if engine is usable * fix: improve first-run UX without downloaded models - Settings dialog now defaults to smallest available model, not saved config - Only show saved model if it's actually downloaded - Add desktop notification when trying to dictate without a model - Prevents auto-downloading large models on first launch * feat: add download progress tracking for Whisper models - Implement custom Whisper model download with progress/speed/ETA - Uses OpenAI's CDN URLs directly instead of whisper.load_model() - Progress callback works the same as VOSK downloads - Supports cancellation during download * fix: enable download progress callback for Whisper models Progress callback was only being set for VOSK downloads, not Whisper. Now both engines show real-time progress, speed, and ETA. * fix: update tests for defer_download and config manager mocking - test_main: Mock ConfigManager to prevent saved config from overriding test values - test_settings_dialog: Mock ModelDownloadDialog to prevent GTK init errors - test_speech_recognition: Use proper MockVoskModule class instead of MagicMock, pass defer_download=False for download test - test_whisper_support: Pass defer_download=False to trigger model loading in test * fix: resolve test failures for CI - test_main: Fix ConfigManager patch path to use source module - test_settings_dialog: Mock _is_vosk_model_downloaded and _is_whisper_model_downloaded to skip download flow, add missing dialog attributes - test_speech_recognition: Replace recognizer with properly configured mock that returns valid JSON, fixing test isolation issues when running all tests together * fix: mock _download_whisper_model in whisper test to avoid requests exception handling issue * fix: add gir1.2-girepository-2.0 dep for PyGObject on Ubuntu 24.04 - Add missing girepository-2.0 package for newer Ubuntu runners - Use --no-build-isolation for PyGObject to use system packages * fix: skip PyGObject pip install, use system python3-gi package - Use --no-deps for editable install to skip PyGObject - Install other deps explicitly without PyGObject - Add python3-gi-cairo for complete GTK support - Removes problematic girepository-2.0 build requirement * fix: install PyGObject via pip with proper build deps - Remove python3-gi/python3-gi-cairo (doesn't work with setup-python) - Install PyGObject from pip after installing build dependencies - Simplify venv setup without --system-site-packages hack * Fix GObject * fix: set GI_TYPELIB_PATH for pip-installed PyGObject to find AppIndicator3
229 lines
8.9 KiB
Python
229 lines
8.9 KiB
Python
"""
|
|
Tests for the SettingsDialog.
|
|
"""
|
|
|
|
import os
|
|
|
|
# Mock GTK before importing anything that might use it
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
sys.modules["gi"] = MagicMock()
|
|
sys.modules["gi.repository"] = MagicMock()
|
|
sys.modules["gi.repository.Gtk"] = MagicMock()
|
|
sys.modules["gi.repository.GLib"] = MagicMock()
|
|
|
|
from vocalinux.common_types import RecognitionState
|
|
|
|
# Now import the class under test with GTK already mocked
|
|
from vocalinux.ui.settings_dialog import ENGINE_MODELS, SettingsDialog
|
|
|
|
# Create mock for speech engine
|
|
mock_speech_engine = Mock()
|
|
mock_speech_engine.state = RecognitionState.IDLE
|
|
mock_speech_engine.reconfigure = Mock()
|
|
mock_speech_engine.start_recognition = Mock()
|
|
mock_speech_engine.stop_recognition = Mock()
|
|
mock_speech_engine.register_text_callback = Mock()
|
|
mock_speech_engine.unregister_text_callback = Mock()
|
|
|
|
# Create mock for config manager
|
|
mock_config_manager = Mock()
|
|
mock_config_manager.get = Mock(
|
|
return_value={
|
|
"speech_recognition": {
|
|
"engine": "vosk",
|
|
"model_size": "small",
|
|
"vad_sensitivity": 3,
|
|
"silence_timeout": 2.0,
|
|
}
|
|
}
|
|
)
|
|
mock_config_manager.update_speech_recognition_settings = Mock()
|
|
mock_config_manager.save_settings = Mock()
|
|
|
|
|
|
class TestSettingsDialog(unittest.TestCase):
|
|
"""Test cases for the settings dialog."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
# Reset mocks before each test
|
|
mock_speech_engine.reset_mock()
|
|
mock_config_manager.reset_mock()
|
|
|
|
# Create mock for dialog components
|
|
self.engine_combo = Mock()
|
|
self.engine_combo.get_active_text.return_value = "Vosk"
|
|
self.engine_combo.get_active_id.return_value = "Vosk"
|
|
|
|
self.model_combo = Mock()
|
|
self.model_combo.get_active_text.return_value = "Small"
|
|
self.model_combo.get_active_id.return_value = "Small"
|
|
|
|
self.vad_spin = Mock()
|
|
self.vad_spin.get_value.return_value = 3
|
|
|
|
self.silence_spin = Mock()
|
|
self.silence_spin.get_value.return_value = 2.0
|
|
|
|
self.vosk_settings_box = Mock()
|
|
self.vosk_settings_box.is_visible.return_value = True
|
|
|
|
self.test_button = Mock()
|
|
|
|
self.test_textview = Mock()
|
|
buffer_mock = Mock()
|
|
buffer_mock.get_start_iter.return_value = Mock()
|
|
buffer_mock.get_end_iter.return_value = Mock()
|
|
buffer_mock.get_text.return_value = ""
|
|
self.test_textview.get_buffer.return_value = buffer_mock
|
|
|
|
# Mock SettingsDialog to avoid actually creating GTK objects
|
|
with patch("vocalinux.ui.settings_dialog.SettingsDialog.__init__", return_value=None):
|
|
self.dialog = SettingsDialog(
|
|
parent=None,
|
|
config_manager=mock_config_manager,
|
|
speech_engine=mock_speech_engine,
|
|
)
|
|
# Set mock attributes on dialog
|
|
self.dialog.engine_combo = self.engine_combo
|
|
self.dialog.model_combo = self.model_combo
|
|
self.dialog.vad_spin = self.vad_spin
|
|
self.dialog.silence_spin = self.silence_spin
|
|
self.dialog.vosk_settings_box = self.vosk_settings_box
|
|
self.dialog.test_button = self.test_button
|
|
self.dialog.test_textview = self.test_textview
|
|
self.dialog.config_manager = mock_config_manager
|
|
self.dialog.speech_engine = mock_speech_engine
|
|
|
|
# Mock methods that interact with UI
|
|
self.dialog.get_selected_settings = Mock(
|
|
return_value={
|
|
"engine": "vosk",
|
|
"model_size": "small",
|
|
"vad_sensitivity": 3,
|
|
"silence_timeout": 2.0,
|
|
}
|
|
)
|
|
|
|
# Create a real method for apply_settings to test
|
|
self.dialog.apply_settings = SettingsDialog.apply_settings.__get__(
|
|
self.dialog, SettingsDialog
|
|
)
|
|
self.dialog._test_text_callback = Mock()
|
|
self.dialog._stop_test_after_delay = Mock()
|
|
self.dialog.destroy = Mock()
|
|
# Add missing attributes for apply_settings
|
|
self.dialog.current_model_size = "small"
|
|
self.dialog.current_engine = "vosk"
|
|
self.dialog._populate_model_options = Mock()
|
|
|
|
def test_apply_settings_success(self):
|
|
"""Test the apply_settings method calls config and engine methods."""
|
|
# Change the returned settings for this test
|
|
self.dialog.get_selected_settings.return_value = {
|
|
"engine": "vosk",
|
|
"model_size": "large",
|
|
"vad_sensitivity": 3,
|
|
"silence_timeout": 2.0,
|
|
}
|
|
|
|
# Ensure reconfigure doesn't raise an exception
|
|
mock_speech_engine.reconfigure.side_effect = None
|
|
|
|
# Mock the Gtk module for this test
|
|
with patch("vocalinux.ui.settings_dialog.Gtk") as mock_gtk, patch(
|
|
"vocalinux.ui.settings_dialog.GLib"
|
|
) as mock_glib, patch("vocalinux.ui.settings_dialog.threading") as mock_threading, patch(
|
|
"vocalinux.ui.settings_dialog.time"
|
|
) as mock_time, patch(
|
|
"vocalinux.ui.settings_dialog.logging"
|
|
) as mock_logging, patch(
|
|
"vocalinux.ui.settings_dialog._is_vosk_model_downloaded", return_value=True
|
|
) as mock_vosk_check, patch(
|
|
"vocalinux.ui.settings_dialog._is_whisper_model_downloaded", return_value=True
|
|
) as mock_whisper_check:
|
|
# Call the method under test
|
|
result = self.dialog.apply_settings()
|
|
|
|
# Verify the result
|
|
self.assertTrue(result)
|
|
|
|
# Verify mocks were called with the right parameters
|
|
mock_config_manager.update_speech_recognition_settings.assert_called_once()
|
|
mock_config_manager.save_settings.assert_called_once()
|
|
mock_speech_engine.reconfigure.assert_called_once()
|
|
|
|
def test_apply_settings_stops_engine_if_running(self):
|
|
"""Test apply_settings stops the engine if it was running."""
|
|
# Set the engine state to running
|
|
mock_speech_engine.state = RecognitionState.LISTENING
|
|
|
|
# Ensure reconfigure doesn't raise an exception
|
|
mock_speech_engine.reconfigure.side_effect = None
|
|
|
|
# Mock the Gtk module for this test
|
|
with patch("vocalinux.ui.settings_dialog.Gtk") as mock_gtk, patch(
|
|
"vocalinux.ui.settings_dialog.GLib"
|
|
) as mock_glib, patch("vocalinux.ui.settings_dialog.threading") as mock_threading, patch(
|
|
"vocalinux.ui.settings_dialog.time"
|
|
) as mock_time, patch(
|
|
"vocalinux.ui.settings_dialog.logging"
|
|
) as mock_logging, patch(
|
|
"vocalinux.ui.settings_dialog._is_vosk_model_downloaded", return_value=True
|
|
) as mock_vosk_check, patch(
|
|
"vocalinux.ui.settings_dialog._is_whisper_model_downloaded", return_value=True
|
|
) as mock_whisper_check:
|
|
# Call the method under test
|
|
result = self.dialog.apply_settings()
|
|
|
|
# Verify the result
|
|
self.assertTrue(result)
|
|
|
|
# Verify engine was stopped before reconfigure
|
|
mock_speech_engine.stop_recognition.assert_called_once()
|
|
mock_speech_engine.reconfigure.assert_called_once()
|
|
|
|
def test_apply_settings_failure_reconfigure(self):
|
|
"""Test apply_settings handles errors during engine reconfiguration."""
|
|
# Set up the reconfigure method to raise an exception
|
|
mock_speech_engine.reconfigure.side_effect = Exception("Model load failed")
|
|
|
|
# Mock the Gtk module for this test
|
|
with patch("vocalinux.ui.settings_dialog.Gtk") as mock_gtk, patch(
|
|
"vocalinux.ui.settings_dialog.GLib"
|
|
) as mock_glib, patch("vocalinux.ui.settings_dialog.threading") as mock_threading, patch(
|
|
"vocalinux.ui.settings_dialog.time"
|
|
) as mock_time, patch(
|
|
"vocalinux.ui.settings_dialog.logging"
|
|
) as mock_logging, patch(
|
|
"vocalinux.ui.settings_dialog._is_vosk_model_downloaded", return_value=True
|
|
) as mock_vosk_check, patch(
|
|
"vocalinux.ui.settings_dialog._is_whisper_model_downloaded", return_value=True
|
|
) as mock_whisper_check:
|
|
# Mock the message dialog
|
|
mock_dialog = MagicMock()
|
|
mock_gtk.MessageDialog.return_value = mock_dialog
|
|
|
|
# Call the method under test
|
|
result = self.dialog.apply_settings()
|
|
|
|
# Verify the result
|
|
self.assertFalse(result)
|
|
|
|
# Verify mocks were called
|
|
mock_config_manager.update_speech_recognition_settings.assert_called_once()
|
|
mock_config_manager.save_settings.assert_called_once()
|
|
mock_speech_engine.reconfigure.assert_called_once()
|
|
|
|
# Verify error dialog was handled properly
|
|
mock_gtk.MessageDialog.assert_called_once()
|
|
mock_dialog.run.assert_called_once()
|
|
mock_dialog.destroy.assert_called_once()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|