test(unit): 第63波 - watermark + noise_reduction + thumbnail 纯逻辑 (+89) #859

Merged
xiaoxia merged 1 commits from test/wave63-more-engine-config-validation into develop 2026-07-25 08:12:57 +08:00
3 changed files with 662 additions and 236 deletions
+219
View File
@@ -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
+70 -41
View File
@@ -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
View File
@@ -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