490 lines
16 KiB
Python
Executable File
490 lines
16 KiB
Python
Executable File
"""pip_config 模块单测 — 纯逻辑,无外部依赖."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from packages.domain.pip_config import (
|
|
ANIMATION_FADE,
|
|
ANIMATION_SCALE,
|
|
ANIMATION_SLIDE_BOTTOM,
|
|
ANIMATION_SLIDE_LEFT,
|
|
ANIMATION_SLIDE_RIGHT,
|
|
ANIMATION_SLIDE_TOP,
|
|
POSITION_BOTTOM_LEFT,
|
|
POSITION_BOTTOM_RIGHT,
|
|
POSITION_CENTER,
|
|
POSITION_CENTER_LEFT,
|
|
POSITION_CENTER_RIGHT,
|
|
POSITION_TOP_CENTER,
|
|
POSITION_TOP_LEFT,
|
|
POSITION_TOP_RIGHT,
|
|
PiPConfig,
|
|
PiPLayerConfig,
|
|
calculate_pip_position,
|
|
parse_size_value,
|
|
)
|
|
|
|
# ── PiPLayerConfig 默认值 ────────────────────────────────────────────────────
|
|
|
|
|
|
class TestPiPLayerConfigDefaults:
|
|
def test_default_values(self):
|
|
cfg = PiPLayerConfig()
|
|
assert cfg.source == ""
|
|
assert cfg.source_type == "asset_id"
|
|
assert cfg.position == POSITION_BOTTOM_RIGHT
|
|
assert cfg.x == 0
|
|
assert cfg.y == 0
|
|
assert cfg.margin == 20
|
|
assert cfg.width == "25%"
|
|
assert cfg.height == ""
|
|
assert cfg.opacity == 1.0
|
|
assert cfg.corner_radius == 0
|
|
assert cfg.border_width == 0
|
|
assert cfg.border_color == "white"
|
|
assert cfg.start_time == 0.0
|
|
assert cfg.duration == 0.0
|
|
assert cfg.animation_in == ""
|
|
assert cfg.animation_out == ""
|
|
assert cfg.animation_duration == 0.5
|
|
assert cfg.z_index == 1
|
|
|
|
|
|
# ── PiPLayerConfig.validate ──────────────────────────────────────────────────
|
|
|
|
|
|
class TestPiPLayerConfigValidate:
|
|
def test_valid_config(self):
|
|
cfg = PiPLayerConfig(source="asset_123")
|
|
ok, err = cfg.validate()
|
|
assert ok is True
|
|
assert err == ""
|
|
|
|
def test_empty_source_invalid(self):
|
|
cfg = PiPLayerConfig(source="")
|
|
ok, err = cfg.validate()
|
|
assert ok is False
|
|
assert "source" in err
|
|
|
|
def test_invalid_position(self):
|
|
cfg = PiPLayerConfig(source="a", position="invalid_pos")
|
|
ok, err = cfg.validate()
|
|
assert ok is False
|
|
assert "position" in err
|
|
|
|
def test_custom_position_valid(self):
|
|
cfg = PiPLayerConfig(source="a", position="custom", x=10, y=20)
|
|
ok, err = cfg.validate()
|
|
assert ok is True
|
|
|
|
def test_opacity_too_low(self):
|
|
cfg = PiPLayerConfig(source="a", opacity=-0.1)
|
|
ok, err = cfg.validate()
|
|
assert ok is False
|
|
assert "opacity" in err
|
|
|
|
def test_opacity_too_high(self):
|
|
cfg = PiPLayerConfig(source="a", opacity=1.5)
|
|
ok, err = cfg.validate()
|
|
assert ok is False
|
|
assert "opacity" in err
|
|
|
|
def test_opacity_boundary_zero(self):
|
|
cfg = PiPLayerConfig(source="a", opacity=0.0)
|
|
ok, _ = cfg.validate()
|
|
assert ok is True
|
|
|
|
def test_opacity_boundary_one(self):
|
|
cfg = PiPLayerConfig(source="a", opacity=1.0)
|
|
ok, _ = cfg.validate()
|
|
assert ok is True
|
|
|
|
def test_negative_corner_radius(self):
|
|
cfg = PiPLayerConfig(source="a", corner_radius=-5)
|
|
ok, err = cfg.validate()
|
|
assert ok is False
|
|
assert "corner_radius" in err
|
|
|
|
def test_negative_start_time(self):
|
|
cfg = PiPLayerConfig(source="a", start_time=-1.0)
|
|
ok, err = cfg.validate()
|
|
assert ok is False
|
|
assert "start_time" in err
|
|
|
|
def test_negative_duration(self):
|
|
cfg = PiPLayerConfig(source="a", duration=-2.0)
|
|
ok, err = cfg.validate()
|
|
assert ok is False
|
|
assert "duration" in err
|
|
|
|
def test_zero_duration_valid(self):
|
|
cfg = PiPLayerConfig(source="a", duration=0.0)
|
|
ok, _ = cfg.validate()
|
|
assert ok is True
|
|
|
|
def test_invalid_animation_in(self):
|
|
cfg = PiPLayerConfig(source="a", animation_in="invalid")
|
|
ok, err = cfg.validate()
|
|
assert ok is False
|
|
assert "入场动画" in err
|
|
|
|
def test_invalid_animation_out(self):
|
|
cfg = PiPLayerConfig(source="a", animation_out="invalid")
|
|
ok, err = cfg.validate()
|
|
assert ok is False
|
|
assert "出场动画" in err
|
|
|
|
def test_valid_animation_fade(self):
|
|
cfg = PiPLayerConfig(source="a", animation_in=ANIMATION_FADE, animation_out=ANIMATION_FADE)
|
|
ok, _ = cfg.validate()
|
|
assert ok is True
|
|
|
|
def test_valid_animation_slide(self):
|
|
cfg = PiPLayerConfig(
|
|
source="a",
|
|
animation_in=ANIMATION_SLIDE_LEFT,
|
|
animation_out=ANIMATION_SLIDE_RIGHT,
|
|
)
|
|
ok, _ = cfg.validate()
|
|
assert ok is True
|
|
|
|
def test_valid_animation_scale(self):
|
|
cfg = PiPLayerConfig(source="a", animation_in=ANIMATION_SCALE)
|
|
ok, _ = cfg.validate()
|
|
assert ok is True
|
|
|
|
def test_empty_animation_valid(self):
|
|
cfg = PiPLayerConfig(source="a", animation_in="", animation_out="")
|
|
ok, _ = cfg.validate()
|
|
assert ok is True
|
|
|
|
def test_negative_animation_duration(self):
|
|
cfg = PiPLayerConfig(source="a", animation_duration=-0.5)
|
|
ok, err = cfg.validate()
|
|
assert ok is False
|
|
assert "animation_duration" in err
|
|
|
|
|
|
# ── PiPConfig.from_dict ──────────────────────────────────────────────────────
|
|
|
|
|
|
class TestPiPConfigFromDict:
|
|
def test_none_data_disabled(self):
|
|
cfg = PiPConfig.from_dict(None)
|
|
assert cfg.enabled is False
|
|
assert cfg.layers == []
|
|
|
|
def test_empty_dict_disabled(self):
|
|
cfg = PiPConfig.from_dict({})
|
|
assert cfg.enabled is False
|
|
assert cfg.layers == []
|
|
|
|
def test_enabled_false(self):
|
|
cfg = PiPConfig.from_dict({"enabled": False, "layers": [{"source": "a"}]})
|
|
assert cfg.enabled is False
|
|
assert cfg.layers == []
|
|
|
|
def test_single_layer(self):
|
|
cfg = PiPConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"layers": [{"source": "asset_1"}],
|
|
}
|
|
)
|
|
assert cfg.enabled is True
|
|
assert cfg.layer_count == 1
|
|
assert cfg.layers[0].source == "asset_1"
|
|
|
|
def test_multiple_layers_sorted_by_z_index(self):
|
|
cfg = PiPConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"layers": [
|
|
{"source": "top", "z_index": 10},
|
|
{"source": "bottom", "z_index": 1},
|
|
{"source": "mid", "z_index": 5},
|
|
],
|
|
}
|
|
)
|
|
assert cfg.layer_count == 3
|
|
assert [l.source for l in cfg.layers] == ["bottom", "mid", "top"]
|
|
|
|
def test_invalid_layer_skipped(self):
|
|
cfg = PiPConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"layers": [
|
|
{"source": "valid"},
|
|
{"source": ""}, # 无效:空source
|
|
],
|
|
}
|
|
)
|
|
assert cfg.layer_count == 1
|
|
assert cfg.layers[0].source == "valid"
|
|
|
|
def test_all_invalid_layers_disabled(self):
|
|
cfg = PiPConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"layers": [
|
|
{"source": ""},
|
|
{"source": "", "opacity": 2.0},
|
|
],
|
|
}
|
|
)
|
|
assert cfg.enabled is False
|
|
assert cfg.layer_count == 0
|
|
|
|
def test_layer_parse_error_skipped(self):
|
|
cfg = PiPConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"layers": [
|
|
{"source": "valid"},
|
|
{"source": "bad_margin", "margin": "not_a_number"},
|
|
],
|
|
}
|
|
)
|
|
assert cfg.layer_count == 1
|
|
|
|
def test_layer_full_fields(self):
|
|
cfg = PiPConfig.from_dict(
|
|
{
|
|
"enabled": True,
|
|
"layers": [
|
|
{
|
|
"source": "asset_1",
|
|
"source_type": "url",
|
|
"position": "top_left",
|
|
"x": 10,
|
|
"y": 20,
|
|
"margin": 30,
|
|
"width": "30%",
|
|
"height": "20%",
|
|
"opacity": 0.8,
|
|
"corner_radius": 10,
|
|
"border_width": 2,
|
|
"border_color": "black",
|
|
"start_time": 1.5,
|
|
"duration": 5.0,
|
|
"animation_in": "fade",
|
|
"animation_out": "slide_right",
|
|
"animation_duration": 0.8,
|
|
"z_index": 3,
|
|
}
|
|
],
|
|
}
|
|
)
|
|
assert cfg.layer_count == 1
|
|
layer = cfg.layers[0]
|
|
assert layer.source == "asset_1"
|
|
assert layer.source_type == "url"
|
|
assert layer.position == "top_left"
|
|
assert layer.margin == 30
|
|
assert layer.width == "30%"
|
|
assert layer.opacity == 0.8
|
|
assert layer.corner_radius == 10
|
|
assert layer.start_time == 1.5
|
|
assert layer.duration == 5.0
|
|
assert layer.animation_in == "fade"
|
|
assert layer.z_index == 3
|
|
|
|
def test_empty_layers_list(self):
|
|
cfg = PiPConfig.from_dict({"enabled": True, "layers": []})
|
|
assert cfg.enabled is False
|
|
assert cfg.layer_count == 0
|
|
|
|
|
|
# ── PiPConfig 属性 ───────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestPiPConfigProperties:
|
|
def test_layer_count_empty(self):
|
|
cfg = PiPConfig()
|
|
assert cfg.layer_count == 0
|
|
|
|
def test_max_z_index_empty(self):
|
|
cfg = PiPConfig()
|
|
assert cfg.max_z_index == 0
|
|
|
|
def test_max_z_index_multiple(self):
|
|
cfg = PiPConfig(
|
|
layers=[
|
|
PiPLayerConfig(source="a", z_index=3),
|
|
PiPLayerConfig(source="b", z_index=7),
|
|
PiPLayerConfig(source="c", z_index=2),
|
|
]
|
|
)
|
|
assert cfg.max_z_index == 7
|
|
|
|
|
|
# ── parse_size_value ─────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestParseSizeValue:
|
|
def test_int_value(self):
|
|
assert parse_size_value(100, 1920) == 100
|
|
|
|
def test_int_value_zero_bumped_to_one(self):
|
|
assert parse_size_value(0, 1920) == 1
|
|
|
|
def test_int_negative_bumped_to_one(self):
|
|
assert parse_size_value(-5, 1920) == 1
|
|
|
|
def test_percentage_string(self):
|
|
assert parse_size_value("50%", 1920) == 960
|
|
|
|
def test_percentage_25pct(self):
|
|
assert parse_size_value("25%", 1920) == 480
|
|
|
|
def test_percentage_small(self):
|
|
assert parse_size_value("1%", 1920) == 19
|
|
|
|
def test_percentage_zero_bumped(self):
|
|
assert parse_size_value("0%", 1920) == 1
|
|
|
|
def test_invalid_percentage_fallback(self):
|
|
assert parse_size_value("abc%", 1920) == 480 # 25% default
|
|
|
|
def test_numeric_string(self):
|
|
assert parse_size_value("200", 1920) == 200
|
|
|
|
def test_invalid_string_fallback(self):
|
|
assert parse_size_value("invalid", 1920) == 480
|
|
|
|
def test_custom_default_pct(self):
|
|
assert parse_size_value("bad", 1000, default_pct=0.5) == 500
|
|
|
|
def test_none_fallback(self):
|
|
assert parse_size_value(None, 1920) == 480 # type: ignore[arg-type]
|
|
|
|
def test_float_int_conversion(self):
|
|
# float 不是 int,会走到 try int(value) 分支
|
|
result = parse_size_value(150.0, 1920) # type: ignore[arg-type]
|
|
assert result == 150
|
|
|
|
|
|
# ── calculate_pip_position ───────────────────────────────────────────────────
|
|
|
|
|
|
class TestCalculatePipPosition:
|
|
W = 1920
|
|
H = 1080
|
|
PW = 300 # pip width
|
|
PH = 200 # pip height
|
|
M = 20 # margin
|
|
|
|
def test_top_left(self):
|
|
x, y = calculate_pip_position(POSITION_TOP_LEFT, self.W, self.H, self.PW, self.PH, self.M)
|
|
assert (x, y) == (20, 20)
|
|
|
|
def test_top_center(self):
|
|
x, y = calculate_pip_position(POSITION_TOP_CENTER, self.W, self.H, self.PW, self.PH, self.M)
|
|
assert x == (self.W - self.PW) // 2
|
|
assert y == self.M
|
|
|
|
def test_top_right(self):
|
|
x, y = calculate_pip_position(POSITION_TOP_RIGHT, self.W, self.H, self.PW, self.PH, self.M)
|
|
assert x == self.W - self.PW - self.M
|
|
assert y == self.M
|
|
|
|
def test_center_left(self):
|
|
x, y = calculate_pip_position(POSITION_CENTER_LEFT, self.W, self.H, self.PW, self.PH, self.M)
|
|
assert x == self.M
|
|
assert y == (self.H - self.PH) // 2
|
|
|
|
def test_center(self):
|
|
x, y = calculate_pip_position(POSITION_CENTER, self.W, self.H, self.PW, self.PH, self.M)
|
|
assert x == (self.W - self.PW) // 2
|
|
assert y == (self.H - self.PH) // 2
|
|
|
|
def test_center_right(self):
|
|
x, y = calculate_pip_position(POSITION_CENTER_RIGHT, self.W, self.H, self.PW, self.PH, self.M)
|
|
assert x == self.W - self.PW - self.M
|
|
assert y == (self.H - self.PH) // 2
|
|
|
|
def test_bottom_left(self):
|
|
x, y = calculate_pip_position(POSITION_BOTTOM_LEFT, self.W, self.H, self.PW, self.PH, self.M)
|
|
assert x == self.M
|
|
assert y == self.H - self.PH - self.M
|
|
|
|
def test_bottom_center(self):
|
|
x, y = calculate_pip_position(POSITION_BOTTOM_RIGHT, self.W, self.H, self.PW, self.PH, self.M)
|
|
# bottom_right 用作 fallback 默认值
|
|
assert x == self.W - self.PW - self.M
|
|
assert y == self.H - self.PH - self.M
|
|
|
|
def test_bottom_right(self):
|
|
x, y = calculate_pip_position(POSITION_BOTTOM_RIGHT, self.W, self.H, self.PW, self.PH, self.M)
|
|
assert x == self.W - self.PW - self.M
|
|
assert y == self.H - self.PH - self.M
|
|
|
|
def test_invalid_position_falls_back_to_bottom_right(self):
|
|
x, y = calculate_pip_position("unknown_pos", self.W, self.H, self.PW, self.PH, self.M)
|
|
assert x == self.W - self.PW - self.M
|
|
assert y == self.H - self.PH - self.M
|
|
|
|
def test_custom_int_coordinates(self):
|
|
x, y = calculate_pip_position("custom", self.W, self.H, self.PW, self.PH, custom_x=100, custom_y=200)
|
|
assert (x, y) == (100, 200)
|
|
|
|
def test_custom_percentage_coordinates(self):
|
|
x, y = calculate_pip_position("custom", self.W, self.H, self.PW, self.PH, custom_x="10%", custom_y="20%")
|
|
assert x == int(1920 * 0.1)
|
|
assert y == int(1080 * 0.2)
|
|
|
|
def test_custom_zero_margin_ignored(self):
|
|
# custom 模式下 margin 参数不影响
|
|
x, y = calculate_pip_position("custom", self.W, self.H, self.PW, self.PH, margin=100, custom_x=50, custom_y=60)
|
|
assert (x, y) == (50, 60)
|
|
|
|
def test_default_margin(self):
|
|
# margin 不传默认为 20
|
|
x, y = calculate_pip_position(POSITION_TOP_LEFT, self.W, self.H, self.PW, self.PH)
|
|
assert (x, y) == (20, 20)
|
|
|
|
def test_large_margin(self):
|
|
x, y = calculate_pip_position(POSITION_TOP_LEFT, self.W, self.H, self.PW, self.PH, margin=50)
|
|
assert (x, y) == (50, 50)
|
|
|
|
def test_small_output_large_pip(self):
|
|
# 极端情况:pip比输出还大,位置计算仍能给出值
|
|
x, y = calculate_pip_position(POSITION_CENTER, 100, 100, 200, 200, 10)
|
|
assert x == (100 - 200) // 2
|
|
assert y == (100 - 200) // 2
|
|
|
|
|
|
# ── 常量导出验证 ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestConstants:
|
|
def test_nine_position_constants_exist(self):
|
|
positions = [
|
|
POSITION_TOP_LEFT,
|
|
POSITION_TOP_CENTER,
|
|
POSITION_TOP_RIGHT,
|
|
POSITION_CENTER_LEFT,
|
|
POSITION_CENTER,
|
|
POSITION_CENTER_RIGHT,
|
|
POSITION_BOTTOM_LEFT,
|
|
POSITION_BOTTOM_RIGHT,
|
|
]
|
|
# bottom_center 也存在
|
|
from packages.domain.pip_config import POSITION_BOTTOM_CENTER
|
|
|
|
positions.append(POSITION_BOTTOM_CENTER)
|
|
assert len(positions) == 9
|
|
assert len(set(positions)) == 9 # 互不相同
|
|
|
|
def test_animation_constants_exist(self):
|
|
animations = [
|
|
ANIMATION_FADE,
|
|
ANIMATION_SLIDE_LEFT,
|
|
ANIMATION_SLIDE_RIGHT,
|
|
ANIMATION_SLIDE_TOP,
|
|
ANIMATION_SLIDE_BOTTOM,
|
|
ANIMATION_SCALE,
|
|
]
|
|
assert len(set(animations)) == 6
|