Spaces:
Sleeping
Sleeping
| """Unit tests for ISpeechSynthesisService interface contract.""" | |
| import pytest | |
| from abc import ABC | |
| from unittest.mock import Mock | |
| from typing import Iterator | |
| from src.domain.interfaces.speech_synthesis import ISpeechSynthesisService | |
| from src.domain.models.speech_synthesis_request import SpeechSynthesisRequest | |
| from src.domain.models.audio_content import AudioContent | |
| from src.domain.models.audio_chunk import AudioChunk | |
| from src.domain.models.text_content import TextContent | |
| from src.domain.models.voice_settings import VoiceSettings | |
| class TestISpeechSynthesisService: | |
| """Test cases for ISpeechSynthesisService interface contract.""" | |
| def test_interface_is_abstract(self): | |
| """Test that ISpeechSynthesisService is an abstract base class.""" | |
| assert issubclass(ISpeechSynthesisService, ABC) | |
| # Should not be able to instantiate directly | |
| with pytest.raises(TypeError): | |
| ISpeechSynthesisService() # type: ignore | |
| def test_interface_has_required_methods(self): | |
| """Test that interface defines the required abstract methods.""" | |
| # Check that both methods exist and are abstract | |
| assert hasattr(ISpeechSynthesisService, 'synthesize') | |
| assert hasattr(ISpeechSynthesisService, 'synthesize_stream') | |
| assert getattr(ISpeechSynthesisService.synthesize, '__isabstractmethod__', False) | |
| assert getattr(ISpeechSynthesisService.synthesize_stream, '__isabstractmethod__', False) | |
| def test_synthesize_method_signature(self): | |
| """Test that the synthesize method has the correct signature.""" | |
| import inspect | |
| method = ISpeechSynthesisService.synthesize | |
| signature = inspect.signature(method) | |
| # Check parameter names | |
| params = list(signature.parameters.keys()) | |
| expected_params = ['self', 'request'] | |
| assert params == expected_params | |
| # Check return annotation | |
| assert signature.return_annotation == "'AudioContent'" | |
| def test_synthesize_stream_method_signature(self): | |
| """Test that the synthesize_stream method has the correct signature.""" | |
| import inspect | |
| method = ISpeechSynthesisService.synthesize_stream | |
| signature = inspect.signature(method) | |
| # Check parameter names | |
| params = list(signature.parameters.keys()) | |
| expected_params = ['self', 'request'] | |
| assert params == expected_params | |
| # Check return annotation | |
| assert signature.return_annotation == "Iterator['AudioChunk']" | |
| def test_concrete_implementation_must_implement_methods(self): | |
| """Test that concrete implementations must implement both abstract methods.""" | |
| class IncompleteImplementation(ISpeechSynthesisService): | |
| def synthesize(self, request): | |
| return AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0) | |
| # Missing synthesize_stream method | |
| # Should not be able to instantiate without implementing all abstract methods | |
| with pytest.raises(TypeError, match="Can't instantiate abstract class"): | |
| IncompleteImplementation() # type: ignore | |
| def test_concrete_implementation_with_both_methods(self): | |
| """Test that concrete implementation with both methods can be instantiated.""" | |
| class ConcreteImplementation(ISpeechSynthesisService): | |
| def synthesize(self, request): | |
| return AudioContent(data=b"synthesized", format="wav", sample_rate=22050, duration=1.0) | |
| def synthesize_stream(self, request): | |
| yield AudioChunk(data=b"chunk1", format="wav", sample_rate=22050, chunk_index=0, is_final=True) | |
| # Should be able to instantiate | |
| implementation = ConcreteImplementation() | |
| assert isinstance(implementation, ISpeechSynthesisService) | |
| def test_synthesize_method_contract_with_mock(self): | |
| """Test the synthesize method contract using a mock implementation.""" | |
| class MockImplementation(ISpeechSynthesisService): | |
| def __init__(self): | |
| self.mock_synthesize = Mock() | |
| self.mock_synthesize_stream = Mock() | |
| def synthesize(self, request): | |
| return self.mock_synthesize(request) | |
| def synthesize_stream(self, request): | |
| return self.mock_synthesize_stream(request) | |
| # Create test data | |
| text_content = TextContent(text="Hello world", language="en") | |
| voice_settings = VoiceSettings(voice_id="test_voice", speed=1.0, language="en") | |
| request = SpeechSynthesisRequest( | |
| text_content=text_content, | |
| voice_settings=voice_settings | |
| ) | |
| expected_result = AudioContent( | |
| data=b"synthesized_audio", | |
| format="wav", | |
| sample_rate=22050, | |
| duration=2.0 | |
| ) | |
| # Setup mock | |
| implementation = MockImplementation() | |
| implementation.mock_synthesize.return_value = expected_result | |
| # Call method | |
| result = implementation.synthesize(request) | |
| # Verify call and result | |
| implementation.mock_synthesize.assert_called_once_with(request) | |
| assert result == expected_result | |
| def test_synthesize_stream_method_contract_with_mock(self): | |
| """Test the synthesize_stream method contract using a mock implementation.""" | |
| class MockImplementation(ISpeechSynthesisService): | |
| def __init__(self): | |
| self.mock_synthesize = Mock() | |
| self.mock_synthesize_stream = Mock() | |
| def synthesize(self, request): | |
| return self.mock_synthesize(request) | |
| def synthesize_stream(self, request): | |
| return self.mock_synthesize_stream(request) | |
| # Create test data | |
| text_content = TextContent(text="Hello world", language="en") | |
| voice_settings = VoiceSettings(voice_id="test_voice", speed=1.0, language="en") | |
| request = SpeechSynthesisRequest( | |
| text_content=text_content, | |
| voice_settings=voice_settings | |
| ) | |
| expected_chunks = [ | |
| AudioChunk(data=b"chunk1", format="wav", sample_rate=22050, chunk_index=0), | |
| AudioChunk(data=b"chunk2", format="wav", sample_rate=22050, chunk_index=1, is_final=True) | |
| ] | |
| # Setup mock | |
| implementation = MockImplementation() | |
| implementation.mock_synthesize_stream.return_value = iter(expected_chunks) | |
| # Call method | |
| result = implementation.synthesize_stream(request) | |
| # Verify call and result | |
| implementation.mock_synthesize_stream.assert_called_once_with(request) | |
| chunks = list(result) | |
| assert chunks == expected_chunks | |
| def test_interface_docstring_requirements(self): | |
| """Test that the interface methods have proper documentation.""" | |
| synthesize_method = ISpeechSynthesisService.synthesize | |
| stream_method = ISpeechSynthesisService.synthesize_stream | |
| # Check synthesize method docstring | |
| assert synthesize_method.__doc__ is not None | |
| synthesize_doc = synthesize_method.__doc__ | |
| assert "Synthesize speech from text" in synthesize_doc | |
| assert "Args:" in synthesize_doc | |
| assert "Returns:" in synthesize_doc | |
| assert "Raises:" in synthesize_doc | |
| assert "SpeechSynthesisException" in synthesize_doc | |
| # Check synthesize_stream method docstring | |
| assert stream_method.__doc__ is not None | |
| stream_doc = stream_method.__doc__ | |
| assert "Synthesize speech from text as a stream" in stream_doc | |
| assert "Args:" in stream_doc | |
| assert "Returns:" in stream_doc | |
| assert "Iterator[AudioChunk]" in stream_doc | |
| assert "Raises:" in stream_doc | |
| assert "SpeechSynthesisException" in stream_doc | |
| def test_interface_type_hints(self): | |
| """Test that the interface uses proper type hints.""" | |
| synthesize_method = ISpeechSynthesisService.synthesize | |
| stream_method = ISpeechSynthesisService.synthesize_stream | |
| # Check synthesize method annotations | |
| synthesize_annotations = getattr(synthesize_method, '__annotations__', {}) | |
| assert 'request' in synthesize_annotations | |
| assert 'return' in synthesize_annotations | |
| assert synthesize_annotations['request'] == "'SpeechSynthesisRequest'" | |
| assert synthesize_annotations['return'] == "'AudioContent'" | |
| # Check synthesize_stream method annotations | |
| stream_annotations = getattr(stream_method, '__annotations__', {}) | |
| assert 'request' in stream_annotations | |
| assert 'return' in stream_annotations | |
| assert stream_annotations['request'] == "'SpeechSynthesisRequest'" | |
| assert stream_annotations['return'] == "Iterator['AudioChunk']" | |
| def test_multiple_implementations_possible(self): | |
| """Test that multiple implementations of the interface are possible.""" | |
| class KokoroImplementation(ISpeechSynthesisService): | |
| def synthesize(self, request): | |
| return AudioContent(data=b"kokoro_audio", format="wav", sample_rate=22050, duration=1.0) | |
| def synthesize_stream(self, request): | |
| yield AudioChunk(data=b"kokoro_chunk", format="wav", sample_rate=22050, chunk_index=0, is_final=True) | |
| class DiaImplementation(ISpeechSynthesisService): | |
| def synthesize(self, request): | |
| return AudioContent(data=b"dia_audio", format="wav", sample_rate=22050, duration=1.0) | |
| def synthesize_stream(self, request): | |
| yield AudioChunk(data=b"dia_chunk", format="wav", sample_rate=22050, chunk_index=0, is_final=True) | |
| kokoro = KokoroImplementation() | |
| dia = DiaImplementation() | |
| assert isinstance(kokoro, ISpeechSynthesisService) | |
| assert isinstance(dia, ISpeechSynthesisService) | |
| assert type(kokoro) != type(dia) | |
| def test_interface_methods_can_be_called_polymorphically(self): | |
| """Test that interface methods can be called polymorphically.""" | |
| class TestImplementation(ISpeechSynthesisService): | |
| def __init__(self, audio_data, chunk_data): | |
| self.audio_data = audio_data | |
| self.chunk_data = chunk_data | |
| def synthesize(self, request): | |
| return AudioContent(data=self.audio_data, format="wav", sample_rate=22050, duration=1.0) | |
| def synthesize_stream(self, request): | |
| yield AudioChunk(data=self.chunk_data, format="wav", sample_rate=22050, chunk_index=0, is_final=True) | |
| # Create different implementations | |
| implementations = [ | |
| TestImplementation(b"audio1", b"chunk1"), | |
| TestImplementation(b"audio2", b"chunk2") | |
| ] | |
| # Test polymorphic usage | |
| text_content = TextContent(text="test", language="en") | |
| voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en") | |
| request = SpeechSynthesisRequest(text_content=text_content, voice_settings=voice_settings) | |
| # Test synthesize method | |
| audio_results = [] | |
| for impl in implementations: | |
| result = impl.synthesize(request) | |
| audio_results.append(result.data) | |
| assert audio_results == [b"audio1", b"audio2"] | |
| # Test synthesize_stream method | |
| chunk_results = [] | |
| for impl in implementations: | |
| chunks = list(impl.synthesize_stream(request)) | |
| chunk_results.append(chunks[0].data) | |
| assert chunk_results == [b"chunk1", b"chunk2"] | |
| def test_interface_inheritance_chain(self): | |
| """Test the inheritance chain of the interface.""" | |
| # Check that it inherits from ABC | |
| assert ABC in ISpeechSynthesisService.__mro__ | |
| # Check that it's at the right position in MRO | |
| mro = ISpeechSynthesisService.__mro__ | |
| assert mro[0] == ISpeechSynthesisService | |
| assert ABC in mro | |
| def test_stream_method_returns_iterator(self): | |
| """Test that synthesize_stream returns an iterator.""" | |
| class StreamingImplementation(ISpeechSynthesisService): | |
| def synthesize(self, request): | |
| return AudioContent(data=b"audio", format="wav", sample_rate=22050, duration=1.0) | |
| def synthesize_stream(self, request): | |
| for i in range(3): | |
| yield AudioChunk( | |
| data=f"chunk{i}".encode(), | |
| format="wav", | |
| sample_rate=22050, | |
| chunk_index=i, | |
| is_final=(i == 2) | |
| ) | |
| impl = StreamingImplementation() | |
| text_content = TextContent(text="test", language="en") | |
| voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en") | |
| request = SpeechSynthesisRequest(text_content=text_content, voice_settings=voice_settings) | |
| # Get the iterator | |
| stream = impl.synthesize_stream(request) | |
| # Verify it's an iterator | |
| assert hasattr(stream, '__iter__') | |
| assert hasattr(stream, '__next__') | |
| # Verify we can iterate through chunks | |
| chunks = list(stream) | |
| assert len(chunks) == 3 | |
| for i, chunk in enumerate(chunks): | |
| assert chunk.data == f"chunk{i}".encode() | |
| assert chunk.chunk_index == i | |
| assert chunk.is_final == (i == 2) | |
| def test_implementation_can_handle_different_formats(self): | |
| """Test that implementations can handle different output formats.""" | |
| class MultiFormatImplementation(ISpeechSynthesisService): | |
| def synthesize(self, request): | |
| format_data = { | |
| "wav": b"wav_audio_data", | |
| "mp3": b"mp3_audio_data", | |
| "flac": b"flac_audio_data", | |
| "ogg": b"ogg_audio_data" | |
| } | |
| audio_data = format_data.get(request.output_format, b"default_audio_data") | |
| return AudioContent( | |
| data=audio_data, | |
| format=request.output_format, | |
| sample_rate=request.effective_sample_rate, | |
| duration=1.0 | |
| ) | |
| def synthesize_stream(self, request): | |
| yield AudioChunk( | |
| data=f"{request.output_format}_chunk".encode(), | |
| format=request.output_format, | |
| sample_rate=request.effective_sample_rate, | |
| chunk_index=0, | |
| is_final=True | |
| ) | |
| impl = MultiFormatImplementation() | |
| text_content = TextContent(text="test", language="en") | |
| voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en") | |
| # Test different formats | |
| formats = ["wav", "mp3", "flac", "ogg"] | |
| for fmt in formats: | |
| request = SpeechSynthesisRequest( | |
| text_content=text_content, | |
| voice_settings=voice_settings, | |
| output_format=fmt | |
| ) | |
| # Test synthesize | |
| audio = impl.synthesize(request) | |
| assert audio.format == fmt | |
| assert audio.data == f"{fmt}_audio_data".encode() | |
| # Test synthesize_stream | |
| chunks = list(impl.synthesize_stream(request)) | |
| assert len(chunks) == 1 | |
| assert chunks[0].format == fmt | |
| assert chunks[0].data == f"{fmt}_chunk".encode() |