test(unit): 第63波 - watermark + noise_reduction + thumbnail 纯逻辑 (+89) #859
Executable
+219
@@ -0,0 +1,219 @@
|
||||
"""降噪引擎单元测试 - 配置解析等纯逻辑."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.noise_reduction_engine import (
|
||||
NoiseReductionConfig,
|
||||
NoiseReductionLevel,
|
||||
)
|
||||
|
||||
|
||||
class TestNoiseReductionLevel:
|
||||
"""降噪等级枚举测试."""
|
||||
|
||||
def test_level_values(self):
|
||||
"""等级枚举值正确."""
|
||||
assert NoiseReductionLevel.LOW.value == "low"
|
||||
assert NoiseReductionLevel.MEDIUM.value == "medium"
|
||||
assert NoiseReductionLevel.HIGH.value == "high"
|
||||
assert NoiseReductionLevel.CUSTOM.value == "custom"
|
||||
|
||||
def test_from_string(self):
|
||||
"""从字符串创建."""
|
||||
assert NoiseReductionLevel("low") == NoiseReductionLevel.LOW
|
||||
assert NoiseReductionLevel("medium") == NoiseReductionLevel.MEDIUM
|
||||
assert NoiseReductionLevel("high") == NoiseReductionLevel.HIGH
|
||||
assert NoiseReductionLevel("custom") == NoiseReductionLevel.CUSTOM
|
||||
|
||||
def test_invalid_string_raises(self):
|
||||
"""无效字符串抛异常."""
|
||||
with pytest.raises(ValueError):
|
||||
NoiseReductionLevel("invalid")
|
||||
|
||||
|
||||
class TestNoiseReductionConfigDefaults:
|
||||
"""默认配置测试."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""默认值正确."""
|
||||
config = NoiseReductionConfig()
|
||||
assert config.enabled is False
|
||||
assert config.level == NoiseReductionLevel.MEDIUM
|
||||
assert config.noise_floor == -25.0
|
||||
assert config.voice_enhance is False
|
||||
|
||||
|
||||
class TestNoiseReductionConfigFromDict:
|
||||
"""from_dict 配置解析测试."""
|
||||
|
||||
def test_none_returns_disabled(self):
|
||||
"""None 返回禁用配置."""
|
||||
config = NoiseReductionConfig.from_dict(None)
|
||||
assert config.enabled is False
|
||||
|
||||
def test_empty_dict_returns_disabled(self):
|
||||
"""空字典返回禁用配置."""
|
||||
config = NoiseReductionConfig.from_dict({})
|
||||
assert config.enabled is False
|
||||
|
||||
def test_disabled_returns_disabled(self):
|
||||
"""enabled=False 返回禁用."""
|
||||
config = NoiseReductionConfig.from_dict({"enabled": False})
|
||||
assert config.enabled is False
|
||||
|
||||
def test_enabled_default_level(self):
|
||||
"""启用时默认等级为 medium."""
|
||||
config = NoiseReductionConfig.from_dict({"enabled": True})
|
||||
assert config.enabled is True
|
||||
assert config.level == NoiseReductionLevel.MEDIUM
|
||||
|
||||
def test_level_low(self):
|
||||
"""low 等级."""
|
||||
config = NoiseReductionConfig.from_dict({"enabled": True, "level": "low"})
|
||||
assert config.level == NoiseReductionLevel.LOW
|
||||
|
||||
def test_level_high(self):
|
||||
"""high 等级."""
|
||||
config = NoiseReductionConfig.from_dict({"enabled": True, "level": "high"})
|
||||
assert config.level == NoiseReductionLevel.HIGH
|
||||
|
||||
def test_level_custom(self):
|
||||
"""custom 等级."""
|
||||
config = NoiseReductionConfig.from_dict({"enabled": True, "level": "custom"})
|
||||
assert config.level == NoiseReductionLevel.CUSTOM
|
||||
|
||||
def test_level_case_insensitive(self):
|
||||
"""等级大小写不敏感."""
|
||||
config = NoiseReductionConfig.from_dict({"enabled": True, "level": "HIGH"})
|
||||
assert config.level == NoiseReductionLevel.HIGH
|
||||
|
||||
def test_invalid_level_falls_back_to_medium(self):
|
||||
"""无效等级 fallback 到 medium."""
|
||||
config = NoiseReductionConfig.from_dict({"enabled": True, "level": "ultra"})
|
||||
assert config.level == NoiseReductionLevel.MEDIUM
|
||||
|
||||
def test_noise_floor_parsed(self):
|
||||
"""噪音阈值解析."""
|
||||
config = NoiseReductionConfig.from_dict({
|
||||
"enabled": True,
|
||||
"level": "custom",
|
||||
"noise_floor": -30.0,
|
||||
})
|
||||
assert config.noise_floor == -30.0
|
||||
|
||||
def test_noise_floor_clamped_min(self):
|
||||
"""噪音阈值下限钳制 (-60)."""
|
||||
config = NoiseReductionConfig.from_dict({
|
||||
"enabled": True,
|
||||
"level": "custom",
|
||||
"noise_floor": -100.0,
|
||||
})
|
||||
assert config.noise_floor == -60.0
|
||||
|
||||
def test_noise_floor_clamped_max(self):
|
||||
"""噪音阈值上限钳制 (-5)."""
|
||||
config = NoiseReductionConfig.from_dict({
|
||||
"enabled": True,
|
||||
"level": "custom",
|
||||
"noise_floor": 0.0,
|
||||
})
|
||||
assert config.noise_floor == -5.0
|
||||
|
||||
def test_noise_floor_boundary_low(self):
|
||||
"""噪音阈值边界值 -60."""
|
||||
config = NoiseReductionConfig.from_dict({
|
||||
"enabled": True,
|
||||
"level": "custom",
|
||||
"noise_floor": -60.0,
|
||||
})
|
||||
assert config.noise_floor == -60.0
|
||||
|
||||
def test_noise_floor_boundary_high(self):
|
||||
"""噪音阈值边界值 -5."""
|
||||
config = NoiseReductionConfig.from_dict({
|
||||
"enabled": True,
|
||||
"level": "custom",
|
||||
"noise_floor": -5.0,
|
||||
})
|
||||
assert config.noise_floor == -5.0
|
||||
|
||||
def test_invalid_noise_floor_falls_back(self):
|
||||
"""无效噪音阈值 fallback 到默认值."""
|
||||
config = NoiseReductionConfig.from_dict({
|
||||
"enabled": True,
|
||||
"level": "custom",
|
||||
"noise_floor": "not_a_number",
|
||||
})
|
||||
assert config.noise_floor == -25.0
|
||||
|
||||
def test_voice_enhance_enabled(self):
|
||||
"""人声增强启用."""
|
||||
config = NoiseReductionConfig.from_dict({
|
||||
"enabled": True,
|
||||
"voice_enhance": True,
|
||||
})
|
||||
assert config.voice_enhance is True
|
||||
|
||||
def test_voice_enhance_disabled_default(self):
|
||||
"""人声增强默认禁用."""
|
||||
config = NoiseReductionConfig.from_dict({"enabled": True})
|
||||
assert config.voice_enhance is False
|
||||
|
||||
|
||||
class TestHasEffect:
|
||||
"""has_effect 方法测试."""
|
||||
|
||||
def test_disabled_no_effect(self):
|
||||
"""禁用时无效果."""
|
||||
config = NoiseReductionConfig(enabled=False)
|
||||
assert config.has_effect() is False
|
||||
|
||||
def test_enabled_has_effect(self):
|
||||
"""启用时有效果."""
|
||||
config = NoiseReductionConfig(enabled=True)
|
||||
assert config.has_effect() is True
|
||||
|
||||
|
||||
class TestGetEffectiveNoiseFloor:
|
||||
"""get_effective_noise_floor 方法测试."""
|
||||
|
||||
def test_custom_level_returns_noise_floor(self):
|
||||
"""custom 等级返回配置的 noise_floor."""
|
||||
config = NoiseReductionConfig(
|
||||
enabled=True,
|
||||
level=NoiseReductionLevel.CUSTOM,
|
||||
noise_floor=-35.0,
|
||||
)
|
||||
assert config.get_effective_noise_floor() == -35.0
|
||||
|
||||
def test_low_level_returns_params(self):
|
||||
"""low 等级返回对应参数值."""
|
||||
config = NoiseReductionConfig(
|
||||
enabled=True,
|
||||
level=NoiseReductionLevel.LOW,
|
||||
)
|
||||
result = config.get_effective_noise_floor()
|
||||
assert isinstance(result, float)
|
||||
assert result < 0 # dB值为负数
|
||||
|
||||
def test_medium_level_returns_params(self):
|
||||
"""medium 等级返回对应参数值."""
|
||||
config = NoiseReductionConfig(
|
||||
enabled=True,
|
||||
level=NoiseReductionLevel.MEDIUM,
|
||||
)
|
||||
result = config.get_effective_noise_floor()
|
||||
assert isinstance(result, float)
|
||||
assert result < 0
|
||||
|
||||
def test_high_level_returns_params(self):
|
||||
"""high 等级返回对应参数值."""
|
||||
config = NoiseReductionConfig(
|
||||
enabled=True,
|
||||
level=NoiseReductionLevel.HIGH,
|
||||
)
|
||||
result = config.get_effective_noise_floor()
|
||||
assert isinstance(result, float)
|
||||
assert result < 0
|
||||
@@ -1,60 +1,89 @@
|
||||
"""
|
||||
缩略图生成器纯函数测试.
|
||||
|
||||
覆盖 _format_seek_time 等纯逻辑.
|
||||
FFmpeg 抽帧与 OSS 上传由集成测试覆盖.
|
||||
"""
|
||||
"""缩略图生成器单元测试 - 纯逻辑函数."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.thumbnail_generator import _format_seek_time
|
||||
|
||||
|
||||
class TestFormatSeekTime:
|
||||
"""_format_seek_time 时间格式化."""
|
||||
"""_format_seek_time 时间格式化测试."""
|
||||
|
||||
def test_zero(self):
|
||||
assert _format_seek_time(0.0) == "00:00:00.00"
|
||||
def test_zero_seconds(self):
|
||||
"""0秒."""
|
||||
result = _format_seek_time(0)
|
||||
assert result == "00:00:00.00"
|
||||
|
||||
def test_seconds_only(self):
|
||||
assert _format_seek_time(5.5) == "00:00:05.50"
|
||||
def test_less_than_one_second(self):
|
||||
"""小于1秒."""
|
||||
result = _format_seek_time(0.5)
|
||||
assert result == "00:00:00.50"
|
||||
|
||||
def test_minutes(self):
|
||||
assert _format_seek_time(65.25) == "00:01:05.25"
|
||||
def test_few_seconds(self):
|
||||
"""几秒."""
|
||||
result = _format_seek_time(5.5)
|
||||
assert result == "00:00:05.50"
|
||||
|
||||
def test_hours(self):
|
||||
assert _format_seek_time(3661.5) == "01:01:01.50"
|
||||
def test_one_minute(self):
|
||||
"""1分钟."""
|
||||
result = _format_seek_time(60.0)
|
||||
assert result == "00:01:00.00"
|
||||
|
||||
def test_exact_minute(self):
|
||||
assert _format_seek_time(60.0) == "00:01:00.00"
|
||||
def test_minutes_and_seconds(self):
|
||||
"""分+秒."""
|
||||
result = _format_seek_time(125.5)
|
||||
assert result == "00:02:05.50"
|
||||
|
||||
def test_exact_hour(self):
|
||||
assert _format_seek_time(3600.0) == "01:00:00.00"
|
||||
def test_one_hour(self):
|
||||
"""1小时."""
|
||||
result = _format_seek_time(3600.0)
|
||||
assert result == "01:00:00.00"
|
||||
|
||||
def test_very_short(self):
|
||||
assert _format_seek_time(0.1) == "00:00:00.10"
|
||||
def test_hours_minutes_seconds(self):
|
||||
"""时+分+秒."""
|
||||
result = _format_seek_time(3725.25)
|
||||
assert result == "01:02:05.25"
|
||||
|
||||
def test_long_video(self):
|
||||
# 超过1小时
|
||||
assert _format_seek_time(7200.0) == "02:00:00.00"
|
||||
def test_long_duration(self):
|
||||
"""长视频(2小时以上)."""
|
||||
result = _format_seek_time(7384.12)
|
||||
assert result == "02:03:04.12"
|
||||
|
||||
def test_sub_second_precision(self):
|
||||
result = _format_seek_time(1.234)
|
||||
def test_precision_two_decimal(self):
|
||||
"""两位小数精度."""
|
||||
result = _format_seek_time(3.14159)
|
||||
assert result == "00:00:03.14"
|
||||
|
||||
def test_always_two_digit_hours(self):
|
||||
"""小时始终两位数字."""
|
||||
result = _format_seek_time(3600 * 9)
|
||||
assert result.startswith("09:")
|
||||
|
||||
def test_always_two_digit_minutes(self):
|
||||
"""分钟始终两位数字."""
|
||||
result = _format_seek_time(300) # 5分钟
|
||||
parts = result.split(":")
|
||||
assert parts[1] == "05"
|
||||
|
||||
def test_float_input(self):
|
||||
"""浮点数输入."""
|
||||
result = _format_seek_time(10.0)
|
||||
assert isinstance(result, str)
|
||||
assert result == "00:00:10.00"
|
||||
|
||||
def test_int_input(self):
|
||||
"""整数输入."""
|
||||
result = _format_seek_time(30)
|
||||
assert result == "00:00:30.00"
|
||||
|
||||
def test_format_structure(self):
|
||||
"""格式结构正确:HH:MM:SS.xx."""
|
||||
result = _format_seek_time(3661.5)
|
||||
# 格式: HH:MM:SS.xx
|
||||
parts = result.split(":")
|
||||
assert len(parts) == 3
|
||||
sec_part = parts[2]
|
||||
assert "." in sec_part
|
||||
decimals = sec_part.split(".")[1]
|
||||
assert len(decimals) == 2
|
||||
|
||||
def test_zero_padded_hours(self):
|
||||
# 小时始终是2位
|
||||
result = _format_seek_time(5.0)
|
||||
assert result.startswith("00:")
|
||||
|
||||
def test_zero_padded_minutes(self):
|
||||
# 分钟始终是2位
|
||||
result = _format_seek_time(5.0)
|
||||
parts = result.split(":")
|
||||
assert len(parts[1]) == 2
|
||||
assert "." in parts[2]
|
||||
sec_parts = parts[2].split(".")
|
||||
assert len(sec_parts) == 2
|
||||
assert len(sec_parts[1]) == 2 # 两位小数
|
||||
|
||||
+373
-195
@@ -1,275 +1,453 @@
|
||||
"""
|
||||
水印引擎配置与纯逻辑测试.
|
||||
"""水印引擎单元测试 - 配置解析 + 位置计算等纯逻辑."""
|
||||
|
||||
覆盖 WatermarkConfig.from_dict / validate / 位置枚举等纯逻辑.
|
||||
引擎核心 render 方法依赖 FFmpeg,由集成测试覆盖.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from video_processing.watermark_engine import WATERMARK_POSITIONS, WatermarkConfig
|
||||
|
||||
from video_processing.watermark_engine import (
|
||||
WATERMARK_POSITIONS,
|
||||
WatermarkConfig,
|
||||
WatermarkEngine,
|
||||
)
|
||||
|
||||
|
||||
class TestWatermarkPositions:
|
||||
"""水印位置枚举."""
|
||||
# ── WatermarkConfig 测试 ──────────────────────────────────────────
|
||||
|
||||
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 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 构造逻辑."""
|
||||
"""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_enabled_false_returns_none(self):
|
||||
def test_disabled_returns_none(self):
|
||||
"""enabled=False 返回 None."""
|
||||
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",
|
||||
}
|
||||
)
|
||||
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_image_mode_with_empty_path_returns_none(self):
|
||||
result = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "image",
|
||||
"image_path": "",
|
||||
}
|
||||
)
|
||||
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_text_mode_without_text_returns_none(self):
|
||||
result = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
}
|
||||
)
|
||||
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_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_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_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_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_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_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_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_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_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_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_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
|
||||
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_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 == ""
|
||||
"""validate 配置校验测试."""
|
||||
|
||||
def test_valid_text_config(self):
|
||||
cfg = WatermarkConfig(
|
||||
mode="text",
|
||||
text="hello",
|
||||
position="center",
|
||||
opacity=1.0,
|
||||
font_size=48,
|
||||
)
|
||||
ok, msg = cfg.validate()
|
||||
"""合法文字水印配置."""
|
||||
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):
|
||||
cfg = WatermarkConfig(mode="text", text="test", position="nowhere")
|
||||
ok, msg = cfg.validate()
|
||||
"""无效位置."""
|
||||
config = WatermarkConfig(mode="text", text="test", position="invalid")
|
||||
ok, msg = config.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()
|
||||
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_above_one(self):
|
||||
cfg = WatermarkConfig(mode="text", text="test", opacity=1.5)
|
||||
ok, msg = cfg.validate()
|
||||
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_zero_is_valid(self):
|
||||
cfg = WatermarkConfig(mode="text", text="test", opacity=0.0)
|
||||
ok, _ = cfg.validate()
|
||||
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_one_is_valid(self):
|
||||
cfg = WatermarkConfig(mode="text", text="test", opacity=1.0)
|
||||
ok, _ = cfg.validate()
|
||||
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):
|
||||
cfg = WatermarkConfig(mode="image", image_path="")
|
||||
ok, msg = cfg.validate()
|
||||
"""图片水印缺少路径."""
|
||||
config = WatermarkConfig(mode="image", image_path="")
|
||||
ok, msg = config.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()
|
||||
"""缩放比例太小."""
|
||||
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):
|
||||
cfg = WatermarkConfig(mode="image", image_path="/tmp/a.png", scale=2.0)
|
||||
ok, msg = cfg.validate()
|
||||
"""缩放比例太大."""
|
||||
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_valid(self):
|
||||
cfg = WatermarkConfig(mode="image", image_path="/tmp/a.png", scale=0.01)
|
||||
ok, _ = cfg.validate()
|
||||
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
|
||||
|
||||
cfg2 = WatermarkConfig(mode="image", image_path="/tmp/a.png", scale=1.0)
|
||||
ok2, _ = cfg2.validate()
|
||||
assert ok2 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_text(self):
|
||||
cfg = WatermarkConfig(mode="text", text="")
|
||||
ok, msg = cfg.validate()
|
||||
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):
|
||||
cfg = WatermarkConfig(mode="text", text="test", font_size=0)
|
||||
ok, msg = cfg.validate()
|
||||
"""字体大小为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):
|
||||
cfg = WatermarkConfig(mode="text", text="test", font_size=-5)
|
||||
ok, msg = cfg.validate()
|
||||
"""字体大小为负."""
|
||||
config = WatermarkConfig(mode="text", text="test", font_size=-5)
|
||||
ok, msg = config.validate()
|
||||
assert ok is False
|
||||
assert "字体大小" in msg
|
||||
|
||||
def test_unsupported_mode(self):
|
||||
cfg = WatermarkConfig(mode="video", text="test")
|
||||
ok, msg = cfg.validate()
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user