Files
xiaoxia-saas/tests/unit/test_render_subtitles.py
T
saas-backend-agent 15e429ffb1
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 24s
AI Code Review / AI Code Review (pull_request) Successful in 45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m29s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m26s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m42s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m48s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m53s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 53s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 14s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 33s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 7m39s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 288h2m19s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 288h2m20s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 288h2m21s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 288h2m20s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 288h9m17s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 288h9m18s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 288h9m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 288h9m23s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 288h9m30s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 288h9m30s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 288h9m35s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 288h9m59s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 288h9m35s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 288h9m59s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 288h10m0s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 288h10m8s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 288h36m48s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 288h43m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 288h44m2s
fix: 标题默认位置 fallback 改为 bottom,与前端 DEFAULT_TITLE_SETTINGS 对齐
- position_to_ass_alignment() fallback 从 8(top) 改为 2(bottom)
- build_ass_content() title 默认 position 从 'top' 改为 'bottom'
- subtitle_generator.generate_ass_from_timeline() title 默认 position 同步
- 更新 6 个测试文件中 fallback 断言(8→2)
- 新增 TestDefaultPositionBottom 测试类(3 个测试)
- 351 个标题/字幕相关测试全通过
2026-09-04 15:14:17 +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 # 两位小数(厘秒)