Files
voculinux/tests/test_main.py
T
Jatin K MalikandGitHub b5e1d61d17 Fix: Audio detection issues - Add device selection and testing (#55)
* Fix audio detection issues (#34)

This commit addresses the audio detection issues reported in GitHub issue #34
where users on various Linux distributions (Fedora, Ubuntu with Wayland)
were unable to get voice recognition working despite their microphones
functioning in other applications.

Changes:
- Add audio input device enumeration and selection
  - New get_audio_input_devices() function lists all available input devices
  - Users can now select which microphone to use in Settings

- Add audio diagnostics and testing
  - New test_audio_input() function for testing microphone input
  - Test Mic button in Settings to verify audio is being captured
  - Audio level bar shows real-time input levels

- Improve audio debugging/logging
  - Log available audio devices on startup
  - Log which device is being used
  - Periodic audio level logging during recording
  - Warning when no speech detected and max level is very low
  - Log when speech is first detected with threshold info

- Add audio device configuration
  - Audio device selection persisted to config file
  - Device loaded on startup from saved config

- Better error handling
  - Graceful handling of audio stream open failures
  - Clear error messages when audio devices are unavailable

The root cause for many users was that PyAudio's default device selection
may not match the system's default input, especially on systems using
PipeWire or complex audio routing. Users can now explicitly select their
microphone and test it before using voice recognition.

* Fix circular import in speech_recognition module

Move lazy imports for get_audio_input_devices and test_audio_input
to avoid circular dependency when settings_dialog imports from
speech_recognition at module load time.

* Fix audio detection issues with device selection

- Add audio device enumeration and selection in Settings
- Add microphone test button with audio level indicator
- Improve VAD logging with periodic audio level output
- Add warning when no speech detected with low audio levels
- Store audio device preference in config

Fixes #34

* Fix linting issues - remove unused imports, fix module-level imports, format code

* Fix remaining linting issues - add type hints back, fix whitespace, add noqa comments

* Fix final linting issue - break long line in config_manager

* Fix formatting - use black formatting, align flake8 line-length with black (100)

* Fix CI test failures: Update mocks and assertions

* Fix audio feedback mock tests for CI

- Move audio feedback imports to top of recognition_manager.py (remove lazy imports)
- Add shared mock_audio_feedback in conftest.py for consistent mocking across tests
- Update test_recognition_manager.py and test_speech_recognition.py to use shared mock
- Fixes test_start_stop_recognition, test_start_recognition, test_stop_recognition
2026-01-19 23:49:39 -08:00

160 lines
5.6 KiB
Python

"""
Tests for the main module functionality.
"""
import argparse
import unittest
from unittest.mock import MagicMock, patch
# Update import to use the new package structure
from vocalinux.main import main, parse_arguments
class TestMainModule(unittest.TestCase):
"""Test cases for the main module."""
def test_parse_arguments_defaults(self):
"""Test argument parsing with defaults."""
# Test with no arguments
with patch("sys.argv", ["vocalinux"]):
args = parse_arguments()
self.assertFalse(args.debug)
self.assertEqual(args.model, "small")
self.assertEqual(args.engine, "vosk")
self.assertFalse(args.wayland)
def test_parse_arguments_custom(self):
"""Test argument parsing with custom values."""
# Test with custom arguments
with patch(
"sys.argv",
[
"vocalinux",
"--debug",
"--model",
"large",
"--engine",
"whisper",
"--wayland",
],
):
args = parse_arguments()
self.assertTrue(args.debug)
self.assertEqual(args.model, "large")
self.assertEqual(args.engine, "whisper")
self.assertTrue(args.wayland)
@patch("vocalinux.main.check_dependencies")
@patch("vocalinux.main.ActionHandler")
@patch("vocalinux.speech_recognition.recognition_manager.SpeechRecognitionManager")
@patch("vocalinux.text_injection.text_injector.TextInjector")
@patch("vocalinux.ui.tray_indicator.TrayIndicator")
@patch("vocalinux.main.logging")
@patch("vocalinux.ui.config_manager.ConfigManager")
def test_main_initializes_components(
self,
mock_config_manager,
mock_logging,
mock_tray,
mock_text,
mock_speech,
mock_action_handler,
mock_check_deps,
):
"""Test that main initializes all the required components."""
# Mock dependency check to return True
mock_check_deps.return_value = True
# Mock ConfigManager to return empty settings (use command-line defaults)
mock_config_instance = MagicMock()
mock_config_instance.get_settings.return_value = {"speech_recognition": {}}
mock_config_manager.return_value = mock_config_instance
# Mock objects
mock_speech_instance = MagicMock()
mock_text_instance = MagicMock()
mock_tray_instance = MagicMock()
mock_action_instance = MagicMock()
# Setup return values
mock_speech.return_value = mock_speech_instance
mock_text.return_value = mock_text_instance
mock_tray.return_value = mock_tray_instance
mock_action_handler.return_value = mock_action_instance
# Mock the arguments
with patch("vocalinux.main.parse_arguments") as mock_parse:
mock_args = MagicMock()
mock_args.debug = False
mock_args.model = "medium"
mock_args.engine = "vosk"
mock_args.wayland = True
mock_parse.return_value = mock_args
# Call main function
main()
# Verify components were initialized correctly
mock_speech.assert_called_once_with(
engine="vosk",
model_size="medium",
vad_sensitivity=3,
silence_timeout=2.0,
audio_device_index=None,
)
mock_text.assert_called_once_with(wayland_mode=True)
mock_action_handler.assert_called_once_with(mock_text_instance)
mock_tray.assert_called_once_with(
speech_engine=mock_speech_instance, text_injector=mock_text_instance
)
# Verify callbacks were registered
mock_speech_instance.register_text_callback.assert_called_once()
mock_speech_instance.register_action_callback.assert_called_once_with(
mock_action_instance.handle_action
)
# Verify the tray indicator was started
mock_tray_instance.run.assert_called_once()
def test_main_with_debug_enabled(self):
"""Test that debug mode enables debug logging."""
import logging # Import for DEBUG constant
# Test with args.debug = True
with patch("vocalinux.main.parse_arguments") as mock_parse, patch(
"vocalinux.main.logging"
) as mock_logging, patch("vocalinux.main.logging.DEBUG", logging.DEBUG), patch(
"vocalinux.speech_recognition.recognition_manager.SpeechRecognitionManager"
), patch(
"vocalinux.text_injection.text_injector.TextInjector"
), patch(
"vocalinux.ui.tray_indicator.TrayIndicator"
), patch(
"vocalinux.main.ActionHandler"
), patch(
"vocalinux.main.check_dependencies"
) as mock_check_deps:
# Mock dependency check to return True
mock_check_deps.return_value = True
# Create mock args
mock_args = MagicMock()
mock_args.debug = True
mock_args.model = "small"
mock_args.engine = "vosk"
mock_args.wayland = False
mock_parse.return_value = mock_args
# Create mock loggers
root_logger = MagicMock()
named_logger = MagicMock()
mock_logging.getLogger.side_effect = [root_logger, named_logger]
# Call main
main()
# Verify root logger had setLevel called with DEBUG
root_logger.setLevel.assert_called_once_with(logging.DEBUG)