88ca8b4406
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
779 lines
28 KiB
Python
Executable File
779 lines
28 KiB
Python
Executable File
"""template_clip_converter 模板片段转换器单元测试."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from packages.domain.template_clip_config import (
|
|
ClipType,
|
|
TemplateClipConfig,
|
|
TransitionEffect,
|
|
)
|
|
from packages.domain.template_clip_converter import (
|
|
_enum_value,
|
|
clip_config_to_snapshot,
|
|
clip_configs_to_snapshots,
|
|
clip_to_template_clip_config,
|
|
clips_to_template_clip_configs,
|
|
filter_clip_config,
|
|
filter_plan_config_to_template,
|
|
safe_parse_clip_type,
|
|
safe_parse_transition_effect,
|
|
snapshot_to_template_clip_config,
|
|
snapshots_to_template_clip_configs,
|
|
validate_template_name,
|
|
)
|
|
|
|
# ── 安全枚举解析测试 ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestSafeParseTransitionEffect:
|
|
"""safe_parse_transition_effect 测试."""
|
|
|
|
def test_enum_passthrough(self):
|
|
"""传入枚举直接返回."""
|
|
result = safe_parse_transition_effect(TransitionEffect.FADE)
|
|
assert result == TransitionEffect.FADE
|
|
assert isinstance(result, TransitionEffect)
|
|
|
|
def test_valid_string(self):
|
|
"""有效字符串解析."""
|
|
assert safe_parse_transition_effect("fade") == TransitionEffect.FADE
|
|
assert safe_parse_transition_effect("cut") == TransitionEffect.CUT
|
|
assert safe_parse_transition_effect("dissolve") == TransitionEffect.DISSOLVE
|
|
|
|
def test_invalid_string_returns_default(self):
|
|
"""无效字符串返回默认值."""
|
|
result = safe_parse_transition_effect("invalid_effect")
|
|
assert result == TransitionEffect.CUT # 默认CUT
|
|
|
|
def test_none_returns_default(self):
|
|
"""None返回默认值."""
|
|
result = safe_parse_transition_effect(None)
|
|
assert result == TransitionEffect.CUT
|
|
|
|
def test_custom_default(self):
|
|
"""自定义默认值."""
|
|
result = safe_parse_transition_effect("bad", default=TransitionEffect.FADE)
|
|
assert result == TransitionEffect.FADE
|
|
|
|
def test_int_returns_default(self):
|
|
"""数字类型返回默认."""
|
|
result = safe_parse_transition_effect(123)
|
|
assert result == TransitionEffect.CUT
|
|
|
|
def test_all_valid_strings(self):
|
|
"""所有有效字符串都能解析."""
|
|
for te in TransitionEffect:
|
|
result = safe_parse_transition_effect(te.value)
|
|
assert result == te
|
|
|
|
|
|
class TestSafeParseClipType:
|
|
"""safe_parse_clip_type 测试."""
|
|
|
|
def test_enum_passthrough(self):
|
|
"""传入枚举直接返回."""
|
|
result = safe_parse_clip_type(ClipType.INTRO)
|
|
assert result == ClipType.INTRO
|
|
assert isinstance(result, ClipType)
|
|
|
|
def test_valid_string(self):
|
|
"""有效字符串解析."""
|
|
assert safe_parse_clip_type("intro") == ClipType.INTRO
|
|
assert safe_parse_clip_type("main") == ClipType.MAIN
|
|
assert safe_parse_clip_type("outro") == ClipType.OUTRO
|
|
assert safe_parse_clip_type("title") == ClipType.TITLE
|
|
|
|
def test_invalid_string_returns_default(self):
|
|
"""无效字符串返回默认值."""
|
|
result = safe_parse_clip_type("invalid_type")
|
|
assert result == ClipType.MAIN # 默认MAIN
|
|
|
|
def test_none_returns_default(self):
|
|
"""None返回默认值."""
|
|
result = safe_parse_clip_type(None)
|
|
assert result == ClipType.MAIN
|
|
|
|
def test_custom_default(self):
|
|
"""自定义默认值."""
|
|
result = safe_parse_clip_type("bad", default=ClipType.TRANSITION)
|
|
assert result == ClipType.TRANSITION
|
|
|
|
def test_all_valid_strings(self):
|
|
"""所有有效字符串都能解析."""
|
|
for ct in ClipType:
|
|
result = safe_parse_clip_type(ct.value)
|
|
assert result == ct
|
|
|
|
|
|
# ── _enum_value 内部函数测试 ─────────────────────────────────────────────────
|
|
|
|
|
|
class TestEnumValue:
|
|
"""_enum_value 内部工具函数测试."""
|
|
|
|
def test_enum_value(self):
|
|
"""枚举返回value."""
|
|
assert _enum_value(TransitionEffect.FADE) == "fade"
|
|
assert _enum_value(ClipType.MAIN) == "main"
|
|
|
|
def test_string_passthrough(self):
|
|
"""字符串直接返回."""
|
|
assert _enum_value("hello") == "hello"
|
|
|
|
def test_int_passthrough(self):
|
|
"""数字直接返回."""
|
|
assert _enum_value(42) == 42
|
|
|
|
def test_none_passthrough(self):
|
|
"""None直接返回."""
|
|
assert _enum_value(None) is None
|
|
|
|
|
|
# ── filter_clip_config 测试 ──────────────────────────────────────────────────
|
|
|
|
|
|
class TestFilterClipConfig:
|
|
"""filter_clip_config 测试."""
|
|
|
|
def test_none_config_no_speed(self):
|
|
"""None config + 无playback_speed."""
|
|
result = filter_clip_config(None)
|
|
assert result == {}
|
|
|
|
def test_none_config_with_speed(self):
|
|
"""None config + 有playback_speed."""
|
|
result = filter_clip_config(None, playback_speed=1.5)
|
|
assert result == {"playback_speed": 1.5}
|
|
|
|
def test_speed_equals_one_not_included(self):
|
|
"""playback_speed=1.0不写入."""
|
|
result = filter_clip_config({"key": "val"}, playback_speed=1.0)
|
|
assert "playback_speed" not in result
|
|
assert result["key"] == "val"
|
|
|
|
def test_speed_not_one_included(self):
|
|
"""playback_speed≠1.0写入."""
|
|
result = filter_clip_config({"key": "val"}, playback_speed=0.8)
|
|
assert result["playback_speed"] == 0.8
|
|
assert result["key"] == "val"
|
|
|
|
def test_speed_none_not_included(self):
|
|
"""playback_speed=None不写入."""
|
|
result = filter_clip_config({"key": "val"}, playback_speed=None)
|
|
assert "playback_speed" not in result
|
|
|
|
def test_removes_asset_info(self):
|
|
"""移除asset_info字段."""
|
|
config = {"a": 1, "asset_info": {"name": "x"}, "b": 2}
|
|
result = filter_clip_config(config)
|
|
assert "asset_info" not in result
|
|
assert result["a"] == 1
|
|
assert result["b"] == 2
|
|
|
|
def test_removes_source_asset_id(self):
|
|
"""移除source_asset_id字段."""
|
|
config = {"source_asset_id": "abc", "other": "val"}
|
|
result = filter_clip_config(config)
|
|
assert "source_asset_id" not in result
|
|
assert result["other"] == "val"
|
|
|
|
def test_custom_skip_keys(self):
|
|
"""自定义跳过字段."""
|
|
config = {"keep": "yes", "skip_me": "no", "also_skip": "no"}
|
|
skip = frozenset({"skip_me", "also_skip"})
|
|
result = filter_clip_config(config, skip_keys=skip)
|
|
assert "keep" in result
|
|
assert "skip_me" not in result
|
|
assert "also_skip" not in result
|
|
|
|
def test_empty_config(self):
|
|
"""空dict config."""
|
|
result = filter_clip_config({})
|
|
assert result == {}
|
|
|
|
def test_does_not_mutate_original(self):
|
|
"""不修改原始config."""
|
|
original = {"a": 1, "asset_info": "x"}
|
|
original_copy = dict(original)
|
|
filter_clip_config(original)
|
|
assert original == original_copy
|
|
|
|
def test_speed_overrides_config(self):
|
|
"""playback_speed 覆盖 clip_config 中的值."""
|
|
result = filter_clip_config({"playback_speed": 2.0}, playback_speed=0.5)
|
|
# clip_config是后面合并的,所以会覆盖前面的playback_speed
|
|
assert result["playback_speed"] == 2.0
|
|
|
|
|
|
# ── filter_plan_config_to_template 测试 ──────────────────────────────────────
|
|
|
|
|
|
class TestFilterPlanConfigToTemplate:
|
|
"""filter_plan_config_to_template 测试."""
|
|
|
|
def test_none_config(self):
|
|
"""None config返回空dict."""
|
|
result = filter_plan_config_to_template(None)
|
|
assert result == {}
|
|
|
|
def test_empty_config(self):
|
|
"""空dict返回空."""
|
|
result = filter_plan_config_to_template({})
|
|
assert result == {}
|
|
|
|
def test_removes_runtime_fields(self):
|
|
"""移除运行时字段."""
|
|
config = {
|
|
"keep": "yes",
|
|
"is_template_draft": True,
|
|
"asset_ids": ["a", "b"],
|
|
"source_edit_plan_id": "plan_123",
|
|
"generation_task_id": "task_456",
|
|
"another_field": "also_keep",
|
|
}
|
|
result = filter_plan_config_to_template(config)
|
|
assert "keep" in result
|
|
assert "another_field" in result
|
|
assert "is_template_draft" not in result
|
|
assert "asset_ids" not in result
|
|
assert "source_edit_plan_id" not in result
|
|
assert "generation_task_id" not in result
|
|
|
|
def test_custom_skip_keys(self):
|
|
"""自定义跳过字段."""
|
|
config = {"keep": "yes", "skip_a": "no", "skip_b": "no"}
|
|
skip = frozenset({"skip_a", "skip_b"})
|
|
result = filter_plan_config_to_template(config, skip_keys=skip)
|
|
assert "keep" in result
|
|
assert "skip_a" not in result
|
|
assert "skip_b" not in result
|
|
|
|
def test_preserves_values(self):
|
|
"""保留字段的值不变."""
|
|
config = {"title": "测试", "duration": 10.5, "items": [1, 2, 3]}
|
|
result = filter_plan_config_to_template(config)
|
|
assert result["title"] == "测试"
|
|
assert result["duration"] == 10.5
|
|
assert result["items"] == [1, 2, 3]
|
|
|
|
|
|
# ── clip_to_template_clip_config 测试 ────────────────────────────────────────
|
|
|
|
|
|
class TestClipToTemplateClipConfig:
|
|
"""clip_to_template_clip_config 测试."""
|
|
|
|
def _make_clip(self, **kwargs):
|
|
"""创建模拟clip对象."""
|
|
defaults = {
|
|
"clip_type": "main",
|
|
"order": 1,
|
|
"duration": 5.0,
|
|
"text_content": "默认文案",
|
|
"transition_effect": "cut",
|
|
"playback_speed": 1.0,
|
|
"config": {},
|
|
}
|
|
defaults.update(kwargs)
|
|
return SimpleNamespace(**defaults)
|
|
|
|
def test_basic_conversion(self):
|
|
"""基础转换."""
|
|
clip = self._make_clip(order=2, duration=3.5)
|
|
result = clip_to_template_clip_config("tpl_001", clip)
|
|
assert isinstance(result, TemplateClipConfig)
|
|
assert result.template_id == "tpl_001"
|
|
assert result.clip_type == ClipType.MAIN
|
|
assert result.order == 2
|
|
assert result.min_duration == 3.5
|
|
assert result.max_duration == 3.5
|
|
assert result.text_template == "默认文案"
|
|
|
|
def test_intro_clip_type(self):
|
|
"""intro类型转换."""
|
|
clip = self._make_clip(clip_type="intro", order=0)
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.clip_type == ClipType.INTRO
|
|
|
|
def test_transition_effect(self):
|
|
"""转场效果转换."""
|
|
clip = self._make_clip(transition_effect="fade")
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.transition_effect == TransitionEffect.FADE
|
|
|
|
def test_fixed_duration(self):
|
|
"""固定时长:min=max=duration."""
|
|
clip = self._make_clip(duration=7.2)
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.min_duration == 7.2
|
|
assert result.max_duration == 7.2
|
|
assert result.min_duration == result.max_duration
|
|
|
|
def test_zero_duration(self):
|
|
"""duration为0."""
|
|
clip = self._make_clip(duration=0)
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.min_duration == 0.0
|
|
assert result.max_duration == 0.0
|
|
|
|
def test_none_duration(self):
|
|
"""duration为None时默认0."""
|
|
clip = self._make_clip(duration=None)
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.min_duration == 0.0
|
|
|
|
def test_text_content_mapped_to_text_template(self):
|
|
"""text_content → text_template."""
|
|
clip = self._make_clip(text_content="你好世界")
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.text_template == "你好世界"
|
|
|
|
def test_none_text_content(self):
|
|
"""text_content为None时默认空字符串."""
|
|
clip = self._make_clip(text_content=None)
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.text_template == ""
|
|
|
|
def test_playback_speed_in_config(self):
|
|
"""非1.0的playback_speed存入config."""
|
|
clip = self._make_clip(playback_speed=1.5)
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.config["playback_speed"] == 1.5
|
|
|
|
def test_playback_speed_one_not_in_config(self):
|
|
"""playback_speed=1.0不存入config."""
|
|
clip = self._make_clip(playback_speed=1.0)
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert "playback_speed" not in result.config
|
|
|
|
def test_config_passed_through(self):
|
|
"""clip.config传递到结果."""
|
|
clip = self._make_clip(config={"font_size": 24, "color": "red"})
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.config["font_size"] == 24
|
|
assert result.config["color"] == "red"
|
|
|
|
def test_config_filters_asset_fields(self):
|
|
"""config中过滤素材字段."""
|
|
clip = self._make_clip(config={"asset_info": {"x": 1}, "keep": "yes"})
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert "asset_info" not in result.config
|
|
assert result.config["keep"] == "yes"
|
|
|
|
def test_invalid_clip_type_defaults_to_main(self):
|
|
"""无效clip_type安全解析为main."""
|
|
clip = self._make_clip(clip_type="unknown_type")
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.clip_type == ClipType.MAIN
|
|
|
|
def test_clip_without_attributes(self):
|
|
"""缺少属性的对象使用默认值."""
|
|
|
|
@dataclass
|
|
class MinimalClip:
|
|
order: int = 0
|
|
|
|
clip = MinimalClip(order=5)
|
|
result = clip_to_template_clip_config("t1", clip)
|
|
assert result.clip_type == ClipType.MAIN # 默认
|
|
assert result.order == 5
|
|
assert result.text_template == ""
|
|
assert result.transition_effect == TransitionEffect.CUT
|
|
|
|
|
|
class TestClipsToTemplateClipConfigs:
|
|
"""clips_to_template_clip_configs 批量转换测试."""
|
|
|
|
def test_empty_list(self):
|
|
"""空列表返回空列表."""
|
|
result = clips_to_template_clip_configs("t1", [])
|
|
assert result == []
|
|
|
|
def test_multiple_clips(self):
|
|
"""多个片段转换."""
|
|
clips = [
|
|
SimpleNamespace(
|
|
clip_type="intro",
|
|
order=0,
|
|
duration=2.0,
|
|
text_content="",
|
|
transition_effect="fade",
|
|
playback_speed=1.0,
|
|
config={},
|
|
),
|
|
SimpleNamespace(
|
|
clip_type="main",
|
|
order=1,
|
|
duration=5.0,
|
|
text_content="主体",
|
|
transition_effect="cut",
|
|
playback_speed=1.0,
|
|
config={},
|
|
),
|
|
SimpleNamespace(
|
|
clip_type="outro",
|
|
order=2,
|
|
duration=3.0,
|
|
text_content="结束",
|
|
transition_effect="fade",
|
|
playback_speed=1.0,
|
|
config={},
|
|
),
|
|
]
|
|
result = clips_to_template_clip_configs("tpl_multi", clips)
|
|
assert len(result) == 3
|
|
assert result[0].clip_type == ClipType.INTRO
|
|
assert result[0].order == 0
|
|
assert result[1].clip_type == ClipType.MAIN
|
|
assert result[1].order == 1
|
|
assert result[2].clip_type == ClipType.OUTRO
|
|
assert result[2].order == 2
|
|
assert all(r.template_id == "tpl_multi" for r in result)
|
|
|
|
|
|
# ── clip_config_to_snapshot 测试 ────────────────────────────────────────────
|
|
|
|
|
|
class TestClipConfigToSnapshot:
|
|
"""clip_config_to_snapshot 测试."""
|
|
|
|
def test_basic_snapshot(self):
|
|
"""基础快照转换."""
|
|
cfg = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type="main",
|
|
order=1,
|
|
min_duration=3.0,
|
|
max_duration=5.0,
|
|
text_template="hello",
|
|
transition_effect="fade",
|
|
config={"key": "val"},
|
|
)
|
|
snap = clip_config_to_snapshot(cfg)
|
|
assert snap["clip_type"] == "main"
|
|
assert snap["order"] == 1
|
|
assert snap["min_duration"] == 3.0
|
|
assert snap["max_duration"] == 5.0
|
|
assert snap["text_template"] == "hello"
|
|
assert snap["transition_effect"] == "fade"
|
|
assert snap["config"] == {"key": "val"}
|
|
|
|
def test_enum_values_in_snapshot(self):
|
|
"""枚举转为字符串value."""
|
|
cfg = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.TITLE,
|
|
order=0,
|
|
transition_effect=TransitionEffect.DISSOLVE,
|
|
)
|
|
snap = clip_config_to_snapshot(cfg)
|
|
assert snap["clip_type"] == "title"
|
|
assert isinstance(snap["clip_type"], str)
|
|
assert snap["transition_effect"] == "dissolve"
|
|
assert isinstance(snap["transition_effect"], str)
|
|
|
|
def test_empty_text_template(self):
|
|
"""空文案模板."""
|
|
cfg = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type="main",
|
|
order=0,
|
|
text_template="",
|
|
)
|
|
snap = clip_config_to_snapshot(cfg)
|
|
assert snap["text_template"] == ""
|
|
|
|
def test_none_text_template_becomes_empty(self):
|
|
"""text_template为None时返回空串."""
|
|
# TemplateClipConfig的text_template默认是空串,模拟一个有None属性的对象
|
|
obj = SimpleNamespace(
|
|
clip_type=ClipType.MAIN,
|
|
order=0,
|
|
min_duration=1.0,
|
|
max_duration=2.0,
|
|
text_template=None,
|
|
transition_effect=TransitionEffect.CUT,
|
|
config={},
|
|
)
|
|
snap = clip_config_to_snapshot(obj)
|
|
assert snap["text_template"] == ""
|
|
|
|
def test_config_is_copy(self):
|
|
"""config是副本,不是原对象引用."""
|
|
original_config = {"a": 1}
|
|
cfg = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type="main",
|
|
order=0,
|
|
config=original_config,
|
|
)
|
|
snap = clip_config_to_snapshot(cfg)
|
|
snap["config"]["b"] = 2
|
|
assert "b" not in cfg.config
|
|
assert original_config.get("b") is None
|
|
|
|
def test_none_config_becomes_empty(self):
|
|
"""config为None时返回空dict."""
|
|
obj = SimpleNamespace(
|
|
clip_type="main",
|
|
order=0,
|
|
min_duration=0,
|
|
max_duration=0,
|
|
text_template="",
|
|
transition_effect="cut",
|
|
config=None,
|
|
)
|
|
snap = clip_config_to_snapshot(obj)
|
|
assert snap["config"] == {}
|
|
|
|
def test_default_values_for_missing_attrs(self):
|
|
"""缺少属性时用默认值."""
|
|
obj = SimpleNamespace()
|
|
snap = clip_config_to_snapshot(obj)
|
|
assert snap["clip_type"] is None # _enum_value(None)=None
|
|
assert snap["order"] == 0
|
|
assert snap["min_duration"] == 0.0
|
|
assert snap["max_duration"] == 0.0
|
|
assert snap["text_template"] == ""
|
|
assert snap["transition_effect"] is None
|
|
assert snap["config"] == {}
|
|
|
|
def test_snapshot_keys(self):
|
|
"""快照包含所有预期字段."""
|
|
cfg = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type="main",
|
|
order=0,
|
|
)
|
|
snap = clip_config_to_snapshot(cfg)
|
|
expected_keys = {
|
|
"clip_type",
|
|
"order",
|
|
"min_duration",
|
|
"max_duration",
|
|
"text_template",
|
|
"transition_effect",
|
|
"config",
|
|
}
|
|
assert set(snap.keys()) == expected_keys
|
|
|
|
|
|
class TestClipConfigsToSnapshots:
|
|
"""clip_configs_to_snapshots 批量测试."""
|
|
|
|
def test_empty_list(self):
|
|
"""空列表返回空列表."""
|
|
assert clip_configs_to_snapshots([]) == []
|
|
|
|
def test_multiple_configs(self):
|
|
"""多个配置转换."""
|
|
configs = [
|
|
TemplateClipConfig.create("t1", "intro", 0, min_duration=2.0, max_duration=2.0),
|
|
TemplateClipConfig.create("t1", "main", 1, min_duration=3.0, max_duration=5.0),
|
|
TemplateClipConfig.create("t1", "outro", 2, min_duration=1.0, max_duration=1.0),
|
|
]
|
|
snaps = clip_configs_to_snapshots(configs)
|
|
assert len(snaps) == 3
|
|
assert snaps[0]["clip_type"] == "intro"
|
|
assert snaps[1]["clip_type"] == "main"
|
|
assert snaps[2]["clip_type"] == "outro"
|
|
|
|
|
|
# ── snapshot_to_template_clip_config 测试 ────────────────────────────────────
|
|
|
|
|
|
class TestSnapshotToTemplateClipConfig:
|
|
"""snapshot_to_template_clip_config 测试."""
|
|
|
|
def test_basic_conversion(self):
|
|
"""基础快照→配置转换."""
|
|
snap = {
|
|
"clip_type": "main",
|
|
"order": 2,
|
|
"min_duration": 3.0,
|
|
"max_duration": 5.0,
|
|
"text_template": "hello",
|
|
"transition_effect": "fade",
|
|
"config": {"key": "val"},
|
|
}
|
|
result = snapshot_to_template_clip_config("tpl_new", snap)
|
|
assert isinstance(result, TemplateClipConfig)
|
|
assert result.template_id == "tpl_new"
|
|
assert result.clip_type == ClipType.MAIN
|
|
assert result.order == 2
|
|
assert result.min_duration == 3.0
|
|
assert result.max_duration == 5.0
|
|
assert result.text_template == "hello"
|
|
assert result.transition_effect == TransitionEffect.FADE
|
|
assert result.config == {"key": "val"}
|
|
|
|
def test_defaults_for_missing_keys(self):
|
|
"""缺失字段使用默认值."""
|
|
snap: dict = {}
|
|
result = snapshot_to_template_clip_config("t1", snap)
|
|
assert result.clip_type == ClipType.MAIN # 默认main
|
|
assert result.order == 0
|
|
assert result.min_duration == 0.0
|
|
assert result.max_duration == 0.0
|
|
assert result.text_template == ""
|
|
assert result.transition_effect == TransitionEffect.CUT # 默认cut
|
|
assert result.config == {}
|
|
|
|
def test_intro_type(self):
|
|
"""intro类型解析."""
|
|
snap = {"clip_type": "intro", "order": 0}
|
|
result = snapshot_to_template_clip_config("t1", snap)
|
|
assert result.clip_type == ClipType.INTRO
|
|
|
|
def test_invalid_clip_type_defaults(self):
|
|
"""无效clip_type安全回退到main."""
|
|
snap = {"clip_type": "weird_type", "order": 0}
|
|
result = snapshot_to_template_clip_config("t1", snap)
|
|
assert result.clip_type == ClipType.MAIN
|
|
|
|
def test_invalid_transition_defaults(self):
|
|
"""无效transition_effect安全回退到cut."""
|
|
snap = {"transition_effect": "invalid_fx"}
|
|
result = snapshot_to_template_clip_config("t1", snap)
|
|
assert result.transition_effect == TransitionEffect.CUT
|
|
|
|
def test_none_config_becomes_empty(self):
|
|
"""config为None时是空dict."""
|
|
snap = {"config": None}
|
|
result = snapshot_to_template_clip_config("t1", snap)
|
|
assert result.config == {}
|
|
|
|
def test_config_is_copy(self):
|
|
"""config是副本."""
|
|
original_config = {"a": 1}
|
|
snap = {"config": original_config}
|
|
result = snapshot_to_template_clip_config("t1", snap)
|
|
result.config["b"] = 2
|
|
assert "b" not in original_config
|
|
|
|
|
|
class TestSnapshotsToTemplateClipConfigs:
|
|
"""snapshots_to_template_clip_configs 批量测试."""
|
|
|
|
def test_empty_list(self):
|
|
"""空列表返回空列表."""
|
|
result = snapshots_to_template_clip_configs("t1", [])
|
|
assert result == []
|
|
|
|
def test_multiple_snapshots(self):
|
|
"""多个快照转换."""
|
|
snaps = [
|
|
{"clip_type": "intro", "order": 0, "min_duration": 2.0, "max_duration": 2.0},
|
|
{"clip_type": "main", "order": 1, "min_duration": 5.0, "max_duration": 5.0},
|
|
{"clip_type": "outro", "order": 2, "min_duration": 3.0, "max_duration": 3.0},
|
|
]
|
|
result = snapshots_to_template_clip_configs("tpl_multi", snaps)
|
|
assert len(result) == 3
|
|
assert result[0].clip_type == ClipType.INTRO
|
|
assert result[1].clip_type == ClipType.MAIN
|
|
assert result[2].clip_type == ClipType.OUTRO
|
|
assert all(r.template_id == "tpl_multi" for r in result)
|
|
|
|
|
|
# ── validate_template_name 测试 ──────────────────────────────────────────────
|
|
|
|
|
|
class TestValidateTemplateName:
|
|
"""validate_template_name 测试."""
|
|
|
|
def test_normal_name(self):
|
|
"""正常名称."""
|
|
assert validate_template_name("我的模板") == "我的模板"
|
|
|
|
def test_strips_whitespace(self):
|
|
"""去除首尾空格."""
|
|
assert validate_template_name(" 测试模板 ") == "测试模板"
|
|
|
|
def test_strips_newlines(self):
|
|
"""去除换行."""
|
|
assert validate_template_name("\n模板名\n") == "模板名"
|
|
|
|
def test_empty_string_raises(self):
|
|
"""空字符串抛错."""
|
|
with pytest.raises(ValueError, match="不能为空"):
|
|
validate_template_name("")
|
|
|
|
def test_none_raises(self):
|
|
"""None抛错."""
|
|
with pytest.raises(ValueError, match="不能为空"):
|
|
validate_template_name(None)
|
|
|
|
def test_whitespace_only_raises(self):
|
|
"""纯空白抛错."""
|
|
with pytest.raises(ValueError, match="不能为空"):
|
|
validate_template_name(" ")
|
|
|
|
def test_newline_only_raises(self):
|
|
"""纯换行抛错."""
|
|
with pytest.raises(ValueError, match="不能为空"):
|
|
validate_template_name("\n\t\r")
|
|
|
|
def test_single_char_name(self):
|
|
"""单字符名称有效."""
|
|
assert validate_template_name("A") == "A"
|
|
|
|
def test_long_name_preserved(self):
|
|
"""长名称保留(长度限制由调用方控制)."""
|
|
long_name = "A" * 200
|
|
assert validate_template_name(long_name) == long_name
|
|
|
|
|
|
# ── 往返转换测试 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestRoundTripConversions:
|
|
"""往返转换一致性测试."""
|
|
|
|
def test_snapshot_round_trip(self):
|
|
"""snapshot → config → snapshot 数据一致."""
|
|
original = {
|
|
"clip_type": "main",
|
|
"order": 5,
|
|
"min_duration": 2.5,
|
|
"max_duration": 7.5,
|
|
"text_template": "测试文案",
|
|
"transition_effect": "dissolve",
|
|
"config": {"speed": 1.2, "theme": "dark"},
|
|
}
|
|
cfg = snapshot_to_template_clip_config("t1", original)
|
|
snap = clip_config_to_snapshot(cfg)
|
|
assert snap["clip_type"] == original["clip_type"]
|
|
assert snap["order"] == original["order"]
|
|
assert snap["min_duration"] == original["min_duration"]
|
|
assert snap["max_duration"] == original["max_duration"]
|
|
assert snap["text_template"] == original["text_template"]
|
|
assert snap["transition_effect"] == original["transition_effect"]
|
|
assert snap["config"] == original["config"]
|
|
|
|
def test_config_snapshot_round_trip(self):
|
|
"""config → snapshot → config 数据一致."""
|
|
cfg = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type="title",
|
|
order=0,
|
|
min_duration=3.0,
|
|
max_duration=3.0,
|
|
text_template="标题",
|
|
transition_effect="wipe",
|
|
config={"font_size": 32},
|
|
)
|
|
snap = clip_config_to_snapshot(cfg)
|
|
restored = snapshot_to_template_clip_config("t1", snap)
|
|
assert restored.clip_type == cfg.clip_type
|
|
assert restored.order == cfg.order
|
|
assert restored.min_duration == cfg.min_duration
|
|
assert restored.max_duration == cfg.max_duration
|
|
assert restored.text_template == cfg.text_template
|
|
assert restored.transition_effect == cfg.transition_effect
|
|
assert restored.config == cfg.config
|