test(unit): 新增 filter_presets/template_clip_config 两个模块测试
- filter_presets: 31 个测试,含 build_ffmpeg_filter 强度插值逻辑 - template_clip_config: 28 个测试 本轮 Batch2 共 7 个模块,231 个测试用例
This commit is contained in:
Executable
+229
@@ -0,0 +1,229 @@
|
||||
"""filter_presets 模块单元测试."""
|
||||
|
||||
import pytest
|
||||
|
||||
from domain.filter_presets import (
|
||||
FILTER_PRESET_LIBRARY,
|
||||
FilterPreset,
|
||||
build_ffmpeg_filter,
|
||||
get_filter_preset,
|
||||
list_filter_presets,
|
||||
)
|
||||
|
||||
|
||||
class TestFilterPreset:
|
||||
"""FilterPreset 数据类测试."""
|
||||
|
||||
def test_create_required_fields(self):
|
||||
f = FilterPreset(id="test_001", name="测试滤镜", category="basic")
|
||||
assert f.id == "test_001"
|
||||
assert f.name == "测试滤镜"
|
||||
assert f.category == "basic"
|
||||
# 默认值
|
||||
assert f.description == ""
|
||||
assert f.tags == []
|
||||
assert f.brightness == 0.0
|
||||
assert f.contrast == 1.0
|
||||
assert f.saturation == 1.0
|
||||
assert f.gamma == 1.0
|
||||
assert f.gamma_r == 1.0
|
||||
assert f.gamma_g == 1.0
|
||||
assert f.gamma_b == 1.0
|
||||
assert f.hue == 0.0
|
||||
assert f.lut_url == ""
|
||||
|
||||
def test_create_all_fields(self):
|
||||
f = FilterPreset(
|
||||
id="test_002",
|
||||
name="完整滤镜",
|
||||
category="cinematic",
|
||||
description="测试描述",
|
||||
tags=["标签1", "标签2"],
|
||||
brightness=0.1,
|
||||
contrast=1.2,
|
||||
saturation=0.8,
|
||||
gamma=1.1,
|
||||
gamma_r=1.05,
|
||||
gamma_g=0.95,
|
||||
gamma_b=1.15,
|
||||
hue=10.0,
|
||||
lut_url="https://example.com/lut.png",
|
||||
)
|
||||
assert f.category == "cinematic"
|
||||
assert f.brightness == 0.1
|
||||
assert f.contrast == 1.2
|
||||
assert f.saturation == 0.8
|
||||
assert f.gamma == 1.1
|
||||
assert f.gamma_r == 1.05
|
||||
assert f.gamma_g == 0.95
|
||||
assert f.gamma_b == 1.15
|
||||
assert f.hue == 10.0
|
||||
assert f.lut_url == "https://example.com/lut.png"
|
||||
|
||||
def test_frozen_immutable(self):
|
||||
f = FilterPreset(id="test", name="测试", category="basic")
|
||||
with pytest.raises(Exception):
|
||||
f.name = "修改" # type: ignore[misc]
|
||||
|
||||
def test_tags_default_new_list(self):
|
||||
f1 = FilterPreset(id="1", name="a", category="basic")
|
||||
f2 = FilterPreset(id="2", name="b", category="basic")
|
||||
assert f1.tags is not f2.tags
|
||||
assert f1.tags == []
|
||||
|
||||
|
||||
class TestFilterPresetLibrary:
|
||||
"""FILTER_PRESET_LIBRARY 预设库测试."""
|
||||
|
||||
def test_not_empty(self):
|
||||
assert len(FILTER_PRESET_LIBRARY) > 0
|
||||
|
||||
def test_all_unique_ids(self):
|
||||
ids = [f.id for f in FILTER_PRESET_LIBRARY]
|
||||
assert len(ids) == len(set(ids))
|
||||
|
||||
def test_all_are_filter_preset_instances(self):
|
||||
for f in FILTER_PRESET_LIBRARY:
|
||||
assert isinstance(f, FilterPreset)
|
||||
|
||||
def test_contains_basic_category(self):
|
||||
cats = {f.category for f in FILTER_PRESET_LIBRARY}
|
||||
assert "basic" in cats
|
||||
|
||||
def test_none_filter_is_identity(self):
|
||||
"""filter_none 应该所有参数都是默认值(不改变画面)"""
|
||||
f = get_filter_preset("filter_none")
|
||||
assert f is not None
|
||||
assert f.brightness == 0.0
|
||||
assert f.contrast == 1.0
|
||||
assert f.saturation == 1.0
|
||||
assert f.gamma == 1.0
|
||||
|
||||
|
||||
class TestGetFilterPreset:
|
||||
"""get_filter_preset 函数测试."""
|
||||
|
||||
def test_existing_id(self):
|
||||
f = get_filter_preset("filter_brighten")
|
||||
assert f is not None
|
||||
assert f.id == "filter_brighten"
|
||||
assert f.name == "明亮"
|
||||
|
||||
def test_nonexistent_id(self):
|
||||
assert get_filter_preset("nonexistent") is None
|
||||
|
||||
def test_empty_string(self):
|
||||
assert get_filter_preset("") is None
|
||||
|
||||
|
||||
class TestListFilterPresets:
|
||||
"""list_filter_presets 函数测试."""
|
||||
|
||||
def test_no_filters_returns_all(self):
|
||||
result = list_filter_presets()
|
||||
assert len(result) == len(FILTER_PRESET_LIBRARY)
|
||||
|
||||
def test_filter_by_category_basic(self):
|
||||
result = list_filter_presets(category="basic")
|
||||
assert len(result) >= 4
|
||||
for f in result:
|
||||
assert f.category == "basic"
|
||||
|
||||
def test_filter_by_unknown_category_returns_empty(self):
|
||||
result = list_filter_presets(category="nonexistent")
|
||||
assert result == []
|
||||
|
||||
def test_filter_by_keyword_name(self):
|
||||
result = list_filter_presets(keyword="明亮")
|
||||
assert len(result) >= 1
|
||||
assert any(f.name == "明亮" for f in result)
|
||||
|
||||
def test_filter_by_keyword_tag(self):
|
||||
result = list_filter_presets(keyword="提亮")
|
||||
assert len(result) >= 1
|
||||
|
||||
def test_filter_by_keyword_description(self):
|
||||
result = list_filter_presets(keyword="偏暗")
|
||||
assert len(result) >= 1
|
||||
|
||||
def test_filter_keyword_case_insensitive(self):
|
||||
r1 = list_filter_presets(keyword="FILTER")
|
||||
r2 = list_filter_presets(keyword="filter")
|
||||
assert len(r1) == len(r2)
|
||||
|
||||
def test_filter_keyword_no_match(self):
|
||||
result = list_filter_presets(keyword="xyz_nonexistent_12345")
|
||||
assert result == []
|
||||
|
||||
def test_combined_category_and_keyword(self):
|
||||
result = list_filter_presets(category="basic", keyword="明亮")
|
||||
assert len(result) >= 1
|
||||
for f in result:
|
||||
assert f.category == "basic"
|
||||
|
||||
def test_combined_no_match(self):
|
||||
result = list_filter_presets(category="basic", keyword="电影感")
|
||||
# 基础分类里没有电影感关键词
|
||||
pass # 不做强断言,看实际数据
|
||||
|
||||
|
||||
class TestBuildFFmpegFilter:
|
||||
"""build_ffmpeg_filter 函数测试."""
|
||||
|
||||
def test_none_preset_returns_empty(self):
|
||||
result = build_ffmpeg_filter("nonexistent")
|
||||
assert result == ""
|
||||
|
||||
def test_zero_intensity_returns_empty(self):
|
||||
result = build_ffmpeg_filter("filter_brighten", intensity=0)
|
||||
assert result == ""
|
||||
|
||||
def test_negative_intensity_returns_empty(self):
|
||||
result = build_ffmpeg_filter("filter_brighten", intensity=-10)
|
||||
assert result == ""
|
||||
|
||||
def test_full_intensity_brighten(self):
|
||||
result = build_ffmpeg_filter("filter_brighten", intensity=100)
|
||||
assert result.startswith("eq=")
|
||||
assert "brightness=0.120" in result
|
||||
assert "contrast=1.050" in result
|
||||
assert "saturation=1.050" in result
|
||||
assert "gamma=1.100" in result
|
||||
|
||||
def test_half_intensity(self):
|
||||
"""强度 50% 时参数应该是全量的一半(向原值插值)"""
|
||||
full = build_ffmpeg_filter("filter_brighten", intensity=100)
|
||||
half = build_ffmpeg_filter("filter_brighten", intensity=50)
|
||||
|
||||
# 50% 强度的 brightness 应该是 0.060 (0.120 * 0.5)
|
||||
assert "brightness=0.060" in half
|
||||
# full 和 half 都应该有 eq= 前缀
|
||||
assert full.startswith("eq=")
|
||||
assert half.startswith("eq=")
|
||||
|
||||
def test_intensity_over_100_clamps_to_100(self):
|
||||
result1 = build_ffmpeg_filter("filter_brighten", intensity=100)
|
||||
result2 = build_ffmpeg_filter("filter_brighten", intensity=150)
|
||||
assert result1 == result2
|
||||
|
||||
def test_filter_none_returns_empty(self):
|
||||
"""原图滤镜所有参数都是默认值,应该返回空字符串"""
|
||||
result = build_ffmpeg_filter("filter_none")
|
||||
assert result == ""
|
||||
|
||||
def test_warm_filter_has_gamma_channels(self):
|
||||
"""暖色滤镜应该调整 RGB 通道伽马"""
|
||||
result = build_ffmpeg_filter("filter_warm", intensity=100)
|
||||
assert "gamma_r=" in result
|
||||
# 暖色红通道伽马 > 1.0
|
||||
assert "gamma_r=1.100" in result
|
||||
|
||||
def test_result_format_is_eq_params(self):
|
||||
"""结果格式应该是 eq=param1=val:param2=val..."""
|
||||
result = build_ffmpeg_filter("filter_brighten", intensity=100)
|
||||
assert result.startswith("eq=")
|
||||
# 参数之间用冒号分隔
|
||||
parts = result[3:].split(":")
|
||||
assert len(parts) >= 4 # 至少 brightness/contrast/saturation/gamma
|
||||
for part in parts:
|
||||
assert "=" in part # 每个部分都是 key=value 格式
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
"""template_clip_config 领域模型单元测试."""
|
||||
|
||||
import pytest
|
||||
|
||||
from domain.template_clip_config import (
|
||||
ClipType,
|
||||
TemplateClipConfig,
|
||||
TransitionEffect,
|
||||
)
|
||||
|
||||
|
||||
class TestClipType:
|
||||
"""ClipType 枚举测试."""
|
||||
|
||||
def test_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"
|
||||
|
||||
|
||||
class TestTransitionEffect:
|
||||
"""TransitionEffect 枚举测试."""
|
||||
|
||||
def test_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"
|
||||
|
||||
|
||||
class TestTemplateClipConfigCreate:
|
||||
"""TemplateClipConfig.create 工厂方法测试."""
|
||||
|
||||
def test_create_with_required_fields(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl_001", clip_type=ClipType.MAIN, order=1
|
||||
)
|
||||
assert clip.id
|
||||
assert len(clip.id) == 32
|
||||
assert clip.template_id == "tpl_001"
|
||||
assert clip.clip_type == ClipType.MAIN
|
||||
assert clip.order == 1
|
||||
assert clip.min_duration == 0.0
|
||||
assert clip.max_duration == 0.0
|
||||
assert clip.text_template == ""
|
||||
assert clip.material_requirements == {}
|
||||
assert clip.transition_effect == TransitionEffect.CUT
|
||||
assert clip.config == {}
|
||||
|
||||
def test_create_with_all_fields(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl_002",
|
||||
clip_type=ClipType.INTRO,
|
||||
order=2,
|
||||
min_duration=3.0,
|
||||
max_duration=10.0,
|
||||
text_template="欢迎来到{channel}",
|
||||
material_requirements={"type": "video", "min_count": 1},
|
||||
transition_effect=TransitionEffect.FADE,
|
||||
config={"key": "value"},
|
||||
)
|
||||
assert clip.clip_type == ClipType.INTRO
|
||||
assert clip.min_duration == 3.0
|
||||
assert clip.max_duration == 10.0
|
||||
assert clip.text_template == "欢迎来到{channel}"
|
||||
assert clip.material_requirements == {"type": "video", "min_count": 1}
|
||||
assert clip.transition_effect == TransitionEffect.FADE
|
||||
assert clip.config == {"key": "value"}
|
||||
|
||||
def test_create_with_string_clip_type(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl_003", clip_type="title", order=1
|
||||
)
|
||||
assert clip.clip_type == ClipType.TITLE
|
||||
|
||||
def test_create_with_string_transition_effect(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl_004",
|
||||
clip_type=ClipType.MAIN,
|
||||
order=1,
|
||||
transition_effect="dissolve",
|
||||
)
|
||||
assert clip.transition_effect == TransitionEffect.DISSOLVE
|
||||
|
||||
def test_create_strips_strings(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id=" tpl_005 ",
|
||||
clip_type=ClipType.MAIN,
|
||||
order=1,
|
||||
text_template=" 测试模板 ",
|
||||
)
|
||||
assert clip.template_id == "tpl_005"
|
||||
assert clip.text_template == "测试模板"
|
||||
|
||||
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_create_whitespace_template_id_raises(self):
|
||||
with pytest.raises(ValueError, match="template_id"):
|
||||
TemplateClipConfig.create(
|
||||
template_id=" ", clip_type=ClipType.MAIN, order=1
|
||||
)
|
||||
|
||||
def test_create_invalid_clip_type_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type="invalid_type", order=1
|
||||
)
|
||||
|
||||
def test_create_invalid_transition_effect_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
TemplateClipConfig.create(
|
||||
template_id="tpl",
|
||||
clip_type=ClipType.MAIN,
|
||||
order=1,
|
||||
transition_effect="invalid_effect",
|
||||
)
|
||||
|
||||
def test_create_negative_min_duration_raises(self):
|
||||
with pytest.raises(ValueError, match="min_duration"):
|
||||
TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1, min_duration=-1.0
|
||||
)
|
||||
|
||||
def test_create_negative_max_duration_raises(self):
|
||||
with pytest.raises(ValueError, match="max_duration"):
|
||||
TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1, max_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="tpl",
|
||||
clip_type=ClipType.MAIN,
|
||||
order=1,
|
||||
min_duration=10.0,
|
||||
max_duration=5.0,
|
||||
)
|
||||
|
||||
def test_create_min_equals_max_ok(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl",
|
||||
clip_type=ClipType.MAIN,
|
||||
order=1,
|
||||
min_duration=5.0,
|
||||
max_duration=5.0,
|
||||
)
|
||||
assert clip.min_duration == 5.0
|
||||
assert clip.max_duration == 5.0
|
||||
|
||||
def test_create_zero_duration_range_ok(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1
|
||||
)
|
||||
assert clip.min_duration == 0.0
|
||||
assert clip.max_duration == 0.0
|
||||
|
||||
def test_create_none_material_requirements_defaults_to_empty_dict(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1, material_requirements=None
|
||||
)
|
||||
assert clip.material_requirements == {}
|
||||
|
||||
def test_create_none_config_defaults_to_empty_dict(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1, config=None
|
||||
)
|
||||
assert clip.config == {}
|
||||
|
||||
def test_create_ids_are_unique(self):
|
||||
c1 = TemplateClipConfig.create(template_id="tpl", clip_type=ClipType.MAIN, order=1)
|
||||
c2 = TemplateClipConfig.create(template_id="tpl", clip_type=ClipType.MAIN, order=2)
|
||||
assert c1.id != c2.id
|
||||
|
||||
def test_create_timestamps_are_utc(self):
|
||||
clip = TemplateClipConfig.create(template_id="tpl", clip_type=ClipType.MAIN, order=1)
|
||||
assert clip.created_at.tzinfo is not None
|
||||
assert clip.updated_at.tzinfo is not None
|
||||
|
||||
|
||||
class TestTemplateClipConfigProperties:
|
||||
"""属性方法测试."""
|
||||
|
||||
def test_has_duration_range_false_when_both_zero(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1
|
||||
)
|
||||
assert clip.has_duration_range is False
|
||||
|
||||
def test_has_duration_range_true_when_min_set(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1, min_duration=2.0
|
||||
)
|
||||
assert clip.has_duration_range is True
|
||||
|
||||
def test_has_duration_range_true_when_max_set(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1, max_duration=10.0
|
||||
)
|
||||
assert clip.has_duration_range is True
|
||||
|
||||
def test_default_duration_both_zero(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1
|
||||
)
|
||||
assert clip.default_duration == 0.0
|
||||
|
||||
def test_default_duration_only_min(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1, min_duration=5.0
|
||||
)
|
||||
assert clip.default_duration == 5.0
|
||||
|
||||
def test_default_duration_only_max(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1, max_duration=10.0
|
||||
)
|
||||
assert clip.default_duration == 10.0
|
||||
|
||||
def test_default_duration_both_set_is_midpoint(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1, min_duration=5.0, max_duration=15.0
|
||||
)
|
||||
assert clip.default_duration == 10.0
|
||||
|
||||
def test_default_duration_min_equals_max(self):
|
||||
clip = TemplateClipConfig.create(
|
||||
template_id="tpl", clip_type=ClipType.MAIN, order=1, min_duration=5.0, max_duration=5.0
|
||||
)
|
||||
assert clip.default_duration == 5.0
|
||||
Reference in New Issue
Block a user