2df7bc9dc8
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 1m41s
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 / Build Staging Web Image (push) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m34s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m49s
CI/CD Pipeline / Frontend Lint (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Unit Tests (push) Failing after 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m10s
276 lines
8.4 KiB
Python
Executable File
276 lines
8.4 KiB
Python
Executable File
"""
|
|
水印引擎配置与纯逻辑测试.
|
|
|
|
覆盖 WatermarkConfig.from_dict / validate / 位置枚举等纯逻辑.
|
|
引擎核心 render 方法依赖 FFmpeg,由集成测试覆盖.
|
|
"""
|
|
|
|
import pytest
|
|
from video_processing.watermark_engine import WATERMARK_POSITIONS, WatermarkConfig
|
|
|
|
|
|
class TestWatermarkPositions:
|
|
"""水印位置枚举."""
|
|
|
|
def test_nine_positions_exist(self):
|
|
assert len(WATERMARK_POSITIONS) == 9
|
|
assert "top_left" in WATERMARK_POSITIONS
|
|
assert "top_center" in WATERMARK_POSITIONS
|
|
assert "top_right" in WATERMARK_POSITIONS
|
|
assert "center_left" in WATERMARK_POSITIONS
|
|
assert "center" in WATERMARK_POSITIONS
|
|
assert "center_right" in WATERMARK_POSITIONS
|
|
assert "bottom_left" in WATERMARK_POSITIONS
|
|
assert "bottom_center" in WATERMARK_POSITIONS
|
|
assert "bottom_right" in WATERMARK_POSITIONS
|
|
|
|
def test_position_values_are_chinese_labels(self):
|
|
for key, label in WATERMARK_POSITIONS.items():
|
|
assert isinstance(label, str)
|
|
assert len(label) >= 2
|
|
|
|
|
|
class TestWatermarkConfigFromDict:
|
|
"""from_dict 构造逻辑."""
|
|
|
|
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_enabled_false_returns_none(self):
|
|
assert WatermarkConfig.from_dict({"enabled": False}) is None
|
|
|
|
def test_image_mode_without_path_returns_none(self):
|
|
result = WatermarkConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"mode": "image",
|
|
}
|
|
)
|
|
assert result is None
|
|
|
|
def test_image_mode_with_empty_path_returns_none(self):
|
|
result = WatermarkConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"mode": "image",
|
|
"image_path": "",
|
|
}
|
|
)
|
|
assert result is None
|
|
|
|
def test_text_mode_without_text_returns_none(self):
|
|
result = WatermarkConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"mode": "text",
|
|
}
|
|
)
|
|
assert result is None
|
|
|
|
def test_text_mode_with_empty_text_returns_none(self):
|
|
result = WatermarkConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"mode": "text",
|
|
"text": "",
|
|
}
|
|
)
|
|
assert result is None
|
|
|
|
def test_image_mode_success(self):
|
|
cfg = WatermarkConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"mode": "image",
|
|
"image_path": "/tmp/logo.png",
|
|
"scale": 0.3,
|
|
"opacity": 0.9,
|
|
"position": "top_left",
|
|
"margin_x": 30,
|
|
"margin_y": 30,
|
|
}
|
|
)
|
|
assert cfg is not None
|
|
assert cfg.mode == "image"
|
|
assert cfg.image_path == "/tmp/logo.png"
|
|
assert cfg.scale == 0.3
|
|
assert cfg.opacity == 0.9
|
|
assert cfg.position == "top_left"
|
|
assert cfg.margin_x == 30
|
|
assert cfg.margin_y == 30
|
|
|
|
def test_image_mode_image_key_fallback(self):
|
|
"""image 字段作为 image_path 的 fallback."""
|
|
cfg = WatermarkConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"mode": "image",
|
|
"image": "/tmp/fallback.png",
|
|
}
|
|
)
|
|
assert cfg is not None
|
|
assert cfg.image_path == "/tmp/fallback.png"
|
|
|
|
def test_text_mode_success(self):
|
|
cfg = WatermarkConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"mode": "text",
|
|
"text": "hello world",
|
|
"font_size": 32,
|
|
"font_color": "red",
|
|
"position": "bottom_left",
|
|
"scroll": True,
|
|
"scroll_speed": 100,
|
|
}
|
|
)
|
|
assert cfg is not None
|
|
assert cfg.mode == "text"
|
|
assert cfg.text == "hello world"
|
|
assert cfg.font_size == 32
|
|
assert cfg.font_color == "red"
|
|
assert cfg.position == "bottom_left"
|
|
assert cfg.scroll is True
|
|
assert cfg.scroll_speed == 100
|
|
|
|
def test_invalid_position_falls_back_to_bottom_right(self):
|
|
cfg = WatermarkConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"mode": "text",
|
|
"text": "test",
|
|
"position": "invalid_position",
|
|
}
|
|
)
|
|
assert cfg is not None
|
|
assert cfg.position == "bottom_right"
|
|
|
|
def test_default_values_applied(self):
|
|
cfg = WatermarkConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"mode": "text",
|
|
"text": "test",
|
|
}
|
|
)
|
|
assert cfg is not None
|
|
assert cfg.position == "bottom_right"
|
|
assert cfg.opacity == 0.8
|
|
assert cfg.scale == 0.2
|
|
assert cfg.font_size == 24
|
|
assert cfg.font_color == "white"
|
|
assert cfg.margin_x == 20
|
|
assert cfg.margin_y == 20
|
|
assert cfg.scroll is False
|
|
assert cfg.scroll_speed == 50
|
|
|
|
|
|
class TestWatermarkConfigValidate:
|
|
"""validate 校验逻辑."""
|
|
|
|
def test_valid_image_config(self):
|
|
cfg = WatermarkConfig(
|
|
mode="image",
|
|
image_path="/tmp/logo.png",
|
|
position="top_right",
|
|
opacity=0.5,
|
|
scale=0.5,
|
|
)
|
|
ok, msg = cfg.validate()
|
|
assert ok is True
|
|
assert msg == ""
|
|
|
|
def test_valid_text_config(self):
|
|
cfg = WatermarkConfig(
|
|
mode="text",
|
|
text="hello",
|
|
position="center",
|
|
opacity=1.0,
|
|
font_size=48,
|
|
)
|
|
ok, msg = cfg.validate()
|
|
assert ok is True
|
|
assert msg == ""
|
|
|
|
def test_invalid_position(self):
|
|
cfg = WatermarkConfig(mode="text", text="test", position="nowhere")
|
|
ok, msg = cfg.validate()
|
|
assert ok is False
|
|
assert "不支持的位置" in msg
|
|
|
|
def test_opacity_below_zero(self):
|
|
cfg = WatermarkConfig(mode="text", text="test", opacity=-0.1)
|
|
ok, msg = cfg.validate()
|
|
assert ok is False
|
|
assert "透明度" in msg
|
|
|
|
def test_opacity_above_one(self):
|
|
cfg = WatermarkConfig(mode="text", text="test", opacity=1.5)
|
|
ok, msg = cfg.validate()
|
|
assert ok is False
|
|
assert "透明度" in msg
|
|
|
|
def test_opacity_zero_is_valid(self):
|
|
cfg = WatermarkConfig(mode="text", text="test", opacity=0.0)
|
|
ok, _ = cfg.validate()
|
|
assert ok is True
|
|
|
|
def test_opacity_one_is_valid(self):
|
|
cfg = WatermarkConfig(mode="text", text="test", opacity=1.0)
|
|
ok, _ = cfg.validate()
|
|
assert ok is True
|
|
|
|
def test_image_missing_path(self):
|
|
cfg = WatermarkConfig(mode="image", image_path="")
|
|
ok, msg = cfg.validate()
|
|
assert ok is False
|
|
assert "图片路径" in msg
|
|
|
|
def test_image_scale_too_small(self):
|
|
cfg = WatermarkConfig(mode="image", image_path="/tmp/a.png", scale=0.001)
|
|
ok, msg = cfg.validate()
|
|
assert ok is False
|
|
assert "缩放比例" in msg
|
|
|
|
def test_image_scale_too_large(self):
|
|
cfg = WatermarkConfig(mode="image", image_path="/tmp/a.png", scale=2.0)
|
|
ok, msg = cfg.validate()
|
|
assert ok is False
|
|
assert "缩放比例" in msg
|
|
|
|
def test_image_scale_boundary_valid(self):
|
|
cfg = WatermarkConfig(mode="image", image_path="/tmp/a.png", scale=0.01)
|
|
ok, _ = cfg.validate()
|
|
assert ok is True
|
|
|
|
cfg2 = WatermarkConfig(mode="image", image_path="/tmp/a.png", scale=1.0)
|
|
ok2, _ = cfg2.validate()
|
|
assert ok2 is True
|
|
|
|
def test_text_missing_text(self):
|
|
cfg = WatermarkConfig(mode="text", text="")
|
|
ok, msg = cfg.validate()
|
|
assert ok is False
|
|
assert "文字内容" in msg
|
|
|
|
def test_text_font_size_zero(self):
|
|
cfg = WatermarkConfig(mode="text", text="test", font_size=0)
|
|
ok, msg = cfg.validate()
|
|
assert ok is False
|
|
assert "字体大小" in msg
|
|
|
|
def test_text_font_size_negative(self):
|
|
cfg = WatermarkConfig(mode="text", text="test", font_size=-5)
|
|
ok, msg = cfg.validate()
|
|
assert ok is False
|
|
assert "字体大小" in msg
|
|
|
|
def test_unsupported_mode(self):
|
|
cfg = WatermarkConfig(mode="video", text="test")
|
|
ok, msg = cfg.validate()
|
|
assert ok is False
|
|
assert "不支持的水印模式" in msg
|