mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 21:50:28 +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.
184 lines
6.4 KiB
Python
184 lines
6.4 KiB
Python
"""Contract tests for capability interfaces.
|
|
|
|
These tests verify that capability interfaces satisfy the architectural requirements:
|
|
- VoiceLister: lists voices for a source
|
|
- PreviewGenerator: generates preview audio
|
|
- StreamingSynthesizer: yields audio chunks
|
|
- CancelableSession: cancels in-progress synthesis
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from abogen.tts_plugin.capabilities import (
|
|
CancelableSession,
|
|
PreviewGenerator,
|
|
StreamingSynthesizer,
|
|
VoiceLister,
|
|
)
|
|
from abogen.tts_plugin.errors import CancelledError, EngineError
|
|
from abogen.tts_plugin.manifest import VoiceManifest
|
|
from abogen.tts_plugin.types import (
|
|
AudioFormat,
|
|
Duration,
|
|
ParameterValues,
|
|
SynthesisRequest,
|
|
SynthesizedAudio,
|
|
VoiceSelection,
|
|
)
|
|
|
|
from .conftest import FakeCancelableSession, FakeStreamingSession, FakeVoiceListerEngine
|
|
|
|
|
|
class TestVoiceListerProtocolContract:
|
|
"""Contract tests for VoiceLister protocol."""
|
|
|
|
def test_voice_lister_is_protocol(self) -> None:
|
|
assert hasattr(VoiceLister, "__protocol_attrs__")
|
|
|
|
def test_voice_lister_satisfied_by_engine(self) -> None:
|
|
engine = FakeVoiceListerEngine()
|
|
assert isinstance(engine, VoiceLister)
|
|
|
|
def test_list_voices_returns_list(self) -> None:
|
|
engine = FakeVoiceListerEngine()
|
|
voices = engine.listVoices("builtin")
|
|
assert isinstance(voices, list)
|
|
|
|
def test_list_voices_returns_voice_manifests(self) -> None:
|
|
engine = FakeVoiceListerEngine()
|
|
voices = engine.listVoices("builtin")
|
|
for voice in voices:
|
|
assert isinstance(voice, VoiceManifest)
|
|
|
|
def test_list_voices_has_required_fields(self) -> None:
|
|
engine = FakeVoiceListerEngine()
|
|
voices = engine.listVoices("builtin")
|
|
for voice in voices:
|
|
assert hasattr(voice, "id")
|
|
assert hasattr(voice, "name")
|
|
assert hasattr(voice, "tags")
|
|
|
|
|
|
class TestPreviewGeneratorProtocolContract:
|
|
"""Contract tests for PreviewGenerator protocol."""
|
|
|
|
def test_preview_generator_is_protocol(self) -> None:
|
|
assert hasattr(PreviewGenerator, "__protocol_attrs__")
|
|
|
|
def test_preview_generator_satisfied_by_engine(self) -> None:
|
|
from .conftest import FakePreviewEngine
|
|
|
|
engine = FakePreviewEngine()
|
|
assert isinstance(engine, PreviewGenerator)
|
|
|
|
def test_generate_preview_returns_synthesized_audio(self) -> None:
|
|
from .conftest import FakePreviewEngine
|
|
|
|
engine = FakePreviewEngine()
|
|
voice = VoiceSelection(source="builtin", key="af_nova")
|
|
result = engine.generatePreview(voice, "Hello")
|
|
assert isinstance(result, SynthesizedAudio)
|
|
|
|
def test_generate_preview_has_valid_data(self) -> None:
|
|
from .conftest import FakePreviewEngine
|
|
|
|
engine = FakePreviewEngine()
|
|
voice = VoiceSelection(source="builtin", key="af_nova")
|
|
result = engine.generatePreview(voice, "Hello")
|
|
assert isinstance(result.data, bytes)
|
|
assert len(result.data) > 0
|
|
|
|
|
|
class TestStreamingSynthesizerProtocolContract:
|
|
"""Contract tests for StreamingSynthesizer protocol."""
|
|
|
|
def test_streaming_synthesizer_is_protocol(self) -> None:
|
|
assert hasattr(StreamingSynthesizer, "__protocol_attrs__")
|
|
|
|
def test_streaming_session_satisfies_protocol(self) -> None:
|
|
session = FakeStreamingSession()
|
|
assert isinstance(session, StreamingSynthesizer)
|
|
|
|
def test_synthesize_stream_yields_bytes(self) -> None:
|
|
session = FakeStreamingSession()
|
|
request = SynthesisRequest(
|
|
text="Hello",
|
|
voice=VoiceSelection(source="builtin", key="af_nova"),
|
|
parameters=ParameterValues(),
|
|
format=AudioFormat(mime="audio/wav", extension="wav"),
|
|
)
|
|
chunks = list(session.synthesizeStream(request))
|
|
assert len(chunks) > 0
|
|
for chunk in chunks:
|
|
assert isinstance(chunk, bytes)
|
|
|
|
def test_streaming_iterator_exhaustion(self) -> None:
|
|
"""Architecture spec: Iterator exhaustion = synthesis complete."""
|
|
session = FakeStreamingSession()
|
|
request = SynthesisRequest(
|
|
text="Hello",
|
|
voice=VoiceSelection(source="builtin", key="af_nova"),
|
|
parameters=ParameterValues(),
|
|
format=AudioFormat(mime="audio/wav", extension="wav"),
|
|
)
|
|
chunks = list(session.synthesizeStream(request))
|
|
assert len(chunks) == 3
|
|
|
|
def test_streaming_after_dispose_raises(self) -> None:
|
|
"""Architecture spec: After dispose(), methods raise EngineError."""
|
|
session = FakeStreamingSession()
|
|
session.dispose()
|
|
request = SynthesisRequest(
|
|
text="Hello",
|
|
voice=VoiceSelection(source="builtin", key="af_nova"),
|
|
parameters=ParameterValues(),
|
|
format=AudioFormat(mime="audio/wav", extension="wav"),
|
|
)
|
|
with pytest.raises(EngineError):
|
|
list(session.synthesizeStream(request))
|
|
|
|
|
|
class TestCancelableSessionProtocolContract:
|
|
"""Contract tests for CancelableSession protocol."""
|
|
|
|
def test_cancelable_session_is_protocol(self) -> None:
|
|
assert hasattr(CancelableSession, "__protocol_attrs__")
|
|
|
|
def test_cancelable_session_satisfies_protocol(self) -> None:
|
|
session = FakeCancelableSession()
|
|
assert isinstance(session, CancelableSession)
|
|
|
|
def test_cancel_causes_synthesize_to_raise_cancelled(self) -> None:
|
|
"""Architecture spec: cancel() causes synthesize() to raise CancelledError."""
|
|
session = FakeCancelableSession()
|
|
request = SynthesisRequest(
|
|
text="Hello",
|
|
voice=VoiceSelection(source="builtin", key="af_nova"),
|
|
parameters=ParameterValues(),
|
|
format=AudioFormat(mime="audio/wav", extension="wav"),
|
|
)
|
|
|
|
# Cancel
|
|
session.cancel()
|
|
|
|
# synthesize should raise CancelledError
|
|
with pytest.raises(CancelledError):
|
|
session.synthesize(request)
|
|
|
|
def test_cancel_after_dispose_raises(self) -> None:
|
|
"""Architecture spec: cancel() raises EngineError if called after dispose()."""
|
|
session = FakeCancelableSession()
|
|
session.dispose()
|
|
with pytest.raises(EngineError):
|
|
session.cancel()
|
|
|
|
def test_session_usable_after_cancel(self) -> None:
|
|
"""Architecture spec: EngineSession remains usable after cancellation."""
|
|
session = FakeCancelableSession()
|
|
|
|
# Cancel
|
|
session.cancel()
|
|
|
|
# Dispose and create new session for synthesis
|
|
session.dispose()
|