2df7bc9dc8
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 1m41s
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 / Build Staging Web Image (push) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m34s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m49s
CI/CD Pipeline / Frontend Lint (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Unit Tests (push) Failing after 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m10s
272 lines
8.1 KiB
Python
Executable File
272 lines
8.1 KiB
Python
Executable File
"""
|
|
视频拼接引擎配置与纯逻辑测试.
|
|
|
|
覆盖 ConcatSegment.from_dict / ConcatConfig.from_config_dict / has_effect / total_segments 等纯逻辑.
|
|
引擎核心 render 方法依赖 FFmpeg,由集成测试覆盖.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from video_processing.concat_engine import ConcatConfig, ConcatSegment
|
|
|
|
|
|
class TestConcatSegmentFromDict:
|
|
"""ConcatSegment.from_dict 构造逻辑."""
|
|
|
|
def test_basic(self):
|
|
seg = ConcatSegment.from_dict({"video_path": "/tmp/a.mp4"})
|
|
assert seg.video_path == "/tmp/a.mp4"
|
|
assert seg.start_time == 0.0
|
|
assert seg.duration == 0.0
|
|
assert seg.has_audio is True
|
|
|
|
def test_full_fields(self):
|
|
seg = ConcatSegment.from_dict(
|
|
{
|
|
"video_path": "/tmp/b.mp4",
|
|
"start_time": 5.5,
|
|
"duration": 10.0,
|
|
"has_audio": False,
|
|
}
|
|
)
|
|
assert seg.video_path == "/tmp/b.mp4"
|
|
assert seg.start_time == 5.5
|
|
assert seg.duration == 10.0
|
|
assert seg.has_audio is False
|
|
|
|
def test_negative_start_time_clamped(self):
|
|
seg = ConcatSegment.from_dict(
|
|
{
|
|
"video_path": "/tmp/a.mp4",
|
|
"start_time": -1.0,
|
|
}
|
|
)
|
|
assert seg.start_time == 0.0
|
|
|
|
def test_negative_duration_clamped(self):
|
|
seg = ConcatSegment.from_dict(
|
|
{
|
|
"video_path": "/tmp/a.mp4",
|
|
"duration": -5.0,
|
|
}
|
|
)
|
|
assert seg.duration == 0.0
|
|
|
|
def test_invalid_start_time_type_falls_back(self):
|
|
seg = ConcatSegment.from_dict(
|
|
{
|
|
"video_path": "/tmp/a.mp4",
|
|
"start_time": "not_a_number",
|
|
}
|
|
)
|
|
assert seg.start_time == 0.0
|
|
|
|
def test_invalid_duration_type_falls_back(self):
|
|
seg = ConcatSegment.from_dict(
|
|
{
|
|
"video_path": "/tmp/a.mp4",
|
|
"duration": "abc",
|
|
}
|
|
)
|
|
assert seg.duration == 0.0
|
|
|
|
def test_start_time_none_falls_back(self):
|
|
seg = ConcatSegment.from_dict(
|
|
{
|
|
"video_path": "/tmp/a.mp4",
|
|
"start_time": None,
|
|
}
|
|
)
|
|
assert seg.start_time == 0.0
|
|
|
|
def test_empty_video_path_stored(self):
|
|
seg = ConcatSegment.from_dict({"video_path": ""})
|
|
assert seg.video_path == ""
|
|
|
|
|
|
class TestConcatConfigFromConfigDict:
|
|
"""ConcatConfig.from_config_dict 构造逻辑."""
|
|
|
|
def test_none_returns_default(self):
|
|
cfg = ConcatConfig.from_config_dict(None)
|
|
assert cfg.segments == []
|
|
assert cfg.output_width == 0
|
|
assert cfg.output_height == 0
|
|
assert cfg.output_fps == 0.0
|
|
assert cfg.force_reencode is False
|
|
|
|
def test_empty_dict_returns_default(self):
|
|
cfg = ConcatConfig.from_config_dict({})
|
|
assert cfg.segments == []
|
|
|
|
def test_non_dict_returns_default(self):
|
|
cfg = ConcatConfig.from_config_dict("not a dict")
|
|
assert cfg.segments == []
|
|
|
|
def test_single_segment(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [
|
|
{"video_path": "/tmp/a.mp4", "duration": 5.0},
|
|
],
|
|
}
|
|
)
|
|
assert len(cfg.segments) == 1
|
|
assert cfg.segments[0].video_path == "/tmp/a.mp4"
|
|
assert cfg.segments[0].duration == 5.0
|
|
|
|
def test_multiple_segments(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [
|
|
{"video_path": "/tmp/a.mp4"},
|
|
{"video_path": "/tmp/b.mp4", "start_time": 2.0},
|
|
{"video_path": "/tmp/c.mp4", "duration": 3.0, "has_audio": False},
|
|
],
|
|
}
|
|
)
|
|
assert len(cfg.segments) == 3
|
|
assert cfg.segments[0].video_path == "/tmp/a.mp4"
|
|
assert cfg.segments[1].start_time == 2.0
|
|
assert cfg.segments[2].has_audio is False
|
|
|
|
def test_invalid_segments_filtered(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [
|
|
{"video_path": "/tmp/valid.mp4"},
|
|
{"video_path": ""}, # 空路径被过滤
|
|
{"not_video_path": "xxx"}, # 没有video_path被过滤
|
|
"not_a_dict", # 不是dict被过滤
|
|
None, # None被过滤
|
|
],
|
|
}
|
|
)
|
|
assert len(cfg.segments) == 1
|
|
assert cfg.segments[0].video_path == "/tmp/valid.mp4"
|
|
|
|
def test_segments_not_a_list(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": "not_a_list",
|
|
}
|
|
)
|
|
assert cfg.segments == []
|
|
|
|
def test_output_params(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [],
|
|
"output_width": 1920,
|
|
"output_height": 1080,
|
|
"output_fps": 30.0,
|
|
"force_reencode": True,
|
|
}
|
|
)
|
|
assert cfg.output_width == 1920
|
|
assert cfg.output_height == 1080
|
|
assert cfg.output_fps == 30.0
|
|
assert cfg.force_reencode is True
|
|
|
|
def test_negative_output_params_clamped(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [],
|
|
"output_width": -100,
|
|
"output_height": -50,
|
|
"output_fps": -1.0,
|
|
}
|
|
)
|
|
assert cfg.output_width == 0
|
|
assert cfg.output_height == 0
|
|
assert cfg.output_fps == 0.0
|
|
|
|
def test_invalid_output_params_fall_back(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [],
|
|
"output_width": "abc",
|
|
"output_height": None,
|
|
"output_fps": "xyz",
|
|
}
|
|
)
|
|
assert cfg.output_width == 0
|
|
assert cfg.output_height == 0
|
|
assert cfg.output_fps == 0.0
|
|
|
|
def test_transition_config(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [],
|
|
"transition": "crossfade",
|
|
"transition_duration": 1.0,
|
|
}
|
|
)
|
|
assert cfg.transition == "crossfade"
|
|
assert cfg.transition_duration == 1.0
|
|
|
|
def test_transition_duration_minimum(self):
|
|
"""transition_duration 不能小于 0.1."""
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [],
|
|
"transition_duration": 0.01,
|
|
}
|
|
)
|
|
assert cfg.transition_duration >= 0.1
|
|
|
|
def test_default_values(self):
|
|
cfg = ConcatConfig.from_config_dict({"segments": []})
|
|
assert cfg.transition == "none"
|
|
assert cfg.transition_duration == 0.3
|
|
assert cfg.force_reencode is False
|
|
|
|
|
|
class TestConcatConfigProperties:
|
|
"""has_effect / total_segments 属性."""
|
|
|
|
def test_has_effect_two_or_more_valid(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path="/tmp/a.mp4"),
|
|
ConcatSegment(video_path="/tmp/b.mp4"),
|
|
]
|
|
)
|
|
assert cfg.has_effect is True
|
|
|
|
def test_no_effect_one_segment(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path="/tmp/a.mp4"),
|
|
]
|
|
)
|
|
assert cfg.has_effect is False
|
|
|
|
def test_no_effect_zero_segments(self):
|
|
cfg = ConcatConfig(segments=[])
|
|
assert cfg.has_effect is False
|
|
|
|
def test_no_effect_empty_paths(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path=""),
|
|
ConcatSegment(video_path=""),
|
|
]
|
|
)
|
|
assert cfg.has_effect is False
|
|
|
|
def test_total_segments(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path="/tmp/a.mp4"),
|
|
ConcatSegment(video_path=""),
|
|
ConcatSegment(video_path="/tmp/b.mp4"),
|
|
]
|
|
)
|
|
assert cfg.total_segments == 2
|
|
|
|
def test_total_segments_empty(self):
|
|
cfg = ConcatConfig(segments=[])
|
|
assert cfg.total_segments == 0
|