Files
MyTTSClient/tests/test_audio_devices.py

407 lines
12 KiB
Python

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