Files
xiaoxia-saas/tests/unit/test_watermark_engine.py
CI Bot 7a72cfd709
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
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 / Validate - Migration (alembic) (push) Successful in 2m9s
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 / Validate - Type Check (mypy) (push) Successful in 2m19s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m7s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m7s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m17s
CI/CD Pipeline / Frontend Lint (push) Successful in 3m37s
CI/CD Pipeline / Integration Tests (push) Successful in 2m18s
CI/CD Pipeline / Unit Tests (push) Failing after 4m59s
CI/CD Pipeline / Build Staging API Image (push) Successful in 8m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m25s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 27m2s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 30m15s
style: auto-format with black + isort + prettier
2026-07-25 00:16:10 +00:00

458 lines
15 KiB
Python
Executable File

"""水印引擎单元测试 - 配置解析 + 位置计算等纯逻辑."""
from __future__ import annotations
import pytest
from video_processing.watermark_engine import (
WATERMARK_POSITIONS,
WatermarkConfig,
WatermarkEngine,
)
# ── WatermarkConfig 测试 ──────────────────────────────────────────
class TestWatermarkConfigDefaults:
"""默认值测试."""
def test_default_values(self):
"""默认配置值正确."""
config = WatermarkConfig()
assert config.mode == "text"
assert config.position == "bottom_right"
assert config.image_path == ""
assert config.scale == 0.2
assert config.opacity == 0.8
assert config.text == ""
assert config.font_size == 24
assert config.font_color == "white"
assert config.font_path == ""
assert config.margin_x == 20
assert config.margin_y == 20
assert config.scroll is False
assert config.scroll_speed == 50
class TestWatermarkConfigFromDict:
"""from_dict 配置解析测试."""
def test_none_returns_none(self):
"""None 返回 None."""
assert WatermarkConfig.from_dict(None) is None
def test_empty_dict_returns_none(self):
"""空字典返回 None."""
assert WatermarkConfig.from_dict({}) is None
def test_disabled_returns_none(self):
"""enabled=False 返回 None."""
assert WatermarkConfig.from_dict({"enabled": False}) is None
def test_text_mode_basic(self):
"""文字水印基本配置."""
config = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "测试水印",
}
)
assert config is not None
assert config.mode == "text"
assert config.text == "测试水印"
assert config.position == "bottom_right" # 默认
def test_text_mode_missing_text_returns_none(self):
"""文字水印缺少 text 返回 None."""
result = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
}
)
assert result is None
def test_text_mode_empty_text_returns_none(self):
"""文字水印 text 为空返回 None."""
result = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "",
}
)
assert result is None
def test_image_mode_basic(self):
"""图片水印基本配置."""
config = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "image",
"image_path": "/path/to/logo.png",
}
)
assert config is not None
assert config.mode == "image"
assert config.image_path == "/path/to/logo.png"
def test_image_mode_missing_image_returns_none(self):
"""图片水印缺少 image_path 返回 None."""
result = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "image",
}
)
assert result is None
def test_image_mode_image_alias(self):
"""image 字段作为 image_path 的别名."""
config = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "image",
"image": "/path/alias.png",
}
)
assert config is not None
assert config.image_path == "/path/alias.png"
def test_invalid_position_falls_back(self):
"""无效位置 fallback 到 bottom_right."""
config = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "test",
"position": "invalid_pos",
}
)
assert config is not None
assert config.position == "bottom_right"
def test_custom_position_valid(self):
"""自定义有效位置."""
config = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "test",
"position": "top_left",
}
)
assert config is not None
assert config.position == "top_left"
def test_all_text_fields_parsed(self):
"""文字水印所有字段正确解析."""
config = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "我的水印",
"font_size": 32,
"font_color": "red",
"font_path": "/fonts/msyh.ttf",
"position": "top_center",
"opacity": 0.5,
"margin_x": 30,
"margin_y": 40,
}
)
assert config is not None
assert config.text == "我的水印"
assert config.font_size == 32
assert config.font_color == "red"
assert config.font_path == "/fonts/msyh.ttf"
assert config.position == "top_center"
assert config.opacity == 0.5
assert config.margin_x == 30
assert config.margin_y == 40
def test_all_image_fields_parsed(self):
"""图片水印所有字段正确解析."""
config = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "image",
"image_path": "/img/logo.png",
"scale": 0.3,
"opacity": 0.9,
"position": "bottom_left",
"margin_x": 10,
"margin_y": 15,
}
)
assert config is not None
assert config.image_path == "/img/logo.png"
assert config.scale == 0.3
assert config.opacity == 0.9
assert config.position == "bottom_left"
def test_scroll_config_parsed(self):
"""滚动水印配置解析."""
config = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "滚动水印",
"scroll": True,
"scroll_speed": 80,
}
)
assert config is not None
assert config.scroll is True
assert config.scroll_speed == 80
def test_default_mode_is_text(self):
"""不传 mode 默认为 text."""
config = WatermarkConfig.from_dict(
{
"enabled": True,
"text": "默认模式",
}
)
assert config is not None
assert config.mode == "text"
class TestWatermarkConfigValidate:
"""validate 配置校验测试."""
def test_valid_text_config(self):
"""合法文字水印配置."""
config = WatermarkConfig(mode="text", text="测试", position="bottom_right")
ok, msg = config.validate()
assert ok is True
assert msg == ""
def test_valid_image_config(self):
"""合法图片水印配置."""
config = WatermarkConfig(
mode="image",
image_path="/a.png",
position="top_left",
scale=0.3,
opacity=0.8,
)
ok, msg = config.validate()
assert ok is True
def test_invalid_position(self):
"""无效位置."""
config = WatermarkConfig(mode="text", text="test", position="invalid")
ok, msg = config.validate()
assert ok is False
assert "不支持的位置" in msg
def test_opacity_too_high(self):
"""透明度超过1."""
config = WatermarkConfig(mode="text", text="test", opacity=1.5)
ok, msg = config.validate()
assert ok is False
assert "透明度" in msg
def test_opacity_negative(self):
"""透明度为负."""
config = WatermarkConfig(mode="text", text="test", opacity=-0.1)
ok, msg = config.validate()
assert ok is False
assert "透明度" in msg
def test_opacity_boundary_zero(self):
"""透明度边界值0."""
config = WatermarkConfig(mode="text", text="test", opacity=0.0)
ok, _ = config.validate()
assert ok is True
def test_opacity_boundary_one(self):
"""透明度边界值1."""
config = WatermarkConfig(mode="text", text="test", opacity=1.0)
ok, _ = config.validate()
assert ok is True
def test_image_missing_path(self):
"""图片水印缺少路径."""
config = WatermarkConfig(mode="image", image_path="")
ok, msg = config.validate()
assert ok is False
assert "图片路径" in msg
def test_image_scale_too_small(self):
"""缩放比例太小."""
config = WatermarkConfig(mode="image", image_path="/a.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="/a.png", scale=2.0)
ok, msg = config.validate()
assert ok is False
assert "缩放比例" in msg
def test_image_scale_boundary_low(self):
"""缩放边界低值."""
config = WatermarkConfig(mode="image", image_path="/a.png", scale=0.01)
ok, _ = config.validate()
assert ok is True
def test_image_scale_boundary_high(self):
"""缩放边界高值."""
config = WatermarkConfig(mode="image", image_path="/a.png", scale=1.0)
ok, _ = config.validate()
assert ok is True
def test_text_missing_content(self):
"""文字水印缺少内容."""
config = WatermarkConfig(mode="text", text="")
ok, msg = config.validate()
assert ok is False
assert "文字内容" in msg
def test_text_font_size_zero(self):
"""字体大小为0."""
config = WatermarkConfig(mode="text", text="test", font_size=0)
ok, msg = config.validate()
assert ok is False
assert "字体大小" in msg
def test_text_font_size_negative(self):
"""字体大小为负."""
config = WatermarkConfig(mode="text", text="test", font_size=-5)
ok, msg = config.validate()
assert ok is False
assert "字体大小" in msg
def test_unknown_mode(self):
"""未知模式."""
config = WatermarkConfig(mode="unknown_mode")
ok, msg = config.validate()
assert ok is False
assert "不支持的水印模式" in msg
# ── WatermarkEngine 位置计算测试 ────────────────────────────────
class TestCalcPosition:
"""9宫格位置计算测试."""
# 测试用:输出 1920x1080,水印 200x100,边距 20
W, H = 1920, 1080
WW, WH = 200, 100
MX, MY = 20, 20
def test_top_left(self):
"""左上角."""
x, y = WatermarkEngine.calc_position("top_left", self.W, self.H, self.WW, self.WH, self.MX, self.MY)
assert (x, y) == (20, 20)
def test_top_center(self):
"""中上."""
x, y = WatermarkEngine.calc_position("top_center", self.W, self.H, self.WW, self.WH, self.MX, self.MY)
assert x == (1920 - 200) // 2
assert y == 20
def test_top_right(self):
"""右上角."""
x, y = WatermarkEngine.calc_position("top_right", self.W, self.H, self.WW, self.WH, self.MX, self.MY)
assert x == 1920 - 200 - 20
assert y == 20
def test_center_left(self):
"""左中."""
x, y = WatermarkEngine.calc_position("center_left", self.W, self.H, self.WW, self.WH, self.MX, self.MY)
assert x == 20
assert y == (1080 - 100) // 2
def test_center(self):
"""中心."""
x, y = WatermarkEngine.calc_position("center", self.W, self.H, self.WW, self.WH, self.MX, self.MY)
assert x == (1920 - 200) // 2
assert y == (1080 - 100) // 2
def test_center_right(self):
"""右中."""
x, y = WatermarkEngine.calc_position("center_right", self.W, self.H, self.WW, self.WH, self.MX, self.MY)
assert x == 1920 - 200 - 20
assert y == (1080 - 100) // 2
def test_bottom_left(self):
"""左下角."""
x, y = WatermarkEngine.calc_position("bottom_left", self.W, self.H, self.WW, self.WH, self.MX, self.MY)
assert x == 20
assert y == 1080 - 100 - 20
def test_bottom_center(self):
"""中下."""
x, y = WatermarkEngine.calc_position("bottom_center", self.W, self.H, self.WW, self.WH, self.MX, self.MY)
assert x == (1920 - 200) // 2
assert y == 1080 - 100 - 20
def test_bottom_right(self):
"""右下角."""
x, y = WatermarkEngine.calc_position("bottom_right", self.W, self.H, self.WW, self.WH, self.MX, self.MY)
assert x == 1920 - 200 - 20
assert y == 1080 - 100 - 20
def test_unknown_position_defaults_bottom_right(self):
"""未知位置默认右下角."""
x, y = WatermarkEngine.calc_position("unknown", self.W, self.H, self.WW, self.WH, self.MX, self.MY)
assert x == 1920 - 200 - 20
assert y == 1080 - 100 - 20
def test_zero_margin(self):
"""零边距."""
x, y = WatermarkEngine.calc_position("top_left", 1000, 500, 100, 50, 0, 0)
assert (x, y) == (0, 0)
def test_small_output(self):
"""小尺寸输出."""
x, y = WatermarkEngine.calc_position("bottom_right", 320, 240, 50, 30, 5, 5)
assert x == 320 - 50 - 5
assert y == 240 - 30 - 5
class TestCalcScrollX:
"""滚动水印x坐标表达式测试."""
def test_returns_string_expression(self):
"""返回字符串表达式."""
expr = WatermarkEngine.calc_scroll_x("bottom_right", 1920, 200, 50)
assert isinstance(expr, str)
assert "1920" in expr
assert "200" in expr
assert "50" in expr
def test_contains_mod_function(self):
"""包含 mod 函数."""
expr = WatermarkEngine.calc_scroll_x("top_left", 1280, 150, 60)
assert "mod(" in expr
assert "t" in expr # 时间变量
class TestWatermarkPositions:
"""位置常量测试."""
def test_nine_positions(self):
"""共9个位置."""
assert len(WATERMARK_POSITIONS) == 9
def test_all_position_keys_valid(self):
"""所有位置键名正确."""
expected = {
"top_left",
"top_center",
"top_right",
"center_left",
"center",
"center_right",
"bottom_left",
"bottom_center",
"bottom_right",
}
assert set(WATERMARK_POSITIONS.keys()) == expected