cfdf7fcefa
AI Code Review / AI Code Review (pull_request) Failing after 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 8s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m22s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m29s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m29s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 38s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m40s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m34s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m13s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m50s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m4s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 52s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Failing after 47s
165 lines
5.8 KiB
Python
Executable File
165 lines
5.8 KiB
Python
Executable File
"""去重纯算法测试 — hamming_distance + histogram_similarity + VideoFingerprint."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from unittest.mock import MagicMock
|
||
|
||
import numpy as np
|
||
import pytest
|
||
|
||
# 模块级mock cv2(dedup模块import时需要)
|
||
sys.modules["cv2"] = MagicMock()
|
||
|
||
from video_processing.dedup import ( # noqa: E402
|
||
VideoDeduplicator,
|
||
VideoFingerprint,
|
||
hamming_distance,
|
||
)
|
||
|
||
|
||
class TestHammingDistance:
|
||
"""hamming_distance 汉明距离计算测试."""
|
||
|
||
def test_identical_hashes_zero(self):
|
||
"""相同哈希距离为0."""
|
||
assert hamming_distance("ff", "ff") == 0
|
||
assert hamming_distance("00", "00") == 0
|
||
|
||
def test_all_different(self):
|
||
"""全不同的8bit哈希距离为8."""
|
||
assert hamming_distance("00", "ff") == 8
|
||
|
||
def test_single_bit_diff(self):
|
||
"""1个bit不同."""
|
||
# 0x01 = 00000001, 0x00 = 00000000 → 1 bit不同
|
||
assert hamming_distance("01", "00") == 1
|
||
|
||
def test_four_bits_diff(self):
|
||
"""4个bit不同."""
|
||
# 0x0F = 00001111, 0xF0 = 11110000 → 8 bits都不同
|
||
assert hamming_distance("0f", "f0") == 8
|
||
|
||
def test_longer_hashes(self):
|
||
"""更长的哈希(如64-bit pHash)."""
|
||
# 两个完全不同的64-bit哈希
|
||
assert hamming_distance("0000000000000000", "ffffffffffffffff") == 64
|
||
|
||
def test_partial_difference(self):
|
||
"""部分bit不同."""
|
||
# a = 1010, 5 = 0101 → 4 bits不同(每个hex digit)
|
||
assert hamming_distance("aa", "55") == 8
|
||
|
||
def test_case_insensitive(self):
|
||
"""十六进制不区分大小写."""
|
||
assert hamming_distance("FF", "ff") == 0
|
||
assert hamming_distance("AbC123", "aBc123") == 0
|
||
|
||
def test_different_length_hashes(self):
|
||
"""不同长度的哈希(短的前补零)."""
|
||
# "ff" = 0xff = 255, "0ff" = 0x0ff = 255
|
||
# int("ff", 16) = 255, int("0ff", 16) = 255
|
||
assert hamming_distance("ff", "0ff") == 0
|
||
|
||
|
||
class TestVideoFingerprint:
|
||
"""VideoFingerprint 数据结构测试."""
|
||
|
||
def test_to_dict_contains_all_fields(self):
|
||
"""to_dict返回完整字典."""
|
||
fp = VideoFingerprint(
|
||
md5="abc123",
|
||
keyframe_phashes=["hash1", "hash2"],
|
||
color_histograms=[[0.1, 0.2], [0.3, 0.4]],
|
||
duration=30.5,
|
||
resolution=(1920, 1080),
|
||
)
|
||
d = fp.to_dict()
|
||
assert d["md5"] == "abc123"
|
||
assert d["keyframe_phashes"] == ["hash1", "hash2"]
|
||
assert d["duration"] == 30.5
|
||
assert d["resolution"] == [1920, 1080]
|
||
assert "color_histograms" in d
|
||
|
||
def test_empty_phashes(self):
|
||
"""空关键帧列表."""
|
||
fp = VideoFingerprint(
|
||
md5="test",
|
||
keyframe_phashes=[],
|
||
color_histograms=[],
|
||
duration=0.0,
|
||
resolution=(0, 0),
|
||
)
|
||
d = fp.to_dict()
|
||
assert d["keyframe_phashes"] == []
|
||
assert d["color_histograms"] == []
|
||
|
||
|
||
class TestAverageHistogramSimilarity:
|
||
"""_average_histogram_similarity 直方图相似度测试."""
|
||
|
||
def test_identical_histograms(self):
|
||
"""完全相同的直方图相似度为1.0."""
|
||
hist = [[0.5, 0.5, 0.0], [0.3, 0.4, 0.3]]
|
||
sim = VideoDeduplicator._average_histogram_similarity(hist, hist)
|
||
assert sim == pytest.approx(1.0)
|
||
|
||
def test_empty_first_list(self):
|
||
"""第一组为空返回0."""
|
||
sim = VideoDeduplicator._average_histogram_similarity([], [[0.5, 0.5]])
|
||
assert sim == 0.0
|
||
|
||
def test_empty_second_list(self):
|
||
"""第二组为空返回0."""
|
||
sim = VideoDeduplicator._average_histogram_similarity([[0.5, 0.5]], [])
|
||
assert sim == 0.0
|
||
|
||
def test_both_empty(self):
|
||
"""两组都为空返回0."""
|
||
sim = VideoDeduplicator._average_histogram_similarity([], [])
|
||
assert sim == 0.0
|
||
|
||
def test_orthogonal_histograms(self):
|
||
"""正交直方图相似度为0."""
|
||
# [1, 0] 和 [0, 1] 正交
|
||
sim = VideoDeduplicator._average_histogram_similarity([[1.0, 0.0]], [[0.0, 1.0]])
|
||
assert sim == pytest.approx(0.0)
|
||
|
||
def test_partial_similarity(self):
|
||
"""部分相似."""
|
||
# [1, 1] 和 [1, 0] 的余弦相似度 = 1/√2 ≈ 0.707
|
||
sim = VideoDeduplicator._average_histogram_similarity([[1.0, 1.0]], [[1.0, 0.0]])
|
||
assert sim == pytest.approx(1.0 / (2**0.5), rel=0.01)
|
||
|
||
def test_multiple_frames_best_match(self):
|
||
"""多帧时取最佳匹配."""
|
||
# 第一帧完全不同,第二帧完全相同 → 平均 best = (0 + 1) / 2 = 0.5
|
||
sim = VideoDeduplicator._average_histogram_similarity(
|
||
[[1.0, 0.0], [0.0, 1.0]],
|
||
[[0.0, 1.0]], # 只有一帧,和第一帧0相似,和第二帧1相似
|
||
)
|
||
# 第一帧最佳匹配=0,第二帧最佳匹配=1,平均=0.5
|
||
assert sim == pytest.approx(0.5)
|
||
|
||
def test_zero_norm_histogram_skipped(self):
|
||
"""零范数直方图被跳过."""
|
||
sim = VideoDeduplicator._average_histogram_similarity([[0.0, 0.0]], [[1.0, 1.0]])
|
||
# 第一组的零范数被跳过,similarities为空,返回0
|
||
assert sim == 0.0
|
||
|
||
def test_different_length_histograms(self):
|
||
"""不同长度的直方图取最小长度对齐."""
|
||
sim = VideoDeduplicator._average_histogram_similarity(
|
||
[[1.0, 1.0, 0.0, 0.0]], # 4维
|
||
[[1.0, 1.0]], # 2维
|
||
)
|
||
# 对齐到前2维,都是[1,1],相似度1.0
|
||
assert sim == pytest.approx(1.0)
|
||
|
||
def test_similarity_in_zero_one_range(self):
|
||
"""相似度在[0, 1]范围内."""
|
||
hist_a = [np.random.rand(96).tolist() for _ in range(5)]
|
||
hist_b = [np.random.rand(96).tolist() for _ in range(5)]
|
||
sim = VideoDeduplicator._average_histogram_similarity(hist_a, hist_b)
|
||
assert 0.0 <= sim <= 1.0
|