Files
xiaoxia-saas/tests/unit/test_asset_quality_scoring.py
T
xiaoxia 3b949a464f
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
第89波 - 拆分asset_analyzer评分纯逻辑 + 64单测 (#938)
2026-07-26 18:15:22 +08:00

668 lines
25 KiB
Python
Executable File

"""asset_quality_scoring 纯逻辑单测 — 第89波.
测试评分纯函数,不依赖 FFmpeg/视频文件。
覆盖:分辨率评分、帧率评分、码率评分、清晰度评分、稳定性评分、
质量总评分、9分类评分、分类结果计算。
"""
import numpy as np
import pytest
from apps.worker.worker_app.tasks.asset_quality_scoring import (
AudioAnalysis,
ClassificationResult,
ColorAnalysis,
MotionAnalysis,
QualityScore,
VideoInfo,
calculate_category_scores,
calculate_quality_score,
classify_from_analysis,
score_bitrate,
score_clarity,
score_framerate,
score_resolution,
score_stability,
)
from packages.domain.classification import AssetClassification
# ── 分辨率评分 ────────────────────────────────────────────────────────
class TestScoreResolution:
"""分辨率评分边界测试."""
def test_4k_full_score(self):
"""4K 及以上满分 25."""
assert score_resolution(3840, 2160) == 25.0
assert score_resolution(4096, 2160) == 25.0
assert score_resolution(7680, 4320) == 25.0 # 8K
def test_2k_score(self):
"""2K 档 22 分."""
assert score_resolution(2560, 1440) == 22.0
assert score_resolution(3000, 1600) == 22.0
# 刚好低于 4K
assert score_resolution(3839, 2159) == 22.0
def test_1080p_score(self):
"""1080p 档 20 分."""
assert score_resolution(1920, 1080) == 20.0
assert score_resolution(2000, 1080) == 20.0
# 刚好低于 2K
assert score_resolution(2559, 1439) == 20.0
def test_720p_score(self):
"""720p 档 15 分."""
assert score_resolution(1280, 720) == 15.0
assert score_resolution(1280, 720) == 15.0
# 刚好低于 1080p
assert score_resolution(1919, 1079) == 15.0
# 1080x720 像素数 < 1280x720,掉到 480p 档
assert score_resolution(1080, 720) == 8.0
def test_480p_score(self):
"""480p 档 8 分."""
assert score_resolution(854, 480) == 8.0
assert score_resolution(854, 480) == 8.0
# 刚好低于 720p
assert score_resolution(1279, 719) == 8.0
# 720x480 像素数 < 854x480,掉到最低档
assert score_resolution(720, 480) == 3.0
def test_low_resolution_score(self):
"""低于 480p 给 3 分."""
assert score_resolution(640, 360) == 3.0
assert score_resolution(320, 240) == 3.0
assert score_resolution(0, 0) == 3.0
def test_non_standard_aspect_ratio(self):
"""非标准宽高比按像素总数计算."""
# 竖屏 1080x1920 像素数 = 1080p
assert score_resolution(1080, 1920) == 20.0
# 超宽屏
assert score_resolution(2560, 1080) == 20.0 # 像素≈2.7M < 2K(3.6M)
# 1x1 极低分辨率
assert score_resolution(1, 1) == 3.0
def test_negative_values(self):
"""负尺寸:负负得正按像素数算,一正一负 = 负数像素 = 最低档."""
# 一正一负 → 负像素总数 → < 480p → 3分
assert score_resolution(1920, -1080) == 3.0
assert score_resolution(-1920, 1080) == 3.0
# 都是 0 → 3分
assert score_resolution(0, 0) == 3.0
# ── 帧率评分 ──────────────────────────────────────────────────────────
class TestScoreFramerate:
"""帧率评分边界测试."""
def test_60fps_full_score(self):
"""60fps 及以上满分 20."""
assert score_framerate(60) == 20.0
assert score_framerate(120) == 20.0
assert score_framerate(240) == 20.0
def test_30fps_score(self):
"""30-59fps 给 15 分."""
assert score_framerate(30) == 15.0
assert score_framerate(59.9) == 15.0
assert score_framerate(59) == 15.0
def test_24fps_score(self):
"""24-29fps 给 10 分."""
assert score_framerate(24) == 10.0
assert score_framerate(29.97) == 10.0
assert score_framerate(25) == 10.0
def test_15fps_score(self):
"""15-23fps 给 7 分."""
assert score_framerate(15) == 7.0
assert score_framerate(23.9) == 7.0
assert score_framerate(20) == 7.0
def test_low_fps_score(self):
"""低于 15fps 给 5 分."""
assert score_framerate(10) == 5.0
assert score_framerate(1) == 5.0
assert score_framerate(0) == 5.0
def test_negative_fps(self):
"""负帧率按最低档."""
assert score_framerate(-30) == 5.0
def test_float_fps(self):
"""浮点帧率正确判断边界."""
# 29.97 (NTSC) < 30 → 24fps 档
assert score_framerate(29.97) == 10.0
assert score_framerate(23.976) == 7.0
assert score_framerate(59.94) == 15.0 # 59.94 < 60 → 30fps 档
assert score_framerate(30.0) == 15.0
# ── 码率评分 ──────────────────────────────────────────────────────────
class TestScoreBitrate:
"""码率评分边界测试."""
def test_high_bitrate_full_score(self):
"""10Mbps 以上满分 20."""
assert score_bitrate(10_000_001) == 20.0
assert score_bitrate(50_000_000) == 20.0
assert score_bitrate(100_000_000) == 20.0
def test_5mbps_score(self):
"""5-10Mbps 给 15 分."""
assert score_bitrate(5_000_000) == 15.0
assert score_bitrate(8_000_000) == 15.0
assert score_bitrate(10_000_000) == 15.0 # 刚好 10Mbps = 不 > 10
def test_2mbps_score(self):
"""2-5Mbps 给 10 分."""
assert score_bitrate(2_000_000) == 10.0
assert score_bitrate(3_000_000) == 10.0
assert score_bitrate(4_999_999) == 10.0
def test_05mbps_score(self):
"""0.5-2Mbps 给 5 分."""
assert score_bitrate(500_000) == 5.0
assert score_bitrate(1_000_000) == 5.0
assert score_bitrate(1_999_999) == 5.0
def test_low_bitrate_score(self):
"""低于 0.5Mbps 给 3 分."""
assert score_bitrate(499_999) == 3.0
assert score_bitrate(100_000) == 3.0
assert score_bitrate(0) == 3.0
def test_negative_bitrate(self):
"""负码率按最低档."""
assert score_bitrate(-5_000_000) == 3.0
def test_zero_bitrate(self):
"""0 码率 = 最低档."""
assert score_bitrate(0) == 3.0
# ── 清晰度评分 ────────────────────────────────────────────────────────
class TestScoreClarity:
"""清晰度评分测试."""
def test_empty_frames_default_score(self):
"""空帧列表给默认 10 分."""
assert score_clarity([]) == 10.0
def test_constant_image_low_clarity(self):
"""纯色图像比高细节图像清晰度低很多."""
# 纯灰色图像
gray_frame = np.full((100, 100), 128, dtype=np.uint8)
score_constant = score_clarity([gray_frame])
# 高细节随机图像
detail_frame = np.random.randint(0, 256, (100, 100), dtype=np.uint8)
score_detail = score_clarity([detail_frame])
# 纯色图应该显著低于高细节图
assert score_constant < score_detail
assert 0.0 <= score_constant <= 20.0
def test_edge_rich_image_high_clarity(self):
"""高频边缘图像有较高清晰度得分."""
# 棋盘格图案,边缘丰富
frame = np.zeros((100, 100), dtype=np.uint8)
for i in range(0, 100, 10):
for j in range(0, 100, 10):
if (i // 10 + j // 10) % 2 == 0:
frame[i : i + 10, j : j + 10] = 255
score = score_clarity([frame])
assert score > 1.0 # 应有一定清晰度
assert 0.0 <= score <= 20.0
def test_rgb_frame_converts_to_gray(self):
"""RGB 帧会被转灰度后计算."""
rgb_frame = np.random.randint(0, 256, (50, 50, 3), dtype=np.uint8)
score_rgb = score_clarity([rgb_frame])
# 对应灰度图
gray = np.dot(rgb_frame[..., :3], [0.299, 0.587, 0.114]).astype(np.uint8)
score_gray = score_clarity([gray])
# 两者应近似相等
assert abs(score_rgb - score_gray) < 0.01
def test_only_first_five_frames_analyzed(self):
"""只分析前 5 帧."""
# 10 帧:前 5 帧纯色,后 5 帧高对比度
frames = []
for _ in range(5):
frames.append(np.full((50, 50), 128, dtype=np.uint8))
for _ in range(5):
high_freq = np.random.randint(0, 256, (50, 50), dtype=np.uint8)
frames.append(high_freq)
score_10 = score_clarity(frames)
score_5 = score_clarity(frames[:5])
# 前 5 帧相同,得分应相同
assert abs(score_10 - score_5) < 0.01
def test_score_within_bounds(self):
"""得分始终在 0-20 范围内."""
for _ in range(10):
frame = np.random.randint(0, 256, (30, 30, 3), dtype=np.uint8)
score = score_clarity([frame])
assert 0.0 <= score <= 20.0
def test_multiple_frames_averaged(self):
"""多帧取平均方差."""
# 第 1 帧低细节,第 2 帧高细节
low_detail = np.full((50, 50), 100, dtype=np.uint8)
high_detail = np.random.randint(0, 256, (50, 50), dtype=np.uint8)
score_low = score_clarity([low_detail])
score_high = score_clarity([high_detail])
score_both = score_clarity([low_detail, high_detail])
# 混合得分应在两者之间
assert score_low <= score_both <= score_high
# ── 稳定性评分 ────────────────────────────────────────────────────────
class TestScoreStability:
"""稳定性评分测试."""
def test_single_frame_default_score(self):
"""不足 2 帧给默认 10 分."""
assert score_stability([]) == 10.0
assert score_stability([np.zeros((10, 10, 3), dtype=np.uint8)]) == 10.0
def test_identical_frames_max_stability(self):
"""完全相同的帧 = 高稳定性."""
frame = np.random.randint(50, 200, (100, 100, 3), dtype=np.uint8)
score = score_stability([frame, frame.copy()])
assert score > 10.0 # 应该接近满分 15
def test_very_different_frames_low_stability(self):
"""位移方差大的多帧序列 = 低稳定性."""
# 构造 4 帧:帧间位移差异大(有的帧相似、有的帧完全不同)
# 位移方差大 → 不稳定 → 低分
base = np.random.randint(100, 150, (100, 100, 3), dtype=np.uint8)
frames = [
base, # 帧0
base, # 帧1 (完全相同 → 位移0)
np.full_like(base, 255), # 帧2 (纯白 → 位移大)
base, # 帧3 (回到基准 → 位移又大)
]
score = score_stability(frames)
# 位移差异大 → 方差大 → 稳定性低
assert score < 10.0
assert 0.0 <= score <= 15.0
def test_score_within_bounds(self):
"""得分始终在 0-15 范围内."""
for _ in range(10):
f1 = np.random.randint(0, 256, (40, 40, 3), dtype=np.uint8)
f2 = np.random.randint(0, 256, (40, 40, 3), dtype=np.uint8)
score = score_stability([f1, f2])
assert 0.0 <= score <= 15.0
def test_gray_frames_also_work(self):
"""灰度帧也能计算."""
f1 = np.random.randint(0, 256, (50, 50), dtype=np.uint8)
f2 = np.random.randint(0, 256, (50, 50), dtype=np.uint8)
score = score_stability([f1, f2])
assert 0.0 <= score <= 15.0
def test_multiple_frame_pairs(self):
"""多对帧取方差."""
base = np.random.randint(100, 150, (60, 60, 3), dtype=np.uint8)
# 5 帧相似的
frames = []
for i in range(5):
f = base.copy()
# 轻微变化
f = np.clip(f.astype(int) + np.random.randint(-5, 6, f.shape), 0, 255).astype(np.uint8)
frames.append(f)
score = score_stability(frames)
assert score > 5.0 # 相似帧应该有一定稳定性
# ── 质量总评分 ────────────────────────────────────────────────────────
class TestCalculateQualityScore:
"""质量综合评分测试."""
def test_perfect_video_near_100(self):
"""完美参数的视频接近 100 分."""
info = VideoInfo(
width=3840,
height=2160,
fps=60,
bitrate=20_000_000,
)
# 用高细节帧提升清晰度分
frame = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
result = calculate_quality_score(info, [frame])
assert isinstance(result, QualityScore)
assert result.resolution_score == 25.0
assert result.fps_score == 20.0
assert result.bitrate_score == 20.0
assert 50.0 <= result.total <= 100.0
def test_low_quality_video(self):
"""低质量视频得分低."""
info = VideoInfo(
width=320,
height=240,
fps=10,
bitrate=100_000,
)
result = calculate_quality_score(info, [])
assert isinstance(result, QualityScore)
assert result.resolution_score == 3.0
assert result.fps_score == 5.0
assert result.bitrate_score == 3.0
# 无帧时清晰度和稳定性各给 10 分默认
assert result.clarity_score == 10.0
assert result.stability_score == 10.0
assert result.total == 31.0 # 3+5+3+10+10
def test_no_frames_uses_defaults(self):
"""不传 frames 时清晰度/稳定性给默认分."""
info = VideoInfo(width=1920, height=1080, fps=30, bitrate=5_000_000)
result = calculate_quality_score(info)
assert result.clarity_score == 10.0
assert result.stability_score == 10.0
assert result.resolution_score == 20.0
assert result.fps_score == 15.0
assert result.bitrate_score == 15.0
assert result.total == 70.0
def test_total_capped_at_100(self):
"""总分不超过 100."""
info = VideoInfo(
width=7680,
height=4320,
fps=240,
bitrate=100_000_000,
)
# 即使所有维度都满,总分不超 100
result = calculate_quality_score(info, [])
assert result.total <= 100.0
def test_total_minimum_zero(self):
"""总分不低于 0."""
info = VideoInfo(width=0, height=0, fps=0, bitrate=0)
result = calculate_quality_score(info, [])
assert result.total >= 0.0
def test_total_is_rounded(self):
"""总分保留 1 位小数."""
info = VideoInfo(width=1920, height=1080, fps=30, bitrate=5_000_000)
result = calculate_quality_score(info, [])
# 检查是 1 位小数
assert round(result.total, 1) == result.total
# ── 分类评分 ──────────────────────────────────────────────────────────
class TestCalculateCategoryScores:
"""分类评分计算测试."""
def test_scenic_high_green_and_motion(self):
"""绿色+适度运动+自然音 → 风景高分."""
color = ColorAnalysis(
green_ratio=0.5,
avg_saturation=0.5,
avg_brightness=0.6,
warm_ratio=0.2,
cool_ratio=0.3,
)
motion = MotionAnalysis(motion_score=0.3, scene_changes=1)
audio = AudioAnalysis(has_audio=True, ambient_ratio=0.7)
scores = calculate_category_scores(color, motion, audio)
assert scores[AssetClassification.SCENIC.value] > 0.5
assert scores[AssetClassification.SCENIC.value] <= 1.0
def test_product_low_motion_cool_tone(self):
"""低运动+冷色调 → 产品高分."""
color = ColorAnalysis(
avg_brightness=0.5,
avg_saturation=0.3,
cool_ratio=0.5,
green_ratio=0.1,
warm_ratio=0.2,
)
motion = MotionAnalysis(motion_score=0.1, scene_changes=0)
audio = AudioAnalysis(has_audio=False)
scores = calculate_category_scores(color, motion, audio)
assert scores[AssetClassification.PRODUCT.value] > 0.5
def test_person_with_speech(self):
"""有人声+适度运动 → 人物高分."""
color = ColorAnalysis(avg_brightness=0.5)
motion = MotionAnalysis(motion_score=0.25, scene_changes=1)
audio = AudioAnalysis(has_audio=True, speech_ratio=0.6)
scores = calculate_category_scores(color, motion, audio)
assert scores[AssetClassification.PERSON.value] > 0.5
def test_animal_high_motion(self):
"""高运动+多场景切换 → 动物高分."""
color = ColorAnalysis()
motion = MotionAnalysis(motion_score=0.6, scene_changes=5)
audio = AudioAnalysis(has_audio=True, ambient_ratio=0.5)
scores = calculate_category_scores(color, motion, audio)
assert scores[AssetClassification.ANIMAL.value] > 0.5
def test_food_warm_saturated(self):
"""暖色调+高饱和 → 美食高分."""
color = ColorAnalysis(
warm_ratio=0.6,
avg_saturation=0.7,
avg_brightness=0.6,
green_ratio=0.1,
cool_ratio=0.2,
)
motion = MotionAnalysis(motion_score=0.1, scene_changes=0)
audio = AudioAnalysis(has_audio=False)
scores = calculate_category_scores(color, motion, audio)
assert scores[AssetClassification.FOOD.value] > 0.5
def test_tech_cool_low_saturation(self):
"""冷色调+低饱和+低运动 → 科技高分."""
color = ColorAnalysis(
cool_ratio=0.6,
avg_saturation=0.3,
avg_brightness=0.5,
green_ratio=0.1,
warm_ratio=0.2,
)
motion = MotionAnalysis(motion_score=0.1, scene_changes=0)
audio = AudioAnalysis(has_audio=False)
scores = calculate_category_scores(color, motion, audio)
assert scores[AssetClassification.TECH.value] > 0.5
def test_sport_high_motion(self):
"""高运动+多场景 → 运动高分."""
color = ColorAnalysis(avg_brightness=0.6)
motion = MotionAnalysis(motion_score=0.7, scene_changes=5)
audio = AudioAnalysis(has_audio=False)
scores = calculate_category_scores(color, motion, audio)
assert scores[AssetClassification.SPORT.value] > 0.5
def test_music_high_music_ratio(self):
"""高音乐比例 → 音乐高分."""
color = ColorAnalysis(avg_saturation=0.6, green_ratio=0.1)
motion = MotionAnalysis(motion_score=0.2)
audio = AudioAnalysis(has_audio=True, music_ratio=0.7)
scores = calculate_category_scores(color, motion, audio)
assert scores[AssetClassification.MUSIC.value] > 0.5
def test_other_has_base_score(self):
"""其他分类有基础分 0.1."""
color = ColorAnalysis()
motion = MotionAnalysis()
audio = AudioAnalysis()
scores = calculate_category_scores(color, motion, audio)
assert scores[AssetClassification.OTHER.value] == 0.1
def test_all_scores_within_bounds(self):
"""所有分类得分都在 0-1 范围内."""
color = ColorAnalysis(
green_ratio=0.9,
warm_ratio=0.9,
cool_ratio=0.9,
avg_saturation=0.99,
avg_brightness=0.99,
)
motion = MotionAnalysis(motion_score=0.9, scene_changes=100)
audio = AudioAnalysis(
has_audio=True,
speech_ratio=0.99,
music_ratio=0.99,
ambient_ratio=0.99,
)
scores = calculate_category_scores(color, motion, audio)
for cat, score in scores.items():
assert 0.0 <= score <= 1.0, f"{cat} score {score} out of bounds"
def test_all_nine_categories_present(self):
"""返回 9 个分类的得分."""
color = ColorAnalysis()
motion = MotionAnalysis()
audio = AudioAnalysis()
scores = calculate_category_scores(color, motion, audio)
assert len(scores) == 9
# ── 分类结果计算 ──────────────────────────────────────────────────────
class TestClassifyFromAnalysis:
"""分类结果计算测试."""
def test_returns_classification_result(self):
"""返回 ClassificationResult 对象."""
color = ColorAnalysis()
motion = MotionAnalysis()
audio = AudioAnalysis()
result = classify_from_analysis(color, motion, audio)
assert isinstance(result, ClassificationResult)
assert isinstance(result.category, AssetClassification)
assert isinstance(result.confidence, float)
assert isinstance(result.scores, dict)
def test_highest_score_wins(self):
"""得分最高的分类胜出."""
# 构造明显偏向风景的特征
color = ColorAnalysis(
green_ratio=0.8,
avg_saturation=0.6,
avg_brightness=0.7,
)
motion = MotionAnalysis(motion_score=0.3)
audio = AudioAnalysis(has_audio=True, ambient_ratio=0.8)
result = classify_from_analysis(color, motion, audio)
assert result.category == AssetClassification.SCENIC
def test_confidence_within_bounds(self):
"""置信度在 0.3-0.95 范围内."""
color = ColorAnalysis()
motion = MotionAnalysis()
audio = AudioAnalysis()
result = classify_from_analysis(color, motion, audio)
assert 0.3 <= result.confidence <= 0.95
def test_confidence_capped_at_095(self):
"""极高得分也被限制在 0.95."""
color = ColorAnalysis(
green_ratio=0.9,
avg_saturation=0.9,
avg_brightness=0.9,
warm_ratio=0.9,
)
motion = MotionAnalysis(motion_score=0.9, scene_changes=10)
audio = AudioAnalysis(
has_audio=True,
speech_ratio=0.9,
music_ratio=0.9,
ambient_ratio=0.9,
)
result = classify_from_analysis(color, motion, audio)
assert result.confidence <= 0.95
def test_confidence_floored_at_03(self):
"""极低得分也有 0.3 最低置信度."""
color = ColorAnalysis()
motion = MotionAnalysis()
audio = AudioAnalysis()
result = classify_from_analysis(color, motion, audio)
assert result.confidence >= 0.3
def test_scores_dict_included(self):
"""结果中包含完整分数字典."""
color = ColorAnalysis()
motion = MotionAnalysis()
audio = AudioAnalysis()
result = classify_from_analysis(color, motion, audio)
assert len(result.scores) == 9
assert AssetClassification.OTHER.value in result.scores
def test_food_category_wins_on_warm_colors(self):
"""暖色调+高饱和 → 美食分类胜出."""
color = ColorAnalysis(
warm_ratio=0.7,
avg_saturation=0.8,
avg_brightness=0.6,
green_ratio=0.05,
)
motion = MotionAnalysis(motion_score=0.05)
audio = AudioAnalysis(has_audio=False)
result = classify_from_analysis(color, motion, audio)
assert result.category == AssetClassification.FOOD
# ── 数据类默认值 ──────────────────────────────────────────────────────
class TestDataclassDefaults:
"""数据类默认值测试."""
def test_video_info_defaults(self):
info = VideoInfo()
assert info.width == 0
assert info.height == 0
assert info.fps == 0.0
assert info.bitrate == 0
assert info.has_audio is False
def test_color_analysis_defaults(self):
color = ColorAnalysis()
assert color.green_ratio == 0.0
assert color.avg_brightness == 0.0
assert color.dominant_hue == 0.0
def test_motion_analysis_defaults(self):
motion = MotionAnalysis()
assert motion.motion_score == 0.0
assert motion.scene_changes == 0
def test_audio_analysis_defaults(self):
audio = AudioAnalysis()
assert audio.has_audio is False
assert audio.speech_ratio == 0.0
assert audio.music_ratio == 0.0
def test_quality_score_requires_total(self):
with pytest.raises(TypeError):
QualityScore()
qs = QualityScore(total=50.0)
assert qs.total == 50.0
assert qs.resolution_score == 0.0