c575a4b835
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m21s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 1m55s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m52s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m27s
CI/CD Pipeline / Unit Tests (push) Failing after 6m39s
CI/CD Pipeline / Integration Tests (push) Successful in 2m41s
CI/CD Pipeline / Frontend Lint (push) Successful in 44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m0s
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 / Build Staging API Image (push) Successful in 15m23s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m49s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m11s
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 / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 5s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m11s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m28s
285 lines
9.7 KiB
Python
285 lines
9.7 KiB
Python
"""
|
|
贴纸引擎配置与纯逻辑测试.
|
|
|
|
覆盖 ImageStickerConfig / TextStickerConfig / _resolve_position / 常量与便捷函数.
|
|
引擎核心滤镜生成与渲染依赖 FFmpeg,由集成测试覆盖.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from video_processing.sticker_engine import (
|
|
POSITION_PRESETS,
|
|
STICKER_CATEGORIES,
|
|
ImageStickerConfig,
|
|
StickerEngine,
|
|
StickerOverlayResult,
|
|
TextStickerConfig,
|
|
get_sticker_categories,
|
|
parse_stickers_from_config,
|
|
)
|
|
|
|
|
|
class TestStickerConstants:
|
|
"""常量测试."""
|
|
|
|
def test_nine_position_presets(self):
|
|
assert len(POSITION_PRESETS) == 9
|
|
assert "top_left" in POSITION_PRESETS
|
|
assert "top_center" in POSITION_PRESETS
|
|
assert "top_right" in POSITION_PRESETS
|
|
assert "center_left" in POSITION_PRESETS
|
|
assert "center" in POSITION_PRESETS
|
|
assert "center_right" in POSITION_PRESETS
|
|
assert "bottom_left" in POSITION_PRESETS
|
|
assert "bottom_center" in POSITION_PRESETS
|
|
assert "bottom_right" in POSITION_PRESETS
|
|
|
|
def test_position_values_are_fractions(self):
|
|
for name, (x, y) in POSITION_PRESETS.items():
|
|
assert 0.0 <= x <= 1.0, f"{name} x={x} out of range"
|
|
assert 0.0 <= y <= 1.0, f"{name} y={y} out of range"
|
|
|
|
def test_sticker_categories(self):
|
|
assert len(STICKER_CATEGORIES) >= 3
|
|
for cat_id, cat_name in STICKER_CATEGORIES:
|
|
assert isinstance(cat_id, str)
|
|
assert isinstance(cat_name, str)
|
|
assert len(cat_id) > 0
|
|
assert len(cat_name) > 0
|
|
|
|
|
|
class TestImageStickerConfig:
|
|
"""图片贴纸配置."""
|
|
|
|
def test_default_values(self):
|
|
cfg = ImageStickerConfig()
|
|
assert cfg.enabled is False
|
|
assert cfg.type == "image"
|
|
assert cfg.position == "top_right"
|
|
assert cfg.x is None
|
|
assert cfg.y is None
|
|
assert cfg.x_unit == "percent"
|
|
assert cfg.y_unit == "percent"
|
|
assert cfg.scale == 1.0
|
|
assert cfg.width is None
|
|
assert cfg.height is None
|
|
assert cfg.opacity == 1.0
|
|
assert cfg.start_time == 0.0
|
|
assert cfg.duration == 0.0
|
|
assert cfg.fade_in == 0.0
|
|
assert cfg.fade_out == 0.0
|
|
assert cfg.z_index == 10
|
|
assert cfg.image_url == ""
|
|
assert cfg.preset_id == ""
|
|
|
|
def test_custom_values(self):
|
|
cfg = ImageStickerConfig(
|
|
enabled=True,
|
|
position="center",
|
|
x=50.0,
|
|
y=30.0,
|
|
scale=0.5,
|
|
opacity=0.8,
|
|
start_time=1.0,
|
|
duration=5.0,
|
|
fade_in=0.5,
|
|
fade_out=0.5,
|
|
z_index=5,
|
|
image_url="/tmp/sticker.png",
|
|
preset_id="sticker_001",
|
|
)
|
|
assert cfg.enabled is True
|
|
assert cfg.position == "center"
|
|
assert cfg.x == 50.0
|
|
assert cfg.y == 30.0
|
|
assert cfg.scale == 0.5
|
|
assert cfg.opacity == 0.8
|
|
assert cfg.start_time == 1.0
|
|
assert cfg.duration == 5.0
|
|
assert cfg.z_index == 5
|
|
assert cfg.image_url == "/tmp/sticker.png"
|
|
|
|
|
|
class TestTextStickerConfig:
|
|
"""文字贴纸配置."""
|
|
|
|
def test_default_values(self):
|
|
cfg = TextStickerConfig()
|
|
assert cfg.enabled is False
|
|
assert cfg.type == "text"
|
|
assert cfg.text == ""
|
|
assert cfg.font_size == 36
|
|
assert cfg.font_color == "#FFFFFF"
|
|
assert cfg.font_family == "sans"
|
|
assert cfg.stroke_color == "#000000"
|
|
assert cfg.stroke_width == 2
|
|
assert cfg.shadow_color == "#000000"
|
|
assert cfg.shadow_x == 2
|
|
assert cfg.shadow_y == 2
|
|
assert cfg.shadow_alpha == 0.5
|
|
assert cfg.position == "center"
|
|
assert cfg.z_index == 10
|
|
assert cfg.bg_color == ""
|
|
assert cfg.bg_padding == 8
|
|
assert cfg.bg_alpha == 0.8
|
|
assert cfg.bg_corner_radius == 8
|
|
|
|
def test_custom_text_sticker(self):
|
|
cfg = TextStickerConfig(
|
|
enabled=True,
|
|
text="Hello",
|
|
font_size=48,
|
|
font_color="#FF0000",
|
|
position="bottom_center",
|
|
bg_color="#000000",
|
|
bg_padding=16,
|
|
)
|
|
assert cfg.enabled is True
|
|
assert cfg.text == "Hello"
|
|
assert cfg.font_size == 48
|
|
assert cfg.font_color == "#FF0000"
|
|
assert cfg.position == "bottom_center"
|
|
assert cfg.bg_color == "#000000"
|
|
assert cfg.bg_padding == 16
|
|
|
|
|
|
class TestStickerOverlayResult:
|
|
"""贴纸叠加结果."""
|
|
|
|
def test_default_values(self):
|
|
result = StickerOverlayResult(filter_str="overlay=10:20", output_label="[out]")
|
|
assert result.filter_str == "overlay=10:20"
|
|
assert result.output_label == "[out]"
|
|
assert result.extra_inputs == []
|
|
|
|
def test_with_extra_inputs(self):
|
|
result = StickerOverlayResult(
|
|
filter_str="overlay=0:0",
|
|
output_label="[out]",
|
|
extra_inputs=["/tmp/sticker.png"],
|
|
)
|
|
assert len(result.extra_inputs) == 1
|
|
assert result.extra_inputs[0] == "/tmp/sticker.png"
|
|
|
|
|
|
class TestResolvePosition:
|
|
"""_resolve_position 位置解析."""
|
|
|
|
def test_top_left_preset(self):
|
|
cfg = ImageStickerConfig(position="top_left")
|
|
x, y = StickerEngine._resolve_position(cfg, 1000, 500, 100, 50)
|
|
# top_left: (0.05, 0.05) → x = 0.05*1000 - 50 = 0, y = 0.05*500 - 25 = 0
|
|
assert x >= 0
|
|
assert y >= 0
|
|
|
|
def test_center_preset(self):
|
|
cfg = ImageStickerConfig(position="center")
|
|
x, y = StickerEngine._resolve_position(cfg, 1000, 500, 200, 100)
|
|
# center: (0.5, 0.5) → x = 500 - 100 = 400, y = 250 - 50 = 200
|
|
assert x == 400.0
|
|
assert y == 200.0
|
|
|
|
def test_bottom_right_preset(self):
|
|
cfg = ImageStickerConfig(position="bottom_right")
|
|
x, y = StickerEngine._resolve_position(cfg, 1000, 500, 100, 50)
|
|
# bottom_right: (0.95, 0.95) → x = 950 - 50 = 900, y = 475 - 25 = 450
|
|
assert x == 900.0
|
|
assert y == 450.0
|
|
|
|
def test_custom_percent_position(self):
|
|
cfg = ImageStickerConfig(position="center", x=25.0, y=75.0, x_unit="percent", y_unit="percent")
|
|
x, y = StickerEngine._resolve_position(cfg, 1000, 500, 100, 50)
|
|
# x = 0.25*1000 - 50 = 200, y = 0.75*500 - 25 = 350
|
|
assert x == 200.0
|
|
assert y == 350.0
|
|
|
|
def test_custom_pixel_position(self):
|
|
cfg = ImageStickerConfig(position="center", x=300, y=200, x_unit="pixel", y_unit="pixel")
|
|
x, y = StickerEngine._resolve_position(cfg, 1000, 500, 100, 50)
|
|
# x = 300/1000*1000 - 50 = 250, y = 200/500*500 - 25 = 175
|
|
# 等等,让我重新算:px = config.x / canvas_w = 300/1000 = 0.3
|
|
# x = px * canvas_w - sticker_w/2 = 0.3*1000 - 50 = 300 - 50 = 250
|
|
assert x == 250.0
|
|
assert y == 175.0
|
|
|
|
def test_zero_size_sticker(self):
|
|
cfg = ImageStickerConfig(position="center")
|
|
x, y = StickerEngine._resolve_position(cfg, 1000, 500, 0, 0)
|
|
# 贴纸尺寸为0时,位置就是中心点
|
|
assert x == 500.0
|
|
assert y == 250.0
|
|
|
|
def test_invalid_position_falls_back_to_center(self):
|
|
cfg = ImageStickerConfig(position="invalid_position")
|
|
x, y = StickerEngine._resolve_position(cfg, 1000, 500, 100, 50)
|
|
# 无效位置 → 默认居中 → x = 500 - 50 = 450, y = 250 - 25 = 225
|
|
assert x == 450.0
|
|
assert y == 225.0
|
|
|
|
def test_position_clamped_to_canvas(self):
|
|
# 贴纸太大,位置被钳制
|
|
cfg = ImageStickerConfig(position="top_left")
|
|
x, y = StickerEngine._resolve_position(cfg, 100, 100, 200, 200)
|
|
# 贴纸比画布还大,应该被钳制到 0
|
|
assert x >= 0
|
|
assert y >= 0
|
|
assert x <= 100
|
|
assert y <= 100
|
|
|
|
def test_zero_canvas_handling(self):
|
|
cfg = ImageStickerConfig(position="center", x=50, y=50, x_unit="pixel", y_unit="pixel")
|
|
x, y = StickerEngine._resolve_position(cfg, 0, 0, 10, 10)
|
|
# 画布为0时不应崩溃,结果被钳制到0
|
|
assert x == 0
|
|
assert y == 0
|
|
|
|
def test_text_sticker_position(self):
|
|
cfg = TextStickerConfig(position="top_right")
|
|
x, y = StickerEngine._resolve_position(cfg, 800, 400, 100, 30)
|
|
# top_right: (0.95, 0.05) → x = 760 - 50 = 710, 但被钳制到 canvas_w - sticker_w = 700
|
|
# y = 20 - 15 = 5
|
|
assert x == 700.0
|
|
assert y == 5.0
|
|
|
|
|
|
class TestParseStickersFromConfig:
|
|
"""parse_stickers_from_config 便捷函数."""
|
|
|
|
def test_none_returns_empty(self):
|
|
assert parse_stickers_from_config(None) == []
|
|
|
|
def test_empty_dict_returns_empty(self):
|
|
assert parse_stickers_from_config({}) == []
|
|
|
|
def test_no_stickers_key_returns_empty(self):
|
|
assert parse_stickers_from_config({"other": "data"}) == []
|
|
|
|
def test_stickers_list_returned(self):
|
|
stickers = [{"type": "image", "url": "/a.png"}, {"type": "text", "text": "hi"}]
|
|
result = parse_stickers_from_config({"stickers": stickers})
|
|
assert result == stickers
|
|
assert len(result) == 2
|
|
|
|
def test_stickers_not_a_list_returns_empty(self):
|
|
assert parse_stickers_from_config({"stickers": "not_a_list"}) == []
|
|
|
|
def test_empty_stickers_list(self):
|
|
assert parse_stickers_from_config({"stickers": []}) == []
|
|
|
|
|
|
class TestGetStickerCategories:
|
|
"""get_sticker_categories 便捷函数."""
|
|
|
|
def test_returns_list_of_tuples(self):
|
|
result = get_sticker_categories()
|
|
assert isinstance(result, list)
|
|
assert len(result) > 0
|
|
for item in result:
|
|
assert isinstance(item, tuple)
|
|
assert len(item) == 2
|
|
|
|
def test_matches_constant(self):
|
|
result = get_sticker_categories()
|
|
assert result == list(STICKER_CATEGORIES)
|