241760ef39
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 37s
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
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 / Unit Tests (push) Successful in 4m21s
CI/CD Pipeline / Integration Tests (push) Failing after 4m25s
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
345 lines
11 KiB
Python
Executable File
345 lines
11 KiB
Python
Executable File
"""水印 + 片头片尾引擎单元测试."""
|
|
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "apps" / "worker"))
|
|
|
|
from video_processing.intro_outro_engine import (
|
|
IntroOutroConfig,
|
|
IntroOutroEngine,
|
|
)
|
|
from video_processing.watermark_engine import (
|
|
WATERMARK_POSITIONS,
|
|
WatermarkConfig,
|
|
WatermarkEngine,
|
|
)
|
|
|
|
|
|
class TestWatermarkConfig(unittest.TestCase):
|
|
"""WatermarkConfig 单元测试."""
|
|
|
|
def test_from_dict_none_disabled(self):
|
|
"""空配置或未启用 → None."""
|
|
self.assertIsNone(WatermarkConfig.from_dict(None))
|
|
self.assertIsNone(WatermarkConfig.from_dict({}))
|
|
self.assertIsNone(WatermarkConfig.from_dict({"enabled": False}))
|
|
|
|
def test_from_dict_text_mode(self):
|
|
"""文字水印模式."""
|
|
cfg = WatermarkConfig.from_dict({
|
|
"enabled": True,
|
|
"mode": "text",
|
|
"text": "hello world",
|
|
"position": "top_left",
|
|
})
|
|
self.assertIsNotNone(cfg)
|
|
self.assertEqual(cfg.mode, "text")
|
|
self.assertEqual(cfg.text, "hello world")
|
|
self.assertEqual(cfg.position, "top_left")
|
|
|
|
def test_from_dict_image_missing_path(self):
|
|
"""图片水印缺路径 → None."""
|
|
cfg = WatermarkConfig.from_dict({
|
|
"enabled": True,
|
|
"mode": "image",
|
|
})
|
|
self.assertIsNone(cfg)
|
|
|
|
def test_from_dict_text_missing_text(self):
|
|
"""文字水印缺文字 → None."""
|
|
cfg = WatermarkConfig.from_dict({
|
|
"enabled": True,
|
|
"mode": "text",
|
|
})
|
|
self.assertIsNone(cfg)
|
|
|
|
def test_validate_text_valid(self):
|
|
"""文字水印合法配置."""
|
|
cfg = WatermarkConfig(
|
|
mode="text",
|
|
text="test",
|
|
position="bottom_right",
|
|
)
|
|
ok, err = cfg.validate()
|
|
self.assertTrue(ok)
|
|
self.assertEqual(err, "")
|
|
|
|
def test_validate_invalid_position(self):
|
|
"""非法位置."""
|
|
cfg = WatermarkConfig(mode="text", text="test", position="invalid")
|
|
ok, err = cfg.validate()
|
|
self.assertFalse(ok)
|
|
self.assertIn("不支持的位置", err)
|
|
|
|
def test_validate_opacity_out_of_range(self):
|
|
"""透明度超范围."""
|
|
cfg = WatermarkConfig(mode="text", text="test", opacity=1.5)
|
|
ok, err = cfg.validate()
|
|
self.assertFalse(ok)
|
|
|
|
def test_validate_image_missing_path(self):
|
|
"""图片水印缺路径."""
|
|
cfg = WatermarkConfig(mode="image")
|
|
ok, err = cfg.validate()
|
|
self.assertFalse(ok)
|
|
|
|
|
|
class TestWatermarkEnginePosition(unittest.TestCase):
|
|
"""水印位置计算单元测试."""
|
|
|
|
def setUp(self):
|
|
self.out_w = 1920
|
|
self.out_h = 1080
|
|
self.wm_w = 200
|
|
self.wm_h = 100
|
|
self.mx = 20
|
|
self.my = 20
|
|
|
|
def test_top_left(self):
|
|
x, y = WatermarkEngine.calc_position(
|
|
"top_left", self.out_w, self.out_h, self.wm_w, self.wm_h, self.mx, self.my
|
|
)
|
|
self.assertEqual(x, 20)
|
|
self.assertEqual(y, 20)
|
|
|
|
def test_top_center(self):
|
|
x, y = WatermarkEngine.calc_position(
|
|
"top_center", self.out_w, self.out_h, self.wm_w, self.wm_h, self.mx, self.my
|
|
)
|
|
self.assertEqual(x, (1920 - 200) // 2)
|
|
self.assertEqual(y, 20)
|
|
|
|
def test_top_right(self):
|
|
x, y = WatermarkEngine.calc_position(
|
|
"top_right", self.out_w, self.out_h, self.wm_w, self.wm_h, self.mx, self.my
|
|
)
|
|
self.assertEqual(x, 1920 - 200 - 20)
|
|
self.assertEqual(y, 20)
|
|
|
|
def test_center_left(self):
|
|
x, y = WatermarkEngine.calc_position(
|
|
"center_left", self.out_w, self.out_h, self.wm_w, self.wm_h, self.mx, self.my
|
|
)
|
|
self.assertEqual(x, 20)
|
|
self.assertEqual(y, (1080 - 100) // 2)
|
|
|
|
def test_center(self):
|
|
x, y = WatermarkEngine.calc_position(
|
|
"center", self.out_w, self.out_h, self.wm_w, self.wm_h, self.mx, self.my
|
|
)
|
|
self.assertEqual(x, (1920 - 200) // 2)
|
|
self.assertEqual(y, (1080 - 100) // 2)
|
|
|
|
def test_center_right(self):
|
|
x, y = WatermarkEngine.calc_position(
|
|
"center_right", self.out_w, self.out_h, self.wm_w, self.wm_h, self.mx, self.my
|
|
)
|
|
self.assertEqual(x, 1920 - 200 - 20)
|
|
self.assertEqual(y, (1080 - 100) // 2)
|
|
|
|
def test_bottom_left(self):
|
|
x, y = WatermarkEngine.calc_position(
|
|
"bottom_left", self.out_w, self.out_h, self.wm_w, self.wm_h, self.mx, self.my
|
|
)
|
|
self.assertEqual(x, 20)
|
|
self.assertEqual(y, 1080 - 100 - 20)
|
|
|
|
def test_bottom_center(self):
|
|
x, y = WatermarkEngine.calc_position(
|
|
"bottom_center", self.out_w, self.out_h, self.wm_w, self.wm_h, self.mx, self.my
|
|
)
|
|
self.assertEqual(x, (1920 - 200) // 2)
|
|
self.assertEqual(y, 1080 - 100 - 20)
|
|
|
|
def test_bottom_right(self):
|
|
x, y = WatermarkEngine.calc_position(
|
|
"bottom_right", self.out_w, self.out_h, self.wm_w, self.wm_h, self.mx, self.my
|
|
)
|
|
self.assertEqual(x, 1920 - 200 - 20)
|
|
self.assertEqual(y, 1080 - 100 - 20)
|
|
|
|
def test_default_fallback(self):
|
|
"""非法位置默认右下角."""
|
|
x, y = WatermarkEngine.calc_position(
|
|
"unknown", self.out_w, self.out_h, self.wm_w, self.wm_h, self.mx, self.my
|
|
)
|
|
self.assertEqual(x, 1920 - 200 - 20)
|
|
self.assertEqual(y, 1080 - 100 - 20)
|
|
|
|
def test_nine_positions_all_present(self):
|
|
"""9宫格位置都有定义."""
|
|
self.assertEqual(len(WATERMARK_POSITIONS), 9)
|
|
|
|
|
|
class TestWatermarkEngineFilters(unittest.TestCase):
|
|
"""水印滤镜构建单元测试."""
|
|
|
|
def test_text_watermark_filter(self):
|
|
"""文字水印滤镜构建."""
|
|
cfg = WatermarkConfig(
|
|
mode="text",
|
|
text="hello",
|
|
position="top_left",
|
|
font_size=24,
|
|
font_color="white",
|
|
opacity=0.8,
|
|
margin_x=10,
|
|
margin_y=10,
|
|
)
|
|
result = WatermarkEngine.build_text_watermark_filter(
|
|
"[in]", "[out]", cfg, 1920, 1080
|
|
)
|
|
self.assertTrue(result.startswith("[in]drawtext="))
|
|
self.assertIn("text='hello'", result)
|
|
self.assertIn("fontsize=24", result)
|
|
self.assertIn("fontcolor=white@0.8", result)
|
|
self.assertTrue(result.endswith("[out]"))
|
|
|
|
def test_text_watermark_scroll(self):
|
|
"""滚动文字水印."""
|
|
cfg = WatermarkConfig(
|
|
mode="text",
|
|
text="scroll",
|
|
position="bottom_left",
|
|
scroll=True,
|
|
scroll_speed=60,
|
|
)
|
|
result = WatermarkEngine.build_text_watermark_filter(
|
|
"[in]", "[out]", cfg, 1920, 1080
|
|
)
|
|
self.assertIn("mod(60*t", result)
|
|
|
|
|
|
class TestIntroOutroConfig(unittest.TestCase):
|
|
"""IntroOutroConfig 单元测试."""
|
|
|
|
def test_from_dict_disabled(self):
|
|
"""未启用 → 空配置."""
|
|
cfg = IntroOutroConfig.from_dict(None)
|
|
self.assertFalse(cfg.enabled)
|
|
self.assertFalse(cfg.has_intro)
|
|
self.assertFalse(cfg.has_outro)
|
|
|
|
def test_from_dict_intro_text(self):
|
|
"""文字片头配置."""
|
|
cfg = IntroOutroConfig.from_dict({
|
|
"enabled": True,
|
|
"intro": {
|
|
"type": "text",
|
|
"title": "欢迎观看",
|
|
"subtitle": "精彩内容马上开始",
|
|
"duration": 3.0,
|
|
"background": "#1a1a2e",
|
|
},
|
|
})
|
|
self.assertTrue(cfg.enabled)
|
|
self.assertTrue(cfg.has_intro)
|
|
self.assertFalse(cfg.has_outro)
|
|
self.assertEqual(cfg.intro_type, "text")
|
|
self.assertEqual(cfg.intro_title, "欢迎观看")
|
|
self.assertEqual(cfg.intro_duration, 3.0)
|
|
|
|
def test_from_dict_outro_video(self):
|
|
"""视频片尾配置."""
|
|
cfg = IntroOutroConfig.from_dict({
|
|
"enabled": True,
|
|
"outro": {
|
|
"type": "video",
|
|
"video_path": "/tmp/outro.mp4",
|
|
"duration": 5.0,
|
|
},
|
|
})
|
|
self.assertTrue(cfg.has_outro)
|
|
self.assertEqual(cfg.outro_type, "video")
|
|
self.assertEqual(cfg.outro_video_path, "/tmp/outro.mp4")
|
|
|
|
def test_validate_valid(self):
|
|
"""合法配置."""
|
|
cfg = IntroOutroConfig(
|
|
enabled=True,
|
|
intro_type="text",
|
|
intro_title="标题",
|
|
intro_duration=3.0,
|
|
outro_type="text",
|
|
outro_title="片尾",
|
|
outro_duration=3.0,
|
|
)
|
|
ok, err = cfg.validate()
|
|
self.assertTrue(ok)
|
|
|
|
def test_validate_video_intro_missing_path(self):
|
|
"""视频片头缺路径."""
|
|
cfg = IntroOutroConfig(
|
|
enabled=True,
|
|
intro_type="video",
|
|
intro_duration=3.0,
|
|
)
|
|
ok, err = cfg.validate()
|
|
self.assertFalse(ok)
|
|
self.assertIn("video_path", err)
|
|
|
|
def test_validate_text_intro_missing_title(self):
|
|
"""文字片头缺标题."""
|
|
cfg = IntroOutroConfig(
|
|
enabled=True,
|
|
intro_type="text",
|
|
intro_duration=3.0,
|
|
)
|
|
ok, err = cfg.validate()
|
|
self.assertFalse(ok)
|
|
|
|
def test_has_intro_false_when_none(self):
|
|
"""type=none 时 has_intro 为 False."""
|
|
cfg = IntroOutroConfig(enabled=True, intro_type="none")
|
|
self.assertFalse(cfg.has_intro)
|
|
|
|
def test_has_outro_follow_type(self):
|
|
"""follow 类型也算有片尾."""
|
|
cfg = IntroOutroConfig(enabled=True, outro_type="follow", outro_title="关注")
|
|
self.assertTrue(cfg.has_outro)
|
|
|
|
|
|
class TestIntroOutroEngineConcat(unittest.TestCase):
|
|
"""片头片尾拼接单元测试."""
|
|
|
|
def test_concat_no_intro_outro(self):
|
|
"""没有片头片尾 → 直接复制."""
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
main_video = Path(tmpdir) / "main.mp4"
|
|
output = Path(tmpdir) / "output.mp4"
|
|
# 创建空文件模拟
|
|
main_video.write_bytes(b"fake video data")
|
|
|
|
result = IntroOutroEngine.concat_with_intro_outro(
|
|
main_video, None, None, output
|
|
)
|
|
self.assertTrue(result)
|
|
self.assertTrue(output.exists())
|
|
self.assertEqual(main_video.read_bytes(), output.read_bytes())
|
|
|
|
def test_concat_intro_only_no_file(self):
|
|
"""只有片头但文件不存在 → 直接复制主视频."""
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
main_video = Path(tmpdir) / "main.mp4"
|
|
output = Path(tmpdir) / "output.mp4"
|
|
main_video.write_bytes(b"fake data")
|
|
|
|
# intro 路径不存在
|
|
intro = Path(tmpdir) / "nonexistent.mp4"
|
|
|
|
result = IntroOutroEngine.concat_with_intro_outro(
|
|
main_video, intro, None, output
|
|
)
|
|
self.assertTrue(result)
|
|
self.assertTrue(output.exists())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|