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
|