""" 贴纸引擎配置与纯逻辑测试. 覆盖 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)