From 156375c60d5dafeadc6f11ef4bd44739de2e652f Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 13:35:04 +0800 Subject: [PATCH] =?UTF-8?q?test(wave152):=20duplication=E6=9F=A5=E9=87=8D?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=8D=95=E6=B5=8B=20+40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 domain/duplication.py 新增 40 个单测,纯逻辑 0 外部依赖: - DuplicateSegment.create 工厂/校验:10个 - DuplicationRecord.create 工厂/校验:11个 - 状态流转 (pending/processing/completed/failed):13个 - can_retry + reset_for_retry:4个 - segments 列表:3个 --- tests/unit/domain/test_duplication.py | 376 ++++++++++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 tests/unit/domain/test_duplication.py diff --git a/tests/unit/domain/test_duplication.py b/tests/unit/domain/test_duplication.py new file mode 100644 index 000000000..884bcbd0b --- /dev/null +++ b/tests/unit/domain/test_duplication.py @@ -0,0 +1,376 @@ +"""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"