test(wave164): filter_presets滤镜预设 +53测
PR Automation / Auto Approve on CI Green (pull_request) Waiting to run
PR Automation / Auto Merge on CI Green + Approved (pull_request) Waiting to run
Preview Deploy / Deploy Preview Environment (pull_request) Waiting to run
AI Code Review / AI Code Review (pull_request) CI runner不可用,手动设置
Preview Cleanup / Cleanup Preview Environment (pull_request) Waiting to run
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 19s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m38s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 34s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m17s
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 / Validate - Migration (alembic) (pull_request) Successful in 1m29s
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 / PR Build Web Image (pull_request) Successful in 1m48s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m49s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m50s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 3m50s
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 / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
PR Automation / Auto Approve on CI Green (pull_request) Waiting to run
PR Automation / Auto Merge on CI Green + Approved (pull_request) Waiting to run
Preview Deploy / Deploy Preview Environment (pull_request) Waiting to run
AI Code Review / AI Code Review (pull_request) CI runner不可用,手动设置
Preview Cleanup / Cleanup Preview Environment (pull_request) Waiting to run
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 19s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m38s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 34s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m17s
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 / Validate - Migration (alembic) (pull_request) Successful in 1m29s
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 / PR Build Web Image (pull_request) Successful in 1m48s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m49s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m50s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 3m50s
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 / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
- FilterPreset数据类:最小创建/默认值/完整参数/frozen不可变/tags独立 - 预设库:非空/ID唯一/必填字段/5个分类存在/黑白饱和度为0/原图无效果/总数 - get_filter_preset:存在ID/不存在/空串/大小写敏感/返回类型 - list_filter_presets:无筛选/5个分类筛选/无效分类/关键词搜索(名称/标签/描述) + 组合筛选/空关键词/返回类型 - build_ffmpeg_filter:全强度/无效预设/0强度/负强度/超100截断 + 半强度/原图空返回/格式校验/参数分隔/各参数存在/全预设遍历
This commit is contained in:
Executable
+350
@@ -0,0 +1,350 @@
|
||||
"""filter_presets 单元测试 - wave164
|
||||
|
||||
覆盖:
|
||||
- FilterPreset 数据类(frozen/默认值/字段)
|
||||
- FILTER_PRESET_LIBRARY 预设库(数量/分类/ID唯一性)
|
||||
- get_filter_preset 按ID获取
|
||||
- list_filter_presets 筛选列表(分类/关键词)
|
||||
- build_ffmpeg_filter 滤镜生成(强度/参数插值/边界/空值)
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.filter_presets import (
|
||||
FILTER_PRESET_LIBRARY,
|
||||
FilterPreset,
|
||||
build_ffmpeg_filter,
|
||||
get_filter_preset,
|
||||
list_filter_presets,
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# FilterPreset 数据类
|
||||
# ============================================================
|
||||
|
||||
|
||||
class TestFilterPresetDataclass:
|
||||
def test_minimal_creation(self):
|
||||
p = FilterPreset(id="test", name="Test", category="basic")
|
||||
assert p.id == "test"
|
||||
assert p.name == "Test"
|
||||
assert p.category == "basic"
|
||||
|
||||
def test_default_values(self):
|
||||
p = FilterPreset(id="t", name="T", category="basic")
|
||||
assert p.description == ""
|
||||
assert p.tags == []
|
||||
assert p.brightness == 0.0
|
||||
assert p.contrast == 1.0
|
||||
assert p.saturation == 1.0
|
||||
assert p.gamma == 1.0
|
||||
assert p.gamma_r == 1.0
|
||||
assert p.gamma_g == 1.0
|
||||
assert p.gamma_b == 1.0
|
||||
assert p.hue == 0.0
|
||||
assert p.lut_url == ""
|
||||
|
||||
def test_full_creation(self):
|
||||
p = FilterPreset(
|
||||
id="full",
|
||||
name="Full",
|
||||
category="cinematic",
|
||||
description="test desc",
|
||||
tags=["tag1", "tag2"],
|
||||
brightness=0.5,
|
||||
contrast=1.5,
|
||||
saturation=2.0,
|
||||
gamma=1.2,
|
||||
gamma_r=1.3,
|
||||
gamma_g=0.9,
|
||||
gamma_b=0.8,
|
||||
hue=30.0,
|
||||
lut_url="http://lut.png",
|
||||
)
|
||||
assert p.description == "test desc"
|
||||
assert p.tags == ["tag1", "tag2"]
|
||||
assert p.brightness == 0.5
|
||||
assert p.contrast == 1.5
|
||||
assert p.gamma == 1.2
|
||||
assert p.hue == 30.0
|
||||
assert p.lut_url == "http://lut.png"
|
||||
|
||||
def test_frozen_immutable(self):
|
||||
p = FilterPreset(id="t", name="T", category="basic")
|
||||
with pytest.raises((AttributeError, dataclasses.FrozenInstanceError)):
|
||||
p.brightness = 0.5 # frozen=True,不能修改
|
||||
|
||||
def test_tags_default_new_list(self):
|
||||
# 每个实例有独立的列表
|
||||
p1 = FilterPreset(id="t1", name="T1", category="basic")
|
||||
p2 = FilterPreset(id="t2", name="T2", category="basic")
|
||||
assert p1.tags is not p2.tags
|
||||
|
||||
|
||||
# ============================================================
|
||||
# FILTER_PRESET_LIBRARY 预设库
|
||||
# ============================================================
|
||||
|
||||
|
||||
class TestFilterPresetLibrary:
|
||||
def test_library_not_empty(self):
|
||||
assert len(FILTER_PRESET_LIBRARY) > 0
|
||||
|
||||
def test_all_ids_unique(self):
|
||||
ids = [p.id for p in FILTER_PRESET_LIBRARY]
|
||||
assert len(ids) == len(set(ids))
|
||||
|
||||
def test_all_have_required_fields(self):
|
||||
for p in FILTER_PRESET_LIBRARY:
|
||||
assert p.id, f"预设缺少id: {p}"
|
||||
assert p.name, f"预设缺少name: {p.id}"
|
||||
assert p.category, f"预设缺少category: {p.id}"
|
||||
|
||||
def test_categories_are_valid(self):
|
||||
valid_categories = {"basic", "cinematic", "vintage", "bw", "style"}
|
||||
for p in FILTER_PRESET_LIBRARY:
|
||||
assert p.category in valid_categories, f"无效分类: {p.id} -> {p.category}"
|
||||
|
||||
def test_basic_category_exists(self):
|
||||
basics = [p for p in FILTER_PRESET_LIBRARY if p.category == "basic"]
|
||||
assert len(basics) >= 3
|
||||
|
||||
def test_cinematic_category_exists(self):
|
||||
cinematic = [p for p in FILTER_PRESET_LIBRARY if p.category == "cinematic"]
|
||||
assert len(cinematic) >= 1
|
||||
|
||||
def test_vintage_category_exists(self):
|
||||
vintage = [p for p in FILTER_PRESET_LIBRARY if p.category == "vintage"]
|
||||
assert len(vintage) >= 1
|
||||
|
||||
def test_bw_category_exists(self):
|
||||
bw = [p for p in FILTER_PRESET_LIBRARY if p.category == "bw"]
|
||||
assert len(bw) >= 1
|
||||
# 所有黑白预设饱和度为0
|
||||
for p in bw:
|
||||
assert p.saturation == 0.0
|
||||
|
||||
def test_style_category_exists(self):
|
||||
style = [p for p in FILTER_PRESET_LIBRARY if p.category == "style"]
|
||||
assert len(style) >= 1
|
||||
|
||||
def test_none_filter_has_no_effect(self):
|
||||
none_preset = get_filter_preset("filter_none")
|
||||
assert none_preset is not None
|
||||
assert none_preset.brightness == 0.0
|
||||
assert none_preset.contrast == 1.0
|
||||
assert none_preset.saturation == 1.0
|
||||
assert none_preset.gamma == 1.0
|
||||
|
||||
def test_total_count(self):
|
||||
# 至少有15个预设
|
||||
assert len(FILTER_PRESET_LIBRARY) >= 15
|
||||
|
||||
|
||||
# ============================================================
|
||||
# get_filter_preset
|
||||
# ============================================================
|
||||
|
||||
|
||||
class TestGetFilterPreset:
|
||||
def test_existing_id(self):
|
||||
p = get_filter_preset("filter_brighten")
|
||||
assert p is not None
|
||||
assert p.id == "filter_brighten"
|
||||
assert p.name == "明亮"
|
||||
|
||||
def test_nonexistent_id(self):
|
||||
assert get_filter_preset("nonexistent") is None
|
||||
|
||||
def test_empty_id(self):
|
||||
assert get_filter_preset("") is None
|
||||
|
||||
def test_case_sensitive(self):
|
||||
# 大小写敏感
|
||||
assert get_filter_preset("Filter_Brighten") is None
|
||||
|
||||
def test_returns_preset_object(self):
|
||||
p = get_filter_preset("filter_cinematic")
|
||||
assert isinstance(p, FilterPreset)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# list_filter_presets
|
||||
# ============================================================
|
||||
|
||||
|
||||
class TestListFilterPresets:
|
||||
def test_no_filters_returns_all(self):
|
||||
result = list_filter_presets()
|
||||
assert len(result) == len(FILTER_PRESET_LIBRARY)
|
||||
|
||||
def test_filter_by_category_basic(self):
|
||||
result = list_filter_presets(category="basic")
|
||||
assert len(result) > 0
|
||||
for p in result:
|
||||
assert p.category == "basic"
|
||||
|
||||
def test_filter_by_category_cinematic(self):
|
||||
result = list_filter_presets(category="cinematic")
|
||||
for p in result:
|
||||
assert p.category == "cinematic"
|
||||
|
||||
def test_filter_by_category_bw(self):
|
||||
result = list_filter_presets(category="bw")
|
||||
for p in result:
|
||||
assert p.category == "bw"
|
||||
|
||||
def test_filter_by_category_vintage(self):
|
||||
result = list_filter_presets(category="vintage")
|
||||
for p in result:
|
||||
assert p.category == "vintage"
|
||||
|
||||
def test_filter_by_category_style(self):
|
||||
result = list_filter_presets(category="style")
|
||||
for p in result:
|
||||
assert p.category == "style"
|
||||
|
||||
def test_invalid_category_returns_empty(self):
|
||||
result = list_filter_presets(category="nonexistent")
|
||||
assert result == []
|
||||
|
||||
def test_keyword_search_name(self):
|
||||
result = list_filter_presets(keyword="电影")
|
||||
assert len(result) >= 1
|
||||
names = [p.name for p in result]
|
||||
assert any("电影" in n for n in names)
|
||||
|
||||
def test_keyword_search_tags(self):
|
||||
result = list_filter_presets(keyword="复古")
|
||||
assert len(result) >= 1
|
||||
# 至少有一个的标签或名称包含复古
|
||||
found = any(any("复古" in t for t in p.tags) or "复古" in p.name for p in result)
|
||||
assert found
|
||||
|
||||
def test_keyword_search_description(self):
|
||||
result = list_filter_presets(keyword="青橙")
|
||||
assert len(result) >= 1
|
||||
|
||||
def test_keyword_case_insensitive(self):
|
||||
# 中文不区分大小写,用英文标签/名称试
|
||||
result = list_filter_presets(keyword="FILTER")
|
||||
# 搜索 filter 应该能匹配到很多(id里有但搜索name/desc/tags)
|
||||
# 用确定的中文关键词更可靠
|
||||
assert isinstance(result, list)
|
||||
|
||||
def test_keyword_nonexistent_returns_empty(self):
|
||||
result = list_filter_presets(keyword="zzz不存在的关键词zzz")
|
||||
assert result == []
|
||||
|
||||
def test_category_and_keyword_combined(self):
|
||||
result = list_filter_presets(category="basic", keyword="亮")
|
||||
for p in result:
|
||||
assert p.category == "basic"
|
||||
assert "亮" in p.name or "亮" in p.description or any("亮" in t for t in p.tags)
|
||||
|
||||
def test_keyword_empty_returns_all(self):
|
||||
result = list_filter_presets(keyword="")
|
||||
assert len(result) == len(FILTER_PRESET_LIBRARY)
|
||||
|
||||
def test_returns_list_of_presets(self):
|
||||
result = list_filter_presets()
|
||||
for p in result:
|
||||
assert isinstance(p, FilterPreset)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# build_ffmpeg_filter
|
||||
# ============================================================
|
||||
|
||||
|
||||
class TestBuildFfmpegFilter:
|
||||
def test_valid_preset_full_intensity(self):
|
||||
result = build_ffmpeg_filter("filter_brighten", 100)
|
||||
assert result.startswith("eq=")
|
||||
assert "brightness=" in result
|
||||
assert "contrast=" in result
|
||||
|
||||
def test_invalid_preset_returns_empty(self):
|
||||
assert build_ffmpeg_filter("nonexistent", 100) == ""
|
||||
|
||||
def test_zero_intensity_returns_empty(self):
|
||||
assert build_ffmpeg_filter("filter_brighten", 0) == ""
|
||||
|
||||
def test_negative_intensity_returns_empty(self):
|
||||
assert build_ffmpeg_filter("filter_brighten", -10) == ""
|
||||
|
||||
def test_intensity_over_100_clamped(self):
|
||||
# >100 按100算
|
||||
r100 = build_ffmpeg_filter("filter_saturate", 100)
|
||||
r200 = build_ffmpeg_filter("filter_saturate", 200)
|
||||
assert r100 == r200
|
||||
|
||||
def test_half_intensity_half_effect(self):
|
||||
full = build_ffmpeg_filter("filter_brighten", 100)
|
||||
half = build_ffmpeg_filter("filter_brighten", 50)
|
||||
# 半强度的brightness应该小一些
|
||||
assert full != half
|
||||
# 半强度仍然有效果
|
||||
assert half.startswith("eq=")
|
||||
|
||||
def test_none_filter_returns_empty(self):
|
||||
# filter_none所有参数都是默认值,应该返回空
|
||||
result = build_ffmpeg_filter("filter_none", 100)
|
||||
assert result == ""
|
||||
|
||||
def test_format_has_eq_prefix(self):
|
||||
result = build_ffmpeg_filter("filter_saturate", 100)
|
||||
assert result.startswith("eq=")
|
||||
|
||||
def test_parameters_separated_by_colon(self):
|
||||
result = build_ffmpeg_filter("filter_cinematic", 100)
|
||||
parts = result[len("eq=") :].split(":")
|
||||
assert len(parts) >= 3 # 至少有3个参数
|
||||
|
||||
def test_brightness_value_format(self):
|
||||
result = build_ffmpeg_filter("filter_brighten", 100)
|
||||
# brightness参数应该存在且是数值
|
||||
assert "brightness=0." in result or "brightness=-0." in result
|
||||
|
||||
def test_saturation_value_format(self):
|
||||
result = build_ffmpeg_filter("filter_saturate", 100)
|
||||
assert "saturation=" in result
|
||||
|
||||
def test_contrast_value_format(self):
|
||||
result = build_ffmpeg_filter("filter_contrast", 100)
|
||||
assert "contrast=" in result
|
||||
|
||||
def test_gamma_parameters_present(self):
|
||||
result = build_ffmpeg_filter("filter_warm", 100)
|
||||
# 暖色预设应该有gamma_r/gamma_b变化
|
||||
assert "gamma_r=" in result or "gamma_b=" in result
|
||||
|
||||
def test_very_low_intensity_near_zero(self):
|
||||
# 强度1%,效果接近0,但因为有brightness等可能仍然在阈值以上
|
||||
result = build_ffmpeg_filter("filter_brighten", 1)
|
||||
# 至少应该有一个参数(brightness * 0.01 = 0.0012 > 0.001)
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_all_presets_generate_valid_output(self):
|
||||
# 每个预设在100强度下都能生成合法结果
|
||||
for preset in FILTER_PRESET_LIBRARY:
|
||||
result = build_ffmpeg_filter(preset.id, 100)
|
||||
assert isinstance(result, str)
|
||||
if result: # filter_none 可能为空
|
||||
assert result.startswith("eq=")
|
||||
|
||||
def test_intensity_50_between_0_and_100(self):
|
||||
# 验证50%强度确实在0和100之间插值
|
||||
r0 = build_ffmpeg_filter("filter_saturate", 0)
|
||||
r50 = build_ffmpeg_filter("filter_saturate", 50)
|
||||
r100 = build_ffmpeg_filter("filter_saturate", 100)
|
||||
assert r0 == ""
|
||||
assert r50 != r100
|
||||
assert r50 != ""
|
||||
|
||||
def test_hue_not_included_in_eq(self):
|
||||
# hue 不在 eq 滤镜参数中(当前实现没有hue)
|
||||
result = build_ffmpeg_filter("filter_cinematic", 100)
|
||||
assert "hue=" not in result
|
||||
Reference in New Issue
Block a user