Files
xiaoxia-saas/tests/unit/domain/test_sticker_config.py

676 lines
20 KiB
Python
Executable File
Raw Permalink 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.
"""sticker_config 单元测试 - wave167
覆盖:
- ImageStickerConfig 数据类 / from_dict / has_time_range / end_time
- TextStickerConfig 数据类 / from_dict / has_background / has_time_range
- StickerOverlayResult 数据类
- resolve_sticker_position 位置解析(9宫格/自定义坐标/单位转换/钳制)
- parse_stickers_from_config 贴纸列表解析
- get_sticker_categories 分类列表
- 常量验证(POSITION_PRESETS / STICKER_CATEGORIES
"""
import pytest
from packages.domain.sticker_config import (
POSITION_PRESETS,
STICKER_CATEGORIES,
ImageStickerConfig,
StickerOverlayResult,
TextStickerConfig,
get_sticker_categories,
parse_stickers_from_config,
resolve_sticker_position,
)
# ============================================================
# 常量验证
# ============================================================
class TestConstants:
def test_nine_position_presets(self):
assert len(POSITION_PRESETS) == 9
def test_positions_in_normalized_range(self):
for name, (x, y) in POSITION_PRESETS.items():
assert 0.0 <= x <= 1.0, f"{name} x out of range: {x}"
assert 0.0 <= y <= 1.0, f"{name} y out of range: {y}"
def test_top_left_is_near_zero(self):
x, y = POSITION_PRESETS["top_left"]
assert x < 0.1
assert y < 0.1
def test_bottom_right_is_near_one(self):
x, y = POSITION_PRESETS["bottom_right"]
assert x > 0.9
assert y > 0.9
def test_center_is_middle(self):
x, y = POSITION_PRESETS["center"]
assert x == 0.5
assert y == 0.5
def test_sticker_categories_not_empty(self):
assert len(STICKER_CATEGORIES) >= 3
def test_sticker_categories_format(self):
for cat in STICKER_CATEGORIES:
assert len(cat) == 2
assert isinstance(cat[0], str)
assert isinstance(cat[1], str)
# ============================================================
# ImageStickerConfig
# ============================================================
class TestImageStickerConfigDefaults:
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_has_time_range_false_when_zero(self):
cfg = ImageStickerConfig()
assert cfg.has_time_range is False
def test_has_time_range_true_when_set(self):
cfg = ImageStickerConfig(duration=5.0)
assert cfg.has_time_range is True
def test_end_time_calculation(self):
cfg = ImageStickerConfig(start_time=2.0, duration=3.0)
assert cfg.end_time == pytest.approx(5.0)
def test_end_time_zero_duration(self):
cfg = ImageStickerConfig(start_time=5.0, duration=0.0)
assert cfg.end_time == pytest.approx(5.0)
class TestImageStickerFromDict:
def test_none_returns_default(self):
cfg = ImageStickerConfig.from_dict(None)
assert cfg.enabled is False
assert cfg.position == "top_right"
def test_empty_dict_returns_default(self):
cfg = ImageStickerConfig.from_dict({})
assert cfg.enabled is False
def test_not_dict_returns_default(self):
cfg = ImageStickerConfig.from_dict("not_a_dict")
assert cfg.enabled is False
def test_enabled_true(self):
cfg = ImageStickerConfig.from_dict({"enabled": True})
assert cfg.enabled is True
def test_position_custom(self):
cfg = ImageStickerConfig.from_dict({"position": "bottom_left"})
assert cfg.position == "bottom_left"
def test_custom_coordinates(self):
cfg = ImageStickerConfig.from_dict({"x": 30.0, "y": 50.0})
assert cfg.x == 30.0
assert cfg.y == 50.0
def test_string_coordinates_converted(self):
cfg = ImageStickerConfig.from_dict({"x": "25.5", "y": "75.0"})
assert cfg.x == 25.5
assert cfg.y == 75.0
def test_invalid_coordinates_become_none(self):
cfg = ImageStickerConfig.from_dict({"x": "invalid", "y": "abc"})
assert cfg.x is None
assert cfg.y is None
def test_scale_clamped_min(self):
cfg = ImageStickerConfig.from_dict({"scale": 0.001})
assert cfg.scale == 0.01
def test_scale_normal(self):
cfg = ImageStickerConfig.from_dict({"scale": 1.5})
assert cfg.scale == 1.5
def test_opacity_clamped_high(self):
cfg = ImageStickerConfig.from_dict({"opacity": 1.5})
assert cfg.opacity == 1.0
def test_opacity_clamped_low(self):
cfg = ImageStickerConfig.from_dict({"opacity": -0.5})
assert cfg.opacity == 0.0
def test_opacity_normal(self):
cfg = ImageStickerConfig.from_dict({"opacity": 0.5})
assert cfg.opacity == 0.5
def test_start_time_clamped_non_negative(self):
cfg = ImageStickerConfig.from_dict({"start_time": -5.0})
assert cfg.start_time == 0.0
def test_duration_clamped_non_negative(self):
cfg = ImageStickerConfig.from_dict({"duration": -3.0})
assert cfg.duration == 0.0
def test_fade_in_non_negative(self):
cfg = ImageStickerConfig.from_dict({"fade_in": -1.0})
assert cfg.fade_in == 0.0
def test_fade_out_non_negative(self):
cfg = ImageStickerConfig.from_dict({"fade_out": -1.0})
assert cfg.fade_out == 0.0
def test_z_index_default_when_zero(self):
cfg = ImageStickerConfig.from_dict({"z_index": 0})
# 0 or None → 10
assert cfg.z_index == 10
def test_z_index_custom(self):
cfg = ImageStickerConfig.from_dict({"z_index": 5})
assert cfg.z_index == 5
def test_width_height(self):
cfg = ImageStickerConfig.from_dict({"width": 200, "height": 100})
assert cfg.width == 200
assert cfg.height == 100
def test_width_height_none(self):
cfg = ImageStickerConfig.from_dict({})
assert cfg.width is None
assert cfg.height is None
def test_image_url_and_preset_id(self):
cfg = ImageStickerConfig.from_dict({"image_url": "http://img.png", "preset_id": "sticker_001"})
assert cfg.image_url == "http://img.png"
assert cfg.preset_id == "sticker_001"
def test_units_custom(self):
cfg = ImageStickerConfig.from_dict({"x_unit": "pixel", "y_unit": "pixel"})
assert cfg.x_unit == "pixel"
assert cfg.y_unit == "pixel"
def test_invalid_numeric_values_use_default(self):
cfg = ImageStickerConfig.from_dict({"scale": "not_a_number"})
assert cfg.scale == 1.0 # 回退到默认值
def test_invalid_int_values_use_default(self):
cfg = ImageStickerConfig.from_dict({"z_index": "invalid"})
assert cfg.z_index == 10
# ============================================================
# TextStickerConfig
# ============================================================
class TestTextStickerConfigDefaults:
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.start_time == 0.0
assert cfg.duration == 0.0
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_has_background_false_when_empty(self):
cfg = TextStickerConfig()
assert cfg.has_background is False
def test_has_background_true_when_set(self):
cfg = TextStickerConfig(bg_color="#FF0000")
assert cfg.has_background is True
def test_has_time_range_false(self):
cfg = TextStickerConfig()
assert cfg.has_time_range is False
def test_has_time_range_true(self):
cfg = TextStickerConfig(duration=10.0)
assert cfg.has_time_range is True
class TestTextStickerFromDict:
def test_none_returns_default(self):
cfg = TextStickerConfig.from_dict(None)
assert cfg.enabled is False
assert cfg.text == ""
def test_empty_dict_returns_default(self):
cfg = TextStickerConfig.from_dict({})
assert cfg.text == ""
def test_not_dict_returns_default(self):
cfg = TextStickerConfig.from_dict(123)
assert cfg.text == ""
def test_text_content(self):
cfg = TextStickerConfig.from_dict({"text": "Hello World"})
assert cfg.text == "Hello World"
def test_font_size_minimum_1(self):
cfg = TextStickerConfig.from_dict({"font_size": 0})
assert cfg.font_size == 1
def test_font_size_normal(self):
cfg = TextStickerConfig.from_dict({"font_size": 48})
assert cfg.font_size == 48
def test_font_color(self):
cfg = TextStickerConfig.from_dict({"font_color": "#00FF00"})
assert cfg.font_color == "#00FF00"
def test_stroke_width_non_negative(self):
cfg = TextStickerConfig.from_dict({"stroke_width": -2})
assert cfg.stroke_width == 0
def test_stroke_width_normal(self):
cfg = TextStickerConfig.from_dict({"stroke_width": 4})
assert cfg.stroke_width == 4
def test_shadow_alpha_clamped(self):
cfg = TextStickerConfig.from_dict({"shadow_alpha": 1.5})
assert cfg.shadow_alpha == 1.0
def test_shadow_alpha_negative(self):
cfg = TextStickerConfig.from_dict({"shadow_alpha": -0.5})
assert cfg.shadow_alpha == 0.0
def test_shadow_offset(self):
cfg = TextStickerConfig.from_dict({"shadow_x": 5, "shadow_y": 3})
assert cfg.shadow_x == 5
assert cfg.shadow_y == 3
def test_position_custom(self):
cfg = TextStickerConfig.from_dict({"position": "top_right"})
assert cfg.position == "top_right"
def test_custom_coordinates(self):
cfg = TextStickerConfig.from_dict({"x": 10, "y": 20})
assert cfg.x == 10.0
assert cfg.y == 20.0
def test_start_time_non_negative(self):
cfg = TextStickerConfig.from_dict({"start_time": -1.0})
assert cfg.start_time == 0.0
def test_duration_non_negative(self):
cfg = TextStickerConfig.from_dict({"duration": -2.0})
assert cfg.duration == 0.0
def test_fade_in_out_non_negative(self):
cfg = TextStickerConfig.from_dict({"fade_in": -1, "fade_out": -1})
assert cfg.fade_in == 0.0
assert cfg.fade_out == 0.0
def test_z_index_default(self):
cfg = TextStickerConfig.from_dict({})
assert cfg.z_index == 10
def test_bg_color(self):
cfg = TextStickerConfig.from_dict({"bg_color": "#0000FF"})
assert cfg.bg_color == "#0000FF"
def test_bg_padding_non_negative(self):
cfg = TextStickerConfig.from_dict({"bg_padding": -5})
assert cfg.bg_padding == 0
def test_bg_alpha_clamped(self):
cfg = TextStickerConfig.from_dict({"bg_alpha": 2.0})
assert cfg.bg_alpha == 1.0
def test_bg_corner_radius_non_negative(self):
cfg = TextStickerConfig.from_dict({"bg_corner_radius": -3})
assert cfg.bg_corner_radius == 0
def test_invalid_numeric_values_use_default(self):
cfg = TextStickerConfig.from_dict({"font_size": "big"})
assert cfg.font_size == 36 # 默认值
# ============================================================
# StickerOverlayResult
# ============================================================
class TestStickerOverlayResult:
def test_minimal_creation(self):
r = StickerOverlayResult(filter_str="overlay=10:20", output_label="[out]")
assert r.filter_str == "overlay=10:20"
assert r.output_label == "[out]"
assert r.extra_inputs == []
def test_with_extra_inputs(self):
r = StickerOverlayResult(
filter_str="overlay",
output_label="[out]",
extra_inputs=["img1.png", "img2.png"],
)
assert len(r.extra_inputs) == 2
assert r.extra_inputs == ["img1.png", "img2.png"]
def test_extra_inputs_independent_lists(self):
r1 = StickerOverlayResult(filter_str="a", output_label="[o1]")
r2 = StickerOverlayResult(filter_str="b", output_label="[o2]")
r1.extra_inputs.append("test.png")
assert r2.extra_inputs == [] # 独立
# ============================================================
# resolve_sticker_position
# ============================================================
class TestResolveStickerPosition:
CANVAS_W = 1920
CANVAS_H = 1080
def test_preset_top_left(self):
x, y = resolve_sticker_position(
"top_left",
None,
None,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
100,
50,
)
# preset x=0.05, y=0.05 → 0.05*1920 - 50 = 96-50=46, 0.05*1080-25=54-25=29
assert x == pytest.approx(0.05 * 1920 - 50)
assert y == pytest.approx(0.05 * 1080 - 25)
def test_preset_center(self):
x, y = resolve_sticker_position(
"center",
None,
None,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
200,
100,
)
# center 0.5,0.5 → 贴纸左上角居中
assert x == pytest.approx(1920 / 2 - 100)
assert y == pytest.approx(1080 / 2 - 50)
def test_preset_bottom_right(self):
x, y = resolve_sticker_position(
"bottom_right",
None,
None,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
100,
50,
)
assert x == pytest.approx(0.95 * 1920 - 50)
assert y == pytest.approx(0.95 * 1080 - 25)
def test_invalid_preset_defaults_center(self):
x, y = resolve_sticker_position(
"invalid",
None,
None,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
100,
50,
)
# 默认居中
assert x == pytest.approx(1920 / 2 - 50)
assert y == pytest.approx(1080 / 2 - 25)
def test_custom_percent_coordinates(self):
x, y = resolve_sticker_position(
"top_left",
30.0,
70.0,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
0,
0,
)
# x=30% of 1920 = 576, y=70% of 1080 = 756
assert x == pytest.approx(576.0)
assert y == pytest.approx(756.0)
def test_custom_percent_clamped_0(self):
x, y = resolve_sticker_position(
"center",
-10.0,
-5.0,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
0,
0,
)
assert x == 0.0
assert y == 0.0
def test_custom_percent_clamped_100(self):
x, y = resolve_sticker_position(
"center",
150.0,
120.0,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
0,
0,
)
assert x == pytest.approx(1920.0)
assert y == pytest.approx(1080.0)
def test_custom_pixel_coordinates(self):
x, y = resolve_sticker_position(
"center",
500,
300,
"pixel",
"pixel",
self.CANVAS_W,
self.CANVAS_H,
0,
0,
)
assert x == pytest.approx(500.0)
assert y == pytest.approx(300.0)
def test_x_only_override(self):
# 只有x覆盖,y用预设
x, y = resolve_sticker_position(
"top_left",
50.0,
None,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
0,
0,
)
assert x == pytest.approx(0.5 * 1920) # 50%
assert y == pytest.approx(0.05 * 1080) # 预设 top_left
def test_y_only_override(self):
x, y = resolve_sticker_position(
"top_left",
None,
50.0,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
0,
0,
)
assert x == pytest.approx(0.05 * 1920)
assert y == pytest.approx(0.5 * 1080)
def test_clamped_to_canvas_with_sticker_size(self):
# 贴纸100x50,放在最右下角,不能超出画布
x, y = resolve_sticker_position(
"bottom_right",
None,
None,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
100,
50,
)
assert x <= self.CANVAS_W - 100
assert y <= self.CANVAS_H - 50
def test_clamped_left_top(self):
# 很大的负偏移,被钳制在0
x, y = resolve_sticker_position(
"center",
0.0,
0.0,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
200,
100,
)
# 0%位置 + 贴纸尺寸的一半偏移 = -100, -50 → 钳制到 0, 0
assert x == 0
assert y == 0
def test_zero_canvas_defaults_center(self):
x, y = resolve_sticker_position(
"top_left",
50,
50,
"pixel",
"pixel",
0,
0,
0,
0,
)
# canvas_w=0 → 回退到0.5 → 0*0.5=0
assert x == 0.0
assert y == 0.0
def test_sticker_size_zero(self):
x, y = resolve_sticker_position(
"center",
None,
None,
"percent",
"percent",
self.CANVAS_W,
self.CANVAS_H,
0,
0,
)
assert x == pytest.approx(960.0)
assert y == pytest.approx(540.0)
# ============================================================
# parse_stickers_from_config
# ============================================================
class TestParseStickersFromConfig:
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_stickers_list(self):
config = {"stickers": [{"type": "text", "text": "hi"}, {"type": "image", "image_url": "a.png"}]}
result = parse_stickers_from_config(config)
assert len(result) == 2
assert result[0]["type"] == "text"
def test_stickers_empty_list(self):
config = {"stickers": []}
assert parse_stickers_from_config(config) == []
def test_stickers_not_list_returns_empty(self):
config = {"stickers": "not_a_list"}
assert parse_stickers_from_config(config) == []
def test_other_config_ignored(self):
config = {"other_field": "value"}
assert parse_stickers_from_config(config) == []
# ============================================================
# get_sticker_categories
# ============================================================
class TestGetStickerCategories:
def test_returns_list(self):
result = get_sticker_categories()
assert isinstance(result, list)
assert len(result) == len(STICKER_CATEGORIES)
def test_returns_copy(self):
# 修改返回值不应影响常量
result = get_sticker_categories()
result.append(("new", "新分类"))
assert len(STICKER_CATEGORIES) == len(STICKER_CATEGORIES)
# 原常量不变
assert ("new", "新分类") not in STICKER_CATEGORIES
def test_format(self):
for cat in get_sticker_categories():
assert len(cat) == 2
assert isinstance(cat[0], str)
assert isinstance(cat[1], str)