edcd1a926f
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 42s
CI/CD Pipeline / Unit Tests (push) Successful in 1m13s
CI/CD Pipeline / Frontend Lint (push) Successful in 1m19s
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Failing after 1m29s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
598 lines
22 KiB
Python
Executable File
598 lines
22 KiB
Python
Executable File
"""画中画(PiP)引擎单元测试."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
from unittest.mock import patch
|
||
|
||
import pytest
|
||
from video_processing.pip_engine import (
|
||
ANIMATION_FADE,
|
||
ANIMATION_SLIDE_BOTTOM,
|
||
ANIMATION_SLIDE_LEFT,
|
||
ANIMATION_SLIDE_RIGHT,
|
||
ANIMATION_SLIDE_TOP,
|
||
POSITION_BOTTOM_LEFT,
|
||
POSITION_BOTTOM_RIGHT,
|
||
POSITION_CENTER,
|
||
POSITION_TOP_LEFT,
|
||
POSITION_TOP_RIGHT,
|
||
PiPConfig,
|
||
PiPEngine,
|
||
PiPLayerConfig,
|
||
)
|
||
|
||
# ── PiPLayerConfig.validate 测试 ──────────────────────────────────────────────
|
||
|
||
|
||
class TestPiPLayerConfigValidate:
|
||
"""PiP图层配置校验测试."""
|
||
|
||
def test_valid_config(self):
|
||
"""正常配置应该通过校验."""
|
||
layer = PiPLayerConfig(source="asset_001")
|
||
ok, err = layer.validate()
|
||
assert ok
|
||
assert err == ""
|
||
|
||
def test_empty_source(self):
|
||
"""空source应该失败."""
|
||
layer = PiPLayerConfig(source="")
|
||
ok, err = layer.validate()
|
||
assert not ok
|
||
assert "source" in err
|
||
|
||
def test_invalid_position(self):
|
||
"""无效位置应该失败."""
|
||
layer = PiPLayerConfig(source="asset_001", position="invalid_pos")
|
||
ok, err = layer.validate()
|
||
assert not ok
|
||
assert "position" in err
|
||
|
||
def test_custom_position_valid(self):
|
||
"""custom位置应该通过."""
|
||
layer = PiPLayerConfig(source="asset_001", position="custom", x=100, y=50)
|
||
ok, err = layer.validate()
|
||
assert ok
|
||
|
||
def test_opacity_out_of_range_high(self):
|
||
"""opacity超过1应该失败."""
|
||
layer = PiPLayerConfig(source="asset_001", opacity=1.5)
|
||
ok, err = layer.validate()
|
||
assert not ok
|
||
assert "opacity" in err
|
||
|
||
def test_opacity_out_of_range_low(self):
|
||
"""opacity小于0应该失败."""
|
||
layer = PiPLayerConfig(source="asset_001", opacity=-0.5)
|
||
ok, err = layer.validate()
|
||
assert not ok
|
||
assert "opacity" in err
|
||
|
||
def test_opacity_boundary_values(self):
|
||
"""opacity边界值应该通过."""
|
||
for val in [0.0, 0.5, 1.0]:
|
||
layer = PiPLayerConfig(source="asset_001", opacity=val)
|
||
ok, _ = layer.validate()
|
||
assert ok
|
||
|
||
def test_negative_corner_radius(self):
|
||
"""负圆角应该失败."""
|
||
layer = PiPLayerConfig(source="asset_001", corner_radius=-5)
|
||
ok, err = layer.validate()
|
||
assert not ok
|
||
assert "corner_radius" in err
|
||
|
||
def test_negative_start_time(self):
|
||
"""负开始时间应该失败."""
|
||
layer = PiPLayerConfig(source="asset_001", start_time=-1.0)
|
||
ok, err = layer.validate()
|
||
assert not ok
|
||
assert "start_time" in err
|
||
|
||
def test_negative_duration(self):
|
||
"""负持续时间应该失败."""
|
||
layer = PiPLayerConfig(source="asset_001", duration=-5.0)
|
||
ok, err = layer.validate()
|
||
assert not ok
|
||
assert "duration" in err
|
||
|
||
def test_invalid_animation_in(self):
|
||
"""无效入场动画应该失败."""
|
||
layer = PiPLayerConfig(source="asset_001", animation_in="spin")
|
||
ok, err = layer.validate()
|
||
assert not ok
|
||
assert "入场动画" in err
|
||
|
||
def test_all_valid_animations(self):
|
||
"""所有有效动画类型应该通过."""
|
||
for anim in [
|
||
ANIMATION_FADE,
|
||
ANIMATION_SLIDE_LEFT,
|
||
ANIMATION_SLIDE_RIGHT,
|
||
ANIMATION_SLIDE_TOP,
|
||
ANIMATION_SLIDE_BOTTOM,
|
||
]:
|
||
layer = PiPLayerConfig(source="asset_001", animation_in=anim, animation_out=anim)
|
||
ok, _ = layer.validate()
|
||
assert ok
|
||
|
||
def test_zero_duration_valid(self):
|
||
"""duration=0(全程显示)应该通过."""
|
||
layer = PiPLayerConfig(source="asset_001", duration=0.0)
|
||
ok, _ = layer.validate()
|
||
assert ok
|
||
|
||
|
||
# ── PiPConfig.from_dict 测试 ──────────────────────────────────────────────────
|
||
|
||
|
||
class TestPiPConfigFromDict:
|
||
"""PiP配置字典解析测试."""
|
||
|
||
def test_none_config(self):
|
||
"""None配置应该返回disabled."""
|
||
config = PiPConfig.from_dict(None)
|
||
assert not config.enabled
|
||
assert len(config.layers) == 0
|
||
|
||
def test_empty_config(self):
|
||
"""空字典应该返回disabled."""
|
||
config = PiPConfig.from_dict({})
|
||
assert not config.enabled
|
||
|
||
def test_enabled_false(self):
|
||
"""enabled=False应该返回disabled."""
|
||
config = PiPConfig.from_dict({"enabled": False, "layers": [{"source": "a"}]})
|
||
assert not config.enabled
|
||
|
||
def test_single_layer(self):
|
||
"""单图层解析."""
|
||
data = {
|
||
"enabled": True,
|
||
"layers": [
|
||
{
|
||
"source": "asset_001",
|
||
"position": POSITION_TOP_RIGHT,
|
||
"width": "30%",
|
||
"opacity": 0.9,
|
||
"corner_radius": 10,
|
||
"start_time": 2.0,
|
||
"duration": 5.0,
|
||
"z_index": 2,
|
||
}
|
||
],
|
||
}
|
||
config = PiPConfig.from_dict(data)
|
||
assert config.enabled
|
||
assert len(config.layers) == 1
|
||
layer = config.layers[0]
|
||
assert layer.source == "asset_001"
|
||
assert layer.position == POSITION_TOP_RIGHT
|
||
assert layer.width == "30%"
|
||
assert layer.opacity == 0.9
|
||
assert layer.corner_radius == 10
|
||
assert layer.start_time == 2.0
|
||
assert layer.duration == 5.0
|
||
assert layer.z_index == 2
|
||
|
||
def test_multiple_layers_sorted_by_z_index(self):
|
||
"""多图层应该按z_index排序."""
|
||
data = {
|
||
"enabled": True,
|
||
"layers": [
|
||
{"source": "asset_high", "z_index": 5},
|
||
{"source": "asset_low", "z_index": 1},
|
||
{"source": "asset_mid", "z_index": 3},
|
||
],
|
||
}
|
||
config = PiPConfig.from_dict(data)
|
||
assert len(config.layers) == 3
|
||
assert config.layers[0].source == "asset_low"
|
||
assert config.layers[1].source == "asset_mid"
|
||
assert config.layers[2].source == "asset_high"
|
||
|
||
def test_invalid_layer_skipped(self):
|
||
"""无效图层应该被跳过."""
|
||
data = {
|
||
"enabled": True,
|
||
"layers": [
|
||
{"source": "asset_good"},
|
||
{"source": "", "position": "invalid"}, # 空source
|
||
{"source": "asset_good2", "opacity": 2.0}, # opacity超范围
|
||
],
|
||
}
|
||
config = PiPConfig.from_dict(data)
|
||
# 第1个有效,第2、3个无效
|
||
assert len(config.layers) == 1
|
||
assert config.layers[0].source == "asset_good"
|
||
|
||
def test_all_invalid_layers_disabled(self):
|
||
"""所有图层都无效时enabled为False."""
|
||
data = {
|
||
"enabled": True,
|
||
"layers": [
|
||
{"source": ""},
|
||
{"source": ""},
|
||
],
|
||
}
|
||
config = PiPConfig.from_dict(data)
|
||
assert not config.enabled
|
||
assert len(config.layers) == 0
|
||
|
||
def test_default_values(self):
|
||
"""默认值应该正确."""
|
||
data = {
|
||
"enabled": True,
|
||
"layers": [{"source": "asset_001"}],
|
||
}
|
||
config = PiPConfig.from_dict(data)
|
||
layer = config.layers[0]
|
||
assert layer.position == POSITION_BOTTOM_RIGHT
|
||
assert layer.width == "25%"
|
||
assert layer.opacity == 1.0
|
||
assert layer.corner_radius == 0
|
||
assert layer.start_time == 0.0
|
||
assert layer.duration == 0.0
|
||
assert layer.z_index == 1
|
||
|
||
|
||
# ── PiPEngine 位置计算测试 ────────────────────────────────────────────────────
|
||
|
||
|
||
class TestPiPEnginePosition:
|
||
"""PiP引擎位置计算测试."""
|
||
|
||
@pytest.fixture
|
||
def engine(self):
|
||
return PiPEngine(output_width=1920, output_height=1080, output_fps=30)
|
||
|
||
def test_top_left_position(self, engine):
|
||
"""左上角位置."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_TOP_LEFT, margin=20)
|
||
x, y = engine._parse_position(layer, 480, 270)
|
||
assert x == 20
|
||
assert y == 20
|
||
|
||
def test_top_right_position(self, engine):
|
||
"""右上角位置."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_TOP_RIGHT, margin=20)
|
||
x, y = engine._parse_position(layer, 480, 270)
|
||
assert x == 1920 - 480 - 20
|
||
assert y == 20
|
||
|
||
def test_bottom_right_position(self, engine):
|
||
"""右下角位置(默认)."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_BOTTOM_RIGHT, margin=30)
|
||
x, y = engine._parse_position(layer, 480, 270)
|
||
assert x == 1920 - 480 - 30
|
||
assert y == 1080 - 270 - 30
|
||
|
||
def test_bottom_left_position(self, engine):
|
||
"""左下角位置."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_BOTTOM_LEFT, margin=15)
|
||
x, y = engine._parse_position(layer, 480, 270)
|
||
assert x == 15
|
||
assert y == 1080 - 270 - 15
|
||
|
||
def test_center_position(self, engine):
|
||
"""中心位置."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_CENTER, margin=0)
|
||
x, y = engine._parse_position(layer, 480, 270)
|
||
assert x == (1920 - 480) // 2
|
||
assert y == (1080 - 270) // 2
|
||
|
||
def test_custom_position_pixel(self, engine):
|
||
"""自定义像素位置."""
|
||
layer = PiPLayerConfig(source="a", position="custom", x=100, y=200)
|
||
x, y = engine._parse_position(layer, 480, 270)
|
||
assert x == 100
|
||
assert y == 200
|
||
|
||
def test_custom_position_percentage(self, engine):
|
||
"""自定义百分比位置."""
|
||
layer = PiPLayerConfig(source="a", position="custom", x="50%", y="25%")
|
||
x, y = engine._parse_position(layer, 480, 270)
|
||
assert x == 1920 // 2
|
||
assert y == 1080 // 4
|
||
|
||
def test_top_center_position(self, engine):
|
||
"""顶部居中位置."""
|
||
layer = PiPLayerConfig(source="a", position="top_center", margin=10)
|
||
x, y = engine._parse_position(layer, 480, 270)
|
||
assert x == (1920 - 480) // 2
|
||
assert y == 10
|
||
|
||
def test_invalid_position_fallback(self, engine):
|
||
"""无效位置应该fallback到右下角."""
|
||
layer = PiPLayerConfig(source="a", position="unknown_position", margin=20)
|
||
# 直接测试_parse_position(注意:validate会拦截,但_parse_position自己也有fallback)
|
||
x, y = engine._parse_position(layer, 480, 270)
|
||
assert x == 1920 - 480 - 20
|
||
assert y == 1080 - 270 - 20
|
||
|
||
|
||
# ── PiPEngine 尺寸解析测试 ────────────────────────────────────────────────────
|
||
|
||
|
||
class TestPiPEngineSize:
|
||
"""PiP引擎尺寸解析测试."""
|
||
|
||
@pytest.fixture
|
||
def engine(self):
|
||
return PiPEngine(output_width=1920, output_height=1080, output_fps=30)
|
||
|
||
def test_pixel_size_int(self, engine):
|
||
"""像素尺寸(整数)."""
|
||
assert engine._parse_size(500, 1920) == 500
|
||
|
||
def test_pixel_size_str(self, engine):
|
||
"""像素尺寸(字符串数字)."""
|
||
assert engine._parse_size("500", 1920) == 500
|
||
|
||
def test_percentage_size(self, engine):
|
||
"""百分比尺寸."""
|
||
assert engine._parse_size("50%", 1920) == 960
|
||
assert engine._parse_size("25%", 1920) == 480
|
||
|
||
def test_zero_size_default(self, engine):
|
||
"""0或无效值应该有最小值保护."""
|
||
assert engine._parse_size(0, 1920) == 1
|
||
assert engine._parse_size("", 1920) == 480 # 默认25%
|
||
|
||
def test_negative_size_default(self, engine):
|
||
"""负值应该取绝对值后至少为1."""
|
||
# _parse_size 用 max(1, value),负值会走 except 分支
|
||
result = engine._parse_size("-100", 1920)
|
||
# 会走ValueError分支,返回默认值
|
||
assert result > 0
|
||
|
||
|
||
# ── PiPEngine 滤镜构建测试 ────────────────────────────────────────────────────
|
||
|
||
|
||
class TestPiPEngineBuildFilters:
|
||
"""PiP引擎滤镜构建测试."""
|
||
|
||
@pytest.fixture
|
||
def engine(self):
|
||
return PiPEngine(output_width=1920, output_height=1080, output_fps=30)
|
||
|
||
@pytest.fixture
|
||
def fake_video(self, tmp_path):
|
||
"""创建一个假的视频文件路径."""
|
||
path = tmp_path / "test_video.mp4"
|
||
path.write_bytes(b"fake video data")
|
||
return path
|
||
|
||
def test_empty_sources(self, engine):
|
||
"""空素材列表应该返回空."""
|
||
filters, inputs, label = engine.build_pip_filters("base_label", [])
|
||
assert filters == []
|
||
assert inputs == []
|
||
assert label == "base_label"
|
||
|
||
def test_single_layer_basic(self, engine, fake_video):
|
||
"""单图层基础滤镜构建."""
|
||
layer = PiPLayerConfig(
|
||
source="asset_001",
|
||
position=POSITION_TOP_RIGHT,
|
||
width="25%",
|
||
)
|
||
sources = [("pip_src_0", layer, fake_video)]
|
||
|
||
filters, inputs, final_label = engine.build_pip_filters("base_video", sources, base_input_idx=3)
|
||
|
||
# 应该有2个滤镜: 预处理 + overlay
|
||
assert len(filters) == 2
|
||
# 输入参数应该有2个(-i + path)
|
||
assert len(inputs) == 2
|
||
assert inputs[0] == "-i"
|
||
assert inputs[1] == str(fake_video)
|
||
|
||
# 预处理滤镜应该使用正确的输入索引
|
||
assert "3:v" in filters[0]
|
||
# 应该包含scale
|
||
assert "scale=" in filters[0]
|
||
# 应该有pip_pre_0标签
|
||
assert "[pip_pre_0]" in filters[0]
|
||
|
||
# overlay滤镜
|
||
assert "overlay=" in filters[1]
|
||
assert "[base_video][pip_pre_0]" in filters[1]
|
||
|
||
def test_single_layer_final_label(self, engine, fake_video):
|
||
"""最终输出标签应该正确."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_CENTER)
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
_, _, final_label = engine.build_pip_filters("main_v", sources)
|
||
assert final_label == "pip_combined_0"
|
||
|
||
def test_multiple_layers(self, engine, fake_video):
|
||
"""多图层叠加."""
|
||
layer1 = PiPLayerConfig(source="a", position=POSITION_TOP_LEFT, z_index=1)
|
||
layer2 = PiPLayerConfig(source="b", position=POSITION_BOTTOM_RIGHT, z_index=2)
|
||
sources = [
|
||
("s0", layer1, fake_video),
|
||
("s1", layer2, fake_video),
|
||
]
|
||
|
||
filters, inputs, final_label = engine.build_pip_filters("base", sources, base_input_idx=0)
|
||
|
||
# 2层 × 2个滤镜(预处理+overlay)= 4个滤镜
|
||
assert len(filters) == 4
|
||
# 2个输入文件
|
||
assert len(inputs) == 4 # 2 × (-i + path)
|
||
|
||
# 输入索引应该连续
|
||
assert "0:v" in filters[0]
|
||
assert "1:v" in filters[2]
|
||
|
||
# 最终标签应该是第二个overlay的输出
|
||
assert final_label == "pip_combined_1"
|
||
|
||
def test_with_opacity(self, engine, fake_video):
|
||
"""透明度应该在滤镜中体现."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_CENTER, opacity=0.5)
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
filters, _, _ = engine.build_pip_filters("base", sources)
|
||
pre_filter = filters[0]
|
||
assert "colorchannelmixer=aa=0.5" in pre_filter
|
||
assert "yuva420p" in pre_filter
|
||
|
||
def test_with_corner_radius(self, engine, fake_video):
|
||
"""圆角裁剪应该在滤镜中体现."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_CENTER, corner_radius=20)
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
filters, _, _ = engine.build_pip_filters("base", sources)
|
||
pre_filter = filters[0]
|
||
assert "geq=" in pre_filter
|
||
|
||
def test_with_border(self, engine, fake_video):
|
||
"""边框应该在滤镜中体现."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_CENTER, border_width=3, border_color="red")
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
filters, _, _ = engine.build_pip_filters("base", sources)
|
||
pre_filter = filters[0]
|
||
assert "pad=" in pre_filter
|
||
assert "red" in pre_filter
|
||
|
||
def test_timing_start_time_and_duration(self, engine, fake_video):
|
||
"""时间控制应该生成enable表达式."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_CENTER, start_time=5.0, duration=10.0)
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
filters, _, _ = engine.build_pip_filters("base", sources)
|
||
overlay_filter = filters[1]
|
||
assert "enable=" in overlay_filter
|
||
assert "between(t,5.0,15.0)" in overlay_filter
|
||
|
||
def test_timing_start_time_only(self, engine, fake_video):
|
||
"""只有开始时间(全程显示到结束)."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_CENTER, start_time=3.0, duration=0.0)
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
filters, _, _ = engine.build_pip_filters("base", sources)
|
||
overlay_filter = filters[1]
|
||
assert "enable=" in overlay_filter
|
||
assert "gte(t,3.0)" in overlay_filter
|
||
|
||
def test_no_timing_no_enable(self, engine, fake_video):
|
||
"""无时间限制时不应该有enable表达式."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_CENTER, start_time=0.0, duration=0.0)
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
filters, _, _ = engine.build_pip_filters("base", sources)
|
||
overlay_filter = filters[1]
|
||
assert "enable=" not in overlay_filter
|
||
|
||
def test_fade_animation(self, engine, fake_video):
|
||
"""淡入淡出动画."""
|
||
layer = PiPLayerConfig(
|
||
source="a",
|
||
position=POSITION_CENTER,
|
||
animation_in=ANIMATION_FADE,
|
||
animation_out=ANIMATION_FADE,
|
||
duration=10.0,
|
||
animation_duration=0.8,
|
||
)
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
filters, _, _ = engine.build_pip_filters("base", sources)
|
||
pre_filter = filters[0]
|
||
assert "fade=t=in" in pre_filter
|
||
assert "fade=t=out" in pre_filter
|
||
assert "alpha=1" in pre_filter
|
||
|
||
def test_slide_animation_in(self, engine, fake_video):
|
||
"""滑入动画应该在overlay表达式中."""
|
||
layer = PiPLayerConfig(
|
||
source="a",
|
||
position=POSITION_CENTER,
|
||
animation_in=ANIMATION_SLIDE_LEFT,
|
||
animation_duration=0.5,
|
||
)
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
filters, _, _ = engine.build_pip_filters("base", sources)
|
||
overlay_filter = filters[1]
|
||
# x表达式应该包含动态变化
|
||
assert "overlay=" in overlay_filter
|
||
|
||
def test_full_opacity_no_alpha(self, engine, fake_video):
|
||
"""opacity=1时不应该有colorchannelmixer."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_CENTER, opacity=1.0)
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
filters, _, _ = engine.build_pip_filters("base", sources)
|
||
pre_filter = filters[0]
|
||
assert "colorchannelmixer" not in pre_filter
|
||
|
||
def test_zero_corner_radius_no_geq(self, engine, fake_video):
|
||
"""corner_radius=0时不应该有geq滤镜."""
|
||
layer = PiPLayerConfig(source="a", position=POSITION_CENTER, corner_radius=0)
|
||
sources = [("s0", layer, fake_video)]
|
||
|
||
filters, _, _ = engine.build_pip_filters("base", sources)
|
||
pre_filter = filters[0]
|
||
assert "geq=" not in pre_filter
|
||
|
||
|
||
# ── PiPEngine 素材验证(降级策略)测试 ────────────────────────────────────────
|
||
|
||
|
||
class TestPiPEngineValidateSource:
|
||
"""PiP引擎素材验证与降级测试."""
|
||
|
||
@pytest.fixture
|
||
def engine(self):
|
||
return PiPEngine(output_width=1920, output_height=1080, output_fps=30)
|
||
|
||
def test_asset_id_in_map(self, engine, tmp_path):
|
||
"""asset_id在map中应该返回路径."""
|
||
asset_path = tmp_path / "test.mp4"
|
||
asset_path.write_bytes(b"data")
|
||
asset_map = {"asset_001": asset_path}
|
||
|
||
layer = PiPLayerConfig(source="asset_001", source_type="asset_id")
|
||
result = engine.validate_layer_source(layer, asset_map)
|
||
assert result == asset_path
|
||
|
||
def test_asset_id_not_in_map(self, engine):
|
||
"""asset_id不在map中应该返回None(降级)."""
|
||
layer = PiPLayerConfig(source="nonexistent", source_type="asset_id")
|
||
result = engine.validate_layer_source(layer, {})
|
||
assert result is None
|
||
|
||
def test_local_path_exists(self, engine, tmp_path):
|
||
"""本地路径存在应该返回."""
|
||
path = tmp_path / "video.mp4"
|
||
path.write_bytes(b"data")
|
||
|
||
layer = PiPLayerConfig(source=str(path), source_type="local_path")
|
||
result = engine.validate_layer_source(layer, {})
|
||
assert result == path
|
||
|
||
def test_local_path_not_exists(self, engine):
|
||
"""本地路径不存在应该返回None(降级)."""
|
||
layer = PiPLayerConfig(source="/nonexistent/path.mp4", source_type="local_path")
|
||
result = engine.validate_layer_source(layer, {})
|
||
assert result is None
|
||
|
||
def test_url_type_not_supported(self, engine):
|
||
"""URL类型暂时不支持,返回None."""
|
||
layer = PiPLayerConfig(source="http://example.com/video.mp4", source_type="url")
|
||
result = engine.validate_layer_source(layer, {})
|
||
assert result is None
|
||
|
||
def test_exception_handling(self, engine):
|
||
"""异常情况应该返回None(不阻断)."""
|
||
layer = PiPLayerConfig(source=None, source_type="local_path") # type: ignore
|
||
# 模拟异常情况
|
||
result = engine.validate_layer_source(layer, {})
|
||
assert result is None
|