Files
xiaoxia-saas/tests/unit/test_xfade_builder.py
xiaoxia d2c067e9bd
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m55s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 4m21s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m35s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m25s
CI/CD Pipeline / Unit Tests (push) Successful in 9m9s
CI/CD Pipeline / Integration Tests (push) Successful in 3m0s
CI/CD Pipeline / Frontend Lint (push) Successful in 2m44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m13s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m34s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m7s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 54s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m23s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m44s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
test(wave121): 抽离xfade_builder领域模型 + 77单测
2026-07-27 11:04:17 +08:00

372 lines
15 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""XFade 转场滤镜构建领域模型单元测试."""
from __future__ import annotations
import pytest
from packages.domain.xfade_builder import (
DEFAULT_TRANSITION_DURATION,
SUPPORTED_TRANSITIONS,
XFADE_TRANSITION_MAP,
XFade_TRANSITION_NAMES,
build_xfade_filter_chain,
chain_filters,
resolve_xfade_transition,
)
# ── 常量测试 ─────────────────────────────────────────────────────────────────
class TestConstants:
def test_default_transition_duration(self):
assert DEFAULT_TRANSITION_DURATION == 0.5
def test_xfade_transition_map_not_empty(self):
assert len(XFADE_TRANSITION_MAP) > 0
def test_supported_transitions(self):
assert "fade" in SUPPORTED_TRANSITIONS
assert "dissolve" in SUPPORTED_TRANSITIONS
def test_xfade_transition_names(self):
assert "fade" in XFade_TRANSITION_NAMES
assert "dissolve" in XFade_TRANSITION_NAMES
# ── chain_filters 测试 ──────────────────────────────────────────────────────
class TestChainFilters:
def test_single_filter(self):
result = chain_filters(["scale=1280:720"], "v0")
assert result == "[0:v]scale=1280:720[v0]"
def test_multiple_filters(self):
result = chain_filters(["scale=1280:720", "fps=25"], "v0")
assert result == "[0:v]scale=1280:720,fps=25[v0]"
def test_empty_filters(self):
result = chain_filters([], "out")
assert result == "[0:v][out]"
def test_custom_input_label(self):
result = chain_filters(["scale=640:480"], "out", input_label="1:v")
assert result == "[1:v]scale=640:480[out]"
def test_three_filters(self):
result = chain_filters(["trim=0:5", "setpts=PTS-STARTPTS", "fps=30"], "v1")
assert result == "[0:v]trim=0:5,setpts=PTS-STARTPTS,fps=30[v1]"
# ── resolve_xfade_transition 测试 ───────────────────────────────────────────
class TestResolveXfadeTransition:
def test_fade(self):
assert resolve_xfade_transition("fade") == "fade"
def test_dissolve(self):
assert resolve_xfade_transition("dissolve") == "dissolve"
def test_crossfade_maps_to_dissolve(self):
assert resolve_xfade_transition("crossfade") == "dissolve"
def test_slideleft(self):
assert resolve_xfade_transition("slideleft") == "slideleft"
def test_slide_left_maps_to_slideleft(self):
assert resolve_xfade_transition("slide_left") == "slideleft"
def test_slide_default_left(self):
assert resolve_xfade_transition("slide") == "slideleft"
def test_slideup(self):
assert resolve_xfade_transition("slideup") == "slideup"
def test_zoom_maps_to_zoomin(self):
assert resolve_xfade_transition("zoom") == "zoomin"
def test_zoomin(self):
assert resolve_xfade_transition("zoomin") == "zoomin"
def test_wipe_default_left(self):
assert resolve_xfade_transition("wipe") == "wipeleft"
def test_wipeup(self):
assert resolve_xfade_transition("wipeup") == "wipeup"
def test_circle_maps_to_circlecrop(self):
assert resolve_xfade_transition("circle") == "circlecrop"
def test_rect_maps_to_rectcrop(self):
assert resolve_xfade_transition("rect") == "rectcrop"
def test_unknown_falls_back_to_fade(self):
assert resolve_xfade_transition("nonexistent_effect") == "fade"
def test_empty_string_falls_back_to_fade(self):
assert resolve_xfade_transition("") == "fade"
def test_enum_with_value_attribute(self):
"""测试带 .value 属性的枚举对象."""
class FakeEnum:
def __init__(self, val):
self.value = val
assert resolve_xfade_transition(FakeEnum("fade")) == "fade"
assert resolve_xfade_transition(FakeEnum("slideleft")) == "slideleft"
assert resolve_xfade_transition(FakeEnum("unknown")) == "fade"
# ── build_xfade_filter_chain 测试 ───────────────────────────────────────────
class TestBuildXfadeFilterChain:
# ── 边界情况 ──────────────────────────────────────────────────────
def test_empty_clips(self):
result, duration = build_xfade_filter_chain([], [], [])
assert result == ""
assert duration == 0.0
def test_single_clip(self):
result, duration = build_xfade_filter_chain([10.0], ["v0"], ["none"])
assert "copy" in result
assert "[v0]copy[outv]" in result
assert duration == 10.0
def test_single_clip_custom_output_label(self):
result, duration = build_xfade_filter_chain([5.0], ["a0"], ["none"], output_label="final")
assert "[a0]copy[final]" in result
assert duration == 5.0
# ── 两片段基础测试 ────────────────────────────────────────────────
def test_two_clips_basic(self):
result, duration = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"])
assert "xfade=transition=fade" in result
assert "[v0][v1]" in result
assert "[outv]" in result
# 总时长 = 10 + 10 - 0.5 = 19.5
assert abs(duration - 19.5) < 0.01
def test_two_clips_custom_duration(self):
result, duration = build_xfade_filter_chain(
[10.0, 10.0],
["v0", "v1"],
["none", "fade"],
transition_duration=1.0,
)
assert "duration=1.000" in result
# 总时长 = 10 + 10 - 1.0 = 19.0
assert abs(duration - 19.0) < 0.01
def test_two_clips_offset(self):
"""两片段时 offset 应该为 0cumulative - td * 1 = 10 - 0.5 = 9.5?不对)。
对于两个片段:
- cumulative = clip_durations[0] = 10.0
- offset = max(0, cumulative - td * i) = max(0, 10.0 - 0.5 * 1) = 9.5
- duration=0.5, offset=9.5
"""
result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"])
assert "offset=9.500" in result
# ── 多片段测试 ────────────────────────────────────────────────────
def test_three_clips(self):
result, duration = build_xfade_filter_chain(
[5.0, 5.0, 5.0],
["v0", "v1", "v2"],
["none", "fade", "dissolve"],
)
# 应该有两个 xfade
assert result.count("xfade=") == 2
# 第一个 xfade 输出标签 xf1,第二个 xfade 输出 outv
assert "xf1" in result
assert "[outv]" in result
# 总时长 ≈ 5 + 5 + 5 - 0.5 - 0.5 = 14.0
assert abs(duration - 14.0) < 0.1
def test_five_clips(self):
result, duration = build_xfade_filter_chain(
[3.0, 3.0, 3.0, 3.0, 3.0],
["v0", "v1", "v2", "v3", "v4"],
["none", "fade", "fade", "fade", "fade"],
)
assert result.count("xfade=") == 4
# 总时长 ≈ 15 - 4 * 0.5 = 13.0
assert abs(duration - 13.0) < 0.2
# ── 转场效果测试 ──────────────────────────────────────────────────
def test_dissolve_transition(self):
result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "dissolve"])
assert "transition=dissolve" in result
def test_slideleft_transition(self):
result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "slideleft"])
assert "transition=slideleft" in result
def test_cut_uses_fade(self):
"""cut 转场效果应该回退到 fade."""
result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "cut"])
# cut 不是 XFADE_TRANSITION_MAP 的键,所以会回退到 fade
assert "transition=fade" in result
def test_transitions_shorter_than_clips(self):
"""如果 transitions 列表比 clips 短,剩余的用 'cut'(→ fade."""
result, _ = build_xfade_filter_chain(
[5.0, 5.0, 5.0],
["v0", "v1", "v2"],
["none"], # 只有一个
)
# 第二个转场(index 2)会回退到 cut → fade
assert result.count("transition=fade") == 2
# ── 时长钳制测试 ──────────────────────────────────────────────────
def test_short_first_clip_truncates_transition(self):
"""第一个片段非常短,转场时长应该被钳制."""
result, duration = build_xfade_filter_chain(
[0.3, 10.0],
["v0", "v1"],
["none", "fade"],
transition_duration=1.0,
)
# offset = max(0, 0.3 - 1.0 * 1) = 0.0
# available = max(0, 0.3 - 0.0) = 0.3
# safe_td = min(1.0, 0.3, 剩余 10.0, clip_durations[1] 10.0) = 0.3
assert "duration=0.300" in result
assert abs(duration - 10.0) < 0.01 # 0.3 + 10.0 - 0.3 = 10.0
def test_zero_duration_clips(self):
"""零时长片段的边界情况."""
result, duration = build_xfade_filter_chain([0.0, 5.0], ["v0", "v1"], ["none", "fade"])
# 第一个片段 0 时长,转场时长应该被钳制到最小值 0.001
# offset = max(0, 0 - 0.5) = 0
# available = max(0, 0 - 0) = 0
# safe_td = min(0.5, 0, ...) = min(0.5, 0, 5.0, 5.0) = 0 → max(0.001, 0) = 0.001
assert "duration=0.001" in result
def test_very_long_transition_duration(self):
"""转场时长超过所有片段时长."""
result, duration = build_xfade_filter_chain(
[2.0, 2.0],
["v0", "v1"],
["none", "fade"],
transition_duration=5.0,
)
# offset = max(0, 2.0 - 5.0) = 0
# available = max(0, 2.0 - 0) = 2.0
# safe_td = min(5.0, 2.0, 剩余 2.0, 2.0) = 2.0
assert "duration=2.000" in result
assert abs(duration - 2.0) < 0.01 # 2 + 2 - 2 = 2
# ── 标签测试 ──────────────────────────────────────────────────────
def test_custom_labels(self):
result, _ = build_xfade_filter_chain(
[10.0, 10.0],
["clip_a", "clip_b"],
["none", "fade"],
output_label="final_v",
)
assert "[clip_a][clip_b]" in result
assert "[final_v]" in result
def test_intermediate_labels_three_clips(self):
result, _ = build_xfade_filter_chain([5.0, 5.0, 5.0], ["v0", "v1", "v2"], ["none", "fade", "fade"])
# 第一个 xfade 输出 xf1
assert "[xf1][v2]" in result or result.count("[xf1]") >= 1
# ── 总时长计算验证 ────────────────────────────────────────────────
def test_total_duration_two_equal_clips(self):
_, duration = build_xfade_filter_chain([8.0, 8.0], ["v0", "v1"], ["none", "fade"])
# 8 + 8 - 0.5 = 15.5
assert abs(duration - 15.5) < 0.01
def test_total_duration_no_transition_impossible(self):
"""即使 transition_duration=0,也有最小 0.001 的钳制."""
_, duration = build_xfade_filter_chain(
[10.0, 10.0],
["v0", "v1"],
["none", "fade"],
transition_duration=0.0,
)
# transition_duration=0,但 safe_td 有下限 0.001
assert duration < 20.0 # 应该小于 20(有重叠)
assert duration > 19.9 # 但接近 20
# ── 滤镜字符串格式验证 ────────────────────────────────────────────
def test_filter_format_contains_xfade_keyword(self):
result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"])
assert "xfade=" in result
def test_filter_uses_semicolon_separator(self):
"""多步 xfade 之间用分号分隔."""
result, _ = build_xfade_filter_chain([5.0, 5.0, 5.0], ["v0", "v1", "v2"], ["none", "fade", "fade"])
assert ";" in result
# 3个片段 → 2个xfade → 1个分号
assert result.count("xfade=") == 2
assert result.count(";") == 1
def test_filter_has_transition_param(self):
result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"])
assert "transition=fade" in result
def test_filter_has_duration_param(self):
result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"])
assert "duration=" in result
def test_filter_has_offset_param(self):
result, _ = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", "fade"])
assert "offset=" in result
# ── 各种转场效果遍历测试 ──────────────────────────────────────────
@pytest.mark.parametrize(
"transition_name",
list(XFADE_TRANSITION_MAP.keys()),
)
def test_all_supported_transitions(self, transition_name):
"""所有支持的转场效果都应该能正确生成滤镜."""
result, duration = build_xfade_filter_chain([10.0, 10.0], ["v0", "v1"], ["none", transition_name])
expected = XFADE_TRANSITION_MAP[transition_name]
assert f"transition={expected}" in result
assert duration > 0
# ── 四片段复杂场景 ────────────────────────────────────────────────
def test_four_clips_different_durations(self):
durations = [3.0, 5.0, 2.0, 7.0]
result, duration = build_xfade_filter_chain(
durations,
["v0", "v1", "v2", "v3"],
["none", "fade", "dissolve", "slideleft"],
)
assert result.count("xfade=") == 3
# 总时长 = sum(durations) - 3 * 0.5 ≈ 17 - 1.5 = 15.5
assert abs(duration - 15.5) < 0.2
# ── transition_duration = 0 的边界 ───────────────────────────────
def test_zero_transition_duration_minimum_clamped(self):
result, _ = build_xfade_filter_chain(
[10.0, 10.0],
["v0", "v1"],
["none", "fade"],
transition_duration=0.0,
)
# 至少 0.001
assert "duration=0.001" in result
# ── 单片段自定义输出标签 ─────────────────────────────────────────
def test_single_clip_output_label(self):
result, _ = build_xfade_filter_chain([5.0], ["v0"], ["none"], output_label="result")
assert "[v0]copy[result]" in result