Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0801a98cc1 |
Executable
+160
@@ -0,0 +1,160 @@
|
||||
"""BGM工具函数领域层单元测试."""
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.bgm_utils import merge_bgm_config
|
||||
|
||||
|
||||
class TestMergeBgmConfig:
|
||||
"""merge_bgm_config 函数测试."""
|
||||
|
||||
def test_both_empty(self):
|
||||
"""两个都是空字典."""
|
||||
result = merge_bgm_config({}, {})
|
||||
assert result == {}
|
||||
|
||||
def test_user_empty_returns_template_copy(self):
|
||||
"""用户配置为空,返回模板配置的拷贝."""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
result = merge_bgm_config(template, {})
|
||||
assert result == template
|
||||
# 确保是副本不是引用
|
||||
result["volume"] = 0.9
|
||||
assert template["volume"] == 0.5
|
||||
|
||||
def test_template_empty_returns_user_copy(self):
|
||||
"""模板配置为空,返回用户配置的拷贝."""
|
||||
user = {"enabled": False, "volume": 0.8}
|
||||
result = merge_bgm_config({}, user)
|
||||
assert result == user
|
||||
# 确保是副本
|
||||
result["volume"] = 0.1
|
||||
assert user["volume"] == 0.8
|
||||
|
||||
def test_user_overrides_template(self):
|
||||
"""用户配置覆盖模板配置."""
|
||||
template = {"enabled": True, "volume": 0.5, "track": "default"}
|
||||
user = {"volume": 0.8}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["volume"] == 0.8
|
||||
assert result["track"] == "default"
|
||||
|
||||
def test_enabled_special_handling_user_not_set(self):
|
||||
"""enabled 特殊处理:用户没传就保留模板的."""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
user = {"volume": 0.8} # 没传 enabled
|
||||
result = merge_bgm_config(template, user)
|
||||
# 用户没传 enabled,保留模板的 True
|
||||
assert result["enabled"] is True
|
||||
assert result["volume"] == 0.8
|
||||
|
||||
def test_enabled_user_explicit_false(self):
|
||||
"""用户显式传 enabled=False,应该覆盖模板."""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
user = {"enabled": False}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["enabled"] is False
|
||||
|
||||
def test_enabled_user_explicit_true(self):
|
||||
"""用户显式传 enabled=True,覆盖模板的 False."""
|
||||
template = {"enabled": False, "volume": 0.5}
|
||||
user = {"enabled": True}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["enabled"] is True
|
||||
|
||||
def test_full_override(self):
|
||||
"""用户完全覆盖模板."""
|
||||
template = {"enabled": True, "volume": 0.3, "track": "piano"}
|
||||
user = {"enabled": False, "volume": 0.9, "track": "guitar"}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result == user
|
||||
|
||||
def test_partial_override_keep_rest(self):
|
||||
"""部分覆盖,其余保留模板值."""
|
||||
template = {
|
||||
"enabled": True,
|
||||
"volume": 0.5,
|
||||
"fade_in": 1.0,
|
||||
"fade_out": 1.0,
|
||||
"track": "default",
|
||||
}
|
||||
user = {"volume": 0.7, "fade_in": 2.0}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["volume"] == 0.7
|
||||
assert result["fade_in"] == 2.0
|
||||
assert result["fade_out"] == 1.0
|
||||
assert result["track"] == "default"
|
||||
assert result["enabled"] is True # 用户没传,保留模板
|
||||
|
||||
def test_user_none(self):
|
||||
"""user_bgm 为 None 的情况."""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
result = merge_bgm_config(template, None) # type: ignore
|
||||
assert result == template
|
||||
|
||||
def test_template_none(self):
|
||||
"""template_bgm 为 None 的情况."""
|
||||
user = {"enabled": False, "volume": 0.8}
|
||||
result = merge_bgm_config(None, user) # type: ignore
|
||||
assert result == user
|
||||
|
||||
def test_preserves_extra_fields(self):
|
||||
"""保留模板中的额外字段(用户没覆盖的)."""
|
||||
template = {"enabled": True, "volume": 0.5, "custom_field": "value"}
|
||||
user = {"volume": 0.6}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["custom_field"] == "value"
|
||||
|
||||
def test_user_adds_new_fields(self):
|
||||
"""用户可以添加模板中没有的新字段."""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
user = {"loop": True, "start_time": 5.0}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["enabled"] is True
|
||||
assert result["volume"] == 0.5
|
||||
assert result["loop"] is True
|
||||
assert result["start_time"] == 5.0
|
||||
|
||||
def test_nested_dict_behavior(self):
|
||||
"""嵌套字典的合并行为(简单替换,不深度合并)."""
|
||||
template = {"enabled": True, "effects": {"fade": True, "reverb": False}}
|
||||
user = {"effects": {"reverb": True}}
|
||||
result = merge_bgm_config(template, user)
|
||||
# 简单合并,用户的 effects 整体覆盖模板的
|
||||
assert result["effects"] == {"reverb": True}
|
||||
|
||||
def test_enabled_in_template_only(self):
|
||||
"""只有模板有 enabled,用户没有."""
|
||||
template = {"enabled": False, "volume": 0.5}
|
||||
user = {"volume": 0.7}
|
||||
result = merge_bgm_config(template, user)
|
||||
# 用户没传 enabled,保留模板的 False
|
||||
assert result["enabled"] is False
|
||||
|
||||
def test_both_have_enabled_false(self):
|
||||
"""两边都有 enabled 且都是 False."""
|
||||
template = {"enabled": False, "volume": 0.5}
|
||||
user = {"enabled": False}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["enabled"] is False
|
||||
|
||||
def test_return_type_is_dict(self):
|
||||
"""返回类型是 dict."""
|
||||
result = merge_bgm_config({"a": 1}, {"b": 2})
|
||||
assert isinstance(result, dict)
|
||||
|
||||
def test_does_not_mutate_template(self):
|
||||
"""不修改原始模板字典."""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
template_copy = template.copy()
|
||||
user = {"volume": 0.9}
|
||||
merge_bgm_config(template, user)
|
||||
assert template == template_copy
|
||||
|
||||
def test_does_not_mutate_user(self):
|
||||
"""不修改原始用户字典."""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
user = {"volume": 0.9}
|
||||
user_copy = user.copy()
|
||||
merge_bgm_config(template, user)
|
||||
assert user == user_copy
|
||||
Executable
+166
@@ -0,0 +1,166 @@
|
||||
"""领域层异常类单元测试."""
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.exceptions import (
|
||||
DomainError,
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
QuotaExceededError,
|
||||
)
|
||||
|
||||
|
||||
class TestDomainError:
|
||||
"""领域异常基类测试."""
|
||||
|
||||
def test_is_exception(self):
|
||||
"""DomainError 继承自 Exception."""
|
||||
err = DomainError("test")
|
||||
assert isinstance(err, Exception)
|
||||
|
||||
def test_message(self):
|
||||
"""可以设置错误消息."""
|
||||
err = DomainError("something wrong")
|
||||
assert str(err) == "something wrong"
|
||||
|
||||
def test_empty_message(self):
|
||||
"""支持空消息."""
|
||||
err = DomainError()
|
||||
assert str(err) == ""
|
||||
|
||||
def test_can_be_raised(self):
|
||||
"""可以被 raise 和 catch."""
|
||||
with pytest.raises(DomainError) as exc_info:
|
||||
raise DomainError("oops")
|
||||
assert str(exc_info.value) == "oops"
|
||||
|
||||
|
||||
class TestNotFoundError:
|
||||
"""资源不存在异常测试."""
|
||||
|
||||
def test_inherits_domain_error(self):
|
||||
"""NotFoundError 继承自 DomainError."""
|
||||
err = NotFoundError("user not found")
|
||||
assert isinstance(err, DomainError)
|
||||
assert isinstance(err, Exception)
|
||||
|
||||
def test_message(self):
|
||||
"""错误消息正确."""
|
||||
err = NotFoundError("project 123 not found")
|
||||
assert str(err) == "project 123 not found"
|
||||
assert "123" in str(err)
|
||||
|
||||
def test_can_catch_as_domain_error(self):
|
||||
"""可以用 DomainError 捕获."""
|
||||
with pytest.raises(DomainError):
|
||||
raise NotFoundError("not found")
|
||||
|
||||
|
||||
class TestValidationError:
|
||||
"""校验失败异常测试."""
|
||||
|
||||
def test_inherits_domain_error(self):
|
||||
"""ValidationError 继承自 DomainError."""
|
||||
err = ValidationError("invalid input")
|
||||
assert isinstance(err, DomainError)
|
||||
|
||||
def test_message(self):
|
||||
"""错误消息正确."""
|
||||
msg = "name must not be empty"
|
||||
err = ValidationError(msg)
|
||||
assert str(err) == msg
|
||||
|
||||
def test_not_not_found(self):
|
||||
"""ValidationError 不是 NotFoundError."""
|
||||
err = ValidationError("bad")
|
||||
assert not isinstance(err, NotFoundError)
|
||||
|
||||
|
||||
class TestQuotaExceededError:
|
||||
"""配额超限异常测试."""
|
||||
|
||||
def test_inherits_domain_error(self):
|
||||
"""QuotaExceededError 继承自 DomainError."""
|
||||
err = QuotaExceededError("storage", 100.0, 150.0)
|
||||
assert isinstance(err, DomainError)
|
||||
|
||||
def test_dimension_attribute(self):
|
||||
"""保存 dimension 属性."""
|
||||
err = QuotaExceededError("storage", 100.0, 150.0)
|
||||
assert err.dimension == "storage"
|
||||
|
||||
def test_limit_attribute(self):
|
||||
"""保存 limit 属性."""
|
||||
err = QuotaExceededError("storage", 100.0, 150.0)
|
||||
assert err.limit == 100.0
|
||||
|
||||
def test_used_attribute(self):
|
||||
"""保存 used 属性."""
|
||||
err = QuotaExceededError("storage", 100.0, 150.0)
|
||||
assert err.used == 150.0
|
||||
|
||||
def test_message_format(self):
|
||||
"""错误消息格式正确."""
|
||||
err = QuotaExceededError("credits", 50.0, 75.0)
|
||||
msg = str(err)
|
||||
assert "credits" in msg
|
||||
assert "50" in msg
|
||||
assert "75" in msg
|
||||
assert "Quota exceeded" in msg
|
||||
|
||||
def test_zero_limit(self):
|
||||
"""limit 为 0 的情况."""
|
||||
err = QuotaExceededError("test", 0.0, 1.0)
|
||||
assert err.limit == 0.0
|
||||
assert err.used == 1.0
|
||||
assert "0" in str(err)
|
||||
|
||||
def test_equal_limit_and_used(self):
|
||||
"""used 刚好等于 limit(边界情况)."""
|
||||
err = QuotaExceededError("test", 100.0, 100.0)
|
||||
assert err.used == 100.0
|
||||
assert err.limit == 100.0
|
||||
|
||||
def test_integer_values(self):
|
||||
"""整数值也能正常工作."""
|
||||
err = QuotaExceededError("count", 10, 20)
|
||||
assert err.dimension == "count"
|
||||
assert err.limit == 10
|
||||
assert err.used == 20
|
||||
|
||||
def test_can_catch_as_domain_error(self):
|
||||
"""可以用 DomainError 捕获."""
|
||||
with pytest.raises(DomainError):
|
||||
raise QuotaExceededError("x", 1.0, 2.0)
|
||||
|
||||
|
||||
class TestExceptionHierarchy:
|
||||
"""异常继承关系验证."""
|
||||
|
||||
def test_all_are_domain_errors(self):
|
||||
"""所有领域异常都是 DomainError."""
|
||||
errors = [
|
||||
NotFoundError("test"),
|
||||
ValidationError("test"),
|
||||
QuotaExceededError("test", 1, 2),
|
||||
]
|
||||
for err in errors:
|
||||
assert isinstance(err, DomainError)
|
||||
|
||||
def test_all_are_exceptions(self):
|
||||
"""所有领域异常都是 Exception."""
|
||||
errors = [
|
||||
DomainError("test"),
|
||||
NotFoundError("test"),
|
||||
ValidationError("test"),
|
||||
QuotaExceededError("test", 1, 2),
|
||||
]
|
||||
for err in errors:
|
||||
assert isinstance(err, Exception)
|
||||
|
||||
def test_not_found_is_not_validation(self):
|
||||
"""不同异常类型不能互相混淆."""
|
||||
assert not isinstance(NotFoundError("x"), ValidationError)
|
||||
assert not isinstance(ValidationError("x"), NotFoundError)
|
||||
assert not isinstance(QuotaExceededError("x", 1, 2), NotFoundError)
|
||||
assert not isinstance(QuotaExceededError("x", 1, 2), ValidationError)
|
||||
@@ -327,3 +327,225 @@ class TestMergeSegments:
|
||||
assert len(result.words) == 2
|
||||
assert result.words[0].text == "你好"
|
||||
assert result.words[1].text == "世界"
|
||||
|
||||
|
||||
class TestMergeShortSegmentsEdgeCases:
|
||||
"""merge_short_segments 边界情况深度测试."""
|
||||
|
||||
def test_all_segments_too_short_merge_into_one(self):
|
||||
"""所有片段都很短,全部合并成一段."""
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="一", start=0.0, end=0.5),
|
||||
SubtitleSegment(text="二", start=0.5, end=1.0),
|
||||
SubtitleSegment(text="三", start=1.0, end=1.5),
|
||||
],
|
||||
total_duration=1.5,
|
||||
)
|
||||
result = timeline.merge_short_segments(min_chars=8)
|
||||
assert result.segment_count == 1
|
||||
assert result.segments[0].text == "一二三"
|
||||
assert result.segments[0].start == 0.0
|
||||
assert result.segments[0].end == 1.5
|
||||
|
||||
def test_exactly_min_chars_no_merge(self):
|
||||
"""刚好等于 min_chars,不合并."""
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="一二三四五六七八", start=0.0, end=1.0),
|
||||
SubtitleSegment(text="一二三四五六七八", start=1.0, end=2.0),
|
||||
],
|
||||
total_duration=2.0,
|
||||
)
|
||||
result = timeline.merge_short_segments(min_chars=8)
|
||||
assert result.segment_count == 2
|
||||
|
||||
def test_min_chars_one(self):
|
||||
"""min_chars=1 时每个都够,不合并."""
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="一", start=0.0, end=0.5),
|
||||
SubtitleSegment(text="二", start=0.5, end=1.0),
|
||||
],
|
||||
total_duration=1.0,
|
||||
)
|
||||
result = timeline.merge_short_segments(min_chars=1)
|
||||
assert result.segment_count == 2
|
||||
|
||||
def test_merge_preserves_words_order(self):
|
||||
"""合并后词的顺序保持正确."""
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(
|
||||
text="你好",
|
||||
start=0.0,
|
||||
end=0.5,
|
||||
words=[
|
||||
SubtitleWord(text="你", start=0.0, end=0.25),
|
||||
SubtitleWord(text="好", start=0.25, end=0.5),
|
||||
],
|
||||
),
|
||||
SubtitleSegment(
|
||||
text="世界",
|
||||
start=0.5,
|
||||
end=1.0,
|
||||
words=[
|
||||
SubtitleWord(text="世", start=0.5, end=0.75),
|
||||
SubtitleWord(text="界", start=0.75, end=1.0),
|
||||
],
|
||||
),
|
||||
],
|
||||
total_duration=1.0,
|
||||
)
|
||||
result = timeline.merge_short_segments(min_chars=8)
|
||||
assert result.segment_count == 1
|
||||
words = result.segments[0].words
|
||||
assert len(words) == 4
|
||||
assert [w.text for w in words] == ["你", "好", "世", "界"]
|
||||
|
||||
def test_last_segment_short_merges_with_previous(self):
|
||||
"""最后一段太短,合并到前一段."""
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="一二三四五六七八", start=0.0, end=1.0),
|
||||
SubtitleSegment(text="九", start=1.0, end=1.2),
|
||||
],
|
||||
total_duration=1.2,
|
||||
)
|
||||
result = timeline.merge_short_segments(min_chars=8)
|
||||
assert result.segment_count == 1
|
||||
assert result.segments[0].text == "一二三四五六七八九"
|
||||
|
||||
|
||||
class TestSplitLongSegmentsEdgeCases:
|
||||
"""split_long_segments 边界情况深度测试."""
|
||||
|
||||
def test_mixed_long_and_short(self):
|
||||
"""长短片段混合,只拆分长的."""
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="短", start=0.0, end=0.5),
|
||||
SubtitleSegment(
|
||||
text="这是一段非常长的字幕内容需要被拆分",
|
||||
start=0.5,
|
||||
end=3.0,
|
||||
),
|
||||
SubtitleSegment(text="短", start=3.0, end=3.5),
|
||||
],
|
||||
total_duration=3.5,
|
||||
)
|
||||
result = timeline.split_long_segments(max_chars=10)
|
||||
assert result.segment_count > 3 # 中间那段被拆分了
|
||||
assert result.segments[0].text == "短"
|
||||
assert result.segments[-1].text == "短"
|
||||
|
||||
def test_split_total_duration_preserved(self):
|
||||
"""拆分后总时长不变."""
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(
|
||||
text="一二三四五六七八九十一二三四五六七八九十",
|
||||
start=0.0,
|
||||
end=10.0,
|
||||
),
|
||||
],
|
||||
total_duration=10.0,
|
||||
)
|
||||
result = timeline.split_long_segments(max_chars=10)
|
||||
assert result.segment_count > 1
|
||||
assert result.segments[0].start == 0.0
|
||||
assert abs(result.segments[-1].end - 10.0) < 0.01
|
||||
|
||||
def test_max_chars_very_small(self):
|
||||
"""max_chars 很小,每个字都要拆."""
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="一二三", start=0.0, end=3.0),
|
||||
],
|
||||
total_duration=3.0,
|
||||
)
|
||||
result = timeline.split_long_segments(max_chars=1)
|
||||
# 没有标点,硬切
|
||||
assert result.segment_count >= 3
|
||||
|
||||
def test_empty_segments_list(self):
|
||||
"""空片段列表不报错."""
|
||||
timeline = SubtitleTimeline(segments=[], total_duration=0.0)
|
||||
result = timeline.split_long_segments(max_chars=10)
|
||||
assert result.segment_count == 0
|
||||
|
||||
|
||||
class TestSplitTextByPunctuationDeep:
|
||||
"""_split_text_by_punctuation 深度测试."""
|
||||
|
||||
def test_multiple_punctuation_types(self):
|
||||
"""多种标点符号混合."""
|
||||
text = "你好!世界?测试,哈哈。"
|
||||
result = SubtitleTimeline._split_text_by_punctuation(text, 5)
|
||||
assert len(result) >= 2
|
||||
|
||||
def test_consecutive_punctuation(self):
|
||||
"""连续标点符号."""
|
||||
text = "你好!!!测试。。。"
|
||||
result = SubtitleTimeline._split_text_by_punctuation(text, 3)
|
||||
assert len(result) >= 1
|
||||
# 确保所有字符都保留
|
||||
assert "".join(result) == text
|
||||
|
||||
def test_no_punctuation_long_text(self):
|
||||
"""长文本没有标点,硬切."""
|
||||
text = "一二三四五六七八九十一二三四五六七八九十"
|
||||
result = SubtitleTimeline._split_text_by_punctuation(text, 10)
|
||||
assert len(result) >= 2
|
||||
assert "".join(result) == text
|
||||
# 每段不超过 max_chars
|
||||
for part in result[:-1]: # 最后一段可能短一些
|
||||
assert len(part) <= 10
|
||||
|
||||
def test_punctuation_at_start(self):
|
||||
"""标点在开头."""
|
||||
text = ",你好世界"
|
||||
result = SubtitleTimeline._split_text_by_punctuation(text, 10)
|
||||
assert "".join(result) == text
|
||||
|
||||
def test_punctuation_at_end(self):
|
||||
"""标点在结尾."""
|
||||
text = "你好世界!"
|
||||
result = SubtitleTimeline._split_text_by_punctuation(text, 10)
|
||||
assert "".join(result) == text
|
||||
assert result[-1].endswith("!")
|
||||
|
||||
|
||||
class TestSubtitleTimelineProperties:
|
||||
"""SubtitleTimeline 属性计算深度测试."""
|
||||
|
||||
def test_total_chars_empty(self):
|
||||
"""空时间轴 total_chars 为 0."""
|
||||
timeline = SubtitleTimeline(segments=[])
|
||||
assert timeline.total_chars == 0
|
||||
|
||||
def test_total_chars_sum(self):
|
||||
"""total_chars 等于所有片段字数之和."""
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="你好", start=0, end=1),
|
||||
SubtitleSegment(text="世界", start=1, end=2),
|
||||
SubtitleSegment(text="123", start=2, end=3),
|
||||
],
|
||||
)
|
||||
assert timeline.total_chars == 2 + 2 + 3
|
||||
|
||||
def test_segment_count(self):
|
||||
"""segment_count 正确."""
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="a", start=0, end=1),
|
||||
SubtitleSegment(text="b", start=1, end=2),
|
||||
],
|
||||
)
|
||||
assert timeline.segment_count == 2
|
||||
|
||||
def test_empty_segment_char_count(self):
|
||||
"""空片段 char_count 为 0."""
|
||||
seg = SubtitleSegment(text="", start=0.0, end=1.0)
|
||||
assert seg.char_count == 0
|
||||
|
||||
Reference in New Issue
Block a user