Added helper functions alongside their unit tests
This commit is contained in:
@@ -1,407 +0,0 @@
|
||||
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
|
||||
95
tests/test_expando.py
Normal file
95
tests/test_expando.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import pytest
|
||||
|
||||
from core.Expando import Expando
|
||||
|
||||
|
||||
def test_i_can_get_properties():
|
||||
props = {"a": 10,
|
||||
"b": {
|
||||
"c": "value",
|
||||
"d": 20
|
||||
}}
|
||||
dynamic = Expando(props)
|
||||
|
||||
assert dynamic.a == 10
|
||||
assert dynamic.b.c == "value"
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
assert dynamic.unknown == "some_value"
|
||||
|
||||
|
||||
def test_i_can_get():
|
||||
props = {"a": 10,
|
||||
"b": {
|
||||
"c": "value",
|
||||
"d": 20
|
||||
}}
|
||||
dynamic = Expando(props)
|
||||
|
||||
assert dynamic.get("a") == 10
|
||||
assert dynamic.get("b.c") == "value"
|
||||
assert dynamic.get("unknown") is None
|
||||
|
||||
|
||||
def test_i_can_get_insensitive():
|
||||
props = {"A": 10,
|
||||
"b": 20}
|
||||
dynamic = Expando(props)
|
||||
|
||||
assert dynamic.geti("a") == 10
|
||||
assert dynamic.geti("A") == 10
|
||||
assert dynamic.geti("a.c", None) is None
|
||||
with pytest.raises(AttributeError):
|
||||
assert dynamic.geti("unknown") == "some_value"
|
||||
|
||||
|
||||
def test_i_can_get_insensitive_when_the_two_entries_exist():
|
||||
props = {"A": 10,
|
||||
"a": 20}
|
||||
dynamic = Expando(props)
|
||||
assert dynamic.geti("A") == 10
|
||||
assert dynamic.geti("a") == 20
|
||||
|
||||
|
||||
def test_i_can_get_from_list():
|
||||
props = {"a": [{"c": "value1", "d": 1}, {"c": "value2", "d": 2}]}
|
||||
dynamic = Expando(props)
|
||||
|
||||
assert dynamic.get("a.c") == ["value1", "value2"]
|
||||
|
||||
|
||||
def test_none_is_returned_when_get_from_list_and_property_does_not_exist():
|
||||
props = {"a": [{"c": "value1", "d": 1},
|
||||
{"a": "value2", "d": 2} # 'c' does not exist in the second row
|
||||
]}
|
||||
dynamic = Expando(props)
|
||||
|
||||
assert dynamic.get("a.c") == ["value1"]
|
||||
|
||||
|
||||
def test_i_can_manage_none_values():
|
||||
props = {"a": 10,
|
||||
"b": None}
|
||||
dynamic = Expando(props)
|
||||
|
||||
assert dynamic.get("b.c") is None
|
||||
|
||||
|
||||
def test_i_can_manage_none_values_in_list():
|
||||
props = {"a": [{"b": {"c": "value"}},
|
||||
{"b": None}
|
||||
]}
|
||||
dynamic = Expando(props)
|
||||
|
||||
assert dynamic.get("a.b.c") == ["value"]
|
||||
|
||||
|
||||
def test_i_can_add_new_properties():
|
||||
props = {"a": 10,
|
||||
"b": 20}
|
||||
dynamic = Expando(props)
|
||||
dynamic["c"] = 30
|
||||
|
||||
assert dynamic.a == 10
|
||||
assert dynamic.b == 20
|
||||
assert dynamic.c == 30
|
||||
165
tests/test_filter_by.py
Normal file
165
tests/test_filter_by.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import pytest
|
||||
|
||||
from src.core.utils import FilterConf, filter_by
|
||||
from src.core.Expando import Expando
|
||||
|
||||
|
||||
def test_i_can_filter_by_matching_string():
|
||||
"""Test basic filtering with string matching."""
|
||||
items = [
|
||||
Expando({"name": "alice"}),
|
||||
Expando({"name": "bob"}),
|
||||
Expando({"name": "charlie"})
|
||||
]
|
||||
filter_conf = FilterConf(property="name", value="alice")
|
||||
|
||||
result = filter_by(items, filter_conf)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "alice"
|
||||
|
||||
|
||||
def test_i_can_filter_by_case_insensitive():
|
||||
"""Test case-insensitive filtering."""
|
||||
items = [
|
||||
Expando({"name": "ALICE"}),
|
||||
Expando({"name": "Bob"}),
|
||||
Expando({"name": "charlie"})
|
||||
]
|
||||
filter_conf = FilterConf(property="name", value="alice")
|
||||
|
||||
result = filter_by(items, filter_conf)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "ALICE"
|
||||
|
||||
|
||||
def test_i_can_filter_by_partial_match():
|
||||
"""Test partial substring matching."""
|
||||
items = [
|
||||
Expando({"name": "alice smith"}),
|
||||
Expando({"name": "bob jones"}),
|
||||
Expando({"name": "charlie brown"})
|
||||
]
|
||||
filter_conf = FilterConf(property="name", value="smith")
|
||||
|
||||
result = filter_by(items, filter_conf)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "alice smith"
|
||||
|
||||
|
||||
def test_i_can_handle_none_filter_conf():
|
||||
"""Test that None filter configuration returns items unchanged."""
|
||||
items = [
|
||||
Expando({"name": "alice"}),
|
||||
Expando({"name": "bob"})
|
||||
]
|
||||
|
||||
result = filter_by(items, None)
|
||||
|
||||
assert result == items
|
||||
assert len(result) == 2
|
||||
|
||||
|
||||
def test_i_can_handle_empty_list():
|
||||
"""Test that empty list returns empty list."""
|
||||
items = []
|
||||
filter_conf = FilterConf(property="name", value="alice")
|
||||
|
||||
result = filter_by(items, filter_conf)
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_i_can_handle_none_property_in_filter_conf():
|
||||
"""Test that FilterConf with None property returns items unchanged."""
|
||||
items = [
|
||||
Expando({"name": "alice"}),
|
||||
Expando({"name": "bob"})
|
||||
]
|
||||
# Create FilterConf with None property (simulating invalid state)
|
||||
filter_conf = FilterConf(property=None, value="alice")
|
||||
|
||||
result = filter_by(items, filter_conf)
|
||||
|
||||
assert result == items
|
||||
assert len(result) == 2
|
||||
|
||||
|
||||
def test_i_can_filter_with_no_matches():
|
||||
"""Test filtering when no items match the criteria."""
|
||||
items = [
|
||||
Expando({"name": "alice"}),
|
||||
Expando({"name": "bob"}),
|
||||
Expando({"name": "charlie"})
|
||||
]
|
||||
filter_conf = FilterConf(property="name", value="xyz")
|
||||
|
||||
result = filter_by(items, filter_conf)
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_i_cannot_filter_with_missing_properties():
|
||||
"""Test that missing properties raise AttributeError."""
|
||||
items = [
|
||||
Expando({"name": "alice"}),
|
||||
Expando({"age": 25}), # no name property
|
||||
Expando({"name": "bob"})
|
||||
]
|
||||
filter_conf = FilterConf(property="name", value="alice")
|
||||
|
||||
# Should raise AttributeError when trying to access missing property
|
||||
with pytest.raises(AttributeError):
|
||||
filter_by(items, filter_conf)
|
||||
|
||||
|
||||
def test_i_can_filter_with_none_values():
|
||||
"""Test filtering when properties have None values."""
|
||||
items = [
|
||||
Expando({"name": "alice"}),
|
||||
Expando({"name": None}),
|
||||
Expando({"name": "bob"})
|
||||
]
|
||||
filter_conf = FilterConf(property="name", value="alice")
|
||||
|
||||
result = filter_by(items, filter_conf)
|
||||
|
||||
# None gets converted to "none" string, shouldn't match "alice"
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "alice"
|
||||
|
||||
|
||||
def test_i_can_filter_with_case_insensitive_property_names():
|
||||
"""Test filtering using case-insensitive property names."""
|
||||
items = [
|
||||
Expando({"Name": "alice"}), # uppercase N
|
||||
Expando({"Name": "bob"}),
|
||||
Expando({"Name": "charlie"})
|
||||
]
|
||||
filter_conf = FilterConf(property="name", value="alice") # lowercase n
|
||||
|
||||
result = filter_by(items, filter_conf)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].Name == "alice"
|
||||
|
||||
|
||||
def test_i_can_filter_with_numbers_and_mixed_types():
|
||||
"""Test filtering with mixed types (numbers, strings)."""
|
||||
items = [
|
||||
Expando({"value": "123"}),
|
||||
Expando({"value": 123}),
|
||||
Expando({"value": "hello123"}),
|
||||
Expando({"value": "world"})
|
||||
]
|
||||
filter_conf = FilterConf(property="value", value="123")
|
||||
|
||||
result = filter_by(items, filter_conf)
|
||||
|
||||
# Should match "123", 456 (converted to "456"), and "hello123"
|
||||
assert len(result) == 3
|
||||
expected_values = ["123", 123, "hello123"]
|
||||
actual_values = [item.value for item in result]
|
||||
assert all(val in expected_values for val in actual_values)
|
||||
85
tests/test_filter_conf.py
Normal file
85
tests/test_filter_conf.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from src.core.utils import FilterConf
|
||||
|
||||
|
||||
def test_i_can_create_filter_conf_from_valid_text():
|
||||
"""Test creating FilterConf from valid property=value format."""
|
||||
result = FilterConf.new("property=value")
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, FilterConf)
|
||||
assert result.property == "property"
|
||||
assert result.value == "value"
|
||||
|
||||
|
||||
def test_i_can_handle_none_input():
|
||||
"""Test that None input returns None."""
|
||||
result = FilterConf.new(None)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_i_can_handle_whitespace_around_equals():
|
||||
"""Test that whitespace around property and value is stripped."""
|
||||
result = FilterConf.new(" property = value ")
|
||||
|
||||
assert result is not None
|
||||
assert result.property == "property"
|
||||
assert result.value == "value"
|
||||
|
||||
|
||||
def test_i_cannot_create_filter_conf_without_equals():
|
||||
"""Test that text without equals sign returns None."""
|
||||
result = FilterConf.new("property")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_i_cannot_create_filter_conf_with_multiple_equals():
|
||||
"""Test that text with multiple equals signs returns None."""
|
||||
result = FilterConf.new("property=value=another")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_i_cannot_create_filter_conf_from_empty_string():
|
||||
"""Test that empty string returns None."""
|
||||
result = FilterConf.new("")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_i_cannot_create_filter_conf_with_empty_property():
|
||||
"""Test that empty property name returns None."""
|
||||
result = FilterConf.new("=value")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_i_cannot_create_filter_conf_with_empty_value():
|
||||
"""Test that empty value returns None."""
|
||||
result = FilterConf.new("property=")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_i_cannot_create_filter_conf_with_both_empty():
|
||||
"""Test that both empty property and value returns None."""
|
||||
result = FilterConf.new("=")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_i_cannot_create_filter_conf_with_whitespace_only():
|
||||
"""Test that whitespace-only parts return None."""
|
||||
result = FilterConf.new(" = ")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_i_can_handle_special_characters_in_values():
|
||||
"""Test that special characters in property names and values are handled correctly."""
|
||||
result = FilterConf.new("device_type=USB Audio Device (2.0)")
|
||||
|
||||
assert result is not None
|
||||
assert result.property == "device_type"
|
||||
assert result.value == "USB Audio Device (2.0)"
|
||||
152
tests/test_sort_by.py
Normal file
152
tests/test_sort_by.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from src.core.utils import FilterConf, SortConf, sort_by
|
||||
from src.core.Expando import Expando
|
||||
|
||||
|
||||
def test_i_can_sort_by_ascending_strings():
|
||||
"""Test sorting strings in ascending order."""
|
||||
items = [
|
||||
Expando({"name": "charlie"}),
|
||||
Expando({"name": "alice"}),
|
||||
Expando({"name": "bob"})
|
||||
]
|
||||
sort_conf = SortConf(property="name", ascending=True)
|
||||
|
||||
result = sort_by(items, sort_conf)
|
||||
|
||||
assert len(result) == 3
|
||||
assert result[0].name == "alice"
|
||||
assert result[1].name == "bob"
|
||||
assert result[2].name == "charlie"
|
||||
|
||||
|
||||
def test_i_can_sort_by_descending_strings():
|
||||
"""Test sorting strings in descending order."""
|
||||
items = [
|
||||
Expando({"name": "alice"}),
|
||||
Expando({"name": "charlie"}),
|
||||
Expando({"name": "bob"})
|
||||
]
|
||||
sort_conf = SortConf(property="name", ascending=False)
|
||||
|
||||
result = sort_by(items, sort_conf)
|
||||
|
||||
assert len(result) == 3
|
||||
assert result[0].name == "charlie"
|
||||
assert result[1].name == "bob"
|
||||
assert result[2].name == "alice"
|
||||
|
||||
|
||||
def test_i_can_handle_none_sort_conf():
|
||||
"""Test that None sort configuration returns items unchanged."""
|
||||
items = [
|
||||
Expando({"name": "charlie"}),
|
||||
Expando({"name": "alice"})
|
||||
]
|
||||
|
||||
result = sort_by(items, None)
|
||||
|
||||
assert result == items
|
||||
assert result[0].name == "charlie"
|
||||
assert result[1].name == "alice"
|
||||
|
||||
|
||||
def test_i_can_handle_empty_list():
|
||||
"""Test that empty list returns empty list."""
|
||||
items = []
|
||||
sort_conf = SortConf(property="name")
|
||||
|
||||
result = sort_by(items, sort_conf)
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_i_can_sort_with_missing_properties():
|
||||
"""Test sorting when some objects don't have the property."""
|
||||
items = [
|
||||
Expando({"name": "bob"}),
|
||||
Expando({"age": 25}), # no name property
|
||||
Expando({"name": "alice"})
|
||||
]
|
||||
sort_conf = SortConf(property="name", ascending=True)
|
||||
|
||||
result = sort_by(items, sort_conf)
|
||||
|
||||
assert len(result) == 3
|
||||
# Items without the property should get empty string and sort first
|
||||
assert result[0].geti("name", "") == "" # the one with missing property
|
||||
assert result[1].name == "alice"
|
||||
assert result[2].name == "bob"
|
||||
|
||||
|
||||
def test_i_can_sort_with_none_values():
|
||||
"""Test sorting when properties have None values."""
|
||||
items = [
|
||||
Expando({"name": "bob"}),
|
||||
Expando({"name": None}),
|
||||
Expando({"name": "alice"})
|
||||
]
|
||||
sort_conf = SortConf(property="name", ascending=True)
|
||||
|
||||
result = sort_by(items, sort_conf)
|
||||
|
||||
assert len(result) == 3
|
||||
# None values should be treated as empty strings and sort first
|
||||
assert result[0].name is None
|
||||
assert result[1].name == "alice"
|
||||
assert result[2].name == "bob"
|
||||
|
||||
|
||||
def test_i_can_sort_with_case_insensitive_property_names():
|
||||
"""Test sorting using case-insensitive property names."""
|
||||
items = [
|
||||
Expando({"Name": "charlie"}), # uppercase N
|
||||
Expando({"Name": "alice"}),
|
||||
Expando({"Name": "bob"})
|
||||
]
|
||||
sort_conf = SortConf(property="name", ascending=True) # lowercase n
|
||||
|
||||
result = sort_by(items, sort_conf)
|
||||
|
||||
assert len(result) == 3
|
||||
assert result[0].Name == "alice"
|
||||
assert result[1].Name == "bob"
|
||||
assert result[2].Name == "charlie"
|
||||
|
||||
|
||||
|
||||
def test_i_can_sort_mixed_types():
|
||||
"""Test sorting with mixed types (numbers, strings, None)."""
|
||||
items = [
|
||||
Expando({"value": "zebra"}),
|
||||
Expando({"value": 10}),
|
||||
Expando({"value": None}),
|
||||
Expando({"value": "apple"})
|
||||
]
|
||||
sort_conf = SortConf(property="value", ascending=True)
|
||||
|
||||
result = sort_by(items, sort_conf)
|
||||
|
||||
assert len(result) == 4
|
||||
# Mixed types will be converted to strings for comparison
|
||||
# None -> "", numbers -> str(number), strings stay strings
|
||||
assert result[0].value is None # None becomes "" and sorts first
|
||||
assert result[1].value == 10 # "10" sorts before "apple"
|
||||
assert result[2].value == "apple" # "apple" sorts before "zebra"
|
||||
assert result[3].value == "zebra"
|
||||
|
||||
|
||||
def test_i_can_sort_with_whitespace_in_property_name():
|
||||
"""Test that property names are stripped of whitespace."""
|
||||
items = [
|
||||
Expando({"name": "charlie"}),
|
||||
Expando({"name": "alice"}),
|
||||
Expando({"name": "bob"})
|
||||
]
|
||||
sort_conf = SortConf(property=" name ", ascending=True) # whitespace around property
|
||||
|
||||
result = sort_by(items, sort_conf)
|
||||
|
||||
assert len(result) == 3
|
||||
assert result[0].name == "alice"
|
||||
assert result[1].name == "bob"
|
||||
assert result[2].name == "charlie"
|
||||
Reference in New Issue
Block a user