Files
xiaoxia-saas/tests/unit/test_asset_scoring.py
T
CI Bot 7f3c462617
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m1s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 56s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 1m30s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m57s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m51s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m40s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 2m32s
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m54s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 31s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m13s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m3s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Failing after 1h12m17s
style: auto-format with black + isort + prettier
2026-07-26 10:47:55 +00:00

756 lines
26 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Deep unit tests for asset_scoring.py — multi-dimensional scoring + diverse selection.
深度覆盖:
- score_resolution: 10+ 边界情况
- score_duration: 10+ 边界情况
- score_bitrate: 10+ 边界情况
- calculate_total_score: 加权验证
- score_asset_detail: 完整评分流程
- diverse_selection: 各种分桶场景
- filter_candidates: 各种过滤条件
- 数据类 + 常量
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
import pytest
from packages.domain.asset_scoring import (
MEDIUM_BUCKET_MAX,
MIN_QUALITY_SCORE,
OPTIMAL_DURATION_MAX,
OPTIMAL_DURATION_MIN,
SHORT_BUCKET_MAX,
TARGET_HEIGHT,
TARGET_WIDTH,
WEIGHT_BITRATE,
WEIGHT_DURATION,
WEIGHT_QUALITY,
WEIGHT_RESOLUTION,
AssetScoreDetail,
SmartSelectResult,
_bucket_by_duration,
calculate_total_score,
diverse_selection,
filter_candidates,
score_asset_detail,
score_bitrate,
score_duration,
score_resolution,
)
# ── 辅助:模拟 asset 对象 ────────────────────────────────────────────────────
class MockStatus:
def __init__(self, value: str):
self.value = value
@dataclass
class MockAsset:
id: str = "asset_001"
status: Any = None
mime_type: str = "video/mp4"
quality_score: Optional[float] = None
width: Optional[int] = None
height: Optional[int] = None
duration: Optional[float] = None
file_size: int = 0
def __post_init__(self):
if self.status is None:
self.status = MockStatus("ready")
# ── 常量测试 ────────────────────────────────────────────────────────────────
class TestConstants:
def test_weights_sum_to_one(self):
total = WEIGHT_QUALITY + WEIGHT_RESOLUTION + WEIGHT_DURATION + WEIGHT_BITRATE
assert abs(total - 1.0) < 0.001
def test_target_resolution_1080p(self):
assert TARGET_WIDTH == 1920
assert TARGET_HEIGHT == 1080
def test_bucket_thresholds(self):
assert SHORT_BUCKET_MAX == 5.0
assert MEDIUM_BUCKET_MAX == 15.0
assert SHORT_BUCKET_MAX < MEDIUM_BUCKET_MAX
def test_optimal_duration_range(self):
assert OPTIMAL_DURATION_MIN == 3.0
assert OPTIMAL_DURATION_MAX == 30.0
assert OPTIMAL_DURATION_MIN < OPTIMAL_DURATION_MAX
def test_min_quality_score(self):
assert MIN_QUALITY_SCORE == 30.0
# ── 数据类测试 ──────────────────────────────────────────────────────────────
class TestDataClasses:
def test_asset_score_detail_defaults(self):
detail = AssetScoreDetail(
asset_id="a1",
total_score=0.8,
quality_score=0.7,
resolution_score=0.9,
duration_score=0.85,
bitrate_score=0.75,
duration=10.0,
)
assert detail.asset_id == "a1"
assert detail.total_score == 0.8
assert detail.duration == 10.0
def test_smart_select_result_defaults(self):
result = SmartSelectResult(
selected_ids=["a1", "a2"],
total_candidates=10,
filtered_out=3,
avg_score=0.75,
)
assert result.selected_ids == ["a1", "a2"]
assert result.details == []
assert result.total_candidates == 10
def test_smart_select_result_with_details(self):
detail = AssetScoreDetail(
asset_id="a1",
total_score=0.9,
quality_score=0.8,
resolution_score=0.95,
duration_score=0.9,
bitrate_score=0.85,
duration=5.0,
)
result = SmartSelectResult(
selected_ids=["a1"],
total_candidates=5,
filtered_out=0,
avg_score=0.9,
details=[detail],
)
assert len(result.details) == 1
assert result.details[0].asset_id == "a1"
# ── score_resolution ────────────────────────────────────────────────────────
class TestScoreResolution:
def test_exact_target_1080p(self):
score = score_resolution(1920, 1080)
assert score == 1.0
def test_4k_full_score(self):
score = score_resolution(3840, 2160)
assert score == 1.0
def test_higher_than_target_full_score(self):
score = score_resolution(2560, 1440)
assert score == 1.0
def test_720p_lower(self):
score = score_resolution(1280, 720)
# 720p 像素 = 921600, 1080p = 2073600
# ratio = 0.444, score = 0.3 + 0.7 * 0.444 = 0.611
assert 0.5 < score < 0.75
def test_480p_much_lower(self):
score = score_resolution(854, 480)
# 480p = 409,920 pixels, ratio = 0.197
# score = 0.3 + 0.7 * 0.197 = 0.438
assert 0.3 < score < 0.5
def test_none_width(self):
score = score_resolution(None, 1080)
assert score == 0.5
def test_none_height(self):
score = score_resolution(1920, None)
assert score == 0.5
def test_both_none(self):
score = score_resolution(None, None)
assert score == 0.5
def test_zero_width(self):
score = score_resolution(0, 1080)
assert score == 0.5
def test_zero_height(self):
score = score_resolution(1920, 0)
assert score == 0.5
def test_negative_width(self):
score = score_resolution(-100, 1080)
assert score == 0.5
def test_very_low_res_floor(self):
score = score_resolution(100, 100)
# 10000 pixels, ratio = 0.0048, score = 0.3 + 0.7*0.0048 = 0.303
# 但最低不低于 0.1
assert score >= 0.1
assert score < 0.5
def test_custom_target_resolution(self):
score = score_resolution(1280, 720, target_width=1280, target_height=720)
assert score == 1.0
def test_sd_resolution(self):
score = score_resolution(640, 480)
# VGA = 307,200, ratio = 0.148
assert score > 0.1
# ── score_duration ──────────────────────────────────────────────────────────
class TestScoreDuration:
def test_none_duration(self):
assert score_duration(None) == 0.5
def test_zero_duration(self):
assert score_duration(0.0) == 0.5
def test_negative_duration(self):
assert score_duration(-5.0) == 0.5
def test_optimal_lower_bound(self):
assert score_duration(OPTIMAL_DURATION_MIN) == 1.0
def test_optimal_upper_bound(self):
assert score_duration(OPTIMAL_DURATION_MAX) == 1.0
def test_optimal_middle(self):
assert score_duration(10.0) == 1.0
def test_below_optimal_short(self):
score = score_duration(1.5)
# ratio = 1.5/3 = 0.5, score = 0.3 + 0.7*0.5 = 0.65
assert score == pytest.approx(0.65, rel=1e-3)
def test_very_short_approaches_03(self):
score = score_duration(0.1)
# ratio = 0.1/3 = 0.033, score = 0.3 + 0.7*0.033 = 0.323
assert 0.3 < score < 0.4
def test_just_below_optimal(self):
score = score_duration(2.9)
assert score < 1.0
assert score > 0.9
def test_above_optimal_slightly(self):
score = score_duration(35.0)
# excess = 5, penalty = 5/10 * 0.1 = 0.05, score = 0.95
assert score == pytest.approx(0.95, rel=1e-3)
def test_above_optimal_moderate(self):
score = score_duration(60.0)
# excess = 30, penalty = 30/10 * 0.1 = 0.3, score = 0.7
assert score == pytest.approx(0.7, rel=1e-3)
def test_very_long_floor(self):
score = score_duration(1000.0)
# excess = 970, penalty = 970/10 * 0.1 = 9.7, capped at 0.8
# score = max(0.2, 1.0 - 0.8) = 0.2
assert score == 0.2
def test_1_second(self):
score = score_duration(1.0)
# ratio = 1/3 = 0.333, score = 0.3 + 0.7*0.333 = 0.533
assert score == pytest.approx(0.3 + 0.7 * (1.0 / 3.0), rel=1e-3)
# ── score_bitrate ───────────────────────────────────────────────────────────
class TestScoreBitrate:
def test_no_file_size(self):
assert score_bitrate(0, 10.0) == 0.5
def test_no_duration(self):
assert score_bitrate(1_000_000, None) == 0.5
def test_zero_duration(self):
assert score_bitrate(1_000_000, 0.0) == 0.5
def test_negative_duration(self):
assert score_bitrate(1_000_000, -5.0) == 0.5
def test_optimal_low_end(self):
# 2 Mbps for 10s = 2.5 MB
file_size = int(2_000_000 * 10 / 8)
score = score_bitrate(file_size, 10.0)
assert score == 1.0
def test_optimal_high_end(self):
# 8 Mbps for 10s = 10 MB
file_size = int(8_000_000 * 10 / 8)
score = score_bitrate(file_size, 10.0)
assert score == 1.0
def test_optimal_middle(self):
# 5 Mbps for 10s = 6.25 MB
file_size = int(5_000_000 * 10 / 8)
score = score_bitrate(file_size, 10.0)
assert score == 1.0
def test_low_bitrate(self):
# 1 Mbps for 10s = 1.25 MB
file_size = int(1_000_000 * 10 / 8)
score = score_bitrate(file_size, 10.0)
# ratio = 1/2 = 0.5, score = 0.3 + 0.7*0.5 = 0.65
assert score == pytest.approx(0.65, rel=1e-2)
def test_very_low_bitrate(self):
# 100 kbps for 10s = 125 KB
file_size = int(100_000 * 10 / 8)
score = score_bitrate(file_size, 10.0)
# ratio = 0.05, score = 0.3 + 0.7*0.05 = 0.335
assert 0.3 < score < 0.5
def test_high_bitrate_slightly(self):
# 10 Mbps (just above 8Mbps)
file_size = int(10_000_000 * 10 / 8)
score = score_bitrate(file_size, 10.0)
# excess ratio = 10/8 - 1 = 0.25, penalty = min(0.5, 0.25*0.2) = 0.05
# score = max(0.5, 1.0 - 0.05) = 0.95
assert score == pytest.approx(0.95, rel=1e-2)
def test_very_high_bitrate_floor(self):
# 100 Mbps
file_size = int(100_000_000 * 10 / 8)
score = score_bitrate(file_size, 10.0)
# excess ratio = 100/8 - 1 = 11.5, penalty = min(0.5, 11.5*0.2) = 0.5
# score = max(0.5, 1.0 - 0.5) = 0.5
assert score == 0.5
def test_1mbps_file_10s(self):
file_size = 1_000_000 # 1 MB
score = score_bitrate(file_size, 10.0)
# bitrate = 8*1M/10 = 0.8 Mbps
assert 0.3 < score < 0.7
# ── calculate_total_score ───────────────────────────────────────────────────
class TestCalculateTotalScore:
def test_perfect_score(self):
total = calculate_total_score(1.0, 1.0, 1.0, 1.0)
assert total == 1.0
def test_zero_score(self):
total = calculate_total_score(0.0, 0.0, 0.0, 0.0)
assert total == 0.0
def test_weighted_sum(self):
# 各维度不同分数
q, r, d, b = 0.8, 0.6, 0.9, 0.7
expected = WEIGHT_QUALITY * q + WEIGHT_RESOLUTION * r + WEIGHT_DURATION * d + WEIGHT_BITRATE * b
total = calculate_total_score(q, r, d, b)
assert total == pytest.approx(expected, rel=1e-4)
def test_quality_dominates(self):
# 质量分权重最高(0.5),变化影响最大
base = calculate_total_score(0.5, 0.5, 0.5, 0.5)
quality_up = calculate_total_score(1.0, 0.5, 0.5, 0.5)
resolution_up = calculate_total_score(0.5, 1.0, 0.5, 0.5)
# 质量分变化带来的差异最大
assert (quality_up - base) > (resolution_up - base)
def test_rounded_to_4_decimals(self):
# 1/3 这样的无限小数应该被截断
total = calculate_total_score(1 / 3, 1 / 3, 1 / 3, 1 / 3)
assert len(str(total).split(".")[-1]) <= 4
# ── score_asset_detail ──────────────────────────────────────────────────────
class TestScoreAssetDetail:
def test_full_asset(self):
detail = score_asset_detail(
asset_id="test_001",
quality=80.0,
width=1920,
height=1080,
duration=10.0,
file_size=5_000_000,
)
assert detail.asset_id == "test_001"
assert detail.quality_score == pytest.approx(0.8, rel=1e-3)
assert detail.resolution_score == 1.0
assert detail.duration_score == 1.0
assert 0.0 < detail.total_score <= 1.0
assert detail.duration == 10.0
def test_no_quality_default_05(self):
detail = score_asset_detail(
asset_id="a1",
quality=None,
width=1920,
height=1080,
duration=10.0,
file_size=5_000_000,
)
assert detail.quality_score == 0.5
def test_quality_100_is_1_0(self):
detail = score_asset_detail(
asset_id="a1",
quality=100.0,
width=1920,
height=1080,
duration=10.0,
file_size=5_000_000,
)
assert detail.quality_score == 1.0
def test_quality_zero_is_zero(self):
detail = score_asset_detail(
asset_id="a1",
quality=0.0,
width=1920,
height=1080,
duration=10.0,
file_size=5_000_000,
)
assert detail.quality_score == 0.0
def test_all_unknown_medium_score(self):
detail = score_asset_detail(
asset_id="a1",
quality=None,
width=None,
height=None,
duration=None,
file_size=0,
)
# 全部未知:质量0.5,分辨率0.5,时长0.5,码率0.5
assert detail.total_score == pytest.approx(0.5, rel=1e-3)
def test_custom_target_resolution(self):
detail = score_asset_detail(
asset_id="a1",
quality=100.0,
width=1280,
height=720,
duration=10.0,
file_size=5_000_000,
target_width=1280,
target_height=720,
)
assert detail.resolution_score == 1.0
def test_scores_are_rounded(self):
detail = score_asset_detail(
asset_id="a1",
quality=33.3,
width=854,
height=480,
duration=1.5,
file_size=1_000_000,
)
# 所有分数字符串长度不超过 0.xxxx 格式
for attr in ["quality_score", "resolution_score", "duration_score", "bitrate_score", "total_score"]:
val = getattr(detail, attr)
assert isinstance(val, float)
# ── _bucket_by_duration ─────────────────────────────────────────────────────
class TestBucketByDuration:
def test_short_bucket(self):
d = AssetScoreDetail("a", 0.5, 0.5, 0.5, 0.5, 0.5, 3.0)
assert _bucket_by_duration(d) == "short"
def test_short_bucket_boundary(self):
d = AssetScoreDetail("a", 0.5, 0.5, 0.5, 0.5, 0.5, 4.9)
assert _bucket_by_duration(d) == "short"
def test_medium_bucket(self):
d = AssetScoreDetail("a", 0.5, 0.5, 0.5, 0.5, 0.5, 10.0)
assert _bucket_by_duration(d) == "medium"
def test_medium_lower_boundary(self):
d = AssetScoreDetail("a", 0.5, 0.5, 0.5, 0.5, 0.5, SHORT_BUCKET_MAX)
assert _bucket_by_duration(d) == "medium"
def test_medium_upper_boundary(self):
d = AssetScoreDetail("a", 0.5, 0.5, 0.5, 0.5, 0.5, 14.9)
assert _bucket_by_duration(d) == "medium"
def test_long_bucket(self):
d = AssetScoreDetail("a", 0.5, 0.5, 0.5, 0.5, 0.5, 20.0)
assert _bucket_by_duration(d) == "long"
def test_long_lower_boundary(self):
d = AssetScoreDetail("a", 0.5, 0.5, 0.5, 0.5, 0.5, MEDIUM_BUCKET_MAX)
assert _bucket_by_duration(d) == "long"
def test_none_duration(self):
d = AssetScoreDetail("a", 0.5, 0.5, 0.5, 0.5, 0.5, None)
assert _bucket_by_duration(d) == "unknown"
# ── diverse_selection ───────────────────────────────────────────────────────
def _make_scored(items: list[tuple[str, float, float]]) -> list[AssetScoreDetail]:
"""构造评分列表: (asset_id, total_score, duration)"""
return [
AssetScoreDetail(
asset_id=aid,
total_score=score,
quality_score=score,
resolution_score=score,
duration_score=score,
bitrate_score=score,
duration=dur,
)
for aid, score, dur in items
]
class TestDiverseSelection:
def test_empty_input(self):
result = diverse_selection([], 5)
assert result == []
def test_zero_count(self):
scored = _make_scored([("a1", 0.9, 10.0)])
result = diverse_selection(scored, 0)
assert result == []
def test_negative_count(self):
scored = _make_scored([("a1", 0.9, 10.0)])
result = diverse_selection(scored, -1)
assert result == []
def test_fewer_than_count(self):
scored = _make_scored([("a1", 0.9, 10.0)])
result = diverse_selection(scored, 5)
assert len(result) == 1
def test_mixed_buckets_diversity(self):
# 3短 + 3中 + 3长,取6个
items = [
("s1", 0.9, 2.0),
("s2", 0.8, 3.0),
("s3", 0.7, 4.0),
("m1", 0.95, 8.0),
("m2", 0.85, 10.0),
("m3", 0.75, 12.0),
("l1", 0.92, 20.0),
("l2", 0.82, 25.0),
("l3", 0.72, 30.0),
]
scored = _make_scored(items)
scored.sort(key=lambda d: d.total_score, reverse=True)
result = diverse_selection(scored, 6)
assert len(result) == 6
# 每个桶至少1个(base_quota = max(1, 6//3) = 2
ids = [r.asset_id for r in result]
short_count = sum(1 for r in result if r.duration and r.duration < SHORT_BUCKET_MAX)
medium_count = sum(1 for r in result if r.duration and SHORT_BUCKET_MAX <= r.duration < MEDIUM_BUCKET_MAX)
long_count = sum(1 for r in result if r.duration and r.duration >= MEDIUM_BUCKET_MAX)
assert short_count >= 1
assert medium_count >= 1
assert long_count >= 1
def test_all_short_fallback_to_global(self):
items = [("s1", 0.9, 2.0), ("s2", 0.8, 3.0), ("s3", 0.7, 4.0)]
scored = _make_scored(items)
result = diverse_selection(scored, 3)
assert len(result) == 3
# 都是短素材,只能取短的
assert all(r.duration and r.duration < SHORT_BUCKET_MAX for r in result)
def test_sorted_by_score_descending(self):
items = [("a1", 0.5, 10.0), ("a2", 0.9, 10.0), ("a3", 0.7, 10.0)]
scored = _make_scored(items)
scored.sort(key=lambda d: d.total_score, reverse=True)
result = diverse_selection(scored, 3)
assert len(result) == 3
assert result[0].total_score >= result[1].total_score >= result[2].total_score
def test_count_one_each_bucket(self):
# count=3, base_quota=max(1,1)=1,每桶1个共3个
items = [
("s1", 0.9, 2.0),
("m1", 0.95, 8.0),
("l1", 0.92, 20.0),
]
scored = _make_scored(items)
scored.sort(key=lambda d: d.total_score, reverse=True)
result = diverse_selection(scored, 3)
assert len(result) == 3
# 每桶1个
assert any(r.duration and r.duration < SHORT_BUCKET_MAX for r in result)
assert any(r.duration and SHORT_BUCKET_MAX <= r.duration < MEDIUM_BUCKET_MAX for r in result)
assert any(r.duration and r.duration >= MEDIUM_BUCKET_MAX for r in result)
def test_unknown_duration_used_last(self):
items = [
("u1", 0.99, None), # 分最高但未知
("s1", 0.9, 2.0),
("m1", 0.8, 10.0),
("l1", 0.7, 20.0),
]
scored = _make_scored(items)
scored.sort(key=lambda d: d.total_score, reverse=True)
result = diverse_selection(scored, 3)
# 前3个应该是三个已知桶各一个
ids = [r.asset_id for r in result]
# u1 不应该在前3(因为 unknown 桶最后才用)
assert "s1" in ids
assert "m1" in ids
assert "l1" in ids
def test_no_duplicates(self):
items = [("s1", 0.9, 2.0), ("s2", 0.8, 3.0)]
scored = _make_scored(items)
result = diverse_selection(scored, 5)
ids = [r.asset_id for r in result]
assert len(ids) == len(set(ids))
def test_many_more_than_count(self):
# 30个素材,取6个
items = []
for i in range(10):
items.append((f"s{i}", 0.9 - i * 0.05, 2.0 + i * 0.2))
items.append((f"m{i}", 0.9 - i * 0.03, 6.0 + i * 0.8))
items.append((f"l{i}", 0.9 - i * 0.04, 16.0 + i * 1.5))
scored = _make_scored(items)
scored.sort(key=lambda d: d.total_score, reverse=True)
result = diverse_selection(scored, 6)
assert len(result) == 6
# 有多样性
durations = [r.duration for r in result]
short = sum(1 for d in durations if d and d < SHORT_BUCKET_MAX)
medium = sum(1 for d in durations if d and SHORT_BUCKET_MAX <= d < MEDIUM_BUCKET_MAX)
long_ = sum(1 for d in durations if d and d >= MEDIUM_BUCKET_MAX)
assert short >= 1
assert medium >= 1
assert long_ >= 1
# ── filter_candidates ───────────────────────────────────────────────────────
class TestFilterCandidates:
def test_empty_list(self):
candidates, filtered = filter_candidates([])
assert candidates == []
assert filtered == 0
def test_ready_video_passes(self):
assets = [MockAsset(id="a1", status=MockStatus("ready"), mime_type="video/mp4")]
candidates, filtered = filter_candidates(assets)
assert len(candidates) == 1
assert filtered == 0
def test_non_ready_filtered(self):
assets = [
MockAsset(id="a1", status=MockStatus("processing"), mime_type="video/mp4"),
MockAsset(id="a2", status=MockStatus("ready"), mime_type="video/mp4"),
]
candidates, filtered = filter_candidates(assets)
assert len(candidates) == 1
assert candidates[0].id == "a2"
assert filtered == 0 # 非ready不算filtered_outfiltered_out只算质量分过滤的)
def test_non_video_filtered(self):
assets = [
MockAsset(id="a1", mime_type="image/jpeg"),
MockAsset(id="a2", mime_type="video/mp4"),
]
candidates, filtered = filter_candidates(assets)
assert len(candidates) == 1
assert candidates[0].id == "a2"
def test_low_quality_filtered(self):
assets = [
MockAsset(id="low", quality_score=20.0),
MockAsset(id="high", quality_score=80.0),
]
candidates, filtered = filter_candidates(assets, min_quality_score=30.0)
assert len(candidates) == 1
assert candidates[0].id == "high"
assert filtered == 1
def test_quality_none_passes(self):
assets = [MockAsset(id="a1", quality_score=None)]
candidates, filtered = filter_candidates(assets)
assert len(candidates) == 1
assert filtered == 0
def test_quality_exact_min_passes(self):
assets = [MockAsset(id="a1", quality_score=30.0)]
candidates, filtered = filter_candidates(assets, min_quality_score=30.0)
assert len(candidates) == 1
assert filtered == 0
def test_string_status(self):
# status 是字符串不是 Enum
@dataclass
class StrAsset:
id: str = "a1"
status: str = "ready"
mime_type: str = "video/mp4"
quality_score: float = 80.0
width: int = 1920
height: int = 1080
duration: float = 10.0
file_size: int = 5_000_000
assets = [StrAsset()]
candidates, filtered = filter_candidates(assets)
assert len(candidates) == 1
def test_empty_mime_type(self):
assets = [MockAsset(id="a1", mime_type="")]
candidates, filtered = filter_candidates(assets)
assert len(candidates) == 0
def test_none_mime_type(self):
# mime_type 是 None
@dataclass
class NoneMimeAsset:
id: str = "a1"
status: Any = None
mime_type: str | None = None
quality_score: float = 80.0
width: int = 1920
height: int = 1080
duration: float = 10.0
file_size: int = 5_000_000
def __post_init__(self):
if self.status is None:
self.status = MockStatus("ready")
assets = [NoneMimeAsset()]
candidates, filtered = filter_candidates(assets)
assert len(candidates) == 0
def test_custom_min_quality(self):
assets = [
MockAsset(id="low", quality_score=40.0),
MockAsset(id="high", quality_score=60.0),
]
candidates, filtered = filter_candidates(assets, min_quality_score=50.0)
assert len(candidates) == 1
assert filtered == 1