* refactor: improve code quality across multiple dimensions Code quality improvements identified and driven by desloppify analysis: **action_handler.py** - Replace 9 repetitive shortcut forwarding methods with data-driven _SHORTCUT_ACTIONS class-level dispatch table - Add _make_shortcut_handler() factory for shortcut closures - Add explicit type annotation for action_handlers dict **config_manager.py** - Move 'import copy' from __init__ to module level (stdlib best practice) - Add _VALID_SECTIONS frozenset for section name validation in set() - Narrow broad Exception catch to (KeyError, TypeError) in set() - Add deprecation warning to save_settings() guiding toward save_config() **tray_indicator.py** - Consolidate all imports at module top level - Remove noqa E402 suppressions for mid-file imports - Move ResourceManager import alongside other local imports **recognition_manager.py** - Narrow broad 'except Exception' to 'except OSError' for audio device enumeration errors - Narrow 'except Exception' to 'except (FileNotFoundError, OSError)' for notification subprocess errors **Tests** - Update test assertions to use specific exception types matching narrowed catches (OSError, FileNotFoundError instead of Exception/RuntimeError) * refactor: improve type safety, callback contracts, and method decomposition **main.py** — Mid-level elegance - Document callback wiring contract: text_callback, action_callback, state_callback with threading context, argument types, and behavior - Add return type annotations to nested callback functions - Reorder: define callbacks before registration for readability **config_manager.py** — Type safety, error consistency - Add typed accessors: get_str(), get_bool(), get_int(), get_float(), get_optional_int() eliminating Any returns for common access patterns - Narrow load_config exception to (json.JSONDecodeError, OSError) - Narrow save_config exception to (OSError, TypeError) **recognition_manager.py** — Low-level elegance - Decompose _init_whispercpp (141 lines) into three focused methods: - _init_whispercpp: validation and download gating (35 lines) - _load_whispercpp_model: backend detection and model loading (55 lines) - _handle_gpu_fallback: GPU incompatibility recovery (40 lines) - Narrow exception catch from broad Exception to specific types **tray_indicator.py, settings_dialog.py** — Type safety - Migrate config access from untyped get() to typed get_str(), get_bool(), get_optional_int() across 8 call sites * refactor: narrow 11 more broad Exception catches to specific types Systematically replaced broad 'except Exception' with specific exception types across recognition_manager.py: - test_audio_input: (OSError, ValueError) for audio read errors - test_audio_input outer: (OSError, ValueError, RuntimeError) for unexpected - _init_whisper: (RuntimeError, OSError) for engine init failures - _transcribe_with_whisper: (RuntimeError, OSError, ValueError) for transcription - _download_whispercpp_model: (OSError, RuntimeError, ValueError) for downloads - _download_vosk_model: (OSError, RuntimeError, ValueError) for downloads - _download_whisper_model: (OSError, RuntimeError, ValueError) for downloads This brings the total narrowed exception catches to 18+ across the codebase, improving error consistency and making failure modes more predictable. * style: fix black formatting issues Auto-format with black to fix CI lint failures in recognition_manager.py, settings_dialog.py, and test_settings_shortcuts.py. * test: add coverage for typed config accessors Add TestTypedAccessors class with 15 tests covering: - get_str(): string values, defaults, non-string conversion, None handling - get_bool(): boolean values, defaults - get_int(): integer values, invalid input fallback, missing key defaults - get_float(): float values, invalid input fallback - get_optional_int(): int values, None values, missing keys, invalid input Fixes codecov/patch coverage requirement for new typed accessor methods.
208 lines
8.4 KiB
Python
208 lines
8.4 KiB
Python
"""
|
|
Tests for keyboard shortcuts section in settings dialog.
|
|
|
|
Uses source code inspection to avoid GTK mocking issues.
|
|
"""
|
|
|
|
import os
|
|
import unittest
|
|
|
|
|
|
def _get_source_code():
|
|
"""Read the settings dialog source file."""
|
|
source_path = os.path.join(
|
|
os.path.dirname(__file__),
|
|
"..",
|
|
"src",
|
|
"vocalinux",
|
|
"ui",
|
|
"settings_dialog.py",
|
|
)
|
|
with open(source_path, "r") as f:
|
|
return f.read()
|
|
|
|
|
|
class TestSettingsDialogShortcutsSection(unittest.TestCase):
|
|
"""Test cases for keyboard shortcuts section in settings dialog."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
self.source_code = _get_source_code()
|
|
|
|
def test_shortcuts_section_exists(self):
|
|
"""Test that shortcuts section build method exists."""
|
|
self.assertIn("def _build_shortcuts_section(self)", self.source_code)
|
|
|
|
def test_shortcuts_section_called_in_init(self):
|
|
"""Test that shortcuts section is built in __init__."""
|
|
self.assertIn("self._build_shortcuts_section()", self.source_code)
|
|
|
|
def test_shortcut_combo_widget_created(self):
|
|
"""Test that shortcut combo box widget is created."""
|
|
self.assertIn("self.shortcut_combo = Gtk.ComboBoxText()", self.source_code)
|
|
|
|
def test_shortcut_combo_size_request(self):
|
|
"""Test that shortcut combo has size request set."""
|
|
self.assertIn("self.shortcut_combo.set_size_request(200, -1)", self.source_code)
|
|
|
|
def test_shortcut_combo_tooltip(self):
|
|
"""Test that shortcut combo has a tooltip."""
|
|
self.assertIn("self.shortcut_combo.set_tooltip_text(", self.source_code)
|
|
|
|
def test_shortcut_options_populated(self):
|
|
"""Test that shortcut options are populated from SHORTCUT_DISPLAY_NAMES."""
|
|
self.assertIn("for group_label, shortcut_ids in SHORTCUT_GROUPS.items()", self.source_code)
|
|
self.assertIn(
|
|
'self.shortcut_combo.append(separator_id, f"── {group_label} ──")', self.source_code
|
|
)
|
|
self.assertIn("for shortcut_id in shortcut_ids", self.source_code)
|
|
|
|
def test_shortcut_config_read(self):
|
|
"""Test that shortcut is read from config."""
|
|
self.assertIn(
|
|
'self.config_manager.get_str("shortcuts", "toggle_recognition"', self.source_code
|
|
)
|
|
|
|
def test_shortcut_changed_handler_exists(self):
|
|
"""Test that shortcut changed handler exists."""
|
|
self.assertIn("def _on_shortcut_changed(self, widget)", self.source_code)
|
|
|
|
def test_shortcut_change_saved_to_config(self):
|
|
"""Test that shortcut change is saved to config."""
|
|
self.assertIn(
|
|
'self.config_manager.set("shortcuts", "toggle_recognition", shortcut_id)',
|
|
self.source_code,
|
|
)
|
|
|
|
def test_shortcut_change_triggers_save(self):
|
|
"""Test that shortcut change triggers config save."""
|
|
# After setting the shortcut, save_settings should be called
|
|
self.assertIn("self.config_manager.save_settings()", self.source_code)
|
|
|
|
def test_shortcut_preference_row_title(self):
|
|
"""Test that preference row has correct title."""
|
|
self.assertIn('title="Shortcut Key"', self.source_code)
|
|
|
|
def test_shortcut_preference_row_subtitle(self):
|
|
"""Test that preference row has descriptive subtitle."""
|
|
# The subtitle is now dynamic based on mode (toggle or push-to-talk)
|
|
self.assertIn("set_subtitle", self.source_code)
|
|
"""Test that preference row has correct title."""
|
|
self.assertIn('title="Shortcut Key"', self.source_code)
|
|
"""Test that preference row has correct title."""
|
|
self.assertIn('title="Toggle Recognition"', self.source_code)
|
|
|
|
def test_shortcut_preference_row_subtitle(self):
|
|
"""Test that preference row has descriptive subtitle."""
|
|
# The subtitle is now dynamic based on mode (toggle or push-to-talk)
|
|
self.assertIn("set_subtitle", self.source_code)
|
|
|
|
def test_shortcut_info_label_exists(self):
|
|
"""Test that info label exists for user guidance."""
|
|
self.assertIn("self.shortcut_info_label", self.source_code)
|
|
|
|
def test_shortcut_info_box_styling(self):
|
|
"""Test that info box has proper styling."""
|
|
self.assertIn('get_style_context().add_class("info-box")', self.source_code)
|
|
|
|
def test_shortcut_group_title(self):
|
|
"""Test that shortcuts group has proper title."""
|
|
self.assertIn('title="Keyboard Shortcuts"', self.source_code)
|
|
|
|
def test_imports_shortcut_constants(self):
|
|
"""Test that shortcut constants are imported."""
|
|
# Check for imports (may be multi-line)
|
|
self.assertIn("from .keyboard_backends import", self.source_code)
|
|
self.assertIn("SHORTCUT_DISPLAY_NAMES", self.source_code)
|
|
self.assertIn("SHORTCUT_GROUPS", self.source_code)
|
|
self.assertIn("SUPPORTED_SHORTCUTS", self.source_code)
|
|
|
|
|
|
class TestKeyboardBackendsBase(unittest.TestCase):
|
|
"""Test cases for keyboard backends base module."""
|
|
|
|
def test_supported_shortcuts_defined(self):
|
|
"""Test that SUPPORTED_SHORTCUTS is defined."""
|
|
from vocalinux.ui.keyboard_backends.base import SUPPORTED_SHORTCUTS
|
|
|
|
self.assertIsInstance(SUPPORTED_SHORTCUTS, dict)
|
|
self.assertIn("ctrl+ctrl", SUPPORTED_SHORTCUTS)
|
|
self.assertIn("alt+alt", SUPPORTED_SHORTCUTS)
|
|
self.assertIn("shift+shift", SUPPORTED_SHORTCUTS)
|
|
|
|
def test_shortcut_display_names_defined(self):
|
|
"""Test that SHORTCUT_DISPLAY_NAMES is defined."""
|
|
from vocalinux.ui.keyboard_backends.base import SHORTCUT_DISPLAY_NAMES
|
|
|
|
self.assertIsInstance(SHORTCUT_DISPLAY_NAMES, dict)
|
|
self.assertEqual(
|
|
set(SHORTCUT_DISPLAY_NAMES.keys()),
|
|
{
|
|
"ctrl+ctrl",
|
|
"alt+alt",
|
|
"shift+shift",
|
|
"left_ctrl+left_ctrl",
|
|
"left_alt+left_alt",
|
|
"left_shift+left_shift",
|
|
"right_ctrl+right_ctrl",
|
|
"right_alt+right_alt",
|
|
"right_shift+right_shift",
|
|
},
|
|
)
|
|
|
|
def test_default_shortcut_defined(self):
|
|
"""Test that DEFAULT_SHORTCUT is defined."""
|
|
from vocalinux.ui.keyboard_backends.base import DEFAULT_SHORTCUT
|
|
|
|
self.assertEqual(DEFAULT_SHORTCUT, "ctrl+ctrl")
|
|
|
|
def test_keyboard_backend_shortcut_property(self):
|
|
"""Test that KeyboardBackend has shortcut property."""
|
|
from vocalinux.ui.keyboard_backends.base import KeyboardBackend
|
|
|
|
# Can't instantiate abstract class, but can check the property exists
|
|
self.assertTrue(hasattr(KeyboardBackend, "shortcut"))
|
|
self.assertTrue(hasattr(KeyboardBackend, "modifier_key"))
|
|
self.assertTrue(hasattr(KeyboardBackend, "set_shortcut"))
|
|
|
|
def test_shortcut_groups_defined(self):
|
|
"""Test that SHORTCUT_GROUPS is defined with 3 groups and correct keys."""
|
|
from vocalinux.ui.keyboard_backends.base import SHORTCUT_GROUPS
|
|
|
|
self.assertIsInstance(SHORTCUT_GROUPS, dict)
|
|
self.assertIn("Either Side", SHORTCUT_GROUPS)
|
|
self.assertIn("Left Side", SHORTCUT_GROUPS)
|
|
self.assertIn("Right Side", SHORTCUT_GROUPS)
|
|
self.assertEqual(len(SHORTCUT_GROUPS.keys()), 3)
|
|
self.assertEqual(list(SHORTCUT_GROUPS.keys()), ["Either Side", "Left Side", "Right Side"])
|
|
self.assertEqual(len(SHORTCUT_GROUPS["Either Side"]), 3)
|
|
self.assertEqual(len(SHORTCUT_GROUPS["Left Side"]), 3)
|
|
self.assertEqual(len(SHORTCUT_GROUPS["Right Side"]), 3)
|
|
|
|
|
|
class TestConfigManagerShortcuts(unittest.TestCase):
|
|
"""Test cases for shortcut configuration in config manager."""
|
|
|
|
def test_shortcuts_section_in_default_config(self):
|
|
"""Test that shortcuts section exists in DEFAULT_CONFIG."""
|
|
from vocalinux.ui.config_manager import DEFAULT_CONFIG
|
|
|
|
self.assertIn("shortcuts", DEFAULT_CONFIG)
|
|
self.assertIn("toggle_recognition", DEFAULT_CONFIG["shortcuts"])
|
|
self.assertEqual(DEFAULT_CONFIG["shortcuts"]["toggle_recognition"], "ctrl+ctrl")
|
|
|
|
def test_config_manager_get_shortcut(self):
|
|
"""Test getting shortcut from config manager."""
|
|
from unittest.mock import patch
|
|
|
|
with patch("vocalinux.ui.config_manager.os.path.exists", return_value=False):
|
|
from vocalinux.ui.config_manager import ConfigManager
|
|
|
|
config = ConfigManager()
|
|
shortcut = config.get("shortcuts", "toggle_recognition", "ctrl+ctrl")
|
|
self.assertEqual(shortcut, "ctrl+ctrl")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|