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:
CI Bot
2026-07-21 00:46:30 +08:00
committed by xiaoxia
parent 17494a1cde
commit 5ecc75c5ba
2 changed files with 466 additions and 0 deletions
+229
View File
@@ -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
View File
@@ -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