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
216 lines
6.3 KiB
Python
Executable File
216 lines
6.3 KiB
Python
Executable File
"""
|
||
render_subtitles ASS 字幕纯函数测试.
|
||
|
||
覆盖 _hex_to_ass_color / _position_to_ass_alignment / _build_ass_style / _escape_ass_text / _format_ass_time 等纯逻辑.
|
||
文件生成与 FFmpeg 渲染由集成测试覆盖.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from video_processing.render_subtitles import (
|
||
_build_ass_style,
|
||
_escape_ass_text,
|
||
_format_ass_time,
|
||
_hex_to_ass_color,
|
||
_position_to_ass_alignment,
|
||
)
|
||
|
||
|
||
class TestHexToAssColor:
|
||
"""HEX → ASS 颜色转换(不含 alpha 前缀版本)."""
|
||
|
||
def test_white(self):
|
||
assert _hex_to_ass_color("#FFFFFF") == "&HFFFFFF"
|
||
|
||
def test_black(self):
|
||
assert _hex_to_ass_color("#000000") == "&H000000"
|
||
|
||
def test_red(self):
|
||
# #FF0000 → R=FF, G=00, B=00 → BGR=0000FF
|
||
assert _hex_to_ass_color("#FF0000") == "&H0000FF"
|
||
|
||
def test_blue(self):
|
||
# #0000FF → R=00, G=00, B=FF → BGR=FF0000
|
||
assert _hex_to_ass_color("#0000FF") == "&HFF0000"
|
||
|
||
def test_green(self):
|
||
# #00FF00 → R=00, G=FF, B=00 → BGR=00FF00
|
||
assert _hex_to_ass_color("#00FF00") == "&H00FF00"
|
||
|
||
def test_without_hash(self):
|
||
assert _hex_to_ass_color("FF0000") == "&H0000FF"
|
||
|
||
def test_lowercase(self):
|
||
assert _hex_to_ass_color("#ff0000") == "&H0000FF"
|
||
|
||
def test_invalid_length_returns_default(self):
|
||
assert _hex_to_ass_color("#FFF") == "&H000000" # 3位
|
||
assert _hex_to_ass_color("") == "&H000000" # 空
|
||
|
||
def test_mixed_case(self):
|
||
result = _hex_to_ass_color("#aBcDeF")
|
||
assert result == "&HEFCDAB"
|
||
|
||
|
||
class TestPositionToAssAlignment:
|
||
"""位置 → ASS 对齐编号映射."""
|
||
|
||
def test_top(self):
|
||
assert _position_to_ass_alignment("top") == 8
|
||
|
||
def test_center(self):
|
||
assert _position_to_ass_alignment("center") == 5
|
||
|
||
def test_bottom(self):
|
||
assert _position_to_ass_alignment("bottom") == 2
|
||
|
||
def test_unknown_returns_top_default(self):
|
||
assert _position_to_ass_alignment("unknown") == 8
|
||
assert _position_to_ass_alignment("") == 8
|
||
assert _position_to_ass_alignment("left") == 8
|
||
assert _position_to_ass_alignment(None) == 8
|
||
|
||
|
||
class TestBuildAssStyle:
|
||
"""构建 ASS Style 行."""
|
||
|
||
def test_basic_style(self):
|
||
style = _build_ass_style("Default")
|
||
assert style.startswith("Style: Default,")
|
||
assert "思源黑体" in style
|
||
assert "48" in style # font_size
|
||
|
||
def test_custom_font(self):
|
||
style = _build_ass_style("Custom", font_name="Arial", font_size=32)
|
||
assert "Arial" in style
|
||
assert ",32," in style
|
||
|
||
def test_bold(self):
|
||
style = _build_ass_style("Bold", bold=True)
|
||
assert ",-1," in style # bold = -1 (true)
|
||
|
||
def test_not_bold(self):
|
||
style = _build_ass_style("Normal", bold=False)
|
||
parts = style.split(",")
|
||
# Bold 是第 8 个字段(index 7)
|
||
assert parts[7] == "0"
|
||
|
||
def test_italic(self):
|
||
style = _build_ass_style("Italic", italic=True)
|
||
parts = style.split(",")
|
||
# Italic 是第 9 个字段(index 8)
|
||
assert parts[8] == "-1"
|
||
|
||
def test_alignment(self):
|
||
style = _build_ass_style("Bottom", alignment=2)
|
||
parts = style.split(",")
|
||
# Alignment 是第 19 个字段(index 18)
|
||
assert parts[18] == "2"
|
||
|
||
def test_margins(self):
|
||
style = _build_ass_style(
|
||
"Margins",
|
||
margin_v=80,
|
||
margin_l=60,
|
||
margin_r=60,
|
||
)
|
||
parts = style.split(",")
|
||
# MarginL = parts[19], MarginR = parts[20], MarginV = parts[21]
|
||
assert parts[19] == "60"
|
||
assert parts[20] == "60"
|
||
assert parts[21] == "80"
|
||
|
||
def test_outline_width(self):
|
||
style = _build_ass_style("Outline", outline_width=3.0)
|
||
parts = style.split(",")
|
||
# Outline 是第 17 个字段(index 16)
|
||
assert parts[16] == "3.0"
|
||
|
||
def test_shadow_with_blur(self):
|
||
style = _build_ass_style(
|
||
"Shadow",
|
||
shadow_blur=1.0,
|
||
shadow_offset=(2, 3),
|
||
)
|
||
parts = style.split(",")
|
||
# Shadow 是第 18 个字段(index 17)
|
||
assert parts[17] == "3" # shadow_offset[1]
|
||
|
||
def test_shadow_without_blur(self):
|
||
style = _build_ass_style(
|
||
"NoShadow",
|
||
shadow_blur=0.0,
|
||
shadow_offset=(2, 3),
|
||
)
|
||
parts = style.split(",")
|
||
assert parts[17] == "0" # 无模糊时阴影深度为0
|
||
|
||
def test_style_format_has_correct_field_count(self):
|
||
"""ASS Style 行应该有 23 个字段."""
|
||
style = _build_ass_style("Test")
|
||
parts = style.split(",")
|
||
assert len(parts) >= 22 # 至少22个字段(Format定义的)
|
||
|
||
|
||
class TestEscapeAssText:
|
||
"""ASS 文本转义."""
|
||
|
||
def test_plain_text(self):
|
||
assert _escape_ass_text("hello") == "hello"
|
||
|
||
def test_newline_unix(self):
|
||
assert _escape_ass_text("a\nb") == "a\\Nb"
|
||
|
||
def test_newline_windows(self):
|
||
assert _escape_ass_text("a\r\nb") == "a\\Nb"
|
||
|
||
def test_newline_mac(self):
|
||
assert _escape_ass_text("a\rb") == "a\\Nb"
|
||
|
||
def test_curly_braces(self):
|
||
assert _escape_ass_text("{text}") == "(text)"
|
||
|
||
def test_multiple_braces(self):
|
||
assert _escape_ass_text("{a}b{c}") == "(a)b(c)"
|
||
|
||
def test_mixed_special_chars(self):
|
||
result = _escape_ass_text("line1\n{bold}\nline3")
|
||
assert "\\N" in result
|
||
assert "(bold)" in result
|
||
assert "{" not in result
|
||
|
||
def test_empty(self):
|
||
assert _escape_ass_text("") == ""
|
||
|
||
|
||
class TestFormatAssTime:
|
||
"""秒 → ASS 时间格式."""
|
||
|
||
def test_zero(self):
|
||
assert _format_ass_time(0.0) == "0:00:00.00"
|
||
|
||
def test_seconds(self):
|
||
assert _format_ass_time(5.5) == "0:00:05.50"
|
||
|
||
def test_minutes(self):
|
||
assert _format_ass_time(65.25) == "0:01:05.25"
|
||
|
||
def test_hours(self):
|
||
assert _format_ass_time(3661.5) == "1:01:01.50"
|
||
|
||
def test_exact_minute(self):
|
||
assert _format_ass_time(60.0) == "0:01:00.00"
|
||
|
||
def test_exact_hour(self):
|
||
assert _format_ass_time(3600.0) == "1:00:00.00"
|
||
|
||
def test_sub_second_precision(self):
|
||
result = _format_ass_time(1.234)
|
||
parts = result.split(":")
|
||
sec_part = parts[2]
|
||
decimals = sec_part.split(".")[1]
|
||
assert len(decimals) == 2 # 两位小数(厘秒)
|