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

603 lines
22 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.
"""watermark_config 单元测试 - wave163
覆盖:
- WatermarkConfig 数据类 / from_dict / validate / has_effect
- calc_position 9宫格位置计算
- calc_scroll_x 滚动计算
- build_image_watermark_filter 图片水印滤镜
- build_text_watermark_filter 文字水印滤镜
- get_position_names / get_position_display_name
- 常量验证
"""
import pytest
from packages.domain.watermark_config import (
DEFAULT_FONT_COLOR,
DEFAULT_FONT_SIZE,
DEFAULT_MARGIN_X,
DEFAULT_MARGIN_Y,
DEFAULT_MODE,
DEFAULT_OPACITY,
DEFAULT_POSITION,
DEFAULT_SCALE,
DEFAULT_SCROLL_SPEED,
VALID_POSITIONS,
WATERMARK_POSITIONS,
WatermarkConfig,
build_image_watermark_filter,
build_text_watermark_filter,
calc_position,
calc_scroll_x,
get_position_display_name,
get_position_names,
)
# ============================================================
# 常量
# ============================================================
class TestConstants:
def test_nine_positions(self):
assert len(VALID_POSITIONS) == 9
def test_watermark_positions_contains_all(self):
assert set(WATERMARK_POSITIONS.keys()) == VALID_POSITIONS
def test_default_position_valid(self):
assert DEFAULT_POSITION in VALID_POSITIONS
def test_default_values(self):
assert DEFAULT_MODE in ("text", "image")
assert 0 < DEFAULT_SCALE <= 1.0
assert 0 <= DEFAULT_OPACITY <= 1.0
assert DEFAULT_FONT_SIZE > 0
assert DEFAULT_MARGIN_X >= 0
assert DEFAULT_MARGIN_Y >= 0
assert DEFAULT_SCROLL_SPEED > 0
assert isinstance(DEFAULT_FONT_COLOR, str)
# ============================================================
# WatermarkConfig 默认值
# ============================================================
class TestWatermarkConfigDefaults:
def test_default_constructor(self):
config = WatermarkConfig()
assert config.mode == DEFAULT_MODE
assert config.position == DEFAULT_POSITION
assert config.scale == DEFAULT_SCALE
assert config.opacity == DEFAULT_OPACITY
assert config.font_size == DEFAULT_FONT_SIZE
assert config.font_color == DEFAULT_FONT_COLOR
assert config.margin_x == DEFAULT_MARGIN_X
assert config.margin_y == DEFAULT_MARGIN_Y
assert config.scroll is False
assert config.scroll_speed == DEFAULT_SCROLL_SPEED
def test_text_mode_default(self):
config = WatermarkConfig()
assert config.mode == "text"
assert config.text == ""
assert config.font_path == ""
def test_image_mode_default(self):
config = WatermarkConfig(mode="image")
assert config.image_path == ""
# ============================================================
# from_dict
# ============================================================
class TestFromDict:
def test_none_returns_none(self):
assert WatermarkConfig.from_dict(None) is None
def test_empty_dict_returns_none(self):
assert WatermarkConfig.from_dict({}) is None
def test_disabled_returns_none(self):
assert WatermarkConfig.from_dict({"enabled": False}) is None
def test_image_mode_missing_path_returns_none(self):
result = WatermarkConfig.from_dict({"enabled": True, "mode": "image"})
assert result is None
def test_image_mode_with_image_field(self):
result = WatermarkConfig.from_dict({"enabled": True, "mode": "image", "image": "/path/to/wm.png"})
assert result is not None
assert result.mode == "image"
assert result.image_path == "/path/to/wm.png"
def test_image_mode_with_image_path_field(self):
result = WatermarkConfig.from_dict({"enabled": True, "mode": "image", "image_path": "/path/wm.png"})
assert result is not None
assert result.image_path == "/path/wm.png"
def test_text_mode_missing_text_returns_none(self):
result = WatermarkConfig.from_dict({"enabled": True, "mode": "text"})
assert result is None
def test_text_mode_with_text(self):
result = WatermarkConfig.from_dict({"enabled": True, "mode": "text", "text": "hello"})
assert result is not None
assert result.mode == "text"
assert result.text == "hello"
def test_invalid_position_falls_back_default(self):
result = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "hello",
"position": "invalid_pos",
}
)
assert result is not None
assert result.position == DEFAULT_POSITION
def test_custom_values_propagated(self):
result = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "测试",
"position": "top_left",
"opacity": 0.5,
"font_size": 32,
"font_color": "red",
"margin_x": 30,
"margin_y": 40,
"scroll": True,
"scroll_speed": 100,
}
)
assert result is not None
assert result.position == "top_left"
assert result.opacity == 0.5
assert result.font_size == 32
assert result.font_color == "red"
assert result.margin_x == 30
assert result.margin_y == 40
assert result.scroll is True
assert result.scroll_speed == 100
def test_image_mode_custom_values(self):
result = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "image",
"image_path": "/wm.png",
"scale": 0.3,
"opacity": 0.6,
}
)
assert result is not None
assert result.scale == 0.3
assert result.opacity == 0.6
def test_default_mode_when_unspecified(self):
# 只给enabled和textmode默认text
result = WatermarkConfig.from_dict({"enabled": True, "text": "hello"})
assert result is not None
assert result.mode == DEFAULT_MODE
def test_text_empty_string_returns_none(self):
result = WatermarkConfig.from_dict({"enabled": True, "mode": "text", "text": ""})
assert result is None
# ============================================================
# validate
# ============================================================
class TestValidate:
def test_valid_text_config(self):
config = WatermarkConfig(mode="text", text="hello")
ok, msg = config.validate()
assert ok is True
assert msg == ""
def test_valid_image_config(self):
config = WatermarkConfig(mode="image", image_path="/wm.png")
ok, msg = config.validate()
assert ok is True
assert msg == ""
def test_invalid_position(self):
config = WatermarkConfig(mode="text", text="hi", position="nowhere")
ok, msg = config.validate()
assert ok is False
assert "位置" in msg
def test_opacity_too_high(self):
config = WatermarkConfig(mode="text", text="hi", opacity=1.5)
ok, msg = config.validate()
assert ok is False
assert "透明度" in msg
def test_opacity_negative(self):
config = WatermarkConfig(mode="text", text="hi", opacity=-0.1)
ok, msg = config.validate()
assert ok is False
assert "透明度" in msg
def test_opacity_boundary_zero(self):
config = WatermarkConfig(mode="text", text="hi", opacity=0.0)
ok, _ = config.validate()
assert ok is True
def test_opacity_boundary_one(self):
config = WatermarkConfig(mode="text", text="hi", opacity=1.0)
ok, _ = config.validate()
assert ok is True
def test_image_missing_path(self):
config = WatermarkConfig(mode="image")
ok, msg = config.validate()
assert ok is False
assert "图片路径" in msg
def test_image_scale_too_small(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", scale=0.001)
ok, msg = config.validate()
assert ok is False
assert "缩放" in msg
def test_image_scale_too_large(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", scale=1.5)
ok, msg = config.validate()
assert ok is False
assert "缩放" in msg
def test_image_scale_boundary_low(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", scale=0.01)
ok, _ = config.validate()
assert ok is True
def test_image_scale_boundary_high(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", scale=1.0)
ok, _ = config.validate()
assert ok is True
def test_text_empty(self):
config = WatermarkConfig(mode="text", text="")
ok, msg = config.validate()
assert ok is False
assert "文字内容" in msg
def test_font_size_zero(self):
config = WatermarkConfig(mode="text", text="hi", font_size=0)
ok, msg = config.validate()
assert ok is False
assert "字体大小" in msg
def test_font_size_negative(self):
config = WatermarkConfig(mode="text", text="hi", font_size=-5)
ok, msg = config.validate()
assert ok is False
assert "字体大小" in msg
def test_unknown_mode(self):
config = WatermarkConfig(mode="video", text="hi")
ok, msg = config.validate()
assert ok is False
assert "模式" in msg
# ============================================================
# has_effect
# ============================================================
class TestHasEffect:
def test_text_has_effect(self):
config = WatermarkConfig(mode="text", text="hello", opacity=0.5, font_size=24)
assert config.has_effect() is True
def test_text_empty_no_effect(self):
config = WatermarkConfig(mode="text", text="")
assert config.has_effect() is False
def test_text_zero_opacity_no_effect(self):
config = WatermarkConfig(mode="text", text="hi", opacity=0.0)
assert config.has_effect() is False
def test_text_zero_font_size_no_effect(self):
config = WatermarkConfig(mode="text", text="hi", font_size=0)
assert config.has_effect() is False
def test_image_has_effect(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", opacity=0.5)
assert config.has_effect() is True
def test_image_no_path_no_effect(self):
config = WatermarkConfig(mode="image")
assert config.has_effect() is False
def test_image_zero_opacity_no_effect(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", opacity=0.0)
assert config.has_effect() is False
def test_unknown_mode_no_effect(self):
config = WatermarkConfig(mode="invalid")
assert config.has_effect() is False
# ============================================================
# calc_position
# ============================================================
class TestCalcPosition:
OUT_W = 1920
OUT_H = 1080
WM_W = 200
WM_H = 50
MX = 20
MY = 20
def test_top_left(self):
x, y = calc_position("top_left", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
assert x == 20
assert y == 20
def test_top_center(self):
x, y = calc_position("top_center", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
assert x == (1920 - 200) // 2
assert y == 20
def test_top_right(self):
x, y = calc_position("top_right", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
assert x == 1920 - 200 - 20
assert y == 20
def test_center_left(self):
x, y = calc_position("center_left", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
assert x == 20
assert y == (1080 - 50) // 2
def test_center(self):
x, y = calc_position("center", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
assert x == (1920 - 200) // 2
assert y == (1080 - 50) // 2
def test_center_right(self):
x, y = calc_position("center_right", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
assert x == 1920 - 200 - 20
assert y == (1080 - 50) // 2
def test_bottom_left(self):
x, y = calc_position("bottom_left", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
assert x == 20
assert y == 1080 - 50 - 20
def test_bottom_center(self):
x, y = calc_position("bottom_center", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
assert x == (1920 - 200) // 2
assert y == 1080 - 50 - 20
def test_bottom_right(self):
x, y = calc_position("bottom_right", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
assert x == 1920 - 200 - 20
assert y == 1080 - 50 - 20
def test_invalid_position_defaults_bottom_right(self):
x, y = calc_position("invalid", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
assert x == 1920 - 200 - 20
assert y == 1080 - 50 - 20
def test_zero_margins(self):
x, y = calc_position("top_left", self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, 0, 0)
assert x == 0
assert y == 0
def test_all_nine_positions_are_unique(self):
positions = set()
for pos in VALID_POSITIONS:
pos_xy = calc_position(pos, self.OUT_W, self.OUT_H, self.WM_W, self.WM_H, self.MX, self.MY)
positions.add(pos_xy)
assert len(positions) == 9
# ============================================================
# calc_scroll_x
# ============================================================
class TestCalcScrollX:
def test_returns_string(self):
result = calc_scroll_x("bottom_left", 1920, 200, 50)
assert isinstance(result, str)
def test_contains_output_width(self):
result = calc_scroll_x("bottom_left", 1920, 200, 50)
assert "1920" in result
def test_contains_wm_width(self):
result = calc_scroll_x("bottom_left", 1920, 200, 50)
assert "200" in result
def test_contains_speed(self):
result = calc_scroll_x("bottom_left", 1920, 200, 50)
assert "50" in result
def test_contains_mod_keyword(self):
result = calc_scroll_x("bottom_left", 1920, 200, 50)
assert "mod" in result
# ============================================================
# build_image_watermark_filter
# ============================================================
class TestBuildImageWatermarkFilter:
def test_returns_tuple(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", position="top_left")
result = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
assert isinstance(result, tuple)
assert len(result) == 2
def test_filter_contains_overlay(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", position="bottom_right")
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
assert "overlay" in filter_str
def test_filter_contains_scale(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", scale=0.3)
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
assert "scale=" in filter_str
def test_opacity_applied_when_below_one(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", opacity=0.5)
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
assert "colorchannelmixer" in filter_str
assert "aa=0.5" in filter_str
def test_opacity_one_no_mixer(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", opacity=1.0)
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
assert "colorchannelmixer" not in filter_str
def test_input_args_contain_image_path(self):
config = WatermarkConfig(mode="image", image_path="/path/to/wm.png")
_, input_args = build_image_watermark_filter("[in]", "/path/to/wm.png", 1920, 1080, "[out]", config)
assert input_args == ["-i", "/path/to/wm.png"]
def test_output_label_present(self):
config = WatermarkConfig(mode="image", image_path="/wm.png")
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[final]", config)
assert "[final]" in filter_str
def test_scroll_mode_contains_t_variable(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", scroll=True, scroll_speed=60)
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
assert "*t" in filter_str or "t*" in filter_str
def test_static_mode_no_t_variable_in_overlay_x(self):
config = WatermarkConfig(mode="image", image_path="/wm.png", scroll=False)
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
# 静态模式 x 是具体数值,不包含 t 变量
overlay_part = filter_str.split("overlay=")[1]
assert "t" not in overlay_part.split(":")[0] or "wm" in overlay_part.split(":")[0]
def test_semicolon_separated_filters(self):
config = WatermarkConfig(mode="image", image_path="/wm.png")
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1920, 1080, "[out]", config)
assert ";" in filter_str
# ============================================================
# build_text_watermark_filter
# ============================================================
class TestBuildTextWatermarkFilter:
def test_returns_string(self):
config = WatermarkConfig(mode="text", text="hello")
result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
assert isinstance(result, str)
def test_contains_drawtext(self):
config = WatermarkConfig(mode="text", text="hello")
result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
assert "drawtext=" in result
def test_contains_text(self):
config = WatermarkConfig(mode="text", text="测试水印")
result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
assert "测试水印" in result
def test_fontsize_in_filter(self):
config = WatermarkConfig(mode="text", text="hi", font_size=36)
result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
assert "fontsize=36" in result
def test_fontcolor_with_opacity(self):
config = WatermarkConfig(mode="text", text="hi", font_color="red", opacity=0.5)
result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
assert "fontcolor=red@0.5" in result
def test_font_path_included(self):
config = WatermarkConfig(mode="text", text="hi", font_path="/fonts/simsun.ttf")
result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
assert "fontfile=" in result
assert "simsun.ttf" in result
def test_no_font_path_when_empty(self):
config = WatermarkConfig(mode="text", text="hi", font_path="")
result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
assert "fontfile=" not in result
def test_text_colon_escaped(self):
config = WatermarkConfig(mode="text", text="time: 00:00")
result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
# 冒号应该被转义
assert "time\\:" in result or "time\\\\:" in result
def test_scroll_mode_contains_mod(self):
config = WatermarkConfig(mode="text", text="scrolling", scroll=True, scroll_speed=50)
result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
assert "mod(" in result
def test_static_mode_has_numeric_x_y(self):
config = WatermarkConfig(mode="text", text="hi", position="top_left")
result = build_text_watermark_filter("[in]", "[out]", config, 1920, 1080)
assert "x=" in result
assert "y=" in result
def test_output_label_present(self):
config = WatermarkConfig(mode="text", text="hi")
result = build_text_watermark_filter("[in]", "[final_out]", config, 1920, 1080)
assert "[final_out]" in result
def test_input_label_present(self):
config = WatermarkConfig(mode="text", text="hi")
result = build_text_watermark_filter("[video_in]", "[out]", config, 1920, 1080)
assert result.startswith("[video_in]")
# ============================================================
# 工具函数
# ============================================================
class TestGetPositionNames:
def test_returns_nine_names(self):
names = get_position_names()
assert len(names) == 9
def test_order_is_correct(self):
names = get_position_names()
# 按从上到下、从左到右
assert names[0] == "top_left"
assert names[1] == "top_center"
assert names[2] == "top_right"
assert names[3] == "center_left"
assert names[4] == "center"
assert names[5] == "center_right"
assert names[6] == "bottom_left"
assert names[7] == "bottom_center"
assert names[8] == "bottom_right"
def test_all_valid(self):
names = get_position_names()
assert set(names) == VALID_POSITIONS
class TestGetPositionDisplayName:
def test_known_position(self):
assert get_position_display_name("top_left") == "左上"
assert get_position_display_name("center") == "中心"
assert get_position_display_name("bottom_right") == "右下"
def test_unknown_position_returns_original(self):
assert get_position_display_name("invalid") == "invalid"