"""水印 + 片头片尾引擎单元测试.""" 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()) class TestResolveWatermarkConfig(unittest.TestCase): """UnifiedRenderService._resolve_watermark_config 兼容性测试.""" def _resolve(self, plan_config): from video_processing.unified_render_service import UnifiedRenderService return UnifiedRenderService._resolve_watermark_config(plan_config) def test_none_or_empty_config(self): """空配置 → None.""" self.assertIsNone(self._resolve(None)) self.assertIsNone(self._resolve({})) self.assertIsNone(self._resolve([])) # 非dict安全处理 def test_nested_format_enabled(self): """嵌套格式 config.watermark 正常解析.""" cfg = {"watermark": {"enabled": True, "mode": "text", "text": "测试水印"}} result = self._resolve(cfg) self.assertIsNotNone(result) self.assertEqual(result.mode, "text") self.assertEqual(result.text, "测试水印") def test_nested_format_disabled(self): """嵌套格式未启用 → None.""" cfg = {"watermark": {"enabled": False, "mode": "text", "text": "测试"}} self.assertIsNone(self._resolve(cfg)) def test_flat_export_format_enabled(self): """扁平格式 config.export.watermark_enabled + text 正常解析.""" cfg = {"export": {"watermark_enabled": True, "watermark_text": "我的水印"}} result = self._resolve(cfg) self.assertIsNotNone(result) self.assertEqual(result.mode, "text") self.assertEqual(result.text, "我的水印") self.assertEqual(result.position, "bottom_right") def test_flat_export_format_disabled(self): """扁平格式未启用 → None.""" cfg = {"export": {"watermark_enabled": False, "watermark_text": "测试"}} self.assertIsNone(self._resolve(cfg)) def test_flat_export_format_no_text(self): """扁平格式启用但无文字 → None.""" cfg = {"export": {"watermark_enabled": True, "watermark_text": ""}} self.assertIsNone(self._resolve(cfg)) def test_nested_takes_priority(self): """嵌套格式存在时优先使用嵌套格式(忽略扁平格式).""" cfg = { "watermark": {"enabled": True, "mode": "text", "text": "嵌套水印"}, "export": {"watermark_enabled": True, "watermark_text": "扁平水印"}, } result = self._resolve(cfg) self.assertIsNotNone(result) self.assertEqual(result.text, "嵌套水印") def test_flat_with_custom_position(self): """扁平格式支持自定义位置、透明度等参数.""" cfg = { "export": { "watermark_enabled": True, "watermark_text": "自定义水印", "watermark_position": "top_left", "watermark_opacity": 0.5, "watermark_font_size": 32, "watermark_font_color": "red", } } result = self._resolve(cfg) self.assertIsNotNone(result) self.assertEqual(result.position, "top_left") self.assertAlmostEqual(result.opacity, 0.5) self.assertEqual(result.font_size, 32) self.assertEqual(result.font_color, "red") def test_no_export_key(self): """没有 export 字段时不报错.""" cfg = {"other": "value"} self.assertIsNone(self._resolve(cfg)) if __name__ == "__main__": unittest.main()