"""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()