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 全绿
319 lines
9.7 KiB
Python
Executable File
319 lines
9.7 KiB
Python
Executable File
"""字幕渲染引擎单元测试 - 工具函数+样式配置等纯逻辑."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from video_processing.subtitle_render_engine import (
|
|
SubtitleStyle,
|
|
_escape_ass_text,
|
|
_format_ass_time,
|
|
_hex_to_ass_bgr,
|
|
_hex_to_ass_color,
|
|
_opacity_to_ass_alpha,
|
|
_wrap_text,
|
|
)
|
|
|
|
|
|
# ── 颜色转换测试 ──────────────────────────────────────────────
|
|
|
|
|
|
class TestHexToAssColor:
|
|
"""_hex_to_ass_color 测试."""
|
|
|
|
def test_white(self):
|
|
"""白色."""
|
|
assert _hex_to_ass_color("#FFFFFF") == "&H00FFFFFF"
|
|
|
|
def test_black(self):
|
|
"""黑色."""
|
|
assert _hex_to_ass_color("#000000") == "&H00000000"
|
|
|
|
def test_red(self):
|
|
"""红色 → BGR: 蓝绿红."""
|
|
assert _hex_to_ass_color("#FF0000") == "&H000000FF"
|
|
|
|
def test_green(self):
|
|
"""绿色."""
|
|
assert _hex_to_ass_color("#00FF00") == "&H0000FF00"
|
|
|
|
def test_blue(self):
|
|
"""蓝色."""
|
|
assert _hex_to_ass_color("#0000FF") == "&H00FF0000"
|
|
|
|
def test_no_hash_prefix(self):
|
|
"""不带#号."""
|
|
assert _hex_to_ass_color("FF0000") == "&H000000FF"
|
|
|
|
def test_invalid_length(self):
|
|
"""长度不对返回默认白色."""
|
|
assert _hex_to_ass_color("#FFF") == "&H00FFFFFF"
|
|
assert _hex_to_ass_color("#FF") == "&H00FFFFFF"
|
|
assert _hex_to_ass_color("") == "&H00FFFFFF"
|
|
|
|
def test_lowercase_input(self):
|
|
"""小写输入转为大写输出."""
|
|
assert _hex_to_ass_color("#aabbcc") == "&H00CCBBAA"
|
|
|
|
|
|
class TestHexToAssBgr:
|
|
"""_hex_to_ass_bgr 测试."""
|
|
|
|
def test_white(self):
|
|
"""白色BGR."""
|
|
assert _hex_to_ass_bgr("#FFFFFF") == "FFFFFF"
|
|
|
|
def test_red_bgr(self):
|
|
"""红色 → BGR = 0000FF."""
|
|
assert _hex_to_ass_bgr("#FF0000") == "0000FF"
|
|
|
|
def test_blue_bgr(self):
|
|
"""蓝色 → BGR = FF0000."""
|
|
assert _hex_to_ass_bgr("#0000FF") == "FF0000"
|
|
|
|
def test_invalid_length(self):
|
|
"""长度不对返回默认."""
|
|
assert _hex_to_ass_bgr("#FF") == "FFFFFF"
|
|
|
|
|
|
class TestOpacityToAssAlpha:
|
|
"""_opacity_to_ass_alpha 测试."""
|
|
|
|
def test_fully_opaque(self):
|
|
"""完全不透明 → 00."""
|
|
assert _opacity_to_ass_alpha(1.0) == "00"
|
|
|
|
def test_fully_transparent(self):
|
|
"""完全透明 → FF."""
|
|
assert _opacity_to_ass_alpha(0.0) == "FF"
|
|
|
|
def test_half(self):
|
|
"""50% → 128 → 80."""
|
|
assert _opacity_to_ass_alpha(0.5) == "80"
|
|
|
|
def test_quarter(self):
|
|
"""75%不透明 → 64 → 40."""
|
|
assert _opacity_to_ass_alpha(0.75) == "40"
|
|
|
|
|
|
# ── 文本处理测试 ──────────────────────────────────────────────
|
|
|
|
|
|
class TestEscapeAssText:
|
|
"""_escape_ass_text 转义测试."""
|
|
|
|
def test_normal_text_unchanged(self):
|
|
"""普通文本不变."""
|
|
assert _escape_ass_text("hello world") == "hello world"
|
|
|
|
def test_newline_converted(self):
|
|
"""换行转成\\N."""
|
|
assert _escape_ass_text("line1\nline2") == "line1\\Nline2"
|
|
|
|
def test_crlf_converted(self):
|
|
"""\\r\\n转成\\N."""
|
|
assert _escape_ass_text("line1\r\nline2") == "line1\\Nline2"
|
|
|
|
def test_carriage_return_converted(self):
|
|
"""\\r转成\\N."""
|
|
assert _escape_ass_text("line1\rline2") == "line1\\Nline2"
|
|
|
|
def test_curly_braces_replaced(self):
|
|
"""花括号替换成圆括号(ASS控制符)."""
|
|
assert _escape_ass_text("{text}") == "(text)"
|
|
|
|
def test_mixed_special_chars(self):
|
|
"""混合特殊字符."""
|
|
text = "hello\n{world}\r\nend"
|
|
result = _escape_ass_text(text)
|
|
assert "\\N" in result
|
|
assert "{" not in result
|
|
assert "}" not in result
|
|
assert "(world)" in result
|
|
|
|
|
|
class TestFormatAssTime:
|
|
"""_format_ass_time 时间格式化测试."""
|
|
|
|
def test_zero(self):
|
|
"""0秒."""
|
|
assert _format_ass_time(0) == "0:00:00.00"
|
|
|
|
def test_seconds_only(self):
|
|
"""只有秒."""
|
|
assert _format_ass_time(5.5) == "0:00:05.50"
|
|
|
|
def test_minutes_and_seconds(self):
|
|
"""分+秒."""
|
|
assert _format_ass_time(125.5) == "0:02:05.50"
|
|
|
|
def test_hours_minutes_seconds(self):
|
|
"""时+分+秒."""
|
|
assert _format_ass_time(3725.25) == "1:02:05.25"
|
|
|
|
def test_exactly_one_hour(self):
|
|
"""刚好1小时."""
|
|
assert _format_ass_time(3600.0) == "1:00:00.00"
|
|
|
|
def test_single_digit_minute(self):
|
|
"""分钟补零."""
|
|
result = _format_ass_time(65.0)
|
|
parts = result.split(":")
|
|
assert parts[1] == "01"
|
|
|
|
def test_always_two_decimal_places(self):
|
|
"""总是两位小数."""
|
|
result = _format_ass_time(3.0)
|
|
assert result.endswith(".00")
|
|
|
|
|
|
class TestWrapText:
|
|
"""_wrap_text 换行测试."""
|
|
|
|
def test_short_text_no_wrap(self):
|
|
"""短文本不换行."""
|
|
result = _wrap_text("hello", 10)
|
|
assert len(result) == 1
|
|
assert result[0] == "hello"
|
|
|
|
def test_exact_length_no_wrap(self):
|
|
"""刚好长度不换行."""
|
|
text = "abcdefghij" # 10 chars
|
|
result = _wrap_text(text, 10)
|
|
assert len(result) == 1
|
|
assert result[0] == text
|
|
|
|
def test_simple_wrap(self):
|
|
"""简单换行."""
|
|
text = "abcdefghijklmnopqrst" # 20 chars
|
|
result = _wrap_text(text, 10)
|
|
assert len(result) == 2
|
|
assert len(result[0]) == 10
|
|
assert len(result[1]) == 10
|
|
|
|
def test_uneven_wrap(self):
|
|
"""不均等换行."""
|
|
text = "abcdefghijklm" # 13 chars
|
|
result = _wrap_text(text, 5)
|
|
assert len(result) == 3
|
|
assert result[0] == "abcde"
|
|
assert result[1] == "fghij"
|
|
assert result[2] == "klm"
|
|
|
|
def test_chinese_text_wrap(self):
|
|
"""中文文本换行(按字符数)."""
|
|
text = "一二三四五六七八九十"
|
|
result = _wrap_text(text, 5)
|
|
assert len(result) == 2
|
|
assert result[0] == "一二三四五"
|
|
assert result[1] == "六七八九十"
|
|
|
|
|
|
# ── SubtitleStyle 测试 ────────────────────────────────────
|
|
|
|
|
|
class TestSubtitleStyleDefaults:
|
|
"""SubtitleStyle 默认值测试."""
|
|
|
|
def test_default_values(self):
|
|
"""默认值正确."""
|
|
style = SubtitleStyle()
|
|
assert style.font_size > 0
|
|
assert style.bold is False
|
|
assert style.italic is False
|
|
assert style.stroke_enabled is True
|
|
assert style.shadow_enabled is False
|
|
assert style.background_enabled is False
|
|
assert style.fade_in == 0.0
|
|
assert style.fade_out == 0.0
|
|
assert style.animation_type == "none"
|
|
|
|
|
|
class TestSubtitleStyleFromDict:
|
|
"""SubtitleStyle.from_dict 测试."""
|
|
|
|
def test_none_returns_default(self):
|
|
"""None返回默认样式."""
|
|
style = SubtitleStyle.from_dict(None)
|
|
assert isinstance(style, SubtitleStyle)
|
|
|
|
def test_empty_dict_returns_default(self):
|
|
"""空dict返回默认."""
|
|
style = SubtitleStyle.from_dict({})
|
|
assert isinstance(style, SubtitleStyle)
|
|
|
|
def test_custom_font_size(self):
|
|
"""自定义字号."""
|
|
style = SubtitleStyle.from_dict({"size": 48})
|
|
assert style.font_size == 48
|
|
|
|
def test_custom_color(self):
|
|
"""自定义颜色."""
|
|
style = SubtitleStyle.from_dict({"color": "#FF0000"})
|
|
assert style.font_color == "#FF0000"
|
|
|
|
def test_bold_enabled(self):
|
|
"""启用粗体."""
|
|
style = SubtitleStyle.from_dict({"bold": True})
|
|
assert style.bold is True
|
|
|
|
def test_stroke_disabled(self):
|
|
"""禁用描边."""
|
|
style = SubtitleStyle.from_dict({"stroke_enabled": False})
|
|
assert style.stroke_enabled is False
|
|
|
|
def test_background_enabled(self):
|
|
"""启用背景框."""
|
|
style = SubtitleStyle.from_dict({"background_enabled": True})
|
|
assert style.background_enabled is True
|
|
|
|
def test_background_opacity_clamped(self):
|
|
"""背景透明度钳制."""
|
|
style = SubtitleStyle.from_dict({
|
|
"background_enabled": True,
|
|
"background_opacity": 2.0,
|
|
})
|
|
assert style.background_opacity == 1.0
|
|
|
|
def test_invalid_position_falls_back(self):
|
|
"""无效位置回退到默认."""
|
|
style = SubtitleStyle.from_dict({"position": "invalid_pos"})
|
|
# 回退到默认位置
|
|
assert style.position is not None
|
|
|
|
def test_fade_in_non_negative(self):
|
|
"""淡入时长不能为负."""
|
|
style = SubtitleStyle.from_dict({"fade_in": -1.0})
|
|
assert style.fade_in == 0.0
|
|
|
|
def test_custom_animation(self):
|
|
"""自定义动画."""
|
|
style = SubtitleStyle.from_dict({"animation_type": "fade"})
|
|
assert style.animation_type == "fade"
|
|
|
|
|
|
class TestSubtitleStyleProperties:
|
|
"""SubtitleStyle 属性测试."""
|
|
|
|
def test_ass_font_color_format(self):
|
|
"""ass_font_color格式正确."""
|
|
style = SubtitleStyle(font_color="#FF0000")
|
|
result = style.ass_font_color
|
|
assert result.startswith("&H")
|
|
assert len(result) == 10 # &H + AABBGGRR = 10 chars
|
|
|
|
def test_ass_background_color_format(self):
|
|
"""背景颜色格式正确."""
|
|
style = SubtitleStyle(
|
|
background_enabled=True,
|
|
background_color="#000000",
|
|
background_opacity=0.5,
|
|
)
|
|
result = style.ass_background_color
|
|
assert result.startswith("&H")
|
|
|
|
def test_alignment_is_int(self):
|
|
"""alignment是整数."""
|
|
style = SubtitleStyle()
|
|
assert isinstance(style.alignment, int)
|