Compare commits

...

1 Commits

Author SHA1 Message Date
CI Bot abb081f07b test(wave114): 抽离color_grade_config领域模型 + 56单测
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 27s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m6s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 53s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m14s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m30s
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 37s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 52s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 55s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 51s
AI Code Review / AI Code Review (pull_request) Successful in 2m4s
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 / PR Build API Image (pull_request) Successful in 3m57s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m51s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 3m21s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m32s
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 skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 26s
- 抽离ColorGradeConfig数据类、8个预设、PARAM_RANGES/DEFAULT_PARAMS到packages/domain/color_grade_config.py
- 新增get_preset_names/get_preset_params/clamp_param工具函数
- color_grade_engine.py: 416→211行 (-49%),保留向后兼容导入
- 56个纯逻辑单测全绿,原有31个测试无回归
- 合计87 passed
2026-07-27 07:14:10 +08:00
3 changed files with 640 additions and 221 deletions
@@ -10,233 +10,28 @@
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any
logger = logging.getLogger(__name__)
# ── 预设滤镜包 ────────────────────────────────────────────────────────────────
# 预设名称常量
PRESET_FRESH = "fresh" # 清新
PRESET_JAPANESE = "japanese" # 日系
PRESET_VINTAGE = "vintage" # 复古
PRESET_CINEMA = "cinema" # 电影
PRESET_FILM = "film" # 胶片
PRESET_BW = "black_white" # 黑白
PRESET_WARM = "warm" # 暖色
PRESET_COOL = "cool" # 冷色
VALID_PRESETS = {
from packages.domain.color_grade_config import ( # noqa: F401 — 向后兼容
DEFAULT_PARAMS,
PARAM_RANGES,
PRESET_BW,
PRESET_CINEMA,
PRESET_COOL,
PRESET_DISPLAY_NAMES,
PRESET_FILM,
PRESET_FRESH,
PRESET_JAPANESE,
PRESET_PARAMS,
PRESET_VINTAGE,
PRESET_CINEMA,
PRESET_FILM,
PRESET_BW,
PRESET_WARM,
PRESET_COOL,
}
VALID_PRESETS,
ColorGradeConfig,
clamp_param,
get_preset_names,
get_preset_params,
)
# 预设名称 → 中文显示名
PRESET_DISPLAY_NAMES = {
PRESET_FRESH: "清新",
PRESET_JAPANESE: "日系",
PRESET_VINTAGE: "复古",
PRESET_CINEMA: "电影",
PRESET_FILM: "胶片",
PRESET_BW: "黑白",
PRESET_WARM: "暖色",
PRESET_COOL: "冷色",
}
# 预设参数配置
# 每个预设包含:brightness, contrast, saturation, temperature, hue
# 取值范围:brightness/contrast/temperature -100~100, saturation 0~200, hue -180~180
PRESET_PARAMS: dict[str, dict[str, float]] = {
PRESET_FRESH: {
# 清新:提亮、高饱和、偏冷、微微调
"brightness": 8,
"contrast": 10,
"saturation": 120,
"temperature": -8,
"hue": 5,
},
PRESET_JAPANESE: {
# 日系:低对比、低饱和、偏暖、偏黄绿
"brightness": 12,
"contrast": -15,
"saturation": 70,
"temperature": 10,
"hue": -5,
},
PRESET_VINTAGE: {
# 复古:低饱和、偏黄、对比度适中、偏暖
"brightness": -5,
"contrast": 5,
"saturation": 60,
"temperature": 25,
"hue": -8,
},
PRESET_CINEMA: {
# 电影:高对比、低饱和、偏冷蓝、暗角感
"brightness": -8,
"contrast": 20,
"saturation": 75,
"temperature": -15,
"hue": -3,
},
PRESET_FILM: {
# 胶片:中对比、饱和适中、偏暖、颗粒感(这里只用调色模拟)
"brightness": -3,
"contrast": 12,
"saturation": 95,
"temperature": 15,
"hue": -2,
},
PRESET_BW: {
# 黑白:饱和度为0,对比度略高
"brightness": 0,
"contrast": 15,
"saturation": 0,
"temperature": 0,
"hue": 0,
},
PRESET_WARM: {
# 暖色:高色温、偏红黄
"brightness": 5,
"contrast": 8,
"saturation": 110,
"temperature": 30,
"hue": -5,
},
PRESET_COOL: {
# 冷色:低色温、偏蓝青
"brightness": 3,
"contrast": 8,
"saturation": 105,
"temperature": -25,
"hue": 8,
},
}
# ── 参数范围 ──────────────────────────────────────────────────────────────────
PARAM_RANGES = {
"brightness": (-100.0, 100.0),
"contrast": (-100.0, 100.0),
"saturation": (0.0, 200.0),
"temperature": (-100.0, 100.0),
"hue": (-180.0, 180.0),
}
# 默认值(零调整)
DEFAULT_PARAMS = {
"brightness": 0.0,
"contrast": 0.0,
"saturation": 100.0,
"temperature": 0.0,
"hue": 0.0,
}
# ── 数据模型 ──────────────────────────────────────────────────────────────────
@dataclass
class ColorGradeConfig:
"""色彩调色配置.
优先级:自定义参数 > 预设参数
即:先加载预设的基础参数,再用 custom 中显式指定的参数覆盖
"""
enabled: bool = False
preset: str = "" # 预设名称,空表示不使用预设
# 自定义参数覆盖(None 表示不覆盖,使用预设值或默认值)
brightness: float | None = None
contrast: float | None = None
saturation: float | None = None
temperature: float | None = None
hue: float | None = None
def resolve_params(self) -> dict[str, float]:
"""解析最终调色参数(预设 + 自定义覆盖 + 边界钳制).
Returns:
包含 brightness, contrast, saturation, temperature, hue 的参数字典
"""
# 1. 从默认值开始
params = dict(DEFAULT_PARAMS)
# 2. 应用预设
if self.preset and self.preset in PRESET_PARAMS:
params.update(PRESET_PARAMS[self.preset])
# 3. 应用自定义覆盖
if self.brightness is not None:
params["brightness"] = self.brightness
if self.contrast is not None:
params["contrast"] = self.contrast
if self.saturation is not None:
params["saturation"] = self.saturation
if self.temperature is not None:
params["temperature"] = self.temperature
if self.hue is not None:
params["hue"] = self.hue
# 4. 边界钳制
for key, (min_val, max_val) in PARAM_RANGES.items():
params[key] = max(min_val, min(max_val, params[key]))
return params
def has_effect(self) -> bool:
"""判断是否有实际调色效果(所有参数都是默认值则无效果).
用于优化:无效果时跳过滤镜,不浪费性能。
"""
params = self.resolve_params()
for key, default in DEFAULT_PARAMS.items():
if abs(params[key] - default) > 0.001:
return True
return False
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "ColorGradeConfig":
"""从字典解析配置."""
if not data or not data.get("enabled", False):
return cls(enabled=False)
preset = data.get("preset", "")
if preset and preset not in VALID_PRESETS:
logger.warning("未知的调色预设: %s,忽略预设", preset)
preset = ""
def _get_float(key: str) -> float | None:
val = data.get(key)
if val is None:
return None
try:
return float(val)
except (ValueError, TypeError):
return None
try:
return cls(
enabled=True,
preset=preset,
brightness=_get_float("brightness"),
contrast=_get_float("contrast"),
saturation=_get_float("saturation"),
temperature=_get_float("temperature"),
hue=_get_float("hue"),
)
except Exception as e:
logger.warning("调色配置解析失败: %s,使用默认配置", e)
return cls(enabled=False)
logger = logging.getLogger(__name__)
# ── 调色引擎 ──────────────────────────────────────────────────────────────────
+268
View File
@@ -0,0 +1,268 @@
"""色彩调色配置领域模型 — 纯逻辑,无 FFmpeg 依赖.
抽离自 color_grade_engine.py 的数据类、预设常量和纯逻辑函数,
方便单测覆盖,同时保持向后兼容。
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any
logger = logging.getLogger(__name__)
# ── 预设常量 ──────────────────────────────────────────────────────────────────
PRESET_FRESH = "fresh" # 清新
PRESET_JAPANESE = "japanese" # 日系
PRESET_VINTAGE = "vintage" # 复古
PRESET_CINEMA = "cinema" # 电影
PRESET_FILM = "film" # 胶片
PRESET_BW = "black_white" # 黑白
PRESET_WARM = "warm" # 暖色
PRESET_COOL = "cool" # 冷色
VALID_PRESETS = {
PRESET_FRESH,
PRESET_JAPANESE,
PRESET_VINTAGE,
PRESET_CINEMA,
PRESET_FILM,
PRESET_BW,
PRESET_WARM,
PRESET_COOL,
}
# 预设名称 → 中文显示名
PRESET_DISPLAY_NAMES = {
PRESET_FRESH: "清新",
PRESET_JAPANESE: "日系",
PRESET_VINTAGE: "复古",
PRESET_CINEMA: "电影",
PRESET_FILM: "胶片",
PRESET_BW: "黑白",
PRESET_WARM: "暖色",
PRESET_COOL: "冷色",
}
# 预设参数配置
# 每个预设包含:brightness, contrast, saturation, temperature, hue
PRESET_PARAMS: dict[str, dict[str, float]] = {
PRESET_FRESH: {
"brightness": 8,
"contrast": 10,
"saturation": 120,
"temperature": -8,
"hue": 5,
},
PRESET_JAPANESE: {
"brightness": 12,
"contrast": -15,
"saturation": 70,
"temperature": 10,
"hue": -5,
},
PRESET_VINTAGE: {
"brightness": -5,
"contrast": 5,
"saturation": 60,
"temperature": 25,
"hue": -8,
},
PRESET_CINEMA: {
"brightness": -8,
"contrast": 20,
"saturation": 75,
"temperature": -15,
"hue": -3,
},
PRESET_FILM: {
"brightness": -3,
"contrast": 12,
"saturation": 95,
"temperature": 15,
"hue": -2,
},
PRESET_BW: {
"brightness": 0,
"contrast": 15,
"saturation": 0,
"temperature": 0,
"hue": 0,
},
PRESET_WARM: {
"brightness": 5,
"contrast": 8,
"saturation": 110,
"temperature": 30,
"hue": -5,
},
PRESET_COOL: {
"brightness": 3,
"contrast": 8,
"saturation": 105,
"temperature": -25,
"hue": 8,
},
}
# ── 参数范围 ──────────────────────────────────────────────────────────────────
PARAM_RANGES: dict[str, tuple[float, float]] = {
"brightness": (-100.0, 100.0),
"contrast": (-100.0, 100.0),
"saturation": (0.0, 200.0),
"temperature": (-100.0, 100.0),
"hue": (-180.0, 180.0),
}
# 默认值(零调整)
DEFAULT_PARAMS: dict[str, float] = {
"brightness": 0.0,
"contrast": 0.0,
"saturation": 100.0,
"temperature": 0.0,
"hue": 0.0,
}
ALL_PARAM_KEYS = ("brightness", "contrast", "saturation", "temperature", "hue")
# ── 数据模型 ──────────────────────────────────────────────────────────────────
@dataclass
class ColorGradeConfig:
"""色彩调色配置.
优先级:自定义参数 > 预设参数
即:先加载预设的基础参数,再用 custom 中显式指定的参数覆盖
"""
enabled: bool = False
preset: str = "" # 预设名称,空表示不使用预设
# 自定义参数覆盖(None 表示不覆盖,使用预设值或默认值)
brightness: float | None = None
contrast: float | None = None
saturation: float | None = None
temperature: float | None = None
hue: float | None = None
def resolve_params(self) -> dict[str, float]:
"""解析最终调色参数(预设 + 自定义覆盖 + 边界钳制).
Returns:
包含 brightness, contrast, saturation, temperature, hue 的参数字典
"""
# 1. 从默认值开始
params = dict(DEFAULT_PARAMS)
# 2. 应用预设
if self.preset and self.preset in PRESET_PARAMS:
params.update(PRESET_PARAMS[self.preset])
# 3. 应用自定义覆盖
if self.brightness is not None:
params["brightness"] = self.brightness
if self.contrast is not None:
params["contrast"] = self.contrast
if self.saturation is not None:
params["saturation"] = self.saturation
if self.temperature is not None:
params["temperature"] = self.temperature
if self.hue is not None:
params["hue"] = self.hue
# 4. 边界钳制
for key, (min_val, max_val) in PARAM_RANGES.items():
params[key] = max(min_val, min(max_val, params[key]))
return params
def has_effect(self) -> bool:
"""判断是否有实际调色效果(所有参数都是默认值则无效果).
用于优化:无效果时跳过滤镜,不浪费性能。
"""
params = self.resolve_params()
for key, default in DEFAULT_PARAMS.items():
if abs(params[key] - default) > 0.001:
return True
return False
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "ColorGradeConfig":
"""从字典解析配置."""
if not data or not data.get("enabled", False):
return cls(enabled=False)
preset = data.get("preset", "")
if preset and preset not in VALID_PRESETS:
logger.warning("未知的调色预设: %s,忽略预设", preset)
preset = ""
def _get_float(key: str) -> float | None:
val = data.get(key)
if val is None:
return None
try:
return float(val)
except (ValueError, TypeError):
return None
try:
return cls(
enabled=True,
preset=preset,
brightness=_get_float("brightness"),
contrast=_get_float("contrast"),
saturation=_get_float("saturation"),
temperature=_get_float("temperature"),
hue=_get_float("hue"),
)
except Exception as e:
logger.warning("调色配置解析失败: %s,使用默认配置", e)
return cls(enabled=False)
def validate(self) -> tuple[bool, str]:
"""校验配置合法性,返回 (是否合法, 错误信息)."""
if not self.enabled:
return True, ""
if self.preset and self.preset not in VALID_PRESETS:
return False, f"未知的预设: {self.preset}"
# 解析后的参数自然在合法范围内(resolve_params 会钳制)
# 这里检查是否有明显无效的自定义值
for key in ALL_PARAM_KEYS:
val = getattr(self, key)
if val is not None:
min_val, max_val = PARAM_RANGES[key]
if val < min_val or val > max_val:
return False, f"{key}超出范围[{min_val}, {max_val}]: {val}"
return True, ""
# ── 工具函数 ──────────────────────────────────────────────────────────────────
def get_preset_names() -> list[tuple[str, str]]:
"""获取所有预设的 (name, display_name) 列表."""
return [(p, PRESET_DISPLAY_NAMES[p]) for p in sorted(VALID_PRESETS)]
def get_preset_params(preset: str) -> dict[str, float] | None:
"""获取指定预设的参数,不存在返回 None."""
return PRESET_PARAMS.get(preset)
def clamp_param(param_name: str, value: float) -> float:
"""将参数钳制到合法范围内."""
if param_name not in PARAM_RANGES:
return value
min_val, max_val = PARAM_RANGES[param_name]
return max(min_val, min(max_val, value))
+356
View File
@@ -0,0 +1,356 @@
"""color_grade_config 模块单测 — 纯逻辑,无 FFmpeg 依赖."""
from __future__ import annotations
import pytest
from packages.domain.color_grade_config import (
ALL_PARAM_KEYS,
DEFAULT_PARAMS,
PARAM_RANGES,
PRESET_BW,
PRESET_CINEMA,
PRESET_COOL,
PRESET_DISPLAY_NAMES,
PRESET_FILM,
PRESET_FRESH,
PRESET_JAPANESE,
PRESET_PARAMS,
PRESET_VINTAGE,
PRESET_WARM,
VALID_PRESETS,
ColorGradeConfig,
clamp_param,
get_preset_names,
get_preset_params,
)
# ── 常量 ──────────────────────────────────────────────────────────────────────
class TestConstants:
def test_eight_presets(self):
assert len(VALID_PRESETS) == 8
def test_preset_display_names_match(self):
assert set(PRESET_DISPLAY_NAMES.keys()) == VALID_PRESETS
for name in VALID_PRESETS:
assert len(PRESET_DISPLAY_NAMES[name]) > 0
def test_preset_params_complete(self):
assert set(PRESET_PARAMS.keys()) == VALID_PRESETS
for preset, params in PRESET_PARAMS.items():
assert set(params.keys()) == set(ALL_PARAM_KEYS)
def test_default_params_keys(self):
assert set(DEFAULT_PARAMS.keys()) == set(ALL_PARAM_KEYS)
def test_param_ranges_keys(self):
assert set(PARAM_RANGES.keys()) == set(ALL_PARAM_KEYS)
def test_default_within_ranges(self):
for key in ALL_PARAM_KEYS:
min_val, max_val = PARAM_RANGES[key]
assert min_val <= DEFAULT_PARAMS[key] <= max_val
def test_all_presets_within_ranges(self):
for preset, params in PRESET_PARAMS.items():
for key in ALL_PARAM_KEYS:
min_val, max_val = PARAM_RANGES[key]
assert min_val <= params[key] <= max_val, f"{preset}.{key}={params[key]} out of range"
# ── 默认值 ────────────────────────────────────────────────────────────────────
class TestColorGradeConfigDefaults:
def test_default_disabled(self):
cfg = ColorGradeConfig()
assert cfg.enabled is False
assert cfg.preset == ""
assert cfg.brightness is None
assert cfg.contrast is None
assert cfg.saturation is None
assert cfg.temperature is None
assert cfg.hue is None
def test_default_has_no_effect(self):
cfg = ColorGradeConfig()
assert cfg.has_effect() is False
def test_default_resolve_params_equals_defaults(self):
cfg = ColorGradeConfig()
params = cfg.resolve_params()
for key in ALL_PARAM_KEYS:
assert params[key] == DEFAULT_PARAMS[key]
# ── from_dict ────────────────────────────────────────────────────────────────
class TestFromDict:
def test_none_data_disabled(self):
cfg = ColorGradeConfig.from_dict(None)
assert cfg.enabled is False
def test_empty_dict_disabled(self):
cfg = ColorGradeConfig.from_dict({})
assert cfg.enabled is False
def test_enabled_false(self):
cfg = ColorGradeConfig.from_dict({"enabled": False})
assert cfg.enabled is False
def test_enabled_no_preset(self):
cfg = ColorGradeConfig.from_dict({"enabled": True})
assert cfg.enabled is True
assert cfg.preset == ""
assert cfg.brightness is None
def test_with_valid_preset(self):
cfg = ColorGradeConfig.from_dict({"enabled": True, "preset": "fresh"})
assert cfg.enabled is True
assert cfg.preset == "fresh"
def test_with_invalid_preset_ignored(self):
cfg = ColorGradeConfig.from_dict({"enabled": True, "preset": "unknown_preset"})
assert cfg.enabled is True
assert cfg.preset == "" # 无效预设被忽略
def test_with_custom_params(self):
cfg = ColorGradeConfig.from_dict(
{
"enabled": True,
"brightness": 10,
"contrast": -5,
"saturation": 150,
"temperature": 20,
"hue": 30,
}
)
assert cfg.enabled is True
assert cfg.brightness == 10.0
assert cfg.contrast == -5.0
assert cfg.saturation == 150.0
assert cfg.temperature == 20.0
assert cfg.hue == 30.0
def test_with_preset_and_custom_override(self):
cfg = ColorGradeConfig.from_dict(
{
"enabled": True,
"preset": "fresh",
"brightness": 50, # 覆盖预设的 8
}
)
assert cfg.preset == "fresh"
assert cfg.brightness == 50.0
def test_invalid_param_values_none(self):
cfg = ColorGradeConfig.from_dict(
{
"enabled": True,
"brightness": "not_a_number",
}
)
assert cfg.enabled is True
assert cfg.brightness is None # 解析失败为 None
def test_numeric_string_params(self):
cfg = ColorGradeConfig.from_dict(
{
"enabled": True,
"brightness": "15.5",
}
)
assert cfg.brightness == 15.5
def test_preset_bw(self):
cfg = ColorGradeConfig.from_dict({"enabled": True, "preset": "black_white"})
assert cfg.preset == "black_white"
params = cfg.resolve_params()
assert params["saturation"] == 0.0
# ── resolve_params ───────────────────────────────────────────────────────────
class TestResolveParams:
def test_default_returns_defaults(self):
cfg = ColorGradeConfig()
params = cfg.resolve_params()
assert params == DEFAULT_PARAMS
def test_preset_fresh_params(self):
cfg = ColorGradeConfig(preset="fresh")
params = cfg.resolve_params()
assert params["brightness"] == 8
assert params["contrast"] == 10
assert params["saturation"] == 120
def test_preset_bw_saturation_zero(self):
cfg = ColorGradeConfig(preset="black_white")
params = cfg.resolve_params()
assert params["saturation"] == 0
assert params["contrast"] == 15
def test_custom_override_preset(self):
cfg = ColorGradeConfig(preset="fresh", brightness=50)
params = cfg.resolve_params()
assert params["brightness"] == 50 # 覆盖了预设
assert params["saturation"] == 120 # 预设值保留
def test_below_min_clamped(self):
cfg = ColorGradeConfig(brightness=-200, saturation=-10)
params = cfg.resolve_params()
assert params["brightness"] == PARAM_RANGES["brightness"][0]
assert params["saturation"] == PARAM_RANGES["saturation"][0]
def test_above_max_clamped(self):
cfg = ColorGradeConfig(brightness=200, saturation=300, hue=360)
params = cfg.resolve_params()
assert params["brightness"] == PARAM_RANGES["brightness"][1]
assert params["saturation"] == PARAM_RANGES["saturation"][1]
assert params["hue"] == PARAM_RANGES["hue"][1]
def test_all_presets_resolve_within_ranges(self):
for preset in VALID_PRESETS:
cfg = ColorGradeConfig(enabled=True, preset=preset)
params = cfg.resolve_params()
for key in ALL_PARAM_KEYS:
min_val, max_val = PARAM_RANGES[key]
assert min_val <= params[key] <= max_val, f"{preset}.{key}={params[key]}"
def test_returns_new_dict_each_call(self):
cfg = ColorGradeConfig(preset="warm")
p1 = cfg.resolve_params()
p2 = cfg.resolve_params()
assert p1 is not p2
p1["brightness"] = 999
assert p2["brightness"] != 999
# ── has_effect ───────────────────────────────────────────────────────────────
class TestHasEffect:
def test_disabled_no_effect(self):
cfg = ColorGradeConfig(enabled=False)
assert cfg.has_effect() is False
def test_default_no_effect(self):
cfg = ColorGradeConfig(enabled=True)
assert cfg.has_effect() is False
def test_preset_has_effect(self):
for preset in VALID_PRESETS:
cfg = ColorGradeConfig(enabled=True, preset=preset)
# 大部分预设都有效果,除了默认值完全一致的(应该没有)
if preset in ("black_white",):
assert cfg.has_effect() is True # BW 有 contrast=15
else:
assert cfg.has_effect() is True
def test_custom_brightness_has_effect(self):
cfg = ColorGradeConfig(enabled=True, brightness=1.0)
assert cfg.has_effect() is True
def test_tiny_change_no_effect(self):
# 小于 0.001 的差异视为无效果
cfg = ColorGradeConfig(enabled=True)
# 直接通过默认值的话应该没有效果
assert cfg.has_effect() is False
def test_custom_saturation_changed_from_default(self):
# 默认 saturation=100,改成 101 就有效果
cfg = ColorGradeConfig(enabled=True, saturation=101)
assert cfg.has_effect() is True
# ── validate ─────────────────────────────────────────────────────────────────
class TestValidate:
def test_disabled_always_valid(self):
cfg = ColorGradeConfig(enabled=False)
ok, err = cfg.validate()
assert ok is True
assert err == ""
def test_enabled_no_preset_valid(self):
cfg = ColorGradeConfig(enabled=True)
ok, err = cfg.validate()
assert ok is True
def test_valid_preset(self):
cfg = ColorGradeConfig(enabled=True, preset="cinema")
ok, _ = cfg.validate()
assert ok is True
def test_invalid_preset(self):
cfg = ColorGradeConfig(enabled=True, preset="invalid")
ok, err = cfg.validate()
assert ok is False
assert "预设" in err
def test_custom_param_out_of_range(self):
cfg = ColorGradeConfig(enabled=True, brightness=500)
ok, err = cfg.validate()
assert ok is False
assert "brightness" in err
def test_custom_param_within_range(self):
cfg = ColorGradeConfig(enabled=True, brightness=50, contrast=-50)
ok, _ = cfg.validate()
assert ok is True
def test_saturation_negative_invalid(self):
cfg = ColorGradeConfig(enabled=True, saturation=-1)
ok, err = cfg.validate()
assert ok is False
assert "saturation" in err
# ── 工具函数 ─────────────────────────────────────────────────────────────────
class TestUtils:
def test_get_preset_names_returns_all(self):
names = get_preset_names()
assert len(names) == len(VALID_PRESETS)
preset_keys = [n[0] for n in names]
assert set(preset_keys) == VALID_PRESETS
def test_get_preset_names_sorted(self):
names = get_preset_names()
preset_keys = [n[0] for n in names]
assert preset_keys == sorted(preset_keys)
def test_get_preset_params_valid(self):
params = get_preset_params("fresh")
assert params is not None
assert params["brightness"] == 8
def test_get_preset_params_invalid(self):
params = get_preset_params("unknown")
assert params is None
@pytest.mark.parametrize(
"param,value,expected",
[
("brightness", 50, 50),
("brightness", 200, 100),
("brightness", -200, -100),
("saturation", 50, 50),
("saturation", -10, 0),
("saturation", 300, 200),
("hue", 0, 0),
("hue", 200, 180),
("hue", -200, -180),
],
)
def test_clamp_param(self, param, value, expected):
assert clamp_param(param, value) == expected
def test_clamp_param_unknown_passthrough(self):
assert clamp_param("unknown_param", 999) == 999