diff --git a/tests/unit/domain/test_tts_config.py b/tests/unit/domain/test_tts_config.py new file mode 100755 index 000000000..7cdad3bd8 --- /dev/null +++ b/tests/unit/domain/test_tts_config.py @@ -0,0 +1,314 @@ +"""TtsConfig 单元测试.""" + +from __future__ import annotations + +import pytest +from domain.tts_config import TtsConfig + + +class TestTtsConfigDefaults: + """默认值测试.""" + + def test_default_values(self): + config = TtsConfig() + assert config.enabled is False + assert config.voice_id == "" + assert config.speed == 1.0 + assert config.pitch == 0.0 + assert config.volume == 0.8 + assert config.text == "" + assert config.align_mode == "full" + assert config.overlap_mode == "replace" + + def test_custom_construction(self): + config = TtsConfig( + enabled=True, + voice_id="voice_001", + speed=1.5, + pitch=3.0, + volume=0.9, + text="hello", + align_mode="subtitle", + overlap_mode="mix", + ) + assert config.enabled is True + assert config.voice_id == "voice_001" + assert config.speed == 1.5 + assert config.pitch == 3.0 + assert config.volume == 0.9 + assert config.text == "hello" + assert config.align_mode == "subtitle" + assert config.overlap_mode == "mix" + + def test_slots_no_extra_attrs(self): + config = TtsConfig() + with pytest.raises((AttributeError, TypeError)): + config.new_attr = "value" # type: ignore[attr-defined] + + def test_equality_same_values(self): + a = TtsConfig(enabled=True, voice_id="v1") + b = TtsConfig(enabled=True, voice_id="v1") + assert a == b + + def test_equality_different_values(self): + a = TtsConfig(enabled=True) + b = TtsConfig(enabled=False) + assert a != b + + +class TestTtsConfigParseNoneAndEmpty: + """parse 空输入测试.""" + + def test_parse_none(self): + config = TtsConfig.parse(None) + assert config == TtsConfig() + + def test_parse_empty_dict(self): + config = TtsConfig.parse({}) + assert config == TtsConfig() + + def test_parse_non_dict_string(self): + config = TtsConfig.parse("not a dict") # type: ignore[arg-type] + assert config == TtsConfig() + + def test_parse_non_dict_list(self): + config = TtsConfig.parse([]) # type: ignore[arg-type] + assert config == TtsConfig() + + def test_parse_non_dict_number(self): + config = TtsConfig.parse(123) # type: ignore[arg-type] + assert config == TtsConfig() + + +class TestTtsConfigParseDisabled: + """parse disabled 场景.""" + + def test_parse_enabled_false_returns_default(self): + config = TtsConfig.parse({"enabled": False}) + assert config.enabled is False + assert config.speed == 1.0 + assert config.voice_id == "" + + def test_parse_enabled_false_ignores_other_fields(self): + config = TtsConfig.parse( + { + "enabled": False, + "voice_id": "v1", + "speed": 1.5, + } + ) + assert config.enabled is False + assert config.voice_id == "" + assert config.speed == 1.0 + + def test_parse_enabled_non_bool_falls_to_false(self): + config = TtsConfig.parse({"enabled": "true"}) + assert config.enabled is False + + def test_parse_enabled_int_falls_to_false(self): + config = TtsConfig.parse({"enabled": 1}) + assert config.enabled is False + + +class TestTtsConfigParseNormal: + """parse 正常数据测试.""" + + def test_parse_full_data(self): + data = { + "enabled": True, + "voice_id": "voice_001", + "speed": 1.5, + "pitch": 2.5, + "volume": 0.7, + "text": "你好世界", + "align_mode": "subtitle", + "overlap_mode": "mix", + } + config = TtsConfig.parse(data) + assert config.enabled is True + assert config.voice_id == "voice_001" + assert config.speed == 1.5 + assert config.pitch == 2.5 + assert config.volume == 0.7 + assert config.text == "你好世界" + assert config.align_mode == "subtitle" + assert config.overlap_mode == "mix" + + def test_parse_int_speed_becomes_float(self): + config = TtsConfig.parse({"enabled": True, "speed": 2}) + assert isinstance(config.speed, float) + assert config.speed == 2.0 + + def test_parse_int_pitch_becomes_float(self): + config = TtsConfig.parse({"enabled": True, "pitch": -3}) + assert isinstance(config.pitch, float) + assert config.pitch == -3.0 + + +class TestTtsConfigParseTypeFallback: + """parse 类型错误回退测试.""" + + def test_parse_voice_id_non_string_fallback(self): + config = TtsConfig.parse({"enabled": True, "voice_id": 123}) + assert config.voice_id == "" + + def test_parse_speed_non_numeric_fallback(self): + config = TtsConfig.parse({"enabled": True, "speed": "fast"}) + assert config.speed == 1.0 + + def test_parse_pitch_non_numeric_fallback(self): + config = TtsConfig.parse({"enabled": True, "pitch": "high"}) + assert config.pitch == 0.0 + + def test_parse_volume_non_numeric_fallback(self): + config = TtsConfig.parse({"enabled": True, "volume": "loud"}) + assert config.volume == 0.8 + + def test_parse_text_non_string_fallback(self): + config = TtsConfig.parse({"enabled": True, "text": 456}) + assert config.text == "" + + def test_parse_voice_id_list_fallback(self): + config = TtsConfig.parse({"enabled": True, "voice_id": ["v1"]}) + assert config.voice_id == "" + + +class TestTtsConfigParseClamp: + """parse 边界钳制测试.""" + + def test_parse_speed_below_min_clamped(self): + config = TtsConfig.parse({"enabled": True, "speed": 0.1}) + assert config.speed == 0.5 + + def test_parse_speed_above_max_clamped(self): + config = TtsConfig.parse({"enabled": True, "speed": 3.0}) + assert config.speed == 2.0 + + def test_parse_speed_at_min_ok(self): + config = TtsConfig.parse({"enabled": True, "speed": 0.5}) + assert config.speed == 0.5 + + def test_parse_speed_at_max_ok(self): + config = TtsConfig.parse({"enabled": True, "speed": 2.0}) + assert config.speed == 2.0 + + def test_parse_pitch_below_min_clamped(self): + config = TtsConfig.parse({"enabled": True, "pitch": -20}) + assert config.pitch == -12 + + def test_parse_pitch_above_max_clamped(self): + config = TtsConfig.parse({"enabled": True, "pitch": 20}) + assert config.pitch == 12 + + def test_parse_pitch_at_min_ok(self): + config = TtsConfig.parse({"enabled": True, "pitch": -12}) + assert config.pitch == -12 + + def test_parse_pitch_at_max_ok(self): + config = TtsConfig.parse({"enabled": True, "pitch": 12}) + assert config.pitch == 12 + + def test_parse_volume_below_min_clamped(self): + config = TtsConfig.parse({"enabled": True, "volume": -0.5}) + assert config.volume == 0.0 + + def test_parse_volume_above_max_clamped(self): + config = TtsConfig.parse({"enabled": True, "volume": 1.5}) + assert config.volume == 1.0 + + def test_parse_volume_at_min_ok(self): + config = TtsConfig.parse({"enabled": True, "volume": 0.0}) + assert config.volume == 0.0 + + def test_parse_volume_at_max_ok(self): + config = TtsConfig.parse({"enabled": True, "volume": 1.0}) + assert config.volume == 1.0 + + +class TestTtsConfigParseAlignMode: + """align_mode 解析测试.""" + + def test_parse_align_mode_subtitle(self): + config = TtsConfig.parse({"enabled": True, "align_mode": "subtitle"}) + assert config.align_mode == "subtitle" + + def test_parse_align_mode_full(self): + config = TtsConfig.parse({"enabled": True, "align_mode": "full"}) + assert config.align_mode == "full" + + def test_parse_align_mode_invalid_fallback(self): + config = TtsConfig.parse({"enabled": True, "align_mode": "auto"}) + assert config.align_mode == "full" + + def test_parse_align_mode_empty_fallback(self): + config = TtsConfig.parse({"enabled": True, "align_mode": ""}) + assert config.align_mode == "full" + + +class TestTtsConfigParseOverlapMode: + """overlap_mode 解析测试.""" + + def test_parse_overlap_mode_replace(self): + config = TtsConfig.parse({"enabled": True, "overlap_mode": "replace"}) + assert config.overlap_mode == "replace" + + def test_parse_overlap_mode_mix(self): + config = TtsConfig.parse({"enabled": True, "overlap_mode": "mix"}) + assert config.overlap_mode == "mix" + + def test_parse_overlap_mode_invalid_fallback(self): + config = TtsConfig.parse({"enabled": True, "overlap_mode": "add"}) + assert config.overlap_mode == "replace" + + def test_parse_overlap_mode_empty_fallback(self): + config = TtsConfig.parse({"enabled": True, "overlap_mode": ""}) + assert config.overlap_mode == "replace" + + +class TestTtsConfigClamp: + """_clamp 直接调用测试.""" + + def test_clamp_speed_low(self): + config = TtsConfig(enabled=True, speed=0.1) + config._clamp() + assert config.speed == 0.5 + + def test_clamp_speed_high(self): + config = TtsConfig(enabled=True, speed=5.0) + config._clamp() + assert config.speed == 2.0 + + def test_clamp_speed_normal_unchanged(self): + config = TtsConfig(enabled=True, speed=1.2) + config._clamp() + assert config.speed == 1.2 + + def test_clamp_pitch_low(self): + config = TtsConfig(enabled=True, pitch=-20) + config._clamp() + assert config.pitch == -12 + + def test_clamp_pitch_high(self): + config = TtsConfig(enabled=True, pitch=20) + config._clamp() + assert config.pitch == 12 + + def test_clamp_pitch_normal_unchanged(self): + config = TtsConfig(enabled=True, pitch=5.0) + config._clamp() + assert config.pitch == 5.0 + + def test_clamp_volume_low(self): + config = TtsConfig(enabled=True, volume=-1.0) + config._clamp() + assert config.volume == 0.0 + + def test_clamp_volume_high(self): + config = TtsConfig(enabled=True, volume=2.0) + config._clamp() + assert config.volume == 1.0 + + def test_clamp_volume_normal_unchanged(self): + config = TtsConfig(enabled=True, volume=0.5) + config._clamp() + assert config.volume == 0.5