Files
xiaoxia-saas/tests/unit/test_ffmpeg_pure_utils.py
CI Bot 7a72cfd709
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
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 / Validate - Migration (alembic) (push) Successful in 2m9s
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 / Validate - Type Check (mypy) (push) Successful in 2m19s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m7s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m7s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m17s
CI/CD Pipeline / Frontend Lint (push) Successful in 3m37s
CI/CD Pipeline / Integration Tests (push) Successful in 2m18s
CI/CD Pipeline / Unit Tests (push) Failing after 4m59s
CI/CD Pipeline / Build Staging API Image (push) Successful in 8m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m25s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 27m2s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 30m15s
style: auto-format with black + isort + prettier
2026-07-25 00:16:10 +00:00

207 lines
7.1 KiB
Python
Executable File

"""FFmpeg工具函数纯逻辑测试 — chain_filters / resolve_xfade_transition / build_xfade_filter_chain."""
from __future__ import annotations
import pytest
from video_processing.ffmpeg_utils import (
XFADE_TRANSITION_MAP,
build_xfade_filter_chain,
chain_filters,
resolve_xfade_transition,
)
class TestChainFilters:
"""chain_filters 滤镜串联测试."""
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", "format=yuv420p"], "out")
assert result == "[0:v]scale=1280:720,fps=25,format=yuv420p[out]"
def test_empty_filters(self):
"""空滤镜列表."""
result = chain_filters([], "v0")
assert result == "[0:v][v0]"
def test_custom_input_label(self):
"""自定义输入标签."""
result = chain_filters(["scale=640:480"], "v1", input_label="1:v")
assert result == "[1:v]scale=640:480[v1]"
class TestResolveXfadeTransition:
"""resolve_xfade_transition 转场名称映射测试."""
def test_direct_match_fade(self):
"""fade直接匹配."""
assert resolve_xfade_transition("fade") == "fade"
def test_direct_match_dissolve(self):
"""dissolve直接匹配."""
assert resolve_xfade_transition("dissolve") == "dissolve"
def test_alias_crossfade(self):
"""crossfade别名→dissolve."""
assert resolve_xfade_transition("crossfade") == "dissolve"
def test_alias_slide_left(self):
"""slide_left别名→slideleft."""
assert resolve_xfade_transition("slide_left") == "slideleft"
def test_unknown_fallback_to_fade(self):
"""未知值回退到fade."""
assert resolve_xfade_transition("nonexistent_effect") == "fade"
def test_empty_string_fallback(self):
"""空字符串回退."""
assert resolve_xfade_transition("") == "fade"
def test_enum_value_support(self):
"""支持带value属性的枚举对象."""
class FakeEnum:
value = "slideup"
assert resolve_xfade_transition(FakeEnum()) == "slideup"
def test_all_map_keys_resolve(self):
"""映射表中所有key都能解析到有效值."""
for key in XFADE_TRANSITION_MAP:
result = resolve_xfade_transition(key)
assert result and isinstance(result, str)
assert result != ""
def test_cut_is_special_fallback(self):
"""cut不在映射表中→回退到fade(硬切由调用方处理)."""
# cut是特殊值,不在映射表里
result = resolve_xfade_transition("cut")
# 不在映射表里就fallback到fade
assert result == "fade"
class TestBuildXfadeFilterChain:
"""build_xfade_filter_chain 转场滤镜链构建测试."""
def test_zero_clips(self):
"""0个片段→空字符串+0时长."""
filter_str, total_dur = build_xfade_filter_chain([], [], [])
assert filter_str == ""
assert total_dur == 0.0
def test_single_clip(self):
"""1个片段→直接copy,总时长等于片段时长."""
filter_str, total_dur = build_xfade_filter_chain([10.0], ["v0"], [], output_label="outv")
assert "[v0]copy[outv]" in filter_str
assert total_dur == pytest.approx(10.0)
def test_two_clips_basic(self):
"""2个片段基本转场."""
filter_str, total_dur = build_xfade_filter_chain(
[5.0, 5.0],
["v0", "v1"],
["", "fade"],
transition_duration=0.5,
output_label="outv",
)
assert "xfade=transition=fade" in filter_str
assert "offset=" in filter_str
# 总时长 = 5 + 5 - 转场重叠
assert total_dur == pytest.approx(9.5)
def test_three_clips_chain(self):
"""3个片段形成链式转场."""
filter_str, total_dur = build_xfade_filter_chain(
[3.0, 4.0, 5.0],
["v0", "v1", "v2"],
["", "fade", "dissolve"],
transition_duration=0.5,
output_label="out",
)
# 应该有2个xfade操作
assert filter_str.count("xfade=") == 2
assert "transition=fade" in filter_str
assert "transition=dissolve" in filter_str
# 总时长 = 3+4+5 - 2*0.5 = 11
assert total_dur == pytest.approx(11.0)
def test_transition_duration_clamped_to_clip(self):
"""转场时长不能超过单个片段时长."""
filter_str, total_dur = build_xfade_filter_chain(
[2.0, 1.0],
["v0", "v1"],
["", "fade"],
transition_duration=3.0, # 比第二个片段还长
output_label="outv",
)
# 转场时长被钳制到第二个片段时长(1.0)
assert "duration=1.000" in filter_str
assert total_dur == pytest.approx(2.0) # 2 + 1 - 1 = 2
def test_very_short_clip_min_transition(self):
"""极短片段至少保留1ms转场."""
filter_str, total_dur = build_xfade_filter_chain(
[1.0, 0.0001],
["v0", "v1"],
["", "fade"],
transition_duration=0.5,
output_label="outv",
)
# 至少有1ms
assert "duration=0.001" in filter_str
def test_transition_offset_calculation(self):
"""offset计算验证."""
filter_str, _ = build_xfade_filter_chain(
[10.0, 10.0],
["v0", "v1"],
["", "fade"],
transition_duration=1.0,
output_label="outv",
)
# offset = max(0, 10 - 1*1) = 9
assert "offset=9.000" in filter_str
def test_fewer_transitions_than_clips(self):
"""转场列表比片段少时使用cut(fallback to fade)."""
filter_str, total_dur = build_xfade_filter_chain(
[5.0, 5.0, 5.0],
["v0", "v1", "v2"],
["fade"], # 只有1个转场,第2个转场缺省
transition_duration=0.5,
output_label="out",
)
# 应该有2个xfade
assert filter_str.count("xfade=") == 2
# 第二个xfade的转场是cut→fade fallback
assert filter_str.count("transition=fade") == 2
def test_output_label_final_clip(self):
"""最后一个xfade的输出标签是output_label."""
filter_str, _ = build_xfade_filter_chain(
[3.0, 4.0, 5.0],
["v0", "v1", "v2"],
["", "fade", "slideleft"],
output_label="final_v",
)
assert filter_str.rstrip().endswith("[final_v]")
def test_intermediate_labels(self):
"""中间步骤使用xf1, xf2等标签(从i=1开始计数)."""
filter_str, _ = build_xfade_filter_chain(
[2.0, 3.0, 4.0, 5.0],
["v0", "v1", "v2", "v3"],
["", "fade", "fade", "fade"],
output_label="out",
)
# 4个片段3次xfade,中间标签是xf1, xf2
assert "[xf1]" in filter_str
assert "[xf2]" in filter_str
# 最后一个是[out]
assert filter_str.rstrip().endswith("[out]")