0bdc3924ec
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 31s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m13s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m42s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m12s
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) Successful in 37s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 22s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 24s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 36s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m17s
AI Code Review / AI Code Review (pull_request) Successful in 9m0s
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
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m39s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m14s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
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 / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m12s
- TrimConfig: from_dict解析 + validate_and_resolve三选二推导 + 边界钳制 + is_valid/is_noop/trim_from_start属性 - TrimSegment: from_dict解析 + segment_id默认值 + order字段 - ConcatConfig: from_config_dict多段解析 + has_effect/total_segments属性 - ConcatSegment: 构造 + duration计算 - SubtitleRender: hex转ASS BGR颜色 + opacity转ASS alpha + 文本转义 + 时间格式化 + 自动换行 + SubtitleStyle配置 +112 tests 全绿
272 lines
9.2 KiB
Python
Executable File
272 lines
9.2 KiB
Python
Executable File
"""裁剪引擎单元测试 - 配置解析+推导等纯逻辑."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
from video_processing.trim_engine import MIN_TRIM_DURATION, TrimConfig, TrimSegment
|
||
|
||
|
||
class TestTrimConfigFromDict:
|
||
"""TrimConfig.from_dict 解析测试."""
|
||
|
||
def test_none_returns_none(self):
|
||
"""None返回None(不裁剪)."""
|
||
assert TrimConfig.from_dict(None) is None
|
||
|
||
def test_empty_dict_returns_none(self):
|
||
"""空dict返回None."""
|
||
assert TrimConfig.from_dict({}) is None
|
||
|
||
def test_all_zero_returns_none(self):
|
||
"""全零返回None."""
|
||
assert TrimConfig.from_dict({
|
||
"start_time": 0,
|
||
"end_time": 0,
|
||
"duration": 0,
|
||
}) is None
|
||
|
||
def test_start_only(self):
|
||
"""只有start_time有效."""
|
||
config = TrimConfig.from_dict({"start_time": 5.0})
|
||
assert config is not None
|
||
assert config.start_time == 5.0
|
||
assert config.end_time == 0
|
||
assert config.duration == 0
|
||
|
||
def test_duration_only(self):
|
||
"""只有duration有效."""
|
||
config = TrimConfig.from_dict({"duration": 10.0})
|
||
assert config is not None
|
||
assert config.start_time == 0
|
||
assert config.duration == 10.0
|
||
|
||
def test_start_and_duration(self):
|
||
"""start + duration."""
|
||
config = TrimConfig.from_dict({"start_time": 2.0, "duration": 5.0})
|
||
assert config is not None
|
||
assert config.start_time == 2.0
|
||
assert config.duration == 5.0
|
||
|
||
def test_start_and_end(self):
|
||
"""start + end."""
|
||
config = TrimConfig.from_dict({"start_time": 1.0, "end_time": 4.0})
|
||
assert config is not None
|
||
assert config.start_time == 1.0
|
||
assert config.end_time == 4.0
|
||
|
||
def test_end_and_duration(self):
|
||
"""end + duration."""
|
||
config = TrimConfig.from_dict({"end_time": 10.0, "duration": 3.0})
|
||
assert config is not None
|
||
assert config.end_time == 10.0
|
||
assert config.duration == 3.0
|
||
|
||
def test_string_values_converted(self):
|
||
"""字符串值会被转换."""
|
||
config = TrimConfig.from_dict({
|
||
"start_time": "5.0",
|
||
"duration": "10.0",
|
||
})
|
||
assert config is not None
|
||
assert config.start_time == 5.0
|
||
assert config.duration == 10.0
|
||
|
||
def test_falsy_start_with_duration(self):
|
||
"""start=0 + duration>0有效."""
|
||
config = TrimConfig.from_dict({"start_time": 0, "duration": 5.0})
|
||
assert config is not None
|
||
assert config.start_time == 0.0
|
||
assert config.duration == 5.0
|
||
|
||
|
||
class TestValidateAndResolve:
|
||
"""validate_and_resolve 推导测试."""
|
||
|
||
def test_start_plus_end(self):
|
||
"""start + end → 推导duration."""
|
||
config = TrimConfig(start_time=2.0, end_time=7.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
assert resolved.start_time == 2.0
|
||
assert resolved.end_time == 7.0
|
||
assert resolved.duration == 5.0
|
||
|
||
def test_start_plus_duration(self):
|
||
"""start + duration → 推导end."""
|
||
config = TrimConfig(start_time=3.0, duration=10.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
assert resolved.start_time == 3.0
|
||
assert resolved.duration == 10.0
|
||
assert resolved.end_time == 13.0
|
||
|
||
def test_end_plus_duration(self):
|
||
"""end + duration → 推导start."""
|
||
config = TrimConfig(end_time=15.0, duration=5.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
assert resolved.end_time == 15.0
|
||
assert resolved.duration == 5.0
|
||
assert resolved.start_time == 10.0
|
||
|
||
def test_start_only_takes_to_end(self):
|
||
"""只有start → 取到素材末尾."""
|
||
config = TrimConfig(start_time=50.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
assert resolved.start_time == 50.0
|
||
assert resolved.end_time == 60.0
|
||
assert resolved.duration == 10.0
|
||
|
||
def test_end_only_takes_from_start(self):
|
||
"""只有end → 从开头取."""
|
||
config = TrimConfig(end_time=20.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
assert resolved.start_time == 0.0
|
||
assert resolved.end_time == 20.0
|
||
assert resolved.duration == 20.0
|
||
|
||
def test_end_before_start_invalid(self):
|
||
"""end < start → 无效(0时长)."""
|
||
config = TrimConfig(start_time=10.0, end_time=5.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
assert resolved.duration == 0.0
|
||
assert resolved.is_valid is False
|
||
|
||
def test_negative_start_clamped(self):
|
||
"""负start钳制到0."""
|
||
config = TrimConfig(start_time=-5.0, duration=10.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
assert resolved.start_time == 0.0
|
||
assert resolved.duration == 10.0
|
||
|
||
def test_end_beyond_asset_clamped(self):
|
||
"""end超过素材时长钳制."""
|
||
config = TrimConfig(start_time=50.0, duration=20.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
assert resolved.end_time == 60.0
|
||
assert resolved.duration == 10.0
|
||
|
||
def test_start_beyond_asset_clamped(self):
|
||
"""start超过素材时长 → 钳制到末尾保留MIN_TRIM."""
|
||
config = TrimConfig(start_time=100.0, duration=5.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
assert resolved.start_time == 60.0 - MIN_TRIM_DURATION
|
||
assert resolved.end_time == 60.0
|
||
|
||
def test_zero_asset_duration(self):
|
||
"""素材时长为0 → 不裁剪."""
|
||
config = TrimConfig(start_time=1.0, duration=5.0)
|
||
resolved = config.validate_and_resolve(0.0)
|
||
assert resolved.start_time == 0.0
|
||
assert resolved.duration == 0.0
|
||
|
||
def test_end_and_duration_with_negative_start(self):
|
||
"""end + duration推导出来负start → 钳制+重算."""
|
||
config = TrimConfig(end_time=3.0, duration=10.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
assert resolved.start_time == 0.0
|
||
assert resolved.end_time == 3.0
|
||
assert resolved.duration == 3.0
|
||
|
||
def test_all_three_params_uses_start_duration(self):
|
||
"""三个都给了,以start+duration为准."""
|
||
# 实际代码是先判断 start+end(情况1),如果都>0就用
|
||
# 所以这里测试 start+end 都给了且都>0的情况
|
||
config = TrimConfig(start_time=2.0, end_time=8.0, duration=10.0)
|
||
resolved = config.validate_and_resolve(60.0)
|
||
# 走情况1(start+end都有)
|
||
assert resolved.start_time == 2.0
|
||
assert resolved.end_time == 8.0
|
||
assert resolved.duration == 6.0
|
||
|
||
|
||
class TestIsValid:
|
||
"""is_valid 属性测试."""
|
||
|
||
def test_valid_duration(self):
|
||
"""时长足够有效."""
|
||
config = TrimConfig(start_time=0.0, end_time=0.0, duration=5.0)
|
||
assert config.is_valid is True
|
||
|
||
def test_zero_duration_invalid(self):
|
||
"""零时长无效."""
|
||
config = TrimConfig(duration=0.0)
|
||
assert config.is_valid is False
|
||
|
||
def test_min_duration_valid(self):
|
||
"""刚好等于最小值有效."""
|
||
config = TrimConfig(duration=MIN_TRIM_DURATION)
|
||
assert config.is_valid is True
|
||
|
||
|
||
class TestIsNoop:
|
||
"""is_noop 属性测试."""
|
||
|
||
def test_zero_is_noop(self):
|
||
"""全零是noop."""
|
||
config = TrimConfig()
|
||
assert config.is_noop is True
|
||
|
||
def test_with_duration_not_noop(self):
|
||
"""有时长不是noop."""
|
||
config = TrimConfig(duration=10.0)
|
||
assert config.is_noop is False
|
||
|
||
def test_with_start_not_noop(self):
|
||
"""有start不是noop."""
|
||
config = TrimConfig(start_time=5.0)
|
||
assert config.is_noop is False
|
||
|
||
|
||
class TestTrimFromStart:
|
||
"""trim_from_start 属性测试."""
|
||
|
||
def test_zero_start_is_from_start(self):
|
||
"""start=0是从开头裁."""
|
||
config = TrimConfig(start_time=0.0)
|
||
assert config.trim_from_start is True
|
||
|
||
def test_positive_start_not_from_start(self):
|
||
"""有start不是从开头裁."""
|
||
config = TrimConfig(start_time=5.0)
|
||
assert config.trim_from_start is False
|
||
|
||
|
||
class TestTrimSegment:
|
||
"""TrimSegment 测试."""
|
||
|
||
def test_from_dict_basic(self):
|
||
"""基本解析."""
|
||
seg = TrimSegment.from_dict({
|
||
"start_time": 5.0,
|
||
"duration": 10.0,
|
||
"segment_id": "seg1",
|
||
}, default_order=0)
|
||
assert seg.segment_id == "seg1"
|
||
assert seg.trim.start_time == 5.0
|
||
assert seg.trim.duration == 10.0
|
||
assert seg.order == 0
|
||
|
||
def test_from_dict_with_order(self):
|
||
"""带order的解析."""
|
||
seg = TrimSegment.from_dict({
|
||
"start_time": 1.0,
|
||
"end_time": 4.0,
|
||
"order": 2,
|
||
})
|
||
assert seg.order == 2
|
||
assert seg.trim.start_time == 1.0
|
||
assert seg.trim.end_time == 4.0
|
||
|
||
def test_from_dict_default_segment_id(self):
|
||
"""缺省segment_id时用默认值."""
|
||
seg = TrimSegment.from_dict({"duration": 5.0}, default_order=3)
|
||
assert seg.segment_id == "seg_3"
|
||
assert seg.order == 3
|
||
|
||
def test_from_dict_empty_string_segment_id(self):
|
||
"""空字符串segment_id走默认."""
|
||
seg = TrimSegment.from_dict({
|
||
"segment_id": "",
|
||
"duration": 5.0,
|
||
}, default_order=5)
|
||
assert seg.segment_id == "seg_5"
|