"""watermark_config 领域模型单测.""" from __future__ import annotations import pytest from packages.domain.watermark_config import ( DEFAULT_FONT_COLOR, DEFAULT_FONT_SIZE, DEFAULT_MODE, DEFAULT_OPACITY, DEFAULT_POSITION, DEFAULT_SCALE, VALID_POSITIONS, WATERMARK_POSITIONS, WatermarkConfig, build_image_watermark_filter, build_text_watermark_filter, calc_position, calc_scroll_x, get_position_display_name, get_position_names, ) # ── 常量测试 ──────────────────────────────────────────────────────────────── class TestConstants: def test_nine_positions(self): assert len(WATERMARK_POSITIONS) == 9 def test_all_position_keys_valid(self): for key in WATERMARK_POSITIONS: assert key in VALID_POSITIONS def test_valid_positions_match(self): assert set(WATERMARK_POSITIONS.keys()) == VALID_POSITIONS def test_default_values(self): assert DEFAULT_POSITION == "bottom_right" assert DEFAULT_MODE == "text" assert DEFAULT_SCALE == 0.2 assert DEFAULT_OPACITY == 0.8 assert DEFAULT_FONT_SIZE == 24 assert DEFAULT_FONT_COLOR == "white" # ── WatermarkConfig.from_dict 测试 ───────────────────────────────────────── class TestWatermarkConfigFromDict: 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_disabled_returns_none(self): assert WatermarkConfig.from_dict({"enabled": False}) is None def test_text_mode_basic(self): cfg = WatermarkConfig.from_dict({"enabled": True, "mode": "text", "text": "hello"}) assert cfg is not None assert cfg.mode == "text" assert cfg.text == "hello" assert cfg.position == DEFAULT_POSITION def test_image_mode_basic(self): cfg = WatermarkConfig.from_dict({"enabled": True, "mode": "image", "image_path": "/tmp/wm.png"}) assert cfg is not None assert cfg.mode == "image" assert cfg.image_path == "/tmp/wm.png" def test_image_mode_accepts_image_key(self): cfg = WatermarkConfig.from_dict({"enabled": True, "mode": "image", "image": "/tmp/wm.png"}) assert cfg is not None assert cfg.image_path == "/tmp/wm.png" def test_image_mode_missing_path_returns_none(self): assert WatermarkConfig.from_dict({"enabled": True, "mode": "image"}) is None def test_text_mode_missing_text_returns_none(self): assert WatermarkConfig.from_dict({"enabled": True, "mode": "text"}) is None def test_text_mode_empty_text_returns_none(self): assert WatermarkConfig.from_dict({"enabled": True, "mode": "text", "text": ""}) is None def test_invalid_position_defaults(self): cfg = WatermarkConfig.from_dict({"enabled": True, "mode": "text", "text": "hi", "position": "invalid"}) assert cfg.position == DEFAULT_POSITION def test_custom_all_params(self): cfg = WatermarkConfig.from_dict( { "enabled": True, "mode": "text", "text": "测试水印", "position": "top_left", "font_size": 32, "font_color": "red", "opacity": 0.5, "margin_x": 30, "margin_y": 40, "scroll": True, "scroll_speed": 100, } ) assert cfg is not None assert cfg.text == "测试水印" assert cfg.position == "top_left" assert cfg.font_size == 32 assert cfg.font_color == "red" assert cfg.opacity == 0.5 assert cfg.margin_x == 30 assert cfg.margin_y == 40 assert cfg.scroll is True assert cfg.scroll_speed == 100 def test_default_mode_is_text(self): cfg = WatermarkConfig.from_dict({"enabled": True, "text": "hi"}) assert cfg is not None assert cfg.mode == "text" # ── WatermarkConfig.validate 测试 ────────────────────────────────────────── class TestWatermarkConfigValidate: def test_valid_text_config(self): cfg = WatermarkConfig(mode="text", text="hello") ok, msg = cfg.validate() assert ok is True assert msg == "" def test_valid_image_config(self): cfg = WatermarkConfig(mode="image", image_path="/tmp/wm.png", scale=0.3) ok, msg = cfg.validate() assert ok is True def test_invalid_position(self): cfg = WatermarkConfig(mode="text", text="hi", position="nowhere") ok, msg = cfg.validate() assert ok is False assert "位置" in msg def test_opacity_negative(self): cfg = WatermarkConfig(mode="text", text="hi", opacity=-0.1) ok, msg = cfg.validate() assert ok is False assert "透明度" in msg def test_opacity_over_one(self): cfg = WatermarkConfig(mode="text", text="hi", opacity=1.5) ok, msg = cfg.validate() assert ok is False def test_opacity_boundary_zero(self): cfg = WatermarkConfig(mode="text", text="hi", opacity=0.0) ok, _ = cfg.validate() assert ok is True def test_opacity_boundary_one(self): cfg = WatermarkConfig(mode="text", text="hi", opacity=1.0) ok, _ = cfg.validate() assert ok is True def test_image_missing_path(self): cfg = WatermarkConfig(mode="image") ok, msg = cfg.validate() assert ok is False assert "图片路径" in msg def test_image_scale_too_small(self): cfg = WatermarkConfig(mode="image", image_path="/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="/a.png", scale=2.0) ok, msg = cfg.validate() assert ok is False def test_image_scale_boundary_low(self): cfg = WatermarkConfig(mode="image", image_path="/a.png", scale=0.01) ok, _ = cfg.validate() assert ok is True def test_image_scale_boundary_high(self): cfg = WatermarkConfig(mode="image", image_path="/a.png", scale=1.0) ok, _ = cfg.validate() assert ok is True def test_text_missing_content(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="hi", 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="hi", font_size=-5) ok, msg = cfg.validate() assert ok is False def test_unknown_mode(self): cfg = WatermarkConfig(mode="video") ok, msg = cfg.validate() assert ok is False assert "模式" in msg # ── has_effect 测试 ──────────────────────────────────────────────────────── class TestHasEffect: def test_text_with_content_has_effect(self): cfg = WatermarkConfig(mode="text", text="hello") assert cfg.has_effect() is True def test_text_empty_no_effect(self): cfg = WatermarkConfig(mode="text", text="") assert cfg.has_effect() is False def test_text_zero_opacity_no_effect(self): cfg = WatermarkConfig(mode="text", text="hello", opacity=0.0) assert cfg.has_effect() is False def test_image_with_path_has_effect(self): cfg = WatermarkConfig(mode="image", image_path="/a.png") assert cfg.has_effect() is True def test_image_no_path_no_effect(self): cfg = WatermarkConfig(mode="image") assert cfg.has_effect() is False def test_unknown_mode_no_effect(self): cfg = WatermarkConfig(mode="unknown") assert cfg.has_effect() is False # ── calc_position 测试 ───────────────────────────────────────────────────── class TestCalcPosition: def test_top_left(self): x, y = calc_position("top_left", 1000, 2000, 100, 50, 10, 20) assert (x, y) == (10, 20) def test_top_center(self): x, y = calc_position("top_center", 1000, 2000, 100, 50, 10, 20) assert (x, y) == (450, 20) def test_top_right(self): x, y = calc_position("top_right", 1000, 2000, 100, 50, 10, 20) assert (x, y) == (890, 20) def test_center_left(self): x, y = calc_position("center_left", 1000, 2000, 100, 50, 10, 20) assert (x, y) == (10, 975) def test_center(self): x, y = calc_position("center", 1000, 2000, 100, 50, 10, 20) assert (x, y) == (450, 975) def test_center_right(self): x, y = calc_position("center_right", 1000, 2000, 100, 50, 10, 20) assert (x, y) == (890, 975) def test_bottom_left(self): x, y = calc_position("bottom_left", 1000, 2000, 100, 50, 10, 20) assert (x, y) == (10, 1930) def test_bottom_center(self): x, y = calc_position("bottom_center", 1000, 2000, 100, 50, 10, 20) assert (x, y) == (450, 1930) def test_bottom_right(self): x, y = calc_position("bottom_right", 1000, 2000, 100, 50, 10, 20) assert (x, y) == (890, 1930) def test_unknown_position_defaults_bottom_right(self): x, y = calc_position("invalid", 1000, 2000, 100, 50, 10, 20) assert (x, y) == (890, 1930) def test_zero_margin(self): x, y = calc_position("top_left", 1000, 2000, 100, 50, 0, 0) assert (x, y) == (0, 0) def test_small_output(self): x, y = calc_position("center", 100, 100, 50, 30, 5, 5) assert (x, y) == (25, 35) # ── calc_scroll_x 测试 ───────────────────────────────────────────────────── class TestCalcScrollX: def test_returns_string_expression(self): result = calc_scroll_x("bottom", 1000, 200, 50) assert isinstance(result, str) def test_contains_mod_function(self): result = calc_scroll_x("bottom", 1000, 200, 50) assert "mod" in result def test_contains_speed_and_width(self): result = calc_scroll_x("bottom", 1080, 300, 60) assert "1080" in result assert "60" in result assert "300" in result # ── build_image_watermark_filter 测试 ────────────────────────────────────── class TestBuildImageWatermarkFilter: def test_returns_tuple(self): cfg = WatermarkConfig(mode="image", image_path="/wm.png") result = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg) assert isinstance(result, tuple) assert len(result) == 2 def test_filter_contains_overlay(self): cfg = WatermarkConfig(mode="image", image_path="/wm.png") filter_str, inputs = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg) assert "overlay" in filter_str def test_filter_contains_scale(self): cfg = WatermarkConfig(mode="image", image_path="/wm.png", scale=0.5) filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg) assert "scale=" in filter_str def test_full_opacity_no_alpha_filter(self): cfg = WatermarkConfig(mode="image", image_path="/wm.png", opacity=1.0) filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg) assert "colorchannelmixer" not in filter_str def test_partial_opacity_has_alpha_filter(self): cfg = WatermarkConfig(mode="image", image_path="/wm.png", opacity=0.5) filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg) assert "colorchannelmixer" in filter_str assert "aa=0.5" in filter_str def test_input_args_contains_image_path(self): cfg = WatermarkConfig(mode="image", image_path="/path/to/wm.png") _, inputs = build_image_watermark_filter("[in]", "/path/to/wm.png", 1080, 1920, "[out]", cfg) assert inputs == ["-i", "/path/to/wm.png"] def test_scroll_mode_has_t_variable(self): cfg = WatermarkConfig(mode="image", image_path="/wm.png", scroll=True, scroll_speed=50) filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg) assert "t" in filter_str def test_output_label_appears(self): cfg = WatermarkConfig(mode="image", image_path="/wm.png") filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[final]", cfg) assert "[final]" in filter_str # ── build_text_watermark_filter 测试 ─────────────────────────────────────── class TestBuildTextWatermarkFilter: def test_returns_string(self): cfg = WatermarkConfig(mode="text", text="hello") result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920) assert isinstance(result, str) def test_contains_drawtext(self): cfg = WatermarkConfig(mode="text", text="hello") result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920) assert "drawtext=" in result def test_contains_text_content(self): cfg = WatermarkConfig(mode="text", text="watermark_test") result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920) assert "watermark_test" in result def test_contains_font_size(self): cfg = WatermarkConfig(mode="text", text="hi", font_size=48) result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920) assert "fontsize=48" in result def test_contains_font_color(self): cfg = WatermarkConfig(mode="text", text="hi", font_color="red") result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920) assert "fontcolor=red" in result def test_font_path_included_when_set(self): cfg = WatermarkConfig(mode="text", text="hi", font_path="/fonts/a.ttf") result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920) assert "fontfile=" in result assert "a.ttf" in result def test_font_path_not_included_when_empty(self): cfg = WatermarkConfig(mode="text", text="hi") result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920) assert "fontfile=" not in result def test_scroll_mode_has_t_variable(self): cfg = WatermarkConfig(mode="text", text="hello", scroll=True, scroll_speed=30) result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920) assert "t" in result def test_no_scroll_uses_fixed_position(self): cfg = WatermarkConfig(mode="text", text="hello", scroll=False) result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920) assert "x=" in result # 非滚动模式 x= 后面应该是数字,不是表达式 # 找 x= 后的第一个字符 import re match = re.search(r"x=(\d+)", result) assert match is not None def test_output_label_appears(self): cfg = WatermarkConfig(mode="text", text="hi") result = build_text_watermark_filter("[in]", "[text_out]", cfg, 1080, 1920) assert "[text_out]" in result def test_special_chars_escaped(self): cfg = WatermarkConfig(mode="text", text="hello:world") result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920) # 冒号应该被转义 assert "hello\\:world" in result or "hello\\\\\\:world" in result or "hello\\:" in result # ── 工具函数测试 ──────────────────────────────────────────────────────────── class TestUtils: def test_get_position_names_returns_nine(self): names = get_position_names() assert len(names) == 9 def test_get_position_names_all_valid(self): names = get_position_names() for name in names: assert name in VALID_POSITIONS def test_get_position_display_name_valid(self): assert get_position_display_name("top_left") == "左上" assert get_position_display_name("bottom_right") == "右下" def test_get_position_display_name_invalid(self): assert get_position_display_name("invalid") == "invalid"