| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 |
- # -*- coding: utf-8 -*-
- """
- Tests für Event-basierte Integrationen.
- Testet:
- - Satellite.speak() und Satellite.play() Event-Auslösung
- - SatelliteAPI.say() mit Warte-Logik
- - WakewordService._wait_for_processing_result()
- """
- import asyncio
- import pytest
- from unittest.mock import AsyncMock, MagicMock, patch
- from dataclasses import dataclass, field
- from trixy_core.events.event_data.basic import (
- TTSRequest,
- TTSCompleted,
- StreamStart,
- StreamStop,
- ProcessingResult,
- )
- from trixy_core.satellite.satellite import Satellite, ConnectionState
- # =============================================================================
- # Fixtures
- # =============================================================================
- @pytest.fixture
- def mock_event_manager():
- """Erstellt einen Mock-EventManager."""
- manager = MagicMock()
- manager.trigger = AsyncMock()
- manager.register = MagicMock()
- manager.unregister = MagicMock(return_value=True)
- return manager
- @pytest.fixture
- def mock_application(mock_event_manager):
- """Erstellt eine Mock-Application."""
- app = MagicMock()
- app.events = mock_event_manager
- app.event_manager = mock_event_manager
- return app
- @pytest.fixture
- def satellite_with_app(mock_application):
- """Erstellt einen Satellite mit Application-Referenz."""
- sat = Satellite(
- satellite_id="test-sat-001",
- room_id="wohnzimmer",
- mac_address="AA:BB:CC:DD:EE:FF",
- alias="TestSatellite",
- application=mock_application
- )
- return sat
- # =============================================================================
- # Tests für Event-Datenklassen
- # =============================================================================
- class TestEventDataClasses:
- """Tests für neue Event-Datenklassen."""
- def test_tts_request_creation(self):
- """Testet TTSRequest Erstellung."""
- request = TTSRequest(
- request_id="req-123",
- satellite_id="sat-001",
- text="Hallo Welt",
- voice="default",
- speed=1.0,
- volume=0.8
- )
- assert request.request_id == "req-123"
- assert request.text == "Hallo Welt"
- assert request.volume == 0.8
- def test_tts_completed_creation(self):
- """Testet TTSCompleted Erstellung."""
- completed = TTSCompleted(
- request_id="req-123",
- satellite_id="sat-001",
- success=True,
- audio_duration=2.5
- )
- assert completed.success is True
- assert completed.audio_duration == 2.5
- def test_stream_start_creation(self):
- """Testet StreamStart Erstellung."""
- stream = StreamStart(
- satellite_id="sat-001",
- source="http://radio.mp3",
- volume=0.7,
- stream_type="music"
- )
- assert stream.source == "http://radio.mp3"
- assert stream.stream_type == "music"
- def test_stream_stop_creation(self):
- """Testet StreamStop Erstellung."""
- stop = StreamStop(
- satellite_id="sat-001",
- stream_type="all"
- )
- assert stop.stream_type == "all"
- def test_processing_result_creation(self):
- """Testet ProcessingResult Erstellung."""
- result = ProcessingResult(
- session_id="session-123",
- success=True,
- text="Wie ist das Wetter?",
- intent="weather.query",
- response_text="Es ist sonnig."
- )
- assert result.intent == "weather.query"
- assert result.response_text == "Es ist sonnig."
- # =============================================================================
- # Tests für Satellite Event-Integration
- # =============================================================================
- class TestSatelliteEventIntegration:
- """Tests für Satellite Event-Methoden."""
- @pytest.mark.asyncio
- async def test_speak_triggers_tts_request_event(self, satellite_with_app, mock_event_manager):
- """Testet, dass speak() das tts_request Event auslöst."""
- result = await satellite_with_app.speak("Hallo Welt", voice="alloy")
- assert result is True
- mock_event_manager.trigger.assert_called_once()
- # Prüfe Event-Name
- call_args = mock_event_manager.trigger.call_args
- assert call_args[0][0] == "tts_request"
- # Prüfe Event-Daten
- event_data = call_args[0][1]
- assert event_data.text == "Hallo Welt"
- assert event_data.voice == "alloy"
- assert event_data.satellite_id == "test-sat-001"
- @pytest.mark.asyncio
- async def test_speak_returns_false_without_application(self):
- """Testet, dass speak() False zurückgibt ohne Application."""
- sat = Satellite(satellite_id="no-app")
- result = await sat.speak("Test")
- assert result is False
- @pytest.mark.asyncio
- async def test_speak_returns_false_when_cancelled(self, satellite_with_app, mock_event_manager):
- """Testet, dass speak() False zurückgibt wenn Event gecancelt."""
- async def cancel_event(event_name, data):
- data.cancel()
- mock_event_manager.trigger = cancel_event
- result = await satellite_with_app.speak("Test")
- assert result is False
- @pytest.mark.asyncio
- async def test_play_triggers_stream_start_event(self, satellite_with_app, mock_event_manager):
- """Testet, dass play() das stream_start Event auslöst."""
- result = await satellite_with_app.play("http://radio.mp3", volume=0.5)
- assert result is True
- mock_event_manager.trigger.assert_called_once()
- call_args = mock_event_manager.trigger.call_args
- assert call_args[0][0] == "stream_start"
- event_data = call_args[0][1]
- assert event_data.source == "http://radio.mp3"
- assert event_data.volume == 0.5
- assert event_data.stream_type == "music"
- @pytest.mark.asyncio
- async def test_play_returns_false_without_application(self):
- """Testet, dass play() False zurückgibt ohne Application."""
- sat = Satellite(satellite_id="no-app")
- result = await sat.play("http://test.mp3")
- assert result is False
- @pytest.mark.asyncio
- async def test_stop_playback_triggers_stream_stop_event(self, satellite_with_app, mock_event_manager):
- """Testet, dass stop_playback() das stream_stop Event auslöst."""
- result = await satellite_with_app.stop_playback()
- assert result is True
- mock_event_manager.trigger.assert_called_once()
- call_args = mock_event_manager.trigger.call_args
- assert call_args[0][0] == "stream_stop"
- event_data = call_args[0][1]
- assert event_data.satellite_id == "test-sat-001"
- assert event_data.stream_type == "all"
- # =============================================================================
- # Tests für Satellite Application-Referenz
- # =============================================================================
- class TestSatelliteApplicationReference:
- """Tests für Satellite Application-Referenz."""
- def test_satellite_with_application(self, mock_application):
- """Testet Satellite-Erstellung mit Application."""
- sat = Satellite(
- satellite_id="test",
- application=mock_application
- )
- assert sat.application is mock_application
- def test_satellite_application_setter(self, mock_application):
- """Testet Application-Setter."""
- sat = Satellite(satellite_id="test")
- assert sat.application is None
- sat.application = mock_application
- assert sat.application is mock_application
- def test_satellite_without_application(self):
- """Testet Satellite ohne Application."""
- sat = Satellite(satellite_id="test")
- assert sat.application is None
- # =============================================================================
- # Tests für SatelliteAPI Warte-Logik
- # =============================================================================
- class TestSatelliteAPIWaitLogic:
- """Tests für SatelliteAPI.say() Warte-Logik."""
- @pytest.fixture
- def mock_satellite_manager(self):
- """Erstellt einen Mock-SatelliteManager."""
- manager = MagicMock()
- connected_sat = MagicMock()
- connected_sat.id = "sat-001"
- connected_sat.is_connected = True
- connected_sat.room_id = "wohnzimmer"
- manager.get = MagicMock(return_value=connected_sat)
- manager.list_connected = MagicMock(return_value=[connected_sat])
- manager.get_by_room = MagicMock(return_value=[connected_sat])
- return manager
- @pytest.fixture
- def satellite_api(self, mock_application, mock_satellite_manager):
- """Erstellt eine SatelliteAPI-Instanz."""
- from trixy_core.satellite.api import SatelliteAPI
- mock_application.satellite_manager = mock_satellite_manager
- return SatelliteAPI(mock_application)
- @pytest.mark.asyncio
- async def test_say_with_wait_registers_handler(self, satellite_api, mock_event_manager):
- """Testet, dass say() mit wait=True einen Handler registriert."""
- # Simuliere sofortige Completion
- async def trigger_and_complete(event_name, data):
- if event_name == "tts_request":
- # Finde den registrierten Handler und rufe ihn auf
- for call in mock_event_manager.register.call_args_list:
- if call[0][0] == "tts_completed":
- handler = call[0][1]
- completed = TTSCompleted(
- request_id=data.request_id,
- success=True
- )
- await handler("tts_completed", completed)
- mock_event_manager.trigger = trigger_and_complete
- result = await satellite_api.say("Test", satellites=["sat-001"], wait=True)
- # Handler sollte registriert worden sein
- assert mock_event_manager.register.called
- # Handler sollte wieder entfernt worden sein
- assert mock_event_manager.unregister.called
- @pytest.mark.asyncio
- async def test_say_without_wait_does_not_wait(self, satellite_api, mock_event_manager):
- """Testet, dass say() mit wait=False nicht wartet."""
- result = await satellite_api.say("Test", satellites=["sat-001"], wait=False)
- assert result is True
- mock_event_manager.trigger.assert_called_once()
- # Kein Handler sollte registriert werden bei wait=False
- # (nur tts_request wird ausgelöst)
- @pytest.mark.asyncio
- async def test_say_returns_false_for_empty_targets(self, satellite_api, mock_satellite_manager):
- """Testet, dass say() False zurückgibt wenn keine Ziele."""
- mock_satellite_manager.get_by_room = MagicMock(return_value=[])
- mock_satellite_manager.list_connected = MagicMock(return_value=[])
- result = await satellite_api.say("Test", room="leerer_raum")
- assert result is False
- # =============================================================================
- # Tests für WakewordService Verarbeitungs-Warte-Logik
- # =============================================================================
- class TestWakewordServiceProcessingWait:
- """Tests für WakewordService._wait_for_processing_result()."""
- @pytest.fixture
- def mock_wakeword_service(self, mock_application):
- """Erstellt einen Mock-WakewordService mittels MagicMock."""
- from trixy_core.wakeword.service import (
- WakewordServiceConfig,
- RecordingSession,
- ServiceState,
- WakewordService,
- )
- from trixy_core.wakeword.detector import WakewordDetection, WakewordType
- from datetime import datetime
- import logging
- # Verwende MagicMock statt echter Klasse
- service = MagicMock(spec=WakewordService)
- service._application = mock_application
- service._config = WakewordServiceConfig(
- standalone_mode=True,
- follow_up_timeout_seconds=0.1 # Kurzer Timeout für Tests
- )
- service.logger = logging.getLogger("test")
- # Simuliere aktive Session
- service._current_session = RecordingSession(
- session_id="test-session-123",
- wakeword_detection=WakewordDetection(
- wakeword_type=WakewordType.CUSTOM,
- model_name="test",
- confidence=0.95,
- audio_level=0.7,
- timestamp=datetime.now()
- ),
- start_time=datetime.now()
- )
- # Binde die echte Methode an den Mock
- from trixy_core.wakeword.service import WakewordService as RealService
- service._wait_for_processing_result = lambda: RealService._wait_for_processing_result(service)
- return service
- @pytest.mark.asyncio
- async def test_wait_for_processing_result_with_immediate_response(
- self, mock_wakeword_service, mock_event_manager
- ):
- """Testet _wait_for_processing_result mit sofortiger Antwort."""
- # Speichere den registrierten Handler um ihn später aufzurufen
- registered_handler = None
- def capture_register(event_name, handler):
- nonlocal registered_handler
- if event_name == "processing_result":
- registered_handler = handler
- mock_event_manager.register = capture_register
- # Starte den Wait in einem Task
- async def wait_and_respond():
- # Kurz warten damit der Handler registriert wird
- await asyncio.sleep(0.01)
- if registered_handler:
- result = ProcessingResult(
- session_id="test-session-123",
- success=True,
- intent="test.intent"
- )
- await registered_handler("processing_result", result)
- # Starte Response-Task
- response_task = asyncio.create_task(wait_and_respond())
- # Führe wait aus
- await mock_wakeword_service._wait_for_processing_result()
- # Warte auf Response-Task
- await response_task
- # Handler sollte registriert worden sein
- assert registered_handler is not None
- @pytest.mark.asyncio
- async def test_wait_for_processing_result_timeout(
- self, mock_wakeword_service, mock_event_manager
- ):
- """Testet Timeout-Handling."""
- # Registriere Handler aber triggere ihn nicht
- mock_event_manager.register = MagicMock()
- # Sollte durch Timeout beendet werden (0.1s Timeout)
- await mock_wakeword_service._wait_for_processing_result()
- # Handler sollte wieder entfernt werden (im finally-Block)
- assert mock_event_manager.unregister.called
- @pytest.mark.asyncio
- async def test_wait_for_processing_result_without_session(
- self, mock_wakeword_service, mock_event_manager
- ):
- """Testet Verhalten ohne aktive Session."""
- mock_wakeword_service._current_session = None
- # Sollte sofort zurückkehren
- await mock_wakeword_service._wait_for_processing_result()
- # Kein Handler sollte registriert werden
- assert not mock_event_manager.register.called
- @pytest.mark.asyncio
- async def test_wait_for_processing_result_without_event_manager(
- self, mock_wakeword_service
- ):
- """Testet Verhalten ohne EventManager."""
- mock_wakeword_service._application.events = None
- # Sollte ohne Fehler zurückkehren
- await mock_wakeword_service._wait_for_processing_result()
- # =============================================================================
- # Tests für SatelliteManager Application-Referenz
- # =============================================================================
- class TestSatelliteManagerApplicationRef:
- """Tests für SatelliteManager Application-Referenz Weitergabe."""
- def test_add_sets_application_reference(self, mock_application):
- """Testet, dass add() die Application-Referenz setzt."""
- from trixy_core.satellite.satellite_manager import SatelliteManager
- manager = SatelliteManager(mock_application)
- sat = Satellite(satellite_id="test-sat")
- assert sat.application is None
- manager.add(sat)
- assert sat.application is mock_application
- # =============================================================================
- # Integration Tests
- # =============================================================================
- class TestFullIntegration:
- """Vollständige Integrationstests."""
- @pytest.mark.asyncio
- async def test_satellite_speak_full_flow(self):
- """Testet den vollständigen speak()-Flow."""
- from trixy_core.events.eventmanager import EventManager
- # Mock Application für EventManager
- app = MagicMock()
- # Echten EventManager verwenden
- event_manager = EventManager(app)
- app.events = event_manager
- # Satellite erstellen
- sat = Satellite(
- satellite_id="integration-test",
- application=app
- )
- # TTS-Handler registrieren
- tts_requests = []
- async def on_tts_request(event_name, data):
- tts_requests.append(data)
- event_manager.register("tts_request", on_tts_request)
- # speak() aufrufen
- result = await sat.speak("Integration Test", voice="nova")
- assert result is True
- assert len(tts_requests) == 1
- assert tts_requests[0].text == "Integration Test"
- assert tts_requests[0].voice == "nova"
- @pytest.mark.asyncio
- async def test_satellite_play_full_flow(self):
- """Testet den vollständigen play()-Flow."""
- from trixy_core.events.eventmanager import EventManager
- app = MagicMock()
- event_manager = EventManager(app)
- app.events = event_manager
- sat = Satellite(satellite_id="play-test", application=app)
- stream_starts = []
- async def on_stream_start(event_name, data):
- stream_starts.append(data)
- event_manager.register("stream_start", on_stream_start)
- result = await sat.play("http://example.com/music.mp3", volume=0.8)
- assert result is True
- assert len(stream_starts) == 1
- assert stream_starts[0].source == "http://example.com/music.mp3"
- assert stream_starts[0].volume == 0.8
|