"""Shared fixtures and stubs for contract tests. This module provides minimal stub implementations that satisfy the public API for testing purposes. These stubs do NOT contain real business logic. """ from __future__ import annotations import logging from pathlib import Path from typing import Iterator import pytest from abogen.tts_plugin.engine import Engine, EngineSession from abogen.tts_plugin.host_context import HostContext from abogen.tts_plugin.types import ( AudioFormat, Duration, EngineConfig, ParameterValues, SynthesisRequest, SynthesizedAudio, VoiceSelection, ) class FakeHttpClient: """Stub HTTP client that satisfies the HttpClient protocol.""" def get(self, url: str, **kwargs: object) -> object: return None def post(self, url: str, **kwargs: object) -> object: return None class FakeEngineSession: """Stub EngineSession for testing protocol compliance.""" def __init__(self) -> None: self._disposed = False def synthesize(self, request: SynthesisRequest) -> SynthesizedAudio: if self._disposed: from abogen.tts_plugin.errors import EngineError raise EngineError("Session disposed") return SynthesizedAudio( data=b"\x00" * 100, format=AudioFormat(mime="audio/wav", extension="wav"), duration=Duration(seconds=1.0), ) def dispose(self) -> None: self._disposed = True class FakeStreamingSession: """Stub EngineSession with StreamingSynthesizer capability.""" def __init__(self) -> None: self._disposed = False def synthesize(self, request: SynthesisRequest) -> SynthesizedAudio: if self._disposed: from abogen.tts_plugin.errors import EngineError raise EngineError("Session disposed") return SynthesizedAudio( data=b"\x00" * 100, format=AudioFormat(mime="audio/wav", extension="wav"), duration=Duration(seconds=1.0), ) def synthesizeStream(self, request: SynthesisRequest) -> Iterator[bytes]: if self._disposed: from abogen.tts_plugin.errors import EngineError raise EngineError("Session disposed") for i in range(3): yield b"\x00" * 50 def dispose(self) -> None: self._disposed = True class FakeCancelableSession: """Stub EngineSession with CancelableSession capability.""" def __init__(self) -> None: self._disposed = False self._cancelled = False def synthesize(self, request: SynthesisRequest) -> SynthesizedAudio: if self._disposed: from abogen.tts_plugin.errors import EngineError raise EngineError("Session disposed") if self._cancelled: from abogen.tts_plugin.errors import CancelledError raise CancelledError("Cancelled") return SynthesizedAudio( data=b"\x00" * 100, format=AudioFormat(mime="audio/wav", extension="wav"), duration=Duration(seconds=1.0), ) def cancel(self) -> None: if self._disposed: from abogen.tts_plugin.errors import EngineError raise EngineError("Session disposed") self._cancelled = True def dispose(self) -> None: self._disposed = True class FakeEngine: """Stub Engine for testing protocol compliance.""" def __init__(self, session_class: type = FakeEngineSession) -> None: self._disposed = False self._session_class = session_class def createSession(self) -> EngineSession: if self._disposed: from abogen.tts_plugin.errors import EngineError raise EngineError("Engine disposed") return self._session_class() def dispose(self) -> None: self._disposed = True class FakeVoiceListerEngine: """Stub Engine that also implements VoiceLister.""" def __init__(self) -> None: self._disposed = False def createSession(self) -> EngineSession: if self._disposed: from abogen.tts_plugin.errors import EngineError raise EngineError("Engine disposed") return FakeEngineSession() def listVoices(self, sourceId: str) -> list: from abogen.tts_plugin.manifest import VoiceManifest return [ VoiceManifest(id="voice1", name="Voice 1", tags=("en",)), VoiceManifest(id="voice2", name="Voice 2", tags=("es",)), ] def dispose(self) -> None: self._disposed = True class FakePreviewEngine: """Stub Engine that also implements PreviewGenerator.""" def __init__(self) -> None: self._disposed = False def createSession(self) -> EngineSession: if self._disposed: from abogen.tts_plugin.errors import EngineError raise EngineError("Engine disposed") return FakeEngineSession() def generatePreview(self, voice: VoiceSelection, text: str) -> SynthesizedAudio: return SynthesizedAudio( data=b"\x00" * 50, format=AudioFormat(mime="audio/wav", extension="wav"), duration=Duration(seconds=0.5), ) def dispose(self) -> None: self._disposed = True @pytest.fixture def fake_http_client() -> FakeHttpClient: return FakeHttpClient() @pytest.fixture def host_context(tmp_path: Path, fake_http_client: FakeHttpClient) -> HostContext: return HostContext( config_dir=tmp_path, logger=logging.getLogger("test"), http_client=fake_http_client, ) @pytest.fixture def fake_engine() -> FakeEngine: return FakeEngine() @pytest.fixture def fake_session() -> FakeEngineSession: return FakeEngineSession() @pytest.fixture def default_voice() -> VoiceSelection: return VoiceSelection(source="builtin", key="af_nova") @pytest.fixture def default_format() -> AudioFormat: return AudioFormat(mime="audio/wav", extension="wav") @pytest.fixture def default_request( default_voice: VoiceSelection, default_format: AudioFormat ) -> SynthesisRequest: return SynthesisRequest( text="Hello, world!", voice=default_voice, parameters=ParameterValues(values={}), format=default_format, )