import pytest from unittest.mock import Mock, patch, mock_open, MagicMock import json import subprocess from pathlib import Path from src.commands.audio import ( detect_wsl, get_windows_audio_devices, get_linux_audio_devices, _categorize_device_type, display_windows_devices, display_linux_devices, WindowsAudioDevice, LinuxAudioDevice ) from src.config import AppConfig, AudioConfig, ServerConfig # WSL Detection Tests @patch('pathlib.Path.exists') @patch('builtins.open', mock_open(read_data='Linux version 5.4.0-microsoft-standard')) def test_detect_wsl_via_proc_version_microsoft(mock_exists): """Test WSL detection via /proc/version containing 'microsoft'.""" mock_exists.return_value = True result = detect_wsl() assert result is True @patch('pathlib.Path.exists') @patch('builtins.open', mock_open(read_data='Linux version 5.4.0-wsl2-standard')) def test_detect_wsl_via_proc_version_wsl(mock_exists): """Test WSL detection via /proc/version containing 'wsl'.""" mock_exists.return_value = True result = detect_wsl() assert result is True @patch('pathlib.Path.exists') @patch('builtins.open', mock_open(read_data='Linux version 5.4.0-generic')) @patch('os.getenv') def test_detect_wsl_via_env_distro_name(mock_getenv, mock_exists): """Test WSL detection via WSL_DISTRO_NAME environment variable.""" mock_exists.return_value = True mock_getenv.side_effect = lambda key: 'Ubuntu' if key == 'WSL_DISTRO_NAME' else None result = detect_wsl() assert result is True @patch('pathlib.Path.exists') @patch('builtins.open', mock_open(read_data='Linux version 5.4.0-generic')) @patch('os.getenv') def test_detect_wsl_via_env_wslenv(mock_getenv, mock_exists): """Test WSL detection via WSLENV environment variable.""" mock_exists.return_value = True mock_getenv.side_effect = lambda key: 'PATH/l' if key == 'WSLENV' else None result = detect_wsl() assert result is True @patch('pathlib.Path.exists') @patch('builtins.open', mock_open(read_data='Linux version 5.4.0-generic')) @patch('os.getenv', return_value=None) def test_detect_wsl_false_native_linux(mock_getenv, mock_exists): """Test WSL detection returns False on native Linux.""" mock_exists.return_value = True result = detect_wsl() assert result is False @patch('pathlib.Path.exists', return_value=False) @patch('os.getenv', return_value=None) def test_detect_wsl_false_no_proc_version(mock_getenv, mock_exists): """Test WSL detection returns False when /proc/version doesn't exist.""" result = detect_wsl() assert result is False @patch('pathlib.Path.exists') @patch('builtins.open', side_effect=Exception("Permission denied")) @patch('os.getenv', return_value=None) def test_detect_wsl_handles_proc_version_read_error(mock_getenv, mock_exists): """Test WSL detection handles /proc/version read errors gracefully.""" mock_exists.return_value = True result = detect_wsl() assert result is False # Device Type Categorization Tests def test_categorize_input_device(): """Test categorization of input device.""" instance_id = "SWD\\MMDEVAPI\\{0.0.1.00000000}.{a1b2c3d4-e5f6-7890-abcd-ef1234567890}\\capture" result = _categorize_device_type(instance_id) assert result == "Input" def test_categorize_output_device(): """Test categorization of output device.""" instance_id = "SWD\\MMDEVAPI\\{0.0.0.00000000}.{a1b2c3d4-e5f6-7890-abcd-ef1234567890}\\render" result = _categorize_device_type(instance_id) assert result == "Output" def test_categorize_unknown_device(): """Test categorization of unknown device type.""" instance_id = "SWD\\MMDEVAPI\\{0.0.2.00000000}.{a1b2c3d4-e5f6-7890-abcd-ef1234567890}\\unknown" result = _categorize_device_type(instance_id) assert result == "Unknown" def test_categorize_empty_instance_id(): """Test categorization with empty instance ID.""" result = _categorize_device_type("") assert result == "Unknown" # Windows Audio Devices Tests @patch('subprocess.run') @patch('typer.echo') def test_get_windows_devices_success_single_device(mock_echo, mock_run): """Test successful retrieval of single Windows device.""" mock_result = Mock() mock_result.returncode = 0 mock_result.stdout = json.dumps({ "FriendlyName": "Blue Yeti Microphone", "Status": "OK", "InstanceId": "USB\\VID_0B05&PID_1234\\capture" }) mock_result.stderr = "" mock_run.return_value = mock_result result = get_windows_audio_devices() assert len(result) == 1 assert result[0].name == "Blue Yeti Microphone" assert result[0].status == "OK" assert result[0].device_type == "Input" assert "USB\\VID_0B05&PID_1234\\capture" in result[0].instance_id @patch('subprocess.run') @patch('typer.echo') def test_get_windows_devices_success_multiple_devices(mock_echo, mock_run): """Test successful retrieval of multiple Windows devices.""" mock_result = Mock() mock_result.returncode = 0 mock_result.stdout = json.dumps([ { "FriendlyName": "Microphone", "Status": "OK", "InstanceId": "capture_device" }, { "FriendlyName": "Speakers", "Status": "OK", "InstanceId": "render_device" } ]) mock_result.stderr = "" mock_run.return_value = mock_result result = get_windows_audio_devices() assert len(result) == 2 assert result[0].device_type == "Input" assert result[1].device_type == "Output" @patch('subprocess.run') @patch('typer.echo') def test_get_windows_devices_powershell_error(mock_echo, mock_run): """Test PowerShell command failure.""" mock_result = Mock() mock_result.returncode = 1 mock_result.stdout = "" mock_result.stderr = "Get-PnpDevice : Access denied" mock_run.return_value = mock_result result = get_windows_audio_devices() assert result == [] mock_echo.assert_called() @patch('subprocess.run') @patch('typer.echo') def test_get_windows_devices_empty_output(mock_echo, mock_run): """Test PowerShell returning empty output.""" mock_result = Mock() mock_result.returncode = 0 mock_result.stdout = "" mock_result.stderr = "" mock_run.return_value = mock_result result = get_windows_audio_devices() assert result == [] mock_echo.assert_called() @patch('subprocess.run') @patch('typer.echo') def test_get_windows_devices_invalid_json(mock_echo, mock_run): """Test invalid JSON response from PowerShell.""" mock_result = Mock() mock_result.returncode = 0 mock_result.stdout = "Invalid JSON {" mock_result.stderr = "" mock_run.return_value = mock_result result = get_windows_audio_devices() assert result == [] mock_echo.assert_called() @patch('subprocess.run', side_effect=subprocess.TimeoutExpired("powershell.exe", 15)) @patch('typer.echo') def test_get_windows_devices_timeout(mock_echo, mock_run): """Test PowerShell command timeout.""" result = get_windows_audio_devices() assert result == [] mock_echo.assert_called() @patch('subprocess.run') @patch('typer.echo') def test_get_windows_devices_filters_empty_names(mock_echo, mock_run): """Test filtering of devices with empty names.""" mock_result = Mock() mock_result.returncode = 0 mock_result.stdout = json.dumps([ { "FriendlyName": "Valid Device", "Status": "OK", "InstanceId": "capture_device" }, { "FriendlyName": "", "Status": "OK", "InstanceId": "empty_name_device" }, { "FriendlyName": None, "Status": "OK", "InstanceId": "null_name_device" } ]) mock_result.stderr = "" mock_run.return_value = mock_result result = get_windows_audio_devices() assert len(result) == 1 assert result[0].name == "Valid Device" # Linux Audio Devices Tests @patch('sounddevice.query_devices') @patch('sounddevice.query_hostapis') @patch('typer.echo') def test_get_linux_devices_success(mock_echo, mock_hostapis, mock_devices): """Test successful retrieval of Linux devices.""" mock_devices.return_value = [ { 'name': 'pulse', 'max_input_channels': 32, 'max_output_channels': 32, 'default_samplerate': 44100.0, 'hostapi': 0 }, { 'name': 'default', 'max_input_channels': 32, 'max_output_channels': 32, 'default_samplerate': 44100.0, 'hostapi': 0 } ] mock_hostapis.return_value = {'name': 'ALSA'} result = get_linux_audio_devices() assert len(result) == 2 assert result[0].name == 'pulse' assert result[0].device_id == 0 assert result[0].hostapi_name == 'ALSA' assert result[1].name == 'default' assert result[1].device_id == 1 @patch('sounddevice.query_devices', side_effect=Exception("ALSA error")) @patch('typer.echo') def test_get_linux_devices_sounddevice_error(mock_echo, mock_devices): """Test sounddevice query failure.""" with pytest.raises(SystemExit): get_linux_audio_devices() # Display Functions Tests @patch('typer.echo') def test_display_windows_devices_empty_list(mock_echo): """Test display with empty device list.""" display_windows_devices([], True, True) mock_echo.assert_called() @patch('typer.echo') @patch('src.commands.audio._display_single_windows_device') def test_display_windows_devices_input_only(mock_display_single, mock_echo): """Test display showing only input devices.""" devices = [ WindowsAudioDevice('Microphone', 'OK', 'Input', 'mic1'), WindowsAudioDevice('Speakers', 'OK', 'Output', 'spk1') ] display_windows_devices(devices, show_inputs=True, show_outputs=False) mock_display_single.assert_called_once_with(devices[0]) @patch('typer.echo') @patch('src.commands.audio._display_single_windows_device') def test_display_windows_devices_with_unknown(mock_display_single, mock_echo): """Test display including unknown device types.""" devices = [ WindowsAudioDevice('Microphone', 'OK', 'Input', 'mic1'), WindowsAudioDevice('Unknown Device', 'OK', 'Unknown', 'unk1') ] display_windows_devices(devices, show_inputs=True, show_outputs=True) assert mock_display_single.call_count == 2 @patch('typer.echo') def test_display_linux_devices_no_matching_devices(mock_echo): """Test display with no matching devices.""" devices = [ LinuxAudioDevice(0, 'pulse', 0, 32, 44100.0, 'ALSA') ] config = AppConfig(ServerConfig(), AudioConfig()) with pytest.raises(SystemExit): display_linux_devices(devices, show_inputs=True, show_outputs=False, config=config) @patch('typer.echo') @patch('sounddevice.default') def test_display_linux_devices_success(mock_default, mock_echo): """Test successful display of Linux devices.""" mock_default.device = [0, 1] devices = [ LinuxAudioDevice(0, 'pulse', 32, 32, 44100.0, 'ALSA') ] config = AppConfig(ServerConfig(), AudioConfig()) display_linux_devices(devices, show_inputs=True, show_outputs=True, config=config) # Verify that echo was called with device information assert mock_echo.call_count > 0 @patch('typer.echo') @patch('sounddevice.default') def test_display_linux_devices_with_configured_device(mock_default, mock_echo): """Test display with configured device marked.""" mock_default.device = [0, 1] devices = [ LinuxAudioDevice(0, 'pulse', 32, 32, 44100.0, 'ALSA'), LinuxAudioDevice(1, 'default', 32, 32, 44100.0, 'ALSA') ] config = AppConfig(ServerConfig(), AudioConfig(device=1)) display_linux_devices(devices, show_inputs=True, show_outputs=True, config=config) # Verify that echo was called and device is marked as configured assert mock_echo.call_count > 0