Files
xiaoxia-saas/tests/unit/test_video_concat.py
T
CI Bot f1db408dcd
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 23s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 7s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m19s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m24s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m23s
AI Code Review / AI Code Review (pull_request) Successful in 1m8s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 54s
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m0s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m4s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m21s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m22s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m49s
test(unit): wave88 - 清理未使用导入和变量,修复code quality
2026-07-26 22:28:54 +08:00

341 lines
12 KiB
Python
Executable File

"""video_concat 模块单元测试."""
from __future__ import annotations
import pytest
from packages.domain.video_concat import ConcatConfig, ConcatSegment
# ── ConcatSegment ──────────────────────────────────────────────────────────
class TestConcatSegment:
def test_default_values(self):
seg = ConcatSegment(video_path="/tmp/test.mp4")
assert seg.video_path == "/tmp/test.mp4"
assert seg.start_time == 0.0
assert seg.duration == 0.0
assert seg.has_audio is True
def test_full_values(self):
seg = ConcatSegment(
video_path="/tmp/test.mp4",
start_time=5.0,
duration=10.0,
has_audio=False,
)
assert seg.start_time == 5.0
assert seg.duration == 10.0
assert seg.has_audio is False
def test_is_valid_with_path(self):
seg = ConcatSegment(video_path="/tmp/test.mp4")
assert seg.is_valid is True
def test_is_valid_empty_path(self):
seg = ConcatSegment(video_path="")
assert seg.is_valid is False
def test_from_dict_basic(self):
seg = ConcatSegment.from_dict({"video_path": "/tmp/test.mp4"})
assert seg.video_path == "/tmp/test.mp4"
assert seg.start_time == 0.0
assert seg.duration == 0.0
assert seg.has_audio is True
def test_from_dict_with_all_fields(self):
seg = ConcatSegment.from_dict({
"video_path": "/tmp/test.mp4",
"start_time": 3.5,
"duration": 7.2,
"has_audio": False,
})
assert seg.video_path == "/tmp/test.mp4"
assert seg.start_time == 3.5
assert seg.duration == 7.2
assert seg.has_audio is False
def test_from_dict_negative_start_time_clamped(self):
seg = ConcatSegment.from_dict({"video_path": "x.mp4", "start_time": -5.0})
assert seg.start_time == 0.0
def test_from_dict_negative_duration_clamped(self):
seg = ConcatSegment.from_dict({"video_path": "x.mp4", "duration": -3.0})
assert seg.duration == 0.0
def test_from_dict_invalid_start_time_falls_back(self):
seg = ConcatSegment.from_dict({"video_path": "x.mp4", "start_time": "abc"})
assert seg.start_time == 0.0
def test_from_dict_invalid_duration_falls_back(self):
seg = ConcatSegment.from_dict({"video_path": "x.mp4", "duration": "abc"})
assert seg.duration == 0.0
def test_from_dict_empty_dict(self):
seg = ConcatSegment.from_dict({})
assert seg.video_path == ""
assert seg.start_time == 0.0
assert seg.duration == 0.0
assert seg.has_audio is True
def test_from_dict_path_converted_to_string(self):
seg = ConcatSegment.from_dict({"video_path": 123})
assert seg.video_path == "123"
def test_from_dict_has_audio_falsy_values(self):
seg = ConcatSegment.from_dict({"video_path": "x.mp4", "has_audio": False})
assert seg.has_audio is False
seg2 = ConcatSegment.from_dict({"video_path": "x.mp4", "has_audio": 0})
assert seg2.has_audio is False
seg3 = ConcatSegment.from_dict({"video_path": "x.mp4", "has_audio": ""})
assert seg3.has_audio is False
# ── ConcatConfig ────────────────────────────────────────────────────────────
class TestConcatConfigDefaults:
def test_default_values(self):
cfg = ConcatConfig()
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
assert cfg.transition == "none"
assert cfg.transition_duration == 0.3
def test_has_effect_no_segments(self):
cfg = ConcatConfig()
assert cfg.has_effect is False
def test_total_segments_no_segments(self):
cfg = ConcatConfig()
assert cfg.total_segments == 0
def test_valid_segments_empty(self):
cfg = ConcatConfig()
assert cfg.valid_segments == []
def test_total_duration_empty(self):
cfg = ConcatConfig()
assert cfg.total_duration == 0.0
def test_has_transition_default(self):
cfg = ConcatConfig()
assert cfg.has_transition is False
def test_auto_output_size_default(self):
cfg = ConcatConfig()
assert cfg.auto_output_size is True
class TestConcatConfigWithSegments:
def test_single_segment_no_effect(self):
cfg = ConcatConfig(segments=[ConcatSegment(video_path="a.mp4")])
assert cfg.has_effect is False
assert cfg.total_segments == 1
def test_two_segments_has_effect(self):
cfg = ConcatConfig(segments=[
ConcatSegment(video_path="a.mp4"),
ConcatSegment(video_path="b.mp4"),
])
assert cfg.has_effect is True
assert cfg.total_segments == 2
def test_invalid_segments_filtered(self):
cfg = ConcatConfig(segments=[
ConcatSegment(video_path="a.mp4"),
ConcatSegment(video_path=""),
ConcatSegment(video_path="b.mp4"),
ConcatSegment(video_path=""),
])
assert cfg.total_segments == 2
assert len(cfg.valid_segments) == 2
assert cfg.valid_segments[0].video_path == "a.mp4"
assert cfg.valid_segments[1].video_path == "b.mp4"
def test_total_duration_with_durations(self):
cfg = ConcatConfig(segments=[
ConcatSegment(video_path="a.mp4", duration=5.0),
ConcatSegment(video_path="b.mp4", duration=3.0),
ConcatSegment(video_path="c.mp4", duration=0), # 不计
])
assert cfg.total_duration == pytest.approx(8.0)
def test_has_transition_enabled(self):
cfg = ConcatConfig(transition="crossfade", transition_duration=0.5)
assert cfg.has_transition is True
def test_has_transition_none_transition(self):
cfg = ConcatConfig(transition="none", transition_duration=0.5)
assert cfg.has_transition is False
def test_has_transition_zero_duration(self):
cfg = ConcatConfig(transition="crossfade", transition_duration=0.0)
assert cfg.has_transition is False
def test_auto_output_size_with_width_only(self):
cfg = ConcatConfig(output_width=1920)
assert cfg.auto_output_size is True
def test_auto_output_size_with_height_only(self):
cfg = ConcatConfig(output_height=1080)
assert cfg.auto_output_size is True
def test_auto_output_size_false_when_both_set(self):
cfg = ConcatConfig(output_width=1920, output_height=1080)
assert cfg.auto_output_size is False
class TestConcatConfigFromDict:
def test_none_config(self):
cfg = ConcatConfig.from_config_dict(None)
assert cfg.segments == []
assert cfg.output_width == 0
def test_empty_dict(self):
cfg = ConcatConfig.from_config_dict({})
assert cfg.segments == []
assert cfg.output_width == 0
def test_not_dict_returns_default(self):
cfg = ConcatConfig.from_config_dict("not a dict")
assert cfg.segments == []
def test_with_segments(self):
cfg = ConcatConfig.from_config_dict({
"segments": [
{"video_path": "a.mp4", "duration": 5.0},
{"video_path": "b.mp4", "duration": 3.0},
]
})
assert cfg.total_segments == 2
assert cfg.segments[0].duration == 5.0
assert cfg.segments[1].duration == 3.0
def test_segments_not_list_ignored(self):
cfg = ConcatConfig.from_config_dict({"segments": "not a list"})
assert cfg.segments == []
def test_invalid_segment_skipped(self):
cfg = ConcatConfig.from_config_dict({
"segments": [
{"video_path": "a.mp4"},
"not a dict",
{"video_path": ""}, # 空路径
{"video_path": "b.mp4"},
]
})
assert cfg.total_segments == 2
def test_output_dimensions(self):
cfg = ConcatConfig.from_config_dict({
"output_width": 1920,
"output_height": 1080,
"output_fps": 30.0,
})
assert cfg.output_width == 1920
assert cfg.output_height == 1080
assert cfg.output_fps == pytest.approx(30.0)
def test_negative_dimensions_clamped(self):
cfg = ConcatConfig.from_config_dict({
"output_width": -100,
"output_height": -50,
"output_fps": -10.0,
})
assert cfg.output_width == 0
assert cfg.output_height == 0
assert cfg.output_fps == 0.0
def test_invalid_dimensions_fall_back(self):
cfg = ConcatConfig.from_config_dict({
"output_width": "abc",
"output_height": "xyz",
"output_fps": "not_a_number",
})
assert cfg.output_width == 0
assert cfg.output_height == 0
assert cfg.output_fps == 0.0
def test_force_reencode(self):
cfg = ConcatConfig.from_config_dict({"force_reencode": True})
assert cfg.force_reencode is True
def test_transition_settings(self):
cfg = ConcatConfig.from_config_dict({
"transition": "crossfade",
"transition_duration": 0.8,
})
assert cfg.transition == "crossfade"
assert cfg.transition_duration == pytest.approx(0.8)
def test_transition_duration_clamped_minimum(self):
cfg = ConcatConfig.from_config_dict({"transition_duration": 0.0})
assert cfg.transition_duration == pytest.approx(0.1)
def test_transition_duration_invalid_falls_back(self):
cfg = ConcatConfig.from_config_dict({"transition_duration": "bad"})
assert cfg.transition_duration == pytest.approx(0.3)
def test_transition_converted_to_string(self):
cfg = ConcatConfig.from_config_dict({"transition": 123})
assert cfg.transition == "123"
def test_full_config(self):
cfg = ConcatConfig.from_config_dict({
"segments": [
{"video_path": "a.mp4", "duration": 3.0},
{"video_path": "b.mp4", "duration": 4.0},
{"video_path": "c.mp4", "duration": 5.0},
],
"output_width": 1280,
"output_height": 720,
"output_fps": 25.0,
"force_reencode": True,
"transition": "crossfade",
"transition_duration": 0.4,
})
assert cfg.total_segments == 3
assert cfg.total_duration == pytest.approx(12.0)
assert cfg.output_width == 1280
assert cfg.output_height == 720
assert cfg.output_fps == 25.0
assert cfg.force_reencode is True
assert cfg.has_transition is True
assert cfg.auto_output_size is False
assert cfg.has_effect is True
class TestConcatConfigEdgeCases:
def test_segment_with_start_time_only(self):
cfg = ConcatConfig(segments=[
ConcatSegment(video_path="a.mp4", start_time=2.0),
])
assert cfg.total_segments == 1
assert cfg.segments[0].start_time == 2.0
def test_mixed_valid_invalid_long_list(self):
segments = []
for i in range(10):
if i % 2 == 0:
segments.append(ConcatSegment(video_path=f"v{i}.mp4"))
else:
segments.append(ConcatSegment(video_path=""))
cfg = ConcatConfig(segments=segments)
assert cfg.total_segments == 5
assert cfg.valid_segments[0].video_path == "v0.mp4"
assert cfg.valid_segments[-1].video_path == "v8.mp4"
def test_total_duration_mixed_zero_and_nonzero(self):
cfg = ConcatConfig(segments=[
ConcatSegment(video_path="a.mp4", duration=0),
ConcatSegment(video_path="b.mp4", duration=5.0),
ConcatSegment(video_path="c.mp4", duration=0),
ConcatSegment(video_path="d.mp4", duration=3.0),
])
assert cfg.total_duration == pytest.approx(8.0)