# -*- coding: utf-8 -*- """ Pytest Konfigurations- und Fixture-Datei. Stellt gemeinsame Fixtures für alle Tests bereit, einschließlich Plugin-Tests. """ import asyncio import sys from pathlib import Path from typing import Any, AsyncGenerator from unittest.mock import AsyncMock, MagicMock import pytest # Projekt-Root zum Pfad hinzufügen PROJECT_ROOT = Path(__file__).parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) # ============================================================================ # Event Manager Fixtures # ============================================================================ @pytest.fixture def event_manager(): """Erstellt einen echten EventManager für Tests.""" from trixy_core.events.eventmanager import EventManager mock_app = MagicMock() em = EventManager(mock_app) return em @pytest.fixture def mock_event_manager(): """Erstellt einen Mock-EventManager für isolierte Tests.""" em = MagicMock() em.emit = AsyncMock() em.trigger = AsyncMock() em.register = MagicMock() em.on = MagicMock(return_value=lambda f: f) return em # ============================================================================ # Application Fixtures # ============================================================================ @pytest.fixture def mock_application(mock_event_manager): """Erstellt eine Mock-Application für Plugin-Tests.""" app = MagicMock() app.events = mock_event_manager app.debug = True app.base_path = PROJECT_ROOT # Extension Points Mock app.extension_points = {} return app @pytest.fixture def mock_plugin_path(tmp_path): """Erstellt einen temporären Plugin-Pfad.""" plugin_dir = tmp_path / "test_plugin" plugin_dir.mkdir() (plugin_dir / "models").mkdir() return plugin_dir # ============================================================================ # Audio Test Data Fixtures # ============================================================================ @pytest.fixture def sample_audio_16khz() -> bytes: """Generiert Sample-Audio-Daten (16kHz, 16-bit, mono, 1 Sekunde Stille).""" import struct sample_rate = 16000 duration = 1 # 1 Sekunde num_samples = sample_rate * duration # Stille generieren (alle Nullen) audio_data = struct.pack(f"<{num_samples}h", *([0] * num_samples)) return audio_data @pytest.fixture def sample_audio_22khz() -> bytes: """Generiert Sample-Audio-Daten (22.05kHz, 16-bit, mono, 1 Sekunde Stille).""" import struct sample_rate = 22050 duration = 1 num_samples = sample_rate * duration audio_data = struct.pack(f"<{num_samples}h", *([0] * num_samples)) return audio_data @pytest.fixture def sample_audio_with_tone() -> bytes: """Generiert Sample-Audio mit einem 440Hz Ton.""" import math import struct sample_rate = 16000 duration = 0.5 # 0.5 Sekunden frequency = 440 # Hz amplitude = 16000 # Lautstärke num_samples = int(sample_rate * duration) samples = [] for i in range(num_samples): t = i / sample_rate sample = int(amplitude * math.sin(2 * math.pi * frequency * t)) samples.append(sample) audio_data = struct.pack(f"<{num_samples}h", *samples) return audio_data # ============================================================================ # Config Fixtures # ============================================================================ @pytest.fixture def default_tts_config(): """Standard TTS-Konfiguration für Tests.""" return { "name": "Test TTS", "enabled": True, "language": "de-DE", "auto_download": False, # Keine echten Downloads in Tests } @pytest.fixture def default_stt_config(): """Standard STT-Konfiguration für Tests.""" return { "name": "Test STT", "enabled": True, "language": "de-DE", "auto_download": False, # Keine echten Downloads in Tests } # ============================================================================ # Plugin Test Helpers # ============================================================================ class PluginTestHelper: """Hilfsklasse für Plugin-Tests.""" def __init__(self, application, plugin_path: Path, config: dict): self.application = application self.plugin_path = plugin_path self.config = config self.emitted_events: list[tuple[str, dict]] = [] # Event-Emit tracken original_emit = application.events.emit async def tracking_emit(event_name, data=None): self.emitted_events.append((event_name, data or {})) return await original_emit(event_name, data) application.events.emit = AsyncMock(side_effect=tracking_emit) def get_emitted_events(self, event_name: str) -> list[dict]: """Gibt alle emittierten Events eines Typs zurück.""" return [data for name, data in self.emitted_events if name == event_name] def assert_event_emitted(self, event_name: str, count: int = 1): """Prüft, ob ein Event emittiert wurde.""" events = self.get_emitted_events(event_name) assert len(events) >= count, f"Expected {count} '{event_name}' events, got {len(events)}" def clear_events(self): """Löscht die Event-Historie.""" self.emitted_events.clear() @pytest.fixture def plugin_test_helper(mock_application, mock_plugin_path, default_tts_config): """Erstellt einen PluginTestHelper.""" return PluginTestHelper(mock_application, mock_plugin_path, default_tts_config) # ============================================================================ # Async Helpers # ============================================================================ @pytest.fixture def event_loop(): """Erstellt eine neue Event-Loop für jeden Test.""" loop = asyncio.new_event_loop() yield loop loop.close() # ============================================================================ # Skip Markers für bedingte Tests # ============================================================================ def pytest_configure(config): """Registriert benutzerdefinierte Marker.""" config.addinivalue_line( "markers", "requires_piper: Test benötigt piper-tts Installation" ) config.addinivalue_line( "markers", "requires_vosk: Test benötigt vosk Installation" ) config.addinivalue_line( "markers", "requires_whisper: Test benötigt openai-whisper Installation" ) config.addinivalue_line( "markers", "requires_coqui: Test benötigt TTS (Coqui) Installation" ) config.addinivalue_line( "markers", "requires_google_tts: Test benötigt google-cloud-texttospeech Installation" ) config.addinivalue_line( "markers", "requires_deepspeech: Test benötigt deepspeech Installation" ) config.addinivalue_line( "markers", "integration: Integrationstests (können langsamer sein)" ) config.addinivalue_line( "markers", "slow: Langsame Tests" ) # Skip-Decorator Factories def skip_if_no_module(module_name: str, pip_name: str = None): """Decorator zum Überspringen wenn ein Modul nicht installiert ist.""" pip_name = pip_name or module_name try: __import__(module_name) return pytest.mark.skipif(False, reason="") except ImportError: return pytest.mark.skip(reason=f"{pip_name} nicht installiert") # Vordefinierte Skip-Marker requires_piper = skip_if_no_module("piper", "piper-tts") requires_vosk = skip_if_no_module("vosk") requires_whisper = skip_if_no_module("whisper", "openai-whisper") requires_coqui = skip_if_no_module("TTS") requires_google_tts = skip_if_no_module("google.cloud.texttospeech", "google-cloud-texttospeech") requires_deepspeech = skip_if_no_module("deepspeech")