| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- # -*- coding: utf-8 -*-
- """
- Tests fuer das Audio-Ducking Plugin.
- Prueft Lautstaerke-Reduktion bei Wakeword-Erkennung und
- Wiederherstellung nach Conversation-Ende.
- """
- from __future__ import annotations
- from pathlib import Path
- from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
- import pytest
- from plugins.audio_ducking.main import AudioDuckingPlugin
- # ============================================================================
- # Fixtures
- # ============================================================================
- @pytest.fixture
- def mock_volume_handler() -> MagicMock:
- """Erstellt einen Mock-VolumeHandler mit allen benoetigten Methoden."""
- handler = MagicMock()
- handler.available = True
- handler.set_output_volume = AsyncMock()
- handler.restore_from_config = AsyncMock()
- return handler
- @pytest.fixture
- def mock_app_with_volume(mock_application, mock_volume_handler) -> MagicMock:
- """Erstellt eine Mock-Application mit VolumeHandler."""
- mock_application._volume_handler = mock_volume_handler
- return mock_application
- @pytest.fixture
- def mock_app_without_volume(mock_application) -> MagicMock:
- """Erstellt eine Mock-Application ohne VolumeHandler."""
- # Sicherstellen, dass kein _volume_handler existiert
- if hasattr(mock_application, "_volume_handler"):
- delattr(mock_application, "_volume_handler")
- return mock_application
- @pytest.fixture
- def plugin(mock_app_with_volume, mock_plugin_path) -> AudioDuckingPlugin:
- """Erstellt eine Plugin-Instanz mit VolumeHandler."""
- config = {"duck_volume_percent": 20}
- return AudioDuckingPlugin(mock_app_with_volume, mock_plugin_path, config)
- @pytest.fixture
- def plugin_no_handler(mock_app_without_volume, mock_plugin_path) -> AudioDuckingPlugin:
- """Erstellt eine Plugin-Instanz ohne VolumeHandler."""
- config = {"duck_volume_percent": 20}
- return AudioDuckingPlugin(mock_app_without_volume, mock_plugin_path, config)
- @pytest.fixture
- def plugin_custom_volume(mock_app_with_volume, mock_plugin_path) -> AudioDuckingPlugin:
- """Erstellt eine Plugin-Instanz mit benutzerdefiniertem duck_volume_percent."""
- config = {"duck_volume_percent": 35}
- return AudioDuckingPlugin(mock_app_with_volume, mock_plugin_path, config)
- # ============================================================================
- # Plugin-Erstellung und Attribute
- # ============================================================================
- class TestPluginAttributes:
- """Prueft Plugin-Attribute und Initialisierung."""
- def test_plugin_name(self, plugin: AudioDuckingPlugin) -> None:
- """NAME ist 'audio_ducking'."""
- assert AudioDuckingPlugin.NAME == "audio_ducking"
- def test_plugin_version(self, plugin: AudioDuckingPlugin) -> None:
- """VERSION ist '2.0.0'."""
- assert AudioDuckingPlugin.VERSION == "2.0.0"
- def test_plugin_description(self, plugin: AudioDuckingPlugin) -> None:
- """DESCRIPTION ist gesetzt."""
- assert AudioDuckingPlugin.DESCRIPTION != ""
- def test_plugin_author(self, plugin: AudioDuckingPlugin) -> None:
- """AUTHOR ist 'Trixy'."""
- assert AudioDuckingPlugin.AUTHOR == "Trixy"
- def test_initial_ducked_state(self, plugin: AudioDuckingPlugin) -> None:
- """_ducked ist nach Erstellung False."""
- assert plugin._ducked is False
- def test_plugin_config(self, plugin: AudioDuckingPlugin) -> None:
- """Plugin hat die uebergebene Konfiguration."""
- assert plugin.get_config_value("duck_volume_percent") == 20
- # ============================================================================
- # on_load / on_unload
- # ============================================================================
- class TestLifecycle:
- """Prueft Plugin-Lebenszyklus (on_load, on_unload)."""
- @pytest.mark.asyncio
- async def test_on_load_does_not_crash(self, plugin: AudioDuckingPlugin) -> None:
- """on_load() laeuft ohne Fehler durch."""
- await plugin.on_load()
- @pytest.mark.asyncio
- async def test_on_unload_restores_if_ducked(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """on_unload() stellt Lautstaerke wieder her, wenn geduckt."""
- # Erst ducken
- await plugin._duck_volume()
- assert plugin._ducked is True
- # Dann entladen
- await plugin.on_unload()
- mock_volume_handler.restore_from_config.assert_called_once_with("output")
- assert plugin._ducked is False
- @pytest.mark.asyncio
- async def test_on_unload_does_nothing_if_not_ducked(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """on_unload() macht nichts, wenn nicht geduckt."""
- assert plugin._ducked is False
- await plugin.on_unload()
- mock_volume_handler.restore_from_config.assert_not_called()
- # ============================================================================
- # _duck_volume
- # ============================================================================
- class TestDuckVolume:
- """Prueft die Lautstaerke-Reduktion."""
- @pytest.mark.asyncio
- async def test_duck_volume_sets_volume(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """_duck_volume() setzt Lautstaerke mit source='duck'."""
- await plugin._duck_volume()
- mock_volume_handler.set_output_volume.assert_called_once_with(20, source="duck")
- @pytest.mark.asyncio
- async def test_duck_volume_sets_ducked_flag(
- self, plugin: AudioDuckingPlugin
- ) -> None:
- """_duck_volume() setzt _ducked auf True."""
- await plugin._duck_volume()
- assert plugin._ducked is True
- @pytest.mark.asyncio
- async def test_duck_volume_idempotent(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """Zweiter Aufruf von _duck_volume() aendert nichts."""
- await plugin._duck_volume()
- await plugin._duck_volume()
- # Nur einmal aufgerufen
- mock_volume_handler.set_output_volume.assert_called_once()
- @pytest.mark.asyncio
- async def test_duck_volume_no_handler(
- self, plugin_no_handler: AudioDuckingPlugin
- ) -> None:
- """_duck_volume() ohne VolumeHandler setzt _ducked nicht."""
- await plugin_no_handler._duck_volume()
- assert plugin_no_handler._ducked is False
- @pytest.mark.asyncio
- async def test_duck_volume_handler_not_available(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """_duck_volume() mit unavailable Handler setzt _ducked nicht."""
- mock_volume_handler.available = False
- await plugin._duck_volume()
- assert plugin._ducked is False
- mock_volume_handler.set_output_volume.assert_not_called()
- @pytest.mark.asyncio
- async def test_duck_volume_reads_config_value(
- self, plugin_custom_volume: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """_duck_volume() liest duck_volume_percent aus der Config."""
- await plugin_custom_volume._duck_volume()
- mock_volume_handler.set_output_volume.assert_called_once_with(35, source="duck")
- @pytest.mark.asyncio
- async def test_duck_volume_default_percent(
- self, mock_app_with_volume: MagicMock, mock_plugin_path: Path,
- mock_volume_handler: MagicMock,
- ) -> None:
- """Ohne Config-Wert wird Default 20 verwendet."""
- # Plugin ohne duck_volume_percent in Config
- p = AudioDuckingPlugin(mock_app_with_volume, mock_plugin_path, config={})
- await p._duck_volume()
- mock_volume_handler.set_output_volume.assert_called_once_with(20, source="duck")
- # ============================================================================
- # _restore_volume
- # ============================================================================
- class TestRestoreVolume:
- """Prueft die Lautstaerke-Wiederherstellung."""
- @pytest.mark.asyncio
- async def test_restore_volume_calls_handler(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """_restore_volume() ruft restore_from_config('output') auf."""
- await plugin._duck_volume()
- await plugin._restore_volume()
- mock_volume_handler.restore_from_config.assert_called_once_with("output")
- @pytest.mark.asyncio
- async def test_restore_volume_clears_ducked_flag(
- self, plugin: AudioDuckingPlugin
- ) -> None:
- """_restore_volume() setzt _ducked auf False."""
- await plugin._duck_volume()
- assert plugin._ducked is True
- await plugin._restore_volume()
- assert plugin._ducked is False
- @pytest.mark.asyncio
- async def test_restore_volume_idempotent(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """Zweiter Aufruf von _restore_volume() aendert nichts."""
- await plugin._duck_volume()
- await plugin._restore_volume()
- await plugin._restore_volume()
- mock_volume_handler.restore_from_config.assert_called_once()
- @pytest.mark.asyncio
- async def test_restore_volume_no_handler(
- self, plugin_no_handler: AudioDuckingPlugin
- ) -> None:
- """_restore_volume() ohne Handler setzt _ducked trotzdem auf False."""
- plugin_no_handler._ducked = True
- await plugin_no_handler._restore_volume()
- assert plugin_no_handler._ducked is False
- @pytest.mark.asyncio
- async def test_restore_volume_handler_not_available(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """_restore_volume() mit unavailable Handler ruft nichts auf, setzt aber Flag."""
- plugin._ducked = True
- mock_volume_handler.available = False
- await plugin._restore_volume()
- assert plugin._ducked is False
- mock_volume_handler.restore_from_config.assert_not_called()
- # ============================================================================
- # Event-Handler
- # ============================================================================
- class TestEventHandlers:
- """Prueft die Event-Handler-Methoden."""
- @pytest.mark.asyncio
- async def test_on_wakeword_ducks(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """on_wakeword() reduziert die Lautstaerke."""
- await plugin.on_wakeword("wakeword_detected", {})
- assert plugin._ducked is True
- mock_volume_handler.set_output_volume.assert_called_once()
- @pytest.mark.asyncio
- async def test_on_manual_trigger_ducks(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """on_manual_trigger() reduziert die Lautstaerke."""
- await plugin.on_manual_trigger("wakeword_manual_trigger", {})
- assert plugin._ducked is True
- @pytest.mark.asyncio
- async def test_on_recording_complete_restores(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """on_recording_complete() stellt Lautstaerke wieder her."""
- await plugin._duck_volume()
- await plugin.on_recording_complete("recording_complete", {})
- assert plugin._ducked is False
- mock_volume_handler.restore_from_config.assert_called_once_with("output")
- @pytest.mark.asyncio
- async def test_on_conversation_started_ducks(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """on_conversation_started() reduziert die Lautstaerke."""
- await plugin.on_conversation_started("conversation_started", {})
- assert plugin._ducked is True
- @pytest.mark.asyncio
- async def test_on_conversation_ended_restores(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """on_conversation_ended() stellt Lautstaerke wieder her."""
- await plugin._duck_volume()
- await plugin.on_conversation_ended("conversation_ended", {})
- assert plugin._ducked is False
- # ============================================================================
- # Komplexe Ablaeufe
- # ============================================================================
- class TestComplexFlows:
- """Prueft vollstaendige Ablaeufe ueber mehrere Events."""
- @pytest.mark.asyncio
- async def test_full_wakeword_flow(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """Wakeword → Duck → Conversation ended → Restore."""
- # Wakeword erkannt
- await plugin.on_wakeword("wakeword_detected", {})
- assert plugin._ducked is True
- # Conversation endet
- await plugin.on_conversation_ended("conversation_ended", {})
- assert plugin._ducked is False
- # Handler-Aufrufe pruefen
- mock_volume_handler.set_output_volume.assert_called_once_with(20, source="duck")
- mock_volume_handler.restore_from_config.assert_called_once_with("output")
- @pytest.mark.asyncio
- async def test_multiple_ducks_dont_stack(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """Mehrfaches Ducken (Wakeword + Conversation) ruft Handler nur einmal auf."""
- # Wakeword erkannt → duckt
- await plugin.on_wakeword("wakeword_detected", {})
- # Conversation gestartet → bereits geduckt, kein zweiter Aufruf
- await plugin.on_conversation_started("conversation_started", {})
- # set_output_volume nur einmal
- mock_volume_handler.set_output_volume.assert_called_once()
- # Ein Restore reicht
- await plugin.on_conversation_ended("conversation_ended", {})
- assert plugin._ducked is False
- @pytest.mark.asyncio
- async def test_restore_after_unload(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """Unload stellt Lautstaerke wieder her, wenn Plugin geduckt ist."""
- await plugin.on_wakeword("wakeword_detected", {})
- assert plugin._ducked is True
- # Plugin wird entladen → Restore
- await plugin.on_unload()
- assert plugin._ducked is False
- mock_volume_handler.restore_from_config.assert_called_once_with("output")
- @pytest.mark.asyncio
- async def test_duck_restore_duck_again(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """Nach Restore kann erneut geduckt werden."""
- # Erster Zyklus
- await plugin.on_wakeword("wakeword_detected", {})
- await plugin.on_conversation_ended("conversation_ended", {})
- assert plugin._ducked is False
- # Zweiter Zyklus
- await plugin.on_manual_trigger("wakeword_manual_trigger", {})
- assert plugin._ducked is True
- await plugin.on_recording_complete("recording_complete", {})
- assert plugin._ducked is False
- # Handler wurde zweimal aufgerufen
- assert mock_volume_handler.set_output_volume.call_count == 2
- assert mock_volume_handler.restore_from_config.call_count == 2
- @pytest.mark.asyncio
- async def test_restore_without_duck_is_noop(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """Restore ohne vorheriges Duck macht nichts."""
- await plugin.on_conversation_ended("conversation_ended", {})
- assert plugin._ducked is False
- mock_volume_handler.restore_from_config.assert_not_called()
- # ============================================================================
- # _get_volume_handler
- # ============================================================================
- class TestGetVolumeHandler:
- """Prueft den Zugriff auf den VolumeHandler."""
- def test_returns_handler_if_present(
- self, plugin: AudioDuckingPlugin, mock_volume_handler: MagicMock
- ) -> None:
- """_get_volume_handler() gibt den Handler zurueck."""
- handler = plugin._get_volume_handler()
- assert handler is mock_volume_handler
- def test_returns_none_if_absent(
- self, plugin_no_handler: AudioDuckingPlugin
- ) -> None:
- """_get_volume_handler() gibt None zurueck ohne Handler."""
- handler = plugin_no_handler._get_volume_handler()
- assert handler is None
|