mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Create reusable contract tests for TTS Plugin Architecture: - conftest.py: shared fixtures and stubs (FakeEngine, FakeSession, etc.) - test_types_contract.py: value object contracts (frozen, immutability, equality) - test_errors_contract.py: error hierarchy contracts - test_manifest_contract.py: manifest type contracts - test_engine_contract.py: Engine protocol contracts (lifecycle, dispose) - test_session_contract.py: EngineSession protocol contracts - test_capabilities_contract.py: capability protocol contracts - test_host_context_contract.py: HostContext contracts - test_plugin_contract.py: plugin contract (exports, create_engine) 124 tests covering all public API contracts.
232 lines
6.2 KiB
Python
232 lines
6.2 KiB
Python
"""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,
|
|
)
|