""" 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()