diff --git a/tests/unit/domain/test_color_grade_config.py b/tests/unit/domain/test_color_grade_config.py new file mode 100755 index 000000000..a6e87fef8 --- /dev/null +++ b/tests/unit/domain/test_color_grade_config.py @@ -0,0 +1,474 @@ +"""ColorGradeConfig 色彩调色配置单测. + +纯逻辑模块,覆盖:数据类、resolve_params参数解析、has_effect效果判断、 +from_dict字典解析、validate校验、预设查询函数、clamp_param钳制。 +""" + +from __future__ import annotations + +from packages.domain.color_grade_config import ( + ALL_PARAM_KEYS, + DEFAULT_PARAMS, + PARAM_RANGES, + PRESET_DISPLAY_NAMES, + PRESET_PARAMS, + VALID_PRESETS, + ColorGradeConfig, + clamp_param, + get_preset_names, + get_preset_params, +) + + +class TestColorGradeConfigDefaults: + def test_default_disabled(self): + config = ColorGradeConfig() + assert config.enabled is False + assert config.preset == "" + assert config.brightness is None + assert config.contrast is None + assert config.saturation is None + assert config.temperature is None + assert config.hue is None + + def test_default_resolve_returns_defaults(self): + config = ColorGradeConfig() + params = config.resolve_params() + for key in ALL_PARAM_KEYS: + assert params[key] == DEFAULT_PARAMS[key] + + def test_default_has_no_effect(self): + config = ColorGradeConfig() + assert config.has_effect() is False + + def test_default_validate_passes(self): + config = ColorGradeConfig() + ok, msg = config.validate() + assert ok is True + assert msg == "" + + +class TestResolveParams: + def test_disabled_still_resolves(self): + """禁用状态下仍能解析参数.""" + config = ColorGradeConfig(enabled=False, brightness=50.0) + params = config.resolve_params() + assert params["brightness"] == 50.0 + + def test_preset_fresh(self): + config = ColorGradeConfig(enabled=True, preset="fresh") + params = config.resolve_params() + assert params["brightness"] == 8 + assert params["contrast"] == 10 + assert params["saturation"] == 120 + assert params["temperature"] == -8 + assert params["hue"] == 5 + + def test_preset_black_white(self): + config = ColorGradeConfig(enabled=True, preset="black_white") + params = config.resolve_params() + assert params["saturation"] == 0 + assert params["contrast"] == 15 + + def test_preset_warm(self): + config = ColorGradeConfig(enabled=True, preset="warm") + params = config.resolve_params() + assert params["temperature"] == 30 + + def test_preset_cool(self): + config = ColorGradeConfig(enabled=True, preset="cool") + params = config.resolve_params() + assert params["temperature"] == -25 + + def test_invalid_preset_uses_defaults(self): + config = ColorGradeConfig(enabled=True, preset="nonexistent") + params = config.resolve_params() + for key in ALL_PARAM_KEYS: + assert params[key] == DEFAULT_PARAMS[key] + + def test_custom_override_preset(self): + config = ColorGradeConfig( + enabled=True, + preset="fresh", + brightness=50.0, + ) + params = config.resolve_params() + # custom覆盖了预设 + assert params["brightness"] == 50.0 + # 其他参数仍用预设值 + assert params["contrast"] == 10 + assert params["saturation"] == 120 + + def test_multiple_custom_overrides(self): + config = ColorGradeConfig( + enabled=True, + preset="vintage", + brightness=20.0, + saturation=150.0, + hue=10.0, + ) + params = config.resolve_params() + assert params["brightness"] == 20.0 + assert params["saturation"] == 150.0 + assert params["hue"] == 10.0 + # 未覆盖的保留预设值 + assert params["contrast"] == 5 + assert params["temperature"] == 25 + + def test_custom_without_preset(self): + config = ColorGradeConfig( + enabled=True, + preset="", + brightness=30.0, + contrast=-20.0, + ) + params = config.resolve_params() + assert params["brightness"] == 30.0 + assert params["contrast"] == -20.0 + # 未设置的用默认值 + assert params["saturation"] == 100.0 + assert params["temperature"] == 0.0 + assert params["hue"] == 0.0 + + def test_clamping_brightness_above_max(self): + config = ColorGradeConfig(enabled=True, brightness=200.0) + params = config.resolve_params() + assert params["brightness"] == 100.0 + + def test_clamping_brightness_below_min(self): + config = ColorGradeConfig(enabled=True, brightness=-200.0) + params = config.resolve_params() + assert params["brightness"] == -100.0 + + def test_clamping_saturation_below_zero(self): + config = ColorGradeConfig(enabled=True, saturation=-10.0) + params = config.resolve_params() + assert params["saturation"] == 0.0 + + def test_clamping_saturation_above_max(self): + config = ColorGradeConfig(enabled=True, saturation=300.0) + params = config.resolve_params() + assert params["saturation"] == 200.0 + + def test_clamping_hue_above_max(self): + config = ColorGradeConfig(enabled=True, hue=200.0) + params = config.resolve_params() + assert params["hue"] == 180.0 + + def test_clamping_hue_below_min(self): + config = ColorGradeConfig(enabled=True, hue=-200.0) + params = config.resolve_params() + assert params["hue"] == -180.0 + + def test_clamping_preset_plus_custom(self): + """预设值+自定义值超出范围时仍会钳制.""" + config = ColorGradeConfig( + enabled=True, + preset="fresh", + saturation=250.0, # 超出200上限 + ) + params = config.resolve_params() + assert params["saturation"] == 200.0 + + def test_returns_new_dict_each_time(self): + config = ColorGradeConfig(enabled=True, brightness=10.0) + p1 = config.resolve_params() + p2 = config.resolve_params() + assert p1 is not p2 + p1["brightness"] = 999 + assert p2["brightness"] == 10.0 + + +class TestHasEffect: + def test_default_no_effect(self): + config = ColorGradeConfig() + assert config.has_effect() is False + + def test_enabled_but_all_defaults(self): + config = ColorGradeConfig( + enabled=True, + brightness=0.0, + contrast=0.0, + saturation=100.0, + temperature=0.0, + hue=0.0, + ) + assert config.has_effect() is False + + def test_brightness_change_has_effect(self): + config = ColorGradeConfig(enabled=True, brightness=1.0) + assert config.has_effect() is True + + def test_contrast_change_has_effect(self): + config = ColorGradeConfig(enabled=True, contrast=1.0) + assert config.has_effect() is True + + def test_saturation_change_has_effect(self): + config = ColorGradeConfig(enabled=True, saturation=99.0) + assert config.has_effect() is True + + def test_temperature_change_has_effect(self): + config = ColorGradeConfig(enabled=True, temperature=1.0) + assert config.has_effect() is True + + def test_hue_change_has_effect(self): + config = ColorGradeConfig(enabled=True, hue=1.0) + assert config.has_effect() is True + + def test_all_presets_have_effect(self): + for preset in VALID_PRESETS: + config = ColorGradeConfig(enabled=True, preset=preset) + assert config.has_effect() is True, f"preset {preset} should have effect" + + def test_very_small_change_no_effect(self): + """小于0.001的浮点误差视为无效果.""" + config = ColorGradeConfig(enabled=True, brightness=0.0001) + assert config.has_effect() is False + + +class TestFromDict: + def test_none_returns_disabled(self): + config = ColorGradeConfig.from_dict(None) + assert config.enabled is False + + def test_empty_dict_returns_disabled(self): + config = ColorGradeConfig.from_dict({}) + assert config.enabled is False + + def test_enabled_false_returns_disabled(self): + config = ColorGradeConfig.from_dict({"enabled": False}) + assert config.enabled is False + + def test_basic_enabled(self): + config = ColorGradeConfig.from_dict({"enabled": True}) + assert config.enabled is True + assert config.preset == "" + assert config.brightness is None + + def test_with_preset(self): + config = ColorGradeConfig.from_dict({"enabled": True, "preset": "warm"}) + assert config.enabled is True + assert config.preset == "warm" + + def test_invalid_preset_ignored(self): + config = ColorGradeConfig.from_dict({"enabled": True, "preset": "invalid"}) + assert config.enabled is True + assert config.preset == "" + + def test_with_custom_params(self): + config = ColorGradeConfig.from_dict( + { + "enabled": True, + "brightness": 50, + "contrast": -10.5, + "saturation": 150.0, + "temperature": 20, + "hue": -15.5, + } + ) + assert config.brightness == 50.0 + assert config.contrast == -10.5 + assert config.saturation == 150.0 + assert config.temperature == 20.0 + assert config.hue == -15.5 + + def test_invalid_float_values_return_none(self): + config = ColorGradeConfig.from_dict( + { + "enabled": True, + "brightness": "not_a_number", + "contrast": None, + } + ) + assert config.brightness is None + assert config.contrast is None + + def test_int_values_work(self): + config = ColorGradeConfig.from_dict({"enabled": True, "brightness": 10}) + assert config.brightness == 10.0 + + def test_string_float_values_work(self): + config = ColorGradeConfig.from_dict({"enabled": True, "brightness": "15.5"}) + assert config.brightness == 15.5 + + def test_partial_custom_params(self): + config = ColorGradeConfig.from_dict( + { + "enabled": True, + "preset": "cinema", + "brightness": 5.0, + } + ) + assert config.preset == "cinema" + assert config.brightness == 5.0 + assert config.contrast is None + + +class TestValidate: + def test_disabled_valid(self): + config = ColorGradeConfig(enabled=False) + ok, msg = config.validate() + assert ok is True + assert msg == "" + + def test_valid_config(self): + config = ColorGradeConfig( + enabled=True, + preset="fresh", + brightness=50.0, + ) + ok, msg = config.validate() + assert ok is True + + def test_invalid_preset(self): + config = ColorGradeConfig(enabled=True, preset="invalid") + ok, msg = config.validate() + assert ok is False + assert "预设" in msg + + def test_brightness_above_max_invalid(self): + config = ColorGradeConfig(enabled=True, brightness=150.0) + ok, msg = config.validate() + assert ok is False + assert "brightness" in msg + + def test_brightness_below_min_invalid(self): + config = ColorGradeConfig(enabled=True, brightness=-150.0) + ok, msg = config.validate() + assert ok is False + assert "brightness" in msg + + def test_saturation_below_zero_invalid(self): + config = ColorGradeConfig(enabled=True, saturation=-10.0) + ok, msg = config.validate() + assert ok is False + assert "saturation" in msg + + def test_saturation_above_max_invalid(self): + config = ColorGradeConfig(enabled=True, saturation=250.0) + ok, msg = config.validate() + assert ok is False + assert "saturation" in msg + + def test_hue_above_max_invalid(self): + config = ColorGradeConfig(enabled=True, hue=200.0) + ok, msg = config.validate() + assert ok is False + assert "hue" in msg + + def test_hue_below_min_invalid(self): + config = ColorGradeConfig(enabled=True, hue=-200.0) + ok, msg = config.validate() + assert ok is False + assert "hue" in msg + + def test_boundary_values_valid(self): + config = ColorGradeConfig( + enabled=True, + brightness=-100.0, + contrast=100.0, + saturation=0.0, + temperature=-100.0, + hue=-180.0, + ) + ok, _ = config.validate() + assert ok is True + + def test_none_values_valid(self): + """None值不参与校验(视为未设置).""" + config = ColorGradeConfig(enabled=True) + ok, _ = config.validate() + assert ok is True + + +class TestGetPresetNames: + def test_returns_sorted_list(self): + names = get_preset_names() + assert isinstance(names, list) + preset_keys = [n[0] for n in names] + assert preset_keys == sorted(preset_keys) + + def test_count_matches_valid_presets(self): + names = get_preset_names() + assert len(names) == len(VALID_PRESETS) + + def test_each_entry_has_name_and_display(self): + names = get_preset_names() + for name, display in names: + assert name in VALID_PRESETS + assert display == PRESET_DISPLAY_NAMES[name] + assert len(display) > 0 + + +class TestGetPresetParams: + def test_existing_preset(self): + params = get_preset_params("fresh") + assert params is not None + assert params["brightness"] == 8 + + def test_nonexistent_preset(self): + params = get_preset_params("nonexistent") + assert params is None + + def test_all_presets_have_all_params(self): + for preset in VALID_PRESETS: + params = get_preset_params(preset) + assert params is not None + for key in ALL_PARAM_KEYS: + assert key in params + + def test_returns_dict_with_correct_values(self): + """返回的字典包含所有预期参数.""" + params = get_preset_params("warm") + assert params["brightness"] == 5 + assert params["temperature"] == 30 + assert len(params) == 5 + + +class TestClampParam: + def test_brightness_within_range(self): + assert clamp_param("brightness", 50.0) == 50.0 + + def test_brightness_above_max(self): + assert clamp_param("brightness", 200.0) == 100.0 + + def test_brightness_below_min(self): + assert clamp_param("brightness", -200.0) == -100.0 + + def test_saturation_within_range(self): + assert clamp_param("saturation", 100.0) == 100.0 + + def test_saturation_at_boundary(self): + assert clamp_param("saturation", 0.0) == 0.0 + assert clamp_param("saturation", 200.0) == 200.0 + + def test_hue_within_range(self): + assert clamp_param("hue", 90.0) == 90.0 + + def test_hue_above_max(self): + assert clamp_param("hue", 200.0) == 180.0 + + def test_unknown_param_passthrough(self): + assert clamp_param("unknown", 999.0) == 999.0 + + +class TestConstants: + def test_all_presets_have_params(self): + for preset in VALID_PRESETS: + assert preset in PRESET_PARAMS + + def test_all_presets_have_display_names(self): + for preset in VALID_PRESETS: + assert preset in PRESET_DISPLAY_NAMES + + def test_param_ranges_has_all_keys(self): + for key in ALL_PARAM_KEYS: + assert key in PARAM_RANGES + + def test_default_params_has_all_keys(self): + for key in ALL_PARAM_KEYS: + assert key in DEFAULT_PARAMS + + def test_param_ranges_min_less_than_max(self): + for key, (min_val, max_val) in PARAM_RANGES.items(): + assert min_val < max_val, f"{key}: min ({min_val}) should be < max ({max_val})"