"""Tests for enhanced dedup: text + structure dimensions (Issue #P2-后端3).""" from __future__ import annotations import sys from unittest.mock import MagicMock # --------------------------------------------------------------------------- # Mock heavy deps before importing dedup module # --------------------------------------------------------------------------- _ORIGINAL_MODULES = dict(sys.modules) _MOCKED_MODULE_NAMES: list[str] = [] def _mock_if_absent(name: str, mock_obj=None): """仅在模块不在 sys.modules 中时注入 mock,并记录以便清理。""" if name not in sys.modules: sys.modules[name] = mock_obj if mock_obj is not None else MagicMock() _MOCKED_MODULE_NAMES.append(name) # Mock heavy deps _mock_if_absent("ffmpeg") _mock_if_absent("ffmpeg.utils") _mock_if_absent("worker_app.celery_app") _mock_if_absent("worker_app.db") _mock_if_absent("packages.adapters.sqlalchemy_impl.generated_video_repository") _mock_if_absent("packages.adapters.sqlalchemy_impl.models") _mock_if_absent("packages.shared.storage") # Mock cv2 and numpy if not available try: import cv2 as _cv2 if not isinstance(_cv2, MagicMock): _HAS_CV2 = True else: _HAS_CV2 = False except ImportError: _HAS_CV2 = False _mock_if_absent("cv2") _mock_if_absent("numpy") import pytest from apps.worker.video_processing.dedup import ( STRUCTURE_WEIGHT, TEXT_WEIGHT, VISUAL_WEIGHT, compute_structure_similarity, compute_text_similarity, ) class TestTextSimilarity: """Tests for compute_text_similarity.""" def test_identical_texts(self): """相同文本返回 1.0。""" assert compute_text_similarity("你好世界", "你好世界") == 1.0 def test_empty_texts(self): """都为空返回 1.0。""" assert compute_text_similarity("", "") == 1.0 def test_one_empty(self): """一个为空返回 0.0。""" assert compute_text_similarity("你好", "") == 0.0 assert compute_text_similarity("", "你好") == 0.0 def test_completely_different(self): """完全不同文本返回低相似度。""" sim = compute_text_similarity("你好世界", "abcdefgh") assert sim < 0.3 def test_partial_overlap(self): """部分重叠文本返回中等相似度。""" sim = compute_text_similarity("今天天气真好", "今天天气不错") assert 0.3 < sim < 0.9 def test_case_insensitive(self): """英文大小写不敏感。""" sim = compute_text_similarity("Hello World", "hello world") assert sim == 1.0 def test_whitespace_ignored(self): """空白字符被忽略。""" sim = compute_text_similarity("你好 世界", "你好世界") assert sim == 1.0 def test_punctuation_ignored(self): """标点符号被忽略。""" sim = compute_text_similarity("你好,世界!", "你好世界") assert sim == 1.0 def test_long_texts(self): """长文本也能计算。""" t1 = "这是一段很长的配音文本,用于测试文案查重功能" t2 = "这是一段较长的配音文字,用于测试文案去重功能" sim = compute_text_similarity(t1, t2) assert 0.0 <= sim <= 1.0 class TestStructureSimilarity: """Tests for compute_structure_similarity.""" def test_identical_structures(self): """完全相同结构返回 1.0。""" clips = [ {"clip_type": "video", "duration": 5.0}, {"clip_type": "title", "duration": 2.0}, {"clip_type": "video", "duration": 8.0}, ] assert compute_structure_similarity(clips, clips) == 1.0 def test_empty_clips(self): """都为空返回 1.0。""" assert compute_structure_similarity([], []) == 1.0 def test_one_empty(self): """一个为空返回 0.0。""" clips = [{"clip_type": "video", "duration": 5.0}] assert compute_structure_similarity(clips, []) == 0.0 assert compute_structure_similarity([], clips) == 0.0 def test_different_count(self): """片段数不同,相似度降低。""" clips1 = [ {"clip_type": "video", "duration": 5.0}, {"clip_type": "title", "duration": 2.0}, ] clips2 = [ {"clip_type": "video", "duration": 5.0}, {"clip_type": "title", "duration": 2.0}, {"clip_type": "video", "duration": 3.0}, {"clip_type": "title", "duration": 1.0}, ] sim = compute_structure_similarity(clips1, clips2) assert 0.0 < sim < 0.8 def test_different_types(self): """片段类型不同,类型相似度低。""" clips1 = [ {"clip_type": "video", "duration": 5.0}, {"clip_type": "video", "duration": 3.0}, ] clips2 = [ {"clip_type": "title", "duration": 5.0}, {"clip_type": "title", "duration": 3.0}, ] sim = compute_structure_similarity(clips1, clips2) assert sim <= 0.6 # 类型全部不同,但数量和时长相同贡献 0.6 def test_different_duration_distribution(self): """时长分布不同,时长相似度低。""" clips1 = [ {"clip_type": "video", "duration": 10.0}, # 占比 80% {"clip_type": "title", "duration": 2.5}, # 占比 20% ] clips2 = [ {"clip_type": "video", "duration": 2.0}, # 占比 20% {"clip_type": "title", "duration": 8.0}, # 占比 80% ] sim = compute_structure_similarity(clips1, clips2) assert 0.7 < sim < 0.9 # 类型相同但时长分布不同,sim=0.82 def test_similar_structure(self): """相似结构返回较高相似度。""" clips1 = [ {"clip_type": "video", "duration": 5.0}, {"clip_type": "title", "duration": 2.0}, {"clip_type": "video", "duration": 8.0}, ] clips2 = [ {"clip_type": "video", "duration": 5.5}, {"clip_type": "title", "duration": 2.2}, {"clip_type": "video", "duration": 7.5}, ] sim = compute_structure_similarity(clips1, clips2) assert sim > 0.8 def test_single_clip(self): """单片段也能计算。""" clips1 = [{"clip_type": "video", "duration": 10.0}] clips2 = [{"clip_type": "video", "duration": 12.0}] sim = compute_structure_similarity(clips1, clips2) assert sim > 0.5 # 类型相同,数量相同,只是时长不同 class TestDimensionWeights: """Tests for dimension weight constants.""" def test_weights_sum_to_one(self): """多维度权重之和为 1.0。""" assert abs(VISUAL_WEIGHT + TEXT_WEIGHT + STRUCTURE_WEIGHT - 1.0) < 1e-9 def test_visual_weight_is_half(self): """视觉权重为 0.5。""" assert VISUAL_WEIGHT == 0.5 def test_text_weight_is_quarter(self): """文案权重为 0.25。""" assert TEXT_WEIGHT == 0.25 def test_structure_weight_is_quarter(self): """结构权重为 0.25。""" assert STRUCTURE_WEIGHT == 0.25