From 5cbac9a15d7b4aa664c673811b0e21753d754376 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 15:31:51 +0800 Subject: [PATCH] =?UTF-8?q?test(wave170):=20chroma=5Fkey=5Fconfig=E7=BB=BF?= =?UTF-8?q?=E5=B9=95=E6=8A=A0=E5=83=8F=E9=85=8D=E7=BD=AE=20+70=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/domain/test_chroma_key_config.py | 470 ++++++++++++++++++++ 1 file changed, 470 insertions(+) create mode 100755 tests/unit/domain/test_chroma_key_config.py diff --git a/tests/unit/domain/test_chroma_key_config.py b/tests/unit/domain/test_chroma_key_config.py new file mode 100755 index 000000000..07454c4ba --- /dev/null +++ b/tests/unit/domain/test_chroma_key_config.py @@ -0,0 +1,470 @@ +"""ChromaKeyConfig 绿幕抠像配置单测. + +纯逻辑模块,覆盖:数据类、from_dict解析、from_preset预设、has_effect、 +validate校验、normalize_color颜色归一化、colorkey滤镜构建、 +chromakey滤镜构建、apply_chroma_key_if_needed便捷函数、get_preset_names。 +""" + +from __future__ import annotations + +from packages.domain.chroma_key_config import ( + CHROMA_KEY_PRESETS, + ChromaKeyConfig, + MAX_BLEND, + MAX_SIMILARITY, + MAX_SPILL_SUPPRESS, + MIN_BLEND, + MIN_SIMILARITY, + MIN_SPILL_SUPPRESS, + VALID_PRESETS, + apply_chroma_key_if_needed, + build_chromakey_filter, + build_colorkey_filter, + get_preset_names, + normalize_color, +) + + +class TestChromaKeyConfigDefaults: + def test_default_disabled(self): + config = ChromaKeyConfig() + assert config.enabled is False + assert config.key_color == "#00FF00" + assert config.similarity == 0.3 + assert config.blend == 0.1 + assert config.spill_suppress == 0.0 + + def test_default_has_no_effect(self): + config = ChromaKeyConfig() + assert config.has_effect() is False + + def test_default_validate_passes(self): + config = ChromaKeyConfig() + ok, msg = config.validate() + assert ok is True + assert msg == "" + + +class TestFromDict: + def test_none_returns_disabled(self): + config = ChromaKeyConfig.from_dict(None) + assert config.enabled is False + + def test_empty_dict_returns_disabled(self): + config = ChromaKeyConfig.from_dict({}) + assert config.enabled is False + + def test_enabled_false_returns_disabled(self): + config = ChromaKeyConfig.from_dict({"enabled": False}) + assert config.enabled is False + + def test_basic_enabled(self): + config = ChromaKeyConfig.from_dict({"enabled": True, "key_color": "#FF0000"}) + assert config.enabled is True + assert config.key_color == "#FF0000" + + def test_default_values_when_enabled(self): + config = ChromaKeyConfig.from_dict({"enabled": True}) + assert config.enabled is True + assert config.key_color == "#00FF00" + assert config.similarity == 0.3 + assert config.blend == 0.1 + assert config.spill_suppress == 0.0 + + def test_custom_values(self): + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "key_color": "#0000FF", + "similarity": 0.5, + "blend": 0.2, + "spill_suppress": 0.3, + } + ) + assert config.key_color == "#0000FF" + assert config.similarity == 0.5 + assert config.blend == 0.2 + assert config.spill_suppress == 0.3 + + def test_similarity_clamped_below_min(self): + config = ChromaKeyConfig.from_dict({"enabled": True, "similarity": 0.0}) + assert config.similarity == MIN_SIMILARITY + + def test_similarity_clamped_above_max(self): + config = ChromaKeyConfig.from_dict({"enabled": True, "similarity": 2.0}) + assert config.similarity == MAX_SIMILARITY + + def test_blend_clamped_below_min(self): + config = ChromaKeyConfig.from_dict({"enabled": True, "blend": -1.0}) + assert config.blend == MIN_BLEND + + def test_blend_clamped_above_max(self): + config = ChromaKeyConfig.from_dict({"enabled": True, "blend": 2.0}) + assert config.blend == MAX_BLEND + + def test_spill_suppress_clamped_below_min(self): + config = ChromaKeyConfig.from_dict({"enabled": True, "spill_suppress": -0.5}) + assert config.spill_suppress == MIN_SPILL_SUPPRESS + + def test_spill_suppress_clamped_above_max(self): + config = ChromaKeyConfig.from_dict({"enabled": True, "spill_suppress": 2.0}) + assert config.spill_suppress == MAX_SPILL_SUPPRESS + + def test_invalid_float_values_use_default(self): + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "similarity": "not_a_number", + "blend": None, + } + ) + assert config.similarity == 0.3 + assert config.blend == 0.1 + + def test_key_color_stripped(self): + config = ChromaKeyConfig.from_dict({"enabled": True, "key_color": " #FF0000 "}) + assert config.key_color == "#FF0000" + + +class TestFromPreset: + def test_green_screen_preset(self): + config = ChromaKeyConfig.from_preset("green_screen") + assert config is not None + assert config.enabled is True + assert config.key_color == "#00FF00" + assert config.similarity == 0.3 + assert config.blend == 0.1 + assert config.spill_suppress == 0.5 + + def test_blue_screen_preset(self): + config = ChromaKeyConfig.from_preset("blue_screen") + assert config is not None + assert config.key_color == "#0000FF" + + def test_red_screen_preset(self): + config = ChromaKeyConfig.from_preset("red_screen") + assert config is not None + assert config.key_color == "#FF0000" + assert config.spill_suppress == 0.0 + + def test_invalid_preset_returns_none(self): + config = ChromaKeyConfig.from_preset("nonexistent") + assert config is None + + def test_all_presets_are_valid(self): + for name in CHROMA_KEY_PRESETS: + config = ChromaKeyConfig.from_preset(name) + assert config is not None + assert config.enabled is True + assert config.has_effect() is True + ok, _ = config.validate() + assert ok is True + + +class TestHasEffect: + def test_disabled_no_effect(self): + config = ChromaKeyConfig(enabled=False) + assert config.has_effect() is False + + def test_enabled_but_zero_similarity(self): + config = ChromaKeyConfig(enabled=True, similarity=0.0) + assert config.has_effect() is False + + def test_enabled_with_similarity_has_effect(self): + config = ChromaKeyConfig(enabled=True, similarity=0.01) + assert config.has_effect() is True + + def test_full_config_has_effect(self): + config = ChromaKeyConfig( + enabled=True, + key_color="#00FF00", + similarity=0.3, + blend=0.1, + spill_suppress=0.5, + ) + assert config.has_effect() is True + + +class TestValidate: + def test_disabled_valid(self): + config = ChromaKeyConfig(enabled=False) + ok, msg = config.validate() + assert ok is True + assert msg == "" + + def test_valid_config(self): + config = ChromaKeyConfig( + enabled=True, + key_color="#00FF00", + similarity=0.3, + blend=0.1, + spill_suppress=0.5, + ) + ok, msg = config.validate() + assert ok is True + assert msg == "" + + def test_empty_key_color_invalid(self): + config = ChromaKeyConfig(enabled=True, key_color="") + ok, msg = config.validate() + assert ok is False + assert "key_color" in msg + + def test_similarity_below_min_invalid(self): + config = ChromaKeyConfig(enabled=True, similarity=0.001) + ok, msg = config.validate() + assert ok is False + assert "similarity" in msg + + def test_similarity_above_max_invalid(self): + config = ChromaKeyConfig(enabled=True, similarity=1.5) + ok, msg = config.validate() + assert ok is False + assert "similarity" in msg + + def test_blend_below_min_invalid(self): + config = ChromaKeyConfig(enabled=True, blend=-0.1) + ok, msg = config.validate() + assert ok is False + assert "blend" in msg + + def test_blend_above_max_invalid(self): + config = ChromaKeyConfig(enabled=True, blend=1.5) + ok, msg = config.validate() + assert ok is False + assert "blend" in msg + + def test_spill_suppress_below_min_invalid(self): + config = ChromaKeyConfig(enabled=True, spill_suppress=-0.1) + ok, msg = config.validate() + assert ok is False + assert "spill_suppress" in msg + + def test_spill_suppress_above_max_invalid(self): + config = ChromaKeyConfig(enabled=True, spill_suppress=1.5) + ok, msg = config.validate() + assert ok is False + assert "spill_suppress" in msg + + def test_boundary_values_valid(self): + config = ChromaKeyConfig( + enabled=True, + key_color="#000", + similarity=MIN_SIMILARITY, + blend=MIN_BLEND, + spill_suppress=MIN_SPILL_SUPPRESS, + ) + ok, _ = config.validate() + assert ok is True + + config2 = ChromaKeyConfig( + enabled=True, + key_color="#fff", + similarity=MAX_SIMILARITY, + blend=MAX_BLEND, + spill_suppress=MAX_SPILL_SUPPRESS, + ) + ok2, _ = config2.validate() + assert ok2 is True + + +class TestNormalizeColor: + def test_hex_with_hash(self): + assert normalize_color("#FF0000") == "0xFF0000" + + def test_hex_without_hash(self): + assert normalize_color("FF0000") == "0xFF0000" + + def test_hex_lowercase(self): + assert normalize_color("#ff0000") == "0xFF0000" + + def test_hex_mixed_case(self): + assert normalize_color("#aBcDeF") == "0xABCDEF" + + def test_hex_with_alpha(self): + assert normalize_color("#FF000080") == "0xFF0000" + + def test_0x_format_passthrough(self): + assert normalize_color("0xFF0000") == "0XFF0000" + + def test_0x_lowercase(self): + assert normalize_color("0xff0000") == "0XFF0000" + + def test_color_name_passthrough(self): + assert normalize_color("green") == "green" + + def test_color_name_blue(self): + assert normalize_color("blue") == "blue" + + def test_whitespace_stripped(self): + assert normalize_color(" #FF0000 ") == "0xFF0000" + + def test_black_color(self): + assert normalize_color("#000000") == "0x000000" + + def test_white_color(self): + assert normalize_color("#FFFFFF") == "0xFFFFFF" + + +class TestBuildColorkeyFilter: + def test_disabled_returns_copy(self): + config = ChromaKeyConfig(enabled=False) + result = build_colorkey_filter(config, "[0:v]", "[out]") + assert result == "[0:v]copy[out]" + + def test_zero_similarity_returns_copy(self): + config = ChromaKeyConfig(enabled=True, similarity=0.0) + result = build_colorkey_filter(config, "[v0]", "[ck]") + assert result == "[v0]copy[ck]" + + def test_basic_colorkey(self): + config = ChromaKeyConfig(enabled=True, key_color="#00FF00", similarity=0.3, blend=0.1) + result = build_colorkey_filter(config, "[0:v]", "[ck]") + assert "colorkey=color=0x00FF00:similarity=0.3:blend=0.1" in result + assert result.startswith("[0:v]") + assert result.endswith("[ck]") + + def test_with_spill_suppress(self): + config = ChromaKeyConfig( + enabled=True, + key_color="#00FF00", + similarity=0.3, + blend=0.1, + spill_suppress=0.5, + ) + result = build_colorkey_filter(config, "[0:v]", "[ck]") + assert "colorkey=" in result + assert "colorchannelmixer=" in result + # Spill suppress reduces green gain + assert "gg=" in result + assert "rr=" in result + assert "bb=" in result + + def test_no_spill_suppress_no_colorchannelmixer(self): + config = ChromaKeyConfig( + enabled=True, + key_color="#00FF00", + similarity=0.3, + blend=0.1, + spill_suppress=0.0, + ) + result = build_colorkey_filter(config, "[0:v]", "[ck]") + assert "colorchannelmixer" not in result + + def test_different_labels(self): + config = ChromaKeyConfig(enabled=True, key_color="#FF0000", similarity=0.5) + result = build_colorkey_filter(config, "[v_in]", "[v_out]") + assert result.startswith("[v_in]") + assert result.endswith("[v_out]") + + def test_spill_suppress_gain_values(self): + config = ChromaKeyConfig( + enabled=True, + key_color="#00FF00", + similarity=0.3, + blend=0.1, + spill_suppress=1.0, + ) + result = build_colorkey_filter(config, "[0:v]", "[ck]") + # At spill=1.0: g_gain = max(0.3, 1.0 - 1.0*0.7) = max(0.3, 0.3) = 0.3 + assert "gg=0.3" in result + + +class TestBuildChromakeyFilter: + def test_disabled_returns_copy(self): + config = ChromaKeyConfig(enabled=False) + result = build_chromakey_filter(config, "[0:v]", "[out]") + assert result == "[0:v]copy[out]" + + def test_basic_chromakey(self): + config = ChromaKeyConfig(enabled=True, key_color="#0000FF", similarity=0.4, blend=0.2) + result = build_chromakey_filter(config, "[0:v]", "[ck]") + assert "chromakey=color=0x0000FF:similarity=0.4:blend=0.2" in result + assert result.startswith("[0:v]") + assert result.endswith("[ck]") + + def test_spill_suppress_not_included(self): + """chromakey滤镜不包含spill_suppress(只有colorkey有).""" + config = ChromaKeyConfig( + enabled=True, + key_color="#00FF00", + similarity=0.3, + blend=0.1, + spill_suppress=0.5, + ) + result = build_chromakey_filter(config, "[0:v]", "[ck]") + assert "colorchannelmixer" not in result + assert "chromakey=" in result + + +class TestApplyChromaKeyIfNeeded: + def test_none_clip_config_returns_none(self): + result = apply_chroma_key_if_needed(None, "[0:v]", "[ck]") + assert result is None + + def test_no_chroma_key_returns_none(self): + result = apply_chroma_key_if_needed({"other": "data"}, "[0:v]", "[ck]") + assert result is None + + def test_chroma_key_disabled_returns_none(self): + config = {"chroma_key": {"enabled": False}} + result = apply_chroma_key_if_needed(config, "[0:v]", "[ck]") + assert result is None + + def test_chroma_key_enabled_returns_filter(self): + config = { + "chroma_key": { + "enabled": True, + "key_color": "#00FF00", + "similarity": 0.3, + "blend": 0.1, + } + } + result = apply_chroma_key_if_needed(config, "[0:v]", "[ck]") + assert result is not None + assert "colorkey=" in result + assert result.startswith("[0:v]") + assert result.endswith("[ck]") + + def test_invalid_config_returns_none(self): + """配置异常时应该返回None而不是抛异常.""" + config = {"chroma_key": "invalid_data"} + result = apply_chroma_key_if_needed(config, "[0:v]", "[ck]") + assert result is None + + +class TestGetPresetNames: + def test_returns_sorted_list(self): + names = get_preset_names() + assert isinstance(names, list) + assert names == sorted(names) + + def test_contains_known_presets(self): + names = get_preset_names() + assert "green_screen" in names + assert "blue_screen" in names + assert "red_screen" in names + + def test_count_matches_presets_dict(self): + names = get_preset_names() + assert len(names) == len(CHROMA_KEY_PRESETS) + + +class TestPresetsAndConstants: + def test_valid_presets_equals_preset_keys(self): + assert VALID_PRESETS == set(CHROMA_KEY_PRESETS.keys()) + + def test_each_preset_has_required_keys(self): + for name, preset in CHROMA_KEY_PRESETS.items(): + assert "key_color" in preset, f"{name} missing key_color" + assert "similarity" in preset, f"{name} missing similarity" + assert "blend" in preset, f"{name} missing blend" + assert "spill_suppress" in preset, f"{name} missing spill_suppress" + + def test_min_less_than_max(self): + assert MIN_SIMILARITY < MAX_SIMILARITY + assert MIN_BLEND <= MAX_BLEND + assert MIN_SPILL_SUPPRESS <= MAX_SPILL_SUPPRESS + + def test_min_similarity_positive(self): + assert MIN_SIMILARITY > 0 -- 2.54.0