Files
xiaoxia-saas/tests/unit/test_render_subtitles.py
T
xiaoxia e86f137c3d
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 5s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 6s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 23s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 16s
CI/CD Pipeline / Build Staging API Image (push) Successful in 20s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 27s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 43s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m8s
CI/CD Pipeline / Integration Tests (push) Successful in 2m33s
CI/CD Pipeline / Validate - Style (push) Successful in 2m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m39s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m20s
CI/CD Pipeline / Validate - Security (push) Successful in 6m18s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
AI Code Review / AI Code Review (pull_request) Successful in 6m21s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m59s
CI/CD Pipeline / Unit Tests (push) Failing after 8m16s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 287h56m33s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 287h56m34s
CI/CD Pipeline / Build Production Web Image (push) Failing after 287h56m34s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 288h1m8s
CI/CD Pipeline / Deploy Production (push) Failing after 287h56m33s
CI/CD Pipeline / Build Production API Image (push) Failing after 287h56m35s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 288h2m47s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 288h2m52s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 288h2m53s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 288h2m52s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 288h3m8s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 288h3m8s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 288h3m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 288h3m11s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 288h3m13s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 288h3m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 288h3m16s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 288h3m18s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 288h3m18s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 288h3m21s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 288h3m22s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 288h3m23s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 288h3m23s
CI/CD Pipeline / Frontend Lint (push) Failing after 288h4m52s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 288h4m55s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 288h4m55s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 288h4m58s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 288h4m55s
CI/CD Pipeline / PR Build API Image (push) Failing after 288h4m58s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 288h4m59s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 288h5m2s
CI/CD Pipeline / CI Gate (push) Failing after 288h31m2s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 288h37m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 288h37m27s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 288h37m45s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 288h37m47s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 288h37m50s
CI/CD Pipeline / PR Build Web Image (push) Failing after 288h39m26s
fix: 标题默认位置 fallback 改为 bottom,与前端 DEFAULT_TITLE_SETTINGS 对齐 (#1691)
2026-09-04 15:19:34 +08:00

216 lines
6.4 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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_bottom_default(self):
assert _position_to_ass_alignment("unknown") == 2
assert _position_to_ass_alignment("") == 2
assert _position_to_ass_alignment("left") == 2
assert _position_to_ass_alignment(None) == 2
class TestBuildAssStyle:
"""构建 ASS Style 行."""
def test_basic_style(self):
style = _build_ass_style("Default")
assert style.startswith("Style: Default,")
assert "Noto Sans SC" in style
assert "65" in style # font_size 48*1.35=65
def test_custom_font(self):
style = _build_ass_style("Custom", font_name="Arial", font_size=32)
assert "Arial" in style
assert ",43," in style # font_size 32*1.35=43
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 # 两位小数(厘秒)