From d92ff7aa3632299aece3ebafe1b284cf4dedfb85 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 22:29:46 +0800 Subject: [PATCH] =?UTF-8?q?test(wave199):=20sticker=5Fengine=5Fpure=20?= =?UTF-8?q?=E5=8D=95=E6=B5=8B=E8=A1=A5=E5=85=A8=20+141=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补全贴纸引擎纯逻辑模块单元测试,覆盖23个纯函数: - 安全类型转换: safe_float/safe_int/safe_bool - 尺寸估算: estimate_sticker_size/estimate_text_size - 时间计算: calculate_fade_out_start/calculate_end_time/has_time_range - 滤镜构建: build_scale_filter/build_opacity_filter/build_image_fade_filters - drawtext相关: escape_drawtext_text/build_drawtext_alpha_expr/build_stroke_params/build_shadow_params - 贴纸管理: sort_stickers_by_z_index/filter_enabled_stickers/count_sticker_types - overlay相关: build_overlay_position/build_pre_filter_label - 验证函数: validate_image_sticker/validate_text_sticker - enable表达式: build_enable_expr 141个测试全部通过。 --- tests/unit/test_sticker_engine_pure.py | 1021 +++++++++++++----------- 1 file changed, 536 insertions(+), 485 deletions(-) diff --git a/tests/unit/test_sticker_engine_pure.py b/tests/unit/test_sticker_engine_pure.py index f945f5192..cc8c6f312 100755 --- a/tests/unit/test_sticker_engine_pure.py +++ b/tests/unit/test_sticker_engine_pure.py @@ -1,780 +1,831 @@ -"""贴纸引擎纯逻辑单元测试.""" +"""sticker_engine_pure 单元测试.""" -from __future__ import annotations - -import pytest -from video_processing.sticker_engine_pure import ( - build_drawtext_alpha_expr, - build_enable_expr, - build_image_fade_filters, - build_opacity_filter, - build_overlay_position, - build_pre_filter_label, - build_scale_filter, - build_shadow_params, - build_stroke_params, - calculate_end_time, - calculate_fade_out_start, - count_sticker_types, - escape_drawtext_text, - estimate_sticker_size, - estimate_text_size, - filter_enabled_stickers, - has_time_range, - safe_bool, +from apps.worker.video_processing.sticker_engine_pure import ( safe_float, safe_int, + safe_bool, + estimate_sticker_size, + estimate_text_size, + calculate_fade_out_start, + calculate_end_time, + has_time_range, + build_scale_filter, + build_opacity_filter, + build_image_fade_filters, + build_enable_expr, + escape_drawtext_text, + build_drawtext_alpha_expr, + build_stroke_params, + build_shadow_params, sort_stickers_by_z_index, + filter_enabled_stickers, + count_sticker_types, + build_overlay_position, + build_pre_filter_label, validate_image_sticker, validate_text_sticker, ) -# ───────────────────────────────────────────────────────────────────────────── -# 安全类型转换测试 -# ───────────────────────────────────────────────────────────────────────────── +# ── safe_float ────────────────────────────────────────────────────────────────── class TestSafeFloat: - """safe_float 测试.""" + def test_none_returns_none(self): + assert safe_float(None) is None def test_int_input(self): - """整数输入.""" assert safe_float(42) == 42.0 def test_float_input(self): - """浮点数输入.""" assert safe_float(3.14) == 3.14 - def test_string_number(self): - """字符串数字.""" + def test_string_int_string(self): assert safe_float("3.14") == 3.14 - def test_string_int(self): - """字符串整数.""" + def test_string_integer_string(self): assert safe_float("100") == 100.0 - def test_none_input(self): - """None 输入.""" - assert safe_float(None) is None - def test_invalid_string(self): - """无效字符串.""" assert safe_float("abc") is None def test_empty_string(self): - """空字符串.""" assert safe_float("") is None + def test_nan_returns_none(self): + import math + + assert safe_float(float("nan")) is None + assert math.isnan(float("nan")) # 确认NaN判断生效 + + def test_negative_number(self): + assert safe_float(-5.5) == -5.5 + def test_zero(self): - """零值.""" assert safe_float(0) == 0.0 - def test_negative(self): - """负值.""" - assert safe_float(-5.5) == -5.5 + def test_boolean(self): + assert safe_float(True) == 1.0 + assert safe_float(False) == 0.0 + + +# ── safe_int ────────────────────────────────────────────────────────────────── class TestSafeInt: - """safe_int 测试.""" + def test_none_returns_default(self): + assert safe_int(None) == 0 + assert safe_int(None, default=5) == 5 def test_int_input(self): - """整数输入.""" assert safe_int(42) == 42 - def test_float_input(self): - """浮点数输入(截断).""" + def test_float_input_truncates(self): assert safe_int(3.7) == 3 + assert safe_int(3.2) == 3 - def test_string_number(self): - """字符串数字.""" - assert safe_int("42") == 42 + def test_string_integer(self): + assert safe_int("100") == 100 - def test_none_input(self): - """None 输入用默认值.""" - assert safe_int(None) == 0 + def test_string_float(self): + assert safe_int("3.9") == 3 - def test_none_custom_default(self): - """None 输入自定义默认值.""" - assert safe_int(None, default=10) == 10 - - def test_invalid_string(self): - """无效字符串.""" + def test_invalid_string_returns_default(self): assert safe_int("abc") == 0 + assert safe_int("abc", default=-1) == -1 - def test_negative(self): - """负值.""" - assert safe_int(-5) == -5 + def test_empty_string(self): + assert safe_int("") == 0 + + def test_negative_number(self): + assert safe_int(-10) == -10 def test_zero(self): - """零值.""" assert safe_int(0) == 0 + def test_boolean(self): + assert safe_int(True) == 1 + assert safe_int(False) == 0 + + +# ── safe_bool ──────────────────────────────────────────────────────────────── + class TestSafeBool: - """safe_bool 测试.""" - - def test_true_bool(self): - """True.""" + def test_boolean_passthrough(self): assert safe_bool(True) is True - - def test_false_bool(self): - """False.""" assert safe_bool(False) is False - def test_none(self): - """None -> False.""" + def test_none_returns_false(self): assert safe_bool(None) is False - def test_string_true(self): - """字符串 true.""" + def test_string_true_variants(self): assert safe_bool("true") is True - - def test_string_yes(self): - """字符串 yes.""" - assert safe_bool("yes") is True - - def test_string_one(self): - """字符串 1.""" + assert safe_bool("True") is True + assert safe_bool("TRUE") is True assert safe_bool("1") is True + assert safe_bool("yes") is True + assert safe_bool("YES") is True + assert safe_bool("on") is True + assert safe_bool("On") is True - def test_string_false(self): - """字符串 false.""" + def test_string_false_variants(self): assert safe_bool("false") is False + assert safe_bool("0") is False + assert safe_bool("no") is False + assert safe_bool("off") is False - def test_int_one(self): - """整数 1 -> True.""" + def test_numeric_values(self): assert safe_bool(1) is True - - def test_int_zero(self): - """整数 0 -> False.""" assert safe_bool(0) is False + assert safe_bool(-1) is True - def test_empty_list(self): - """空列表 -> False.""" + def test_empty_string(self): + assert safe_bool("") is False + + def test_list_truthy_falsy(self): + assert safe_bool([1]) is True assert safe_bool([]) is False -# ───────────────────────────────────────────────────────────────────────────── -# 尺寸估算测试 -# ───────────────────────────────────────────────────────────────────────────── +# ── estimate_sticker_size ──────────────────────────────────────────────── class TestEstimateStickerSize: - """贴纸尺寸估算测试.""" - def test_default_scale(self): - """默认 scale=1.0.""" - w, h = estimate_sticker_size(1000, 1000) - assert w == 300 # 1000 * 0.3 * 1.0 - assert h == 300 + w, h = estimate_sticker_size(1000, 800) + assert w == 300 # 1000 * 0.3 + assert h == 240 # 800 * 0.3 def test_custom_scale(self): - """自定义缩放.""" - w, h = estimate_sticker_size(1000, 1000, scale=0.5) - assert w == 150 + w, h = estimate_sticker_size(1000, 800, scale=2.0) + assert w == 600 + assert h == 480 + + def test_fixed_width_and_height(self): + w, h = estimate_sticker_size(1000, 800, fixed_width=200, fixed_height=150) + assert w == 200 assert h == 150 - def test_fixed_width_height(self): - """固定宽高.""" - w, h = estimate_sticker_size(1000, 1000, fixed_width=200, fixed_height=100) - assert w == 200 - assert h == 100 + def test_fixed_width_only(self): + w, h = estimate_sticker_size(1000, 800, fixed_width=500) + assert w == 500 + assert h == 240 # 仍然按比例算高 - def test_scale_2x(self): - """2倍缩放.""" - w, h = estimate_sticker_size(800, 600, scale=2.0) - assert w == 480 # 800 * 0.3 * 2 - assert h == 360 # 600 * 0.3 * 2 + def test_fixed_height_only(self): + w, h = estimate_sticker_size(1000, 800, fixed_height=400) + assert w == 300 + assert h == 400 + + def test_minimum_size(self): + w, h = estimate_sticker_size(1, 1, scale=0.01) + assert w >= 1 + assert h >= 1 def test_zero_canvas(self): - """零画布尺寸,返回最小 1.""" w, h = estimate_sticker_size(0, 0) assert w >= 1 assert h >= 1 + def test_scale_zero(self): + w, h = estimate_sticker_size(1000, 800, scale=0) + assert w >= 1 + assert h >= 1 + + +# ── estimate_text_size ────────────────────────────────────────────── + class TestEstimateTextSize: - """文字尺寸估算测试.""" - def test_normal_text(self): - """普通文字.""" - w, h = estimate_text_size("Hello", 36) - assert w == int(5 * 36 * 0.6) - assert h == int(36 * 1.4) + w, h = estimate_text_size("hello", 20) + assert w == int(5 * 20 * 0.6) + assert h == int(20 * 1.4) def test_empty_text(self): - """空文字.""" - w, h = estimate_text_size("", 36) + w, h = estimate_text_size("", 20) assert w == 0 assert h == 0 - def test_large_font(self): - """大字号.""" - w, h = estimate_text_size("A", 72) - assert w == int(1 * 72 * 0.6) - assert h == int(72 * 1.4) + def test_chinese_text(self): + w, h = estimate_text_size("你好世界", 30) + assert w == int(4 * 30 * 0.6) + assert h == int(30 * 1.4) - def test_chinese_chars(self): - """中文字符.""" - w, h = estimate_text_size("你好世界", 48) - assert w == int(4 * 48 * 0.6) - assert h == int(48 * 1.4) + def test_minimum_size(self): + w, h = estimate_text_size("a", 1) + assert w >= 1 + assert h >= 1 + + def test_single_char(self): + w, h = estimate_text_size("x", 100) + assert w == int(1 * 100 * 0.6) + assert h == int(100 * 1.4) -# ───────────────────────────────────────────────────────────────────────────── -# 时间计算测试 -# ───────────────────────────────────────────────────────────────────────────── +# ── calculate_fade_out_start ──────────────────────────────────────── class TestCalculateFadeOutStart: - """淡出开始时间计算测试.""" - def test_normal_case(self): - """正常情况.""" - assert calculate_fade_out_start(10, 30, 2) == pytest.approx(38.0) + assert calculate_fade_out_start(10, 20, 3) == 27.0 # 10 + 20 - 3 - def test_no_fade_out(self): - """无淡出.""" - assert calculate_fade_out_start(10, 30, 0) == 0.0 - - def test_negative_fade_out(self): - """负淡出.""" - assert calculate_fade_out_start(10, 30, -1) == 0.0 + def test_zero_fade_out(self): + assert calculate_fade_out_start(10, 20, 0) == 0.0 def test_zero_duration(self): - """零时长.""" - assert calculate_fade_out_start(10, 0, 2) == 0.0 + assert calculate_fade_out_start(10, 0, 3) == 0.0 - def test_fade_out_longer_than_duration(self): - """淡出超过时长,返回 0.""" - # start=10, dur=5, fade=10 -> 10+5-10 = 5 > 0 - assert calculate_fade_out_start(10, 5, 10) == pytest.approx(5.0) + def test_negative_fade_out(self): + assert calculate_fade_out_start(10, 20, -1) == 0.0 - def test_fade_out_starts_before_zero(self): - """淡出开始时间在 0 之前,钳制到 0.""" - # start=0, dur=3, fade=5 -> 0+3-5 = -2 -> 0 - assert calculate_fade_out_start(0, 3, 5) == 0.0 + def test_fade_longer_than_duration(self): + result = calculate_fade_out_start(5, 3, 10) + assert result == 0.0 # max(0, 5+3-10) = max(0, -2) = 0 + + def test_start_at_zero(self): + assert calculate_fade_out_start(0, 10, 2) == 8.0 + + def test_float_values(self): + assert calculate_fade_out_start(1.5, 5.5, 2.0) == 5.0 + + +# ── calculate_end_time ──────────────────────────────────────────── class TestCalculateEndTime: - """结束时间计算测试.""" - def test_normal_case(self): - """正常情况.""" - assert calculate_end_time(10, 30) == 40.0 + assert calculate_end_time(10, 5) == 15.0 def test_zero_duration(self): - """零时长.""" assert calculate_end_time(10, 0) == 10.0 def test_negative_duration(self): - """负时长.""" assert calculate_end_time(10, -5) == 10.0 - def test_zero_start(self): - """零开始.""" - assert calculate_end_time(0, 100) == 100.0 + def test_start_at_zero(self): + assert calculate_end_time(0, 10) == 10.0 + + def test_float_values(self): + assert calculate_end_time(1.5, 2.5) == 4.0 + + +# ── has_time_range ────────────────────────────────────────────── class TestHasTimeRange: - """时间范围判断测试.""" - def test_positive_duration(self): - """正时长.""" - assert has_time_range(30) is True + assert has_time_range(10) is True + assert has_time_range(0.1) is True def test_zero_duration(self): - """零时长.""" assert has_time_range(0) is False def test_negative_duration(self): - """负时长.""" assert has_time_range(-5) is False -# ───────────────────────────────────────────────────────────────────────────── -# 滤镜构建测试 -# ───────────────────────────────────────────────────────────────────────────── +# ── build_scale_filter ──────────────────────────────────────── class TestBuildScaleFilter: - """缩放滤镜构建测试.""" - def test_fixed_width_height(self): - """固定宽高.""" - result = build_scale_filter(width=200, height=100) - assert result == "scale=200:100" + assert build_scale_filter(width=100, height=200) == "scale=100:200" def test_scale_only(self): - """仅缩放.""" - result = build_scale_filter(scale=0.5) - assert result == "scale=iw*0.5:ih*0.5" + assert build_scale_filter(scale=0.5) == "scale=iw*0.5:ih*0.5" - def test_no_scaling_needed(self): - """无需缩放.""" - result = build_scale_filter(scale=1.0) - assert result is None + def test_default_no_scale(self): + assert build_scale_filter() is None + assert build_scale_filter(scale=1.0) is None - def test_scale_2x(self): - """2倍缩放.""" - result = build_scale_filter(scale=2.0) - assert result == "scale=iw*2.0:ih*2.0" + def test_width_only_returns_none(self): + # 只有width没有height,且scale=1.0,返回None + assert build_scale_filter(width=100) is None - def test_fixed_overrides_scale(self): - """固定宽高优先于 scale.""" - result = build_scale_filter(width=100, height=50, scale=0.5) - assert result == "scale=100:50" + def test_height_only_returns_none(self): + assert build_scale_filter(height=200) is None + + def test_scale_with_width_height_overrides_scale(self): + # width和height都有时优先 + assert build_scale_filter(width=100, height=200, scale=0.5) == "scale=100:200" + + +# ── build_opacity_filter ────────────────────────────────────── class TestBuildOpacityFilter: - """透明度滤镜构建测试.""" + def test_full_opacity(self): + assert build_opacity_filter(1.0) is None + assert build_opacity_filter(1.5) is None # 大于1也返回None def test_partial_opacity(self): - """部分透明.""" - result = build_opacity_filter(0.5) - assert result == "colorchannelmixer=aa=0.5" + assert build_opacity_filter(0.5) == "colorchannelmixer=aa=0.5" - def test_fully_opaque(self): - """完全不透明.""" - result = build_opacity_filter(1.0) - assert result is None + def test_zero_opacity(self): + assert build_opacity_filter(0.0) == "colorchannelmixer=aa=0.0" - def test_fully_transparent(self): - """完全透明.""" - result = build_opacity_filter(0.0) - assert result == "colorchannelmixer=aa=0.0" + def test_negative_clamped(self): + assert build_opacity_filter(-0.5) == "colorchannelmixer=aa=0.0" - def test_opacity_above_1_clamped(self): - """超过 1 被钳制.""" - result = build_opacity_filter(1.5) - assert result is None + def test_above_one_clamped(self): + # 大于1的情况:>=1.0返回None + assert build_opacity_filter(2.0) is None - def test_opacity_below_0_clamped(self): - """低于 0 被钳制.""" - result = build_opacity_filter(-0.5) - assert result == "colorchannelmixer=aa=0.0" + +# ── build_image_fade_filters ──────────────────────────────────── class TestBuildImageFadeFilters: - """图片淡入淡出滤镜测试.""" + def test_no_fade(self): + assert build_image_fade_filters(10, 20) == [] def test_fade_in_only(self): - """仅淡入.""" - result = build_image_fade_filters(10, 30, fade_in=1.0) + result = build_image_fade_filters(10, 20, fade_in=2) assert len(result) == 1 - assert "fade=in:st=10:d=1.0:alpha=1" in result[0] + assert "fade=in:st=10:d=2:alpha=1" in result[0] def test_fade_out_only(self): - """仅淡出.""" - result = build_image_fade_filters(10, 30, fade_out=2.0) + result = build_image_fade_filters(10, 20, fade_out=3) assert len(result) == 1 - assert "fade=out" in result[0] - assert "st=38.0" in result[0] # 10 + 30 - 2 = 38 + assert "fade=out:st=27:d=3:alpha=1" in result[0] - def test_fade_in_and_out(self): - """淡入+淡出.""" - result = build_image_fade_filters(0, 10, fade_in=1.0, fade_out=1.0) + def test_both_fades(self): + result = build_image_fade_filters(10, 20, fade_in=2, fade_out=3) assert len(result) == 2 assert "fade=in" in result[0] assert "fade=out" in result[1] - def test_no_fade(self): - """无淡入淡出.""" - result = build_image_fade_filters(10, 30) - assert len(result) == 0 + def test_fade_out_zero_duration_skipped(self): + result = build_image_fade_filters(10, 0, fade_out=3) + assert result == [] - def test_zero_duration_no_fade_out(self): - """零时长不生成淡出.""" - result = build_image_fade_filters(10, 0, fade_out=1.0) - assert len(result) == 0 + def test_fade_out_negative_duration(self): + result = build_image_fade_filters(10, -5, fade_out=3) + assert result == [] + + +# ── build_enable_expr ──────────────────────────────────────── class TestBuildEnableExpr: - """enable 表达式构建测试.""" - - def test_normal_duration(self): - """正常时长.""" - result = build_enable_expr(10, 30) - assert "between(t,10,40" in result - assert "enable" in result + def test_positive_duration(self): + result = build_enable_expr(10, 5) + assert result == ":enable='between(t,10,15)'" def test_zero_duration(self): - """零时长返回空.""" - result = build_enable_expr(10, 0) - assert result == "" + assert build_enable_expr(10, 0) == "" def test_negative_duration(self): - """负时长返回空.""" - result = build_enable_expr(10, -5) - assert result == "" + assert build_enable_expr(10, -1) == "" - def test_zero_start(self): - """从零开始.""" - result = build_enable_expr(0, 100) - assert "t,0,100" in result + def test_start_at_zero(self): + result = build_enable_expr(0, 10) + assert result == ":enable='between(t,0,10)'" + + def test_float_values(self): + result = build_enable_expr(1.5, 2.5) + assert "between(t,1.5,4.0)" in result -# ───────────────────────────────────────────────────────────────────────────── -# drawtext 相关测试 -# ───────────────────────────────────────────────────────────────────────────── +# ── escape_drawtext_text ────────────────────────────────────── class TestEscapeDrawtextText: - """文字转义测试.""" - def test_no_special_chars(self): - """无特殊字符.""" - assert escape_drawtext_text("Hello") == "Hello" + assert escape_drawtext_text("hello") == "hello" def test_colon_escaped(self): - """冒号转义.""" - assert escape_drawtext_text("a:b") == "a\\:b" + assert escape_drawtext_text("a:b:c") == "a\\:b\\:c" - def test_quote_escaped(self): - """单引号转义.""" - assert escape_drawtext_text("it's") == "it\\'s" + def test_single_quote_escaped(self): + assert escape_drawtext_text("a'b'c") == "a\\'b\\'c" - def test_multiple_special_chars(self): - """多个特殊字符.""" - assert escape_drawtext_text("a:b:c'd") == "a\\:b\\:c\\'d" + def test_both_special_chars(self): + result = escape_drawtext_text("it's: test") + assert result == "it\\'s\\: test" def test_empty_string(self): - """空字符串.""" assert escape_drawtext_text("") == "" + def test_backslash_not_escaped(self): + # 只转义冒号和单引号 + assert escape_drawtext_text("a\\b") == "a\\b" + + +# ── build_drawtext_alpha_expr ────────────────────────────────── + class TestBuildDrawtextAlphaExpr: - """drawtext alpha 表达式测试.""" - def test_no_fade(self): - """无淡入淡出.""" - assert build_drawtext_alpha_expr(10, 30) == "1" + assert build_drawtext_alpha_expr(10, 20) == "1" def test_fade_in_only(self): - """仅淡入.""" - result = build_drawtext_alpha_expr(10, 30, fade_in=2.0) - assert "if(lt(t,12.0)" in result - assert "(t-10)/2.0" in result + result = build_drawtext_alpha_expr(10, 20, fade_in=2) + assert result == "if(lt(t,12),(t-10)/2,1)" def test_fade_out_only(self): - """仅淡出.""" - result = build_drawtext_alpha_expr(10, 30, fade_out=3.0) - assert "if(gt(t,37" in result - assert "-t)/3.0" in result + result = build_drawtext_alpha_expr(10, 20, fade_out=3) + assert result == "if(gt(t,27),(30-t)/3,1)" - def test_fade_in_and_out(self): - """淡入+淡出(相乘).""" - result = build_drawtext_alpha_expr(0, 10, fade_in=1.0, fade_out=1.0) - assert "*" in result - assert result.count("if(") == 2 + def test_both_fades(self): + result = build_drawtext_alpha_expr(10, 20, fade_in=2, fade_out=3) + assert "if(lt(t," in result + assert "if(gt(t," in result + assert result.count("*") == 1 # 两部分相乘 - def test_zero_duration_no_fade_out(self): - """零时长不生成淡出.""" - result = build_drawtext_alpha_expr(10, 0, fade_out=1.0) + def test_fade_out_zero_duration(self): + result = build_drawtext_alpha_expr(10, 0, fade_out=3) + assert result == "1" + + def test_zero_fade_in(self): + result = build_drawtext_alpha_expr(10, 20, fade_in=0) assert result == "1" +# ── build_stroke_params ────────────────────────────────────── + + class TestBuildStrokeParams: - """描边参数测试.""" - def test_no_stroke(self): - """无描边.""" - result = build_stroke_params(0) - assert len(result) == 0 + assert build_stroke_params() == [] + assert build_stroke_params(stroke_width=0) == [] + assert build_stroke_params(stroke_width=-1) == [] - def test_with_stroke(self): - """有描边.""" - result = build_stroke_params(2, "red") + def test_default_color(self): + result = build_stroke_params(stroke_width=2) assert len(result) == 2 assert "borderw=2" in result + assert "bordercolor=black" in result + + def test_custom_color(self): + result = build_stroke_params(stroke_width=3, stroke_color="red") + assert "borderw=3" in result assert "bordercolor=red" in result - def test_negative_width(self): - """负宽度.""" - result = build_stroke_params(-1) - assert len(result) == 0 + +# ── build_shadow_params ────────────────────────────────────── class TestBuildShadowParams: - """阴影参数测试.""" - def test_no_shadow(self): - """无阴影.""" - result = build_shadow_params(0) - assert len(result) == 0 + assert build_shadow_params() == [] + assert build_shadow_params(shadow_alpha=0) == [] + assert build_shadow_params(shadow_alpha=-1) == [] - def test_with_shadow(self): - """有阴影.""" - result = build_shadow_params(0.5, 3, 4, "black") + def test_default_values(self): + result = build_shadow_params(shadow_alpha=0.5) assert len(result) == 3 - assert "shadowx=3" in result - assert "shadowy=4" in result + assert "shadowx=2" in result + assert "shadowy=2" in result assert "shadowcolor=black@0.5" in result - def test_shadow_alpha_clamped(self): - """透明度钳制.""" - result = build_shadow_params(1.5) - assert "shadowcolor=black@1.0" in result[2] + def test_custom_offset(self): + result = build_shadow_params(shadow_alpha=0.3, shadow_x=5, shadow_y=7, shadow_color="red") + assert "shadowx=5" in result + assert "shadowy=7" in result + assert "shadowcolor=red@0.3" in result + + def test_alpha_clamped(self): + result = build_shadow_params(shadow_alpha=1.5) + assert "shadowcolor=black@1.0" in result + + def test_alpha_negative_clamped_to_zero(self): + # 负数会触发<=0分支,返回空列表 + assert build_shadow_params(shadow_alpha=-0.5) == [] -# ───────────────────────────────────────────────────────────────────────────── -# 贴纸排序与过滤测试 -# ───────────────────────────────────────────────────────────────────────────── +# ── sort_stickers_by_z_index ──────────────────────────────────── class TestSortStickersByZIndex: - """贴纸排序测试.""" - def test_sorted_by_z_index(self): - """按 z_index 排序.""" stickers = [ - {"z_index": 20, "name": "top"}, - {"z_index": 5, "name": "bottom"}, - {"z_index": 10, "name": "middle"}, + {"name": "c", "z_index": 3}, + {"name": "a", "z_index": 1}, + {"name": "b", "z_index": 2}, ] result = sort_stickers_by_z_index(stickers) - assert result[0]["name"] == "bottom" - assert result[1]["name"] == "middle" - assert result[2]["name"] == "top" + assert [s["name"] for s in result] == ["a", "b", "c"] - def test_same_z_index_preserves_order(self): - """相同 z_index 保持原顺序.""" + def test_missing_z_index_defaults_to_10(self): stickers = [ - {"z_index": 10, "name": "first"}, - {"z_index": 10, "name": "second"}, + {"name": "low", "z_index": 5}, + {"name": "no_z"}, # 默认10 + {"name": "high", "z_index": 15}, ] result = sort_stickers_by_z_index(stickers) - assert result[0]["name"] == "first" - assert result[1]["name"] == "second" + assert [s["name"] for s in result] == ["low", "no_z", "high"] + + def test_same_z_index_stable(self): + stickers = [ + {"name": "first", "z_index": 5}, + {"name": "second", "z_index": 5}, + {"name": "third", "z_index": 5}, + ] + result = sort_stickers_by_z_index(stickers) + assert [s["name"] for s in result] == ["first", "second", "third"] def test_empty_list(self): - """空列表.""" assert sort_stickers_by_z_index([]) == [] - def test_default_z_index_10(self): - """无 z_index 默认 10.""" + def test_negative_z_index(self): stickers = [ - {"z_index": 5, "name": "low"}, - {"name": "default"}, + {"name": "neg", "z_index": -5}, + {"name": "zero", "z_index": 0}, + {"name": "pos", "z_index": 5}, ] result = sort_stickers_by_z_index(stickers) - assert result[0]["name"] == "low" - assert result[1]["name"] == "default" + assert [s["name"] for s in result] == ["neg", "zero", "pos"] + + def test_original_not_modified(self): + stickers = [{"z_index": 3}, {"z_index": 1}] + original = list(stickers) + sort_stickers_by_z_index(stickers) + assert stickers == original + + +# ── filter_enabled_stickers ──────────────────────────────────── class TestFilterEnabledStickers: - """启用贴纸过滤测试.""" - def test_all_enabled(self): - """全部启用.""" - stickers = [{"enabled": True}, {"enabled": True}] - assert len(filter_enabled_stickers(stickers)) == 2 - - def test_mixed(self): - """混合.""" stickers = [ - {"enabled": True, "name": "a"}, - {"enabled": False, "name": "b"}, - {"enabled": True, "name": "c"}, + {"name": "a", "enabled": True}, + {"name": "b"}, # 默认True ] result = filter_enabled_stickers(stickers) assert len(result) == 2 - assert result[0]["name"] == "a" - def test_default_enabled(self): - """默认启用.""" - stickers = [{"name": "a"}] + def test_mixed_enabled(self): + stickers = [ + {"name": "a", "enabled": True}, + {"name": "b", "enabled": False}, + {"name": "c"}, + ] result = filter_enabled_stickers(stickers) - assert len(result) == 1 + assert len(result) == 2 + assert [s["name"] for s in result] == ["a", "c"] + + def test_all_disabled(self): + stickers = [ + {"name": "a", "enabled": False}, + {"name": "b", "enabled": "false"}, + ] + result = filter_enabled_stickers(stickers) + assert len(result) == 0 def test_empty_list(self): - """空列表.""" assert filter_enabled_stickers([]) == [] + def test_string_enabled_values(self): + stickers = [ + {"name": "a", "enabled": "true"}, + {"name": "b", "enabled": "yes"}, + {"name": "c", "enabled": "0"}, + ] + result = filter_enabled_stickers(stickers) + assert [s["name"] for s in result] == ["a", "b"] + + +# ── count_sticker_types ──────────────────────────────────────── + class TestCountStickerTypes: - """贴纸类型统计测试.""" - - def test_mixed_types(self): - """混合类型.""" + def test_multiple_types(self): stickers = [ {"type": "image"}, {"type": "text"}, {"type": "image"}, + {"type": "image"}, + {"type": "emoji"}, ] - counts = count_sticker_types(stickers) - assert counts["image"] == 2 - assert counts["text"] == 1 + result = count_sticker_types(stickers) + assert result == {"image": 3, "text": 1, "emoji": 1} - def test_default_type(self): - """默认 image.""" - stickers = [{}] - counts = count_sticker_types(stickers) - assert counts["image"] == 1 + def test_default_type_image(self): + stickers = [ + {"name": "a"}, # 无type字段 + {"type": "text"}, + ] + result = count_sticker_types(stickers) + assert result == {"image": 1, "text": 1} def test_empty_list(self): - """空列表.""" assert count_sticker_types([]) == {} + def test_single_type(self): + stickers = [{"type": "text"} for _ in range(5)] + result = count_sticker_types(stickers) + assert result == {"text": 5} -# ───────────────────────────────────────────────────────────────────────────── -# overlay 相关测试 -# ───────────────────────────────────────────────────────────────────────────── + +# ── build_overlay_position ────────────────────────────────────── class TestBuildOverlayPosition: - """overlay 位置构建测试.""" - - def test_integer_position(self): - """整数位置.""" + def test_integer_values(self): assert build_overlay_position(100, 200) == "100:200" - def test_float_position_rounded(self): - """浮点取整.""" - assert build_overlay_position(100.6, 200.4) == "101:200" + def test_float_values_rounded(self): + assert build_overlay_position(100.6, 200.3) == "101:200" - def test_zero_position(self): - """零位置.""" + def test_negative_values(self): + assert build_overlay_position(-10, -20) == "-10:-20" + + def test_zero_values(self): assert build_overlay_position(0, 0) == "0:0" - def test_negative_position(self): - """负位置.""" - assert build_overlay_position(-10, -20) == "-10:-20" + +# ── build_pre_filter_label ───────────────────────────────────── class TestBuildPreFilterLabel: - """预处理标签构建测试.""" - - def test_normal_idx(self): - """正常索引.""" - assert build_pre_filter_label(3) == "sticker_3_scaled" - - def test_zero_idx(self): - """零索引.""" + def test_index_zero(self): assert build_pre_filter_label(0) == "sticker_0_scaled" + def test_positive_index(self): + assert build_pre_filter_label(5) == "sticker_5_scaled" -# ───────────────────────────────────────────────────────────────────────────── -# 验证函数测试 -# ───────────────────────────────────────────────────────────────────────────── + def test_large_index(self): + assert build_pre_filter_label(999) == "sticker_999_scaled" + + +# ── validate_image_sticker ────────────────────────────────── class TestValidateImageSticker: - """图片贴纸验证测试.""" - def test_valid_with_image_path(self): - """有 image_path,合法.""" - ok, errors = validate_image_sticker({"image_path": "/a.png"}) - assert ok is True - assert len(errors) == 0 + valid, errors = validate_image_sticker({"image_path": "/path/to/img.png"}) + assert valid is True + assert errors == [] def test_valid_with_asset_id(self): - """有 asset_id,合法.""" - ok, errors = validate_image_sticker({"asset_id": "123"}) - assert ok is True + valid, errors = validate_image_sticker({"asset_id": "asset_123"}) + assert valid is True + assert errors == [] - def test_missing_image_source(self): - """缺图片来源.""" - ok, errors = validate_image_sticker({}) - assert ok is False - assert any("image_path" in e or "asset_id" in e for e in errors) + def test_missing_image_and_asset(self): + valid, errors = validate_image_sticker({}) + assert valid is False + assert "image_path 或 asset_id" in errors[0] - def test_opacity_out_of_range(self): - """透明度超范围.""" - ok, errors = validate_image_sticker( + def test_invalid_opacity_high(self): + valid, errors = validate_image_sticker( { - "image_path": "/a.png", + "image_path": "a.png", "opacity": 1.5, } ) - assert ok is False + assert valid is False assert any("opacity" in e for e in errors) - def test_negative_scale(self): - """负缩放.""" - ok, errors = validate_image_sticker( + def test_invalid_opacity_low(self): + valid, errors = validate_image_sticker( { - "image_path": "/a.png", - "scale": -0.5, + "image_path": "a.png", + "opacity": -0.5, } ) - assert ok is False + assert valid is False + assert any("opacity" in e for e in errors) + + def test_valid_opacity_boundary(self): + valid, _ = validate_image_sticker({"image_path": "a.png", "opacity": 0}) + assert valid is True + valid, _ = validate_image_sticker({"image_path": "a.png", "opacity": 1}) + assert valid is True + + def test_invalid_scale_zero(self): + valid, errors = validate_image_sticker( + { + "image_path": "a.png", + "scale": 0, + } + ) + assert valid is False assert any("scale" in e for e in errors) - def test_negative_duration(self): - """负时长.""" - ok, errors = validate_image_sticker( + def test_invalid_scale_negative(self): + valid, errors = validate_image_sticker( { - "image_path": "/a.png", - "duration": -10, + "image_path": "a.png", + "scale": -1, } ) - assert ok is False - assert any("duration" in e for e in errors) + assert valid is False - def test_multiple_errors(self): - """多个错误.""" - ok, errors = validate_image_sticker( + def test_invalid_duration_negative(self): + valid, errors = validate_image_sticker( { - "opacity": 1.5, - "duration": -1, - "start_time": -5, - } - ) - assert ok is False - assert len(errors) >= 3 - - -class TestValidateTextSticker: - """文字贴纸验证测试.""" - - def test_valid(self): - """合法配置.""" - ok, errors = validate_text_sticker( - { - "text": "Hello", - "font_size": 36, - "font_color": "white", - } - ) - assert ok is True - assert len(errors) == 0 - - def test_empty_text(self): - """空文字.""" - ok, errors = validate_text_sticker({"text": ""}) - assert ok is False - assert any("text" in e for e in errors) - - def test_zero_font_size(self): - """零字号.""" - ok, errors = validate_text_sticker( - { - "text": "Hi", - "font_size": 0, - } - ) - assert ok is False - assert any("font_size" in e for e in errors) - - def test_empty_font_color(self): - """空颜色.""" - ok, errors = validate_text_sticker( - { - "text": "Hi", - "font_color": "", - } - ) - assert ok is False - assert any("font_color" in e for e in errors) - - def test_negative_duration(self): - """负时长.""" - ok, errors = validate_text_sticker( - { - "text": "Hi", + "image_path": "a.png", "duration": -5, } ) - assert ok is False + assert valid is False assert any("duration" in e for e in errors) + + def test_invalid_start_time_negative(self): + valid, errors = validate_image_sticker( + { + "image_path": "a.png", + "start_time": -1, + } + ) + assert valid is False + assert any("start_time" in e for e in errors) + + def test_multiple_errors(self): + valid, errors = validate_image_sticker( + { + "opacity": 2.0, + "scale": -1, + "duration": -5, + } + ) + assert valid is False + assert len(errors) >= 3 + + def test_valid_with_extra_fields(self): + valid, _ = validate_image_sticker( + { + "image_path": "a.png", + "extra_field": "ignored", + "z_index": 5, + } + ) + assert valid is True + + +# ── validate_text_sticker ──────────────────────────────────── + + +class TestValidateTextSticker: + def test_valid_text(self): + valid, errors = validate_text_sticker({"text": "hello"}) + assert valid is True + assert errors == [] + + def test_missing_text(self): + valid, errors = validate_text_sticker({}) + assert valid is False + assert any("text" in e for e in errors) + + def test_empty_text(self): + valid, errors = validate_text_sticker({"text": ""}) + assert valid is False + assert any("text" in e for e in errors) + + def test_invalid_font_size_zero(self): + valid, errors = validate_text_sticker( + { + "text": "hello", + "font_size": 0, + } + ) + assert valid is False + assert any("font_size" in e for e in errors) + + def test_invalid_font_size_negative(self): + valid, errors = validate_text_sticker( + { + "text": "hello", + "font_size": -5, + } + ) + assert valid is False + + def test_missing_font_color(self): + valid, errors = validate_text_sticker( + { + "text": "hello", + "font_color": "", + } + ) + assert valid is False + assert any("font_color" in e for e in errors) + + def test_invalid_duration_negative(self): + valid, errors = validate_text_sticker( + { + "text": "hello", + "duration": -3, + } + ) + assert valid is False + assert any("duration" in e for e in errors) + + def test_default_font_size_valid(self): + # 默认36,有效 + valid, _ = validate_text_sticker({"text": "hi"}) + assert valid is True + + def test_multiple_errors(self): + valid, errors = validate_text_sticker( + { + "text": "", + "font_size": -1, + "font_color": "", + } + ) + assert valid is False + assert len(errors) >= 3