"""Shared fixtures for backend tests. Heavy third-party modules (torch, sounddevice, numpy, RealtimeSTT, etc.) are stubbed at the *sys.modules* level before any backend code is imported. This lets the test suite run on a plain Python install without GPU drivers, audio hardware, or heavyweight ML libraries. """ import sys import types from pathlib import Path from unittest.mock import MagicMock import pytest # ── Project root on sys.path ──────────────────────────────────────── project_root = Path(__file__).resolve().parent.parent.parent if str(project_root) not in sys.path: sys.path.insert(0, str(project_root)) # ── Stub heavy modules before anything imports them ───────────────── def _stub(name: str) -> types.ModuleType: """Create a stub module and register it in sys.modules if not already present.""" if name in sys.modules: return sys.modules[name] mod = types.ModuleType(name) sys.modules[name] = mod return mod # numpy -- must behave like a real module for `import numpy as np` _np = _stub("numpy") _np.float32 = float _np.float64 = float _np.int16 = int _np.ndarray = MagicMock _np.array = MagicMock(return_value=MagicMock()) _np.zeros = MagicMock(return_value=MagicMock()) _np.frombuffer = MagicMock(return_value=MagicMock()) # torch + sub-modules _torch = _stub("torch") _torch.cuda = MagicMock() _torch.cuda.is_available = MagicMock(return_value=False) _torch.backends = MagicMock() _torch.backends.mps = MagicMock() _torch.backends.mps.is_available = MagicMock(return_value=False) _stub("torch.cuda") _stub("torch.backends") _stub("torch.backends.mps") _stub("torchaudio") # sounddevice _sd = _stub("sounddevice") _sd.query_devices = MagicMock(return_value=[]) # RealtimeSTT (imported by transcription_engine_realtime) _rtstt = _stub("RealtimeSTT") _rtstt.AudioToTextRecorder = MagicMock # faster_whisper (sometimes imported transitively) _stub("faster_whisper") # noisereduce _stub("noisereduce") # scipy _scipy = _stub("scipy") _stub("scipy.signal") _stub("scipy.io") _stub("scipy.io.wavfile") # webrtcvad _stub("webrtcvad") # openwakeword _stub("openwakeword") # pvporcupine _stub("pvporcupine") # PySide6 (should not be needed, but just in case) _stub("PySide6") _stub("PySide6.QtWidgets") _stub("PySide6.QtCore") _stub("PySide6.QtGui") # websockets _ws = _stub("websockets") _ws.connect = MagicMock # deepgram (cloud transcription) _stub("deepgram") # ── Fixtures ──────────────────────────────────────────────────────── @pytest.fixture def mock_config(tmp_path): """Return a Config object backed by a temporary file. This avoids touching the real user config at ~/.local-transcription/. """ config_file = tmp_path / "test_config.yaml" from client.config import Config config = Config(config_path=str(config_file)) return config @pytest.fixture def controller(mock_config): """Return an AppController wired to *mock_config* without starting heavy subsystems (engine, web server, device manager). The transcription engine, web server thread, and DeviceManager are all replaced with lightweight mocks so the test suite can run without a GPU, audio hardware, or a free network port. """ from unittest.mock import patch with patch("backend.app_controller.DeviceManager") as MockDM, \ patch("backend.app_controller.RealtimeTranscriptionEngine"), \ patch("backend.app_controller.DeepgramTranscriptionEngine"), \ patch("backend.app_controller.TranscriptionWebServer"), \ patch("backend.app_controller.ServerSyncClient"): # DeviceManager stub dm_instance = MagicMock() dm_instance.get_device_info.return_value = [("cpu", "CPU")] dm_instance.get_device_for_whisper.return_value = "cpu" MockDM.return_value = dm_instance from backend.app_controller import AppController ctrl = AppController(config=mock_config) yield ctrl @pytest.fixture def api_client(controller): """Return an httpx.AsyncClient speaking ASGI to the APIServer's FastAPI app. Usage in tests:: async def test_something(api_client): resp = await api_client.get("/api/status") assert resp.status_code == 200 """ from backend.api_server import APIServer import httpx api = APIServer(controller) transport = httpx.ASGITransport(app=api.app) client = httpx.AsyncClient(transport=transport, base_url="http://testserver") return client