@@ -0,0 +1,602 @@
""" watermark_config 单元测试 - wave163
覆盖:
- WatermarkConfig 数据类 / from_dict / validate / has_effect
- calc_position 9宫格位置计算
- calc_scroll_x 滚动计算
- build_image_watermark_filter 图片水印滤镜
- build_text_watermark_filter 文字水印滤镜
- get_position_names / get_position_display_name
- 常量验证
"""
import pytest
from packages . domain . watermark_config import (
DEFAULT_FONT_COLOR ,
DEFAULT_FONT_SIZE ,
DEFAULT_MARGIN_X ,
DEFAULT_MARGIN_Y ,
DEFAULT_MODE ,
DEFAULT_OPACITY ,
DEFAULT_POSITION ,
DEFAULT_SCALE ,
DEFAULT_SCROLL_SPEED ,
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 ( VALID_POSITIONS ) == 9
def test_watermark_positions_contains_all ( self ) :
assert set ( WATERMARK_POSITIONS . keys ( ) ) == VALID_POSITIONS
def test_default_position_valid ( self ) :
assert DEFAULT_POSITION in VALID_POSITIONS
def test_default_values ( self ) :
assert DEFAULT_MODE in ( " text " , " image " )
assert 0 < DEFAULT_SCALE < = 1.0
assert 0 < = DEFAULT_OPACITY < = 1.0
assert DEFAULT_FONT_SIZE > 0
assert DEFAULT_MARGIN_X > = 0
assert DEFAULT_MARGIN_Y > = 0
assert DEFAULT_SCROLL_SPEED > 0
assert isinstance ( DEFAULT_FONT_COLOR , str )
# ============================================================
# WatermarkConfig 默认值
# ============================================================
class TestWatermarkConfigDefaults :
def test_default_constructor ( self ) :
config = WatermarkConfig ( )
assert config . mode == DEFAULT_MODE
assert config . position == DEFAULT_POSITION
assert config . scale == DEFAULT_SCALE
assert config . opacity == DEFAULT_OPACITY
assert config . font_size == DEFAULT_FONT_SIZE
assert config . font_color == DEFAULT_FONT_COLOR
assert config . margin_x == DEFAULT_MARGIN_X
assert config . margin_y == DEFAULT_MARGIN_Y
assert config . scroll is False
assert config . scroll_speed == DEFAULT_SCROLL_SPEED
def test_text_mode_default ( self ) :
config = WatermarkConfig ( )
assert config . mode == " text "
assert config . text == " "
assert config . font_path == " "
def test_image_mode_default ( self ) :
config = WatermarkConfig ( mode = " image " )
assert config . image_path == " "
# ============================================================
# from_dict
# ============================================================
class TestFromDict :
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_image_mode_missing_path_returns_none ( self ) :
result = WatermarkConfig . from_dict ( { " enabled " : True , " mode " : " image " } )
assert result is None
def test_image_mode_with_image_field ( self ) :
result = WatermarkConfig . from_dict ( { " enabled " : True , " mode " : " image " , " image " : " /path/to/wm.png " } )
assert result is not None
assert result . mode == " image "
assert result . image_path == " /path/to/wm.png "
def test_image_mode_with_image_path_field ( self ) :
result = WatermarkConfig . from_dict ( { " enabled " : True , " mode " : " image " , " image_path " : " /path/wm.png " } )
assert result is not None
assert result . image_path == " /path/wm.png "
def test_text_mode_missing_text_returns_none ( self ) :
result = WatermarkConfig . from_dict ( { " enabled " : True , " mode " : " text " } )
assert result is None
def test_text_mode_with_text ( self ) :
result = WatermarkConfig . from_dict ( { " enabled " : True , " mode " : " text " , " text " : " hello " } )
assert result is not None
assert result . mode == " text "
assert result . text == " hello "
def test_invalid_position_falls_back_default ( self ) :
result = WatermarkConfig . from_dict (
{
" enabled " : True ,
" mode " : " text " ,
" text " : " hello " ,
" position " : " invalid_pos " ,
}
)
assert result is not None
assert result . position == DEFAULT_POSITION
def test_custom_values_propagated ( self ) :
result = WatermarkConfig . from_dict (
{
" enabled " : True ,
" mode " : " text " ,
" text " : " 测试 " ,
" position " : " top_left " ,
" opacity " : 0.5 ,
" font_size " : 32 ,
" font_color " : " red " ,
" margin_x " : 30 ,
" margin_y " : 40 ,
" scroll " : True ,
" scroll_speed " : 100 ,
}
)
assert result is not None
assert result . position == " top_left "
assert result . opacity == 0.5
assert result . font_size == 32
assert result . font_color == " red "
assert result . margin_x == 30
assert result . margin_y == 40
assert result . scroll is True
assert result . scroll_speed == 100
def test_image_mode_custom_values ( self ) :
result = WatermarkConfig . from_dict (
{
" enabled " : True ,
" mode " : " image " ,
" image_path " : " /wm.png " ,
" scale " : 0.3 ,
" opacity " : 0.6 ,
}
)
assert result is not None
assert result . scale == 0.3
assert result . opacity == 0.6
def test_default_mode_when_unspecified ( self ) :
# 只给enabled和text, mode默认text
result = WatermarkConfig . from_dict ( { " enabled " : True , " text " : " hello " } )
assert result is not None
assert result . mode == DEFAULT_MODE
def test_text_empty_string_returns_none ( self ) :
result = WatermarkConfig . from_dict ( { " enabled " : True , " mode " : " text " , " text " : " " } )
assert result is None
# ============================================================
# validate
# ============================================================
class TestValidate :
def test_valid_text_config ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hello " )
ok , msg = config . validate ( )
assert ok is True
assert msg == " "
def test_valid_image_config ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " )
ok , msg = config . validate ( )
assert ok is True
assert msg == " "
def test_invalid_position ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , position = " nowhere " )
ok , msg = config . validate ( )
assert ok is False
assert " 位置 " in msg
def test_opacity_too_high ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , opacity = 1.5 )
ok , msg = config . validate ( )
assert ok is False
assert " 透明度 " in msg
def test_opacity_negative ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , opacity = - 0.1 )
ok , msg = config . validate ( )
assert ok is False
assert " 透明度 " in msg
def test_opacity_boundary_zero ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , opacity = 0.0 )
ok , _ = config . validate ( )
assert ok is True
def test_opacity_boundary_one ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , opacity = 1.0 )
ok , _ = config . validate ( )
assert ok is True
def test_image_missing_path ( self ) :
config = WatermarkConfig ( mode = " image " )
ok , msg = config . validate ( )
assert ok is False
assert " 图片路径 " in msg
def test_image_scale_too_small ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.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 = " /wm.png " , scale = 1.5 )
ok , msg = config . validate ( )
assert ok is False
assert " 缩放 " in msg
def test_image_scale_boundary_low ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , scale = 0.01 )
ok , _ = config . validate ( )
assert ok is True
def test_image_scale_boundary_high ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , scale = 1.0 )
ok , _ = config . validate ( )
assert ok is True
def test_text_empty ( self ) :
config = WatermarkConfig ( mode = " text " , text = " " )
ok , msg = config . validate ( )
assert ok is False
assert " 文字内容 " in msg
def test_font_size_zero ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , font_size = 0 )
ok , msg = config . validate ( )
assert ok is False
assert " 字体大小 " in msg
def test_font_size_negative ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , font_size = - 5 )
ok , msg = config . validate ( )
assert ok is False
assert " 字体大小 " in msg
def test_unknown_mode ( self ) :
config = WatermarkConfig ( mode = " video " , text = " hi " )
ok , msg = config . validate ( )
assert ok is False
assert " 模式 " in msg
# ============================================================
# has_effect
# ============================================================
class TestHasEffect :
def test_text_has_effect ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hello " , opacity = 0.5 , font_size = 24 )
assert config . has_effect ( ) is True
def test_text_empty_no_effect ( self ) :
config = WatermarkConfig ( mode = " text " , text = " " )
assert config . has_effect ( ) is False
def test_text_zero_opacity_no_effect ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , opacity = 0.0 )
assert config . has_effect ( ) is False
def test_text_zero_font_size_no_effect ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , font_size = 0 )
assert config . has_effect ( ) is False
def test_image_has_effect ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , opacity = 0.5 )
assert config . has_effect ( ) is True
def test_image_no_path_no_effect ( self ) :
config = WatermarkConfig ( mode = " image " )
assert config . has_effect ( ) is False
def test_image_zero_opacity_no_effect ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , opacity = 0.0 )
assert config . has_effect ( ) is False
def test_unknown_mode_no_effect ( self ) :
config = WatermarkConfig ( mode = " invalid " )
assert config . has_effect ( ) is False
# ============================================================
# calc_position
# ============================================================
class TestCalcPosition :
OUT_W = 1920
OUT_H = 1080
WM_W = 200
WM_H = 50
MX = 20
MY = 20
def test_top_left ( self ) :
x , y = calc_position ( " top_left " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
assert x == 20
assert y == 20
def test_top_center ( self ) :
x , y = calc_position ( " top_center " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
assert x == ( 1920 - 200 ) / / 2
assert y == 20
def test_top_right ( self ) :
x , y = calc_position ( " top_right " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
assert x == 1920 - 200 - 20
assert y == 20
def test_center_left ( self ) :
x , y = calc_position ( " center_left " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
assert x == 20
assert y == ( 1080 - 50 ) / / 2
def test_center ( self ) :
x , y = calc_position ( " center " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
assert x == ( 1920 - 200 ) / / 2
assert y == ( 1080 - 50 ) / / 2
def test_center_right ( self ) :
x , y = calc_position ( " center_right " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
assert x == 1920 - 200 - 20
assert y == ( 1080 - 50 ) / / 2
def test_bottom_left ( self ) :
x , y = calc_position ( " bottom_left " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
assert x == 20
assert y == 1080 - 50 - 20
def test_bottom_center ( self ) :
x , y = calc_position ( " bottom_center " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
assert x == ( 1920 - 200 ) / / 2
assert y == 1080 - 50 - 20
def test_bottom_right ( self ) :
x , y = calc_position ( " bottom_right " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
assert x == 1920 - 200 - 20
assert y == 1080 - 50 - 20
def test_invalid_position_defaults_bottom_right ( self ) :
x , y = calc_position ( " invalid " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
assert x == 1920 - 200 - 20
assert y == 1080 - 50 - 20
def test_zero_margins ( self ) :
x , y = calc_position ( " top_left " , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , 0 , 0 )
assert x == 0
assert y == 0
def test_all_nine_positions_are_unique ( self ) :
positions = set ( )
for pos in VALID_POSITIONS :
pos_xy = calc_position ( pos , self . OUT_W , self . OUT_H , self . WM_W , self . WM_H , self . MX , self . MY )
positions . add ( pos_xy )
assert len ( positions ) == 9
# ============================================================
# calc_scroll_x
# ============================================================
class TestCalcScrollX :
def test_returns_string ( self ) :
result = calc_scroll_x ( " bottom_left " , 1920 , 200 , 50 )
assert isinstance ( result , str )
def test_contains_output_width ( self ) :
result = calc_scroll_x ( " bottom_left " , 1920 , 200 , 50 )
assert " 1920 " in result
def test_contains_wm_width ( self ) :
result = calc_scroll_x ( " bottom_left " , 1920 , 200 , 50 )
assert " 200 " in result
def test_contains_speed ( self ) :
result = calc_scroll_x ( " bottom_left " , 1920 , 200 , 50 )
assert " 50 " in result
def test_contains_mod_keyword ( self ) :
result = calc_scroll_x ( " bottom_left " , 1920 , 200 , 50 )
assert " mod " in result
# ============================================================
# build_image_watermark_filter
# ============================================================
class TestBuildImageWatermarkFilter :
def test_returns_tuple ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , position = " top_left " )
result = build_image_watermark_filter ( " [in] " , " /wm.png " , 1920 , 1080 , " [out] " , config )
assert isinstance ( result , tuple )
assert len ( result ) == 2
def test_filter_contains_overlay ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , position = " bottom_right " )
filter_str , _ = build_image_watermark_filter ( " [in] " , " /wm.png " , 1920 , 1080 , " [out] " , config )
assert " overlay " in filter_str
def test_filter_contains_scale ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , scale = 0.3 )
filter_str , _ = build_image_watermark_filter ( " [in] " , " /wm.png " , 1920 , 1080 , " [out] " , config )
assert " scale= " in filter_str
def test_opacity_applied_when_below_one ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , opacity = 0.5 )
filter_str , _ = build_image_watermark_filter ( " [in] " , " /wm.png " , 1920 , 1080 , " [out] " , config )
assert " colorchannelmixer " in filter_str
assert " aa=0.5 " in filter_str
def test_opacity_one_no_mixer ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , opacity = 1.0 )
filter_str , _ = build_image_watermark_filter ( " [in] " , " /wm.png " , 1920 , 1080 , " [out] " , config )
assert " colorchannelmixer " not in filter_str
def test_input_args_contain_image_path ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /path/to/wm.png " )
_ , input_args = build_image_watermark_filter ( " [in] " , " /path/to/wm.png " , 1920 , 1080 , " [out] " , config )
assert input_args == [ " -i " , " /path/to/wm.png " ]
def test_output_label_present ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " )
filter_str , _ = build_image_watermark_filter ( " [in] " , " /wm.png " , 1920 , 1080 , " [final] " , config )
assert " [final] " in filter_str
def test_scroll_mode_contains_t_variable ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , scroll = True , scroll_speed = 60 )
filter_str , _ = build_image_watermark_filter ( " [in] " , " /wm.png " , 1920 , 1080 , " [out] " , config )
assert " *t " in filter_str or " t* " in filter_str
def test_static_mode_no_t_variable_in_overlay_x ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " , scroll = False )
filter_str , _ = build_image_watermark_filter ( " [in] " , " /wm.png " , 1920 , 1080 , " [out] " , config )
# 静态模式 x 是具体数值,不包含 t 变量
overlay_part = filter_str . split ( " overlay= " ) [ 1 ]
assert " t " not in overlay_part . split ( " : " ) [ 0 ] or " wm " in overlay_part . split ( " : " ) [ 0 ]
def test_semicolon_separated_filters ( self ) :
config = WatermarkConfig ( mode = " image " , image_path = " /wm.png " )
filter_str , _ = build_image_watermark_filter ( " [in] " , " /wm.png " , 1920 , 1080 , " [out] " , config )
assert " ; " in filter_str
# ============================================================
# build_text_watermark_filter
# ============================================================
class TestBuildTextWatermarkFilter :
def test_returns_string ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hello " )
result = build_text_watermark_filter ( " [in] " , " [out] " , config , 1920 , 1080 )
assert isinstance ( result , str )
def test_contains_drawtext ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hello " )
result = build_text_watermark_filter ( " [in] " , " [out] " , config , 1920 , 1080 )
assert " drawtext= " in result
def test_contains_text ( self ) :
config = WatermarkConfig ( mode = " text " , text = " 测试水印 " )
result = build_text_watermark_filter ( " [in] " , " [out] " , config , 1920 , 1080 )
assert " 测试水印 " in result
def test_fontsize_in_filter ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , font_size = 36 )
result = build_text_watermark_filter ( " [in] " , " [out] " , config , 1920 , 1080 )
assert " fontsize=36 " in result
def test_fontcolor_with_opacity ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , font_color = " red " , opacity = 0.5 )
result = build_text_watermark_filter ( " [in] " , " [out] " , config , 1920 , 1080 )
assert " fontcolor=red@0.5 " in result
def test_font_path_included ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , font_path = " /fonts/simsun.ttf " )
result = build_text_watermark_filter ( " [in] " , " [out] " , config , 1920 , 1080 )
assert " fontfile= " in result
assert " simsun.ttf " in result
def test_no_font_path_when_empty ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , font_path = " " )
result = build_text_watermark_filter ( " [in] " , " [out] " , config , 1920 , 1080 )
assert " fontfile= " not in result
def test_text_colon_escaped ( self ) :
config = WatermarkConfig ( mode = " text " , text = " time: 00:00 " )
result = build_text_watermark_filter ( " [in] " , " [out] " , config , 1920 , 1080 )
# 冒号应该被转义
assert " time \\ : " in result or " time \\ \\ : " in result
def test_scroll_mode_contains_mod ( self ) :
config = WatermarkConfig ( mode = " text " , text = " scrolling " , scroll = True , scroll_speed = 50 )
result = build_text_watermark_filter ( " [in] " , " [out] " , config , 1920 , 1080 )
assert " mod( " in result
def test_static_mode_has_numeric_x_y ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " , position = " top_left " )
result = build_text_watermark_filter ( " [in] " , " [out] " , config , 1920 , 1080 )
assert " x= " in result
assert " y= " in result
def test_output_label_present ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " )
result = build_text_watermark_filter ( " [in] " , " [final_out] " , config , 1920 , 1080 )
assert " [final_out] " in result
def test_input_label_present ( self ) :
config = WatermarkConfig ( mode = " text " , text = " hi " )
result = build_text_watermark_filter ( " [video_in] " , " [out] " , config , 1920 , 1080 )
assert result . startswith ( " [video_in] " )
# ============================================================
# 工具函数
# ============================================================
class TestGetPositionNames :
def test_returns_nine_names ( self ) :
names = get_position_names ( )
assert len ( names ) == 9
def test_order_is_correct ( self ) :
names = get_position_names ( )
# 按从上到下、从左到右
assert names [ 0 ] == " top_left "
assert names [ 1 ] == " top_center "
assert names [ 2 ] == " top_right "
assert names [ 3 ] == " center_left "
assert names [ 4 ] == " center "
assert names [ 5 ] == " center_right "
assert names [ 6 ] == " bottom_left "
assert names [ 7 ] == " bottom_center "
assert names [ 8 ] == " bottom_right "
def test_all_valid ( self ) :
names = get_position_names ( )
assert set ( names ) == VALID_POSITIONS
class TestGetPositionDisplayName :
def test_known_position ( self ) :
assert get_position_display_name ( " top_left " ) == " 左上 "
assert get_position_display_name ( " center " ) == " 中心 "
assert get_position_display_name ( " bottom_right " ) == " 右下 "
def test_unknown_position_returns_original ( self ) :
assert get_position_display_name ( " invalid " ) == " invalid "