"""duplication 单测. domain 层查重记录纯逻辑模块,0 外部依赖。 覆盖:DuplicateSegment 工厂/校验、DuplicationRecord 创建/状态流转/重试。 """ from __future__ import annotations from packages.domain.duplication import DuplicateSegment, DuplicationRecord class TestDuplicateSegmentCreate: """DuplicateSegment.create 工厂方法测试.""" def test_create_valid(self): """正常创建.""" seg = DuplicateSegment.create( source_start=1.0, source_end=5.0, matched_video_id="vid123", matched_video_name="测试视频", matched_start=10.0, matched_end=14.0, similarity=85.5, ) assert seg.source_start == 1.0 assert seg.source_end == 5.0 assert seg.matched_video_id == "vid123" assert seg.matched_video_name == "测试视频" assert seg.matched_start == 10.0 assert seg.matched_end == 14.0 assert seg.similarity == 85.5 assert isinstance(seg.id, str) assert len(seg.id) > 0 def test_create_generates_unique_id(self): """每次创建生成不同的 id.""" seg1 = DuplicateSegment.create(0, 1, "v", "n", 0, 1, 50.0) seg2 = DuplicateSegment.create(0, 1, "v", "n", 0, 1, 50.0) assert seg1.id != seg2.id def test_create_negative_source_start(self): """source_start 为负抛出 ValueError.""" try: DuplicateSegment.create(-1, 5, "v", "n", 0, 1, 50.0) assert False, "应该抛出 ValueError" except ValueError as e: assert "source" in str(e).lower() def test_create_source_end_equals_start(self): """source_end 等于 source_start 无效.""" try: DuplicateSegment.create(5, 5, "v", "n", 0, 1, 50.0) assert False except ValueError as e: assert "source" in str(e).lower() def test_create_source_end_less_than_start(self): """source_end 小于 source_start 无效.""" try: DuplicateSegment.create(5, 3, "v", "n", 0, 1, 50.0) assert False except ValueError as e: assert "source" in str(e).lower() def test_create_negative_matched_start(self): """matched_start 为负无效.""" try: DuplicateSegment.create(0, 5, "v", "n", -1, 1, 50.0) assert False except ValueError as e: assert "matched" in str(e).lower() def test_create_matched_end_invalid(self): """matched_end <= matched_start 无效.""" try: DuplicateSegment.create(0, 5, "v", "n", 5, 5, 50.0) assert False except ValueError as e: assert "matched" in str(e).lower() def test_create_similarity_zero(self): """similarity = 0 是合法的.""" seg = DuplicateSegment.create(0, 1, "v", "n", 0, 1, 0.0) assert seg.similarity == 0.0 def test_create_similarity_100(self): """similarity = 100 是合法的.""" seg = DuplicateSegment.create(0, 1, "v", "n", 0, 1, 100.0) assert seg.similarity == 100.0 def test_create_similarity_negative(self): """similarity < 0 无效.""" try: DuplicateSegment.create(0, 1, "v", "n", 0, 1, -1.0) assert False except ValueError as e: assert "similarity" in str(e).lower() def test_create_similarity_over_100(self): """similarity > 100 无效.""" try: DuplicateSegment.create(0, 1, "v", "n", 0, 1, 101.0) assert False except ValueError as e: assert "similarity" in str(e).lower() class TestDuplicationRecordCreate: """DuplicationRecord.create 工厂方法测试.""" def test_create_minimal(self): """最简创建.""" rec = DuplicationRecord.create( user_id="user1", filename="test.mp4", file_size=1024, storage_key="oss://bucket/test.mp4", ) assert rec.user_id == "user1" assert rec.filename == "test.mp4" assert rec.file_size == 1024 assert rec.storage_key == "oss://bucket/test.mp4" assert rec.duration_seconds == 0.0 assert rec.status == "pending" assert rec.duplicate_rate is None assert rec.duplicate_count == 0 assert rec.segments == [] assert rec.error_message == "" assert isinstance(rec.id, str) assert len(rec.id) > 0 def test_create_with_duration(self): """带时长创建.""" rec = DuplicationRecord.create( user_id="user1", filename="test.mp4", file_size=1024, storage_key="oss://key", duration_seconds=120.5, ) assert rec.duration_seconds == 120.5 def test_create_strips_whitespace(self): """user_id 和 filename 会 strip.""" rec = DuplicationRecord.create( user_id=" user1 ", filename=" test.mp4 ", file_size=1024, storage_key="oss://key", ) assert rec.user_id == "user1" assert rec.filename == "test.mp4" def test_create_empty_user_id(self): """空 user_id 无效.""" try: DuplicationRecord.create("", "test.mp4", 1024, "oss://key") assert False except ValueError as e: assert "user_id" in str(e) def test_create_whitespace_user_id(self): """纯空白 user_id 无效.""" try: DuplicationRecord.create(" ", "test.mp4", 1024, "oss://key") assert False except ValueError as e: assert "user_id" in str(e) def test_create_empty_filename(self): """空 filename 无效.""" try: DuplicationRecord.create("user1", "", 1024, "oss://key") assert False except ValueError as e: assert "filename" in str(e) def test_create_whitespace_filename(self): """纯空白 filename 无效.""" try: DuplicationRecord.create("user1", " ", 1024, "oss://key") assert False except ValueError as e: assert "filename" in str(e) def test_create_zero_file_size(self): """file_size = 0 无效.""" try: DuplicationRecord.create("user1", "test.mp4", 0, "oss://key") assert False except ValueError as e: assert "file_size" in str(e) def test_create_negative_file_size(self): """file_size 为负无效.""" try: DuplicationRecord.create("user1", "test.mp4", -1, "oss://key") assert False except ValueError as e: assert "file_size" in str(e) def test_create_unique_id(self): """不同记录 id 不同.""" r1 = DuplicationRecord.create("u", "f", 1, "k") r2 = DuplicationRecord.create("u", "f", 1, "k") assert r1.id != r2.id def test_create_has_timestamps(self): """有创建和更新时间.""" rec = DuplicationRecord.create("u", "f", 1, "k") assert rec.created_at is not None assert rec.updated_at is not None # 两者应该很接近(都是 now) delta = (rec.updated_at - rec.created_at).total_seconds() assert abs(delta) < 1.0 class TestDuplicationRecordStatusFlow: """状态流转测试.""" def _make_record(self): return DuplicationRecord.create("user1", "test.mp4", 1024, "oss://key") def test_initial_status_pending(self): """初始状态 pending.""" rec = self._make_record() assert rec.status == "pending" def test_mark_processing(self): """标记为处理中.""" rec = self._make_record() old_updated = rec.updated_at rec.mark_processing() assert rec.status == "processing" assert rec.updated_at >= old_updated def test_mark_completed(self): """标记为完成.""" rec = self._make_record() seg = DuplicateSegment.create(0, 1, "v", "n", 0, 1, 80.0) rec.mark_completed(duplicate_rate=45.5, duplicate_count=3, segments=[seg]) assert rec.status == "completed" assert rec.duplicate_rate == 45.5 assert rec.duplicate_count == 3 assert len(rec.segments) == 1 assert rec.segments[0].similarity == 80.0 def test_mark_completed_zero_rate(self): """重复率为 0 合法.""" rec = self._make_record() rec.mark_completed(0.0, 0, []) assert rec.status == "completed" assert rec.duplicate_rate == 0.0 assert rec.duplicate_count == 0 assert rec.segments == [] def test_mark_completed_full_rate(self): """重复率 100 合法.""" rec = self._make_record() rec.mark_completed(100.0, 1, []) assert rec.duplicate_rate == 100.0 def test_mark_completed_negative_rate(self): """重复率为负无效.""" rec = self._make_record() try: rec.mark_completed(-1, 0, []) assert False except ValueError as e: assert "duplicate_rate" in str(e) def test_mark_completed_over_100(self): """重复率超过 100 无效.""" rec = self._make_record() try: rec.mark_completed(101, 0, []) assert False except ValueError as e: assert "duplicate_rate" in str(e) def test_mark_failed(self): """标记为失败.""" rec = self._make_record() rec.mark_failed("网络超时") assert rec.status == "failed" assert rec.error_message == "网络超时" def test_mark_failed_empty_message(self): """失败信息可以为空字符串.""" rec = self._make_record() rec.mark_failed("") assert rec.status == "failed" assert rec.error_message == "" def test_can_retry_failed(self): """failed 状态可以重试.""" rec = self._make_record() rec.mark_failed("error") assert rec.can_retry() is True def test_cannot_retry_pending(self): """pending 状态不可重试.""" rec = self._make_record() assert rec.can_retry() is False def test_cannot_retry_processing(self): """processing 状态不可重试.""" rec = self._make_record() rec.mark_processing() assert rec.can_retry() is False def test_cannot_retry_completed(self): """completed 状态不可重试.""" rec = self._make_record() rec.mark_completed(50, 1, []) assert rec.can_retry() is False def test_reset_for_retry(self): """重置回 pending.""" rec = self._make_record() rec.mark_failed("error") seg = DuplicateSegment.create(0, 1, "v", "n", 0, 1, 50.0) rec.segments = [seg] rec.video_fingerprint = {"hash": "abc"} rec.duplicate_rate = 50.0 rec.duplicate_count = 5 rec.reset_for_retry() assert rec.status == "pending" assert rec.duplicate_rate is None assert rec.duplicate_count == 0 assert rec.error_message == "" assert rec.segments == [] assert rec.video_fingerprint is None def test_reset_updates_timestamp(self): """重置更新 updated_at.""" rec = self._make_record() rec.mark_failed("error") old_updated = rec.updated_at rec.reset_for_retry() assert rec.updated_at >= old_updated class TestDuplicationRecordSegments: """segments 列表相关测试.""" def _make_record(self): return DuplicationRecord.create("user1", "test.mp4", 1024, "oss://key") def test_segments_default_empty(self): """初始 segments 为空列表.""" rec = self._make_record() assert rec.segments == [] def test_segments_independent_list(self): """不同记录的 segments 是独立列表.""" r1 = self._make_record() r2 = self._make_record() r1.segments.append("fake") assert len(r2.segments) == 0 def test_completed_with_multiple_segments(self): """完成时带多个片段.""" rec = self._make_record() segs = [ DuplicateSegment.create(0, 1, "v1", "n1", 0, 1, 90.0), DuplicateSegment.create(2, 3, "v2", "n2", 5, 6, 70.0), DuplicateSegment.create(4, 5, "v3", "n3", 10, 11, 85.0), ] rec.mark_completed(60.0, 3, segs) assert len(rec.segments) == 3 assert rec.segments[0].similarity == 90.0 assert rec.segments[1].matched_video_id == "v2" assert rec.segments[2].matched_video_name == "n3"