358 lines
11 KiB
Python
358 lines
11 KiB
Python
"""
|
|
Unit tests for Audio Manager service.
|
|
|
|
Tests cover:
|
|
- Singleton pattern
|
|
- Backend initialization
|
|
- Sound playback
|
|
- Volume control
|
|
- Mute/unmute functionality
|
|
- Backend fallback
|
|
"""
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch, mock_open
|
|
|
|
# Add project root to path
|
|
project_root = Path(__file__).parent.parent.parent
|
|
if str(project_root) not in sys.path:
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
from core.audio import AudioManager, get_audio_manager, SoundConfig
|
|
|
|
|
|
class TestAudioManagerSingleton(unittest.TestCase):
|
|
"""Test AudioManager singleton behavior."""
|
|
|
|
def tearDown(self):
|
|
"""Reset singleton after each test."""
|
|
AudioManager._instance = None
|
|
|
|
def test_singleton_instance(self):
|
|
"""Test that AudioManager is a proper singleton."""
|
|
am1 = get_audio_manager()
|
|
am2 = get_audio_manager()
|
|
|
|
self.assertIs(am1, am2)
|
|
self.assertIsInstance(am1, AudioManager)
|
|
|
|
def test_singleton_reset(self):
|
|
"""Test that singleton can be reset."""
|
|
am1 = get_audio_manager()
|
|
AudioManager._instance = None
|
|
am2 = get_audio_manager()
|
|
|
|
self.assertIsNot(am1, am2)
|
|
|
|
|
|
class TestAudioManagerInitialization(unittest.TestCase):
|
|
"""Test AudioManager initialization."""
|
|
|
|
def tearDown(self):
|
|
"""Reset singleton after each test."""
|
|
AudioManager._instance = None
|
|
|
|
@patch('core.audio.AudioManager._init_backend')
|
|
def test_initialization_sets_defaults(self, mock_init):
|
|
"""Test that initialization sets default values."""
|
|
am = AudioManager()
|
|
|
|
self.assertIsInstance(am._config, SoundConfig)
|
|
self.assertEqual(am._config.enabled, True)
|
|
self.assertEqual(am._config.volume, 0.7)
|
|
self.assertEqual(am._sound_cache, {})
|
|
|
|
@patch('PyQt6.QtMultimedia.QSoundEffect')
|
|
def test_qt_backend_initialization(self, mock_qt):
|
|
"""Test Qt backend initialization."""
|
|
AudioManager._instance = None
|
|
am = AudioManager()
|
|
|
|
self.assertEqual(am._backend, 'qt')
|
|
self.assertTrue(am._available)
|
|
|
|
@patch('core.audio.print')
|
|
def test_no_backend_available(self, mock_print):
|
|
"""Test when no audio backend is available."""
|
|
AudioManager._instance = None
|
|
|
|
with patch.dict('sys.modules', {'PyQt6.QtMultimedia': None}):
|
|
with patch('platform.system', return_value='Linux'):
|
|
am = AudioManager()
|
|
|
|
self.assertFalse(am._available)
|
|
self.assertIsNone(am._backend)
|
|
|
|
|
|
class TestAudioAvailability(unittest.TestCase):
|
|
"""Test audio availability checking."""
|
|
|
|
def tearDown(self):
|
|
"""Reset singleton after each test."""
|
|
AudioManager._instance = None
|
|
|
|
def test_is_available_true(self):
|
|
"""Test availability when backend is working."""
|
|
am = get_audio_manager()
|
|
am._available = True
|
|
|
|
self.assertTrue(am.is_available())
|
|
|
|
def test_is_available_false(self):
|
|
"""Test availability when no backend."""
|
|
am = get_audio_manager()
|
|
am._available = False
|
|
|
|
self.assertFalse(am.is_available())
|
|
|
|
|
|
class TestVolumeControl(unittest.TestCase):
|
|
"""Test volume control functionality."""
|
|
|
|
def tearDown(self):
|
|
"""Reset singleton after each test."""
|
|
AudioManager._instance = None
|
|
|
|
def test_set_volume(self):
|
|
"""Test setting volume."""
|
|
am = get_audio_manager()
|
|
|
|
am.set_volume(0.5)
|
|
self.assertEqual(am._config.volume, 0.5)
|
|
|
|
def test_set_volume_clamping_high(self):
|
|
"""Test volume clamping above 1.0."""
|
|
am = get_audio_manager()
|
|
|
|
am.set_volume(1.5)
|
|
self.assertEqual(am._config.volume, 1.0)
|
|
|
|
def test_set_volume_clamping_low(self):
|
|
"""Test volume clamping below 0.0."""
|
|
am = get_audio_manager()
|
|
|
|
am.set_volume(-0.5)
|
|
self.assertEqual(am._config.volume, 0.0)
|
|
|
|
def test_get_volume(self):
|
|
"""Test getting volume."""
|
|
am = get_audio_manager()
|
|
am._config.volume = 0.75
|
|
|
|
self.assertEqual(am.get_volume(), 0.75)
|
|
|
|
def test_set_volume_updates_cached_effects(self):
|
|
"""Test that set_volume updates cached sound effects."""
|
|
am = get_audio_manager()
|
|
mock_effect = MagicMock()
|
|
am._sound_cache['test'] = mock_effect
|
|
|
|
am.set_volume(0.5)
|
|
|
|
mock_effect.setVolume.assert_called_once_with(0.5)
|
|
|
|
|
|
class TestMuteFunctionality(unittest.TestCase):
|
|
"""Test mute/unmute functionality."""
|
|
|
|
def tearDown(self):
|
|
"""Reset singleton after each test."""
|
|
AudioManager._instance = None
|
|
|
|
def test_mute(self):
|
|
"""Test muting."""
|
|
am = get_audio_manager()
|
|
|
|
am.mute()
|
|
self.assertFalse(am._config.enabled)
|
|
|
|
def test_unmute(self):
|
|
"""Test unmuting."""
|
|
am = get_audio_manager()
|
|
|
|
am._config.enabled = False
|
|
am.unmute()
|
|
self.assertTrue(am._config.enabled)
|
|
|
|
def test_is_muted(self):
|
|
"""Test checking mute status."""
|
|
am = get_audio_manager()
|
|
|
|
am._config.enabled = True
|
|
self.assertFalse(am.is_muted())
|
|
|
|
am._config.enabled = False
|
|
self.assertTrue(am.is_muted())
|
|
|
|
def test_set_enabled(self):
|
|
"""Test set_enabled method."""
|
|
am = get_audio_manager()
|
|
|
|
am.set_enabled(False)
|
|
self.assertFalse(am._config.enabled)
|
|
|
|
am.set_enabled(True)
|
|
self.assertTrue(am._config.enabled)
|
|
|
|
|
|
class TestSoundPlayback(unittest.TestCase):
|
|
"""Test sound playback functionality."""
|
|
|
|
def tearDown(self):
|
|
"""Reset singleton after each test."""
|
|
AudioManager._instance = None
|
|
|
|
def test_play_sound_not_available(self):
|
|
"""Test play_sound when audio not available."""
|
|
am = get_audio_manager()
|
|
am._available = False
|
|
|
|
# Should not raise
|
|
result = am.play_sound("test")
|
|
self.assertIsNone(result)
|
|
|
|
def test_play_sound_disabled(self):
|
|
"""Test play_sound when audio disabled."""
|
|
am = get_audio_manager()
|
|
am._config.enabled = False
|
|
|
|
# Should not raise
|
|
result = am.play_sound("test")
|
|
self.assertIsNone(result)
|
|
|
|
@patch.object(AudioManager, '_get_sound_file')
|
|
@patch.object(AudioManager, '_play_beep')
|
|
def test_play_sound_fallback_beep(self, mock_beep, mock_get_file):
|
|
"""Test fallback to beep when sound file not found."""
|
|
am = get_audio_manager()
|
|
mock_get_file.return_value = None
|
|
|
|
am.play_sound("nonexistent")
|
|
|
|
mock_beep.assert_called_once()
|
|
|
|
@patch('PyQt6.QtMultimedia.QSoundEffect')
|
|
@patch.object(AudioManager, '_get_sound_file')
|
|
def test_play_qt_backend(self, mock_get_file, mock_qt):
|
|
"""Test Qt backend sound playback."""
|
|
am = get_audio_manager()
|
|
am._backend = 'qt'
|
|
|
|
mock_effect = MagicMock()
|
|
mock_qt.return_value = mock_effect
|
|
mock_get_file.return_value = Path("/test/sound.wav")
|
|
|
|
am.play_sound("test")
|
|
|
|
mock_effect.setSource.assert_called_once_with("/test/sound.wav")
|
|
mock_effect.play.assert_called_once()
|
|
|
|
|
|
class TestSoundFileResolution(unittest.TestCase):
|
|
"""Test sound file resolution."""
|
|
|
|
def tearDown(self):
|
|
"""Reset singleton after each test."""
|
|
AudioManager._instance = None
|
|
|
|
def test_get_sound_file_no_directory(self):
|
|
"""Test get_sound_file when no sounds directory set."""
|
|
am = get_audio_manager()
|
|
am._config.sounds_dir = None
|
|
|
|
result = am._get_sound_file("test")
|
|
self.assertIsNone(result)
|
|
|
|
@patch('pathlib.Path.exists')
|
|
def test_get_sound_file_with_extension(self, mock_exists):
|
|
"""Test getting sound file with correct extension."""
|
|
am = get_audio_manager()
|
|
am._config.sounds_dir = Path("/sounds")
|
|
|
|
mock_exists.return_value = True
|
|
|
|
result = am._get_sound_file("global")
|
|
self.assertIsNotNone(result)
|
|
self.assertIn("global", str(result))
|
|
|
|
@patch('pathlib.Path.exists')
|
|
def test_get_sound_file_try_alternative_extensions(self, mock_exists):
|
|
"""Test trying alternative extensions."""
|
|
am = get_audio_manager()
|
|
am._config.sounds_dir = Path("/sounds")
|
|
|
|
# Return True for .mp3 extension
|
|
def side_effect():
|
|
return str(mock_exists.call_args[0][0]).endswith('.mp3')
|
|
|
|
mock_exists.side_effect = [False, True]
|
|
|
|
result = am._get_sound_file("test")
|
|
self.assertIsNotNone(result)
|
|
|
|
|
|
class TestBackendInfo(unittest.TestCase):
|
|
"""Test backend information methods."""
|
|
|
|
def tearDown(self):
|
|
"""Reset singleton after each test."""
|
|
AudioManager._instance = None
|
|
|
|
def test_get_backend(self):
|
|
"""Test getting current backend name."""
|
|
am = get_audio_manager()
|
|
am._backend = 'qt'
|
|
|
|
self.assertEqual(am.get_backend(), 'qt')
|
|
|
|
def test_get_backend_none(self):
|
|
"""Test getting backend when none set."""
|
|
am = get_audio_manager()
|
|
am._backend = None
|
|
|
|
self.assertIsNone(am.get_backend())
|
|
|
|
|
|
class TestSoundConfig(unittest.TestCase):
|
|
"""Test SoundConfig dataclass."""
|
|
|
|
def test_default_values(self):
|
|
"""Test default configuration values."""
|
|
config = SoundConfig()
|
|
|
|
self.assertTrue(config.enabled)
|
|
self.assertEqual(config.volume, 0.7)
|
|
self.assertIsNone(config.sounds_dir)
|
|
|
|
def test_custom_values(self):
|
|
"""Test custom configuration values."""
|
|
config = SoundConfig(enabled=False, volume=0.5, sounds_dir=Path("/custom"))
|
|
|
|
self.assertFalse(config.enabled)
|
|
self.assertEqual(config.volume, 0.5)
|
|
self.assertEqual(config.sounds_dir, Path("/custom"))
|
|
|
|
|
|
class TestShutdown(unittest.TestCase):
|
|
"""Test shutdown functionality."""
|
|
|
|
def tearDown(self):
|
|
"""Reset singleton after each test."""
|
|
AudioManager._instance = None
|
|
|
|
def test_shutdown_clears_cache(self):
|
|
"""Test that shutdown clears sound cache."""
|
|
am = get_audio_manager()
|
|
mock_effect = MagicMock()
|
|
am._sound_cache['test'] = mock_effect
|
|
|
|
am.shutdown()
|
|
|
|
mock_effect.stop.assert_called_once()
|
|
self.assertEqual(am._sound_cache, {})
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|