365 lines
12 KiB
Python
Executable File
365 lines
12 KiB
Python
Executable File
"""video_concat 领域模型单测 — 纯逻辑,48个测试用例."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from packages.domain.video_concat import (
|
|
ALLOWED_VIDEO_EXTENSIONS,
|
|
CONCAT_DEMUXER_REQUIRED_PARAMS,
|
|
MAX_CONCAT_SEGMENTS,
|
|
ConcatConfig,
|
|
ConcatSegment,
|
|
)
|
|
|
|
# ── ConcatSegment 测试 ───────────────────────────────────────────────────────
|
|
|
|
|
|
class TestConcatSegmentBasics:
|
|
def test_default_values(self):
|
|
seg = ConcatSegment(video_path="test.mp4")
|
|
assert seg.video_path == "test.mp4"
|
|
assert seg.start_time == 0.0
|
|
assert seg.duration == 0.0
|
|
assert seg.has_audio is True
|
|
|
|
def test_full_params(self):
|
|
seg = ConcatSegment(
|
|
video_path="video.mp4",
|
|
start_time=5.5,
|
|
duration=10.0,
|
|
has_audio=False,
|
|
)
|
|
assert seg.video_path == "video.mp4"
|
|
assert seg.start_time == 5.5
|
|
assert seg.duration == 10.0
|
|
assert seg.has_audio is False
|
|
|
|
|
|
class TestConcatSegmentFromDict:
|
|
def test_normal_dict(self):
|
|
seg = ConcatSegment.from_dict(
|
|
{
|
|
"video_path": "test.mp4",
|
|
"start_time": 2.0,
|
|
"duration": 5.0,
|
|
"has_audio": False,
|
|
}
|
|
)
|
|
assert seg.video_path == "test.mp4"
|
|
assert seg.start_time == 2.0
|
|
assert seg.duration == 5.0
|
|
assert seg.has_audio is False
|
|
|
|
def test_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_none_input(self):
|
|
seg = ConcatSegment.from_dict(None)
|
|
assert seg.video_path == ""
|
|
assert seg.is_valid is False
|
|
|
|
def test_non_dict_input(self):
|
|
seg = ConcatSegment.from_dict("not a dict")
|
|
assert seg.video_path == ""
|
|
|
|
def test_start_time_negative_clamped(self):
|
|
seg = ConcatSegment.from_dict({"video_path": "a.mp4", "start_time": -5})
|
|
assert seg.start_time == 0.0
|
|
|
|
def test_duration_negative_clamped(self):
|
|
seg = ConcatSegment.from_dict({"video_path": "a.mp4", "duration": -10})
|
|
assert seg.duration == 0.0
|
|
|
|
def test_start_time_invalid_string(self):
|
|
seg = ConcatSegment.from_dict({"video_path": "a.mp4", "start_time": "abc"})
|
|
assert seg.start_time == 0.0
|
|
|
|
def test_duration_invalid_string(self):
|
|
seg = ConcatSegment.from_dict({"video_path": "a.mp4", "duration": "xyz"})
|
|
assert seg.duration == 0.0
|
|
|
|
def test_start_time_int_casted(self):
|
|
seg = ConcatSegment.from_dict({"video_path": "a.mp4", "start_time": 3})
|
|
assert seg.start_time == 3.0
|
|
|
|
def test_duration_int_casted(self):
|
|
seg = ConcatSegment.from_dict({"video_path": "a.mp4", "duration": 7})
|
|
assert seg.duration == 7.0
|
|
|
|
def test_video_path_casted_to_string(self):
|
|
seg = ConcatSegment.from_dict({"video_path": 12345})
|
|
assert seg.video_path == "12345"
|
|
|
|
def test_has_audio_false(self):
|
|
seg = ConcatSegment.from_dict({"video_path": "a.mp4", "has_audio": False})
|
|
assert seg.has_audio is False
|
|
|
|
def test_has_audio_truthy_value(self):
|
|
seg = ConcatSegment.from_dict({"video_path": "a.mp4", "has_audio": 1})
|
|
assert seg.has_audio is True
|
|
|
|
|
|
class TestConcatSegmentProperties:
|
|
def test_is_valid_with_path(self):
|
|
seg = ConcatSegment(video_path="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_effective_duration_positive(self):
|
|
seg = ConcatSegment(video_path="a.mp4", duration=10.5)
|
|
assert seg.effective_duration == 10.5
|
|
|
|
def test_effective_duration_zero(self):
|
|
seg = ConcatSegment(video_path="a.mp4", duration=0.0)
|
|
assert seg.effective_duration == 0.0
|
|
|
|
def test_effective_duration_negative(self):
|
|
seg = ConcatSegment(video_path="a.mp4", duration=-5.0)
|
|
assert seg.effective_duration == 0.0
|
|
|
|
|
|
# ── ConcatConfig 测试 ────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestConcatConfigBasics:
|
|
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_with_segments(self):
|
|
segs = [ConcatSegment(video_path="a.mp4")]
|
|
cfg = ConcatConfig(segments=segs)
|
|
assert len(cfg.segments) == 1
|
|
assert cfg.segments[0].video_path == "a.mp4"
|
|
|
|
|
|
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 == []
|
|
|
|
def test_non_dict_input(self):
|
|
cfg = ConcatConfig.from_config_dict("config")
|
|
assert cfg.segments == []
|
|
|
|
def test_with_valid_segments(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [
|
|
{"video_path": "a.mp4", "duration": 10},
|
|
{"video_path": "b.mp4", "duration": 20},
|
|
],
|
|
}
|
|
)
|
|
assert len(cfg.segments) == 2
|
|
assert cfg.segments[0].video_path == "a.mp4"
|
|
assert cfg.segments[1].video_path == "b.mp4"
|
|
|
|
def test_skips_empty_video_path(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [
|
|
{"video_path": "a.mp4"},
|
|
{"video_path": ""},
|
|
{"video_path": "b.mp4"},
|
|
],
|
|
}
|
|
)
|
|
assert len(cfg.segments) == 2
|
|
|
|
def test_skips_invalid_segment_dict(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"segments": [
|
|
{"video_path": "a.mp4"},
|
|
"not a dict",
|
|
{"video_path": "b.mp4"},
|
|
],
|
|
}
|
|
)
|
|
assert len(cfg.segments) == 2
|
|
|
|
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(
|
|
{
|
|
"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_output_width_negative_clamped(self):
|
|
cfg = ConcatConfig.from_config_dict({"output_width": -100})
|
|
assert cfg.output_width == 0
|
|
|
|
def test_output_height_invalid_string(self):
|
|
cfg = ConcatConfig.from_config_dict({"output_height": "abc"})
|
|
assert cfg.output_height == 0
|
|
|
|
def test_output_fps_invalid_string(self):
|
|
cfg = ConcatConfig.from_config_dict({"output_fps": "xyz"})
|
|
assert cfg.output_fps == 0.0
|
|
|
|
def test_transition_params(self):
|
|
cfg = ConcatConfig.from_config_dict(
|
|
{
|
|
"transition": "crossfade",
|
|
"transition_duration": 1.0,
|
|
}
|
|
)
|
|
assert cfg.transition == "crossfade"
|
|
assert cfg.transition_duration == 1.0
|
|
|
|
def test_transition_duration_minimum(self):
|
|
cfg = ConcatConfig.from_config_dict({"transition_duration": 0.01})
|
|
assert cfg.transition_duration == 0.1
|
|
|
|
def test_transition_duration_negative(self):
|
|
cfg = ConcatConfig.from_config_dict({"transition_duration": -1})
|
|
assert cfg.transition_duration == 0.1
|
|
|
|
def test_force_reencode_false_by_default(self):
|
|
cfg = ConcatConfig.from_config_dict({})
|
|
assert cfg.force_reencode is False
|
|
|
|
|
|
class TestConcatConfigProperties:
|
|
def test_has_effect_two_segments(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path="a.mp4"),
|
|
ConcatSegment(video_path="b.mp4"),
|
|
]
|
|
)
|
|
assert cfg.has_effect is True
|
|
|
|
def test_has_effect_one_segment(self):
|
|
cfg = ConcatConfig(segments=[ConcatSegment(video_path="a.mp4")])
|
|
assert cfg.has_effect is False
|
|
|
|
def test_has_effect_empty(self):
|
|
cfg = ConcatConfig()
|
|
assert cfg.has_effect is False
|
|
|
|
def test_has_effect_skips_invalid(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path="a.mp4"),
|
|
ConcatSegment(video_path=""),
|
|
ConcatSegment(video_path="b.mp4"),
|
|
]
|
|
)
|
|
assert cfg.has_effect is True
|
|
|
|
def test_valid_segment_count(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path="a.mp4"),
|
|
ConcatSegment(video_path=""),
|
|
ConcatSegment(video_path="b.mp4"),
|
|
]
|
|
)
|
|
assert cfg.valid_segment_count == 2
|
|
|
|
def test_first_valid_segment(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path=""),
|
|
ConcatSegment(video_path="first.mp4"),
|
|
ConcatSegment(video_path="second.mp4"),
|
|
]
|
|
)
|
|
assert cfg.first_valid_segment is not None
|
|
assert cfg.first_valid_segment.video_path == "first.mp4"
|
|
|
|
def test_first_valid_segment_none_when_all_empty(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path=""),
|
|
ConcatSegment(video_path=""),
|
|
]
|
|
)
|
|
assert cfg.first_valid_segment is None
|
|
|
|
def test_first_valid_segment_empty_list(self):
|
|
cfg = ConcatConfig()
|
|
assert cfg.first_valid_segment is None
|
|
|
|
def test_estimated_total_duration(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path="a.mp4", duration=10.0),
|
|
ConcatSegment(video_path="b.mp4", duration=20.0),
|
|
ConcatSegment(video_path="c.mp4", duration=0.0),
|
|
]
|
|
)
|
|
assert cfg.estimated_total_duration == 30.0
|
|
|
|
def test_estimated_total_duration_skips_invalid(self):
|
|
cfg = ConcatConfig(
|
|
segments=[
|
|
ConcatSegment(video_path="", duration=10.0),
|
|
ConcatSegment(video_path="a.mp4", duration=5.0),
|
|
]
|
|
)
|
|
assert cfg.estimated_total_duration == 5.0
|
|
|
|
|
|
class TestConcatConfigClampSegments:
|
|
def test_clamp_when_over_max(self):
|
|
segs = [ConcatSegment(video_path=f"s{i}.mp4") for i in range(100)]
|
|
cfg = ConcatConfig(segments=segs)
|
|
cfg.clamp_segments(50)
|
|
assert len(cfg.segments) == 50
|
|
assert cfg.segments[0].video_path == "s0.mp4"
|
|
assert cfg.segments[-1].video_path == "s49.mp4"
|
|
|
|
def test_no_clamp_when_under_max(self):
|
|
segs = [ConcatSegment(video_path=f"s{i}.mp4") for i in range(10)]
|
|
cfg = ConcatConfig(segments=segs)
|
|
cfg.clamp_segments(50)
|
|
assert len(cfg.segments) == 10
|
|
|
|
def test_default_max_constant(self):
|
|
assert MAX_CONCAT_SEGMENTS == 50
|
|
|
|
|
|
class TestConstants:
|
|
def test_allowed_extensions(self):
|
|
assert ".mp4" in ALLOWED_VIDEO_EXTENSIONS
|
|
assert ".mov" in ALLOWED_VIDEO_EXTENSIONS
|
|
assert ".webm" in ALLOWED_VIDEO_EXTENSIONS
|
|
|
|
def test_demuxer_params(self):
|
|
assert "codec_name" in CONCAT_DEMUXER_REQUIRED_PARAMS
|
|
assert "width" in CONCAT_DEMUXER_REQUIRED_PARAMS
|
|
assert "r_frame_rate" in CONCAT_DEMUXER_REQUIRED_PARAMS
|