conftest.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. # -*- coding: utf-8 -*-
  2. """
  3. Pytest Konfigurations- und Fixture-Datei.
  4. Stellt gemeinsame Fixtures für alle Tests bereit, einschließlich Plugin-Tests.
  5. """
  6. import asyncio
  7. import sys
  8. from pathlib import Path
  9. from typing import Any, AsyncGenerator
  10. from unittest.mock import AsyncMock, MagicMock
  11. import pytest
  12. # Projekt-Root zum Pfad hinzufügen
  13. PROJECT_ROOT = Path(__file__).parent
  14. if str(PROJECT_ROOT) not in sys.path:
  15. sys.path.insert(0, str(PROJECT_ROOT))
  16. # ============================================================================
  17. # Event Manager Fixtures
  18. # ============================================================================
  19. @pytest.fixture
  20. def event_manager():
  21. """Erstellt einen echten EventManager für Tests."""
  22. from trixy_core.events.eventmanager import EventManager
  23. mock_app = MagicMock()
  24. em = EventManager(mock_app)
  25. return em
  26. @pytest.fixture
  27. def mock_event_manager():
  28. """Erstellt einen Mock-EventManager für isolierte Tests."""
  29. em = MagicMock()
  30. em.emit = AsyncMock()
  31. em.trigger = AsyncMock()
  32. em.register = MagicMock()
  33. em.on = MagicMock(return_value=lambda f: f)
  34. return em
  35. # ============================================================================
  36. # Application Fixtures
  37. # ============================================================================
  38. @pytest.fixture
  39. def mock_application(mock_event_manager):
  40. """Erstellt eine Mock-Application für Plugin-Tests."""
  41. app = MagicMock()
  42. app.events = mock_event_manager
  43. app.debug = True
  44. app.base_path = PROJECT_ROOT
  45. # Extension Points Mock
  46. app.extension_points = {}
  47. return app
  48. @pytest.fixture
  49. def mock_plugin_path(tmp_path):
  50. """Erstellt einen temporären Plugin-Pfad."""
  51. plugin_dir = tmp_path / "test_plugin"
  52. plugin_dir.mkdir()
  53. (plugin_dir / "models").mkdir()
  54. return plugin_dir
  55. # ============================================================================
  56. # Audio Test Data Fixtures
  57. # ============================================================================
  58. @pytest.fixture
  59. def sample_audio_16khz() -> bytes:
  60. """Generiert Sample-Audio-Daten (16kHz, 16-bit, mono, 1 Sekunde Stille)."""
  61. import struct
  62. sample_rate = 16000
  63. duration = 1 # 1 Sekunde
  64. num_samples = sample_rate * duration
  65. # Stille generieren (alle Nullen)
  66. audio_data = struct.pack(f"<{num_samples}h", *([0] * num_samples))
  67. return audio_data
  68. @pytest.fixture
  69. def sample_audio_22khz() -> bytes:
  70. """Generiert Sample-Audio-Daten (22.05kHz, 16-bit, mono, 1 Sekunde Stille)."""
  71. import struct
  72. sample_rate = 22050
  73. duration = 1
  74. num_samples = sample_rate * duration
  75. audio_data = struct.pack(f"<{num_samples}h", *([0] * num_samples))
  76. return audio_data
  77. @pytest.fixture
  78. def sample_audio_with_tone() -> bytes:
  79. """Generiert Sample-Audio mit einem 440Hz Ton."""
  80. import math
  81. import struct
  82. sample_rate = 16000
  83. duration = 0.5 # 0.5 Sekunden
  84. frequency = 440 # Hz
  85. amplitude = 16000 # Lautstärke
  86. num_samples = int(sample_rate * duration)
  87. samples = []
  88. for i in range(num_samples):
  89. t = i / sample_rate
  90. sample = int(amplitude * math.sin(2 * math.pi * frequency * t))
  91. samples.append(sample)
  92. audio_data = struct.pack(f"<{num_samples}h", *samples)
  93. return audio_data
  94. # ============================================================================
  95. # Config Fixtures
  96. # ============================================================================
  97. @pytest.fixture
  98. def default_tts_config():
  99. """Standard TTS-Konfiguration für Tests."""
  100. return {
  101. "name": "Test TTS",
  102. "enabled": True,
  103. "language": "de-DE",
  104. "auto_download": False, # Keine echten Downloads in Tests
  105. }
  106. @pytest.fixture
  107. def default_stt_config():
  108. """Standard STT-Konfiguration für Tests."""
  109. return {
  110. "name": "Test STT",
  111. "enabled": True,
  112. "language": "de-DE",
  113. "auto_download": False, # Keine echten Downloads in Tests
  114. }
  115. # ============================================================================
  116. # Plugin Test Helpers
  117. # ============================================================================
  118. class PluginTestHelper:
  119. """Hilfsklasse für Plugin-Tests."""
  120. def __init__(self, application, plugin_path: Path, config: dict):
  121. self.application = application
  122. self.plugin_path = plugin_path
  123. self.config = config
  124. self.emitted_events: list[tuple[str, dict]] = []
  125. # Event-Emit tracken
  126. original_emit = application.events.emit
  127. async def tracking_emit(event_name, data=None):
  128. self.emitted_events.append((event_name, data or {}))
  129. return await original_emit(event_name, data)
  130. application.events.emit = AsyncMock(side_effect=tracking_emit)
  131. def get_emitted_events(self, event_name: str) -> list[dict]:
  132. """Gibt alle emittierten Events eines Typs zurück."""
  133. return [data for name, data in self.emitted_events if name == event_name]
  134. def assert_event_emitted(self, event_name: str, count: int = 1):
  135. """Prüft, ob ein Event emittiert wurde."""
  136. events = self.get_emitted_events(event_name)
  137. assert len(events) >= count, f"Expected {count} '{event_name}' events, got {len(events)}"
  138. def clear_events(self):
  139. """Löscht die Event-Historie."""
  140. self.emitted_events.clear()
  141. @pytest.fixture
  142. def plugin_test_helper(mock_application, mock_plugin_path, default_tts_config):
  143. """Erstellt einen PluginTestHelper."""
  144. return PluginTestHelper(mock_application, mock_plugin_path, default_tts_config)
  145. # ============================================================================
  146. # Async Helpers
  147. # ============================================================================
  148. @pytest.fixture
  149. def event_loop():
  150. """Erstellt eine neue Event-Loop für jeden Test."""
  151. loop = asyncio.new_event_loop()
  152. yield loop
  153. loop.close()
  154. # ============================================================================
  155. # Skip Markers für bedingte Tests
  156. # ============================================================================
  157. def pytest_configure(config):
  158. """Registriert benutzerdefinierte Marker."""
  159. config.addinivalue_line(
  160. "markers", "requires_piper: Test benötigt piper-tts Installation"
  161. )
  162. config.addinivalue_line(
  163. "markers", "requires_vosk: Test benötigt vosk Installation"
  164. )
  165. config.addinivalue_line(
  166. "markers", "requires_whisper: Test benötigt openai-whisper Installation"
  167. )
  168. config.addinivalue_line(
  169. "markers", "requires_coqui: Test benötigt TTS (Coqui) Installation"
  170. )
  171. config.addinivalue_line(
  172. "markers", "requires_google_tts: Test benötigt google-cloud-texttospeech Installation"
  173. )
  174. config.addinivalue_line(
  175. "markers", "requires_deepspeech: Test benötigt deepspeech Installation"
  176. )
  177. config.addinivalue_line(
  178. "markers", "integration: Integrationstests (können langsamer sein)"
  179. )
  180. config.addinivalue_line(
  181. "markers", "slow: Langsame Tests"
  182. )
  183. # Skip-Decorator Factories
  184. def skip_if_no_module(module_name: str, pip_name: str = None):
  185. """Decorator zum Überspringen wenn ein Modul nicht installiert ist."""
  186. pip_name = pip_name or module_name
  187. try:
  188. __import__(module_name)
  189. return pytest.mark.skipif(False, reason="")
  190. except ImportError:
  191. return pytest.mark.skip(reason=f"{pip_name} nicht installiert")
  192. # Vordefinierte Skip-Marker
  193. requires_piper = skip_if_no_module("piper", "piper-tts")
  194. requires_vosk = skip_if_no_module("vosk")
  195. requires_whisper = skip_if_no_module("whisper", "openai-whisper")
  196. requires_coqui = skip_if_no_module("TTS")
  197. requires_google_tts = skip_if_no_module("google.cloud.texttospeech", "google-cloud-texttospeech")
  198. requires_deepspeech = skip_if_no_module("deepspeech")