2026-01-26 14:50:44 +01:00
|
|
|
# Unit tests for AudioManager service
|
|
|
|
|
import unittest
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
# Import centralized mocks
|
|
|
|
|
from mpos.testing import (
|
|
|
|
|
MockMachine,
|
|
|
|
|
MockThread,
|
|
|
|
|
inject_mocks,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Inject mocks before importing AudioManager
|
|
|
|
|
inject_mocks({
|
|
|
|
|
'machine': MockMachine(),
|
|
|
|
|
'_thread': MockThread,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Now import the module to test
|
|
|
|
|
from mpos.audio.audiomanager import AudioManager
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAudioManager(unittest.TestCase):
|
|
|
|
|
"""Test cases for AudioManager service."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
"""Initialize AudioManager before each test."""
|
2026-02-24 16:39:26 +01:00
|
|
|
self.buzzer_pin = 46
|
2026-01-26 14:50:44 +01:00
|
|
|
self.i2s_pins = {'sck': 2, 'ws': 47, 'sd': 16}
|
|
|
|
|
|
|
|
|
|
# Reset singleton instance for each test
|
|
|
|
|
AudioManager._instance = None
|
|
|
|
|
|
2026-02-24 16:39:26 +01:00
|
|
|
AudioManager()
|
|
|
|
|
AudioManager.add(AudioManager.Output("speaker", "i2s", i2s_pins=self.i2s_pins))
|
|
|
|
|
AudioManager.add(AudioManager.Output("buzzer", "buzzer", buzzer_pin=self.buzzer_pin))
|
|
|
|
|
|
2026-01-26 14:50:44 +01:00
|
|
|
# Reset volume to default after creating instance
|
|
|
|
|
AudioManager.set_volume(70)
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
"""Clean up after each test."""
|
|
|
|
|
AudioManager.stop()
|
|
|
|
|
|
|
|
|
|
def test_initialization(self):
|
|
|
|
|
"""Test that AudioManager initializes correctly."""
|
|
|
|
|
am = AudioManager.get()
|
2026-02-24 16:39:26 +01:00
|
|
|
self.assertEqual(len(am._outputs), 2)
|
|
|
|
|
self.assertEqual(am._outputs[0].i2s_pins, self.i2s_pins)
|
|
|
|
|
self.assertEqual(am._outputs[1].buzzer_pin, self.buzzer_pin)
|
2026-01-26 14:50:44 +01:00
|
|
|
|
2026-02-24 16:39:26 +01:00
|
|
|
def test_get_outputs(self):
|
|
|
|
|
"""Test that get_outputs() returns configured outputs."""
|
|
|
|
|
outputs = AudioManager.get_outputs()
|
|
|
|
|
self.assertEqual(len(outputs), 2)
|
|
|
|
|
self.assertEqual(outputs[0].kind, "i2s")
|
|
|
|
|
self.assertEqual(outputs[1].kind, "buzzer")
|
2026-01-26 14:50:44 +01:00
|
|
|
|
2026-02-24 16:39:26 +01:00
|
|
|
def test_default_output(self):
|
|
|
|
|
"""Test default output selection."""
|
|
|
|
|
default_output = AudioManager.get_default_output()
|
|
|
|
|
self.assertIsNotNone(default_output)
|
|
|
|
|
self.assertEqual(default_output.kind, "i2s")
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
def test_stream_types(self):
|
|
|
|
|
"""Test stream type constants and priority order."""
|
|
|
|
|
self.assertEqual(AudioManager.STREAM_MUSIC, 0)
|
|
|
|
|
self.assertEqual(AudioManager.STREAM_NOTIFICATION, 1)
|
|
|
|
|
self.assertEqual(AudioManager.STREAM_ALARM, 2)
|
|
|
|
|
|
|
|
|
|
# Higher number = higher priority
|
|
|
|
|
self.assertTrue(AudioManager.STREAM_MUSIC < AudioManager.STREAM_NOTIFICATION)
|
|
|
|
|
self.assertTrue(AudioManager.STREAM_NOTIFICATION < AudioManager.STREAM_ALARM)
|
|
|
|
|
|
|
|
|
|
def test_volume_control(self):
|
|
|
|
|
"""Test volume get/set operations."""
|
|
|
|
|
# Set volume
|
|
|
|
|
AudioManager.set_volume(50)
|
|
|
|
|
self.assertEqual(AudioManager.get_volume(), 50)
|
|
|
|
|
|
|
|
|
|
# Test clamping to 0-100 range
|
|
|
|
|
AudioManager.set_volume(150)
|
|
|
|
|
self.assertEqual(AudioManager.get_volume(), 100)
|
|
|
|
|
|
|
|
|
|
AudioManager.set_volume(-10)
|
|
|
|
|
self.assertEqual(AudioManager.get_volume(), 0)
|
|
|
|
|
|
|
|
|
|
def test_no_hardware_rejects_playback(self):
|
|
|
|
|
"""Test that no hardware rejects all playback requests."""
|
|
|
|
|
# Re-initialize with no hardware
|
|
|
|
|
AudioManager._instance = None
|
2026-02-24 16:39:26 +01:00
|
|
|
AudioManager()
|
2026-01-26 14:50:44 +01:00
|
|
|
|
2026-02-24 16:39:26 +01:00
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
AudioManager.player(file_path="test.wav").start()
|
2026-01-26 14:50:44 +01:00
|
|
|
|
2026-02-24 16:39:26 +01:00
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
AudioManager.player(rtttl="Test:d=4,o=5,b=120:c").start()
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
def test_i2s_only_rejects_rtttl(self):
|
|
|
|
|
"""Test that I2S-only config rejects buzzer playback."""
|
|
|
|
|
# Re-initialize with I2S only
|
|
|
|
|
AudioManager._instance = None
|
2026-02-24 16:39:26 +01:00
|
|
|
AudioManager()
|
|
|
|
|
AudioManager.add(AudioManager.Output("speaker", "i2s", i2s_pins=self.i2s_pins))
|
2026-01-26 14:50:44 +01:00
|
|
|
|
2026-02-24 16:39:26 +01:00
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
AudioManager.player(rtttl="Test:d=4,o=5,b=120:c").start()
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
def test_buzzer_only_rejects_wav(self):
|
|
|
|
|
"""Test that buzzer-only config rejects I2S playback."""
|
|
|
|
|
# Re-initialize with buzzer only
|
|
|
|
|
AudioManager._instance = None
|
2026-02-24 16:39:26 +01:00
|
|
|
AudioManager()
|
|
|
|
|
AudioManager.add(AudioManager.Output("buzzer", "buzzer", buzzer_pin=self.buzzer_pin))
|
2026-01-26 14:50:44 +01:00
|
|
|
|
2026-02-24 16:39:26 +01:00
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
AudioManager.player(file_path="test.wav").start()
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
def test_is_playing_initially_false(self):
|
|
|
|
|
"""Test that is_playing() returns False initially."""
|
|
|
|
|
# Reset to ensure clean state
|
|
|
|
|
AudioManager._instance = None
|
2026-02-24 16:39:26 +01:00
|
|
|
AudioManager()
|
|
|
|
|
AudioManager.add(AudioManager.Output("speaker", "i2s", i2s_pins=self.i2s_pins))
|
|
|
|
|
self.assertFalse(AudioManager.player(file_path="test.wav").is_playing())
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
def test_stop_with_no_playback(self):
|
|
|
|
|
"""Test that stop() can be called when nothing is playing."""
|
|
|
|
|
# Should not raise exception
|
|
|
|
|
AudioManager.stop()
|
|
|
|
|
|
|
|
|
|
def test_volume_default_value(self):
|
|
|
|
|
"""Test that default volume is reasonable."""
|
2026-02-24 16:39:26 +01:00
|
|
|
# After init, volume should be at default (50)
|
|
|
|
|
AudioManager._instance = None
|
|
|
|
|
AudioManager()
|
|
|
|
|
self.assertEqual(AudioManager.get_volume(), 50)
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAudioManagerRecording(unittest.TestCase):
|
|
|
|
|
"""Test cases for AudioManager recording functionality."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
"""Initialize AudioManager with microphone before each test."""
|
|
|
|
|
# I2S pins with microphone input
|
2026-02-24 16:39:26 +01:00
|
|
|
self.i2s_pins_with_mic = {'sck': 2, 'ws': 47, 'sd_in': 15}
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
# Reset singleton instance for each test
|
|
|
|
|
AudioManager._instance = None
|
|
|
|
|
|
2026-02-24 16:39:26 +01:00
|
|
|
AudioManager()
|
|
|
|
|
AudioManager.add(AudioManager.Input("mic", "i2s", i2s_pins=self.i2s_pins_with_mic))
|
|
|
|
|
|
2026-01-26 14:50:44 +01:00
|
|
|
# Reset volume to default after creating instance
|
|
|
|
|
AudioManager.set_volume(70)
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
"""Clean up after each test."""
|
|
|
|
|
AudioManager.stop()
|
|
|
|
|
|
2026-02-24 16:39:26 +01:00
|
|
|
def test_get_inputs(self):
|
|
|
|
|
"""Test get_inputs() returns configured inputs."""
|
|
|
|
|
inputs = AudioManager.get_inputs()
|
|
|
|
|
self.assertEqual(len(inputs), 1)
|
|
|
|
|
self.assertEqual(inputs[0].kind, "i2s")
|
2026-01-26 14:50:44 +01:00
|
|
|
|
2026-02-24 16:39:26 +01:00
|
|
|
def test_default_input(self):
|
|
|
|
|
"""Test default input selection."""
|
|
|
|
|
default_input = AudioManager.get_default_input()
|
|
|
|
|
self.assertIsNotNone(default_input)
|
|
|
|
|
self.assertEqual(default_input.kind, "i2s")
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
def test_is_recording_initially_false(self):
|
|
|
|
|
"""Test that is_recording() returns False initially."""
|
2026-02-24 16:39:26 +01:00
|
|
|
recorder = AudioManager.recorder(file_path="test.wav")
|
|
|
|
|
self.assertFalse(recorder.is_recording())
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
def test_record_wav_no_microphone(self):
|
2026-02-24 16:39:26 +01:00
|
|
|
"""Test that recorder() fails when no microphone is configured."""
|
2026-01-26 14:50:44 +01:00
|
|
|
AudioManager._instance = None
|
2026-02-24 16:39:26 +01:00
|
|
|
AudioManager()
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
AudioManager.recorder(file_path="test.wav").start()
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
def test_record_wav_no_i2s(self):
|
|
|
|
|
AudioManager._instance = None
|
2026-02-24 16:39:26 +01:00
|
|
|
AudioManager()
|
|
|
|
|
AudioManager.add(AudioManager.Input("mic", "adc", adc_mic_pin=4))
|
|
|
|
|
recorder = AudioManager.recorder(file_path="test.wav")
|
|
|
|
|
self.assertFalse(recorder.is_recording())
|
2026-01-26 14:50:44 +01:00
|
|
|
|
|
|
|
|
def test_stop_with_no_recording(self):
|
|
|
|
|
"""Test that stop() can be called when nothing is recording."""
|
|
|
|
|
# Should not raise exception
|
|
|
|
|
AudioManager.stop()
|