"""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, MAX_BLEND, MAX_SIMILARITY, MAX_SPILL_SUPPRESS, MIN_BLEND, MIN_SIMILARITY, MIN_SPILL_SUPPRESS, VALID_PRESETS, ChromaKeyConfig, 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