Skip to content

Commit d2d1584

Browse files
committed
update tests
1 parent bc6d271 commit d2d1584

File tree

4 files changed

+1294
-93
lines changed

4 files changed

+1294
-93
lines changed
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc)
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
import pytest
6+
from unittest.mock import patch
7+
8+
import alsaaudio
9+
import numpy as np
10+
11+
from arduino.app_peripherals.speaker.speaker import Speaker
12+
from arduino.app_peripherals.speaker.alsa_speaker import ALSASpeaker, _alsa_format_name_to_dtype, _dtype_to_alsa_format_name
13+
from arduino.app_peripherals.speaker.errors import SpeakerConfigError, SpeakerOpenError
14+
15+
16+
class TestALSASpeakerInitialization:
17+
"""Test ALSA speaker initialization."""
18+
19+
def test_alsa_start_opens_device(self, pcm_registry):
20+
"""Test that start() opens ALSA device."""
21+
spkr = Speaker(device=0)
22+
23+
assert not spkr.is_started()
24+
25+
spkr.start()
26+
27+
assert spkr.is_started()
28+
pcm_instance = pcm_registry.get_last_instance()
29+
assert pcm_instance is not None
30+
31+
def test_alsa_stop_closes_device(self, pcm_registry):
32+
"""Test that stop() closes ALSA device."""
33+
spkr = Speaker(device=0)
34+
spkr.start()
35+
spkr.stop()
36+
37+
assert not spkr.is_started()
38+
pcm_instance = pcm_registry.get_last_instance()
39+
assert pcm_instance.close.called
40+
41+
42+
class TestALSASpeakerDeviceResolution:
43+
"""Test ALSA device resolution."""
44+
45+
def test_resolve_by_shorthand(self, mock_alsa_usb_speakers):
46+
"""Test resolving device by integer index."""
47+
spkr = ALSASpeaker()
48+
assert spkr.device_stable_ref == "CARD=SomeCard,DEV=0"
49+
50+
spkr = ALSASpeaker(device=Speaker.USB_SPEAKER_1)
51+
assert spkr.device_stable_ref == "CARD=SomeCard,DEV=0"
52+
53+
spkr = ALSASpeaker(device=Speaker.USB_SPEAKER_2)
54+
assert spkr.device_stable_ref == "CARD=AnotherCard,DEV=0"
55+
56+
def test_resolve_by_integer_index(self):
57+
"""Test resolving device by integer index."""
58+
spkr = ALSASpeaker(device=0)
59+
assert spkr.device_stable_ref == "CARD=SomeCard,DEV=0"
60+
61+
spkr = ALSASpeaker(device=1)
62+
assert spkr.device_stable_ref == "CARD=AnotherCard,DEV=0"
63+
64+
@patch("arduino.app_peripherals.speaker.alsa_speaker.alsaaudio.pcms", return_value=[])
65+
def test_resolve_no_usb_devices_raises_error(self, mock_pcms):
66+
"""Test that error is raised when no devices found."""
67+
with pytest.raises(SpeakerConfigError) as exc_info:
68+
ALSASpeaker(device=0)
69+
70+
assert "No ALSA speakers found" in str(exc_info.value)
71+
72+
def test_resolve_out_of_range_raises_error(self, mock_alsa_usb_speakers):
73+
"""Test that out of range index raises error."""
74+
with pytest.raises(SpeakerConfigError) as exc_info:
75+
ALSASpeaker(device=5)
76+
77+
assert "out of range" in str(exc_info.value)
78+
79+
def test_resolve_explicit_device_name(self, mock_alsa_usb_speakers):
80+
"""Test that explicit device names are passed through."""
81+
spkr = ALSASpeaker(device="CARD=SomeCard,DEV=0")
82+
assert spkr.device_stable_ref == "CARD=SomeCard,DEV=0"
83+
84+
spkr = ALSASpeaker(device="plughw:CARD=SomeCard,DEV=0")
85+
assert spkr.device_stable_ref == "CARD=SomeCard,DEV=0"
86+
87+
spkr = ALSASpeaker(device="plughw:CARD=AnotherCard,DEV=0")
88+
assert spkr.device_stable_ref == "CARD=AnotherCard,DEV=0"
89+
90+
91+
class TestALSAErrorManagement:
92+
"""Test handling ALSA errors."""
93+
94+
def test_device_busy_error(self):
95+
"""Test that device busy error is properly reported."""
96+
spkr = ALSASpeaker(device="CARD=SomeCard,DEV=0")
97+
spkr.auto_reconnect_delay = 0
98+
99+
with patch(
100+
"arduino.app_peripherals.speaker.alsa_speaker.alsaaudio.PCM",
101+
side_effect=alsaaudio.ALSAAudioError("Device or resource busy"),
102+
return_value=[],
103+
):
104+
with pytest.raises(SpeakerOpenError) as exc_info:
105+
spkr.start()
106+
107+
assert "busy" in str(exc_info.value).lower()
108+
109+
def test_generic_alsa_error(self):
110+
"""Test generic ALSA error handling."""
111+
spkr = ALSASpeaker(device="CARD=SomeCard,DEV=0")
112+
spkr.auto_reconnect_delay = 0
113+
114+
with patch(
115+
"arduino.app_peripherals.speaker.alsa_speaker.alsaaudio.PCM",
116+
side_effect=alsaaudio.ALSAAudioError("Some generic ALSA error"),
117+
return_value=[],
118+
):
119+
with pytest.raises(SpeakerOpenError):
120+
spkr.start()
121+
122+
def test_write_error_doesnt_raise(self, pcm_registry):
123+
"""Test that ALSA errors when writing don't raise exceptions."""
124+
spkr = ALSASpeaker(device="CARD=SomeCard,DEV=0")
125+
spkr.start()
126+
127+
# Return ALSA error that's not disconnection
128+
pcm_instance = pcm_registry.get_last_instance()
129+
pcm_instance.write = lambda data: -32 # EPIPE error
130+
131+
audio_data = np.zeros(1024, dtype=np.int16)
132+
spkr.play(audio_data) # Should not raise
133+
134+
def test_stop_with_close_error(self, pcm_registry):
135+
"""Test that stop handles close errors gracefully."""
136+
spkr = ALSASpeaker(device="CARD=SomeCard,DEV=0")
137+
spkr.start()
138+
139+
pcm_instance = pcm_registry.get_last_instance()
140+
pcm_instance.close.side_effect = alsaaudio.ALSAAudioError("Close failed")
141+
142+
# Should not raise
143+
spkr.stop()
144+
145+
assert not spkr.is_started()
146+
147+
148+
class TestALSADeviceDisconnection:
149+
"""Test ALSA device disconnection handling."""
150+
151+
def test_detect_device_disconnection(self, mock_alsa_usb_speakers, pcm_registry):
152+
"""Test device disconnection detection during playback."""
153+
spkr = ALSASpeaker()
154+
spkr.start()
155+
156+
# Simulate device disconnection
157+
pcm_instance = pcm_registry.get_last_instance()
158+
pcm_instance.write = lambda data: None # Simulate write failure
159+
160+
with patch("arduino.app_peripherals.speaker.alsa_speaker.alsaaudio.pcms", side_effect=None, return_value=[]):
161+
# Attempt to write should detect disconnection
162+
audio_data = np.zeros(1024, dtype=np.int16)
163+
spkr.play(audio_data) # Should handle disconnection gracefully
164+
165+
assert spkr._pcm is None # PCM should be cleared
166+
167+
def test_list_devices_check(self, mock_alsa_usb_speakers):
168+
"""Test device disconnection detection by enumerating devices."""
169+
spkr = ALSASpeaker()
170+
spkr.start()
171+
172+
devices = ALSASpeaker.list_devices()
173+
assert len(devices) > 0
174+
175+
# Simulate device removal
176+
with patch("arduino.app_peripherals.speaker.alsa_speaker.alsaaudio.pcms", side_effect=None, return_value=[]):
177+
devices = ALSASpeaker.list_devices()
178+
assert len(devices) == 0
179+
180+
181+
class TestALSADeviceReconnection:
182+
"""Test ALSA device reconnection logic."""
183+
184+
def test_reconnection_after_device_available(self, mock_alsa_usb_speakers):
185+
"""Test reconnection when device becomes available."""
186+
# Initially no devices - creation should fail
187+
with patch("arduino.app_peripherals.speaker.alsa_speaker.alsaaudio.pcms", side_effect=None, return_value=[]):
188+
with pytest.raises(SpeakerConfigError):
189+
spkr = ALSASpeaker(device="CARD=SomeCard,DEV=0")
190+
191+
# Now creation and start should work
192+
spkr = ALSASpeaker(device="CARD=SomeCard,DEV=0")
193+
spkr.start()
194+
195+
assert spkr.is_started()
196+
197+
spkr.stop()
198+
199+
def test_write_reconnects(self, mock_alsa_usb_speakers, pcm_registry):
200+
"""Test write attempts reconnection after disconnection."""
201+
spkr = ALSASpeaker()
202+
spkr.start()
203+
204+
# Simulate a disconnection
205+
pcm_instance = pcm_registry.get_last_instance()
206+
pcm_instance.write = lambda data: None
207+
208+
audio_data = np.zeros(1024, dtype=np.int16)
209+
spkr.play(audio_data)
210+
211+
# Mock successful reconnection
212+
pcm_instance.write = lambda data: len(data)
213+
214+
# Playing a second time should trigger reconnection attempt
215+
spkr.play(audio_data) # Should handle gracefully
216+
217+
218+
class TestALSAPlayback:
219+
"""Test ALSA speaker playback methods."""
220+
221+
def test_alsa_speaker_play(self, mock_alsa_usb_speakers):
222+
"""Test play with ALSA speaker."""
223+
spkr = ALSASpeaker()
224+
spkr.start()
225+
226+
audio_data = np.zeros(1024, dtype=np.int16)
227+
spkr.play(audio_data) # Should not raise
228+
229+
@pytest.mark.parametrize(
230+
"format",
231+
[np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32, np.float32, np.float64],
232+
)
233+
def test_alsa_has_correct_format(self, mock_alsa_usb_speakers, pcm_registry, format):
234+
"""Test that ALSA is configured with correct format."""
235+
format_dtype = np.dtype(format)
236+
237+
spkr = ALSASpeaker(format=format, buffer_size=128)
238+
spkr.start()
239+
240+
audio_data = np.zeros(128, dtype=format_dtype)
241+
spkr.play(audio_data)
242+
243+
pcm_instance = pcm_registry.get_last_instance()
244+
assert format_dtype == _alsa_format_name_to_dtype(spkr.alsa_format_name)
245+
assert spkr.alsa_format_name == _dtype_to_alsa_format_name(format_dtype)
246+
assert spkr.alsa_format_idx == pcm_instance.info()["format"]
247+
assert spkr.alsa_format_name == pcm_instance.info()["format_name"]
248+
249+
def test_unsupported_format_with_none_dtype(self):
250+
"""Test that unsupported formats trigger an error."""
251+
with pytest.raises(SpeakerConfigError):
252+
ALSASpeaker(format=None)
253+
254+
with pytest.raises(SpeakerConfigError):
255+
ALSASpeaker(format="unsupported_format")
256+
257+
258+
class TestALSAVolumeControl:
259+
"""Test ALSA speaker volume control."""
260+
261+
def test_volume_default(self, mock_alsa_usb_speakers):
262+
"""Test that default volume is 100."""
263+
spkr = ALSASpeaker()
264+
assert spkr.volume == 100
265+
266+
def test_volume_setter(self, mock_alsa_usb_speakers):
267+
"""Test setting volume."""
268+
spkr = ALSASpeaker()
269+
spkr.volume = 50
270+
assert spkr.volume == 50
271+
272+
spkr.volume = 0
273+
assert spkr.volume == 0
274+
275+
spkr.volume = 100
276+
assert spkr.volume == 100
277+
278+
def test_volume_out_of_range(self, mock_alsa_usb_speakers):
279+
"""Test that volume out of range raises error."""
280+
spkr = ALSASpeaker()
281+
282+
with pytest.raises(ValueError):
283+
spkr.volume = -1
284+
285+
with pytest.raises(ValueError):
286+
spkr.volume = 101
287+
288+
def test_volume_affects_output(self, mock_alsa_usb_speakers, pcm_registry):
289+
"""Test that volume changes affect audio output."""
290+
spkr = ALSASpeaker()
291+
spkr.start()
292+
spkr.volume = 50
293+
294+
audio_data = np.full(1024, 1000, dtype=np.int16)
295+
spkr.play(audio_data)
296+
297+
# Volume should scale the audio
298+
# (we can't directly test the output, but we verify no errors)
299+
300+
301+
class TestALSASharedMode:
302+
"""Test ALSA speaker shared mode."""
303+
304+
def test_shared_mode_default(self, mock_alsa_usb_speakers):
305+
"""Test that default shared mode is True."""
306+
spkr = ALSASpeaker()
307+
assert spkr.shared is True
308+
309+
def test_exclusive_mode(self, mock_alsa_usb_speakers):
310+
"""Test exclusive mode."""
311+
spkr = ALSASpeaker(shared=False)
312+
assert spkr.shared is False
313+
spkr.start()
314+
assert spkr.is_started()

0 commit comments

Comments
 (0)