032c07fdc3
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 44s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m45s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m8s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 31s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m32s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 48s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m46s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 28s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 59s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m32s
AI Code Review / AI Code Review (pull_request) Successful in 4m18s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m48s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m41s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 3m5s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m7s
- test_watermark_engine: 水印引擎,56个用例 - WatermarkConfig.from_dict (16个) + validate (17个) - WatermarkEngine.calc_position 9宫格 + 边界 (14个) - calc_scroll_x + 位置常量 (5个) - test_noise_reduction_engine: 降噪引擎,20个用例 - NoiseReductionLevel 枚举 - NoiseReductionConfig.from_dict 解析 + 边界钳制 - has_effect + get_effective_noise_floor - test_thumbnail_generator: 缩略图生成,13个用例 - _format_seek_time 时间格式化 - 零值/分/时/长视频/精度校验 +89
454 lines
14 KiB
Python
Executable File
454 lines
14 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
|