From 817c5a46dcc670be070dfe8dfbac6d66351a063b Mon Sep 17 00:00:00 2001 From: CI Bot Date: Fri, 24 Jul 2026 19:14:17 +0800 Subject: [PATCH] =?UTF-8?q?test(p3-1):=20wave54=20-=20domain=E5=B1=82?= =?UTF-8?q?=E5=89=A9=E4=BD=99=E5=B0=8F=E6=A8=A1=E5=9D=97=E5=8D=95=E6=B5=8B?= =?UTF-8?q?=20+62?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖以下 12 个模块: - bgm_utils: merge_bgm_config 合并逻辑 +10 - editing_mode: EditingMode 枚举 +3 - exceptions: 4个异常类 +7 - recipe: Recipe/RecipeItem +4 - template: Template/TemplateSegment/TemplateCategory +4 - template_version: EditTemplateVersion +4 - template_clip_config: ClipType/TransitionEffect/TemplateClipConfig +6 - preset_bgm: PresetBGM/预设库/查询函数 +10 - preset_voices: PresetVoice/预置音色/查询函数 +10 - title_library: TitleLibraryItem +2 - voice_library: VoiceLibraryItem +2 - 合计 +62 --- .../test_domain_remaining_small_modules.py | 596 ++++++++++++++++++ 1 file changed, 596 insertions(+) create mode 100755 tests/unit/test_domain_remaining_small_modules.py diff --git a/tests/unit/test_domain_remaining_small_modules.py b/tests/unit/test_domain_remaining_small_modules.py new file mode 100755 index 000000000..b312958c4 --- /dev/null +++ b/tests/unit/test_domain_remaining_small_modules.py @@ -0,0 +1,596 @@ +"""domain 层剩余小模块单测 - bgm_utils/exceptions/editing_mode/recipe/template/template_version/template_clip_config/preset_bgm/preset_voices/title_library/voice_library""" + +import pytest + +from packages.domain.bgm_utils import merge_bgm_config +from packages.domain.editing_mode import EditingMode +from packages.domain.exceptions import ( + DomainError, + NotFoundError, + QuotaExceededError, + ValidationError, +) +from packages.domain.preset_bgm import ( + PRESET_BGM_LIBRARY, + PresetBGM, + get_preset_bgm, + list_preset_bgm_by_style, + search_preset_bgm, +) +from packages.domain.preset_voices import ( + PRESET_VOICES, + PresetVoice, + get_preset_voice_by_id, + get_preset_voices, + is_preset_voice, +) +from packages.domain.recipe import Recipe, RecipeItem +from packages.domain.template import Template, TemplateCategory, TemplateSegment +from packages.domain.template_clip_config import ( + ClipType, + TemplateClipConfig, + TransitionEffect, +) +from packages.domain.template_version import EditTemplateVersion +from packages.domain.title_library import TitleLibraryItem +from packages.domain.voice_library import VoiceLibraryItem + + +# ── EditingMode ─────────────────────────────────────────────────────────────── + + +class TestEditingMode: + """EditingMode 枚举测试""" + + def test_all_modes_exist(self): + assert EditingMode.ONE_TAKE == "one_take" + assert EditingMode.PIP == "pip" + assert EditingMode.VOICE_OVER == "voice_over" + assert EditingMode.VOICE_PIP == "voice_pip" + + def test_str_enum(self): + assert isinstance(EditingMode.ONE_TAKE, str) + + def test_four_modes(self): + assert len(EditingMode) == 4 + + +# ── Exceptions ──────────────────────────────────────────────────────────────── + + +class TestDomainExceptions: + """异常类测试""" + + def test_domain_error_base(self): + err = DomainError("test") + assert isinstance(err, Exception) + assert str(err) == "test" + + def test_not_found_error(self): + err = NotFoundError("not found") + assert isinstance(err, DomainError) + assert str(err) == "not found" + + def test_validation_error(self): + err = ValidationError("invalid") + assert isinstance(err, DomainError) + assert str(err) == "invalid" + + def test_quota_exceeded_error(self): + err = QuotaExceededError(dimension="storage", limit=100, used=150) + assert isinstance(err, DomainError) + assert err.dimension == "storage" + assert err.limit == 100 + assert err.used == 150 + assert "storage" in str(err) + assert "150/100" in str(err) + + def test_not_found_is_domain_error(self): + assert issubclass(NotFoundError, DomainError) + + def test_validation_is_domain_error(self): + assert issubclass(ValidationError, DomainError) + + def test_quota_exceeded_is_domain_error(self): + assert issubclass(QuotaExceededError, DomainError) + + +# ── BGM Utils ──────────────────────────────────────────────────────────────── + + +class TestMergeBgmConfig: + """merge_bgm_config 函数测试""" + + def test_user_empty_returns_template_copy(self): + template = {"enabled": True, "volume": 0.5, "source": "library"} + 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.3} + result = merge_bgm_config({}, user) + assert result == user + + def test_user_none_returns_template(self): + template = {"enabled": True, "volume": 0.5} + result = merge_bgm_config(template, None) # type: ignore + assert result == template + + def test_template_none_returns_user(self): + user = {"enabled": True, "volume": 0.5} + result = merge_bgm_config(None, user) # type: ignore + assert result == user + + def test_user_overrides_template(self): + template = {"volume": 0.3, "source": "library", "asset_id": "tpl-1"} + user = {"volume": 0.8, "asset_id": "user-1"} + result = merge_bgm_config(template, user) + assert result["volume"] == 0.8 + assert result["asset_id"] == "user-1" + assert result["source"] == "library" # 模板保留 + + def test_enabled_special_handling_user_not_set(self): + """用户没传 enabled 时保留模板的 enabled""" + template = {"enabled": True, "volume": 0.5} + user = {"volume": 0.8} + result = merge_bgm_config(template, user) + assert result["enabled"] is True # 保留模板值 + + 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 则覆盖""" + template = {"enabled": False, "volume": 0.5} + user = {"enabled": True} + result = merge_bgm_config(template, user) + assert result["enabled"] is True + + def test_full_merge(self): + """完整合并场景""" + template = { + "enabled": True, + "source": "library", + "volume": 0.3, + "fade_in": 0.0, + "fade_out": 0.0, + "loop_enabled": True, + } + user = { + "volume": 0.7, + "asset_id": "my-bgm", + "fade_in": 1.0, + } + result = merge_bgm_config(template, user) + assert result["enabled"] is True # 保留模板 + assert result["volume"] == 0.7 # 用户覆盖 + assert result["source"] == "library" # 模板保留 + assert result["asset_id"] == "my-bgm" # 用户新增 + assert result["fade_in"] == 1.0 # 用户覆盖 + assert result["fade_out"] == 0.0 # 模板保留 + assert result["loop_enabled"] is True # 模板保留 + + +# ── Recipe ─────────────────────────────────────────────────────────────────── + + +class TestRecipe: + """Recipe / RecipeItem 测试""" + + def test_recipe_item_create(self): + item = RecipeItem(id="item-1", recipe_id="r1", item_type="asset", item_id="a1") + assert item.id == "item-1" + assert item.recipe_id == "r1" + assert item.item_type == "asset" + assert item.item_id == "a1" + assert item.position == 0 + assert item.metadata_ == {} + + def test_recipe_create(self): + recipe = Recipe(id="r1", user_id="u1", name="我的配方") + assert recipe.id == "r1" + assert recipe.user_id == "u1" + assert recipe.name == "我的配方" + assert recipe.description == "" + assert recipe.items == [] + assert recipe.is_active is True + assert recipe.generation_params == {} + assert recipe.created_at is not None + + def test_recipe_with_items(self): + items = [ + RecipeItem(id="i1", recipe_id="r1", item_type="asset", item_id="a1", position=0), + RecipeItem(id="i2", recipe_id="r1", item_type="title", item_id="t1", position=1), + ] + recipe = Recipe(id="r1", user_id="u1", name="test", items=items) + assert len(recipe.items) == 2 + assert recipe.items[0].item_type == "asset" + assert recipe.items[1].position == 1 + + def test_recipe_items_independent_list(self): + """不同 recipe 的 items 是独立列表""" + r1 = Recipe(id="r1", user_id="u1", name="r1") + r2 = Recipe(id="r2", user_id="u1", name="r2") + assert r1.items is not r2.items + + +# ── Template ───────────────────────────────────────────────────────────────── + + +class TestTemplate: + """Template 相关实体测试""" + + def test_template_segment(self): + seg = TemplateSegment( + id="seg-1", + template_id="t1", + segment_order=0, + duration_min=3.0, + duration_max=5.0, + ) + assert seg.id == "seg-1" + assert seg.template_id == "t1" + assert seg.segment_order == 0 + assert seg.duration_min == 3.0 + assert seg.duration_max == 5.0 + assert seg.material_type is None + assert seg.created_at is not None + + def test_template_create(self): + tpl = Template( + id="t1", + user_id="u1", + name="口播模板", + mode="voice_over", + ) + assert tpl.id == "t1" + assert tpl.user_id == "u1" + assert tpl.name == "口播模板" + assert tpl.mode == "voice_over" + assert tpl.category == "" + assert tpl.tags == [] + assert tpl.segments == [] + assert tpl.is_active is True + assert tpl.estimated_duration == 0.0 + + def test_template_with_segments(self): + segs = [ + TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=2, duration_max=4), + TemplateSegment(id="s2", template_id="t1", segment_order=1, duration_min=5, duration_max=8), + ] + tpl = Template(id="t1", user_id="u1", name="test", mode="one_take", segments=segs) + assert len(tpl.segments) == 2 + assert tpl.segments[0].segment_order == 0 + + def test_template_category(self): + cat = TemplateCategory(id="cat-1", user_id="u1", name="热门") + assert cat.id == "cat-1" + assert cat.user_id == "u1" + assert cat.name == "热门" + assert cat.created_at is not None + + +# ── TemplateVersion ────────────────────────────────────────────────────────── + + +class TestEditTemplateVersion: + """EditTemplateVersion 测试""" + + def test_create_basic(self): + v = EditTemplateVersion.create(template_id="t1", version=1) + assert v.id + assert len(v.id) == 32 + assert v.template_id == "t1" + assert v.version == 1 + assert v.name == "" + assert v.editing_mode == "one_take" + assert v.config == {} + assert v.clip_configs == [] + assert v.change_note == "" + assert v.published_by == "" + + def test_create_with_details(self): + config = {"title": {"size": 36}} + clips = [{"clip_type": "intro"}, {"clip_type": "outro"}] + v = EditTemplateVersion.create( + template_id="t1", + version=2, + name="V2 优化版", + editing_mode="voice_over", + config=config, + clip_configs=clips, + change_note="优化了节奏", + published_by="user-1", + ) + assert v.version == 2 + assert v.name == "V2 优化版" + assert v.editing_mode == "voice_over" + assert v.config == config + assert v.clip_configs == clips + assert v.change_note == "优化了节奏" + assert v.published_by == "user-1" + + def test_create_none_config_defaults_empty(self): + v = EditTemplateVersion.create(template_id="t1", version=1, config=None) + assert v.config == {} + + def test_create_none_clip_configs_defaults_empty(self): + v = EditTemplateVersion.create(template_id="t1", version=1, clip_configs=None) + assert v.clip_configs == [] + + +# ── TemplateClipConfig ─────────────────────────────────────────────────────── + + +class TestTemplateClipConfig: + """TemplateClipConfig + 枚举测试""" + + def test_clip_type_values(self): + assert ClipType.INTRO == "intro" + assert ClipType.MAIN == "main" + assert ClipType.TRANSITION == "transition" + assert ClipType.OUTRO == "outro" + assert ClipType.TITLE == "title" + assert ClipType.SUBTITLE == "subtitle" + + def test_clip_type_count(self): + assert len(ClipType) == 6 + + def test_transition_effect_values(self): + assert TransitionEffect.CUT == "cut" + assert TransitionEffect.FADE == "fade" + assert TransitionEffect.SLIDE_LEFT == "slide_left" + assert TransitionEffect.SLIDE_RIGHT == "slide_right" + assert TransitionEffect.DISSOLVE == "dissolve" + assert TransitionEffect.WIPE == "wipe" + + def test_transition_effect_count(self): + assert len(TransitionEffect) == 6 + + def test_template_clip_config(self): + clip = TemplateClipConfig( + id="clip-1", + template_id="t1", + clip_type=ClipType.MAIN, + order=1, + min_duration=2.0, + max_duration=5.0, + ) + assert clip.id == "clip-1" + assert clip.template_id == "t1" + assert clip.clip_type == ClipType.MAIN + assert clip.order == 1 + assert clip.min_duration == 2.0 + assert clip.max_duration == 5.0 + + def test_str_enum(self): + assert isinstance(ClipType.INTRO, str) + assert isinstance(TransitionEffect.FADE, str) + + +# ── PresetBGM ──────────────────────────────────────────────────────────────── + + +class TestPresetBGM: + """PresetBGM + 查询函数测试""" + + def test_preset_bgm_create(self): + bgm = PresetBGM( + id="bgm_test_001", + name="测试音乐", + style="upbeat", + duration=120.0, + ) + assert bgm.id == "bgm_test_001" + assert bgm.name == "测试音乐" + assert bgm.style == "upbeat" + assert bgm.duration == 120.0 + assert bgm.artist == "" + assert bgm.tags == [] + assert bgm.audio_url == "" + + def test_preset_bgm_frozen(self): + bgm = PresetBGM(id="t1", name="t", style="x", duration=10.0) + with pytest.raises(Exception): + bgm.name = "改名" + + def test_library_not_empty(self): + assert len(PRESET_BGM_LIBRARY) > 0 + + def test_all_presets_have_required_fields(self): + for bgm in PRESET_BGM_LIBRARY: + assert bgm.id + assert bgm.name + assert bgm.style + assert bgm.duration > 0 + + def test_get_preset_bgm_existing(self): + first = PRESET_BGM_LIBRARY[0] + result = get_preset_bgm(first.id) + assert result is not None + assert result.id == first.id + + def test_get_preset_bgm_nonexistent(self): + assert get_preset_bgm("nonexistent_bgm") is None + + def test_list_preset_bgm_by_style(self): + upbeat = list_preset_bgm_by_style("upbeat") + assert len(upbeat) > 0 + assert all(b.style == "upbeat" for b in upbeat) + + def test_list_preset_bgm_by_style_empty(self): + result = list_preset_bgm_by_style("nonexistent_style") + assert result == [] + + def test_search_preset_bgm_by_name(self): + result = search_preset_bgm("阳光") + assert len(result) >= 1 + assert any("阳光" in b.name for b in result) + + def test_search_preset_bgm_by_tag(self): + result = search_preset_bgm("轻快") + assert len(result) >= 1 + assert any(any("轻快" in t for t in b.tags) for b in result) + + def test_search_preset_bgm_empty_result(self): + result = search_preset_bgm("xyz_not_exist_keyword") + assert result == [] + + +# ── PresetVoices ───────────────────────────────────────────────────────────── + + +class TestPresetVoices: + """PresetVoice + 查询函数测试""" + + def test_preset_voice_create(self): + v = PresetVoice( + voice_id="test_voice", + name="测试音色", + description="测试用", + gender="female", + ) + assert v.voice_id == "test_voice" + assert v.name == "测试音色" + assert v.gender == "female" + assert v.language == "zh-CN" + assert v.preview_url == "" + assert v.tags is None + + def test_preset_voice_to_dict(self): + v = PresetVoice( + voice_id="v1", + name="音色1", + description="desc", + gender="male", + language="zh-CN", + tags=["温柔", "男声"], + ) + d = v.to_dict() + assert d["voice_id"] == "v1" + assert d["name"] == "音色1" + assert d["description"] == "desc" + assert d["gender"] == "male" + assert d["language"] == "zh-CN" + assert d["tags"] == ["温柔", "男声"] + + def test_preset_voice_to_dict_no_tags(self): + v = PresetVoice(voice_id="v1", name="t", description="d", gender="female") + d = v.to_dict() + assert d["tags"] == [] + + def test_preset_voice_frozen(self): + v = PresetVoice(voice_id="v1", name="t", description="d", gender="female") + with pytest.raises(Exception): + v.name = "改名" + + def test_preset_voices_list_not_empty(self): + assert len(PRESET_VOICES) > 0 + + def test_get_preset_voices(self): + voices = get_preset_voices() + assert len(voices) == len(PRESET_VOICES) + assert all(isinstance(v, PresetVoice) for v in voices) + + def test_get_preset_voice_by_id_existing(self): + first = PRESET_VOICES[0] + result = get_preset_voice_by_id(first.voice_id) + assert result is not None + assert result.voice_id == first.voice_id + + def test_get_preset_voice_by_id_nonexistent(self): + assert get_preset_voice_by_id("nonexistent_voice") is None + + def test_is_preset_voice_true(self): + first = PRESET_VOICES[0] + assert is_preset_voice(first.voice_id) is True + + def test_is_preset_voice_false(self): + assert is_preset_voice("fake_voice_id") is False + + +# ── TitleLibrary ───────────────────────────────────────────────────────────── + + +class TestTitleLibraryItem: + """TitleLibraryItem 测试""" + + def test_create_basic(self): + item = TitleLibraryItem(id="t1", user_id="u1", name="标题1", text="这是标题文案") + assert item.id == "t1" + assert item.user_id == "u1" + assert item.name == "标题1" + assert item.text == "这是标题文案" + assert item.category == "default" + assert item.tags == [] + assert item.usage_count == 0 + assert item.is_active is True + assert item.created_at is not None + + def test_create_with_details(self): + item = TitleLibraryItem( + id="t1", + user_id="u1", + name="爆款标题", + text="三个方法教你...", + category="爆款", + description="高点击率", + tags=["热门", "干货"], + usage_count=100, + ) + assert item.category == "爆款" + assert item.description == "高点击率" + assert item.tags == ["热门", "干货"] + assert item.usage_count == 100 + + +# ── VoiceLibrary ───────────────────────────────────────────────────────────── + + +class TestVoiceLibraryItem: + """VoiceLibraryItem 测试""" + + def test_create_basic(self): + item = VoiceLibraryItem(id="v1", user_id="u1", name="我的配音") + assert item.id == "v1" + assert item.user_id == "u1" + assert item.name == "我的配音" + assert item.text == "" + assert item.voice_provider == "" + assert item.duration == 0 + assert item.status == "completed" + assert item.project_id is None + assert item.tags == [] + assert item.created_at is not None + + def test_create_with_details(self): + item = VoiceLibraryItem( + id="v1", + user_id="u1", + name="产品介绍", + text="大家好,今天给大家介绍...", + voice_provider="cosyvoice", + voice_id="voice_001", + voice_name="温柔女声", + audio_url="http://cdn/audio.mp3", + duration=30.5, + file_size=102400, + status="processing", + project_id="proj-1", + tags=["产品", "介绍"], + ) + assert item.voice_provider == "cosyvoice" + assert item.voice_id == "voice_001" + assert item.audio_url == "http://cdn/audio.mp3" + assert item.duration == 30.5 + assert item.file_size == 102400 + assert item.status == "processing" + assert item.project_id == "proj-1" + assert item.tags == ["产品", "介绍"] -- 2.54.0