Skip to content

Commit f519dde

Browse files
added unit tests for platformAudio
1 parent 7903a75 commit f519dde

1 file changed

Lines changed: 280 additions & 0 deletions

File tree

tests/rtc/test_platform_audio.py

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
"""
2+
Tests for PlatformAudio functionality.
3+
4+
These tests require audio hardware to be available. On CI environments without
5+
audio devices, tests will be skipped automatically.
6+
"""
7+
8+
import pytest
9+
from typing import Optional
10+
11+
from livekit import rtc
12+
13+
14+
# Global to cache PlatformAudio availability check
15+
_platform_audio_available: Optional[bool] = None
16+
_platform_audio_error: Optional[str] = None
17+
18+
19+
def _check_platform_audio_available() -> tuple[bool, Optional[str]]:
20+
"""Check if PlatformAudio can be initialized on this system."""
21+
global _platform_audio_available, _platform_audio_error
22+
23+
if _platform_audio_available is not None:
24+
return _platform_audio_available, _platform_audio_error
25+
26+
try:
27+
pa = rtc.PlatformAudio()
28+
_platform_audio_available = True
29+
_platform_audio_error = None
30+
# Keep reference to avoid cleanup issues
31+
del pa
32+
except rtc.PlatformAudioError as e:
33+
_platform_audio_available = False
34+
_platform_audio_error = str(e)
35+
36+
return _platform_audio_available, _platform_audio_error
37+
38+
39+
def requires_platform_audio(func):
40+
"""Decorator to skip tests if PlatformAudio is not available."""
41+
42+
@pytest.mark.skipif(
43+
not _check_platform_audio_available()[0],
44+
reason=f"PlatformAudio not available: {_check_platform_audio_available()[1]}",
45+
)
46+
def wrapper(*args, **kwargs):
47+
return func(*args, **kwargs)
48+
49+
wrapper.__name__ = func.__name__
50+
wrapper.__doc__ = func.__doc__
51+
return wrapper
52+
53+
54+
# Use a module-scoped fixture to avoid creating multiple PlatformAudio instances
55+
@pytest.fixture(scope="module")
56+
def platform_audio():
57+
"""Create a PlatformAudio instance for testing."""
58+
available, error = _check_platform_audio_available()
59+
if not available:
60+
pytest.skip(f"PlatformAudio not available: {error}")
61+
62+
pa = rtc.PlatformAudio()
63+
yield pa
64+
# Cleanup handled by garbage collection
65+
66+
67+
class TestPlatformAudioCreation:
68+
"""Tests for PlatformAudio initialization."""
69+
70+
def test_platform_audio_creation(self, platform_audio):
71+
"""Test that PlatformAudio can be created successfully."""
72+
assert platform_audio is not None
73+
74+
def test_platform_audio_multiple_instances(self, platform_audio):
75+
"""Test that multiple PlatformAudio instances can coexist."""
76+
# Creating another instance should reuse the same underlying ADM
77+
available, error = _check_platform_audio_available()
78+
if not available:
79+
pytest.skip(f"PlatformAudio not available: {error}")
80+
81+
pa2 = rtc.PlatformAudio()
82+
assert pa2 is not None
83+
# Both should work
84+
assert platform_audio is not None
85+
86+
87+
class TestDeviceEnumeration:
88+
"""Tests for audio device enumeration."""
89+
90+
def test_recording_devices_returns_list(self, platform_audio):
91+
"""Test that recording_devices() returns a list."""
92+
devices = platform_audio.recording_devices()
93+
assert isinstance(devices, list)
94+
95+
def test_playout_devices_returns_list(self, platform_audio):
96+
"""Test that playout_devices() returns a list."""
97+
devices = platform_audio.playout_devices()
98+
assert isinstance(devices, list)
99+
100+
def test_recording_device_info_structure(self, platform_audio):
101+
"""Test that recording devices have correct structure."""
102+
devices = platform_audio.recording_devices()
103+
if not devices:
104+
pytest.skip("No recording devices available")
105+
106+
device = devices[0]
107+
assert isinstance(device, rtc.AudioDeviceInfo)
108+
assert isinstance(device.index, int)
109+
assert isinstance(device.name, str)
110+
assert isinstance(device.id, str)
111+
assert device.index >= 0
112+
assert len(device.name) > 0
113+
114+
def test_playout_device_info_structure(self, platform_audio):
115+
"""Test that playout devices have correct structure."""
116+
devices = platform_audio.playout_devices()
117+
if not devices:
118+
pytest.skip("No playout devices available")
119+
120+
device = devices[0]
121+
assert isinstance(device, rtc.AudioDeviceInfo)
122+
assert isinstance(device.index, int)
123+
assert isinstance(device.name, str)
124+
assert isinstance(device.id, str)
125+
assert device.index >= 0
126+
assert len(device.name) > 0
127+
128+
def test_device_indices_are_sequential(self, platform_audio):
129+
"""Test that device indices start at 0 and are sequential."""
130+
recording = platform_audio.recording_devices()
131+
playout = platform_audio.playout_devices()
132+
133+
for i, device in enumerate(recording):
134+
assert device.index == i, f"Recording device index mismatch: {device.index} != {i}"
135+
136+
for i, device in enumerate(playout):
137+
assert device.index == i, f"Playout device index mismatch: {device.index} != {i}"
138+
139+
140+
class TestDeviceSelection:
141+
"""Tests for audio device selection."""
142+
143+
def test_set_recording_device_valid(self, platform_audio):
144+
"""Test setting a valid recording device."""
145+
devices = platform_audio.recording_devices()
146+
if not devices:
147+
pytest.skip("No recording devices available")
148+
149+
# Should not raise
150+
platform_audio.set_recording_device(devices[0].id)
151+
152+
def test_set_playout_device_valid(self, platform_audio):
153+
"""Test setting a valid playout device."""
154+
devices = platform_audio.playout_devices()
155+
if not devices:
156+
pytest.skip("No playout devices available")
157+
158+
# Should not raise
159+
platform_audio.set_playout_device(devices[0].id)
160+
161+
def test_set_recording_device_invalid(self, platform_audio):
162+
"""Test that setting an invalid recording device raises an error."""
163+
with pytest.raises(rtc.PlatformAudioError) as exc_info:
164+
platform_audio.set_recording_device("invalid-device-id-that-does-not-exist")
165+
166+
assert "not found" in str(exc_info.value).lower() or "failed" in str(exc_info.value).lower()
167+
168+
def test_set_playout_device_invalid(self, platform_audio):
169+
"""Test that setting an invalid playout device raises an error."""
170+
with pytest.raises(rtc.PlatformAudioError) as exc_info:
171+
platform_audio.set_playout_device("invalid-device-id-that-does-not-exist")
172+
173+
assert "not found" in str(exc_info.value).lower() or "failed" in str(exc_info.value).lower()
174+
175+
176+
class TestAudioSourceCreation:
177+
"""Tests for PlatformAudioSource creation."""
178+
179+
def test_create_audio_source_default_options(self, platform_audio):
180+
"""Test creating an audio source with default options."""
181+
source = platform_audio.create_audio_source()
182+
assert source is not None
183+
assert isinstance(source, rtc.PlatformAudioSource)
184+
185+
def test_create_audio_source_custom_options(self, platform_audio):
186+
"""Test creating an audio source with custom options."""
187+
options = rtc.PlatformAudioOptions(
188+
echo_cancellation=True,
189+
noise_suppression=True,
190+
auto_gain_control=True,
191+
prefer_hardware=False,
192+
)
193+
source = platform_audio.create_audio_source(options)
194+
assert source is not None
195+
assert isinstance(source, rtc.PlatformAudioSource)
196+
197+
def test_create_audio_source_all_processing_disabled(self, platform_audio):
198+
"""Test creating an audio source with all processing disabled."""
199+
options = rtc.PlatformAudioOptions(
200+
echo_cancellation=False,
201+
noise_suppression=False,
202+
auto_gain_control=False,
203+
)
204+
source = platform_audio.create_audio_source(options)
205+
assert source is not None
206+
207+
def test_audio_source_has_handle(self, platform_audio):
208+
"""Test that created audio source has a valid internal handle."""
209+
source = platform_audio.create_audio_source()
210+
# The _handle property is used internally for track creation
211+
assert hasattr(source, "_handle")
212+
assert source._handle > 0
213+
214+
def test_create_multiple_audio_sources(self, platform_audio):
215+
"""Test creating multiple audio sources from the same PlatformAudio."""
216+
source1 = platform_audio.create_audio_source()
217+
source2 = platform_audio.create_audio_source()
218+
219+
assert source1 is not None
220+
assert source2 is not None
221+
# Each source should have a unique handle
222+
assert source1._handle != source2._handle
223+
224+
225+
class TestPlatformAudioOptions:
226+
"""Tests for PlatformAudioOptions dataclass."""
227+
228+
def test_default_options(self):
229+
"""Test default PlatformAudioOptions values."""
230+
options = rtc.PlatformAudioOptions()
231+
assert options.echo_cancellation is True
232+
assert options.noise_suppression is True
233+
assert options.auto_gain_control is True
234+
assert options.prefer_hardware is False
235+
236+
def test_custom_options(self):
237+
"""Test custom PlatformAudioOptions values."""
238+
options = rtc.PlatformAudioOptions(
239+
echo_cancellation=False,
240+
noise_suppression=False,
241+
auto_gain_control=False,
242+
prefer_hardware=True,
243+
)
244+
assert options.echo_cancellation is False
245+
assert options.noise_suppression is False
246+
assert options.auto_gain_control is False
247+
assert options.prefer_hardware is True
248+
249+
250+
class TestAudioDeviceInfo:
251+
"""Tests for AudioDeviceInfo dataclass."""
252+
253+
def test_audio_device_info_creation(self):
254+
"""Test creating AudioDeviceInfo manually."""
255+
info = rtc.AudioDeviceInfo(index=0, name="Test Mic", id="test-guid-123")
256+
assert info.index == 0
257+
assert info.name == "Test Mic"
258+
assert info.id == "test-guid-123"
259+
260+
def test_audio_device_info_equality(self):
261+
"""Test AudioDeviceInfo equality comparison."""
262+
info1 = rtc.AudioDeviceInfo(index=0, name="Test Mic", id="guid-1")
263+
info2 = rtc.AudioDeviceInfo(index=0, name="Test Mic", id="guid-1")
264+
info3 = rtc.AudioDeviceInfo(index=1, name="Other Mic", id="guid-2")
265+
266+
assert info1 == info2
267+
assert info1 != info3
268+
269+
270+
class TestIntegrationWithTrack:
271+
"""Integration tests with LocalAudioTrack."""
272+
273+
def test_create_track_from_platform_audio_source(self, platform_audio):
274+
"""Test creating a LocalAudioTrack from PlatformAudioSource."""
275+
source = platform_audio.create_audio_source()
276+
track = rtc.LocalAudioTrack.create_audio_track("test-mic", source)
277+
278+
assert track is not None
279+
assert track.name == "test-mic"
280+
assert track.kind == rtc.TrackKind.KIND_AUDIO

0 commit comments

Comments
 (0)