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
207 lines
7.1 KiB
Python
Executable File
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]")
|