From c9caad236c4eba3debfc227fa9494d3154846b5f Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 11:03:04 +0800 Subject: [PATCH] test(wave150): add tts_config unit tests (+57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 默认值: enabled/voice_id/speed/pitch/volume/text/align_mode/overlap_mode - parse基础: None/空dict/非dict/disabled快速返回/enabled基础/完整配置 - parse类型校验: enabled非bool/voice_id非字符串/speed非数字/整数speed/ pitch非数字/整数pitch/volume非数字/整数volume/text非字符串 - parse边界钳制: speed(低于min/负/高于max/边界值/正常)、pitch(低/高/边界/正常)、 volume(低/高/边界/正常) - parse枚举校验: align_mode(subtitle/full/无效)、overlap_mode(replace/mix/无效) - _clamp直接调用: speed/pitch/volume 上下界钳制、范围内保持 - 结构验证: dataclass、相等性、不等性 --- tests/unit/test_tts_config.py | 472 +++++++++++++++++++++++++--------- 1 file changed, 353 insertions(+), 119 deletions(-) diff --git a/tests/unit/test_tts_config.py b/tests/unit/test_tts_config.py index 3586d2080..ec04af19b 100755 --- a/tests/unit/test_tts_config.py +++ b/tests/unit/test_tts_config.py @@ -1,153 +1,387 @@ -"""TtsConfig 配音配置模型单测.""" +"""TTS 配音配置模型单元测试.""" + +from __future__ import annotations import pytest from packages.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" + """TtsConfig 默认值测试.""" + + def test_default_enabled_false(self): + """默认禁用配音.""" + cfg = TtsConfig() + assert cfg.enabled is False + + def test_default_voice_id_empty(self): + """默认空音色ID.""" + cfg = TtsConfig() + assert cfg.voice_id == "" + + def test_default_speed(self): + """默认语速1.0.""" + cfg = TtsConfig() + assert cfg.speed == 1.0 + + def test_default_pitch(self): + """默认语调0.""" + cfg = TtsConfig() + assert cfg.pitch == 0.0 + + def test_default_volume(self): + """默认音量0.8.""" + cfg = TtsConfig() + assert cfg.volume == 0.8 + + def test_default_text_empty(self): + """默认空文本.""" + cfg = TtsConfig() + assert cfg.text == "" + + def test_default_align_mode(self): + """默认整段配音对齐.""" + cfg = TtsConfig() + assert cfg.align_mode == "full" + + def test_default_overlap_mode(self): + """默认替换原音.""" + cfg = TtsConfig() + assert cfg.overlap_mode == "replace" -class TestTtsConfigParse: - def test_parse_none(self): - config = TtsConfig.parse(None) - assert config.enabled is False - assert isinstance(config, TtsConfig) +# ── parse - 基础场景测试 ───────────────────────────────────────────────────── - def test_parse_empty_dict(self): - config = TtsConfig.parse({}) - assert config.enabled is False - def test_parse_not_dict(self): - config = TtsConfig.parse("not a dict") - assert config.enabled is False +class TestTtsConfigParseBasic: + """TtsConfig.parse 基础场景测试.""" - def test_parse_enabled_false_returns_disabled(self): - # 即使传了其他参数,enabled=False 就直接返回禁用 - 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_none_data(self): + """None输入返回默认配置(禁用).""" + cfg = TtsConfig.parse(None) + assert cfg.enabled is False - def test_parse_enabled_true_with_all_fields(self): - config = TtsConfig.parse( + def test_empty_dict(self): + """空dict返回默认配置.""" + cfg = TtsConfig.parse({}) + assert cfg.enabled is False + + def test_not_dict_returns_default(self): + """非dict输入返回默认.""" + cfg = TtsConfig.parse("not_a_dict") + assert cfg.enabled is False + assert cfg.speed == 1.0 + + def test_disabled_returns_fast(self): + """enabled为False时直接返回disabled配置.""" + cfg = TtsConfig.parse({"enabled": False, "voice_id": "v1", "speed": 1.5}) + assert cfg.enabled is False + # 其他字段为默认值 + assert cfg.voice_id == "" + assert cfg.speed == 1.0 + + def test_enabled_basic(self): + """启用配音基础配置.""" + cfg = TtsConfig.parse({"enabled": True, "voice_id": "voice_001"}) + assert cfg.enabled is True + assert cfg.voice_id == "voice_001" + + def test_full_config(self): + """完整配置解析.""" + cfg = TtsConfig.parse( { "enabled": True, - "voice_id": "female_warm", - "speed": 1.5, - "pitch": 2.0, + "voice_id": "v_test", + "speed": 1.2, + "pitch": 2.5, "volume": 0.9, - "text": "你好世界", + "text": "大家好", "align_mode": "subtitle", "overlap_mode": "mix", } ) - assert config.enabled is True - assert config.voice_id == "female_warm" - assert config.speed == 1.5 - assert config.pitch == 2.0 - assert config.volume == 0.9 - assert config.text == "你好世界" - assert config.align_mode == "subtitle" - assert config.overlap_mode == "mix" - - def test_parse_enabled_not_bool(self): - config = TtsConfig.parse({"enabled": "true", "voice_id": "v1"}) - assert config.enabled is False # 非 bool 值视为 False - - def test_parse_voice_id_not_string(self): - config = TtsConfig.parse({"enabled": True, "voice_id": 123}) - assert config.voice_id == "" - - def test_parse_speed_not_number(self): - config = TtsConfig.parse({"enabled": True, "speed": "fast"}) - assert config.speed == 1.0 - - def test_parse_pitch_not_number(self): - config = TtsConfig.parse({"enabled": True, "pitch": "high"}) - assert config.pitch == 0.0 - - def test_parse_volume_not_number(self): - config = TtsConfig.parse({"enabled": True, "volume": "loud"}) - assert config.volume == 0.8 - - def test_parse_text_not_string(self): - config = TtsConfig.parse({"enabled": True, "text": 12345}) - assert config.text == "" - - def test_parse_invalid_align_mode(self): - config = TtsConfig.parse({"enabled": True, "align_mode": "invalid"}) - assert config.align_mode == "full" - - def test_parse_invalid_overlap_mode(self): - config = TtsConfig.parse({"enabled": True, "overlap_mode": "invalid"}) - assert config.overlap_mode == "replace" + assert cfg.enabled is True + assert cfg.voice_id == "v_test" + assert cfg.speed == 1.2 + assert cfg.pitch == 2.5 + assert cfg.volume == 0.9 + assert cfg.text == "大家好" + assert cfg.align_mode == "subtitle" + assert cfg.overlap_mode == "mix" -class TestTtsConfigClamp: - def test_speed_below_min(self): - config = TtsConfig.parse({"enabled": True, "speed": 0.1}) - assert config.speed == 0.5 +# ── parse - 类型校验测试 ───────────────────────────────────────────────────── - def test_speed_above_max(self): - config = TtsConfig.parse({"enabled": True, "speed": 3.0}) - assert config.speed == 2.0 - def test_speed_within_range(self): - config = TtsConfig.parse({"enabled": True, "speed": 1.2}) - assert config.speed == 1.2 +class TestTtsConfigParseTypeChecks: + """TtsConfig.parse 类型校验测试.""" - def test_speed_boundary_values(self): - config_low = TtsConfig.parse({"enabled": True, "speed": 0.5}) - assert config_low.speed == 0.5 - config_high = TtsConfig.parse({"enabled": True, "speed": 2.0}) - assert config_high.speed == 2.0 + def test_enabled_not_bool(self): + """enabled不是bool时视为False.""" + cfg = TtsConfig.parse({"enabled": "true", "voice_id": "v1"}) + assert cfg.enabled is False - def test_pitch_below_min(self): - config = TtsConfig.parse({"enabled": True, "pitch": -20}) - assert config.pitch == -12 + def test_enabled_int_treated_as_non_bool(self): + """enabled为整数时视为非bool(Python里1是True但isinstance(1, bool)是True?).""" + # Python里bool是int的子类,isinstance(True, int)为True + # 反过来 isinstance(1, bool) 为 False,所以1会被当作无效值 + cfg = TtsConfig.parse({"enabled": 1, "voice_id": "v1"}) + assert cfg.enabled is False - def test_pitch_above_max(self): - config = TtsConfig.parse({"enabled": True, "pitch": 20}) - assert config.pitch == 12 + def test_voice_id_not_string(self): + """voice_id不是字符串时回退到空.""" + cfg = TtsConfig.parse({"enabled": True, "voice_id": 123}) + assert cfg.voice_id == "" - def test_pitch_within_range(self): - config = TtsConfig.parse({"enabled": True, "pitch": -3.5}) - assert config.pitch == -3.5 + def test_speed_not_number(self): + """speed不是数字时回退到1.0.""" + cfg = TtsConfig.parse({"enabled": True, "speed": "fast"}) + assert cfg.speed == 1.0 - def test_volume_below_min(self): - config = TtsConfig.parse({"enabled": True, "volume": -0.5}) - assert config.volume == 0.0 + def test_speed_int_accepted(self): + """整数speed也接受.""" + cfg = TtsConfig.parse({"enabled": True, "speed": 2}) + assert cfg.speed == 2.0 - def test_volume_above_max(self): - config = TtsConfig.parse({"enabled": True, "volume": 2.0}) - assert config.volume == 1.0 + def test_pitch_not_number(self): + """pitch不是数字时回退到0.""" + cfg = TtsConfig.parse({"enabled": True, "pitch": "high"}) + assert cfg.pitch == 0.0 - def test_volume_within_range(self): - config = TtsConfig.parse({"enabled": True, "volume": 0.5}) - assert config.volume == 0.5 + def test_pitch_int_accepted(self): + """整数pitch也接受.""" + cfg = TtsConfig.parse({"enabled": True, "pitch": 5}) + assert cfg.pitch == 5.0 - def test_int_speed_converted_to_float(self): - config = TtsConfig.parse({"enabled": True, "speed": 1}) - assert isinstance(config.speed, float) - assert config.speed == 1.0 + def test_volume_not_number(self): + """volume不是数字时回退到0.8.""" + cfg = TtsConfig.parse({"enabled": True, "volume": "loud"}) + assert cfg.volume == 0.8 - def test_int_pitch_converted_to_float(self): - config = TtsConfig.parse({"enabled": True, "pitch": 2}) - assert isinstance(config.pitch, float) - assert config.pitch == 2.0 + def test_volume_int_accepted(self): + """整数volume也接受.""" + cfg = TtsConfig.parse({"enabled": True, "volume": 1}) + assert cfg.volume == 1.0 - def test_int_volume_converted_to_float(self): - config = TtsConfig.parse({"enabled": True, "volume": 1}) - assert isinstance(config.volume, float) - assert config.volume == 1.0 + def test_text_not_string(self): + """text不是字符串时回退到空.""" + cfg = TtsConfig.parse({"enabled": True, "text": 12345}) + assert cfg.text == "" + + def test_text_empty_string(self): + """空文本字符串是有效的.""" + cfg = TtsConfig.parse({"enabled": True, "text": ""}) + assert cfg.text == "" + + +# ── parse - 边界钳制测试 ───────────────────────────────────────────────────── + + +class TestTtsConfigParseClamping: + """TtsConfig.parse 边界钳制测试.""" + + # speed 边界 + + def test_speed_below_min_clamped(self): + """语速低于最小值钳制到0.5.""" + cfg = TtsConfig.parse({"enabled": True, "speed": 0.1}) + assert cfg.speed == 0.5 + + def test_speed_negative_clamped(self): + """负语速钳制到0.5.""" + cfg = TtsConfig.parse({"enabled": True, "speed": -1.0}) + assert cfg.speed == 0.5 + + def test_speed_above_max_clamped(self): + """语速高于最大值钳制到2.0.""" + cfg = TtsConfig.parse({"enabled": True, "speed": 5.0}) + assert cfg.speed == 2.0 + + def test_speed_at_min_ok(self): + """刚好等于最小值正常.""" + cfg = TtsConfig.parse({"enabled": True, "speed": 0.5}) + assert cfg.speed == 0.5 + + def test_speed_at_max_ok(self): + """刚好等于最大值正常.""" + cfg = TtsConfig.parse({"enabled": True, "speed": 2.0}) + assert cfg.speed == 2.0 + + def test_speed_normal_ok(self): + """正常范围内语速保持不变.""" + cfg = TtsConfig.parse({"enabled": True, "speed": 1.5}) + assert cfg.speed == 1.5 + + # pitch 边界 + + def test_pitch_below_min_clamped(self): + """语调低于最小值钳制到-12.""" + cfg = TtsConfig.parse({"enabled": True, "pitch": -20}) + assert cfg.pitch == -12 + + def test_pitch_above_max_clamped(self): + """语调高于最大值钳制到12.""" + cfg = TtsConfig.parse({"enabled": True, "pitch": 20}) + assert cfg.pitch == 12 + + def test_pitch_at_min_ok(self): + """刚好等于最小值正常.""" + cfg = TtsConfig.parse({"enabled": True, "pitch": -12}) + assert cfg.pitch == -12 + + def test_pitch_at_max_ok(self): + """刚好等于最大值正常.""" + cfg = TtsConfig.parse({"enabled": True, "pitch": 12}) + assert cfg.pitch == 12 + + def test_pitch_normal_ok(self): + """正常范围内语调保持不变.""" + cfg = TtsConfig.parse({"enabled": True, "pitch": 3.5}) + assert cfg.pitch == 3.5 + + # volume 边界 + + def test_volume_below_min_clamped(self): + """音量低于最小值钳制到0.""" + cfg = TtsConfig.parse({"enabled": True, "volume": -0.5}) + assert cfg.volume == 0.0 + + def test_volume_above_max_clamped(self): + """音量高于最大值钳制到1.""" + cfg = TtsConfig.parse({"enabled": True, "volume": 2.0}) + assert cfg.volume == 1.0 + + def test_volume_at_min_ok(self): + """刚好等于最小值正常.""" + cfg = TtsConfig.parse({"enabled": True, "volume": 0.0}) + assert cfg.volume == 0.0 + + def test_volume_at_max_ok(self): + """刚好等于最大值正常.""" + cfg = TtsConfig.parse({"enabled": True, "volume": 1.0}) + assert cfg.volume == 1.0 + + def test_volume_normal_ok(self): + """正常范围内音量保持不变.""" + cfg = TtsConfig.parse({"enabled": True, "volume": 0.6}) + assert cfg.volume == 0.6 + + +# ── parse - 枚举值校验测试 ─────────────────────────────────────────────────── + + +class TestTtsConfigParseEnumValues: + """TtsConfig.parse 枚举值校验测试.""" + + # align_mode + + def test_align_mode_subtitle(self): + """subtitle对齐模式有效.""" + cfg = TtsConfig.parse({"enabled": True, "align_mode": "subtitle"}) + assert cfg.align_mode == "subtitle" + + def test_align_mode_full(self): + """full对齐模式有效.""" + cfg = TtsConfig.parse({"enabled": True, "align_mode": "full"}) + assert cfg.align_mode == "full" + + def test_align_mode_invalid_fallback(self): + """无效align_mode回退到full.""" + cfg = TtsConfig.parse({"enabled": True, "align_mode": "word_by_word"}) + assert cfg.align_mode == "full" + + # overlap_mode + + def test_overlap_mode_replace(self): + """replace叠加模式有效.""" + cfg = TtsConfig.parse({"enabled": True, "overlap_mode": "replace"}) + assert cfg.overlap_mode == "replace" + + def test_overlap_mode_mix(self): + """mix叠加模式有效.""" + cfg = TtsConfig.parse({"enabled": True, "overlap_mode": "mix"}) + assert cfg.overlap_mode == "mix" + + def test_overlap_mode_invalid_fallback(self): + """无效overlap_mode回退到replace.""" + cfg = TtsConfig.parse({"enabled": True, "overlap_mode": "duck"}) + assert cfg.overlap_mode == "replace" + + +# ── _clamp 直接测试 ────────────────────────────────────────────────────────── + + +class TestTtsConfigClampDirect: + """_clamp 方法直接调用测试.""" + + def test_clamp_speed_low(self): + """手动构造低语速再clamp.""" + cfg = TtsConfig(enabled=True, speed=0.1) + cfg._clamp() + assert cfg.speed == 0.5 + + def test_clamp_speed_high(self): + """手动构造高速再clamp.""" + cfg = TtsConfig(enabled=True, speed=10) + cfg._clamp() + assert cfg.speed == 2.0 + + def test_clamp_pitch_low(self): + """手动构造低调再clamp.""" + cfg = TtsConfig(enabled=True, pitch=-20) + cfg._clamp() + assert cfg.pitch == -12 + + def test_clamp_pitch_high(self): + """手动构造高调再clamp.""" + cfg = TtsConfig(enabled=True, pitch=20) + cfg._clamp() + assert cfg.pitch == 12 + + def test_clamp_volume_low(self): + """手动构造低音量再clamp.""" + cfg = TtsConfig(enabled=True, volume=-1) + cfg._clamp() + assert cfg.volume == 0.0 + + def test_clamp_volume_high(self): + """手动构造高音量再clamp.""" + cfg = TtsConfig(enabled=True, volume=2) + cfg._clamp() + assert cfg.volume == 1.0 + + def test_clamp_preserves_in_range(self): + """范围内的值不变.""" + cfg = TtsConfig(enabled=True, speed=1.2, pitch=3, volume=0.7) + cfg._clamp() + assert cfg.speed == 1.2 + assert cfg.pitch == 3 + assert cfg.volume == 0.7 + + +# ── is_dataclass 验证 ─────────────────────────────────────────────────────── + + +class TestTtsConfigStructure: + """TtsConfig 结构验证.""" + + def test_is_dataclass(self): + """是dataclass.""" + from dataclasses import is_dataclass + + assert is_dataclass(TtsConfig) + + def test_equality(self): + """相同配置相等.""" + c1 = TtsConfig(enabled=True, voice_id="v1") + c2 = TtsConfig(enabled=True, voice_id="v1") + assert c1 == c2 + + def test_inequality(self): + """不同配置不等.""" + c1 = TtsConfig(enabled=True, voice_id="v1") + c2 = TtsConfig(enabled=True, voice_id="v2") + assert c1 != c2 -- 2.54.0