test(P3-1): 第52波 domain 单测 - config_schemas/filter_presets/transition_presets(+129) #842
Executable
+508
@@ -0,0 +1,508 @@
|
||||
"""config_schemas 领域层单元测试 - 配置 schema / 枚举 / 标准化函数"""
|
||||
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from packages.domain.config_schemas import (
|
||||
BGMSource,
|
||||
BGMConfig,
|
||||
CoverConfig,
|
||||
CoverType,
|
||||
DEFAULT_EDIT_PLAN_CONFIG,
|
||||
DEFAULT_EDIT_TEMPLATE_CONFIG,
|
||||
EditPlanConfigSchema,
|
||||
EditTemplateConfigSchema,
|
||||
ExportConfig,
|
||||
FilterConfig,
|
||||
ShadowConfig,
|
||||
StrokeConfig,
|
||||
SubtitleConfig,
|
||||
TextAnimation,
|
||||
TextPosition,
|
||||
TitleConfig,
|
||||
normalize_plan_config,
|
||||
normalize_template_config,
|
||||
)
|
||||
|
||||
|
||||
class TestEnums:
|
||||
"""枚举类型测试"""
|
||||
|
||||
def test_cover_type_values(self):
|
||||
assert CoverType.AI_FRAME == "ai_frame"
|
||||
assert CoverType.MANUAL == "manual"
|
||||
assert CoverType.UPLOAD == "upload"
|
||||
assert CoverType.AI_REGENERATE == "ai_regenerate"
|
||||
|
||||
def test_text_position_values(self):
|
||||
assert TextPosition.TOP == "top"
|
||||
assert TextPosition.CENTER == "center"
|
||||
assert TextPosition.BOTTOM == "bottom"
|
||||
|
||||
def test_text_animation_values(self):
|
||||
assert TextAnimation.NONE == "none"
|
||||
assert TextAnimation.FADE_IN == "fade_in"
|
||||
assert TextAnimation.SLIDE_UP == "slide_up"
|
||||
assert TextAnimation.SLIDE_DOWN == "slide_down"
|
||||
assert TextAnimation.SCALE == "scale"
|
||||
|
||||
def test_bgm_source_values(self):
|
||||
assert BGMSource.LIBRARY == "library"
|
||||
assert BGMSource.UPLOAD == "upload"
|
||||
assert BGMSource.AI_RECOMMEND == "ai_recommend"
|
||||
|
||||
def test_enums_are_str_enum(self):
|
||||
"""枚举都是 str 类型"""
|
||||
assert isinstance(CoverType.AI_FRAME, str)
|
||||
assert isinstance(TextPosition.TOP, str)
|
||||
assert isinstance(TextAnimation.FADE_IN, str)
|
||||
assert isinstance(BGMSource.LIBRARY, str)
|
||||
|
||||
|
||||
class TestStrokeConfig:
|
||||
"""StrokeConfig 测试"""
|
||||
|
||||
def test_defaults(self):
|
||||
config = StrokeConfig()
|
||||
assert config.enabled is False
|
||||
assert config.color == "#000000"
|
||||
assert config.width == 1
|
||||
|
||||
def test_width_min(self):
|
||||
config = StrokeConfig(width=1)
|
||||
assert config.width == 1
|
||||
|
||||
def test_width_max(self):
|
||||
config = StrokeConfig(width=10)
|
||||
assert config.width == 10
|
||||
|
||||
def test_width_below_min_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
StrokeConfig(width=0)
|
||||
|
||||
def test_width_above_max_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
StrokeConfig(width=11)
|
||||
|
||||
|
||||
class TestShadowConfig:
|
||||
"""ShadowConfig 测试"""
|
||||
|
||||
def test_defaults(self):
|
||||
config = ShadowConfig()
|
||||
assert config.enabled is False
|
||||
assert config.blur == 4
|
||||
assert config.offset_x == 2
|
||||
assert config.offset_y == 2
|
||||
|
||||
def test_blur_min(self):
|
||||
config = ShadowConfig(blur=0)
|
||||
assert config.blur == 0
|
||||
|
||||
def test_blur_max(self):
|
||||
config = ShadowConfig(blur=20)
|
||||
assert config.blur == 20
|
||||
|
||||
def test_blur_below_min_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
ShadowConfig(blur=-1)
|
||||
|
||||
def test_blur_above_max_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
ShadowConfig(blur=21)
|
||||
|
||||
|
||||
class TestCoverConfig:
|
||||
"""CoverConfig 测试"""
|
||||
|
||||
def test_defaults(self):
|
||||
config = CoverConfig()
|
||||
assert config.type == CoverType.AI_FRAME
|
||||
assert config.image_url == ""
|
||||
assert config.frame_time is None
|
||||
|
||||
def test_frame_time_ge_zero(self):
|
||||
config = CoverConfig(frame_time=0.0)
|
||||
assert config.frame_time == 0.0
|
||||
|
||||
def test_frame_time_negative_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
CoverConfig(frame_time=-1.0)
|
||||
|
||||
def test_custom_values(self):
|
||||
config = CoverConfig(
|
||||
type=CoverType.MANUAL,
|
||||
image_url="http://example.com/cover.jpg",
|
||||
frame_time=5.5,
|
||||
)
|
||||
assert config.type == CoverType.MANUAL
|
||||
assert config.image_url == "http://example.com/cover.jpg"
|
||||
assert config.frame_time == 5.5
|
||||
|
||||
|
||||
class TestTitleConfig:
|
||||
"""TitleConfig 测试"""
|
||||
|
||||
def test_defaults(self):
|
||||
config = TitleConfig()
|
||||
assert config.enabled is True
|
||||
assert config.ai_auto is True
|
||||
assert config.text == ""
|
||||
assert config.position == TextPosition.TOP
|
||||
assert config.font == "思源黑体"
|
||||
assert config.color == "#ffffff"
|
||||
assert config.size == 48
|
||||
assert config.bold is True
|
||||
assert config.italic is False
|
||||
assert isinstance(config.stroke, StrokeConfig)
|
||||
assert isinstance(config.shadow, ShadowConfig)
|
||||
|
||||
def test_size_min(self):
|
||||
config = TitleConfig(size=12)
|
||||
assert config.size == 12
|
||||
|
||||
def test_size_max(self):
|
||||
config = TitleConfig(size=120)
|
||||
assert config.size == 120
|
||||
|
||||
def test_size_below_min_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
TitleConfig(size=11)
|
||||
|
||||
def test_size_above_max_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
TitleConfig(size=121)
|
||||
|
||||
|
||||
class TestSubtitleConfig:
|
||||
"""SubtitleConfig 测试"""
|
||||
|
||||
def test_defaults(self):
|
||||
config = SubtitleConfig()
|
||||
assert config.enabled is True
|
||||
assert config.position == TextPosition.BOTTOM
|
||||
assert config.font == "思源黑体"
|
||||
assert config.color == "#ffffff"
|
||||
assert config.size == 24
|
||||
assert config.animation == TextAnimation.FADE_IN
|
||||
assert config.auto_generated is False
|
||||
assert config.language == ""
|
||||
assert config.max_chars_per_line == 20
|
||||
assert config.min_chars_per_segment == 8
|
||||
|
||||
def test_size_min(self):
|
||||
config = SubtitleConfig(size=12)
|
||||
assert config.size == 12
|
||||
|
||||
def test_size_max(self):
|
||||
config = SubtitleConfig(size=60)
|
||||
assert config.size == 60
|
||||
|
||||
def test_size_out_of_range_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
SubtitleConfig(size=61)
|
||||
|
||||
def test_max_chars_per_line_range(self):
|
||||
config = SubtitleConfig(max_chars_per_line=40)
|
||||
assert config.max_chars_per_line == 40
|
||||
with pytest.raises(ValidationError):
|
||||
SubtitleConfig(max_chars_per_line=7)
|
||||
with pytest.raises(ValidationError):
|
||||
SubtitleConfig(max_chars_per_line=41)
|
||||
|
||||
def test_min_chars_per_segment_range(self):
|
||||
config = SubtitleConfig(min_chars_per_segment=20)
|
||||
assert config.min_chars_per_segment == 20
|
||||
with pytest.raises(ValidationError):
|
||||
SubtitleConfig(min_chars_per_segment=1)
|
||||
with pytest.raises(ValidationError):
|
||||
SubtitleConfig(min_chars_per_segment=21)
|
||||
|
||||
|
||||
class TestBGMConfig:
|
||||
"""BGMConfig 测试"""
|
||||
|
||||
def test_defaults(self):
|
||||
config = BGMConfig()
|
||||
assert config.enabled is False
|
||||
assert config.source == BGMSource.LIBRARY
|
||||
assert config.asset_id == ""
|
||||
assert config.preset_id == ""
|
||||
assert config.audio_url == ""
|
||||
assert config.volume == 0.3
|
||||
assert config.fade_in == 0.0
|
||||
assert config.fade_out == 0.0
|
||||
assert config.loop_enabled is True
|
||||
assert config.sidechain_enabled is False
|
||||
assert config.sidechain_ratio == 0.3
|
||||
assert config.sidechain_attack == 0.02
|
||||
assert config.sidechain_release == 0.5
|
||||
assert config.sidechain_threshold == -25.0
|
||||
|
||||
def test_volume_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(volume=-0.1)
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(volume=1.1)
|
||||
|
||||
def test_fade_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(fade_in=31.0)
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(fade_out=31.0)
|
||||
|
||||
def test_sidechain_ratio_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_ratio=1.1)
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_ratio=-0.1)
|
||||
|
||||
def test_sidechain_attack_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_attack=0.0001)
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_attack=1.1)
|
||||
|
||||
def test_sidechain_release_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_release=0.001)
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_release=5.1)
|
||||
|
||||
def test_sidechain_threshold_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_threshold=-61.0)
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_threshold=1.0)
|
||||
|
||||
|
||||
class TestExportConfig:
|
||||
"""ExportConfig 测试"""
|
||||
|
||||
def test_defaults(self):
|
||||
config = ExportConfig()
|
||||
assert config.resolution == "1080x1920"
|
||||
assert config.fps == 30
|
||||
assert config.video_bitrate == 8000
|
||||
assert config.audio_bitrate == 128
|
||||
assert config.format == "mp4"
|
||||
assert config.quality_preset == "balanced"
|
||||
assert config.watermark_enabled is False
|
||||
assert config.watermark_text == ""
|
||||
|
||||
def test_fps_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
ExportConfig(fps=14)
|
||||
with pytest.raises(ValidationError):
|
||||
ExportConfig(fps=61)
|
||||
|
||||
def test_video_bitrate_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
ExportConfig(video_bitrate=999)
|
||||
with pytest.raises(ValidationError):
|
||||
ExportConfig(video_bitrate=20001)
|
||||
|
||||
def test_audio_bitrate_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
ExportConfig(audio_bitrate=63)
|
||||
with pytest.raises(ValidationError):
|
||||
ExportConfig(audio_bitrate=321)
|
||||
|
||||
|
||||
class TestFilterConfig:
|
||||
"""FilterConfig 测试"""
|
||||
|
||||
def test_defaults(self):
|
||||
config = FilterConfig()
|
||||
assert config.enabled is False
|
||||
assert config.preset_id == "filter_none"
|
||||
assert config.intensity == 100
|
||||
assert config.brightness == 0.0
|
||||
assert config.contrast == 1.0
|
||||
assert config.saturation == 1.0
|
||||
assert config.warmth == 0.0
|
||||
|
||||
def test_intensity_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(intensity=-1)
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(intensity=101)
|
||||
|
||||
def test_brightness_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(brightness=-1.1)
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(brightness=1.1)
|
||||
|
||||
def test_contrast_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(contrast=-0.1)
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(contrast=2.1)
|
||||
|
||||
def test_saturation_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(saturation=-0.1)
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(saturation=3.1)
|
||||
|
||||
def test_warmth_range(self):
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(warmth=-1.1)
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(warmth=1.1)
|
||||
|
||||
|
||||
class TestEditPlanConfigSchema:
|
||||
"""EditPlanConfigSchema 完整配置测试"""
|
||||
|
||||
def test_defaults(self):
|
||||
config = EditPlanConfigSchema()
|
||||
assert isinstance(config.cover, CoverConfig)
|
||||
assert isinstance(config.title, TitleConfig)
|
||||
assert isinstance(config.subtitle, SubtitleConfig)
|
||||
assert isinstance(config.bgm, BGMConfig)
|
||||
assert isinstance(config.export, ExportConfig)
|
||||
assert isinstance(config.filter, FilterConfig)
|
||||
assert config.editing_mode == "one_take"
|
||||
|
||||
def test_partial_update(self):
|
||||
"""部分字段更新,其他保持默认"""
|
||||
config = EditPlanConfigSchema(
|
||||
title={"text": "自定义标题", "size": 36},
|
||||
bgm={"enabled": True, "volume": 0.5},
|
||||
)
|
||||
assert config.title.text == "自定义标题"
|
||||
assert config.title.size == 36
|
||||
assert config.title.font == "思源黑体" # 其他字段默认
|
||||
assert config.bgm.enabled is True
|
||||
assert config.bgm.volume == 0.5
|
||||
assert config.cover.type == CoverType.AI_FRAME # 未设置的保持默认
|
||||
|
||||
|
||||
class TestEditTemplateConfigSchema:
|
||||
"""EditTemplateConfigSchema 测试"""
|
||||
|
||||
def test_defaults(self):
|
||||
config = EditTemplateConfigSchema()
|
||||
assert isinstance(config.cover, CoverConfig)
|
||||
assert config.editing_mode == "one_take"
|
||||
assert config.transition_enabled is True
|
||||
|
||||
def test_custom_transition_enabled(self):
|
||||
config = EditTemplateConfigSchema(transition_enabled=False)
|
||||
assert config.transition_enabled is False
|
||||
|
||||
|
||||
class TestDefaultConfigs:
|
||||
"""默认配置常量测试"""
|
||||
|
||||
def test_plan_config_structure(self):
|
||||
assert "cover" in DEFAULT_EDIT_PLAN_CONFIG
|
||||
assert "title" in DEFAULT_EDIT_PLAN_CONFIG
|
||||
assert "subtitle" in DEFAULT_EDIT_PLAN_CONFIG
|
||||
assert "bgm" in DEFAULT_EDIT_PLAN_CONFIG
|
||||
assert "export" in DEFAULT_EDIT_PLAN_CONFIG
|
||||
assert "filter" in DEFAULT_EDIT_PLAN_CONFIG
|
||||
assert "editing_mode" in DEFAULT_EDIT_PLAN_CONFIG
|
||||
|
||||
def test_template_config_has_transition_enabled(self):
|
||||
assert "transition_enabled" in DEFAULT_EDIT_TEMPLATE_CONFIG
|
||||
assert DEFAULT_EDIT_TEMPLATE_CONFIG["transition_enabled"] is True
|
||||
|
||||
def test_template_inherits_from_plan(self):
|
||||
"""模板配置继承计划配置的所有字段"""
|
||||
for key in DEFAULT_EDIT_PLAN_CONFIG:
|
||||
assert key in DEFAULT_EDIT_TEMPLATE_CONFIG
|
||||
|
||||
|
||||
class TestNormalizePlanConfig:
|
||||
"""normalize_plan_config 工具函数测试"""
|
||||
|
||||
def test_none_returns_default_copy(self):
|
||||
result = normalize_plan_config(None)
|
||||
assert result == DEFAULT_EDIT_PLAN_CONFIG
|
||||
# 确保是深拷贝
|
||||
result["title"]["text"] = "modified"
|
||||
assert DEFAULT_EDIT_PLAN_CONFIG["title"]["text"] == ""
|
||||
|
||||
def test_empty_dict_returns_default(self):
|
||||
result = normalize_plan_config({})
|
||||
assert result == DEFAULT_EDIT_PLAN_CONFIG
|
||||
|
||||
def test_updates_cover_section(self):
|
||||
result = normalize_plan_config({"cover": {"type": "manual", "frame_time": 5.0}})
|
||||
assert result["cover"]["type"] == "manual"
|
||||
assert result["cover"]["frame_time"] == 5.0
|
||||
assert result["cover"]["image_url"] == "" # 默认保留
|
||||
|
||||
def test_updates_title_section(self):
|
||||
result = normalize_plan_config({"title": {"text": "hello", "size": 32}})
|
||||
assert result["title"]["text"] == "hello"
|
||||
assert result["title"]["size"] == 32
|
||||
assert result["title"]["font"] == "思源黑体"
|
||||
|
||||
def test_updates_bgm_section(self):
|
||||
result = normalize_plan_config({"bgm": {"enabled": True, "volume": 0.8}})
|
||||
assert result["bgm"]["enabled"] is True
|
||||
assert result["bgm"]["volume"] == 0.8
|
||||
|
||||
def test_updates_editing_mode(self):
|
||||
result = normalize_plan_config({"editing_mode": "smart"})
|
||||
assert result["editing_mode"] == "smart"
|
||||
|
||||
def test_preserves_extra_fields(self):
|
||||
"""非标准字段被保留"""
|
||||
result = normalize_plan_config({"custom_field": "value", "generation_task_id": "task-1"})
|
||||
assert result["custom_field"] == "value"
|
||||
assert result["generation_task_id"] == "task-1"
|
||||
|
||||
def test_ignores_non_dict_section(self):
|
||||
"""section 不是 dict 时忽略"""
|
||||
result = normalize_plan_config({"cover": "not_a_dict"})
|
||||
assert result["cover"] == DEFAULT_EDIT_PLAN_CONFIG["cover"]
|
||||
|
||||
def test_ignores_non_str_editing_mode(self):
|
||||
result = normalize_plan_config({"editing_mode": 123})
|
||||
assert result["editing_mode"] == "one_take"
|
||||
|
||||
def test_deep_copy_independence(self):
|
||||
"""修改结果不影响默认值"""
|
||||
result = normalize_plan_config({})
|
||||
result["title"]["size"] = 999
|
||||
assert DEFAULT_EDIT_PLAN_CONFIG["title"]["size"] == 48
|
||||
|
||||
|
||||
class TestNormalizeTemplateConfig:
|
||||
"""normalize_template_config 工具函数测试"""
|
||||
|
||||
def test_none_returns_default_copy(self):
|
||||
result = normalize_template_config(None)
|
||||
assert result == DEFAULT_EDIT_TEMPLATE_CONFIG
|
||||
|
||||
def test_updates_transition_enabled(self):
|
||||
result = normalize_template_config({"transition_enabled": False})
|
||||
assert result["transition_enabled"] is False
|
||||
|
||||
def test_ignores_non_bool_transition_enabled(self):
|
||||
result = normalize_template_config({"transition_enabled": "yes"})
|
||||
assert result["transition_enabled"] is True
|
||||
|
||||
def test_updates_sections(self):
|
||||
result = normalize_template_config({
|
||||
"title": {"text": "模板标题"},
|
||||
"bgm": {"enabled": True},
|
||||
})
|
||||
assert result["title"]["text"] == "模板标题"
|
||||
assert result["bgm"]["enabled"] is True
|
||||
|
||||
def test_preserves_extra_fields(self):
|
||||
result = normalize_template_config({"custom": "value"})
|
||||
assert result["custom"] == "value"
|
||||
|
||||
def test_deep_copy_independence(self):
|
||||
result = normalize_template_config({})
|
||||
result["title"]["font"] = "CustomFont"
|
||||
assert DEFAULT_EDIT_TEMPLATE_CONFIG["title"]["font"] == "思源黑体"
|
||||
Executable
+230
@@ -0,0 +1,230 @@
|
||||
"""filter_presets 领域层单元测试 - 滤镜预设库"""
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.filter_presets import (
|
||||
FILTER_PRESET_LIBRARY,
|
||||
FilterPreset,
|
||||
build_ffmpeg_filter,
|
||||
get_filter_preset,
|
||||
list_filter_presets,
|
||||
)
|
||||
|
||||
|
||||
class TestFilterPreset:
|
||||
"""FilterPreset 数据类测试"""
|
||||
|
||||
def test_create_minimal(self):
|
||||
preset = FilterPreset(id="test", name="测试", category="basic")
|
||||
assert preset.id == "test"
|
||||
assert preset.name == "测试"
|
||||
assert preset.category == "basic"
|
||||
assert preset.description == ""
|
||||
assert preset.tags == []
|
||||
assert preset.brightness == 0.0
|
||||
assert preset.contrast == 1.0
|
||||
assert preset.saturation == 1.0
|
||||
assert preset.gamma == 1.0
|
||||
assert preset.gamma_r == 1.0
|
||||
assert preset.gamma_g == 1.0
|
||||
assert preset.gamma_b == 1.0
|
||||
assert preset.hue == 0.0
|
||||
assert preset.lut_url == ""
|
||||
|
||||
def test_create_with_all_params(self):
|
||||
preset = FilterPreset(
|
||||
id="custom",
|
||||
name="自定义",
|
||||
category="cinematic",
|
||||
description="电影感调色",
|
||||
tags=["电影", "调色"],
|
||||
brightness=0.1,
|
||||
contrast=1.2,
|
||||
saturation=0.9,
|
||||
gamma=1.1,
|
||||
gamma_r=1.05,
|
||||
gamma_g=1.0,
|
||||
gamma_b=0.95,
|
||||
hue=10.0,
|
||||
lut_url="http://example.com/lut.png",
|
||||
)
|
||||
assert preset.category == "cinematic"
|
||||
assert preset.description == "电影感调色"
|
||||
assert preset.tags == ["电影", "调色"]
|
||||
assert preset.brightness == 0.1
|
||||
assert preset.contrast == 1.2
|
||||
assert preset.lut_url == "http://example.com/lut.png"
|
||||
|
||||
def test_frozen_immutable(self):
|
||||
"""frozen dataclass 不可修改"""
|
||||
preset = FilterPreset(id="test", name="测试", category="basic")
|
||||
with pytest.raises(Exception): # FrozenInstanceError
|
||||
preset.name = "改名"
|
||||
|
||||
def test_tags_default_empty_list(self):
|
||||
preset = FilterPreset(id="test", name="测试", category="basic")
|
||||
assert preset.tags == []
|
||||
# 每次创建独立的 list
|
||||
preset2 = FilterPreset(id="test2", name="测试2", category="basic")
|
||||
assert preset.tags is not preset2.tags
|
||||
|
||||
|
||||
class TestFilterPresetLibrary:
|
||||
"""FILTER_PRESET_LIBRARY 预设库测试"""
|
||||
|
||||
def test_library_not_empty(self):
|
||||
assert len(FILTER_PRESET_LIBRARY) > 0
|
||||
|
||||
def test_all_presets_have_unique_ids(self):
|
||||
"""所有预设 ID 唯一"""
|
||||
ids = [p.id for p in FILTER_PRESET_LIBRARY]
|
||||
assert len(ids) == len(set(ids))
|
||||
|
||||
def test_all_presets_have_name_and_category(self):
|
||||
for preset in FILTER_PRESET_LIBRARY:
|
||||
assert preset.id, f"{preset} has no id"
|
||||
assert preset.name, f"{preset.id} has no name"
|
||||
assert preset.category, f"{preset.id} has no category"
|
||||
|
||||
def test_filter_none_exists(self):
|
||||
"""原图预设存在"""
|
||||
none_preset = next((p for p in FILTER_PRESET_LIBRARY if p.id == "filter_none"), None)
|
||||
assert none_preset is not None
|
||||
assert none_preset.name == "原图"
|
||||
assert none_preset.category == "basic"
|
||||
|
||||
def test_known_categories_exist(self):
|
||||
"""已知分类都有预设"""
|
||||
categories = {p.category for p in FILTER_PRESET_LIBRARY}
|
||||
assert "basic" in categories
|
||||
|
||||
def test_basic_category_presets(self):
|
||||
"""基础分类至少有几个预设"""
|
||||
basic = [p for p in FILTER_PRESET_LIBRARY if p.category == "basic"]
|
||||
assert len(basic) >= 3
|
||||
|
||||
def test_preset_params_in_reasonable_range(self):
|
||||
"""预设参数在合理范围内"""
|
||||
for preset in FILTER_PRESET_LIBRARY:
|
||||
assert -1.0 <= preset.brightness <= 1.0, f"{preset.id} brightness out of range"
|
||||
assert 0.0 <= preset.contrast <= 2.0, f"{preset.id} contrast out of range"
|
||||
assert 0.0 <= preset.saturation <= 3.0, f"{preset.id} saturation out of range"
|
||||
|
||||
|
||||
class TestGetFilterPreset:
|
||||
"""get_filter_preset 函数测试"""
|
||||
|
||||
def test_get_existing_preset(self):
|
||||
preset = get_filter_preset("filter_none")
|
||||
assert preset is not None
|
||||
assert preset.id == "filter_none"
|
||||
|
||||
def test_get_nonexistent_preset(self):
|
||||
assert get_filter_preset("nonexistent_filter") is None
|
||||
|
||||
def test_get_returns_correct_type(self):
|
||||
preset = get_filter_preset("filter_none")
|
||||
assert isinstance(preset, FilterPreset)
|
||||
|
||||
|
||||
class TestListFilterPresets:
|
||||
"""list_filter_presets 函数测试"""
|
||||
|
||||
def test_list_all(self):
|
||||
"""不带参数返回所有预设"""
|
||||
all_presets = list_filter_presets()
|
||||
assert len(all_presets) == len(FILTER_PRESET_LIBRARY)
|
||||
|
||||
def test_filter_by_category(self):
|
||||
"""按分类筛选"""
|
||||
basic_presets = list_filter_presets(category="basic")
|
||||
assert len(basic_presets) > 0
|
||||
assert all(p.category == "basic" for p in basic_presets)
|
||||
|
||||
def test_filter_by_nonexistent_category(self):
|
||||
"""不存在的分类返回空列表"""
|
||||
result = list_filter_presets(category="nonexistent_category")
|
||||
assert result == []
|
||||
|
||||
def test_search_by_name(self):
|
||||
"""按名称搜索"""
|
||||
result = list_filter_presets(keyword="明")
|
||||
assert len(result) >= 1
|
||||
assert any("明" in p.name for p in result)
|
||||
|
||||
def test_search_by_tag(self):
|
||||
"""按标签搜索"""
|
||||
# 找到有标签的预设
|
||||
tagged = [p for p in FILTER_PRESET_LIBRARY if p.tags]
|
||||
if tagged:
|
||||
tag = tagged[0].tags[0]
|
||||
result = list_filter_presets(keyword=tag)
|
||||
assert len(result) >= 1
|
||||
|
||||
def test_search_case_insensitive_in_name(self):
|
||||
"""搜索对中文名称有效"""
|
||||
result = list_filter_presets(keyword="原图")
|
||||
assert any(p.id == "filter_none" for p in result)
|
||||
|
||||
def test_search_empty_returns_all(self):
|
||||
"""空搜索返回所有"""
|
||||
result = list_filter_presets(keyword="")
|
||||
assert len(result) == len(FILTER_PRESET_LIBRARY)
|
||||
|
||||
def test_combined_category_and_search(self):
|
||||
"""同时按分类和搜索筛选"""
|
||||
result = list_filter_presets(category="basic", keyword="明")
|
||||
assert all(p.category == "basic" for p in result)
|
||||
if result:
|
||||
assert any("明" in p.name or any("明" in t for t in p.tags) for p in result)
|
||||
|
||||
def test_returns_list_of_filterpreset(self):
|
||||
result = list_filter_presets()
|
||||
assert all(isinstance(p, FilterPreset) for p in result)
|
||||
|
||||
|
||||
class TestBuildFfmpegFilter:
|
||||
"""build_ffmpeg_filter 函数测试"""
|
||||
|
||||
def test_filter_none_returns_empty_or_simple(self):
|
||||
"""原图滤镜应该返回空字符串或无操作滤镜"""
|
||||
result = build_ffmpeg_filter("filter_none", 100)
|
||||
# 应该是字符串,且不包含实质性调色参数
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_full_intensity(self):
|
||||
"""强度 100 时应用全量参数"""
|
||||
result = build_ffmpeg_filter("filter_brighten", 100)
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_zero_intensity(self):
|
||||
"""强度 0 时应该是原图效果"""
|
||||
result = build_ffmpeg_filter("filter_brighten", 0)
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_half_intensity(self):
|
||||
"""强度 50 时参数减半"""
|
||||
result50 = build_ffmpeg_filter("filter_brighten", 50)
|
||||
result100 = build_ffmpeg_filter("filter_brighten", 100)
|
||||
# 50% 和 100% 的结果应该不同
|
||||
assert result50 != result100
|
||||
|
||||
def test_nonexistent_preset(self):
|
||||
"""不存在的预设返回空字符串或默认值"""
|
||||
result = build_ffmpeg_filter("nonexistent", 100)
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_intensity_clamped(self):
|
||||
"""强度超过 100 或低于 0 的处理"""
|
||||
result_high = build_ffmpeg_filter("filter_brighten", 150)
|
||||
result_low = build_ffmpeg_filter("filter_brighten", -10)
|
||||
assert isinstance(result_high, str)
|
||||
assert isinstance(result_low, str)
|
||||
|
||||
def test_contains_eq_filter(self):
|
||||
"""结果应该包含 eq 滤镜参数"""
|
||||
result = build_ffmpeg_filter("filter_brighten", 100)
|
||||
# FFmpeg eq 滤镜通常包含 brightness/contrast/saturation 等参数
|
||||
# 至少应该有滤镜相关的字符串
|
||||
assert len(result) > 0
|
||||
Executable
+209
@@ -0,0 +1,209 @@
|
||||
"""transition_presets 领域层单元测试 - 转场预设库"""
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.transition_presets import (
|
||||
TRANSITION_PRESET_LIBRARY,
|
||||
TransitionPreset,
|
||||
get_default_transition,
|
||||
get_transition_preset,
|
||||
list_transition_presets,
|
||||
)
|
||||
|
||||
|
||||
class TestTransitionPreset:
|
||||
"""TransitionPreset 数据类测试"""
|
||||
|
||||
def test_create_minimal(self):
|
||||
preset = TransitionPreset(id="test", name="测试", category="basic", transition="fade")
|
||||
assert preset.id == "test"
|
||||
assert preset.name == "测试"
|
||||
assert preset.category == "basic"
|
||||
assert preset.transition == "fade"
|
||||
assert preset.description == ""
|
||||
assert preset.tags == []
|
||||
assert preset.default_duration == 0.5
|
||||
assert preset.min_duration == 0.1
|
||||
assert preset.max_duration == 3.0
|
||||
assert preset.has_custom_params is False
|
||||
|
||||
def test_create_with_all_params(self):
|
||||
preset = TransitionPreset(
|
||||
id="custom",
|
||||
name="自定义转场",
|
||||
category="special",
|
||||
description="炫酷特效",
|
||||
tags=["炫酷", "特效"],
|
||||
transition="custom",
|
||||
default_duration=1.0,
|
||||
min_duration=0.5,
|
||||
max_duration=5.0,
|
||||
has_custom_params=True,
|
||||
)
|
||||
assert preset.description == "炫酷特效"
|
||||
assert preset.tags == ["炫酷", "特效"]
|
||||
assert preset.default_duration == 1.0
|
||||
assert preset.min_duration == 0.5
|
||||
assert preset.max_duration == 5.0
|
||||
assert preset.has_custom_params is True
|
||||
|
||||
def test_frozen_immutable(self):
|
||||
"""frozen dataclass 不可修改"""
|
||||
preset = TransitionPreset(id="test", name="测试", category="basic")
|
||||
with pytest.raises(Exception):
|
||||
preset.name = "改名"
|
||||
|
||||
def test_tags_default_empty_list(self):
|
||||
preset = TransitionPreset(id="t1", name="t1", category="basic")
|
||||
preset2 = TransitionPreset(id="t2", name="t2", category="basic")
|
||||
assert preset.tags == []
|
||||
assert preset.tags is not preset2.tags
|
||||
|
||||
def test_default_transition_is_fade(self):
|
||||
preset = TransitionPreset(id="test", name="测试", category="basic")
|
||||
assert preset.transition == "fade"
|
||||
|
||||
|
||||
class TestTransitionPresetLibrary:
|
||||
"""TRANSITION_PRESET_LIBRARY 预设库测试"""
|
||||
|
||||
def test_library_not_empty(self):
|
||||
assert len(TRANSITION_PRESET_LIBRARY) > 0
|
||||
|
||||
def test_all_presets_have_unique_ids(self):
|
||||
"""所有预设 ID 唯一"""
|
||||
ids = [p.id for p in TRANSITION_PRESET_LIBRARY]
|
||||
assert len(ids) == len(set(ids))
|
||||
|
||||
def test_all_presets_have_required_fields(self):
|
||||
"""所有预设都有必填字段"""
|
||||
for preset in TRANSITION_PRESET_LIBRARY:
|
||||
assert preset.id, f"missing id"
|
||||
assert preset.name, f"{preset.id} missing name"
|
||||
assert preset.category, f"{preset.id} missing category"
|
||||
assert preset.transition, f"{preset.id} missing transition"
|
||||
|
||||
def test_transition_none_exists(self):
|
||||
"""无转场预设存在"""
|
||||
none_preset = next((p for p in TRANSITION_PRESET_LIBRARY if p.id == "transition_none"), None)
|
||||
assert none_preset is not None
|
||||
assert none_preset.name == "无转场"
|
||||
assert none_preset.transition == "none"
|
||||
|
||||
def test_transition_random_exists(self):
|
||||
"""随机转场预设存在"""
|
||||
random_preset = next((p for p in TRANSITION_PRESET_LIBRARY if p.id == "transition_random"), None)
|
||||
assert random_preset is not None
|
||||
assert random_preset.name == "随机"
|
||||
|
||||
def test_fade_category_exists(self):
|
||||
"""淡入淡出分类有预设"""
|
||||
fade_presets = [p for p in TRANSITION_PRESET_LIBRARY if p.category == "fade"]
|
||||
assert len(fade_presets) >= 2
|
||||
|
||||
def test_duration_constraints_valid(self):
|
||||
"""时长约束:min <= default <= max"""
|
||||
for preset in TRANSITION_PRESET_LIBRARY:
|
||||
assert preset.min_duration <= preset.default_duration, \
|
||||
f"{preset.id}: min > default"
|
||||
assert preset.default_duration <= preset.max_duration, \
|
||||
f"{preset.id}: default > max"
|
||||
assert preset.min_duration >= 0, f"{preset.id}: min < 0"
|
||||
|
||||
def test_known_categories_exist(self):
|
||||
"""已知分类都有预设"""
|
||||
categories = {p.category for p in TRANSITION_PRESET_LIBRARY}
|
||||
assert "basic" in categories
|
||||
assert "fade" in categories
|
||||
|
||||
|
||||
class TestGetTransitionPreset:
|
||||
"""get_transition_preset 函数测试"""
|
||||
|
||||
def test_get_existing_preset(self):
|
||||
preset = get_transition_preset("transition_none")
|
||||
assert preset is not None
|
||||
assert preset.id == "transition_none"
|
||||
|
||||
def test_get_fade_preset(self):
|
||||
preset = get_transition_preset("transition_fade")
|
||||
assert preset is not None
|
||||
assert preset.transition == "fade"
|
||||
|
||||
def test_get_nonexistent_preset(self):
|
||||
assert get_transition_preset("nonexistent_transition") is None
|
||||
|
||||
def test_returns_transitionpreset_type(self):
|
||||
preset = get_transition_preset("transition_fade")
|
||||
assert isinstance(preset, TransitionPreset)
|
||||
|
||||
|
||||
class TestListTransitionPresets:
|
||||
"""list_transition_presets 函数测试"""
|
||||
|
||||
def test_list_all(self):
|
||||
"""不带参数返回所有预设"""
|
||||
all_presets = list_transition_presets()
|
||||
assert len(all_presets) == len(TRANSITION_PRESET_LIBRARY)
|
||||
|
||||
def test_filter_by_category(self):
|
||||
"""按分类筛选"""
|
||||
fade_presets = list_transition_presets(category="fade")
|
||||
assert len(fade_presets) > 0
|
||||
assert all(p.category == "fade" for p in fade_presets)
|
||||
|
||||
def test_filter_by_basic_category(self):
|
||||
basic_presets = list_transition_presets(category="basic")
|
||||
assert len(basic_presets) >= 2
|
||||
|
||||
def test_filter_by_nonexistent_category(self):
|
||||
result = list_transition_presets(category="nonexistent")
|
||||
assert result == []
|
||||
|
||||
def test_search_by_name(self):
|
||||
"""按名称搜索"""
|
||||
result = list_transition_presets(keyword="淡入")
|
||||
assert len(result) >= 1
|
||||
assert any("淡入" in p.name for p in result)
|
||||
|
||||
def test_search_by_tag(self):
|
||||
"""按标签搜索"""
|
||||
tagged = [p for p in TRANSITION_PRESET_LIBRARY if p.tags]
|
||||
if tagged:
|
||||
tag = tagged[0].tags[0]
|
||||
result = list_transition_presets(keyword=tag)
|
||||
assert len(result) >= 1
|
||||
|
||||
def test_search_empty_returns_all(self):
|
||||
result = list_transition_presets(keyword="")
|
||||
assert len(result) == len(TRANSITION_PRESET_LIBRARY)
|
||||
|
||||
def test_combined_category_and_search(self):
|
||||
result = list_transition_presets(category="fade", keyword="淡入")
|
||||
assert all(p.category == "fade" for p in result)
|
||||
|
||||
def test_returns_list_of_transitionpreset(self):
|
||||
result = list_transition_presets()
|
||||
assert all(isinstance(p, TransitionPreset) for p in result)
|
||||
|
||||
|
||||
class TestGetDefaultTransition:
|
||||
"""get_default_transition 函数测试"""
|
||||
|
||||
def test_returns_preset(self):
|
||||
preset = get_default_transition()
|
||||
assert preset is not None
|
||||
assert isinstance(preset, TransitionPreset)
|
||||
|
||||
def test_default_is_none(self):
|
||||
"""默认转场是无转场(硬切)"""
|
||||
preset = get_default_transition()
|
||||
assert preset.id == "transition_none"
|
||||
assert preset.transition == "none"
|
||||
|
||||
def test_default_has_zero_duration(self):
|
||||
"""无转场默认时长为 0"""
|
||||
preset = get_default_transition()
|
||||
assert preset.default_duration == 0.0
|
||||
assert preset.min_duration == 0.0
|
||||
assert preset.max_duration == 0.0
|
||||
Reference in New Issue
Block a user