| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- # -*- 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")
|