ac01bee33b
Clean up zero-test small domain modules in one batch: - EditingMode enum: 4 tests - TemplateSegment dataclass: 2 tests - ClipType + TransitionEffect enums: 3 tests - TemplateClipConfig create + properties: 11 tests (validation + duration range + transition) - EditTemplateVersion create: 6 tests - VoiceLibraryItem: 2 tests - TitleLibraryItem: 4 tests - RecipeItem: 3 tests Total: 35 tests across 8 modules
392 lines
12 KiB
Python
Executable File
392 lines
12 KiB
Python
Executable File
"""Domain 小模块合集单元测试。
|
|
|
|
覆盖零测试的小 domain 模块:
|
|
- EditingMode 枚举
|
|
- Template / TemplateSegment
|
|
- TemplateClipConfig + ClipType + TransitionEffect
|
|
- EditTemplateVersion
|
|
- VoiceLibraryItem
|
|
- TitleLibraryItem
|
|
- Recipe / RecipeItem
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
|
|
from packages.domain.editing_mode import EditingMode
|
|
from packages.domain.recipe import RecipeItem
|
|
from packages.domain.template import 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
|
|
|
|
|
|
class TestEditingMode:
|
|
def test_all_modes_exist(self):
|
|
assert EditingMode.ONE_TAKE.value == "one_take"
|
|
assert EditingMode.PIP.value == "pip"
|
|
assert EditingMode.VOICE_OVER.value == "voice_over"
|
|
assert EditingMode.VOICE_PIP.value == "voice_pip"
|
|
|
|
def test_from_string(self):
|
|
assert EditingMode("one_take") == EditingMode.ONE_TAKE
|
|
assert EditingMode("voice_over") == EditingMode.VOICE_OVER
|
|
|
|
def test_invalid_mode_raises(self):
|
|
with pytest.raises(ValueError):
|
|
EditingMode("invalid_mode")
|
|
|
|
def test_is_str_enum(self):
|
|
# StrEnum 的值是字符串,可以直接比较
|
|
assert EditingMode.ONE_TAKE == "one_take"
|
|
|
|
|
|
class TestTemplateSegment:
|
|
def test_create_minimal(self):
|
|
seg = TemplateSegment(
|
|
id="seg1",
|
|
template_id="tpl1",
|
|
segment_order=1,
|
|
duration_min=5.0,
|
|
duration_max=10.0,
|
|
)
|
|
assert seg.id == "seg1"
|
|
assert seg.template_id == "tpl1"
|
|
assert seg.segment_order == 1
|
|
assert seg.duration_min == 5.0
|
|
assert seg.duration_max == 10.0
|
|
assert seg.material_type is None
|
|
assert isinstance(seg.created_at, datetime)
|
|
|
|
def test_create_with_material_type(self):
|
|
seg = TemplateSegment(
|
|
id="seg2",
|
|
template_id="tpl1",
|
|
segment_order=2,
|
|
duration_min=3.0,
|
|
duration_max=8.0,
|
|
material_type="人物",
|
|
)
|
|
assert seg.material_type == "人物"
|
|
|
|
|
|
class TestClipType:
|
|
def test_basic_types_exist(self):
|
|
assert hasattr(ClipType, "MAIN")
|
|
assert hasattr(ClipType, "INTRO")
|
|
assert hasattr(ClipType, "OUTRO")
|
|
assert hasattr(ClipType, "TRANSITION")
|
|
|
|
def test_values_are_strings(self):
|
|
for ct in ClipType:
|
|
assert isinstance(ct.value, str)
|
|
|
|
|
|
class TestTransitionEffect:
|
|
def test_effects_exist(self):
|
|
assert TransitionEffect.CUT.value == "cut"
|
|
assert TransitionEffect.FADE.value == "fade"
|
|
assert TransitionEffect.DISSOLVE.value == "dissolve"
|
|
# 至少有 5 种以上转场效果
|
|
assert len(list(TransitionEffect)) >= 5
|
|
|
|
|
|
class TestTemplateClipConfig:
|
|
def test_create_minimal(self):
|
|
config = TemplateClipConfig.create(
|
|
template_id="tpl1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=3.0,
|
|
max_duration=8.0,
|
|
)
|
|
assert config.id is not None
|
|
assert config.template_id == "tpl1"
|
|
assert config.clip_type == ClipType.MAIN
|
|
assert config.order == 1
|
|
assert config.min_duration == 3.0
|
|
assert config.max_duration == 8.0
|
|
|
|
def test_create_with_string_type(self):
|
|
config = TemplateClipConfig.create(
|
|
template_id="tpl1",
|
|
clip_type="intro",
|
|
order=0,
|
|
min_duration=2.0,
|
|
max_duration=5.0,
|
|
)
|
|
assert config.clip_type == ClipType.INTRO
|
|
|
|
def test_has_duration_range_true(self):
|
|
config = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=3.0,
|
|
max_duration=8.0,
|
|
)
|
|
assert config.has_duration_range is True
|
|
|
|
def test_has_duration_range_false_when_both_zero(self):
|
|
config = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
)
|
|
assert config.has_duration_range is False
|
|
|
|
def test_default_duration_midpoint(self):
|
|
config = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=4.0,
|
|
max_duration=6.0,
|
|
)
|
|
assert config.default_duration == pytest.approx(5.0)
|
|
|
|
def test_default_duration_when_only_max(self):
|
|
config = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
max_duration=5.0,
|
|
)
|
|
assert config.default_duration == 5.0
|
|
|
|
def test_create_negative_min_duration_raises(self):
|
|
with pytest.raises(ValueError, match="min_duration"):
|
|
TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=-1.0,
|
|
)
|
|
|
|
def test_create_min_greater_than_max_raises(self):
|
|
with pytest.raises(ValueError, match="min_duration.*max_duration"):
|
|
TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=10.0,
|
|
max_duration=5.0,
|
|
)
|
|
|
|
def test_create_empty_template_id_raises(self):
|
|
with pytest.raises(ValueError, match="template_id"):
|
|
TemplateClipConfig.create(
|
|
template_id="",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
)
|
|
|
|
def test_default_transition_is_cut(self):
|
|
config = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
)
|
|
assert config.transition_effect == TransitionEffect.CUT
|
|
|
|
def test_custom_transition_effect(self):
|
|
config = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
transition_effect="fade",
|
|
)
|
|
assert config.transition_effect == TransitionEffect.FADE
|
|
|
|
|
|
class TestEditTemplateVersion:
|
|
def test_create_minimal(self):
|
|
version = EditTemplateVersion.create(
|
|
template_id="tpl1",
|
|
version=1,
|
|
)
|
|
assert version.id is not None
|
|
assert len(version.id) == 32
|
|
assert version.template_id == "tpl1"
|
|
assert version.version == 1
|
|
assert version.config == {}
|
|
assert version.clip_configs == []
|
|
assert version.published_by == ""
|
|
assert version.change_note == ""
|
|
assert version.name == ""
|
|
assert version.editing_mode == "one_take"
|
|
assert isinstance(version.created_at, datetime)
|
|
|
|
def test_create_with_config_and_clip_configs(self):
|
|
version = EditTemplateVersion.create(
|
|
template_id="tpl1",
|
|
version=2,
|
|
config={"layout": "one_take"},
|
|
clip_configs=[{"clip_id": "c1", "type": "main"}],
|
|
published_by="user1",
|
|
change_note="添加了片头效果",
|
|
)
|
|
assert version.config == {"layout": "one_take"}
|
|
assert len(version.clip_configs) == 1
|
|
assert version.published_by == "user1"
|
|
assert version.change_note == "添加了片头效果"
|
|
|
|
def test_create_with_name_and_mode(self):
|
|
version = EditTemplateVersion.create(
|
|
template_id="t1",
|
|
version=1,
|
|
name="v1.0 正式版",
|
|
editing_mode="voice_over",
|
|
)
|
|
assert version.name == "v1.0 正式版"
|
|
assert version.editing_mode == "voice_over"
|
|
|
|
def test_create_unique_ids(self):
|
|
v1 = EditTemplateVersion.create("t1", 1)
|
|
v2 = EditTemplateVersion.create("t1", 2)
|
|
assert v1.id != v2.id
|
|
|
|
def test_none_config_defaults_to_empty_dict(self):
|
|
version = EditTemplateVersion.create("t1", 1, config=None)
|
|
assert version.config == {}
|
|
|
|
def test_none_clip_configs_defaults_to_empty_list(self):
|
|
version = EditTemplateVersion.create("t1", 1, clip_configs=None)
|
|
assert version.clip_configs == []
|
|
|
|
|
|
class TestVoiceLibraryItem:
|
|
def test_create_minimal(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.tags == []
|
|
assert item.project_id is None
|
|
assert isinstance(item.created_at, datetime)
|
|
|
|
def test_create_with_all_fields(self):
|
|
item = VoiceLibraryItem(
|
|
id="v2",
|
|
user_id="u1",
|
|
name="产品介绍",
|
|
text="欢迎来到我们的产品",
|
|
voice_provider="cosyvoice",
|
|
voice_id="voice_001",
|
|
voice_name="温柔女声",
|
|
audio_url="https://cdn/v2.mp3",
|
|
duration=30.5,
|
|
file_size=102400,
|
|
status="processing",
|
|
project_id="proj1",
|
|
tags=["产品", "介绍"],
|
|
)
|
|
assert item.text == "欢迎来到我们的产品"
|
|
assert item.voice_provider == "cosyvoice"
|
|
assert item.voice_id == "voice_001"
|
|
assert item.audio_url == "https://cdn/v2.mp3"
|
|
assert item.duration == 30.5
|
|
assert item.file_size == 102400
|
|
assert item.status == "processing"
|
|
assert item.project_id == "proj1"
|
|
assert item.tags == ["产品", "介绍"]
|
|
|
|
|
|
class TestTitleLibraryItem:
|
|
def test_create_minimal(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.description == ""
|
|
assert item.tags == []
|
|
assert item.usage_count == 0
|
|
assert item.is_active is True
|
|
|
|
def test_create_with_category(self):
|
|
item = TitleLibraryItem(
|
|
id="t2",
|
|
user_id="u1",
|
|
name="美食标题",
|
|
text="太好吃了!",
|
|
category="美食",
|
|
)
|
|
assert item.category == "美食"
|
|
|
|
def test_inactive_item(self):
|
|
item = TitleLibraryItem(
|
|
id="t3",
|
|
user_id="u1",
|
|
name="旧标题",
|
|
text="旧文案",
|
|
is_active=False,
|
|
)
|
|
assert item.is_active is False
|
|
|
|
def test_usage_count_increment(self):
|
|
item = TitleLibraryItem(
|
|
id="t4",
|
|
user_id="u1",
|
|
name="T",
|
|
text="T",
|
|
)
|
|
item.usage_count += 1
|
|
assert item.usage_count == 1
|
|
|
|
|
|
class TestRecipeItem:
|
|
def test_create_minimal(self):
|
|
item = RecipeItem(
|
|
id="ri1",
|
|
recipe_id="r1",
|
|
item_type="asset",
|
|
item_id="asset_001",
|
|
)
|
|
assert item.id == "ri1"
|
|
assert item.recipe_id == "r1"
|
|
assert item.item_type == "asset"
|
|
assert item.item_id == "asset_001"
|
|
assert item.position == 0
|
|
assert item.metadata_ == {}
|
|
|
|
def test_create_with_position_and_metadata(self):
|
|
item = RecipeItem(
|
|
id="ri2",
|
|
recipe_id="r1",
|
|
item_type="title",
|
|
item_id="title_001",
|
|
position=2,
|
|
metadata_={"style": "bold"},
|
|
)
|
|
assert item.position == 2
|
|
assert item.metadata_ == {"style": "bold"}
|
|
|
|
def test_item_types_variety(self):
|
|
asset_item = RecipeItem(id="a", recipe_id="r", item_type="asset", item_id="i1")
|
|
title_item = RecipeItem(id="t", recipe_id="r", item_type="title", item_id="i2")
|
|
voice_item = RecipeItem(id="v", recipe_id="r", item_type="voice", item_id="i3")
|
|
assert asset_item.item_type == "asset"
|
|
assert title_item.item_type == "title"
|
|
assert voice_item.item_type == "voice"
|