feat: add contract test suite for Plugin API

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.
This commit is contained in:
Artem Akymenko
2026-07-12 16:20:05 +03:00
parent 79b3d26f66
commit 0f568120f4
10 changed files with 1477 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
"""Contract tests for Engine protocol.
These tests verify that Engine implementations satisfy the architectural requirements:
- createSession() returns EngineSession
- dispose() is idempotent
- After dispose(), createSession() raises EngineError
- Engine is thread-safe for createSession()
"""
import pytest
from abogen.tts_plugin.engine import Engine, EngineSession
from abogen.tts_plugin.errors import EngineError
from .conftest import FakeEngine, FakeEngineSession
class TestEngineProtocolContract:
"""Contract tests for the Engine protocol itself."""
def test_engine_is_protocol(self) -> None:
assert hasattr(Engine, "__protocol_attrs__")
def test_engine_session_is_protocol(self) -> None:
assert hasattr(EngineSession, "__protocol_attrs__")
def test_fake_engine_satisfies_protocol(self) -> None:
engine = FakeEngine()
assert isinstance(engine, Engine)
def test_fake_session_satisfies_protocol(self) -> None:
session = FakeEngineSession()
assert isinstance(session, EngineSession)
class TestEngineCreateSessionContract:
"""Contract tests for Engine.createSession()."""
def test_create_session_returns_engine_session(self) -> None:
engine = FakeEngine()
session = engine.createSession()
assert isinstance(session, EngineSession)
def test_create_session_returns_new_instance(self) -> None:
engine = FakeEngine()
session1 = engine.createSession()
session2 = engine.createSession()
assert session1 is not session2
def test_create_session_ownership_transfers(self) -> None:
"""Architecture spec: Ownership transfers to caller."""
engine = FakeEngine()
session = engine.createSession()
assert isinstance(session, EngineSession)
class TestEngineDisposeContract:
"""Contract tests for Engine.dispose()."""
def test_dispose_is_idempotent(self) -> None:
"""Architecture spec: dispose() is idempotent."""
engine = FakeEngine()
engine.dispose()
engine.dispose() # Should not raise
def test_dispose_never_raises(self) -> None:
"""Architecture spec: dispose() never raises exceptions."""
engine = FakeEngine()
engine.dispose() # Should not raise
def test_create_session_after_dispose_raises(self) -> None:
"""Architecture spec: After dispose(), all methods except dispose() raise EngineError."""
engine = FakeEngine()
engine.dispose()
with pytest.raises(EngineError):
engine.createSession()
class TestEngineLifecycleContract:
"""Contract tests for Engine lifecycle."""
def test_full_lifecycle(self) -> None:
"""Test complete engine lifecycle: create -> sessions -> dispose."""
engine = FakeEngine()
# Create sessions
session1 = engine.createSession()
session2 = engine.createSession()
# Use sessions
assert isinstance(session1, EngineSession)
assert isinstance(session2, EngineSession)
# Dispose sessions
session1.dispose()
session2.dispose()
# Dispose engine
engine.dispose()
def test_engine_disposed_session_raises(self) -> None:
"""Architecture spec: After dispose(), all methods except dispose() raise EngineError."""
engine = FakeEngine()
engine.dispose()
with pytest.raises(EngineError):
engine.createSession()