Spaces:
Build error
Build error
Michael Hu
commited on
Commit
·
48f8a08
1
Parent(s):
6613cd9
Create unit tests for domain layer
Browse files- tests/unit/domain/interfaces/__init__.py +1 -0
- tests/unit/domain/interfaces/test_audio_processing.py +212 -0
- tests/unit/domain/interfaces/test_speech_recognition.py +241 -0
- tests/unit/domain/interfaces/test_speech_synthesis.py +378 -0
- tests/unit/domain/interfaces/test_translation.py +303 -0
- tests/unit/domain/models/test_audio_chunk.py +322 -0
- tests/unit/domain/models/test_processing_result.py +411 -0
- tests/unit/domain/models/test_speech_synthesis_request.py +207 -233
- tests/unit/domain/models/test_translation_request.py +133 -162
- tests/unit/domain/test_exceptions.py +240 -0
tests/unit/domain/interfaces/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Domain interface tests
|
tests/unit/domain/interfaces/test_audio_processing.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for IAudioProcessingService interface contract."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from abc import ABC
|
| 5 |
+
from unittest.mock import Mock
|
| 6 |
+
from src.domain.interfaces.audio_processing import IAudioProcessingService
|
| 7 |
+
from src.domain.models.audio_content import AudioContent
|
| 8 |
+
from src.domain.models.voice_settings import VoiceSettings
|
| 9 |
+
from src.domain.models.processing_result import ProcessingResult
|
| 10 |
+
from src.domain.models.text_content import TextContent
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class TestIAudioProcessingService:
|
| 14 |
+
"""Test cases for IAudioProcessingService interface contract."""
|
| 15 |
+
|
| 16 |
+
def test_interface_is_abstract(self):
|
| 17 |
+
"""Test that IAudioProcessingService is an abstract base class."""
|
| 18 |
+
assert issubclass(IAudioProcessingService, ABC)
|
| 19 |
+
|
| 20 |
+
# Should not be able to instantiate directly
|
| 21 |
+
with pytest.raises(TypeError):
|
| 22 |
+
IAudioProcessingService() # type: ignore
|
| 23 |
+
|
| 24 |
+
def test_interface_has_required_method(self):
|
| 25 |
+
"""Test that interface defines the required abstract method."""
|
| 26 |
+
# Check that the method exists and is abstract
|
| 27 |
+
assert hasattr(IAudioProcessingService, 'process_audio_pipeline')
|
| 28 |
+
assert getattr(IAudioProcessingService.process_audio_pipeline, '__isabstractmethod__', False)
|
| 29 |
+
|
| 30 |
+
def test_method_signature(self):
|
| 31 |
+
"""Test that the method has the correct signature."""
|
| 32 |
+
import inspect
|
| 33 |
+
|
| 34 |
+
method = IAudioProcessingService.process_audio_pipeline
|
| 35 |
+
signature = inspect.signature(method)
|
| 36 |
+
|
| 37 |
+
# Check parameter names and types
|
| 38 |
+
params = list(signature.parameters.keys())
|
| 39 |
+
expected_params = ['self', 'audio', 'target_language', 'voice_settings']
|
| 40 |
+
|
| 41 |
+
assert params == expected_params
|
| 42 |
+
|
| 43 |
+
# Check return annotation
|
| 44 |
+
assert signature.return_annotation == "'ProcessingResult'"
|
| 45 |
+
|
| 46 |
+
def test_concrete_implementation_must_implement_method(self):
|
| 47 |
+
"""Test that concrete implementations must implement the abstract method."""
|
| 48 |
+
|
| 49 |
+
class IncompleteImplementation(IAudioProcessingService):
|
| 50 |
+
pass
|
| 51 |
+
|
| 52 |
+
# Should not be able to instantiate without implementing abstract method
|
| 53 |
+
with pytest.raises(TypeError, match="Can't instantiate abstract class"):
|
| 54 |
+
IncompleteImplementation() # type: ignore
|
| 55 |
+
|
| 56 |
+
def test_concrete_implementation_with_method(self):
|
| 57 |
+
"""Test that concrete implementation with method can be instantiated."""
|
| 58 |
+
|
| 59 |
+
class ConcreteImplementation(IAudioProcessingService):
|
| 60 |
+
def process_audio_pipeline(self, audio, target_language, voice_settings):
|
| 61 |
+
return ProcessingResult.success_result(
|
| 62 |
+
original_text=TextContent(text="test", language="en")
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Should be able to instantiate
|
| 66 |
+
implementation = ConcreteImplementation()
|
| 67 |
+
assert isinstance(implementation, IAudioProcessingService)
|
| 68 |
+
|
| 69 |
+
def test_method_contract_with_mock(self):
|
| 70 |
+
"""Test the method contract using a mock implementation."""
|
| 71 |
+
|
| 72 |
+
class MockImplementation(IAudioProcessingService):
|
| 73 |
+
def __init__(self):
|
| 74 |
+
self.mock_method = Mock()
|
| 75 |
+
|
| 76 |
+
def process_audio_pipeline(self, audio, target_language, voice_settings):
|
| 77 |
+
return self.mock_method(audio, target_language, voice_settings)
|
| 78 |
+
|
| 79 |
+
# Create test data
|
| 80 |
+
audio = AudioContent(
|
| 81 |
+
data=b"test_audio",
|
| 82 |
+
format="wav",
|
| 83 |
+
sample_rate=22050,
|
| 84 |
+
duration=5.0
|
| 85 |
+
)
|
| 86 |
+
voice_settings = VoiceSettings(
|
| 87 |
+
voice_id="test_voice",
|
| 88 |
+
speed=1.0,
|
| 89 |
+
language="es"
|
| 90 |
+
)
|
| 91 |
+
expected_result = ProcessingResult.success_result(
|
| 92 |
+
original_text=TextContent(text="test", language="en")
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# Setup mock
|
| 96 |
+
implementation = MockImplementation()
|
| 97 |
+
implementation.mock_method.return_value = expected_result
|
| 98 |
+
|
| 99 |
+
# Call method
|
| 100 |
+
result = implementation.process_audio_pipeline(
|
| 101 |
+
audio=audio,
|
| 102 |
+
target_language="es",
|
| 103 |
+
voice_settings=voice_settings
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
# Verify call and result
|
| 107 |
+
implementation.mock_method.assert_called_once_with(audio, "es", voice_settings)
|
| 108 |
+
assert result == expected_result
|
| 109 |
+
|
| 110 |
+
def test_interface_docstring_requirements(self):
|
| 111 |
+
"""Test that the interface method has proper documentation."""
|
| 112 |
+
method = IAudioProcessingService.process_audio_pipeline
|
| 113 |
+
|
| 114 |
+
assert method.__doc__ is not None
|
| 115 |
+
docstring = method.__doc__
|
| 116 |
+
|
| 117 |
+
# Check that docstring contains key information
|
| 118 |
+
assert "Process audio through the complete pipeline" in docstring
|
| 119 |
+
assert "STT -> Translation -> TTS" in docstring
|
| 120 |
+
assert "Args:" in docstring
|
| 121 |
+
assert "Returns:" in docstring
|
| 122 |
+
assert "Raises:" in docstring
|
| 123 |
+
assert "AudioProcessingException" in docstring
|
| 124 |
+
|
| 125 |
+
def test_interface_type_hints(self):
|
| 126 |
+
"""Test that the interface uses proper type hints."""
|
| 127 |
+
import inspect
|
| 128 |
+
from typing import get_type_hints
|
| 129 |
+
|
| 130 |
+
# Get type hints (this will resolve string annotations)
|
| 131 |
+
try:
|
| 132 |
+
hints = get_type_hints(IAudioProcessingService.process_audio_pipeline)
|
| 133 |
+
except NameError:
|
| 134 |
+
# If forward references can't be resolved, check annotations directly
|
| 135 |
+
method = IAudioProcessingService.process_audio_pipeline
|
| 136 |
+
annotations = getattr(method, '__annotations__', {})
|
| 137 |
+
|
| 138 |
+
assert 'audio' in annotations
|
| 139 |
+
assert 'target_language' in annotations
|
| 140 |
+
assert 'voice_settings' in annotations
|
| 141 |
+
assert 'return' in annotations
|
| 142 |
+
|
| 143 |
+
# Check that type annotations are strings (forward references)
|
| 144 |
+
assert annotations['audio'] == "'AudioContent'"
|
| 145 |
+
assert annotations['target_language'] == str
|
| 146 |
+
assert annotations['voice_settings'] == "'VoiceSettings'"
|
| 147 |
+
assert annotations['return'] == "'ProcessingResult'"
|
| 148 |
+
|
| 149 |
+
def test_multiple_implementations_possible(self):
|
| 150 |
+
"""Test that multiple implementations of the interface are possible."""
|
| 151 |
+
|
| 152 |
+
class Implementation1(IAudioProcessingService):
|
| 153 |
+
def process_audio_pipeline(self, audio, target_language, voice_settings):
|
| 154 |
+
return ProcessingResult.success_result(
|
| 155 |
+
original_text=TextContent(text="impl1", language="en")
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
class Implementation2(IAudioProcessingService):
|
| 159 |
+
def process_audio_pipeline(self, audio, target_language, voice_settings):
|
| 160 |
+
return ProcessingResult.failure_result(error_message="impl2 failed")
|
| 161 |
+
|
| 162 |
+
impl1 = Implementation1()
|
| 163 |
+
impl2 = Implementation2()
|
| 164 |
+
|
| 165 |
+
assert isinstance(impl1, IAudioProcessingService)
|
| 166 |
+
assert isinstance(impl2, IAudioProcessingService)
|
| 167 |
+
assert type(impl1) != type(impl2)
|
| 168 |
+
|
| 169 |
+
def test_interface_method_can_be_called_polymorphically(self):
|
| 170 |
+
"""Test that interface methods can be called polymorphically."""
|
| 171 |
+
|
| 172 |
+
class TestImplementation(IAudioProcessingService):
|
| 173 |
+
def __init__(self, result):
|
| 174 |
+
self.result = result
|
| 175 |
+
|
| 176 |
+
def process_audio_pipeline(self, audio, target_language, voice_settings):
|
| 177 |
+
return self.result
|
| 178 |
+
|
| 179 |
+
# Create different implementations
|
| 180 |
+
success_result = ProcessingResult.success_result(
|
| 181 |
+
original_text=TextContent(text="success", language="en")
|
| 182 |
+
)
|
| 183 |
+
failure_result = ProcessingResult.failure_result(error_message="failed")
|
| 184 |
+
|
| 185 |
+
implementations = [
|
| 186 |
+
TestImplementation(success_result),
|
| 187 |
+
TestImplementation(failure_result)
|
| 188 |
+
]
|
| 189 |
+
|
| 190 |
+
# Test polymorphic usage
|
| 191 |
+
audio = AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0)
|
| 192 |
+
voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en")
|
| 193 |
+
|
| 194 |
+
results = []
|
| 195 |
+
for impl in implementations:
|
| 196 |
+
# Can call the same method on different implementations
|
| 197 |
+
result = impl.process_audio_pipeline(audio, "en", voice_settings)
|
| 198 |
+
results.append(result)
|
| 199 |
+
|
| 200 |
+
assert len(results) == 2
|
| 201 |
+
assert results[0].success is True
|
| 202 |
+
assert results[1].success is False
|
| 203 |
+
|
| 204 |
+
def test_interface_inheritance_chain(self):
|
| 205 |
+
"""Test the inheritance chain of the interface."""
|
| 206 |
+
# Check that it inherits from ABC
|
| 207 |
+
assert ABC in IAudioProcessingService.__mro__
|
| 208 |
+
|
| 209 |
+
# Check that it's at the right position in MRO
|
| 210 |
+
mro = IAudioProcessingService.__mro__
|
| 211 |
+
assert mro[0] == IAudioProcessingService
|
| 212 |
+
assert ABC in mro
|
tests/unit/domain/interfaces/test_speech_recognition.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for ISpeechRecognitionService interface contract."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from abc import ABC
|
| 5 |
+
from unittest.mock import Mock
|
| 6 |
+
from src.domain.interfaces.speech_recognition import ISpeechRecognitionService
|
| 7 |
+
from src.domain.models.audio_content import AudioContent
|
| 8 |
+
from src.domain.models.text_content import TextContent
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TestISpeechRecognitionService:
|
| 12 |
+
"""Test cases for ISpeechRecognitionService interface contract."""
|
| 13 |
+
|
| 14 |
+
def test_interface_is_abstract(self):
|
| 15 |
+
"""Test that ISpeechRecognitionService is an abstract base class."""
|
| 16 |
+
assert issubclass(ISpeechRecognitionService, ABC)
|
| 17 |
+
|
| 18 |
+
# Should not be able to instantiate directly
|
| 19 |
+
with pytest.raises(TypeError):
|
| 20 |
+
ISpeechRecognitionService() # type: ignore
|
| 21 |
+
|
| 22 |
+
def test_interface_has_required_method(self):
|
| 23 |
+
"""Test that interface defines the required abstract method."""
|
| 24 |
+
# Check that the method exists and is abstract
|
| 25 |
+
assert hasattr(ISpeechRecognitionService, 'transcribe')
|
| 26 |
+
assert getattr(ISpeechRecognitionService.transcribe, '__isabstractmethod__', False)
|
| 27 |
+
|
| 28 |
+
def test_method_signature(self):
|
| 29 |
+
"""Test that the method has the correct signature."""
|
| 30 |
+
import inspect
|
| 31 |
+
|
| 32 |
+
method = ISpeechRecognitionService.transcribe
|
| 33 |
+
signature = inspect.signature(method)
|
| 34 |
+
|
| 35 |
+
# Check parameter names
|
| 36 |
+
params = list(signature.parameters.keys())
|
| 37 |
+
expected_params = ['self', 'audio', 'model']
|
| 38 |
+
|
| 39 |
+
assert params == expected_params
|
| 40 |
+
|
| 41 |
+
# Check return annotation
|
| 42 |
+
assert signature.return_annotation == "'TextContent'"
|
| 43 |
+
|
| 44 |
+
def test_concrete_implementation_must_implement_method(self):
|
| 45 |
+
"""Test that concrete implementations must implement the abstract method."""
|
| 46 |
+
|
| 47 |
+
class IncompleteImplementation(ISpeechRecognitionService):
|
| 48 |
+
pass
|
| 49 |
+
|
| 50 |
+
# Should not be able to instantiate without implementing abstract method
|
| 51 |
+
with pytest.raises(TypeError, match="Can't instantiate abstract class"):
|
| 52 |
+
IncompleteImplementation() # type: ignore
|
| 53 |
+
|
| 54 |
+
def test_concrete_implementation_with_method(self):
|
| 55 |
+
"""Test that concrete implementation with method can be instantiated."""
|
| 56 |
+
|
| 57 |
+
class ConcreteImplementation(ISpeechRecognitionService):
|
| 58 |
+
def transcribe(self, audio, model):
|
| 59 |
+
return TextContent(text="transcribed text", language="en")
|
| 60 |
+
|
| 61 |
+
# Should be able to instantiate
|
| 62 |
+
implementation = ConcreteImplementation()
|
| 63 |
+
assert isinstance(implementation, ISpeechRecognitionService)
|
| 64 |
+
|
| 65 |
+
def test_method_contract_with_mock(self):
|
| 66 |
+
"""Test the method contract using a mock implementation."""
|
| 67 |
+
|
| 68 |
+
class MockImplementation(ISpeechRecognitionService):
|
| 69 |
+
def __init__(self):
|
| 70 |
+
self.mock_method = Mock()
|
| 71 |
+
|
| 72 |
+
def transcribe(self, audio, model):
|
| 73 |
+
return self.mock_method(audio, model)
|
| 74 |
+
|
| 75 |
+
# Create test data
|
| 76 |
+
audio = AudioContent(
|
| 77 |
+
data=b"test_audio",
|
| 78 |
+
format="wav",
|
| 79 |
+
sample_rate=22050,
|
| 80 |
+
duration=5.0
|
| 81 |
+
)
|
| 82 |
+
model = "whisper-base"
|
| 83 |
+
expected_result = TextContent(text="Hello world", language="en")
|
| 84 |
+
|
| 85 |
+
# Setup mock
|
| 86 |
+
implementation = MockImplementation()
|
| 87 |
+
implementation.mock_method.return_value = expected_result
|
| 88 |
+
|
| 89 |
+
# Call method
|
| 90 |
+
result = implementation.transcribe(audio=audio, model=model)
|
| 91 |
+
|
| 92 |
+
# Verify call and result
|
| 93 |
+
implementation.mock_method.assert_called_once_with(audio, model)
|
| 94 |
+
assert result == expected_result
|
| 95 |
+
|
| 96 |
+
def test_interface_docstring_requirements(self):
|
| 97 |
+
"""Test that the interface method has proper documentation."""
|
| 98 |
+
method = ISpeechRecognitionService.transcribe
|
| 99 |
+
|
| 100 |
+
assert method.__doc__ is not None
|
| 101 |
+
docstring = method.__doc__
|
| 102 |
+
|
| 103 |
+
# Check that docstring contains key information
|
| 104 |
+
assert "Transcribe audio content to text" in docstring
|
| 105 |
+
assert "Args:" in docstring
|
| 106 |
+
assert "Returns:" in docstring
|
| 107 |
+
assert "Raises:" in docstring
|
| 108 |
+
assert "SpeechRecognitionException" in docstring
|
| 109 |
+
|
| 110 |
+
def test_interface_type_hints(self):
|
| 111 |
+
"""Test that the interface uses proper type hints."""
|
| 112 |
+
method = ISpeechRecognitionService.transcribe
|
| 113 |
+
annotations = getattr(method, '__annotations__', {})
|
| 114 |
+
|
| 115 |
+
assert 'audio' in annotations
|
| 116 |
+
assert 'model' in annotations
|
| 117 |
+
assert 'return' in annotations
|
| 118 |
+
|
| 119 |
+
# Check that type annotations are correct
|
| 120 |
+
assert annotations['audio'] == "'AudioContent'"
|
| 121 |
+
assert annotations['model'] == str
|
| 122 |
+
assert annotations['return'] == "'TextContent'"
|
| 123 |
+
|
| 124 |
+
def test_multiple_implementations_possible(self):
|
| 125 |
+
"""Test that multiple implementations of the interface are possible."""
|
| 126 |
+
|
| 127 |
+
class WhisperImplementation(ISpeechRecognitionService):
|
| 128 |
+
def transcribe(self, audio, model):
|
| 129 |
+
return TextContent(text="whisper transcription", language="en")
|
| 130 |
+
|
| 131 |
+
class ParakeetImplementation(ISpeechRecognitionService):
|
| 132 |
+
def transcribe(self, audio, model):
|
| 133 |
+
return TextContent(text="parakeet transcription", language="en")
|
| 134 |
+
|
| 135 |
+
whisper = WhisperImplementation()
|
| 136 |
+
parakeet = ParakeetImplementation()
|
| 137 |
+
|
| 138 |
+
assert isinstance(whisper, ISpeechRecognitionService)
|
| 139 |
+
assert isinstance(parakeet, ISpeechRecognitionService)
|
| 140 |
+
assert type(whisper) != type(parakeet)
|
| 141 |
+
|
| 142 |
+
def test_interface_method_can_be_called_polymorphically(self):
|
| 143 |
+
"""Test that interface methods can be called polymorphically."""
|
| 144 |
+
|
| 145 |
+
class TestImplementation(ISpeechRecognitionService):
|
| 146 |
+
def __init__(self, transcription_text):
|
| 147 |
+
self.transcription_text = transcription_text
|
| 148 |
+
|
| 149 |
+
def transcribe(self, audio, model):
|
| 150 |
+
return TextContent(text=self.transcription_text, language="en")
|
| 151 |
+
|
| 152 |
+
# Create different implementations
|
| 153 |
+
implementations = [
|
| 154 |
+
TestImplementation("first transcription"),
|
| 155 |
+
TestImplementation("second transcription")
|
| 156 |
+
]
|
| 157 |
+
|
| 158 |
+
# Test polymorphic usage
|
| 159 |
+
audio = AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0)
|
| 160 |
+
model = "test-model"
|
| 161 |
+
|
| 162 |
+
results = []
|
| 163 |
+
for impl in implementations:
|
| 164 |
+
# Can call the same method on different implementations
|
| 165 |
+
result = impl.transcribe(audio, model)
|
| 166 |
+
results.append(result.text)
|
| 167 |
+
|
| 168 |
+
assert results == ["first transcription", "second transcription"]
|
| 169 |
+
|
| 170 |
+
def test_interface_inheritance_chain(self):
|
| 171 |
+
"""Test the inheritance chain of the interface."""
|
| 172 |
+
# Check that it inherits from ABC
|
| 173 |
+
assert ABC in ISpeechRecognitionService.__mro__
|
| 174 |
+
|
| 175 |
+
# Check that it's at the right position in MRO
|
| 176 |
+
mro = ISpeechRecognitionService.__mro__
|
| 177 |
+
assert mro[0] == ISpeechRecognitionService
|
| 178 |
+
assert ABC in mro
|
| 179 |
+
|
| 180 |
+
def test_method_parameter_validation_in_implementation(self):
|
| 181 |
+
"""Test that implementations can validate parameters."""
|
| 182 |
+
|
| 183 |
+
class ValidatingImplementation(ISpeechRecognitionService):
|
| 184 |
+
def transcribe(self, audio, model):
|
| 185 |
+
if not isinstance(audio, AudioContent):
|
| 186 |
+
raise TypeError("audio must be AudioContent")
|
| 187 |
+
if not isinstance(model, str):
|
| 188 |
+
raise TypeError("model must be string")
|
| 189 |
+
if not model.strip():
|
| 190 |
+
raise ValueError("model cannot be empty")
|
| 191 |
+
|
| 192 |
+
return TextContent(text="validated transcription", language="en")
|
| 193 |
+
|
| 194 |
+
impl = ValidatingImplementation()
|
| 195 |
+
|
| 196 |
+
# Valid call should work
|
| 197 |
+
audio = AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0)
|
| 198 |
+
result = impl.transcribe(audio, "whisper-base")
|
| 199 |
+
assert result.text == "validated transcription"
|
| 200 |
+
|
| 201 |
+
# Invalid calls should raise appropriate errors
|
| 202 |
+
with pytest.raises(TypeError, match="audio must be AudioContent"):
|
| 203 |
+
impl.transcribe("not audio", "whisper-base") # type: ignore
|
| 204 |
+
|
| 205 |
+
with pytest.raises(TypeError, match="model must be string"):
|
| 206 |
+
impl.transcribe(audio, 123) # type: ignore
|
| 207 |
+
|
| 208 |
+
with pytest.raises(ValueError, match="model cannot be empty"):
|
| 209 |
+
impl.transcribe(audio, "")
|
| 210 |
+
|
| 211 |
+
def test_implementation_can_handle_different_models(self):
|
| 212 |
+
"""Test that implementations can handle different model types."""
|
| 213 |
+
|
| 214 |
+
class MultiModelImplementation(ISpeechRecognitionService):
|
| 215 |
+
def transcribe(self, audio, model):
|
| 216 |
+
model_responses = {
|
| 217 |
+
"whisper-tiny": "tiny transcription",
|
| 218 |
+
"whisper-base": "base transcription",
|
| 219 |
+
"whisper-large": "large transcription",
|
| 220 |
+
"parakeet": "parakeet transcription"
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
transcription = model_responses.get(model, "unknown model transcription")
|
| 224 |
+
return TextContent(text=transcription, language="en")
|
| 225 |
+
|
| 226 |
+
impl = MultiModelImplementation()
|
| 227 |
+
audio = AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0)
|
| 228 |
+
|
| 229 |
+
# Test different models
|
| 230 |
+
models_and_expected = [
|
| 231 |
+
("whisper-tiny", "tiny transcription"),
|
| 232 |
+
("whisper-base", "base transcription"),
|
| 233 |
+
("whisper-large", "large transcription"),
|
| 234 |
+
("parakeet", "parakeet transcription"),
|
| 235 |
+
("unknown-model", "unknown model transcription")
|
| 236 |
+
]
|
| 237 |
+
|
| 238 |
+
for model, expected_text in models_and_expected:
|
| 239 |
+
result = impl.transcribe(audio, model)
|
| 240 |
+
assert result.text == expected_text
|
| 241 |
+
assert result.language == "en"
|
tests/unit/domain/interfaces/test_speech_synthesis.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for ISpeechSynthesisService interface contract."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from abc import ABC
|
| 5 |
+
from unittest.mock import Mock
|
| 6 |
+
from typing import Iterator
|
| 7 |
+
from src.domain.interfaces.speech_synthesis import ISpeechSynthesisService
|
| 8 |
+
from src.domain.models.speech_synthesis_request import SpeechSynthesisRequest
|
| 9 |
+
from src.domain.models.audio_content import AudioContent
|
| 10 |
+
from src.domain.models.audio_chunk import AudioChunk
|
| 11 |
+
from src.domain.models.text_content import TextContent
|
| 12 |
+
from src.domain.models.voice_settings import VoiceSettings
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class TestISpeechSynthesisService:
|
| 16 |
+
"""Test cases for ISpeechSynthesisService interface contract."""
|
| 17 |
+
|
| 18 |
+
def test_interface_is_abstract(self):
|
| 19 |
+
"""Test that ISpeechSynthesisService is an abstract base class."""
|
| 20 |
+
assert issubclass(ISpeechSynthesisService, ABC)
|
| 21 |
+
|
| 22 |
+
# Should not be able to instantiate directly
|
| 23 |
+
with pytest.raises(TypeError):
|
| 24 |
+
ISpeechSynthesisService() # type: ignore
|
| 25 |
+
|
| 26 |
+
def test_interface_has_required_methods(self):
|
| 27 |
+
"""Test that interface defines the required abstract methods."""
|
| 28 |
+
# Check that both methods exist and are abstract
|
| 29 |
+
assert hasattr(ISpeechSynthesisService, 'synthesize')
|
| 30 |
+
assert hasattr(ISpeechSynthesisService, 'synthesize_stream')
|
| 31 |
+
|
| 32 |
+
assert getattr(ISpeechSynthesisService.synthesize, '__isabstractmethod__', False)
|
| 33 |
+
assert getattr(ISpeechSynthesisService.synthesize_stream, '__isabstractmethod__', False)
|
| 34 |
+
|
| 35 |
+
def test_synthesize_method_signature(self):
|
| 36 |
+
"""Test that the synthesize method has the correct signature."""
|
| 37 |
+
import inspect
|
| 38 |
+
|
| 39 |
+
method = ISpeechSynthesisService.synthesize
|
| 40 |
+
signature = inspect.signature(method)
|
| 41 |
+
|
| 42 |
+
# Check parameter names
|
| 43 |
+
params = list(signature.parameters.keys())
|
| 44 |
+
expected_params = ['self', 'request']
|
| 45 |
+
|
| 46 |
+
assert params == expected_params
|
| 47 |
+
|
| 48 |
+
# Check return annotation
|
| 49 |
+
assert signature.return_annotation == "'AudioContent'"
|
| 50 |
+
|
| 51 |
+
def test_synthesize_stream_method_signature(self):
|
| 52 |
+
"""Test that the synthesize_stream method has the correct signature."""
|
| 53 |
+
import inspect
|
| 54 |
+
|
| 55 |
+
method = ISpeechSynthesisService.synthesize_stream
|
| 56 |
+
signature = inspect.signature(method)
|
| 57 |
+
|
| 58 |
+
# Check parameter names
|
| 59 |
+
params = list(signature.parameters.keys())
|
| 60 |
+
expected_params = ['self', 'request']
|
| 61 |
+
|
| 62 |
+
assert params == expected_params
|
| 63 |
+
|
| 64 |
+
# Check return annotation
|
| 65 |
+
assert signature.return_annotation == "Iterator['AudioChunk']"
|
| 66 |
+
|
| 67 |
+
def test_concrete_implementation_must_implement_methods(self):
|
| 68 |
+
"""Test that concrete implementations must implement both abstract methods."""
|
| 69 |
+
|
| 70 |
+
class IncompleteImplementation(ISpeechSynthesisService):
|
| 71 |
+
def synthesize(self, request):
|
| 72 |
+
return AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0)
|
| 73 |
+
# Missing synthesize_stream method
|
| 74 |
+
|
| 75 |
+
# Should not be able to instantiate without implementing all abstract methods
|
| 76 |
+
with pytest.raises(TypeError, match="Can't instantiate abstract class"):
|
| 77 |
+
IncompleteImplementation() # type: ignore
|
| 78 |
+
|
| 79 |
+
def test_concrete_implementation_with_both_methods(self):
|
| 80 |
+
"""Test that concrete implementation with both methods can be instantiated."""
|
| 81 |
+
|
| 82 |
+
class ConcreteImplementation(ISpeechSynthesisService):
|
| 83 |
+
def synthesize(self, request):
|
| 84 |
+
return AudioContent(data=b"synthesized", format="wav", sample_rate=22050, duration=1.0)
|
| 85 |
+
|
| 86 |
+
def synthesize_stream(self, request):
|
| 87 |
+
yield AudioChunk(data=b"chunk1", format="wav", sample_rate=22050, chunk_index=0, is_final=True)
|
| 88 |
+
|
| 89 |
+
# Should be able to instantiate
|
| 90 |
+
implementation = ConcreteImplementation()
|
| 91 |
+
assert isinstance(implementation, ISpeechSynthesisService)
|
| 92 |
+
|
| 93 |
+
def test_synthesize_method_contract_with_mock(self):
|
| 94 |
+
"""Test the synthesize method contract using a mock implementation."""
|
| 95 |
+
|
| 96 |
+
class MockImplementation(ISpeechSynthesisService):
|
| 97 |
+
def __init__(self):
|
| 98 |
+
self.mock_synthesize = Mock()
|
| 99 |
+
self.mock_synthesize_stream = Mock()
|
| 100 |
+
|
| 101 |
+
def synthesize(self, request):
|
| 102 |
+
return self.mock_synthesize(request)
|
| 103 |
+
|
| 104 |
+
def synthesize_stream(self, request):
|
| 105 |
+
return self.mock_synthesize_stream(request)
|
| 106 |
+
|
| 107 |
+
# Create test data
|
| 108 |
+
text_content = TextContent(text="Hello world", language="en")
|
| 109 |
+
voice_settings = VoiceSettings(voice_id="test_voice", speed=1.0, language="en")
|
| 110 |
+
request = SpeechSynthesisRequest(
|
| 111 |
+
text_content=text_content,
|
| 112 |
+
voice_settings=voice_settings
|
| 113 |
+
)
|
| 114 |
+
expected_result = AudioContent(
|
| 115 |
+
data=b"synthesized_audio",
|
| 116 |
+
format="wav",
|
| 117 |
+
sample_rate=22050,
|
| 118 |
+
duration=2.0
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
# Setup mock
|
| 122 |
+
implementation = MockImplementation()
|
| 123 |
+
implementation.mock_synthesize.return_value = expected_result
|
| 124 |
+
|
| 125 |
+
# Call method
|
| 126 |
+
result = implementation.synthesize(request)
|
| 127 |
+
|
| 128 |
+
# Verify call and result
|
| 129 |
+
implementation.mock_synthesize.assert_called_once_with(request)
|
| 130 |
+
assert result == expected_result
|
| 131 |
+
|
| 132 |
+
def test_synthesize_stream_method_contract_with_mock(self):
|
| 133 |
+
"""Test the synthesize_stream method contract using a mock implementation."""
|
| 134 |
+
|
| 135 |
+
class MockImplementation(ISpeechSynthesisService):
|
| 136 |
+
def __init__(self):
|
| 137 |
+
self.mock_synthesize = Mock()
|
| 138 |
+
self.mock_synthesize_stream = Mock()
|
| 139 |
+
|
| 140 |
+
def synthesize(self, request):
|
| 141 |
+
return self.mock_synthesize(request)
|
| 142 |
+
|
| 143 |
+
def synthesize_stream(self, request):
|
| 144 |
+
return self.mock_synthesize_stream(request)
|
| 145 |
+
|
| 146 |
+
# Create test data
|
| 147 |
+
text_content = TextContent(text="Hello world", language="en")
|
| 148 |
+
voice_settings = VoiceSettings(voice_id="test_voice", speed=1.0, language="en")
|
| 149 |
+
request = SpeechSynthesisRequest(
|
| 150 |
+
text_content=text_content,
|
| 151 |
+
voice_settings=voice_settings
|
| 152 |
+
)
|
| 153 |
+
expected_chunks = [
|
| 154 |
+
AudioChunk(data=b"chunk1", format="wav", sample_rate=22050, chunk_index=0),
|
| 155 |
+
AudioChunk(data=b"chunk2", format="wav", sample_rate=22050, chunk_index=1, is_final=True)
|
| 156 |
+
]
|
| 157 |
+
|
| 158 |
+
# Setup mock
|
| 159 |
+
implementation = MockImplementation()
|
| 160 |
+
implementation.mock_synthesize_stream.return_value = iter(expected_chunks)
|
| 161 |
+
|
| 162 |
+
# Call method
|
| 163 |
+
result = implementation.synthesize_stream(request)
|
| 164 |
+
|
| 165 |
+
# Verify call and result
|
| 166 |
+
implementation.mock_synthesize_stream.assert_called_once_with(request)
|
| 167 |
+
chunks = list(result)
|
| 168 |
+
assert chunks == expected_chunks
|
| 169 |
+
|
| 170 |
+
def test_interface_docstring_requirements(self):
|
| 171 |
+
"""Test that the interface methods have proper documentation."""
|
| 172 |
+
synthesize_method = ISpeechSynthesisService.synthesize
|
| 173 |
+
stream_method = ISpeechSynthesisService.synthesize_stream
|
| 174 |
+
|
| 175 |
+
# Check synthesize method docstring
|
| 176 |
+
assert synthesize_method.__doc__ is not None
|
| 177 |
+
synthesize_doc = synthesize_method.__doc__
|
| 178 |
+
assert "Synthesize speech from text" in synthesize_doc
|
| 179 |
+
assert "Args:" in synthesize_doc
|
| 180 |
+
assert "Returns:" in synthesize_doc
|
| 181 |
+
assert "Raises:" in synthesize_doc
|
| 182 |
+
assert "SpeechSynthesisException" in synthesize_doc
|
| 183 |
+
|
| 184 |
+
# Check synthesize_stream method docstring
|
| 185 |
+
assert stream_method.__doc__ is not None
|
| 186 |
+
stream_doc = stream_method.__doc__
|
| 187 |
+
assert "Synthesize speech from text as a stream" in stream_doc
|
| 188 |
+
assert "Args:" in stream_doc
|
| 189 |
+
assert "Returns:" in stream_doc
|
| 190 |
+
assert "Iterator[AudioChunk]" in stream_doc
|
| 191 |
+
assert "Raises:" in stream_doc
|
| 192 |
+
assert "SpeechSynthesisException" in stream_doc
|
| 193 |
+
|
| 194 |
+
def test_interface_type_hints(self):
|
| 195 |
+
"""Test that the interface uses proper type hints."""
|
| 196 |
+
synthesize_method = ISpeechSynthesisService.synthesize
|
| 197 |
+
stream_method = ISpeechSynthesisService.synthesize_stream
|
| 198 |
+
|
| 199 |
+
# Check synthesize method annotations
|
| 200 |
+
synthesize_annotations = getattr(synthesize_method, '__annotations__', {})
|
| 201 |
+
assert 'request' in synthesize_annotations
|
| 202 |
+
assert 'return' in synthesize_annotations
|
| 203 |
+
assert synthesize_annotations['request'] == "'SpeechSynthesisRequest'"
|
| 204 |
+
assert synthesize_annotations['return'] == "'AudioContent'"
|
| 205 |
+
|
| 206 |
+
# Check synthesize_stream method annotations
|
| 207 |
+
stream_annotations = getattr(stream_method, '__annotations__', {})
|
| 208 |
+
assert 'request' in stream_annotations
|
| 209 |
+
assert 'return' in stream_annotations
|
| 210 |
+
assert stream_annotations['request'] == "'SpeechSynthesisRequest'"
|
| 211 |
+
assert stream_annotations['return'] == "Iterator['AudioChunk']"
|
| 212 |
+
|
| 213 |
+
def test_multiple_implementations_possible(self):
|
| 214 |
+
"""Test that multiple implementations of the interface are possible."""
|
| 215 |
+
|
| 216 |
+
class KokoroImplementation(ISpeechSynthesisService):
|
| 217 |
+
def synthesize(self, request):
|
| 218 |
+
return AudioContent(data=b"kokoro_audio", format="wav", sample_rate=22050, duration=1.0)
|
| 219 |
+
|
| 220 |
+
def synthesize_stream(self, request):
|
| 221 |
+
yield AudioChunk(data=b"kokoro_chunk", format="wav", sample_rate=22050, chunk_index=0, is_final=True)
|
| 222 |
+
|
| 223 |
+
class DiaImplementation(ISpeechSynthesisService):
|
| 224 |
+
def synthesize(self, request):
|
| 225 |
+
return AudioContent(data=b"dia_audio", format="wav", sample_rate=22050, duration=1.0)
|
| 226 |
+
|
| 227 |
+
def synthesize_stream(self, request):
|
| 228 |
+
yield AudioChunk(data=b"dia_chunk", format="wav", sample_rate=22050, chunk_index=0, is_final=True)
|
| 229 |
+
|
| 230 |
+
kokoro = KokoroImplementation()
|
| 231 |
+
dia = DiaImplementation()
|
| 232 |
+
|
| 233 |
+
assert isinstance(kokoro, ISpeechSynthesisService)
|
| 234 |
+
assert isinstance(dia, ISpeechSynthesisService)
|
| 235 |
+
assert type(kokoro) != type(dia)
|
| 236 |
+
|
| 237 |
+
def test_interface_methods_can_be_called_polymorphically(self):
|
| 238 |
+
"""Test that interface methods can be called polymorphically."""
|
| 239 |
+
|
| 240 |
+
class TestImplementation(ISpeechSynthesisService):
|
| 241 |
+
def __init__(self, audio_data, chunk_data):
|
| 242 |
+
self.audio_data = audio_data
|
| 243 |
+
self.chunk_data = chunk_data
|
| 244 |
+
|
| 245 |
+
def synthesize(self, request):
|
| 246 |
+
return AudioContent(data=self.audio_data, format="wav", sample_rate=22050, duration=1.0)
|
| 247 |
+
|
| 248 |
+
def synthesize_stream(self, request):
|
| 249 |
+
yield AudioChunk(data=self.chunk_data, format="wav", sample_rate=22050, chunk_index=0, is_final=True)
|
| 250 |
+
|
| 251 |
+
# Create different implementations
|
| 252 |
+
implementations = [
|
| 253 |
+
TestImplementation(b"audio1", b"chunk1"),
|
| 254 |
+
TestImplementation(b"audio2", b"chunk2")
|
| 255 |
+
]
|
| 256 |
+
|
| 257 |
+
# Test polymorphic usage
|
| 258 |
+
text_content = TextContent(text="test", language="en")
|
| 259 |
+
voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en")
|
| 260 |
+
request = SpeechSynthesisRequest(text_content=text_content, voice_settings=voice_settings)
|
| 261 |
+
|
| 262 |
+
# Test synthesize method
|
| 263 |
+
audio_results = []
|
| 264 |
+
for impl in implementations:
|
| 265 |
+
result = impl.synthesize(request)
|
| 266 |
+
audio_results.append(result.data)
|
| 267 |
+
|
| 268 |
+
assert audio_results == [b"audio1", b"audio2"]
|
| 269 |
+
|
| 270 |
+
# Test synthesize_stream method
|
| 271 |
+
chunk_results = []
|
| 272 |
+
for impl in implementations:
|
| 273 |
+
chunks = list(impl.synthesize_stream(request))
|
| 274 |
+
chunk_results.append(chunks[0].data)
|
| 275 |
+
|
| 276 |
+
assert chunk_results == [b"chunk1", b"chunk2"]
|
| 277 |
+
|
| 278 |
+
def test_interface_inheritance_chain(self):
|
| 279 |
+
"""Test the inheritance chain of the interface."""
|
| 280 |
+
# Check that it inherits from ABC
|
| 281 |
+
assert ABC in ISpeechSynthesisService.__mro__
|
| 282 |
+
|
| 283 |
+
# Check that it's at the right position in MRO
|
| 284 |
+
mro = ISpeechSynthesisService.__mro__
|
| 285 |
+
assert mro[0] == ISpeechSynthesisService
|
| 286 |
+
assert ABC in mro
|
| 287 |
+
|
| 288 |
+
def test_stream_method_returns_iterator(self):
|
| 289 |
+
"""Test that synthesize_stream returns an iterator."""
|
| 290 |
+
|
| 291 |
+
class StreamingImplementation(ISpeechSynthesisService):
|
| 292 |
+
def synthesize(self, request):
|
| 293 |
+
return AudioContent(data=b"audio", format="wav", sample_rate=22050, duration=1.0)
|
| 294 |
+
|
| 295 |
+
def synthesize_stream(self, request):
|
| 296 |
+
for i in range(3):
|
| 297 |
+
yield AudioChunk(
|
| 298 |
+
data=f"chunk{i}".encode(),
|
| 299 |
+
format="wav",
|
| 300 |
+
sample_rate=22050,
|
| 301 |
+
chunk_index=i,
|
| 302 |
+
is_final=(i == 2)
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
impl = StreamingImplementation()
|
| 306 |
+
text_content = TextContent(text="test", language="en")
|
| 307 |
+
voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en")
|
| 308 |
+
request = SpeechSynthesisRequest(text_content=text_content, voice_settings=voice_settings)
|
| 309 |
+
|
| 310 |
+
# Get the iterator
|
| 311 |
+
stream = impl.synthesize_stream(request)
|
| 312 |
+
|
| 313 |
+
# Verify it's an iterator
|
| 314 |
+
assert hasattr(stream, '__iter__')
|
| 315 |
+
assert hasattr(stream, '__next__')
|
| 316 |
+
|
| 317 |
+
# Verify we can iterate through chunks
|
| 318 |
+
chunks = list(stream)
|
| 319 |
+
assert len(chunks) == 3
|
| 320 |
+
|
| 321 |
+
for i, chunk in enumerate(chunks):
|
| 322 |
+
assert chunk.data == f"chunk{i}".encode()
|
| 323 |
+
assert chunk.chunk_index == i
|
| 324 |
+
assert chunk.is_final == (i == 2)
|
| 325 |
+
|
| 326 |
+
def test_implementation_can_handle_different_formats(self):
|
| 327 |
+
"""Test that implementations can handle different output formats."""
|
| 328 |
+
|
| 329 |
+
class MultiFormatImplementation(ISpeechSynthesisService):
|
| 330 |
+
def synthesize(self, request):
|
| 331 |
+
format_data = {
|
| 332 |
+
"wav": b"wav_audio_data",
|
| 333 |
+
"mp3": b"mp3_audio_data",
|
| 334 |
+
"flac": b"flac_audio_data",
|
| 335 |
+
"ogg": b"ogg_audio_data"
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
audio_data = format_data.get(request.output_format, b"default_audio_data")
|
| 339 |
+
return AudioContent(
|
| 340 |
+
data=audio_data,
|
| 341 |
+
format=request.output_format,
|
| 342 |
+
sample_rate=request.effective_sample_rate,
|
| 343 |
+
duration=1.0
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
def synthesize_stream(self, request):
|
| 347 |
+
yield AudioChunk(
|
| 348 |
+
data=f"{request.output_format}_chunk".encode(),
|
| 349 |
+
format=request.output_format,
|
| 350 |
+
sample_rate=request.effective_sample_rate,
|
| 351 |
+
chunk_index=0,
|
| 352 |
+
is_final=True
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
impl = MultiFormatImplementation()
|
| 356 |
+
text_content = TextContent(text="test", language="en")
|
| 357 |
+
voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en")
|
| 358 |
+
|
| 359 |
+
# Test different formats
|
| 360 |
+
formats = ["wav", "mp3", "flac", "ogg"]
|
| 361 |
+
|
| 362 |
+
for fmt in formats:
|
| 363 |
+
request = SpeechSynthesisRequest(
|
| 364 |
+
text_content=text_content,
|
| 365 |
+
voice_settings=voice_settings,
|
| 366 |
+
output_format=fmt
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
# Test synthesize
|
| 370 |
+
audio = impl.synthesize(request)
|
| 371 |
+
assert audio.format == fmt
|
| 372 |
+
assert audio.data == f"{fmt}_audio_data".encode()
|
| 373 |
+
|
| 374 |
+
# Test synthesize_stream
|
| 375 |
+
chunks = list(impl.synthesize_stream(request))
|
| 376 |
+
assert len(chunks) == 1
|
| 377 |
+
assert chunks[0].format == fmt
|
| 378 |
+
assert chunks[0].data == f"{fmt}_chunk".encode()
|
tests/unit/domain/interfaces/test_translation.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for ITranslationService interface contract."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from abc import ABC
|
| 5 |
+
from unittest.mock import Mock
|
| 6 |
+
from src.domain.interfaces.translation import ITranslationService
|
| 7 |
+
from src.domain.models.translation_request import TranslationRequest
|
| 8 |
+
from src.domain.models.text_content import TextContent
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TestITranslationService:
|
| 12 |
+
"""Test cases for ITranslationService interface contract."""
|
| 13 |
+
|
| 14 |
+
def test_interface_is_abstract(self):
|
| 15 |
+
"""Test that ITranslationService is an abstract base class."""
|
| 16 |
+
assert issubclass(ITranslationService, ABC)
|
| 17 |
+
|
| 18 |
+
# Should not be able to instantiate directly
|
| 19 |
+
with pytest.raises(TypeError):
|
| 20 |
+
ITranslationService() # type: ignore
|
| 21 |
+
|
| 22 |
+
def test_interface_has_required_method(self):
|
| 23 |
+
"""Test that interface defines the required abstract method."""
|
| 24 |
+
# Check that the method exists and is abstract
|
| 25 |
+
assert hasattr(ITranslationService, 'translate')
|
| 26 |
+
assert getattr(ITranslationService.translate, '__isabstractmethod__', False)
|
| 27 |
+
|
| 28 |
+
def test_method_signature(self):
|
| 29 |
+
"""Test that the method has the correct signature."""
|
| 30 |
+
import inspect
|
| 31 |
+
|
| 32 |
+
method = ITranslationService.translate
|
| 33 |
+
signature = inspect.signature(method)
|
| 34 |
+
|
| 35 |
+
# Check parameter names
|
| 36 |
+
params = list(signature.parameters.keys())
|
| 37 |
+
expected_params = ['self', 'request']
|
| 38 |
+
|
| 39 |
+
assert params == expected_params
|
| 40 |
+
|
| 41 |
+
# Check return annotation
|
| 42 |
+
assert signature.return_annotation == "'TextContent'"
|
| 43 |
+
|
| 44 |
+
def test_concrete_implementation_must_implement_method(self):
|
| 45 |
+
"""Test that concrete implementations must implement the abstract method."""
|
| 46 |
+
|
| 47 |
+
class IncompleteImplementation(ITranslationService):
|
| 48 |
+
pass
|
| 49 |
+
|
| 50 |
+
# Should not be able to instantiate without implementing abstract method
|
| 51 |
+
with pytest.raises(TypeError, match="Can't instantiate abstract class"):
|
| 52 |
+
IncompleteImplementation() # type: ignore
|
| 53 |
+
|
| 54 |
+
def test_concrete_implementation_with_method(self):
|
| 55 |
+
"""Test that concrete implementation with method can be instantiated."""
|
| 56 |
+
|
| 57 |
+
class ConcreteImplementation(ITranslationService):
|
| 58 |
+
def translate(self, request):
|
| 59 |
+
return TextContent(text="translated text", language=request.target_language)
|
| 60 |
+
|
| 61 |
+
# Should be able to instantiate
|
| 62 |
+
implementation = ConcreteImplementation()
|
| 63 |
+
assert isinstance(implementation, ITranslationService)
|
| 64 |
+
|
| 65 |
+
def test_method_contract_with_mock(self):
|
| 66 |
+
"""Test the method contract using a mock implementation."""
|
| 67 |
+
|
| 68 |
+
class MockImplementation(ITranslationService):
|
| 69 |
+
def __init__(self):
|
| 70 |
+
self.mock_method = Mock()
|
| 71 |
+
|
| 72 |
+
def translate(self, request):
|
| 73 |
+
return self.mock_method(request)
|
| 74 |
+
|
| 75 |
+
# Create test data
|
| 76 |
+
source_text = TextContent(text="Hello world", language="en")
|
| 77 |
+
request = TranslationRequest(
|
| 78 |
+
source_text=source_text,
|
| 79 |
+
target_language="es"
|
| 80 |
+
)
|
| 81 |
+
expected_result = TextContent(text="Hola mundo", language="es")
|
| 82 |
+
|
| 83 |
+
# Setup mock
|
| 84 |
+
implementation = MockImplementation()
|
| 85 |
+
implementation.mock_method.return_value = expected_result
|
| 86 |
+
|
| 87 |
+
# Call method
|
| 88 |
+
result = implementation.translate(request)
|
| 89 |
+
|
| 90 |
+
# Verify call and result
|
| 91 |
+
implementation.mock_method.assert_called_once_with(request)
|
| 92 |
+
assert result == expected_result
|
| 93 |
+
|
| 94 |
+
def test_interface_docstring_requirements(self):
|
| 95 |
+
"""Test that the interface method has proper documentation."""
|
| 96 |
+
method = ITranslationService.translate
|
| 97 |
+
|
| 98 |
+
assert method.__doc__ is not None
|
| 99 |
+
docstring = method.__doc__
|
| 100 |
+
|
| 101 |
+
# Check that docstring contains key information
|
| 102 |
+
assert "Translate text from source language to target language" in docstring
|
| 103 |
+
assert "Args:" in docstring
|
| 104 |
+
assert "Returns:" in docstring
|
| 105 |
+
assert "Raises:" in docstring
|
| 106 |
+
assert "TranslationFailedException" in docstring
|
| 107 |
+
|
| 108 |
+
def test_interface_type_hints(self):
|
| 109 |
+
"""Test that the interface uses proper type hints."""
|
| 110 |
+
method = ITranslationService.translate
|
| 111 |
+
annotations = getattr(method, '__annotations__', {})
|
| 112 |
+
|
| 113 |
+
assert 'request' in annotations
|
| 114 |
+
assert 'return' in annotations
|
| 115 |
+
|
| 116 |
+
# Check that type annotations are correct
|
| 117 |
+
assert annotations['request'] == "'TranslationRequest'"
|
| 118 |
+
assert annotations['return'] == "'TextContent'"
|
| 119 |
+
|
| 120 |
+
def test_multiple_implementations_possible(self):
|
| 121 |
+
"""Test that multiple implementations of the interface are possible."""
|
| 122 |
+
|
| 123 |
+
class NLLBImplementation(ITranslationService):
|
| 124 |
+
def translate(self, request):
|
| 125 |
+
return TextContent(text="NLLB translation", language=request.target_language)
|
| 126 |
+
|
| 127 |
+
class GoogleImplementation(ITranslationService):
|
| 128 |
+
def translate(self, request):
|
| 129 |
+
return TextContent(text="Google translation", language=request.target_language)
|
| 130 |
+
|
| 131 |
+
nllb = NLLBImplementation()
|
| 132 |
+
google = GoogleImplementation()
|
| 133 |
+
|
| 134 |
+
assert isinstance(nllb, ITranslationService)
|
| 135 |
+
assert isinstance(google, ITranslationService)
|
| 136 |
+
assert type(nllb) != type(google)
|
| 137 |
+
|
| 138 |
+
def test_interface_method_can_be_called_polymorphically(self):
|
| 139 |
+
"""Test that interface methods can be called polymorphically."""
|
| 140 |
+
|
| 141 |
+
class TestImplementation(ITranslationService):
|
| 142 |
+
def __init__(self, translation_prefix):
|
| 143 |
+
self.translation_prefix = translation_prefix
|
| 144 |
+
|
| 145 |
+
def translate(self, request):
|
| 146 |
+
translated_text = f"{self.translation_prefix}: {request.source_text.text}"
|
| 147 |
+
return TextContent(text=translated_text, language=request.target_language)
|
| 148 |
+
|
| 149 |
+
# Create different implementations
|
| 150 |
+
implementations = [
|
| 151 |
+
TestImplementation("Provider1"),
|
| 152 |
+
TestImplementation("Provider2")
|
| 153 |
+
]
|
| 154 |
+
|
| 155 |
+
# Test polymorphic usage
|
| 156 |
+
source_text = TextContent(text="Hello", language="en")
|
| 157 |
+
request = TranslationRequest(source_text=source_text, target_language="es")
|
| 158 |
+
|
| 159 |
+
results = []
|
| 160 |
+
for impl in implementations:
|
| 161 |
+
# Can call the same method on different implementations
|
| 162 |
+
result = impl.translate(request)
|
| 163 |
+
results.append(result.text)
|
| 164 |
+
|
| 165 |
+
assert results == ["Provider1: Hello", "Provider2: Hello"]
|
| 166 |
+
|
| 167 |
+
def test_interface_inheritance_chain(self):
|
| 168 |
+
"""Test the inheritance chain of the interface."""
|
| 169 |
+
# Check that it inherits from ABC
|
| 170 |
+
assert ABC in ITranslationService.__mro__
|
| 171 |
+
|
| 172 |
+
# Check that it's at the right position in MRO
|
| 173 |
+
mro = ITranslationService.__mro__
|
| 174 |
+
assert mro[0] == ITranslationService
|
| 175 |
+
assert ABC in mro
|
| 176 |
+
|
| 177 |
+
def test_method_parameter_validation_in_implementation(self):
|
| 178 |
+
"""Test that implementations can validate parameters."""
|
| 179 |
+
|
| 180 |
+
class ValidatingImplementation(ITranslationService):
|
| 181 |
+
def translate(self, request):
|
| 182 |
+
if not isinstance(request, TranslationRequest):
|
| 183 |
+
raise TypeError("request must be TranslationRequest")
|
| 184 |
+
|
| 185 |
+
# Validate that source and target languages are different
|
| 186 |
+
if request.effective_source_language == request.target_language:
|
| 187 |
+
raise ValueError("Source and target languages cannot be the same")
|
| 188 |
+
|
| 189 |
+
return TextContent(
|
| 190 |
+
text=f"Translated: {request.source_text.text}",
|
| 191 |
+
language=request.target_language
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
impl = ValidatingImplementation()
|
| 195 |
+
|
| 196 |
+
# Valid call should work
|
| 197 |
+
source_text = TextContent(text="Hello", language="en")
|
| 198 |
+
request = TranslationRequest(source_text=source_text, target_language="es")
|
| 199 |
+
result = impl.translate(request)
|
| 200 |
+
assert result.text == "Translated: Hello"
|
| 201 |
+
assert result.language == "es"
|
| 202 |
+
|
| 203 |
+
# Invalid parameter type should raise error
|
| 204 |
+
with pytest.raises(TypeError, match="request must be TranslationRequest"):
|
| 205 |
+
impl.translate("not a request") # type: ignore
|
| 206 |
+
|
| 207 |
+
# Same language should raise error
|
| 208 |
+
same_lang_text = TextContent(text="Hello", language="en")
|
| 209 |
+
same_lang_request = TranslationRequest(source_text=same_lang_text, target_language="en")
|
| 210 |
+
with pytest.raises(ValueError, match="Source and target languages cannot be the same"):
|
| 211 |
+
impl.translate(same_lang_request)
|
| 212 |
+
|
| 213 |
+
def test_implementation_can_handle_different_language_pairs(self):
|
| 214 |
+
"""Test that implementations can handle different language pairs."""
|
| 215 |
+
|
| 216 |
+
class MultiLanguageImplementation(ITranslationService):
|
| 217 |
+
def __init__(self):
|
| 218 |
+
self.translations = {
|
| 219 |
+
("en", "es"): {"Hello": "Hola", "world": "mundo"},
|
| 220 |
+
("en", "fr"): {"Hello": "Bonjour", "world": "monde"},
|
| 221 |
+
("es", "en"): {"Hola": "Hello", "mundo": "world"},
|
| 222 |
+
("fr", "en"): {"Bonjour": "Hello", "monde": "world"}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
def translate(self, request):
|
| 226 |
+
source_lang = request.effective_source_language
|
| 227 |
+
target_lang = request.target_language
|
| 228 |
+
|
| 229 |
+
translation_dict = self.translations.get((source_lang, target_lang), {})
|
| 230 |
+
|
| 231 |
+
# Simple word-by-word translation for testing
|
| 232 |
+
words = request.source_text.text.split()
|
| 233 |
+
translated_words = [translation_dict.get(word, word) for word in words]
|
| 234 |
+
translated_text = " ".join(translated_words)
|
| 235 |
+
|
| 236 |
+
return TextContent(text=translated_text, language=target_lang)
|
| 237 |
+
|
| 238 |
+
impl = MultiLanguageImplementation()
|
| 239 |
+
|
| 240 |
+
# Test different language pairs
|
| 241 |
+
test_cases = [
|
| 242 |
+
("Hello world", "en", "es", "Hola mundo"),
|
| 243 |
+
("Hello world", "en", "fr", "Bonjour monde"),
|
| 244 |
+
("Hola mundo", "es", "en", "Hello world"),
|
| 245 |
+
("Bonjour monde", "fr", "en", "Hello world")
|
| 246 |
+
]
|
| 247 |
+
|
| 248 |
+
for text, source_lang, target_lang, expected in test_cases:
|
| 249 |
+
source_text = TextContent(text=text, language=source_lang)
|
| 250 |
+
request = TranslationRequest(
|
| 251 |
+
source_text=source_text,
|
| 252 |
+
target_language=target_lang,
|
| 253 |
+
source_language=source_lang
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
result = impl.translate(request)
|
| 257 |
+
assert result.text == expected
|
| 258 |
+
assert result.language == target_lang
|
| 259 |
+
|
| 260 |
+
def test_implementation_can_handle_auto_detect_source_language(self):
|
| 261 |
+
"""Test that implementations can handle auto-detection of source language."""
|
| 262 |
+
|
| 263 |
+
class AutoDetectImplementation(ITranslationService):
|
| 264 |
+
def translate(self, request):
|
| 265 |
+
# Use the effective source language (from TextContent if not explicitly set)
|
| 266 |
+
source_lang = request.effective_source_language
|
| 267 |
+
target_lang = request.target_language
|
| 268 |
+
|
| 269 |
+
# Simple mock translation based on detected language
|
| 270 |
+
if source_lang == "en" and target_lang == "es":
|
| 271 |
+
translated_text = f"ES: {request.source_text.text}"
|
| 272 |
+
elif source_lang == "es" and target_lang == "en":
|
| 273 |
+
translated_text = f"EN: {request.source_text.text}"
|
| 274 |
+
else:
|
| 275 |
+
translated_text = f"UNKNOWN: {request.source_text.text}"
|
| 276 |
+
|
| 277 |
+
return TextContent(text=translated_text, language=target_lang)
|
| 278 |
+
|
| 279 |
+
impl = AutoDetectImplementation()
|
| 280 |
+
|
| 281 |
+
# Test with explicit source language
|
| 282 |
+
source_text = TextContent(text="Hello", language="en")
|
| 283 |
+
explicit_request = TranslationRequest(
|
| 284 |
+
source_text=source_text,
|
| 285 |
+
target_language="es",
|
| 286 |
+
source_language="en"
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
result = impl.translate(explicit_request)
|
| 290 |
+
assert result.text == "ES: Hello"
|
| 291 |
+
assert result.language == "es"
|
| 292 |
+
|
| 293 |
+
# Test with auto-detected source language (None)
|
| 294 |
+
auto_request = TranslationRequest(
|
| 295 |
+
source_text=source_text, # language="en" in TextContent
|
| 296 |
+
target_language="es"
|
| 297 |
+
# source_language=None (default)
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
result = impl.translate(auto_request)
|
| 301 |
+
assert result.text == "ES: Hello" # Should use language from TextContent
|
| 302 |
+
assert result.language == "es"
|
| 303 |
+
assert auto_request.is_auto_detect_source is True
|
tests/unit/domain/models/test_audio_chunk.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for AudioChunk value object."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from src.domain.models.audio_chunk import AudioChunk
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class TestAudioChunk:
|
| 8 |
+
"""Test cases for AudioChunk value object."""
|
| 9 |
+
|
| 10 |
+
def test_valid_audio_chunk_creation(self):
|
| 11 |
+
"""Test creating valid AudioChunk instance."""
|
| 12 |
+
chunk = AudioChunk(
|
| 13 |
+
data=b"fake_audio_chunk_data",
|
| 14 |
+
format="wav",
|
| 15 |
+
sample_rate=22050,
|
| 16 |
+
chunk_index=0,
|
| 17 |
+
is_final=False,
|
| 18 |
+
timestamp=1.5
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
assert chunk.data == b"fake_audio_chunk_data"
|
| 22 |
+
assert chunk.format == "wav"
|
| 23 |
+
assert chunk.sample_rate == 22050
|
| 24 |
+
assert chunk.chunk_index == 0
|
| 25 |
+
assert chunk.is_final is False
|
| 26 |
+
assert chunk.timestamp == 1.5
|
| 27 |
+
assert chunk.size_bytes == len(b"fake_audio_chunk_data")
|
| 28 |
+
|
| 29 |
+
def test_audio_chunk_with_defaults(self):
|
| 30 |
+
"""Test creating AudioChunk with default values."""
|
| 31 |
+
chunk = AudioChunk(
|
| 32 |
+
data=b"fake_audio_chunk_data",
|
| 33 |
+
format="wav",
|
| 34 |
+
sample_rate=22050,
|
| 35 |
+
chunk_index=0
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
assert chunk.is_final is False
|
| 39 |
+
assert chunk.timestamp is None
|
| 40 |
+
|
| 41 |
+
def test_final_chunk_creation(self):
|
| 42 |
+
"""Test creating final AudioChunk."""
|
| 43 |
+
chunk = AudioChunk(
|
| 44 |
+
data=b"final_chunk_data",
|
| 45 |
+
format="wav",
|
| 46 |
+
sample_rate=22050,
|
| 47 |
+
chunk_index=5,
|
| 48 |
+
is_final=True
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
assert chunk.is_final is True
|
| 52 |
+
assert chunk.chunk_index == 5
|
| 53 |
+
|
| 54 |
+
def test_non_bytes_data_raises_error(self):
|
| 55 |
+
"""Test that non-bytes data raises TypeError."""
|
| 56 |
+
with pytest.raises(TypeError, match="Audio data must be bytes"):
|
| 57 |
+
AudioChunk(
|
| 58 |
+
data="not_bytes", # type: ignore
|
| 59 |
+
format="wav",
|
| 60 |
+
sample_rate=22050,
|
| 61 |
+
chunk_index=0
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
def test_empty_data_raises_error(self):
|
| 65 |
+
"""Test that empty data raises ValueError."""
|
| 66 |
+
with pytest.raises(ValueError, match="Audio data cannot be empty"):
|
| 67 |
+
AudioChunk(
|
| 68 |
+
data=b"",
|
| 69 |
+
format="wav",
|
| 70 |
+
sample_rate=22050,
|
| 71 |
+
chunk_index=0
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
def test_unsupported_format_raises_error(self):
|
| 75 |
+
"""Test that unsupported format raises ValueError."""
|
| 76 |
+
with pytest.raises(ValueError, match="Unsupported audio format: xyz"):
|
| 77 |
+
AudioChunk(
|
| 78 |
+
data=b"fake_data",
|
| 79 |
+
format="xyz",
|
| 80 |
+
sample_rate=22050,
|
| 81 |
+
chunk_index=0
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
def test_supported_formats(self):
|
| 85 |
+
"""Test all supported audio formats."""
|
| 86 |
+
supported_formats = ['wav', 'mp3', 'flac', 'ogg', 'raw']
|
| 87 |
+
|
| 88 |
+
for fmt in supported_formats:
|
| 89 |
+
chunk = AudioChunk(
|
| 90 |
+
data=b"fake_data",
|
| 91 |
+
format=fmt,
|
| 92 |
+
sample_rate=22050,
|
| 93 |
+
chunk_index=0
|
| 94 |
+
)
|
| 95 |
+
assert chunk.format == fmt
|
| 96 |
+
|
| 97 |
+
def test_non_integer_sample_rate_raises_error(self):
|
| 98 |
+
"""Test that non-integer sample rate raises ValueError."""
|
| 99 |
+
with pytest.raises(ValueError, match="Sample rate must be a positive integer"):
|
| 100 |
+
AudioChunk(
|
| 101 |
+
data=b"fake_data",
|
| 102 |
+
format="wav",
|
| 103 |
+
sample_rate=22050.5, # type: ignore
|
| 104 |
+
chunk_index=0
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
def test_negative_sample_rate_raises_error(self):
|
| 108 |
+
"""Test that negative sample rate raises ValueError."""
|
| 109 |
+
with pytest.raises(ValueError, match="Sample rate must be a positive integer"):
|
| 110 |
+
AudioChunk(
|
| 111 |
+
data=b"fake_data",
|
| 112 |
+
format="wav",
|
| 113 |
+
sample_rate=-1,
|
| 114 |
+
chunk_index=0
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
def test_zero_sample_rate_raises_error(self):
|
| 118 |
+
"""Test that zero sample rate raises ValueError."""
|
| 119 |
+
with pytest.raises(ValueError, match="Sample rate must be a positive integer"):
|
| 120 |
+
AudioChunk(
|
| 121 |
+
data=b"fake_data",
|
| 122 |
+
format="wav",
|
| 123 |
+
sample_rate=0,
|
| 124 |
+
chunk_index=0
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
def test_non_integer_chunk_index_raises_error(self):
|
| 128 |
+
"""Test that non-integer chunk index raises ValueError."""
|
| 129 |
+
with pytest.raises(ValueError, match="Chunk index must be a non-negative integer"):
|
| 130 |
+
AudioChunk(
|
| 131 |
+
data=b"fake_data",
|
| 132 |
+
format="wav",
|
| 133 |
+
sample_rate=22050,
|
| 134 |
+
chunk_index=1.5 # type: ignore
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
def test_negative_chunk_index_raises_error(self):
|
| 138 |
+
"""Test that negative chunk index raises ValueError."""
|
| 139 |
+
with pytest.raises(ValueError, match="Chunk index must be a non-negative integer"):
|
| 140 |
+
AudioChunk(
|
| 141 |
+
data=b"fake_data",
|
| 142 |
+
format="wav",
|
| 143 |
+
sample_rate=22050,
|
| 144 |
+
chunk_index=-1
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
def test_valid_chunk_index_zero(self):
|
| 148 |
+
"""Test that chunk index of zero is valid."""
|
| 149 |
+
chunk = AudioChunk(
|
| 150 |
+
data=b"fake_data",
|
| 151 |
+
format="wav",
|
| 152 |
+
sample_rate=22050,
|
| 153 |
+
chunk_index=0
|
| 154 |
+
)
|
| 155 |
+
assert chunk.chunk_index == 0
|
| 156 |
+
|
| 157 |
+
def test_non_boolean_is_final_raises_error(self):
|
| 158 |
+
"""Test that non-boolean is_final raises TypeError."""
|
| 159 |
+
with pytest.raises(TypeError, match="is_final must be a boolean"):
|
| 160 |
+
AudioChunk(
|
| 161 |
+
data=b"fake_data",
|
| 162 |
+
format="wav",
|
| 163 |
+
sample_rate=22050,
|
| 164 |
+
chunk_index=0,
|
| 165 |
+
is_final="true" # type: ignore
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
def test_non_numeric_timestamp_raises_error(self):
|
| 169 |
+
"""Test that non-numeric timestamp raises ValueError."""
|
| 170 |
+
with pytest.raises(ValueError, match="Timestamp must be a non-negative number"):
|
| 171 |
+
AudioChunk(
|
| 172 |
+
data=b"fake_data",
|
| 173 |
+
format="wav",
|
| 174 |
+
sample_rate=22050,
|
| 175 |
+
chunk_index=0,
|
| 176 |
+
timestamp="1.5" # type: ignore
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
def test_negative_timestamp_raises_error(self):
|
| 180 |
+
"""Test that negative timestamp raises ValueError."""
|
| 181 |
+
with pytest.raises(ValueError, match="Timestamp must be a non-negative number"):
|
| 182 |
+
AudioChunk(
|
| 183 |
+
data=b"fake_data",
|
| 184 |
+
format="wav",
|
| 185 |
+
sample_rate=22050,
|
| 186 |
+
chunk_index=0,
|
| 187 |
+
timestamp=-1.0
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
def test_valid_timestamp_zero(self):
|
| 191 |
+
"""Test that timestamp of zero is valid."""
|
| 192 |
+
chunk = AudioChunk(
|
| 193 |
+
data=b"fake_data",
|
| 194 |
+
format="wav",
|
| 195 |
+
sample_rate=22050,
|
| 196 |
+
chunk_index=0,
|
| 197 |
+
timestamp=0.0
|
| 198 |
+
)
|
| 199 |
+
assert chunk.timestamp == 0.0
|
| 200 |
+
|
| 201 |
+
def test_valid_timestamp_values(self):
|
| 202 |
+
"""Test valid timestamp values."""
|
| 203 |
+
valid_timestamps = [0.0, 1.5, 10, 100.123]
|
| 204 |
+
|
| 205 |
+
for timestamp in valid_timestamps:
|
| 206 |
+
chunk = AudioChunk(
|
| 207 |
+
data=b"fake_data",
|
| 208 |
+
format="wav",
|
| 209 |
+
sample_rate=22050,
|
| 210 |
+
chunk_index=0,
|
| 211 |
+
timestamp=timestamp
|
| 212 |
+
)
|
| 213 |
+
assert chunk.timestamp == timestamp
|
| 214 |
+
|
| 215 |
+
def test_size_bytes_property(self):
|
| 216 |
+
"""Test size_bytes property returns correct value."""
|
| 217 |
+
test_data = b"test_audio_chunk_data_123"
|
| 218 |
+
chunk = AudioChunk(
|
| 219 |
+
data=test_data,
|
| 220 |
+
format="wav",
|
| 221 |
+
sample_rate=22050,
|
| 222 |
+
chunk_index=0
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
assert chunk.size_bytes == len(test_data)
|
| 226 |
+
|
| 227 |
+
def test_duration_estimate_property(self):
|
| 228 |
+
"""Test duration_estimate property calculation."""
|
| 229 |
+
# Create chunk with known data size
|
| 230 |
+
test_data = b"x" * 44100 # 44100 bytes
|
| 231 |
+
chunk = AudioChunk(
|
| 232 |
+
data=test_data,
|
| 233 |
+
format="wav",
|
| 234 |
+
sample_rate=22050, # 22050 samples per second
|
| 235 |
+
chunk_index=0
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
# Expected duration: 44100 bytes / (22050 samples/sec * 2 bytes/sample) = 1.0 second
|
| 239 |
+
expected_duration = 44100 / (22050 * 2)
|
| 240 |
+
assert abs(chunk.duration_estimate - expected_duration) < 0.01
|
| 241 |
+
|
| 242 |
+
def test_duration_estimate_with_zero_sample_rate(self):
|
| 243 |
+
"""Test duration_estimate with edge case of zero calculation."""
|
| 244 |
+
# This shouldn't happen due to validation, but test the property logic
|
| 245 |
+
chunk = AudioChunk(
|
| 246 |
+
data=b"test_data",
|
| 247 |
+
format="wav",
|
| 248 |
+
sample_rate=22050,
|
| 249 |
+
chunk_index=0
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# Should return a reasonable estimate
|
| 253 |
+
assert chunk.duration_estimate >= 0
|
| 254 |
+
|
| 255 |
+
def test_audio_chunk_is_immutable(self):
|
| 256 |
+
"""Test that AudioChunk is immutable (frozen dataclass)."""
|
| 257 |
+
chunk = AudioChunk(
|
| 258 |
+
data=b"fake_data",
|
| 259 |
+
format="wav",
|
| 260 |
+
sample_rate=22050,
|
| 261 |
+
chunk_index=0
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
with pytest.raises(AttributeError):
|
| 265 |
+
chunk.format = "mp3" # type: ignore
|
| 266 |
+
|
| 267 |
+
def test_chunk_sequence_ordering(self):
|
| 268 |
+
"""Test that chunks can be ordered by chunk_index."""
|
| 269 |
+
chunks = [
|
| 270 |
+
AudioChunk(data=b"chunk2", format="wav", sample_rate=22050, chunk_index=2),
|
| 271 |
+
AudioChunk(data=b"chunk0", format="wav", sample_rate=22050, chunk_index=0),
|
| 272 |
+
AudioChunk(data=b"chunk1", format="wav", sample_rate=22050, chunk_index=1),
|
| 273 |
+
]
|
| 274 |
+
|
| 275 |
+
# Sort by chunk_index
|
| 276 |
+
sorted_chunks = sorted(chunks, key=lambda c: c.chunk_index)
|
| 277 |
+
|
| 278 |
+
assert sorted_chunks[0].chunk_index == 0
|
| 279 |
+
assert sorted_chunks[1].chunk_index == 1
|
| 280 |
+
assert sorted_chunks[2].chunk_index == 2
|
| 281 |
+
|
| 282 |
+
def test_streaming_scenario(self):
|
| 283 |
+
"""Test typical streaming scenario with multiple chunks."""
|
| 284 |
+
# First chunk
|
| 285 |
+
chunk1 = AudioChunk(
|
| 286 |
+
data=b"first_chunk_data",
|
| 287 |
+
format="wav",
|
| 288 |
+
sample_rate=22050,
|
| 289 |
+
chunk_index=0,
|
| 290 |
+
is_final=False,
|
| 291 |
+
timestamp=0.0
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# Middle chunk
|
| 295 |
+
chunk2 = AudioChunk(
|
| 296 |
+
data=b"middle_chunk_data",
|
| 297 |
+
format="wav",
|
| 298 |
+
sample_rate=22050,
|
| 299 |
+
chunk_index=1,
|
| 300 |
+
is_final=False,
|
| 301 |
+
timestamp=1.0
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
# Final chunk
|
| 305 |
+
chunk3 = AudioChunk(
|
| 306 |
+
data=b"final_chunk_data",
|
| 307 |
+
format="wav",
|
| 308 |
+
sample_rate=22050,
|
| 309 |
+
chunk_index=2,
|
| 310 |
+
is_final=True,
|
| 311 |
+
timestamp=2.0
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
assert not chunk1.is_final
|
| 315 |
+
assert not chunk2.is_final
|
| 316 |
+
assert chunk3.is_final
|
| 317 |
+
|
| 318 |
+
# Verify ordering
|
| 319 |
+
chunks = [chunk1, chunk2, chunk3]
|
| 320 |
+
for i, chunk in enumerate(chunks):
|
| 321 |
+
assert chunk.chunk_index == i
|
| 322 |
+
assert chunk.timestamp == float(i)
|
tests/unit/domain/models/test_processing_result.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for ProcessingResult value object."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from src.domain.models.processing_result import ProcessingResult
|
| 5 |
+
from src.domain.models.text_content import TextContent
|
| 6 |
+
from src.domain.models.audio_content import AudioContent
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class TestProcessingResult:
|
| 10 |
+
"""Test cases for ProcessingResult value object."""
|
| 11 |
+
|
| 12 |
+
@pytest.fixture
|
| 13 |
+
def sample_text_content(self):
|
| 14 |
+
"""Sample text content for testing."""
|
| 15 |
+
return TextContent(text="Hello, world!", language="en")
|
| 16 |
+
|
| 17 |
+
@pytest.fixture
|
| 18 |
+
def sample_translated_text(self):
|
| 19 |
+
"""Sample translated text content for testing."""
|
| 20 |
+
return TextContent(text="Hola, mundo!", language="es")
|
| 21 |
+
|
| 22 |
+
@pytest.fixture
|
| 23 |
+
def sample_audio_content(self):
|
| 24 |
+
"""Sample audio content for testing."""
|
| 25 |
+
return AudioContent(
|
| 26 |
+
data=b"fake_audio_data",
|
| 27 |
+
format="wav",
|
| 28 |
+
sample_rate=22050,
|
| 29 |
+
duration=5.0
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
def test_valid_successful_processing_result(self, sample_text_content, sample_translated_text, sample_audio_content):
|
| 33 |
+
"""Test creating valid successful ProcessingResult."""
|
| 34 |
+
result = ProcessingResult(
|
| 35 |
+
success=True,
|
| 36 |
+
original_text=sample_text_content,
|
| 37 |
+
translated_text=sample_translated_text,
|
| 38 |
+
audio_output=sample_audio_content,
|
| 39 |
+
error_message=None,
|
| 40 |
+
processing_time=2.5
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
assert result.success is True
|
| 44 |
+
assert result.original_text == sample_text_content
|
| 45 |
+
assert result.translated_text == sample_translated_text
|
| 46 |
+
assert result.audio_output == sample_audio_content
|
| 47 |
+
assert result.error_message is None
|
| 48 |
+
assert result.processing_time == 2.5
|
| 49 |
+
assert result.has_translation is True
|
| 50 |
+
assert result.has_audio_output is True
|
| 51 |
+
assert result.is_complete_pipeline is True
|
| 52 |
+
|
| 53 |
+
def test_valid_failed_processing_result(self):
|
| 54 |
+
"""Test creating valid failed ProcessingResult."""
|
| 55 |
+
result = ProcessingResult(
|
| 56 |
+
success=False,
|
| 57 |
+
original_text=None,
|
| 58 |
+
translated_text=None,
|
| 59 |
+
audio_output=None,
|
| 60 |
+
error_message="Processing failed",
|
| 61 |
+
processing_time=1.0
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
assert result.success is False
|
| 65 |
+
assert result.original_text is None
|
| 66 |
+
assert result.translated_text is None
|
| 67 |
+
assert result.audio_output is None
|
| 68 |
+
assert result.error_message == "Processing failed"
|
| 69 |
+
assert result.processing_time == 1.0
|
| 70 |
+
assert result.has_translation is False
|
| 71 |
+
assert result.has_audio_output is False
|
| 72 |
+
assert result.is_complete_pipeline is False
|
| 73 |
+
|
| 74 |
+
def test_non_boolean_success_raises_error(self, sample_text_content):
|
| 75 |
+
"""Test that non-boolean success raises TypeError."""
|
| 76 |
+
with pytest.raises(TypeError, match="Success must be a boolean"):
|
| 77 |
+
ProcessingResult(
|
| 78 |
+
success="true", # type: ignore
|
| 79 |
+
original_text=sample_text_content,
|
| 80 |
+
translated_text=None,
|
| 81 |
+
audio_output=None,
|
| 82 |
+
error_message=None,
|
| 83 |
+
processing_time=1.0
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
def test_invalid_original_text_type_raises_error(self):
|
| 87 |
+
"""Test that invalid original text type raises TypeError."""
|
| 88 |
+
with pytest.raises(TypeError, match="Original text must be a TextContent instance or None"):
|
| 89 |
+
ProcessingResult(
|
| 90 |
+
success=True,
|
| 91 |
+
original_text="not a TextContent", # type: ignore
|
| 92 |
+
translated_text=None,
|
| 93 |
+
audio_output=None,
|
| 94 |
+
error_message=None,
|
| 95 |
+
processing_time=1.0
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
def test_invalid_translated_text_type_raises_error(self, sample_text_content):
|
| 99 |
+
"""Test that invalid translated text type raises TypeError."""
|
| 100 |
+
with pytest.raises(TypeError, match="Translated text must be a TextContent instance or None"):
|
| 101 |
+
ProcessingResult(
|
| 102 |
+
success=True,
|
| 103 |
+
original_text=sample_text_content,
|
| 104 |
+
translated_text="not a TextContent", # type: ignore
|
| 105 |
+
audio_output=None,
|
| 106 |
+
error_message=None,
|
| 107 |
+
processing_time=1.0
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
def test_invalid_audio_output_type_raises_error(self, sample_text_content):
|
| 111 |
+
"""Test that invalid audio output type raises TypeError."""
|
| 112 |
+
with pytest.raises(TypeError, match="Audio output must be an AudioContent instance or None"):
|
| 113 |
+
ProcessingResult(
|
| 114 |
+
success=True,
|
| 115 |
+
original_text=sample_text_content,
|
| 116 |
+
translated_text=None,
|
| 117 |
+
audio_output="not an AudioContent", # type: ignore
|
| 118 |
+
error_message=None,
|
| 119 |
+
processing_time=1.0
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
def test_invalid_error_message_type_raises_error(self, sample_text_content):
|
| 123 |
+
"""Test that invalid error message type raises TypeError."""
|
| 124 |
+
with pytest.raises(TypeError, match="Error message must be a string or None"):
|
| 125 |
+
ProcessingResult(
|
| 126 |
+
success=True,
|
| 127 |
+
original_text=sample_text_content,
|
| 128 |
+
translated_text=None,
|
| 129 |
+
audio_output=None,
|
| 130 |
+
error_message=123, # type: ignore
|
| 131 |
+
processing_time=1.0
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
def test_non_numeric_processing_time_raises_error(self, sample_text_content):
|
| 135 |
+
"""Test that non-numeric processing time raises TypeError."""
|
| 136 |
+
with pytest.raises(TypeError, match="Processing time must be a number"):
|
| 137 |
+
ProcessingResult(
|
| 138 |
+
success=True,
|
| 139 |
+
original_text=sample_text_content,
|
| 140 |
+
translated_text=None,
|
| 141 |
+
audio_output=None,
|
| 142 |
+
error_message=None,
|
| 143 |
+
processing_time="1.0" # type: ignore
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
def test_negative_processing_time_raises_error(self, sample_text_content):
|
| 147 |
+
"""Test that negative processing time raises ValueError."""
|
| 148 |
+
with pytest.raises(ValueError, match="Processing time cannot be negative"):
|
| 149 |
+
ProcessingResult(
|
| 150 |
+
success=True,
|
| 151 |
+
original_text=sample_text_content,
|
| 152 |
+
translated_text=None,
|
| 153 |
+
audio_output=None,
|
| 154 |
+
error_message=None,
|
| 155 |
+
processing_time=-1.0
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
def test_successful_result_with_error_message_raises_error(self, sample_text_content):
|
| 159 |
+
"""Test that successful result with error message raises ValueError."""
|
| 160 |
+
with pytest.raises(ValueError, match="Successful result cannot have an error message"):
|
| 161 |
+
ProcessingResult(
|
| 162 |
+
success=True,
|
| 163 |
+
original_text=sample_text_content,
|
| 164 |
+
translated_text=None,
|
| 165 |
+
audio_output=None,
|
| 166 |
+
error_message="This should not be here",
|
| 167 |
+
processing_time=1.0
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
def test_successful_result_without_original_text_raises_error(self):
|
| 171 |
+
"""Test that successful result without original text raises ValueError."""
|
| 172 |
+
with pytest.raises(ValueError, match="Successful result must have original text"):
|
| 173 |
+
ProcessingResult(
|
| 174 |
+
success=True,
|
| 175 |
+
original_text=None,
|
| 176 |
+
translated_text=None,
|
| 177 |
+
audio_output=None,
|
| 178 |
+
error_message=None,
|
| 179 |
+
processing_time=1.0
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
def test_failed_result_without_error_message_raises_error(self):
|
| 183 |
+
"""Test that failed result without error message raises ValueError."""
|
| 184 |
+
with pytest.raises(ValueError, match="Failed result must have a non-empty error message"):
|
| 185 |
+
ProcessingResult(
|
| 186 |
+
success=False,
|
| 187 |
+
original_text=None,
|
| 188 |
+
translated_text=None,
|
| 189 |
+
audio_output=None,
|
| 190 |
+
error_message=None,
|
| 191 |
+
processing_time=1.0
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
def test_failed_result_with_empty_error_message_raises_error(self):
|
| 195 |
+
"""Test that failed result with empty error message raises ValueError."""
|
| 196 |
+
with pytest.raises(ValueError, match="Failed result must have a non-empty error message"):
|
| 197 |
+
ProcessingResult(
|
| 198 |
+
success=False,
|
| 199 |
+
original_text=None,
|
| 200 |
+
translated_text=None,
|
| 201 |
+
audio_output=None,
|
| 202 |
+
error_message="",
|
| 203 |
+
processing_time=1.0
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
def test_failed_result_with_whitespace_error_message_raises_error(self):
|
| 207 |
+
"""Test that failed result with whitespace-only error message raises ValueError."""
|
| 208 |
+
with pytest.raises(ValueError, match="Failed result must have a non-empty error message"):
|
| 209 |
+
ProcessingResult(
|
| 210 |
+
success=False,
|
| 211 |
+
original_text=None,
|
| 212 |
+
translated_text=None,
|
| 213 |
+
audio_output=None,
|
| 214 |
+
error_message=" ",
|
| 215 |
+
processing_time=1.0
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
def test_has_translation_property(self, sample_text_content, sample_translated_text):
|
| 219 |
+
"""Test has_translation property."""
|
| 220 |
+
# With translation
|
| 221 |
+
result_with_translation = ProcessingResult(
|
| 222 |
+
success=True,
|
| 223 |
+
original_text=sample_text_content,
|
| 224 |
+
translated_text=sample_translated_text,
|
| 225 |
+
audio_output=None,
|
| 226 |
+
error_message=None,
|
| 227 |
+
processing_time=1.0
|
| 228 |
+
)
|
| 229 |
+
assert result_with_translation.has_translation is True
|
| 230 |
+
|
| 231 |
+
# Without translation
|
| 232 |
+
result_without_translation = ProcessingResult(
|
| 233 |
+
success=True,
|
| 234 |
+
original_text=sample_text_content,
|
| 235 |
+
translated_text=None,
|
| 236 |
+
audio_output=None,
|
| 237 |
+
error_message=None,
|
| 238 |
+
processing_time=1.0
|
| 239 |
+
)
|
| 240 |
+
assert result_without_translation.has_translation is False
|
| 241 |
+
|
| 242 |
+
def test_has_audio_output_property(self, sample_text_content, sample_audio_content):
|
| 243 |
+
"""Test has_audio_output property."""
|
| 244 |
+
# With audio output
|
| 245 |
+
result_with_audio = ProcessingResult(
|
| 246 |
+
success=True,
|
| 247 |
+
original_text=sample_text_content,
|
| 248 |
+
translated_text=None,
|
| 249 |
+
audio_output=sample_audio_content,
|
| 250 |
+
error_message=None,
|
| 251 |
+
processing_time=1.0
|
| 252 |
+
)
|
| 253 |
+
assert result_with_audio.has_audio_output is True
|
| 254 |
+
|
| 255 |
+
# Without audio output
|
| 256 |
+
result_without_audio = ProcessingResult(
|
| 257 |
+
success=True,
|
| 258 |
+
original_text=sample_text_content,
|
| 259 |
+
translated_text=None,
|
| 260 |
+
audio_output=None,
|
| 261 |
+
error_message=None,
|
| 262 |
+
processing_time=1.0
|
| 263 |
+
)
|
| 264 |
+
assert result_without_audio.has_audio_output is False
|
| 265 |
+
|
| 266 |
+
def test_is_complete_pipeline_property(self, sample_text_content, sample_translated_text, sample_audio_content):
|
| 267 |
+
"""Test is_complete_pipeline property."""
|
| 268 |
+
# Complete pipeline
|
| 269 |
+
complete_result = ProcessingResult(
|
| 270 |
+
success=True,
|
| 271 |
+
original_text=sample_text_content,
|
| 272 |
+
translated_text=sample_translated_text,
|
| 273 |
+
audio_output=sample_audio_content,
|
| 274 |
+
error_message=None,
|
| 275 |
+
processing_time=1.0
|
| 276 |
+
)
|
| 277 |
+
assert complete_result.is_complete_pipeline is True
|
| 278 |
+
|
| 279 |
+
# Incomplete pipeline (missing translation)
|
| 280 |
+
incomplete_result = ProcessingResult(
|
| 281 |
+
success=True,
|
| 282 |
+
original_text=sample_text_content,
|
| 283 |
+
translated_text=None,
|
| 284 |
+
audio_output=sample_audio_content,
|
| 285 |
+
error_message=None,
|
| 286 |
+
processing_time=1.0
|
| 287 |
+
)
|
| 288 |
+
assert incomplete_result.is_complete_pipeline is False
|
| 289 |
+
|
| 290 |
+
# Failed result
|
| 291 |
+
failed_result = ProcessingResult(
|
| 292 |
+
success=False,
|
| 293 |
+
original_text=None,
|
| 294 |
+
translated_text=None,
|
| 295 |
+
audio_output=None,
|
| 296 |
+
error_message="Failed",
|
| 297 |
+
processing_time=1.0
|
| 298 |
+
)
|
| 299 |
+
assert failed_result.is_complete_pipeline is False
|
| 300 |
+
|
| 301 |
+
def test_success_result_class_method(self, sample_text_content, sample_translated_text, sample_audio_content):
|
| 302 |
+
"""Test success_result class method."""
|
| 303 |
+
result = ProcessingResult.success_result(
|
| 304 |
+
original_text=sample_text_content,
|
| 305 |
+
translated_text=sample_translated_text,
|
| 306 |
+
audio_output=sample_audio_content,
|
| 307 |
+
processing_time=2.5
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
assert result.success is True
|
| 311 |
+
assert result.original_text == sample_text_content
|
| 312 |
+
assert result.translated_text == sample_translated_text
|
| 313 |
+
assert result.audio_output == sample_audio_content
|
| 314 |
+
assert result.error_message is None
|
| 315 |
+
assert result.processing_time == 2.5
|
| 316 |
+
|
| 317 |
+
def test_success_result_with_minimal_parameters(self, sample_text_content):
|
| 318 |
+
"""Test success_result class method with minimal parameters."""
|
| 319 |
+
result = ProcessingResult.success_result(
|
| 320 |
+
original_text=sample_text_content
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
assert result.success is True
|
| 324 |
+
assert result.original_text == sample_text_content
|
| 325 |
+
assert result.translated_text is None
|
| 326 |
+
assert result.audio_output is None
|
| 327 |
+
assert result.error_message is None
|
| 328 |
+
assert result.processing_time == 0.0
|
| 329 |
+
|
| 330 |
+
def test_failure_result_class_method(self, sample_text_content):
|
| 331 |
+
"""Test failure_result class method."""
|
| 332 |
+
result = ProcessingResult.failure_result(
|
| 333 |
+
error_message="Something went wrong",
|
| 334 |
+
processing_time=1.5,
|
| 335 |
+
original_text=sample_text_content
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
assert result.success is False
|
| 339 |
+
assert result.original_text == sample_text_content
|
| 340 |
+
assert result.translated_text is None
|
| 341 |
+
assert result.audio_output is None
|
| 342 |
+
assert result.error_message == "Something went wrong"
|
| 343 |
+
assert result.processing_time == 1.5
|
| 344 |
+
|
| 345 |
+
def test_failure_result_with_minimal_parameters(self):
|
| 346 |
+
"""Test failure_result class method with minimal parameters."""
|
| 347 |
+
result = ProcessingResult.failure_result(
|
| 348 |
+
error_message="Failed"
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
assert result.success is False
|
| 352 |
+
assert result.original_text is None
|
| 353 |
+
assert result.translated_text is None
|
| 354 |
+
assert result.audio_output is None
|
| 355 |
+
assert result.error_message == "Failed"
|
| 356 |
+
assert result.processing_time == 0.0
|
| 357 |
+
|
| 358 |
+
def test_processing_result_is_immutable(self, sample_text_content):
|
| 359 |
+
"""Test that ProcessingResult is immutable (frozen dataclass)."""
|
| 360 |
+
result = ProcessingResult(
|
| 361 |
+
success=True,
|
| 362 |
+
original_text=sample_text_content,
|
| 363 |
+
translated_text=None,
|
| 364 |
+
audio_output=None,
|
| 365 |
+
error_message=None,
|
| 366 |
+
processing_time=1.0
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
with pytest.raises(AttributeError):
|
| 370 |
+
result.success = False # type: ignore
|
| 371 |
+
|
| 372 |
+
def test_zero_processing_time_valid(self, sample_text_content):
|
| 373 |
+
"""Test that zero processing time is valid."""
|
| 374 |
+
result = ProcessingResult(
|
| 375 |
+
success=True,
|
| 376 |
+
original_text=sample_text_content,
|
| 377 |
+
translated_text=None,
|
| 378 |
+
audio_output=None,
|
| 379 |
+
error_message=None,
|
| 380 |
+
processing_time=0.0
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
assert result.processing_time == 0.0
|
| 384 |
+
|
| 385 |
+
def test_partial_success_scenarios(self, sample_text_content, sample_translated_text):
|
| 386 |
+
"""Test various partial success scenarios."""
|
| 387 |
+
# Only STT completed
|
| 388 |
+
stt_only = ProcessingResult(
|
| 389 |
+
success=True,
|
| 390 |
+
original_text=sample_text_content,
|
| 391 |
+
translated_text=None,
|
| 392 |
+
audio_output=None,
|
| 393 |
+
error_message=None,
|
| 394 |
+
processing_time=1.0
|
| 395 |
+
)
|
| 396 |
+
assert stt_only.has_translation is False
|
| 397 |
+
assert stt_only.has_audio_output is False
|
| 398 |
+
assert stt_only.is_complete_pipeline is False
|
| 399 |
+
|
| 400 |
+
# STT + Translation completed
|
| 401 |
+
stt_translation = ProcessingResult(
|
| 402 |
+
success=True,
|
| 403 |
+
original_text=sample_text_content,
|
| 404 |
+
translated_text=sample_translated_text,
|
| 405 |
+
audio_output=None,
|
| 406 |
+
error_message=None,
|
| 407 |
+
processing_time=1.5
|
| 408 |
+
)
|
| 409 |
+
assert stt_translation.has_translation is True
|
| 410 |
+
assert stt_translation.has_audio_output is False
|
| 411 |
+
assert stt_translation.is_complete_pipeline is False
|
tests/unit/domain/models/test_speech_synthesis_request.py
CHANGED
|
@@ -8,342 +8,316 @@ from src.domain.models.voice_settings import VoiceSettings
|
|
| 8 |
|
| 9 |
class TestSpeechSynthesisRequest:
|
| 10 |
"""Test cases for SpeechSynthesisRequest value object."""
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"""Test creating valid SpeechSynthesisRequest instance."""
|
| 14 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 15 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.2, language="en")
|
| 16 |
-
|
| 17 |
request = SpeechSynthesisRequest(
|
| 18 |
-
|
| 19 |
-
voice_settings=
|
| 20 |
output_format="wav",
|
| 21 |
-
sample_rate=
|
| 22 |
)
|
| 23 |
-
|
| 24 |
-
assert request.
|
| 25 |
-
assert request.voice_settings ==
|
| 26 |
assert request.output_format == "wav"
|
| 27 |
-
assert request.sample_rate ==
|
| 28 |
-
assert request.effective_sample_rate ==
|
| 29 |
-
|
| 30 |
-
def test_speech_synthesis_request_with_defaults(self):
|
| 31 |
"""Test creating SpeechSynthesisRequest with default values."""
|
| 32 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 33 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 34 |
-
|
| 35 |
request = SpeechSynthesisRequest(
|
| 36 |
-
|
| 37 |
-
voice_settings=
|
| 38 |
)
|
| 39 |
-
|
| 40 |
assert request.output_format == "wav"
|
| 41 |
assert request.sample_rate is None
|
| 42 |
assert request.effective_sample_rate == 22050 # Default
|
| 43 |
-
|
| 44 |
-
def test_non_text_content_raises_error(self):
|
| 45 |
-
"""Test that non-TextContent
|
| 46 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 47 |
-
|
| 48 |
with pytest.raises(TypeError, match="Text must be a TextContent instance"):
|
| 49 |
SpeechSynthesisRequest(
|
| 50 |
-
|
| 51 |
-
voice_settings=
|
| 52 |
)
|
| 53 |
-
|
| 54 |
-
def test_non_voice_settings_raises_error(self):
|
| 55 |
-
"""Test that non-VoiceSettings
|
| 56 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 57 |
-
|
| 58 |
with pytest.raises(TypeError, match="Voice settings must be a VoiceSettings instance"):
|
| 59 |
SpeechSynthesisRequest(
|
| 60 |
-
|
| 61 |
-
voice_settings=
|
| 62 |
)
|
| 63 |
-
|
| 64 |
-
def test_non_string_output_format_raises_error(self):
|
| 65 |
-
"""Test that non-string
|
| 66 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 67 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 68 |
-
|
| 69 |
with pytest.raises(TypeError, match="Output format must be a string"):
|
| 70 |
SpeechSynthesisRequest(
|
| 71 |
-
|
| 72 |
-
voice_settings=
|
| 73 |
output_format=123 # type: ignore
|
| 74 |
)
|
| 75 |
-
|
| 76 |
-
def test_unsupported_output_format_raises_error(self):
|
| 77 |
-
"""Test that unsupported
|
| 78 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 79 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 80 |
-
|
| 81 |
with pytest.raises(ValueError, match="Unsupported output format: xyz"):
|
| 82 |
SpeechSynthesisRequest(
|
| 83 |
-
|
| 84 |
-
voice_settings=
|
| 85 |
output_format="xyz"
|
| 86 |
)
|
| 87 |
-
|
| 88 |
-
def test_supported_output_formats(self):
|
| 89 |
"""Test all supported output formats."""
|
| 90 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 91 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 92 |
supported_formats = ['wav', 'mp3', 'flac', 'ogg']
|
| 93 |
-
|
| 94 |
for fmt in supported_formats:
|
| 95 |
request = SpeechSynthesisRequest(
|
| 96 |
-
|
| 97 |
-
voice_settings=
|
| 98 |
output_format=fmt
|
| 99 |
)
|
| 100 |
assert request.output_format == fmt
|
| 101 |
-
|
| 102 |
-
def test_non_integer_sample_rate_raises_error(self):
|
| 103 |
-
"""Test that non-integer
|
| 104 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 105 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 106 |
-
|
| 107 |
with pytest.raises(TypeError, match="Sample rate must be an integer"):
|
| 108 |
SpeechSynthesisRequest(
|
| 109 |
-
|
| 110 |
-
voice_settings=
|
| 111 |
-
sample_rate=
|
| 112 |
)
|
| 113 |
-
|
| 114 |
-
def test_negative_sample_rate_raises_error(self):
|
| 115 |
-
"""Test that negative
|
| 116 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 117 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 118 |
-
|
| 119 |
with pytest.raises(ValueError, match="Sample rate must be positive"):
|
| 120 |
SpeechSynthesisRequest(
|
| 121 |
-
|
| 122 |
-
voice_settings=
|
| 123 |
sample_rate=-1
|
| 124 |
)
|
| 125 |
-
|
| 126 |
-
def test_zero_sample_rate_raises_error(self):
|
| 127 |
-
"""Test that zero
|
| 128 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 129 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 130 |
-
|
| 131 |
with pytest.raises(ValueError, match="Sample rate must be positive"):
|
| 132 |
SpeechSynthesisRequest(
|
| 133 |
-
|
| 134 |
-
voice_settings=
|
| 135 |
sample_rate=0
|
| 136 |
)
|
| 137 |
-
|
| 138 |
-
def test_sample_rate_too_low_raises_error(self):
|
| 139 |
"""Test that sample rate below 8000 raises ValueError."""
|
| 140 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 141 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 142 |
-
|
| 143 |
with pytest.raises(ValueError, match="Sample rate must be between 8000 and 192000 Hz"):
|
| 144 |
SpeechSynthesisRequest(
|
| 145 |
-
|
| 146 |
-
voice_settings=
|
| 147 |
sample_rate=7999
|
| 148 |
)
|
| 149 |
-
|
| 150 |
-
def test_sample_rate_too_high_raises_error(self):
|
| 151 |
"""Test that sample rate above 192000 raises ValueError."""
|
| 152 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 153 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 154 |
-
|
| 155 |
with pytest.raises(ValueError, match="Sample rate must be between 8000 and 192000 Hz"):
|
| 156 |
SpeechSynthesisRequest(
|
| 157 |
-
|
| 158 |
-
voice_settings=
|
| 159 |
sample_rate=192001
|
| 160 |
)
|
| 161 |
-
|
| 162 |
-
def test_valid_sample_rate_boundaries(self):
|
| 163 |
"""Test valid sample rate boundaries."""
|
| 164 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 165 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 166 |
-
|
| 167 |
# Test minimum valid sample rate
|
| 168 |
request_min = SpeechSynthesisRequest(
|
| 169 |
-
|
| 170 |
-
voice_settings=
|
| 171 |
sample_rate=8000
|
| 172 |
)
|
| 173 |
assert request_min.sample_rate == 8000
|
| 174 |
-
|
| 175 |
# Test maximum valid sample rate
|
| 176 |
request_max = SpeechSynthesisRequest(
|
| 177 |
-
|
| 178 |
-
voice_settings=
|
| 179 |
sample_rate=192000
|
| 180 |
)
|
| 181 |
assert request_max.sample_rate == 192000
|
| 182 |
-
|
| 183 |
-
def
|
| 184 |
-
"""Test that
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
with pytest.raises(ValueError, match="Text language \\(en\\) must match voice language \\(fr\\)"):
|
| 189 |
SpeechSynthesisRequest(
|
| 190 |
-
|
| 191 |
-
voice_settings=
|
| 192 |
)
|
| 193 |
-
|
| 194 |
def test_matching_languages_success(self):
|
| 195 |
-
"""Test that matching text and voice
|
| 196 |
-
|
| 197 |
-
voice_settings = VoiceSettings(voice_id="
|
| 198 |
-
|
| 199 |
request = SpeechSynthesisRequest(
|
| 200 |
-
|
| 201 |
voice_settings=voice_settings
|
| 202 |
)
|
| 203 |
-
|
| 204 |
-
assert request.
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
def test_estimated_duration_seconds_property(self):
|
| 208 |
"""Test estimated_duration_seconds property calculation."""
|
| 209 |
-
text = TextContent(text="Hello world test", language="en") # 3 words
|
| 210 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 211 |
-
|
| 212 |
request = SpeechSynthesisRequest(
|
| 213 |
-
|
| 214 |
-
voice_settings=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
)
|
| 216 |
-
|
| 217 |
-
#
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
def test_estimated_duration_with_speed_adjustment(self):
|
| 222 |
-
"""Test estimated duration with different speed settings."""
|
| 223 |
-
text = TextContent(text="Hello world test", language="en") # 3 words
|
| 224 |
-
voice_settings_slow = VoiceSettings(voice_id="en_male_001", speed=0.5, language="en")
|
| 225 |
-
voice_settings_fast = VoiceSettings(voice_id="en_male_001", speed=2.0, language="en")
|
| 226 |
-
|
| 227 |
-
request_slow = SpeechSynthesisRequest(text=text, voice_settings=voice_settings_slow)
|
| 228 |
-
request_fast = SpeechSynthesisRequest(text=text, voice_settings=voice_settings_fast)
|
| 229 |
-
|
| 230 |
-
# Slower speed should result in longer duration
|
| 231 |
-
assert request_slow.estimated_duration_seconds > request_fast.estimated_duration_seconds
|
| 232 |
-
|
| 233 |
-
def test_is_long_text_property(self):
|
| 234 |
"""Test is_long_text property."""
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
request_long = SpeechSynthesisRequest(text=long_text, voice_settings=voice_settings)
|
| 241 |
-
|
| 242 |
-
assert request_short.is_long_text is False
|
| 243 |
-
assert request_long.is_long_text is True
|
| 244 |
-
|
| 245 |
-
def test_effective_sample_rate_property(self):
|
| 246 |
-
"""Test effective_sample_rate property."""
|
| 247 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 248 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 249 |
-
|
| 250 |
-
# With explicit sample rate
|
| 251 |
-
request_explicit = SpeechSynthesisRequest(
|
| 252 |
-
text=text,
|
| 253 |
-
voice_settings=voice_settings,
|
| 254 |
-
sample_rate=44100
|
| 255 |
)
|
| 256 |
-
assert
|
| 257 |
-
|
| 258 |
-
#
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
|
|
|
| 262 |
)
|
| 263 |
-
assert
|
| 264 |
-
|
| 265 |
-
def test_with_output_format_method(self):
|
| 266 |
"""Test with_output_format method creates new instance."""
|
| 267 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 268 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 269 |
-
|
| 270 |
original = SpeechSynthesisRequest(
|
| 271 |
-
|
| 272 |
-
voice_settings=
|
| 273 |
output_format="wav",
|
| 274 |
-
sample_rate=
|
| 275 |
)
|
| 276 |
-
|
| 277 |
new_request = original.with_output_format("mp3")
|
| 278 |
-
|
| 279 |
assert new_request.output_format == "mp3"
|
| 280 |
-
assert new_request.
|
| 281 |
assert new_request.voice_settings == original.voice_settings
|
| 282 |
assert new_request.sample_rate == original.sample_rate
|
| 283 |
assert new_request is not original # Different instances
|
| 284 |
-
|
| 285 |
-
def test_with_sample_rate_method(self):
|
| 286 |
"""Test with_sample_rate method creates new instance."""
|
| 287 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 288 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 289 |
-
|
| 290 |
original = SpeechSynthesisRequest(
|
| 291 |
-
|
| 292 |
-
voice_settings=
|
| 293 |
-
sample_rate=
|
| 294 |
)
|
| 295 |
-
|
| 296 |
-
new_request = original.with_sample_rate(
|
| 297 |
-
|
| 298 |
-
assert new_request.sample_rate ==
|
| 299 |
-
assert new_request.
|
| 300 |
assert new_request.voice_settings == original.voice_settings
|
| 301 |
assert new_request.output_format == original.output_format
|
| 302 |
assert new_request is not original # Different instances
|
| 303 |
-
|
| 304 |
-
def test_with_sample_rate_none(self):
|
| 305 |
"""Test with_sample_rate method with None value."""
|
| 306 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 307 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 308 |
-
|
| 309 |
original = SpeechSynthesisRequest(
|
| 310 |
-
|
| 311 |
-
voice_settings=
|
| 312 |
-
sample_rate=
|
| 313 |
)
|
| 314 |
-
|
| 315 |
new_request = original.with_sample_rate(None)
|
| 316 |
assert new_request.sample_rate is None
|
| 317 |
-
assert new_request.effective_sample_rate == 22050
|
| 318 |
-
|
| 319 |
-
def test_with_voice_settings_method(self):
|
| 320 |
"""Test with_voice_settings method creates new instance."""
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
new_voice = VoiceSettings(voice_id="en_female_001", speed=1.5, language="en")
|
| 324 |
-
|
| 325 |
original = SpeechSynthesisRequest(
|
| 326 |
-
|
| 327 |
-
voice_settings=
|
| 328 |
)
|
| 329 |
-
|
| 330 |
-
new_request = original.with_voice_settings(
|
| 331 |
-
|
| 332 |
-
assert new_request.voice_settings ==
|
| 333 |
-
assert new_request.
|
| 334 |
assert new_request.output_format == original.output_format
|
| 335 |
assert new_request.sample_rate == original.sample_rate
|
| 336 |
assert new_request is not original # Different instances
|
| 337 |
-
|
| 338 |
-
def test_speech_synthesis_request_is_immutable(self):
|
| 339 |
"""Test that SpeechSynthesisRequest is immutable (frozen dataclass)."""
|
| 340 |
-
text = TextContent(text="Hello, world!", language="en")
|
| 341 |
-
voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
|
| 342 |
-
|
| 343 |
request = SpeechSynthesisRequest(
|
| 344 |
-
|
| 345 |
-
voice_settings=
|
| 346 |
)
|
| 347 |
-
|
| 348 |
with pytest.raises(AttributeError):
|
| 349 |
-
request.output_format = "mp3" # type: ignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
class TestSpeechSynthesisRequest:
|
| 10 |
"""Test cases for SpeechSynthesisRequest value object."""
|
| 11 |
+
|
| 12 |
+
@pytest.fixture
|
| 13 |
+
def sample_text_content(self):
|
| 14 |
+
"""Sample text content for testing."""
|
| 15 |
+
return TextContent(
|
| 16 |
+
text="Hello, world!",
|
| 17 |
+
language="en"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
@pytest.fixture
|
| 21 |
+
def sample_voice_settings(self):
|
| 22 |
+
"""Sample voice settings for testing."""
|
| 23 |
+
return VoiceSettings(
|
| 24 |
+
voice_id="en_male_001",
|
| 25 |
+
speed=1.0,
|
| 26 |
+
language="en"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
def test_valid_speech_synthesis_request_creation(self, sample_text_content, sample_voice_settings):
|
| 30 |
"""Test creating valid SpeechSynthesisRequest instance."""
|
|
|
|
|
|
|
|
|
|
| 31 |
request = SpeechSynthesisRequest(
|
| 32 |
+
text_content=sample_text_content,
|
| 33 |
+
voice_settings=sample_voice_settings,
|
| 34 |
output_format="wav",
|
| 35 |
+
sample_rate=22050
|
| 36 |
)
|
| 37 |
+
|
| 38 |
+
assert request.text_content == sample_text_content
|
| 39 |
+
assert request.voice_settings == sample_voice_settings
|
| 40 |
assert request.output_format == "wav"
|
| 41 |
+
assert request.sample_rate == 22050
|
| 42 |
+
assert request.effective_sample_rate == 22050
|
| 43 |
+
|
| 44 |
+
def test_speech_synthesis_request_with_defaults(self, sample_text_content, sample_voice_settings):
|
| 45 |
"""Test creating SpeechSynthesisRequest with default values."""
|
|
|
|
|
|
|
|
|
|
| 46 |
request = SpeechSynthesisRequest(
|
| 47 |
+
text_content=sample_text_content,
|
| 48 |
+
voice_settings=sample_voice_settings
|
| 49 |
)
|
| 50 |
+
|
| 51 |
assert request.output_format == "wav"
|
| 52 |
assert request.sample_rate is None
|
| 53 |
assert request.effective_sample_rate == 22050 # Default
|
| 54 |
+
|
| 55 |
+
def test_non_text_content_raises_error(self, sample_voice_settings):
|
| 56 |
+
"""Test that non-TextContent raises TypeError."""
|
|
|
|
|
|
|
| 57 |
with pytest.raises(TypeError, match="Text must be a TextContent instance"):
|
| 58 |
SpeechSynthesisRequest(
|
| 59 |
+
text_content="not a TextContent", # type: ignore
|
| 60 |
+
voice_settings=sample_voice_settings
|
| 61 |
)
|
| 62 |
+
|
| 63 |
+
def test_non_voice_settings_raises_error(self, sample_text_content):
|
| 64 |
+
"""Test that non-VoiceSettings raises TypeError."""
|
|
|
|
|
|
|
| 65 |
with pytest.raises(TypeError, match="Voice settings must be a VoiceSettings instance"):
|
| 66 |
SpeechSynthesisRequest(
|
| 67 |
+
text_content=sample_text_content,
|
| 68 |
+
voice_settings="not voice settings" # type: ignore
|
| 69 |
)
|
| 70 |
+
|
| 71 |
+
def test_non_string_output_format_raises_error(self, sample_text_content, sample_voice_settings):
|
| 72 |
+
"""Test that non-string output format raises TypeError."""
|
|
|
|
|
|
|
|
|
|
| 73 |
with pytest.raises(TypeError, match="Output format must be a string"):
|
| 74 |
SpeechSynthesisRequest(
|
| 75 |
+
text_content=sample_text_content,
|
| 76 |
+
voice_settings=sample_voice_settings,
|
| 77 |
output_format=123 # type: ignore
|
| 78 |
)
|
| 79 |
+
|
| 80 |
+
def test_unsupported_output_format_raises_error(self, sample_text_content, sample_voice_settings):
|
| 81 |
+
"""Test that unsupported output format raises ValueError."""
|
|
|
|
|
|
|
|
|
|
| 82 |
with pytest.raises(ValueError, match="Unsupported output format: xyz"):
|
| 83 |
SpeechSynthesisRequest(
|
| 84 |
+
text_content=sample_text_content,
|
| 85 |
+
voice_settings=sample_voice_settings,
|
| 86 |
output_format="xyz"
|
| 87 |
)
|
| 88 |
+
|
| 89 |
+
def test_supported_output_formats(self, sample_text_content, sample_voice_settings):
|
| 90 |
"""Test all supported output formats."""
|
|
|
|
|
|
|
| 91 |
supported_formats = ['wav', 'mp3', 'flac', 'ogg']
|
| 92 |
+
|
| 93 |
for fmt in supported_formats:
|
| 94 |
request = SpeechSynthesisRequest(
|
| 95 |
+
text_content=sample_text_content,
|
| 96 |
+
voice_settings=sample_voice_settings,
|
| 97 |
output_format=fmt
|
| 98 |
)
|
| 99 |
assert request.output_format == fmt
|
| 100 |
+
|
| 101 |
+
def test_non_integer_sample_rate_raises_error(self, sample_text_content, sample_voice_settings):
|
| 102 |
+
"""Test that non-integer sample rate raises TypeError."""
|
|
|
|
|
|
|
|
|
|
| 103 |
with pytest.raises(TypeError, match="Sample rate must be an integer"):
|
| 104 |
SpeechSynthesisRequest(
|
| 105 |
+
text_content=sample_text_content,
|
| 106 |
+
voice_settings=sample_voice_settings,
|
| 107 |
+
sample_rate=22050.5 # type: ignore
|
| 108 |
)
|
| 109 |
+
|
| 110 |
+
def test_negative_sample_rate_raises_error(self, sample_text_content, sample_voice_settings):
|
| 111 |
+
"""Test that negative sample rate raises ValueError."""
|
|
|
|
|
|
|
|
|
|
| 112 |
with pytest.raises(ValueError, match="Sample rate must be positive"):
|
| 113 |
SpeechSynthesisRequest(
|
| 114 |
+
text_content=sample_text_content,
|
| 115 |
+
voice_settings=sample_voice_settings,
|
| 116 |
sample_rate=-1
|
| 117 |
)
|
| 118 |
+
|
| 119 |
+
def test_zero_sample_rate_raises_error(self, sample_text_content, sample_voice_settings):
|
| 120 |
+
"""Test that zero sample rate raises ValueError."""
|
|
|
|
|
|
|
|
|
|
| 121 |
with pytest.raises(ValueError, match="Sample rate must be positive"):
|
| 122 |
SpeechSynthesisRequest(
|
| 123 |
+
text_content=sample_text_content,
|
| 124 |
+
voice_settings=sample_voice_settings,
|
| 125 |
sample_rate=0
|
| 126 |
)
|
| 127 |
+
|
| 128 |
+
def test_sample_rate_too_low_raises_error(self, sample_text_content, sample_voice_settings):
|
| 129 |
"""Test that sample rate below 8000 raises ValueError."""
|
|
|
|
|
|
|
|
|
|
| 130 |
with pytest.raises(ValueError, match="Sample rate must be between 8000 and 192000 Hz"):
|
| 131 |
SpeechSynthesisRequest(
|
| 132 |
+
text_content=sample_text_content,
|
| 133 |
+
voice_settings=sample_voice_settings,
|
| 134 |
sample_rate=7999
|
| 135 |
)
|
| 136 |
+
|
| 137 |
+
def test_sample_rate_too_high_raises_error(self, sample_text_content, sample_voice_settings):
|
| 138 |
"""Test that sample rate above 192000 raises ValueError."""
|
|
|
|
|
|
|
|
|
|
| 139 |
with pytest.raises(ValueError, match="Sample rate must be between 8000 and 192000 Hz"):
|
| 140 |
SpeechSynthesisRequest(
|
| 141 |
+
text_content=sample_text_content,
|
| 142 |
+
voice_settings=sample_voice_settings,
|
| 143 |
sample_rate=192001
|
| 144 |
)
|
| 145 |
+
|
| 146 |
+
def test_valid_sample_rate_boundaries(self, sample_text_content, sample_voice_settings):
|
| 147 |
"""Test valid sample rate boundaries."""
|
|
|
|
|
|
|
|
|
|
| 148 |
# Test minimum valid sample rate
|
| 149 |
request_min = SpeechSynthesisRequest(
|
| 150 |
+
text_content=sample_text_content,
|
| 151 |
+
voice_settings=sample_voice_settings,
|
| 152 |
sample_rate=8000
|
| 153 |
)
|
| 154 |
assert request_min.sample_rate == 8000
|
| 155 |
+
|
| 156 |
# Test maximum valid sample rate
|
| 157 |
request_max = SpeechSynthesisRequest(
|
| 158 |
+
text_content=sample_text_content,
|
| 159 |
+
voice_settings=sample_voice_settings,
|
| 160 |
sample_rate=192000
|
| 161 |
)
|
| 162 |
assert request_max.sample_rate == 192000
|
| 163 |
+
|
| 164 |
+
def test_language_mismatch_raises_error(self, sample_voice_settings):
|
| 165 |
+
"""Test that language mismatch between text and voice raises ValueError."""
|
| 166 |
+
text_content = TextContent(text="Hola mundo", language="es")
|
| 167 |
+
|
| 168 |
+
with pytest.raises(ValueError, match="Text language \\(es\\) must match voice language \\(en\\)"):
|
|
|
|
| 169 |
SpeechSynthesisRequest(
|
| 170 |
+
text_content=text_content,
|
| 171 |
+
voice_settings=sample_voice_settings # language="en"
|
| 172 |
)
|
| 173 |
+
|
| 174 |
def test_matching_languages_success(self):
|
| 175 |
+
"""Test that matching languages between text and voice works."""
|
| 176 |
+
text_content = TextContent(text="Hola mundo", language="es")
|
| 177 |
+
voice_settings = VoiceSettings(voice_id="es_female_001", speed=1.0, language="es")
|
| 178 |
+
|
| 179 |
request = SpeechSynthesisRequest(
|
| 180 |
+
text_content=text_content,
|
| 181 |
voice_settings=voice_settings
|
| 182 |
)
|
| 183 |
+
|
| 184 |
+
assert request.text_content.language == request.voice_settings.language
|
| 185 |
+
|
| 186 |
+
def test_estimated_duration_seconds_property(self, sample_text_content, sample_voice_settings):
|
|
|
|
| 187 |
"""Test estimated_duration_seconds property calculation."""
|
|
|
|
|
|
|
|
|
|
| 188 |
request = SpeechSynthesisRequest(
|
| 189 |
+
text_content=sample_text_content,
|
| 190 |
+
voice_settings=sample_voice_settings
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
# Should be based on word count and speed
|
| 194 |
+
expected_duration = (sample_text_content.word_count / (175 / sample_voice_settings.speed)) * 60
|
| 195 |
+
assert abs(request.estimated_duration_seconds - expected_duration) < 0.01
|
| 196 |
+
|
| 197 |
+
def test_estimated_duration_with_different_speed(self, sample_text_content):
|
| 198 |
+
"""Test estimated duration with different speech speed."""
|
| 199 |
+
fast_voice = VoiceSettings(voice_id="test", speed=2.0, language="en")
|
| 200 |
+
slow_voice = VoiceSettings(voice_id="test", speed=0.5, language="en")
|
| 201 |
+
|
| 202 |
+
fast_request = SpeechSynthesisRequest(
|
| 203 |
+
text_content=sample_text_content,
|
| 204 |
+
voice_settings=fast_voice
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
slow_request = SpeechSynthesisRequest(
|
| 208 |
+
text_content=sample_text_content,
|
| 209 |
+
voice_settings=slow_voice
|
| 210 |
)
|
| 211 |
+
|
| 212 |
+
# Faster speed should result in shorter duration
|
| 213 |
+
assert fast_request.estimated_duration_seconds < slow_request.estimated_duration_seconds
|
| 214 |
+
|
| 215 |
+
def test_is_long_text_property(self, sample_voice_settings):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
"""Test is_long_text property."""
|
| 217 |
+
# Short text
|
| 218 |
+
short_text = TextContent(text="Hello", language="en")
|
| 219 |
+
short_request = SpeechSynthesisRequest(
|
| 220 |
+
text_content=short_text,
|
| 221 |
+
voice_settings=sample_voice_settings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
)
|
| 223 |
+
assert short_request.is_long_text is False
|
| 224 |
+
|
| 225 |
+
# Long text (over 5000 characters)
|
| 226 |
+
long_text = TextContent(text="a" * 5001, language="en")
|
| 227 |
+
long_request = SpeechSynthesisRequest(
|
| 228 |
+
text_content=long_text,
|
| 229 |
+
voice_settings=sample_voice_settings
|
| 230 |
)
|
| 231 |
+
assert long_request.is_long_text is True
|
| 232 |
+
|
| 233 |
+
def test_with_output_format_method(self, sample_text_content, sample_voice_settings):
|
| 234 |
"""Test with_output_format method creates new instance."""
|
|
|
|
|
|
|
|
|
|
| 235 |
original = SpeechSynthesisRequest(
|
| 236 |
+
text_content=sample_text_content,
|
| 237 |
+
voice_settings=sample_voice_settings,
|
| 238 |
output_format="wav",
|
| 239 |
+
sample_rate=22050
|
| 240 |
)
|
| 241 |
+
|
| 242 |
new_request = original.with_output_format("mp3")
|
| 243 |
+
|
| 244 |
assert new_request.output_format == "mp3"
|
| 245 |
+
assert new_request.text_content == original.text_content
|
| 246 |
assert new_request.voice_settings == original.voice_settings
|
| 247 |
assert new_request.sample_rate == original.sample_rate
|
| 248 |
assert new_request is not original # Different instances
|
| 249 |
+
|
| 250 |
+
def test_with_sample_rate_method(self, sample_text_content, sample_voice_settings):
|
| 251 |
"""Test with_sample_rate method creates new instance."""
|
|
|
|
|
|
|
|
|
|
| 252 |
original = SpeechSynthesisRequest(
|
| 253 |
+
text_content=sample_text_content,
|
| 254 |
+
voice_settings=sample_voice_settings,
|
| 255 |
+
sample_rate=22050
|
| 256 |
)
|
| 257 |
+
|
| 258 |
+
new_request = original.with_sample_rate(44100)
|
| 259 |
+
|
| 260 |
+
assert new_request.sample_rate == 44100
|
| 261 |
+
assert new_request.text_content == original.text_content
|
| 262 |
assert new_request.voice_settings == original.voice_settings
|
| 263 |
assert new_request.output_format == original.output_format
|
| 264 |
assert new_request is not original # Different instances
|
| 265 |
+
|
| 266 |
+
def test_with_sample_rate_none(self, sample_text_content, sample_voice_settings):
|
| 267 |
"""Test with_sample_rate method with None value."""
|
|
|
|
|
|
|
|
|
|
| 268 |
original = SpeechSynthesisRequest(
|
| 269 |
+
text_content=sample_text_content,
|
| 270 |
+
voice_settings=sample_voice_settings,
|
| 271 |
+
sample_rate=22050
|
| 272 |
)
|
| 273 |
+
|
| 274 |
new_request = original.with_sample_rate(None)
|
| 275 |
assert new_request.sample_rate is None
|
| 276 |
+
assert new_request.effective_sample_rate == 22050 # Default
|
| 277 |
+
|
| 278 |
+
def test_with_voice_settings_method(self, sample_text_content, sample_voice_settings):
|
| 279 |
"""Test with_voice_settings method creates new instance."""
|
| 280 |
+
new_voice_settings = VoiceSettings(voice_id="en_female_001", speed=1.5, language="en")
|
| 281 |
+
|
|
|
|
|
|
|
| 282 |
original = SpeechSynthesisRequest(
|
| 283 |
+
text_content=sample_text_content,
|
| 284 |
+
voice_settings=sample_voice_settings
|
| 285 |
)
|
| 286 |
+
|
| 287 |
+
new_request = original.with_voice_settings(new_voice_settings)
|
| 288 |
+
|
| 289 |
+
assert new_request.voice_settings == new_voice_settings
|
| 290 |
+
assert new_request.text_content == original.text_content
|
| 291 |
assert new_request.output_format == original.output_format
|
| 292 |
assert new_request.sample_rate == original.sample_rate
|
| 293 |
assert new_request is not original # Different instances
|
| 294 |
+
|
| 295 |
+
def test_speech_synthesis_request_is_immutable(self, sample_text_content, sample_voice_settings):
|
| 296 |
"""Test that SpeechSynthesisRequest is immutable (frozen dataclass)."""
|
|
|
|
|
|
|
|
|
|
| 297 |
request = SpeechSynthesisRequest(
|
| 298 |
+
text_content=sample_text_content,
|
| 299 |
+
voice_settings=sample_voice_settings
|
| 300 |
)
|
| 301 |
+
|
| 302 |
with pytest.raises(AttributeError):
|
| 303 |
+
request.output_format = "mp3" # type: ignore
|
| 304 |
+
|
| 305 |
+
def test_effective_sample_rate_with_none(self, sample_text_content, sample_voice_settings):
|
| 306 |
+
"""Test effective_sample_rate when sample_rate is None."""
|
| 307 |
+
request = SpeechSynthesisRequest(
|
| 308 |
+
text_content=sample_text_content,
|
| 309 |
+
voice_settings=sample_voice_settings,
|
| 310 |
+
sample_rate=None
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
assert request.effective_sample_rate == 22050 # Default value
|
| 314 |
+
|
| 315 |
+
def test_effective_sample_rate_with_value(self, sample_text_content, sample_voice_settings):
|
| 316 |
+
"""Test effective_sample_rate when sample_rate has a value."""
|
| 317 |
+
request = SpeechSynthesisRequest(
|
| 318 |
+
text_content=sample_text_content,
|
| 319 |
+
voice_settings=sample_voice_settings,
|
| 320 |
+
sample_rate=44100
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
assert request.effective_sample_rate == 44100
|
tests/unit/domain/models/test_translation_request.py
CHANGED
|
@@ -7,266 +7,237 @@ from src.domain.models.text_content import TextContent
|
|
| 7 |
|
| 8 |
class TestTranslationRequest:
|
| 9 |
"""Test cases for TranslationRequest value object."""
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"""Test creating valid TranslationRequest instance."""
|
| 13 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 14 |
request = TranslationRequest(
|
| 15 |
-
source_text=
|
| 16 |
-
target_language="
|
| 17 |
source_language="en"
|
| 18 |
)
|
| 19 |
-
|
| 20 |
-
assert request.source_text ==
|
| 21 |
-
assert request.target_language == "
|
| 22 |
assert request.source_language == "en"
|
| 23 |
assert request.effective_source_language == "en"
|
| 24 |
assert request.is_auto_detect_source is False
|
| 25 |
-
|
| 26 |
-
def test_translation_request_without_source_language(self):
|
| 27 |
"""Test creating TranslationRequest without explicit source language."""
|
| 28 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 29 |
request = TranslationRequest(
|
| 30 |
-
source_text=
|
| 31 |
-
target_language="
|
| 32 |
)
|
| 33 |
-
|
| 34 |
assert request.source_language is None
|
| 35 |
assert request.effective_source_language == "en" # From TextContent
|
| 36 |
assert request.is_auto_detect_source is True
|
| 37 |
-
|
| 38 |
def test_non_text_content_source_raises_error(self):
|
| 39 |
-
"""Test that non-TextContent
|
| 40 |
with pytest.raises(TypeError, match="Source text must be a TextContent instance"):
|
| 41 |
TranslationRequest(
|
| 42 |
-
source_text="
|
| 43 |
-
target_language="
|
| 44 |
)
|
| 45 |
-
|
| 46 |
-
def test_non_string_target_language_raises_error(self):
|
| 47 |
-
"""Test that non-string
|
| 48 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 49 |
with pytest.raises(TypeError, match="Target language must be a string"):
|
| 50 |
TranslationRequest(
|
| 51 |
-
source_text=
|
| 52 |
target_language=123 # type: ignore
|
| 53 |
)
|
| 54 |
-
|
| 55 |
-
def test_empty_target_language_raises_error(self):
|
| 56 |
-
"""Test that empty
|
| 57 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 58 |
with pytest.raises(ValueError, match="Target language cannot be empty"):
|
| 59 |
TranslationRequest(
|
| 60 |
-
source_text=
|
| 61 |
target_language=""
|
| 62 |
)
|
| 63 |
-
|
| 64 |
-
def test_whitespace_target_language_raises_error(self):
|
| 65 |
-
"""Test that whitespace-only
|
| 66 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 67 |
with pytest.raises(ValueError, match="Target language cannot be empty"):
|
| 68 |
TranslationRequest(
|
| 69 |
-
source_text=
|
| 70 |
target_language=" "
|
| 71 |
)
|
| 72 |
-
|
| 73 |
-
def test_invalid_target_language_format_raises_error(self):
|
| 74 |
"""Test that invalid target language format raises ValueError."""
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
for code in invalid_codes:
|
| 79 |
with pytest.raises(ValueError, match="Invalid target language code format"):
|
| 80 |
TranslationRequest(
|
| 81 |
-
source_text=
|
| 82 |
target_language=code
|
| 83 |
)
|
| 84 |
-
|
| 85 |
-
def test_valid_target_language_codes(self):
|
| 86 |
"""Test valid target language code formats."""
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
for code in valid_codes:
|
| 91 |
request = TranslationRequest(
|
| 92 |
-
source_text=
|
| 93 |
target_language=code
|
| 94 |
)
|
| 95 |
assert request.target_language == code
|
| 96 |
-
|
| 97 |
-
def test_non_string_source_language_raises_error(self):
|
| 98 |
-
"""Test that non-string
|
| 99 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 100 |
with pytest.raises(TypeError, match="Source language must be a string"):
|
| 101 |
TranslationRequest(
|
| 102 |
-
source_text=
|
| 103 |
-
target_language="
|
| 104 |
source_language=123 # type: ignore
|
| 105 |
)
|
| 106 |
-
|
| 107 |
-
def test_empty_source_language_raises_error(self):
|
| 108 |
-
"""Test that empty
|
| 109 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 110 |
with pytest.raises(ValueError, match="Source language cannot be empty string"):
|
| 111 |
TranslationRequest(
|
| 112 |
-
source_text=
|
| 113 |
-
target_language="
|
| 114 |
source_language=""
|
| 115 |
)
|
| 116 |
-
|
| 117 |
-
def test_whitespace_source_language_raises_error(self):
|
| 118 |
-
"""Test that whitespace-only
|
| 119 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 120 |
with pytest.raises(ValueError, match="Source language cannot be empty string"):
|
| 121 |
TranslationRequest(
|
| 122 |
-
source_text=
|
| 123 |
-
target_language="
|
| 124 |
source_language=" "
|
| 125 |
)
|
| 126 |
-
|
| 127 |
-
def test_invalid_source_language_format_raises_error(self):
|
| 128 |
"""Test that invalid source language format raises ValueError."""
|
| 129 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 130 |
invalid_codes = ["e", "ENG", "en-us", "en-USA", "123", "en_US"]
|
| 131 |
-
|
| 132 |
for code in invalid_codes:
|
| 133 |
with pytest.raises(ValueError, match="Invalid source language code format"):
|
| 134 |
TranslationRequest(
|
| 135 |
-
source_text=
|
| 136 |
-
target_language="
|
| 137 |
source_language=code
|
| 138 |
)
|
| 139 |
-
|
| 140 |
-
def
|
| 141 |
-
"""Test that same source and target
|
| 142 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 143 |
-
|
| 144 |
-
# Explicit source language same as target
|
| 145 |
with pytest.raises(ValueError, match="Source and target languages cannot be the same"):
|
| 146 |
TranslationRequest(
|
| 147 |
-
source_text=
|
| 148 |
target_language="en",
|
| 149 |
source_language="en"
|
| 150 |
)
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
| 153 |
with pytest.raises(ValueError, match="Source and target languages cannot be the same"):
|
| 154 |
TranslationRequest(
|
| 155 |
-
source_text=
|
| 156 |
-
target_language="en"
|
| 157 |
)
|
| 158 |
-
|
| 159 |
-
def
|
| 160 |
-
"""Test effective_source_language property."""
|
| 161 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 162 |
-
|
| 163 |
-
# With explicit source language
|
| 164 |
-
request_explicit = TranslationRequest(
|
| 165 |
-
source_text=source_text,
|
| 166 |
-
target_language="fr",
|
| 167 |
-
source_language="de"
|
| 168 |
-
)
|
| 169 |
-
assert request_explicit.effective_source_language == "de"
|
| 170 |
-
|
| 171 |
-
# Without explicit source language (uses TextContent language)
|
| 172 |
-
request_implicit = TranslationRequest(
|
| 173 |
-
source_text=source_text,
|
| 174 |
-
target_language="fr"
|
| 175 |
-
)
|
| 176 |
-
assert request_implicit.effective_source_language == "en"
|
| 177 |
-
|
| 178 |
-
def test_text_length_property(self):
|
| 179 |
"""Test text_length property."""
|
| 180 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 181 |
request = TranslationRequest(
|
| 182 |
-
source_text=
|
| 183 |
-
target_language="
|
| 184 |
)
|
| 185 |
-
|
| 186 |
-
assert request.text_length == len(
|
| 187 |
-
|
| 188 |
-
def test_word_count_property(self):
|
| 189 |
"""Test word_count property."""
|
| 190 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 191 |
request = TranslationRequest(
|
| 192 |
-
source_text=
|
| 193 |
-
target_language="
|
| 194 |
)
|
| 195 |
-
|
| 196 |
-
assert request.word_count ==
|
| 197 |
-
|
| 198 |
-
def test_with_target_language_method(self):
|
| 199 |
"""Test with_target_language method creates new instance."""
|
| 200 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 201 |
original = TranslationRequest(
|
| 202 |
-
source_text=
|
| 203 |
-
target_language="
|
| 204 |
source_language="en"
|
| 205 |
)
|
| 206 |
-
|
| 207 |
-
new_request = original.with_target_language("
|
| 208 |
-
|
| 209 |
-
assert new_request.target_language == "
|
| 210 |
assert new_request.source_text == original.source_text
|
| 211 |
assert new_request.source_language == original.source_language
|
| 212 |
assert new_request is not original # Different instances
|
| 213 |
-
|
| 214 |
-
def test_with_source_language_method(self):
|
| 215 |
"""Test with_source_language method creates new instance."""
|
| 216 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 217 |
original = TranslationRequest(
|
| 218 |
-
source_text=
|
| 219 |
-
target_language="
|
| 220 |
source_language="en"
|
| 221 |
)
|
| 222 |
-
|
| 223 |
new_request = original.with_source_language("de")
|
| 224 |
-
|
| 225 |
assert new_request.source_language == "de"
|
| 226 |
assert new_request.target_language == original.target_language
|
| 227 |
assert new_request.source_text == original.source_text
|
| 228 |
assert new_request is not original # Different instances
|
| 229 |
-
|
| 230 |
-
def test_with_source_language_none(self):
|
| 231 |
"""Test with_source_language method with None value."""
|
| 232 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 233 |
original = TranslationRequest(
|
| 234 |
-
source_text=
|
| 235 |
-
target_language="
|
| 236 |
source_language="en"
|
| 237 |
)
|
| 238 |
-
|
| 239 |
new_request = original.with_source_language(None)
|
| 240 |
assert new_request.source_language is None
|
| 241 |
assert new_request.is_auto_detect_source is True
|
| 242 |
-
|
| 243 |
-
def test_translation_request_is_immutable(self):
|
| 244 |
"""Test that TranslationRequest is immutable (frozen dataclass)."""
|
| 245 |
-
source_text = TextContent(text="Hello, world!", language="en")
|
| 246 |
request = TranslationRequest(
|
| 247 |
-
source_text=
|
| 248 |
-
target_language="
|
| 249 |
)
|
| 250 |
-
|
| 251 |
with pytest.raises(AttributeError):
|
| 252 |
-
request.target_language = "
|
| 253 |
-
|
| 254 |
-
def
|
| 255 |
-
"""Test
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
assert request.source_language == source_lang
|
|
|
|
| 7 |
|
| 8 |
class TestTranslationRequest:
|
| 9 |
"""Test cases for TranslationRequest value object."""
|
| 10 |
+
|
| 11 |
+
@pytest.fixture
|
| 12 |
+
def sample_text_content(self):
|
| 13 |
+
"""Sample text content for testing."""
|
| 14 |
+
return TextContent(
|
| 15 |
+
text="Hello, world!",
|
| 16 |
+
language="en"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
def test_valid_translation_request_creation(self, sample_text_content):
|
| 20 |
"""Test creating valid TranslationRequest instance."""
|
|
|
|
| 21 |
request = TranslationRequest(
|
| 22 |
+
source_text=sample_text_content,
|
| 23 |
+
target_language="es",
|
| 24 |
source_language="en"
|
| 25 |
)
|
| 26 |
+
|
| 27 |
+
assert request.source_text == sample_text_content
|
| 28 |
+
assert request.target_language == "es"
|
| 29 |
assert request.source_language == "en"
|
| 30 |
assert request.effective_source_language == "en"
|
| 31 |
assert request.is_auto_detect_source is False
|
| 32 |
+
|
| 33 |
+
def test_translation_request_without_source_language(self, sample_text_content):
|
| 34 |
"""Test creating TranslationRequest without explicit source language."""
|
|
|
|
| 35 |
request = TranslationRequest(
|
| 36 |
+
source_text=sample_text_content,
|
| 37 |
+
target_language="es"
|
| 38 |
)
|
| 39 |
+
|
| 40 |
assert request.source_language is None
|
| 41 |
assert request.effective_source_language == "en" # From TextContent
|
| 42 |
assert request.is_auto_detect_source is True
|
| 43 |
+
|
| 44 |
def test_non_text_content_source_raises_error(self):
|
| 45 |
+
"""Test that non-TextContent source raises TypeError."""
|
| 46 |
with pytest.raises(TypeError, match="Source text must be a TextContent instance"):
|
| 47 |
TranslationRequest(
|
| 48 |
+
source_text="not a TextContent", # type: ignore
|
| 49 |
+
target_language="es"
|
| 50 |
)
|
| 51 |
+
|
| 52 |
+
def test_non_string_target_language_raises_error(self, sample_text_content):
|
| 53 |
+
"""Test that non-string target language raises TypeError."""
|
|
|
|
| 54 |
with pytest.raises(TypeError, match="Target language must be a string"):
|
| 55 |
TranslationRequest(
|
| 56 |
+
source_text=sample_text_content,
|
| 57 |
target_language=123 # type: ignore
|
| 58 |
)
|
| 59 |
+
|
| 60 |
+
def test_empty_target_language_raises_error(self, sample_text_content):
|
| 61 |
+
"""Test that empty target language raises ValueError."""
|
|
|
|
| 62 |
with pytest.raises(ValueError, match="Target language cannot be empty"):
|
| 63 |
TranslationRequest(
|
| 64 |
+
source_text=sample_text_content,
|
| 65 |
target_language=""
|
| 66 |
)
|
| 67 |
+
|
| 68 |
+
def test_whitespace_target_language_raises_error(self, sample_text_content):
|
| 69 |
+
"""Test that whitespace-only target language raises ValueError."""
|
|
|
|
| 70 |
with pytest.raises(ValueError, match="Target language cannot be empty"):
|
| 71 |
TranslationRequest(
|
| 72 |
+
source_text=sample_text_content,
|
| 73 |
target_language=" "
|
| 74 |
)
|
| 75 |
+
|
| 76 |
+
def test_invalid_target_language_format_raises_error(self, sample_text_content):
|
| 77 |
"""Test that invalid target language format raises ValueError."""
|
| 78 |
+
invalid_codes = ["e", "ENG", "en-us", "en-USA", "123", "en_US"]
|
| 79 |
+
|
|
|
|
| 80 |
for code in invalid_codes:
|
| 81 |
with pytest.raises(ValueError, match="Invalid target language code format"):
|
| 82 |
TranslationRequest(
|
| 83 |
+
source_text=sample_text_content,
|
| 84 |
target_language=code
|
| 85 |
)
|
| 86 |
+
|
| 87 |
+
def test_valid_target_language_codes(self, sample_text_content):
|
| 88 |
"""Test valid target language code formats."""
|
| 89 |
+
valid_codes = ["es", "fr", "de", "zh", "ja", "en-US", "fr-FR", "zh-CN"]
|
| 90 |
+
|
|
|
|
| 91 |
for code in valid_codes:
|
| 92 |
request = TranslationRequest(
|
| 93 |
+
source_text=sample_text_content,
|
| 94 |
target_language=code
|
| 95 |
)
|
| 96 |
assert request.target_language == code
|
| 97 |
+
|
| 98 |
+
def test_non_string_source_language_raises_error(self, sample_text_content):
|
| 99 |
+
"""Test that non-string source language raises TypeError."""
|
|
|
|
| 100 |
with pytest.raises(TypeError, match="Source language must be a string"):
|
| 101 |
TranslationRequest(
|
| 102 |
+
source_text=sample_text_content,
|
| 103 |
+
target_language="es",
|
| 104 |
source_language=123 # type: ignore
|
| 105 |
)
|
| 106 |
+
|
| 107 |
+
def test_empty_source_language_raises_error(self, sample_text_content):
|
| 108 |
+
"""Test that empty source language raises ValueError."""
|
|
|
|
| 109 |
with pytest.raises(ValueError, match="Source language cannot be empty string"):
|
| 110 |
TranslationRequest(
|
| 111 |
+
source_text=sample_text_content,
|
| 112 |
+
target_language="es",
|
| 113 |
source_language=""
|
| 114 |
)
|
| 115 |
+
|
| 116 |
+
def test_whitespace_source_language_raises_error(self, sample_text_content):
|
| 117 |
+
"""Test that whitespace-only source language raises ValueError."""
|
|
|
|
| 118 |
with pytest.raises(ValueError, match="Source language cannot be empty string"):
|
| 119 |
TranslationRequest(
|
| 120 |
+
source_text=sample_text_content,
|
| 121 |
+
target_language="es",
|
| 122 |
source_language=" "
|
| 123 |
)
|
| 124 |
+
|
| 125 |
+
def test_invalid_source_language_format_raises_error(self, sample_text_content):
|
| 126 |
"""Test that invalid source language format raises ValueError."""
|
|
|
|
| 127 |
invalid_codes = ["e", "ENG", "en-us", "en-USA", "123", "en_US"]
|
| 128 |
+
|
| 129 |
for code in invalid_codes:
|
| 130 |
with pytest.raises(ValueError, match="Invalid source language code format"):
|
| 131 |
TranslationRequest(
|
| 132 |
+
source_text=sample_text_content,
|
| 133 |
+
target_language="es",
|
| 134 |
source_language=code
|
| 135 |
)
|
| 136 |
+
|
| 137 |
+
def test_same_source_and_target_language_explicit_raises_error(self, sample_text_content):
|
| 138 |
+
"""Test that same explicit source and target language raises ValueError."""
|
|
|
|
|
|
|
|
|
|
| 139 |
with pytest.raises(ValueError, match="Source and target languages cannot be the same"):
|
| 140 |
TranslationRequest(
|
| 141 |
+
source_text=sample_text_content,
|
| 142 |
target_language="en",
|
| 143 |
source_language="en"
|
| 144 |
)
|
| 145 |
+
|
| 146 |
+
def test_same_source_and_target_language_implicit_raises_error(self):
|
| 147 |
+
"""Test that same implicit source and target language raises ValueError."""
|
| 148 |
+
text_content = TextContent(text="Hello", language="en")
|
| 149 |
+
|
| 150 |
with pytest.raises(ValueError, match="Source and target languages cannot be the same"):
|
| 151 |
TranslationRequest(
|
| 152 |
+
source_text=text_content,
|
| 153 |
+
target_language="en" # Same as text_content.language
|
| 154 |
)
|
| 155 |
+
|
| 156 |
+
def test_text_length_property(self, sample_text_content):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
"""Test text_length property."""
|
|
|
|
| 158 |
request = TranslationRequest(
|
| 159 |
+
source_text=sample_text_content,
|
| 160 |
+
target_language="es"
|
| 161 |
)
|
| 162 |
+
|
| 163 |
+
assert request.text_length == len(sample_text_content.text)
|
| 164 |
+
|
| 165 |
+
def test_word_count_property(self, sample_text_content):
|
| 166 |
"""Test word_count property."""
|
|
|
|
| 167 |
request = TranslationRequest(
|
| 168 |
+
source_text=sample_text_content,
|
| 169 |
+
target_language="es"
|
| 170 |
)
|
| 171 |
+
|
| 172 |
+
assert request.word_count == sample_text_content.word_count
|
| 173 |
+
|
| 174 |
+
def test_with_target_language_method(self, sample_text_content):
|
| 175 |
"""Test with_target_language method creates new instance."""
|
|
|
|
| 176 |
original = TranslationRequest(
|
| 177 |
+
source_text=sample_text_content,
|
| 178 |
+
target_language="es",
|
| 179 |
source_language="en"
|
| 180 |
)
|
| 181 |
+
|
| 182 |
+
new_request = original.with_target_language("fr")
|
| 183 |
+
|
| 184 |
+
assert new_request.target_language == "fr"
|
| 185 |
assert new_request.source_text == original.source_text
|
| 186 |
assert new_request.source_language == original.source_language
|
| 187 |
assert new_request is not original # Different instances
|
| 188 |
+
|
| 189 |
+
def test_with_source_language_method(self, sample_text_content):
|
| 190 |
"""Test with_source_language method creates new instance."""
|
|
|
|
| 191 |
original = TranslationRequest(
|
| 192 |
+
source_text=sample_text_content,
|
| 193 |
+
target_language="es",
|
| 194 |
source_language="en"
|
| 195 |
)
|
| 196 |
+
|
| 197 |
new_request = original.with_source_language("de")
|
| 198 |
+
|
| 199 |
assert new_request.source_language == "de"
|
| 200 |
assert new_request.target_language == original.target_language
|
| 201 |
assert new_request.source_text == original.source_text
|
| 202 |
assert new_request is not original # Different instances
|
| 203 |
+
|
| 204 |
+
def test_with_source_language_none(self, sample_text_content):
|
| 205 |
"""Test with_source_language method with None value."""
|
|
|
|
| 206 |
original = TranslationRequest(
|
| 207 |
+
source_text=sample_text_content,
|
| 208 |
+
target_language="es",
|
| 209 |
source_language="en"
|
| 210 |
)
|
| 211 |
+
|
| 212 |
new_request = original.with_source_language(None)
|
| 213 |
assert new_request.source_language is None
|
| 214 |
assert new_request.is_auto_detect_source is True
|
| 215 |
+
|
| 216 |
+
def test_translation_request_is_immutable(self, sample_text_content):
|
| 217 |
"""Test that TranslationRequest is immutable (frozen dataclass)."""
|
|
|
|
| 218 |
request = TranslationRequest(
|
| 219 |
+
source_text=sample_text_content,
|
| 220 |
+
target_language="es"
|
| 221 |
)
|
| 222 |
+
|
| 223 |
with pytest.raises(AttributeError):
|
| 224 |
+
request.target_language = "fr" # type: ignore
|
| 225 |
+
|
| 226 |
+
def test_effective_source_language_with_explicit_source(self, sample_text_content):
|
| 227 |
+
"""Test effective_source_language with explicit source language."""
|
| 228 |
+
request = TranslationRequest(
|
| 229 |
+
source_text=sample_text_content,
|
| 230 |
+
target_language="es",
|
| 231 |
+
source_language="de" # Different from text_content.language
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
assert request.effective_source_language == "de"
|
| 235 |
+
|
| 236 |
+
def test_effective_source_language_with_implicit_source(self, sample_text_content):
|
| 237 |
+
"""Test effective_source_language with implicit source language."""
|
| 238 |
+
request = TranslationRequest(
|
| 239 |
+
source_text=sample_text_content,
|
| 240 |
+
target_language="es"
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
assert request.effective_source_language == sample_text_content.language
|
|
|
tests/unit/domain/test_exceptions.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for domain exceptions."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from src.domain.exceptions import (
|
| 5 |
+
DomainException,
|
| 6 |
+
InvalidAudioFormatException,
|
| 7 |
+
InvalidTextContentException,
|
| 8 |
+
TranslationFailedException,
|
| 9 |
+
SpeechRecognitionException,
|
| 10 |
+
SpeechSynthesisException,
|
| 11 |
+
InvalidVoiceSettingsException,
|
| 12 |
+
AudioProcessingException
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class TestDomainExceptions:
|
| 17 |
+
"""Test cases for domain exceptions."""
|
| 18 |
+
|
| 19 |
+
def test_domain_exception_is_base_exception(self):
|
| 20 |
+
"""Test that DomainException is the base exception."""
|
| 21 |
+
exception = DomainException("Base domain error")
|
| 22 |
+
|
| 23 |
+
assert isinstance(exception, Exception)
|
| 24 |
+
assert str(exception) == "Base domain error"
|
| 25 |
+
|
| 26 |
+
def test_domain_exception_without_message(self):
|
| 27 |
+
"""Test DomainException without message."""
|
| 28 |
+
exception = DomainException()
|
| 29 |
+
|
| 30 |
+
assert isinstance(exception, Exception)
|
| 31 |
+
assert str(exception) == ""
|
| 32 |
+
|
| 33 |
+
def test_invalid_audio_format_exception_inheritance(self):
|
| 34 |
+
"""Test that InvalidAudioFormatException inherits from DomainException."""
|
| 35 |
+
exception = InvalidAudioFormatException("Invalid audio format")
|
| 36 |
+
|
| 37 |
+
assert isinstance(exception, DomainException)
|
| 38 |
+
assert isinstance(exception, Exception)
|
| 39 |
+
assert str(exception) == "Invalid audio format"
|
| 40 |
+
|
| 41 |
+
def test_invalid_audio_format_exception_usage(self):
|
| 42 |
+
"""Test InvalidAudioFormatException usage scenario."""
|
| 43 |
+
try:
|
| 44 |
+
raise InvalidAudioFormatException("Unsupported format: xyz")
|
| 45 |
+
except DomainException as e:
|
| 46 |
+
assert "Unsupported format: xyz" in str(e)
|
| 47 |
+
except Exception:
|
| 48 |
+
pytest.fail("Should have caught as DomainException")
|
| 49 |
+
|
| 50 |
+
def test_invalid_text_content_exception_inheritance(self):
|
| 51 |
+
"""Test that InvalidTextContentException inherits from DomainException."""
|
| 52 |
+
exception = InvalidTextContentException("Invalid text content")
|
| 53 |
+
|
| 54 |
+
assert isinstance(exception, DomainException)
|
| 55 |
+
assert isinstance(exception, Exception)
|
| 56 |
+
assert str(exception) == "Invalid text content"
|
| 57 |
+
|
| 58 |
+
def test_invalid_text_content_exception_usage(self):
|
| 59 |
+
"""Test InvalidTextContentException usage scenario."""
|
| 60 |
+
try:
|
| 61 |
+
raise InvalidTextContentException("Text content is empty")
|
| 62 |
+
except DomainException as e:
|
| 63 |
+
assert "Text content is empty" in str(e)
|
| 64 |
+
except Exception:
|
| 65 |
+
pytest.fail("Should have caught as DomainException")
|
| 66 |
+
|
| 67 |
+
def test_translation_failed_exception_inheritance(self):
|
| 68 |
+
"""Test that TranslationFailedException inherits from DomainException."""
|
| 69 |
+
exception = TranslationFailedException("Translation failed")
|
| 70 |
+
|
| 71 |
+
assert isinstance(exception, DomainException)
|
| 72 |
+
assert isinstance(exception, Exception)
|
| 73 |
+
assert str(exception) == "Translation failed"
|
| 74 |
+
|
| 75 |
+
def test_translation_failed_exception_usage(self):
|
| 76 |
+
"""Test TranslationFailedException usage scenario."""
|
| 77 |
+
try:
|
| 78 |
+
raise TranslationFailedException("Translation service unavailable")
|
| 79 |
+
except DomainException as e:
|
| 80 |
+
assert "Translation service unavailable" in str(e)
|
| 81 |
+
except Exception:
|
| 82 |
+
pytest.fail("Should have caught as DomainException")
|
| 83 |
+
|
| 84 |
+
def test_speech_recognition_exception_inheritance(self):
|
| 85 |
+
"""Test that SpeechRecognitionException inherits from DomainException."""
|
| 86 |
+
exception = SpeechRecognitionException("Speech recognition failed")
|
| 87 |
+
|
| 88 |
+
assert isinstance(exception, DomainException)
|
| 89 |
+
assert isinstance(exception, Exception)
|
| 90 |
+
assert str(exception) == "Speech recognition failed"
|
| 91 |
+
|
| 92 |
+
def test_speech_recognition_exception_usage(self):
|
| 93 |
+
"""Test SpeechRecognitionException usage scenario."""
|
| 94 |
+
try:
|
| 95 |
+
raise SpeechRecognitionException("STT model not available")
|
| 96 |
+
except DomainException as e:
|
| 97 |
+
assert "STT model not available" in str(e)
|
| 98 |
+
except Exception:
|
| 99 |
+
pytest.fail("Should have caught as DomainException")
|
| 100 |
+
|
| 101 |
+
def test_speech_synthesis_exception_inheritance(self):
|
| 102 |
+
"""Test that SpeechSynthesisException inherits from DomainException."""
|
| 103 |
+
exception = SpeechSynthesisException("Speech synthesis failed")
|
| 104 |
+
|
| 105 |
+
assert isinstance(exception, DomainException)
|
| 106 |
+
assert isinstance(exception, Exception)
|
| 107 |
+
assert str(exception) == "Speech synthesis failed"
|
| 108 |
+
|
| 109 |
+
def test_speech_synthesis_exception_usage(self):
|
| 110 |
+
"""Test SpeechSynthesisException usage scenario."""
|
| 111 |
+
try:
|
| 112 |
+
raise SpeechSynthesisException("TTS voice not found")
|
| 113 |
+
except DomainException as e:
|
| 114 |
+
assert "TTS voice not found" in str(e)
|
| 115 |
+
except Exception:
|
| 116 |
+
pytest.fail("Should have caught as DomainException")
|
| 117 |
+
|
| 118 |
+
def test_invalid_voice_settings_exception_inheritance(self):
|
| 119 |
+
"""Test that InvalidVoiceSettingsException inherits from DomainException."""
|
| 120 |
+
exception = InvalidVoiceSettingsException("Invalid voice settings")
|
| 121 |
+
|
| 122 |
+
assert isinstance(exception, DomainException)
|
| 123 |
+
assert isinstance(exception, Exception)
|
| 124 |
+
assert str(exception) == "Invalid voice settings"
|
| 125 |
+
|
| 126 |
+
def test_invalid_voice_settings_exception_usage(self):
|
| 127 |
+
"""Test InvalidVoiceSettingsException usage scenario."""
|
| 128 |
+
try:
|
| 129 |
+
raise InvalidVoiceSettingsException("Voice speed out of range")
|
| 130 |
+
except DomainException as e:
|
| 131 |
+
assert "Voice speed out of range" in str(e)
|
| 132 |
+
except Exception:
|
| 133 |
+
pytest.fail("Should have caught as DomainException")
|
| 134 |
+
|
| 135 |
+
def test_audio_processing_exception_inheritance(self):
|
| 136 |
+
"""Test that AudioProcessingException inherits from DomainException."""
|
| 137 |
+
exception = AudioProcessingException("Audio processing failed")
|
| 138 |
+
|
| 139 |
+
assert isinstance(exception, DomainException)
|
| 140 |
+
assert isinstance(exception, Exception)
|
| 141 |
+
assert str(exception) == "Audio processing failed"
|
| 142 |
+
|
| 143 |
+
def test_audio_processing_exception_usage(self):
|
| 144 |
+
"""Test AudioProcessingException usage scenario."""
|
| 145 |
+
try:
|
| 146 |
+
raise AudioProcessingException("Pipeline validation failed")
|
| 147 |
+
except DomainException as e:
|
| 148 |
+
assert "Pipeline validation failed" in str(e)
|
| 149 |
+
except Exception:
|
| 150 |
+
pytest.fail("Should have caught as DomainException")
|
| 151 |
+
|
| 152 |
+
def test_all_exceptions_inherit_from_domain_exception(self):
|
| 153 |
+
"""Test that all domain exceptions inherit from DomainException."""
|
| 154 |
+
exceptions = [
|
| 155 |
+
InvalidAudioFormatException("test"),
|
| 156 |
+
InvalidTextContentException("test"),
|
| 157 |
+
TranslationFailedException("test"),
|
| 158 |
+
SpeechRecognitionException("test"),
|
| 159 |
+
SpeechSynthesisException("test"),
|
| 160 |
+
InvalidVoiceSettingsException("test"),
|
| 161 |
+
AudioProcessingException("test")
|
| 162 |
+
]
|
| 163 |
+
|
| 164 |
+
for exception in exceptions:
|
| 165 |
+
assert isinstance(exception, DomainException)
|
| 166 |
+
assert isinstance(exception, Exception)
|
| 167 |
+
|
| 168 |
+
def test_exception_chaining_support(self):
|
| 169 |
+
"""Test that exceptions support chaining."""
|
| 170 |
+
original_error = ValueError("Original error")
|
| 171 |
+
|
| 172 |
+
try:
|
| 173 |
+
raise TranslationFailedException("Translation failed") from original_error
|
| 174 |
+
except TranslationFailedException as e:
|
| 175 |
+
assert e.__cause__ is original_error
|
| 176 |
+
assert str(e) == "Translation failed"
|
| 177 |
+
|
| 178 |
+
def test_exception_with_none_message(self):
|
| 179 |
+
"""Test exceptions with None message."""
|
| 180 |
+
exception = AudioProcessingException(None)
|
| 181 |
+
|
| 182 |
+
assert isinstance(exception, DomainException)
|
| 183 |
+
# Python converts None to empty string for exception messages
|
| 184 |
+
assert str(exception) == "None"
|
| 185 |
+
|
| 186 |
+
def test_exception_hierarchy_catching(self):
|
| 187 |
+
"""Test catching exceptions at different levels of hierarchy."""
|
| 188 |
+
# Test catching specific exception
|
| 189 |
+
try:
|
| 190 |
+
raise SpeechSynthesisException("TTS failed")
|
| 191 |
+
except SpeechSynthesisException as e:
|
| 192 |
+
assert "TTS failed" in str(e)
|
| 193 |
+
except Exception:
|
| 194 |
+
pytest.fail("Should have caught SpeechSynthesisException")
|
| 195 |
+
|
| 196 |
+
# Test catching at domain level
|
| 197 |
+
try:
|
| 198 |
+
raise SpeechSynthesisException("TTS failed")
|
| 199 |
+
except DomainException as e:
|
| 200 |
+
assert "TTS failed" in str(e)
|
| 201 |
+
except Exception:
|
| 202 |
+
pytest.fail("Should have caught as DomainException")
|
| 203 |
+
|
| 204 |
+
# Test catching at base level
|
| 205 |
+
try:
|
| 206 |
+
raise SpeechSynthesisException("TTS failed")
|
| 207 |
+
except Exception as e:
|
| 208 |
+
assert "TTS failed" in str(e)
|
| 209 |
+
|
| 210 |
+
def test_exception_equality(self):
|
| 211 |
+
"""Test exception equality comparison."""
|
| 212 |
+
exc1 = AudioProcessingException("Same message")
|
| 213 |
+
exc2 = AudioProcessingException("Same message")
|
| 214 |
+
exc3 = AudioProcessingException("Different message")
|
| 215 |
+
|
| 216 |
+
# Exceptions are not equal even with same message (different instances)
|
| 217 |
+
assert exc1 is not exc2
|
| 218 |
+
assert exc1 is not exc3
|
| 219 |
+
|
| 220 |
+
# But they have the same type and message
|
| 221 |
+
assert type(exc1) == type(exc2)
|
| 222 |
+
assert str(exc1) == str(exc2)
|
| 223 |
+
assert str(exc1) != str(exc3)
|
| 224 |
+
|
| 225 |
+
def test_exception_repr(self):
|
| 226 |
+
"""Test exception string representation."""
|
| 227 |
+
exception = TranslationFailedException("Translation service error")
|
| 228 |
+
|
| 229 |
+
# Test that repr includes class name and message
|
| 230 |
+
repr_str = repr(exception)
|
| 231 |
+
assert "TranslationFailedException" in repr_str
|
| 232 |
+
assert "Translation service error" in repr_str
|
| 233 |
+
|
| 234 |
+
def test_exception_args_property(self):
|
| 235 |
+
"""Test exception args property."""
|
| 236 |
+
message = "Test error message"
|
| 237 |
+
exception = SpeechRecognitionException(message)
|
| 238 |
+
|
| 239 |
+
assert exception.args == (message,)
|
| 240 |
+
assert exception.args[0] == message
|