test: P3-1 第48波单元测试(tag/verification_code/subtitle/voice_presets,+59) #836

Merged
xiaoxia merged 1 commits from test/wave48-domain-tag-verification-subtitle-voicepresets into develop 2026-07-24 18:58:14 +08:00
4 changed files with 432 additions and 0 deletions
+199
View File
@@ -0,0 +1,199 @@
"""Subtitle 领域模型单测."""
import pytest
from packages.domain.subtitle import SubtitleWord, SubtitleSegment, SubtitleTimeline
class TestSubtitleWord:
def test_duration_normal(self):
word = SubtitleWord(text="你好", start=1.0, end=2.5)
assert word.duration == pytest.approx(1.5)
def test_duration_zero(self):
word = SubtitleWord(text="", start=3.0, end=3.0)
assert word.duration == 0.0
def test_duration_negative_returns_zero(self):
word = SubtitleWord(text="test", start=5.0, end=3.0)
assert word.duration == 0.0
class TestSubtitleSegment:
def test_duration(self):
seg = SubtitleSegment(text="大家好", start=0.0, end=3.0)
assert seg.duration == pytest.approx(3.0)
def test_char_count(self):
seg = SubtitleSegment(text="今天天气真好", start=0.0, end=5.0)
assert seg.char_count == 6
def test_empty_text(self):
seg = SubtitleSegment(text="", start=0.0, end=1.0)
assert seg.char_count == 0
def test_default_words_empty(self):
seg = SubtitleSegment(text="test", start=0.0, end=1.0)
assert seg.words == []
class TestSubtitleTimeline:
def test_empty_timeline(self):
tl = SubtitleTimeline()
assert tl.segment_count == 0
assert tl.total_chars == 0
assert tl.language == "zh"
assert tl.total_duration == 0.0
def test_segment_count(self):
tl = SubtitleTimeline(segments=[
SubtitleSegment(text="第一段", start=0.0, end=2.0),
SubtitleSegment(text="第二段", start=2.0, end=5.0),
])
assert tl.segment_count == 2
assert tl.total_chars == 6
def test_total_chars(self):
tl = SubtitleTimeline(segments=[
SubtitleSegment(text="abc", start=0, end=1),
SubtitleSegment(text="defg", start=1, end=2),
])
assert tl.total_chars == 7
class TestMergeShortSegments:
def test_single_segment_no_change(self):
tl = SubtitleTimeline(segments=[
SubtitleSegment(text="你好", start=0.0, end=1.0),
])
result = tl.merge_short_segments(min_chars=8)
assert result.segment_count == 1
assert result.segments[0].text == "你好"
def test_empty_timeline(self):
tl = SubtitleTimeline()
result = tl.merge_short_segments()
assert result.segment_count == 0
def test_merge_short_segments(self):
tl = SubtitleTimeline(segments=[
SubtitleSegment(text="你好", start=0.0, end=1.0),
SubtitleSegment(text="今天", start=1.0, end=2.0),
SubtitleSegment(text="天气", start=2.0, end=3.0),
SubtitleSegment(text="真好", start=3.0, end=4.0),
])
result = tl.merge_short_segments(min_chars=4)
# 每段2字,min=4,应该每2段合并
assert result.segment_count == 2
assert result.segments[0].text == "你好今天"
assert result.segments[0].start == 0.0
assert result.segments[0].end == 2.0
assert result.segments[1].text == "天气真好"
assert result.segments[1].start == 2.0
assert result.segments[1].end == 4.0
def test_remaining_merged_to_last(self):
# 3段,每段2字,min=5 → 前5字合并,剩余1字并到最后
tl = SubtitleTimeline(segments=[
SubtitleSegment(text="一二", start=0, end=1),
SubtitleSegment(text="三四", start=1, end=2),
SubtitleSegment(text="", start=2, end=3),
])
result = tl.merge_short_segments(min_chars=5)
assert result.segment_count == 1
assert result.segments[0].text == "一二三四五"
def test_merge_with_words(self):
tl = SubtitleTimeline(segments=[
SubtitleSegment(text="你好", start=0.0, end=1.0, words=[
SubtitleWord(text="", start=0.0, end=0.5),
SubtitleWord(text="", start=0.5, end=1.0),
]),
SubtitleSegment(text="世界", start=1.0, end=2.0, words=[
SubtitleWord(text="", start=1.0, end=1.5),
SubtitleWord(text="", start=1.5, end=2.0),
]),
])
result = tl.merge_short_segments(min_chars=10)
assert result.segment_count == 1
assert len(result.segments[0].words) == 4
class TestSplitLongSegments:
def test_short_segments_no_split(self):
tl = SubtitleTimeline(segments=[
SubtitleSegment(text="短文本", start=0.0, end=1.0),
])
result = tl.split_long_segments(max_chars=20)
assert result.segment_count == 1
def test_split_by_punctuation(self):
text = "今天天气真好。我们出去玩吧!"
tl = SubtitleTimeline(segments=[
SubtitleSegment(text=text, start=0.0, end=5.0),
])
result = tl.split_long_segments(max_chars=10)
assert result.segment_count >= 2
# 合并起来应该等于原文
assert "".join(s.text for s in result.segments) == text
def test_split_preserves_time_order(self):
tl = SubtitleTimeline(segments=[
SubtitleSegment(text="一二三四五六七八九十。十一二三四五六七八九十。", start=0.0, end=10.0),
])
result = tl.split_long_segments(max_chars=10)
# 时间应该是递增的
for i in range(len(result.segments) - 1):
assert result.segments[i].end <= result.segments[i + 1].start + 0.001
def test_empty_timeline(self):
tl = SubtitleTimeline()
result = tl.split_long_segments()
assert result.segment_count == 0
class TestSplitTextByPunctuation:
def test_no_punctuation_short(self):
result = SubtitleTimeline._split_text_by_punctuation("你好世界", 20)
assert len(result) == 1
assert result[0] == "你好世界"
def test_sentence_end_punctuation_long_enough(self):
# 每段超过 max_chars//2 才会在句末标点断开
text = "今天天气真的非常好。明天天气也不错。"
result = SubtitleTimeline._split_text_by_punctuation(text, 10)
assert len(result) >= 2
assert "".join(result) == text
def test_short_text_with_punctuation_no_split(self):
# 文本太短(< max_chars//2),即使有标点也不断开
result = SubtitleTimeline._split_text_by_punctuation("你好。世界。", 20)
assert len(result) == 1
def test_long_text_hard_split(self):
text = "一二三四五六七八九十十一二三四五六七八九十"
result = SubtitleTimeline._split_text_by_punctuation(text, 10)
assert len(result) >= 2
assert "".join(result) == text
def test_empty_text(self):
result = SubtitleTimeline._split_text_by_punctuation("", 10)
assert result == []
class TestMergeSegments:
def test_merge_two_segments(self):
segs = [
SubtitleSegment(text="你好", start=0.0, end=1.0),
SubtitleSegment(text="世界", start=1.0, end=2.0),
]
result = SubtitleTimeline._merge_segments(segs)
assert result.text == "你好世界"
assert result.start == 0.0
assert result.end == 2.0
def test_merge_empty_list(self):
result = SubtitleTimeline._merge_segments([])
assert result.text == ""
assert result.start == 0
assert result.end == 0
+27
View File
@@ -0,0 +1,27 @@
"""Tag 领域实体单测."""
import pytest
from packages.domain.tag import Tag
class TestTagCreate:
def test_create_normal(self):
tag = Tag.create(user_id="user1", name=" 美食 ")
assert tag.id
assert tag.user_id == "user1"
assert tag.name == "美食" # 自动 strip
assert tag.created_at is not None
def test_create_empty_name_raises(self):
with pytest.raises(ValueError, match="标签名称不能为空"):
Tag.create(user_id="user1", name="")
def test_create_whitespace_name_raises(self):
with pytest.raises(ValueError, match="标签名称不能为空"):
Tag.create(user_id="user1", name=" ")
def test_create_generates_unique_ids(self):
tag1 = Tag.create(user_id="u1", name="tag1")
tag2 = Tag.create(user_id="u1", name="tag2")
assert tag1.id != tag2.id
+69
View File
@@ -0,0 +1,69 @@
"""VerificationCode 领域实体单测."""
from datetime import datetime, timedelta, timezone
import pytest
from packages.domain.verification_code import VerificationCode
class TestVerificationCodeCreate:
def test_create_default_ttl(self):
code = VerificationCode.create(recipient="test@example.com", code_type="email_bind")
assert code.id
assert code.recipient == "test@example.com"
assert code.code_type == "email_bind"
assert len(code.code) == 6
assert code.code.isdigit()
assert code.used_at is None
assert code.attempts == 0
# 默认5分钟过期
assert code.expires_at > code.created_at
assert (code.expires_at - code.created_at).total_seconds() == pytest.approx(300, abs=1)
def test_create_custom_ttl(self):
code = VerificationCode.create(recipient="13800138000", code_type="phone_login", ttl_seconds=60)
assert (code.expires_at - code.created_at).total_seconds() == pytest.approx(60, abs=1)
def test_create_custom_code(self):
code = VerificationCode.create(recipient="test@example.com", code_type="reset_password", custom_code="123456")
assert code.code == "123456"
def test_create_recipient_stripped(self):
code = VerificationCode.create(recipient=" test@example.com ", code_type="email_bind")
assert code.recipient == "test@example.com"
class TestVerificationCodeStatus:
def test_is_valid_initial(self):
code = VerificationCode.create(recipient="test@example.com", code_type="email_bind")
assert code.is_valid is True
assert code.is_expired is False
assert code.is_used is False
def test_mark_used(self):
code = VerificationCode.create(recipient="test@example.com", code_type="email_bind")
code.mark_used()
assert code.is_used is True
assert code.used_at is not None
assert code.is_valid is False
def test_is_expired_future(self):
code = VerificationCode.create(recipient="test@example.com", code_type="email_bind", ttl_seconds=3600)
assert code.is_expired is False
def test_increment_attempts(self):
code = VerificationCode.create(recipient="test@example.com", code_type="email_bind")
assert code.attempts == 0
code.increment_attempts()
assert code.attempts == 1
code.increment_attempts()
assert code.attempts == 2
def test_is_valid_after_expired(self):
code = VerificationCode.create(recipient="test@example.com", code_type="email_bind", ttl_seconds=0)
# 0秒TTL,立即可能过期(有极小概率因时间差没过)
import time
time.sleep(0.01)
assert code.is_expired is True
assert code.is_valid is False
+137
View File
@@ -0,0 +1,137 @@
"""voice_presets 音色预设单测."""
import pytest
from packages.domain.voice_presets import (
MOCK_VOICES,
VoiceGender,
VoicePreset,
VoiceStyle,
get_default_voice,
get_voice,
list_voices,
)
class TestVoiceGender:
def test_values(self):
assert VoiceGender.MALE.value == "male"
assert VoiceGender.FEMALE.value == "female"
assert VoiceGender.CHILD.value == "child"
def test_is_str(self):
assert isinstance(VoiceGender.FEMALE, str)
class TestVoiceStyle:
def test_values(self):
assert VoiceStyle.STABLE.value == "stable"
assert VoiceStyle.LIVELY.value == "lively"
assert VoiceStyle.NARRATION.value == "narration"
assert VoiceStyle.NEWS.value == "news"
assert VoiceStyle.STORY.value == "story"
class TestVoicePreset:
def test_default_values(self):
v = VoicePreset(voice_id="test", name="测试音色")
assert v.gender == VoiceGender.FEMALE
assert v.style == VoiceStyle.NARRATION
assert v.provider == "mock"
assert v.default_speed == 1.0
assert v.default_pitch == 0.0
assert v.sample_rate == 22050
assert v.language == "zh-CN"
def test_custom_values(self):
v = VoicePreset(
voice_id="male1",
name="男声",
gender=VoiceGender.MALE,
style=VoiceStyle.STABLE,
provider="aliyun",
default_speed=0.9,
)
assert v.gender == VoiceGender.MALE
assert v.style == VoiceStyle.STABLE
assert v.provider == "aliyun"
assert v.default_speed == 0.9
class TestMockVoices:
def test_mock_voices_not_empty(self):
assert len(MOCK_VOICES) > 0
def test_all_mock_voices_have_ids(self):
for v in MOCK_VOICES:
assert v.voice_id
assert v.name
assert v.provider == "mock"
def test_unique_voice_ids(self):
ids = [v.voice_id for v in MOCK_VOICES]
assert len(ids) == len(set(ids))
class TestGetVoice:
def test_get_existing_voice(self):
v = get_voice("female_warm")
assert v is not None
assert v.voice_id == "female_warm"
assert v.name == "温暖女声"
def test_get_nonexistent_voice(self):
v = get_voice("nonexistent")
assert v is None
def test_non_mock_provider_returns_none(self):
v = get_voice("female_warm", provider="aliyun")
assert v is None
class TestListVoices:
def test_list_all(self):
voices = list_voices()
assert len(voices) == len(MOCK_VOICES)
def test_filter_by_gender(self):
female_voices = list_voices(gender="female")
assert len(female_voices) > 0
assert all(v.gender == VoiceGender.FEMALE for v in female_voices)
def test_filter_by_style(self):
story_voices = list_voices(style="story")
assert len(story_voices) > 0
assert all(v.style == VoiceStyle.STORY for v in story_voices)
def test_filter_by_keyword_name(self):
voices = list_voices(keyword="女声")
assert len(voices) > 0
assert all("女声" in v.name for v in voices)
def test_filter_by_keyword_description(self):
voices = list_voices(keyword="商务")
assert len(voices) > 0
assert any("商务" in v.description for v in voices)
def test_filter_by_provider_non_mock(self):
voices = list_voices(provider="aliyun")
assert len(voices) == 0
def test_filter_multiple_conditions(self):
voices = list_voices(gender="female", style="narration")
assert len(voices) > 0
assert all(v.gender == VoiceGender.FEMALE for v in voices)
assert all(v.style == VoiceStyle.NARRATION for v in voices)
def test_keyword_case_insensitive(self):
voices1 = list_voices(keyword="FEMALE")
voices2 = list_voices(keyword="female")
assert len(voices1) == len(voices2)
class TestGetDefaultVoice:
def test_default_voice_exists(self):
v = get_default_voice()
assert v is not None
assert v == MOCK_VOICES[0]