test(P3-1): 第57波 worker层小工具+配置单测(+60) #849

Merged
xiaoxia merged 1 commits from test/wave57-worker-render-subtitles-and-utils into develop 2026-07-24 21:29:40 +08:00
3 changed files with 360 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
"""
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 # 两位小数(厘秒)
+61
View File
@@ -0,0 +1,61 @@
"""
缩略图生成器纯函数测试.
覆盖 _format_seek_time 等纯逻辑.
FFmpeg 抽帧与 OSS 上传由集成测试覆盖.
"""
from __future__ import annotations
import pytest
from video_processing.thumbnail_generator import _format_seek_time
class TestFormatSeekTime:
"""_format_seek_time 时间格式化."""
def test_zero(self):
assert _format_seek_time(0.0) == "00:00:00.00"
def test_seconds_only(self):
assert _format_seek_time(5.5) == "00:00:05.50"
def test_minutes(self):
assert _format_seek_time(65.25) == "00:01:05.25"
def test_hours(self):
assert _format_seek_time(3661.5) == "01:01:01.50"
def test_exact_minute(self):
assert _format_seek_time(60.0) == "00:01:00.00"
def test_exact_hour(self):
assert _format_seek_time(3600.0) == "01:00:00.00"
def test_very_short(self):
assert _format_seek_time(0.1) == "00:00:00.10"
def test_long_video(self):
# 超过1小时
assert _format_seek_time(7200.0) == "02:00:00.00"
def test_sub_second_precision(self):
result = _format_seek_time(1.234)
parts = result.split(":")
assert len(parts) == 3
sec_part = parts[2]
assert "." in sec_part
decimals = sec_part.split(".")[1]
assert len(decimals) == 2
def test_zero_padded_hours(self):
# 小时始终是2位
result = _format_seek_time(5.0)
assert result.startswith("00:")
def test_zero_padded_minutes(self):
# 分钟始终是2位
result = _format_seek_time(5.0)
parts = result.split(":")
assert len(parts[1]) == 2
+83
View File
@@ -0,0 +1,83 @@
"""
Worker 配置测试.
覆盖 WorkerSettings 默认值、属性别名等纯逻辑.
环境变量加载由集成测试覆盖.
"""
from __future__ import annotations
import pytest
from packages.config.worker_settings import WorkerSettings
class TestWorkerSettingsDefaults:
"""WorkerSettings 默认值."""
def test_worker_name_default(self):
settings = WorkerSettings()
assert settings.worker_name == "xiaoxia-saas-worker"
def test_worker_concurrency_default(self):
settings = WorkerSettings()
assert settings.worker_concurrency == 4
def test_worker_max_tasks_per_child_default(self):
settings = WorkerSettings()
assert settings.worker_max_tasks_per_child == 1000
def test_inherits_shared_settings(self):
"""继承 SharedSettings 的字段."""
settings = WorkerSettings()
# 验证至少有一些 SharedSettings 的字段存在
assert hasattr(settings, "celery_broker_url")
assert hasattr(settings, "celery_result_backend")
def test_custom_values(self):
settings = WorkerSettings(
worker_name="test-worker",
worker_concurrency=8,
worker_max_tasks_per_child=500,
)
assert settings.worker_name == "test-worker"
assert settings.worker_concurrency == 8
assert settings.worker_max_tasks_per_child == 500
class TestWorkerSettingsAliases:
"""Celery 字段名向后兼容别名."""
def test_broker_url_alias(self):
settings = WorkerSettings(celery_broker_url="redis://localhost:6379/0")
assert settings.broker_url == settings.celery_broker_url
assert settings.broker_url == "redis://localhost:6379/0"
def test_result_backend_alias(self):
settings = WorkerSettings(celery_result_backend="redis://localhost:6379/1")
assert settings.result_backend == settings.celery_result_backend
assert settings.result_backend == "redis://localhost:6379/1"
def test_broker_url_is_property(self):
"""broker_url 是 property,每次读取都返回最新值."""
settings = WorkerSettings()
# 验证是 property 描述符
assert isinstance(type(settings).broker_url, property)
assert isinstance(type(settings).result_backend, property)
class TestWorkerSettingsType:
"""类型验证."""
def test_worker_concurrency_is_int(self):
settings = WorkerSettings()
assert isinstance(settings.worker_concurrency, int)
def test_worker_max_tasks_is_int(self):
settings = WorkerSettings()
assert isinstance(settings.worker_max_tasks_per_child, int)
def test_worker_name_is_str(self):
settings = WorkerSettings()
assert isinstance(settings.worker_name, str)
assert len(settings.worker_name) > 0