test(p3-1): 第八波 - config_schemas 领域单测 105个 #710
Executable
+815
@@ -0,0 +1,815 @@
|
||||
"""
|
||||
config_schemas 配置结构定义单元测试
|
||||
|
||||
覆盖:
|
||||
- 4个枚举类型
|
||||
- 10个Pydantic模型
|
||||
- 2个normalize工具函数
|
||||
- 2个默认值常量
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from packages.domain.config_schemas import (
|
||||
DEFAULT_EDIT_PLAN_CONFIG,
|
||||
DEFAULT_EDIT_TEMPLATE_CONFIG,
|
||||
BGMConfig,
|
||||
BGMSource,
|
||||
CoverConfig,
|
||||
CoverType,
|
||||
EditPlanConfigSchema,
|
||||
EditTemplateConfigSchema,
|
||||
ExportConfig,
|
||||
FilterConfig,
|
||||
ShadowConfig,
|
||||
StrokeConfig,
|
||||
SubtitleConfig,
|
||||
TextAnimation,
|
||||
TextPosition,
|
||||
TitleConfig,
|
||||
normalize_plan_config,
|
||||
normalize_template_config,
|
||||
)
|
||||
|
||||
# ── 枚举测试 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestCoverType:
|
||||
"""CoverType 枚举测试"""
|
||||
|
||||
def test_all_types_exist(self):
|
||||
assert CoverType.AI_FRAME == "ai_frame"
|
||||
assert CoverType.MANUAL == "manual"
|
||||
assert CoverType.UPLOAD == "upload"
|
||||
assert CoverType.AI_REGENERATE == "ai_regenerate"
|
||||
|
||||
def test_total_count(self):
|
||||
assert len(CoverType) == 4
|
||||
|
||||
def test_is_string_enum(self):
|
||||
for t in CoverType:
|
||||
assert isinstance(t.value, str)
|
||||
assert isinstance(t, str)
|
||||
|
||||
def test_string_comparison(self):
|
||||
assert CoverType.AI_FRAME == "ai_frame"
|
||||
assert CoverType.UPLOAD != "manual"
|
||||
|
||||
|
||||
class TestTextPosition:
|
||||
"""TextPosition 枚举测试"""
|
||||
|
||||
def test_all_positions_exist(self):
|
||||
assert TextPosition.TOP == "top"
|
||||
assert TextPosition.CENTER == "center"
|
||||
assert TextPosition.BOTTOM == "bottom"
|
||||
|
||||
def test_total_count(self):
|
||||
assert len(TextPosition) == 3
|
||||
|
||||
def test_is_string_enum(self):
|
||||
for p in TextPosition:
|
||||
assert isinstance(p.value, str)
|
||||
assert isinstance(p, str)
|
||||
|
||||
|
||||
class TestTextAnimation:
|
||||
"""TextAnimation 枚举测试"""
|
||||
|
||||
def test_all_animations_exist(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_total_count(self):
|
||||
assert len(TextAnimation) == 5
|
||||
|
||||
def test_is_string_enum(self):
|
||||
for a in TextAnimation:
|
||||
assert isinstance(a.value, str)
|
||||
assert isinstance(a, str)
|
||||
|
||||
|
||||
class TestBGMSource:
|
||||
"""BGMSource 枚举测试"""
|
||||
|
||||
def test_all_sources_exist(self):
|
||||
assert BGMSource.LIBRARY == "library"
|
||||
assert BGMSource.UPLOAD == "upload"
|
||||
assert BGMSource.AI_RECOMMEND == "ai_recommend"
|
||||
|
||||
def test_total_count(self):
|
||||
assert len(BGMSource) == 3
|
||||
|
||||
def test_is_string_enum(self):
|
||||
for s in BGMSource:
|
||||
assert isinstance(s.value, str)
|
||||
assert isinstance(s, str)
|
||||
|
||||
|
||||
# ── 子结构模型测试 ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestStrokeConfig:
|
||||
"""StrokeConfig 描边配置测试"""
|
||||
|
||||
def test_default_values(self):
|
||||
config = StrokeConfig()
|
||||
assert config.enabled is False
|
||||
assert config.color == "#000000"
|
||||
assert config.width == 1
|
||||
|
||||
def test_custom_values(self):
|
||||
config = StrokeConfig(enabled=True, color="#ff0000", width=5)
|
||||
assert config.enabled is True
|
||||
assert config.color == "#ff0000"
|
||||
assert config.width == 5
|
||||
|
||||
def test_width_min_boundary(self):
|
||||
config = StrokeConfig(width=1)
|
||||
assert config.width == 1
|
||||
|
||||
def test_width_max_boundary(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_default_values(self):
|
||||
config = ShadowConfig()
|
||||
assert config.enabled is False
|
||||
assert config.blur == 4
|
||||
assert config.offset_x == 2
|
||||
assert config.offset_y == 2
|
||||
|
||||
def test_custom_values(self):
|
||||
config = ShadowConfig(enabled=True, blur=10, offset_x=5, offset_y=3)
|
||||
assert config.enabled is True
|
||||
assert config.blur == 10
|
||||
assert config.offset_x == 5
|
||||
assert config.offset_y == 3
|
||||
|
||||
def test_blur_min_boundary(self):
|
||||
config = ShadowConfig(blur=0)
|
||||
assert config.blur == 0
|
||||
|
||||
def test_blur_max_boundary(self):
|
||||
config = ShadowConfig(blur=20)
|
||||
assert config.blur == 20
|
||||
|
||||
def test_blur_above_max_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
ShadowConfig(blur=21)
|
||||
|
||||
def test_negative_offset(self):
|
||||
config = ShadowConfig(offset_x=-3, offset_y=-2)
|
||||
assert config.offset_x == -3
|
||||
assert config.offset_y == -2
|
||||
|
||||
|
||||
class TestCoverConfig:
|
||||
"""CoverConfig 封面配置测试"""
|
||||
|
||||
def test_default_values(self):
|
||||
config = CoverConfig()
|
||||
assert config.type == CoverType.AI_FRAME
|
||||
assert config.image_url == ""
|
||||
assert config.frame_time is None
|
||||
|
||||
def test_custom_values(self):
|
||||
config = CoverConfig(type=CoverType.MANUAL, image_url="http://img/1.jpg", frame_time=5.5)
|
||||
assert config.type == CoverType.MANUAL
|
||||
assert config.image_url == "http://img/1.jpg"
|
||||
assert config.frame_time == 5.5
|
||||
|
||||
def test_frame_time_min_boundary(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_string_type_conversion(self):
|
||||
"""字符串枚举值自动转换"""
|
||||
config = CoverConfig(type="upload")
|
||||
assert config.type == CoverType.UPLOAD
|
||||
|
||||
|
||||
class TestTitleConfig:
|
||||
"""TitleConfig 标题配置测试"""
|
||||
|
||||
def test_default_values(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_custom_values(self):
|
||||
config = TitleConfig(
|
||||
enabled=False,
|
||||
ai_auto=False,
|
||||
text="测试标题",
|
||||
position=TextPosition.BOTTOM,
|
||||
font="微软雅黑",
|
||||
color="#000000",
|
||||
size=72,
|
||||
bold=False,
|
||||
italic=True,
|
||||
)
|
||||
assert config.enabled is False
|
||||
assert config.ai_auto is False
|
||||
assert config.text == "测试标题"
|
||||
assert config.position == TextPosition.BOTTOM
|
||||
assert config.size == 72
|
||||
|
||||
def test_size_min_boundary(self):
|
||||
config = TitleConfig(size=12)
|
||||
assert config.size == 12
|
||||
|
||||
def test_size_max_boundary(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)
|
||||
|
||||
def test_nested_stroke_config(self):
|
||||
config = TitleConfig(stroke={"enabled": True, "width": 3})
|
||||
assert config.stroke.enabled is True
|
||||
assert config.stroke.width == 3
|
||||
|
||||
def test_nested_shadow_config(self):
|
||||
config = TitleConfig(shadow={"enabled": True, "blur": 8})
|
||||
assert config.shadow.enabled is True
|
||||
assert config.shadow.blur == 8
|
||||
|
||||
|
||||
class TestSubtitleConfig:
|
||||
"""SubtitleConfig 字幕配置测试"""
|
||||
|
||||
def test_default_values(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_custom_values(self):
|
||||
config = SubtitleConfig(
|
||||
enabled=False,
|
||||
position=TextPosition.TOP,
|
||||
size=36,
|
||||
animation=TextAnimation.SLIDE_UP,
|
||||
auto_generated=True,
|
||||
language="zh",
|
||||
max_chars_per_line=30,
|
||||
min_chars_per_segment=10,
|
||||
)
|
||||
assert config.enabled is False
|
||||
assert config.position == TextPosition.TOP
|
||||
assert config.animation == TextAnimation.SLIDE_UP
|
||||
assert config.auto_generated is True
|
||||
assert config.language == "zh"
|
||||
|
||||
def test_size_min_boundary(self):
|
||||
config = SubtitleConfig(size=12)
|
||||
assert config.size == 12
|
||||
|
||||
def test_size_max_boundary(self):
|
||||
config = SubtitleConfig(size=60)
|
||||
assert config.size == 60
|
||||
|
||||
def test_size_below_min_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
SubtitleConfig(size=11)
|
||||
|
||||
def test_max_chars_min_boundary(self):
|
||||
config = SubtitleConfig(max_chars_per_line=8)
|
||||
assert config.max_chars_per_line == 8
|
||||
|
||||
def test_max_chars_max_boundary(self):
|
||||
config = SubtitleConfig(max_chars_per_line=40)
|
||||
assert config.max_chars_per_line == 40
|
||||
|
||||
def test_min_chars_min_boundary(self):
|
||||
config = SubtitleConfig(min_chars_per_segment=2)
|
||||
assert config.min_chars_per_segment == 2
|
||||
|
||||
def test_min_chars_max_boundary(self):
|
||||
config = SubtitleConfig(min_chars_per_segment=20)
|
||||
assert config.min_chars_per_segment == 20
|
||||
|
||||
|
||||
class TestBGMConfig:
|
||||
"""BGMConfig 背景音乐配置测试"""
|
||||
|
||||
def test_default_values(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_custom_values(self):
|
||||
config = BGMConfig(
|
||||
enabled=True,
|
||||
source=BGMSource.UPLOAD,
|
||||
asset_id="bgm_001",
|
||||
volume=0.5,
|
||||
fade_in=2.0,
|
||||
fade_out=3.0,
|
||||
loop_enabled=False,
|
||||
sidechain_enabled=True,
|
||||
sidechain_ratio=0.5,
|
||||
sidechain_attack=0.05,
|
||||
sidechain_release=1.0,
|
||||
sidechain_threshold=-20.0,
|
||||
)
|
||||
assert config.enabled is True
|
||||
assert config.source == BGMSource.UPLOAD
|
||||
assert config.asset_id == "bgm_001"
|
||||
assert config.volume == 0.5
|
||||
assert config.loop_enabled is False
|
||||
assert config.sidechain_enabled is True
|
||||
assert config.sidechain_ratio == 0.5
|
||||
|
||||
def test_volume_min_boundary(self):
|
||||
config = BGMConfig(volume=0.0)
|
||||
assert config.volume == 0.0
|
||||
|
||||
def test_volume_max_boundary(self):
|
||||
config = BGMConfig(volume=1.0)
|
||||
assert config.volume == 1.0
|
||||
|
||||
def test_volume_above_max_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(volume=1.1)
|
||||
|
||||
def test_fade_in_max_boundary(self):
|
||||
config = BGMConfig(fade_in=30.0)
|
||||
assert config.fade_in == 30.0
|
||||
|
||||
def test_fade_in_above_max_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(fade_in=31.0)
|
||||
|
||||
def test_sidechain_ratio_range(self):
|
||||
config = BGMConfig(sidechain_ratio=0.0)
|
||||
assert config.sidechain_ratio == 0.0
|
||||
config = BGMConfig(sidechain_ratio=1.0)
|
||||
assert config.sidechain_ratio == 1.0
|
||||
|
||||
def test_sidechain_attack_min(self):
|
||||
config = BGMConfig(sidechain_attack=0.001)
|
||||
assert config.sidechain_attack == 0.001
|
||||
|
||||
def test_sidechain_attack_below_min_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_attack=0.0001)
|
||||
|
||||
def test_sidechain_threshold_range(self):
|
||||
config = BGMConfig(sidechain_threshold=-60.0)
|
||||
assert config.sidechain_threshold == -60.0
|
||||
config = BGMConfig(sidechain_threshold=0.0)
|
||||
assert config.sidechain_threshold == 0.0
|
||||
|
||||
def test_sidechain_threshold_out_of_range_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_threshold=-61.0)
|
||||
with pytest.raises(ValidationError):
|
||||
BGMConfig(sidechain_threshold=1.0)
|
||||
|
||||
|
||||
class TestExportConfig:
|
||||
"""ExportConfig 导出配置测试"""
|
||||
|
||||
def test_default_values(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_custom_values(self):
|
||||
config = ExportConfig(
|
||||
resolution="2160x3840",
|
||||
fps=60,
|
||||
video_bitrate=20000,
|
||||
audio_bitrate=320,
|
||||
format="mov",
|
||||
quality_preset="best",
|
||||
watermark_enabled=True,
|
||||
watermark_text="测试水印",
|
||||
)
|
||||
assert config.resolution == "2160x3840"
|
||||
assert config.fps == 60
|
||||
assert config.video_bitrate == 20000
|
||||
assert config.format == "mov"
|
||||
|
||||
def test_fps_min_boundary(self):
|
||||
config = ExportConfig(fps=15)
|
||||
assert config.fps == 15
|
||||
|
||||
def test_fps_max_boundary(self):
|
||||
config = ExportConfig(fps=60)
|
||||
assert config.fps == 60
|
||||
|
||||
def test_fps_below_min_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
ExportConfig(fps=14)
|
||||
|
||||
def test_fps_above_max_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
ExportConfig(fps=61)
|
||||
|
||||
def test_video_bitrate_range(self):
|
||||
config = ExportConfig(video_bitrate=1000)
|
||||
assert config.video_bitrate == 1000
|
||||
config = ExportConfig(video_bitrate=20000)
|
||||
assert config.video_bitrate == 20000
|
||||
|
||||
def test_audio_bitrate_range(self):
|
||||
config = ExportConfig(audio_bitrate=64)
|
||||
assert config.audio_bitrate == 64
|
||||
config = ExportConfig(audio_bitrate=320)
|
||||
assert config.audio_bitrate == 320
|
||||
|
||||
|
||||
class TestFilterConfig:
|
||||
"""FilterConfig 滤镜配置测试"""
|
||||
|
||||
def test_default_values(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_custom_values(self):
|
||||
config = FilterConfig(
|
||||
enabled=True,
|
||||
preset_id="filter_vintage",
|
||||
intensity=50,
|
||||
brightness=0.5,
|
||||
contrast=1.5,
|
||||
saturation=2.0,
|
||||
warmth=0.3,
|
||||
)
|
||||
assert config.enabled is True
|
||||
assert config.preset_id == "filter_vintage"
|
||||
assert config.intensity == 50
|
||||
assert config.brightness == 0.5
|
||||
|
||||
def test_intensity_min_boundary(self):
|
||||
config = FilterConfig(intensity=0)
|
||||
assert config.intensity == 0
|
||||
|
||||
def test_intensity_max_boundary(self):
|
||||
config = FilterConfig(intensity=100)
|
||||
assert config.intensity == 100
|
||||
|
||||
def test_intensity_out_of_range_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(intensity=-1)
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(intensity=101)
|
||||
|
||||
def test_brightness_range(self):
|
||||
config = FilterConfig(brightness=-1.0)
|
||||
assert config.brightness == -1.0
|
||||
config = FilterConfig(brightness=1.0)
|
||||
assert config.brightness == 1.0
|
||||
|
||||
def test_contrast_range(self):
|
||||
config = FilterConfig(contrast=0.0)
|
||||
assert config.contrast == 0.0
|
||||
config = FilterConfig(contrast=2.0)
|
||||
assert config.contrast == 2.0
|
||||
|
||||
def test_saturation_range(self):
|
||||
config = FilterConfig(saturation=0.0)
|
||||
assert config.saturation == 0.0
|
||||
config = FilterConfig(saturation=3.0)
|
||||
assert config.saturation == 3.0
|
||||
|
||||
def test_warmth_range(self):
|
||||
config = FilterConfig(warmth=-1.0)
|
||||
assert config.warmth == -1.0
|
||||
config = FilterConfig(warmth=1.0)
|
||||
assert config.warmth == 1.0
|
||||
|
||||
def test_brightness_out_of_range_raises(self):
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(brightness=-1.1)
|
||||
with pytest.raises(ValidationError):
|
||||
FilterConfig(brightness=1.1)
|
||||
|
||||
|
||||
# ── 完整 config 模型测试 ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestEditPlanConfigSchema:
|
||||
"""EditPlanConfigSchema 完整计划配置测试"""
|
||||
|
||||
def test_default_values(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={"enabled": False},
|
||||
bgm={"enabled": True, "volume": 0.5},
|
||||
editing_mode="pip",
|
||||
)
|
||||
assert config.title.enabled is False
|
||||
assert config.bgm.enabled is True
|
||||
assert config.bgm.volume == 0.5
|
||||
assert config.editing_mode == "pip"
|
||||
# 其他字段保持默认
|
||||
assert config.cover.type == CoverType.AI_FRAME
|
||||
assert config.subtitle.enabled is True
|
||||
|
||||
def test_nested_model_preservation(self):
|
||||
"""嵌套模型完整可用"""
|
||||
config = EditPlanConfigSchema()
|
||||
assert config.title.stroke.enabled is False
|
||||
assert config.title.shadow.blur == 4
|
||||
assert config.bgm.sidechain_ratio == 0.3
|
||||
|
||||
|
||||
class TestEditTemplateConfigSchema:
|
||||
"""EditTemplateConfigSchema 模板配置测试"""
|
||||
|
||||
def test_default_values(self):
|
||||
config = EditTemplateConfigSchema()
|
||||
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"
|
||||
assert config.transition_enabled is True
|
||||
|
||||
def test_transition_enabled_false(self):
|
||||
config = EditTemplateConfigSchema(transition_enabled=False)
|
||||
assert config.transition_enabled is False
|
||||
|
||||
def test_partial_update(self):
|
||||
config = EditTemplateConfigSchema(
|
||||
filter={"enabled": True, "preset_id": "filter_cinematic"},
|
||||
transition_enabled=False,
|
||||
editing_mode="voice_over",
|
||||
)
|
||||
assert config.filter.enabled is True
|
||||
assert config.filter.preset_id == "filter_cinematic"
|
||||
assert config.transition_enabled is False
|
||||
assert config.editing_mode == "voice_over"
|
||||
|
||||
|
||||
# ── 默认值常量测试 ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestDefaultConfigs:
|
||||
"""默认值常量测试"""
|
||||
|
||||
def test_plan_config_structure(self):
|
||||
"""DEFAULT_EDIT_PLAN_CONFIG 结构完整"""
|
||||
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(self):
|
||||
"""模板配置比计划配置多一个 transition_enabled"""
|
||||
assert "transition_enabled" in DEFAULT_EDIT_TEMPLATE_CONFIG
|
||||
assert DEFAULT_EDIT_TEMPLATE_CONFIG["transition_enabled"] is True
|
||||
|
||||
def test_template_extends_plan(self):
|
||||
"""模板配置是计划配置的超集"""
|
||||
for key in DEFAULT_EDIT_PLAN_CONFIG:
|
||||
assert key in DEFAULT_EDIT_TEMPLATE_CONFIG
|
||||
|
||||
def test_defaults_are_deep_copy_safe(self):
|
||||
"""修改默认值不会影响常量本身"""
|
||||
original = copy.deepcopy(DEFAULT_EDIT_PLAN_CONFIG)
|
||||
config = EditPlanConfigSchema()
|
||||
config.title.text = "modified"
|
||||
assert DEFAULT_EDIT_PLAN_CONFIG == original
|
||||
|
||||
|
||||
# ── normalize 函数测试 ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestNormalizePlanConfig:
|
||||
"""normalize_plan_config 函数测试"""
|
||||
|
||||
def test_none_returns_default(self):
|
||||
result = normalize_plan_config(None)
|
||||
assert result == DEFAULT_EDIT_PLAN_CONFIG
|
||||
# 确保是深拷贝
|
||||
result["cover"]["type"] = "manual"
|
||||
assert DEFAULT_EDIT_PLAN_CONFIG["cover"]["type"] == "ai_frame"
|
||||
|
||||
def test_empty_dict_returns_default(self):
|
||||
result = normalize_plan_config({})
|
||||
assert result == DEFAULT_EDIT_PLAN_CONFIG
|
||||
|
||||
def test_partial_cover_update(self):
|
||||
raw = {"cover": {"type": "manual", "frame_time": 5.0}}
|
||||
result = normalize_plan_config(raw)
|
||||
assert result["cover"]["type"] == "manual"
|
||||
assert result["cover"]["frame_time"] == 5.0
|
||||
# 其他字段保留默认
|
||||
assert result["cover"]["image_url"] == ""
|
||||
|
||||
def test_partial_title_update(self):
|
||||
raw = {"title": {"enabled": False, "text": "自定义标题"}}
|
||||
result = normalize_plan_config(raw)
|
||||
assert result["title"]["enabled"] is False
|
||||
assert result["title"]["text"] == "自定义标题"
|
||||
assert result["title"]["size"] == 48 # 默认值保留
|
||||
|
||||
def test_partial_subtitle_update(self):
|
||||
raw = {"subtitle": {"enabled": False, "size": 32}}
|
||||
result = normalize_plan_config(raw)
|
||||
assert result["subtitle"]["enabled"] is False
|
||||
assert result["subtitle"]["size"] == 32
|
||||
|
||||
def test_partial_bgm_update(self):
|
||||
raw = {"bgm": {"enabled": True, "volume": 0.8}}
|
||||
result = normalize_plan_config(raw)
|
||||
assert result["bgm"]["enabled"] is True
|
||||
assert result["bgm"]["volume"] == 0.8
|
||||
|
||||
def test_editing_mode_update(self):
|
||||
raw = {"editing_mode": "pip"}
|
||||
result = normalize_plan_config(raw)
|
||||
assert result["editing_mode"] == "pip"
|
||||
|
||||
def test_non_standard_fields_preserved(self):
|
||||
"""非标准字段会被保留(透传)"""
|
||||
raw = {"generation_task_id": "task_123", "custom_field": "value"}
|
||||
result = normalize_plan_config(raw)
|
||||
assert result["generation_task_id"] == "task_123"
|
||||
assert result["custom_field"] == "value"
|
||||
|
||||
def test_full_update(self):
|
||||
"""多个字段同时更新"""
|
||||
raw = {
|
||||
"cover": {"type": "upload", "image_url": "http://img.jpg"},
|
||||
"title": {"enabled": False},
|
||||
"bgm": {"enabled": True, "volume": 0.5},
|
||||
"editing_mode": "voice_pip",
|
||||
"extra_key": "extra_val",
|
||||
}
|
||||
result = normalize_plan_config(raw)
|
||||
assert result["cover"]["type"] == "upload"
|
||||
assert result["title"]["enabled"] is False
|
||||
assert result["bgm"]["enabled"] is True
|
||||
assert result["bgm"]["volume"] == 0.5
|
||||
assert result["editing_mode"] == "voice_pip"
|
||||
assert result["extra_key"] == "extra_val"
|
||||
|
||||
def test_non_dict_section_ignored(self):
|
||||
"""section不是dict时忽略"""
|
||||
raw = {"cover": "not_a_dict"}
|
||||
result = normalize_plan_config(raw)
|
||||
# cover 应保持默认值
|
||||
assert result["cover"]["type"] == "ai_frame"
|
||||
# 但 cover 字段本身作为非标准字段保留
|
||||
# 不对,看实现:只有当 isinstance(raw[section_key], dict) 时才 update
|
||||
# 非dict的section会作为非标准字段保留吗?看实现:
|
||||
# section_key in raw and isinstance -> update
|
||||
# 然后遍历所有key,不在标准列表中的才会保留
|
||||
# cover 在标准列表中,所以不会被保留为非标准字段
|
||||
# 所以结果应该是默认的cover配置
|
||||
assert isinstance(result["cover"], dict)
|
||||
assert result["cover"]["type"] == "ai_frame"
|
||||
|
||||
def test_editing_mode_non_string_ignored(self):
|
||||
"""editing_mode不是字符串时忽略"""
|
||||
raw = {"editing_mode": 123}
|
||||
result = normalize_plan_config(raw)
|
||||
# 作为非标准字段保留?不,看实现:
|
||||
# 如果 "editing_mode" in raw and isinstance(str) 才更新
|
||||
# 然后遍历所有key,不在标准列表中的保留
|
||||
# editing_mode 在标准列表中,所以不会保留为非标准
|
||||
# 所以 editing_mode 保持默认
|
||||
assert result["editing_mode"] == "one_take"
|
||||
|
||||
|
||||
class TestNormalizeTemplateConfig:
|
||||
"""normalize_template_config 函数测试"""
|
||||
|
||||
def test_none_returns_default(self):
|
||||
result = normalize_template_config(None)
|
||||
assert result == DEFAULT_EDIT_TEMPLATE_CONFIG
|
||||
# 确保是深拷贝
|
||||
result["transition_enabled"] = False
|
||||
assert DEFAULT_EDIT_TEMPLATE_CONFIG["transition_enabled"] is True
|
||||
|
||||
def test_empty_dict_returns_default(self):
|
||||
result = normalize_template_config({})
|
||||
assert result == DEFAULT_EDIT_TEMPLATE_CONFIG
|
||||
|
||||
def test_transition_enabled_update(self):
|
||||
raw = {"transition_enabled": False}
|
||||
result = normalize_template_config(raw)
|
||||
assert result["transition_enabled"] is False
|
||||
|
||||
def test_transition_enabled_non_bool_ignored(self):
|
||||
raw = {"transition_enabled": "yes"}
|
||||
result = normalize_template_config(raw)
|
||||
# transition_enabled 在标准列表中,非bool则不更新
|
||||
# 也不会作为非标准字段保留
|
||||
assert result["transition_enabled"] is True
|
||||
|
||||
def test_partial_sections_update(self):
|
||||
raw = {
|
||||
"cover": {"type": "ai_regenerate"},
|
||||
"filter": {"enabled": True, "intensity": 75},
|
||||
"transition_enabled": False,
|
||||
"editing_mode": "pip",
|
||||
}
|
||||
result = normalize_template_config(raw)
|
||||
assert result["cover"]["type"] == "ai_regenerate"
|
||||
assert result["filter"]["enabled"] is True
|
||||
assert result["filter"]["intensity"] == 75
|
||||
assert result["transition_enabled"] is False
|
||||
assert result["editing_mode"] == "pip"
|
||||
|
||||
def test_non_standard_fields_preserved(self):
|
||||
raw = {"template_version": 3, "author": "test"}
|
||||
result = normalize_template_config(raw)
|
||||
assert result["template_version"] == 3
|
||||
assert result["author"] == "test"
|
||||
|
||||
def test_template_has_extra_field_over_plan(self):
|
||||
"""模板normalize结果比计划多transition_enabled"""
|
||||
plan_result = normalize_plan_config({"bgm": {"enabled": True}})
|
||||
template_result = normalize_template_config({"bgm": {"enabled": True}})
|
||||
assert "transition_enabled" in template_result
|
||||
assert "transition_enabled" not in plan_result
|
||||
Reference in New Issue
Block a user