|
7 | 7 | import pytest |
8 | 8 |
|
9 | 9 | from viseron.config import ( |
| 10 | + ComponentChange, |
| 11 | + ConfigDiff, |
10 | 12 | DomainChange, |
11 | 13 | IdentifierChange, |
12 | 14 | create_default_config, |
| 15 | + diff_config, |
13 | 16 | diff_domain_config, |
14 | 17 | diff_identifier_config, |
15 | 18 | load_config, |
@@ -715,3 +718,276 @@ def test_unchanged_domains_not_included(self) -> None: |
715 | 718 | result = diff_domain_config("darknet", old, new) |
716 | 719 | assert len(result) == 1 |
717 | 720 | assert result[0].domain == "object_detector" |
| 721 | + |
| 722 | + |
| 723 | +class TestComponentChange: |
| 724 | + """Test ComponentChange dataclass.""" |
| 725 | + |
| 726 | + def test_component_added(self) -> None: |
| 727 | + """Test is_added/is_removed/is_component_level_change for added component.""" |
| 728 | + change = ComponentChange( |
| 729 | + component_name="ffmpeg", |
| 730 | + old_config=None, |
| 731 | + new_config={"camera": {"cam1": {"host": "192.168.1.1"}}}, |
| 732 | + ) |
| 733 | + assert change.is_added is True |
| 734 | + assert change.is_removed is False |
| 735 | + assert change.is_component_level_change is True |
| 736 | + |
| 737 | + def test_component_removed(self) -> None: |
| 738 | + """Test is_added/is_removed/is_component_level_change for removed component.""" |
| 739 | + change = ComponentChange( |
| 740 | + component_name="ffmpeg", |
| 741 | + old_config={"camera": {"cam1": {"host": "192.168.1.1"}}}, |
| 742 | + new_config=None, |
| 743 | + ) |
| 744 | + assert change.is_added is False |
| 745 | + assert change.is_removed is True |
| 746 | + assert change.is_component_level_change is True |
| 747 | + |
| 748 | + def test_component_both_none(self) -> None: |
| 749 | + """Test all properties return False when both configs are None.""" |
| 750 | + change = ComponentChange( |
| 751 | + component_name="ffmpeg", |
| 752 | + old_config=None, |
| 753 | + new_config=None, |
| 754 | + ) |
| 755 | + assert change.is_added is False |
| 756 | + assert change.is_removed is False |
| 757 | + assert change.is_component_level_change is False |
| 758 | + |
| 759 | + def test_is_component_level_change_only_domain_keys_differ(self) -> None: |
| 760 | + """Test component-level change is False when only domain keys differ.""" |
| 761 | + change = ComponentChange( |
| 762 | + component_name="darknet", |
| 763 | + old_config={ |
| 764 | + "object_detector": {"threshold": 0.5, "cameras": {"cam1": {}}}, |
| 765 | + }, |
| 766 | + new_config={ |
| 767 | + "object_detector": {"threshold": 0.9, "cameras": {"cam1": {}}}, |
| 768 | + }, |
| 769 | + ) |
| 770 | + assert change.is_component_level_change is False |
| 771 | + |
| 772 | + def test_is_component_level_change_non_domain_key_differs(self) -> None: |
| 773 | + """Test component-level change is True when non-domain config differs.""" |
| 774 | + change = ComponentChange( |
| 775 | + component_name="darknet", |
| 776 | + old_config={ |
| 777 | + "log_level": "info", |
| 778 | + "object_detector": {"cameras": {"cam1": {}}}, |
| 779 | + }, |
| 780 | + new_config={ |
| 781 | + "log_level": "debug", |
| 782 | + "object_detector": {"cameras": {"cam1": {}}}, |
| 783 | + }, |
| 784 | + ) |
| 785 | + assert change.is_component_level_change is True |
| 786 | + |
| 787 | + def test_is_component_level_change_no_change(self) -> None: |
| 788 | + """Test component-level change is False when configs are identical.""" |
| 789 | + config: dict[str, Any] = { |
| 790 | + "log_level": "info", |
| 791 | + "camera": {"cam1": {"host": "192.168.1.1"}}, |
| 792 | + } |
| 793 | + change = ComponentChange( |
| 794 | + component_name="ffmpeg", |
| 795 | + old_config=config.copy(), |
| 796 | + new_config=config.copy(), |
| 797 | + ) |
| 798 | + assert change.is_component_level_change is False |
| 799 | + |
| 800 | + def test_get_domain_configs(self) -> None: |
| 801 | + """Test get_domain_configs extracts only domain keys from configs.""" |
| 802 | + change = ComponentChange( |
| 803 | + component_name="darknet", |
| 804 | + old_config={ |
| 805 | + "log_level": "info", |
| 806 | + "object_detector": {"threshold": 0.5}, |
| 807 | + "camera": {"cam1": {}}, |
| 808 | + }, |
| 809 | + new_config={ |
| 810 | + "log_level": "debug", |
| 811 | + "object_detector": {"threshold": 0.9}, |
| 812 | + "motion_detector": {"area": 100}, |
| 813 | + }, |
| 814 | + ) |
| 815 | + old_domains, new_domains = change.get_domain_configs() |
| 816 | + assert old_domains == { |
| 817 | + "object_detector": {"threshold": 0.5}, |
| 818 | + "camera": {"cam1": {}}, |
| 819 | + } |
| 820 | + assert new_domains == { |
| 821 | + "object_detector": {"threshold": 0.9}, |
| 822 | + "motion_detector": {"area": 100}, |
| 823 | + } |
| 824 | + |
| 825 | + def test_get_domain_configs_no_domains(self) -> None: |
| 826 | + """Test get_domain_configs returns empty dicts when no domain keys exist.""" |
| 827 | + change = ComponentChange( |
| 828 | + component_name="mqtt", |
| 829 | + old_config={"broker": "localhost", "port": 1883}, |
| 830 | + new_config={"broker": "10.0.0.1", "port": 1883}, |
| 831 | + ) |
| 832 | + old_domains, new_domains = change.get_domain_configs() |
| 833 | + assert not old_domains |
| 834 | + assert not new_domains |
| 835 | + |
| 836 | + def test_get_domain_configs_both_none(self) -> None: |
| 837 | + """Test get_domain_configs when both configs are None.""" |
| 838 | + change = ComponentChange( |
| 839 | + component_name="ffmpeg", |
| 840 | + old_config=None, |
| 841 | + new_config=None, |
| 842 | + ) |
| 843 | + old_domains, new_domains = change.get_domain_configs() |
| 844 | + assert not old_domains |
| 845 | + assert not new_domains |
| 846 | + |
| 847 | + def test_domain_changes_default_empty(self) -> None: |
| 848 | + """Test that domain_changes defaults to an empty list.""" |
| 849 | + change = ComponentChange( |
| 850 | + component_name="ffmpeg", |
| 851 | + old_config={}, |
| 852 | + new_config={}, |
| 853 | + ) |
| 854 | + assert not change.domain_changes |
| 855 | + |
| 856 | + |
| 857 | +class TestConfigDiff: |
| 858 | + """Test ConfigDiff dataclass.""" |
| 859 | + |
| 860 | + def test_has_changes_empty(self) -> None: |
| 861 | + """Test has_changes is False for empty ConfigDiff.""" |
| 862 | + diff = ConfigDiff() |
| 863 | + assert diff.has_changes is False |
| 864 | + |
| 865 | + def test_has_changes_with_entries(self) -> None: |
| 866 | + """Test has_changes is True when component_changes are present.""" |
| 867 | + diff = ConfigDiff( |
| 868 | + component_changes={ |
| 869 | + "ffmpeg": ComponentChange("ffmpeg", None, {"camera": {}}), |
| 870 | + } |
| 871 | + ) |
| 872 | + assert diff.has_changes is True |
| 873 | + |
| 874 | + def test_get_added_removed_modified(self) -> None: |
| 875 | + """Test get_added/removed/modified_components categorization.""" |
| 876 | + diff = ConfigDiff( |
| 877 | + component_changes={ |
| 878 | + "new_comp": ComponentChange("new_comp", None, {"key": "val"}), |
| 879 | + "old_comp": ComponentChange("old_comp", {"key": "val"}, None), |
| 880 | + "mod_comp": ComponentChange("mod_comp", {"key": "old"}, {"key": "new"}), |
| 881 | + } |
| 882 | + ) |
| 883 | + assert diff.get_added_components() == ["new_comp"] |
| 884 | + assert diff.get_removed_components() == ["old_comp"] |
| 885 | + assert diff.get_modified_components() == ["mod_comp"] |
| 886 | + |
| 887 | + def test_get_component_change(self) -> None: |
| 888 | + """Test get_component_change returns the correct entry or None.""" |
| 889 | + change = ComponentChange("ffmpeg", None, {"camera": {}}) |
| 890 | + diff = ConfigDiff(component_changes={"ffmpeg": change}) |
| 891 | + assert diff.get_component_change("ffmpeg") is change |
| 892 | + assert diff.get_component_change("nonexistent") is None |
| 893 | + |
| 894 | + |
| 895 | +class TestDiffConfig: |
| 896 | + """Test diff_config function.""" |
| 897 | + |
| 898 | + def test_no_changes(self) -> None: |
| 899 | + """Test diff when old and new configs are identical.""" |
| 900 | + config: dict[str, Any] = { |
| 901 | + "ffmpeg": {"camera": {"cam1": {}}}, |
| 902 | + "mqtt": {"broker": "localhost"}, |
| 903 | + } |
| 904 | + result = diff_config(config, config.copy()) |
| 905 | + assert not result.has_changes |
| 906 | + |
| 907 | + def test_component_added(self) -> None: |
| 908 | + """Test diff detects a newly added component.""" |
| 909 | + old: dict[str, Any] = {"ffmpeg": {"camera": {"cam1": {}}}} |
| 910 | + new: dict[str, Any] = { |
| 911 | + "ffmpeg": {"camera": {"cam1": {}}}, |
| 912 | + "mqtt": {"broker": "localhost"}, |
| 913 | + } |
| 914 | + result = diff_config(old, new) |
| 915 | + assert result.get_added_components() == ["mqtt"] |
| 916 | + change = result.get_component_change("mqtt") |
| 917 | + assert change is not None |
| 918 | + assert change.old_config is None |
| 919 | + assert change.new_config == {"broker": "localhost"} |
| 920 | + |
| 921 | + def test_component_removed(self) -> None: |
| 922 | + """Test diff detects a removed component.""" |
| 923 | + old: dict[str, Any] = { |
| 924 | + "ffmpeg": {"camera": {"cam1": {}}}, |
| 925 | + "mqtt": {"broker": "localhost"}, |
| 926 | + } |
| 927 | + new: dict[str, Any] = {"ffmpeg": {"camera": {"cam1": {}}}} |
| 928 | + result = diff_config(old, new) |
| 929 | + assert result.get_removed_components() == ["mqtt"] |
| 930 | + change = result.get_component_change("mqtt") |
| 931 | + assert change is not None |
| 932 | + assert change.old_config == {"broker": "localhost"} |
| 933 | + assert change.new_config is None |
| 934 | + |
| 935 | + def test_component_modified(self) -> None: |
| 936 | + """Test diff detects a modified component.""" |
| 937 | + old: dict[str, Any] = {"mqtt": {"broker": "localhost"}} |
| 938 | + new: dict[str, Any] = {"mqtt": {"broker": "10.0.0.1"}} |
| 939 | + result = diff_config(old, new) |
| 940 | + assert result.get_modified_components() == ["mqtt"] |
| 941 | + change = result.get_component_change("mqtt") |
| 942 | + assert change is not None |
| 943 | + assert change.old_config == {"broker": "localhost"} |
| 944 | + assert change.new_config == {"broker": "10.0.0.1"} |
| 945 | + |
| 946 | + def test_multiple_changes(self) -> None: |
| 947 | + """Test diff with added, removed, and modified components at once.""" |
| 948 | + old: dict[str, Any] = { |
| 949 | + "ffmpeg": {"camera": {"cam1": {}}}, |
| 950 | + "mqtt": {"broker": "localhost"}, |
| 951 | + } |
| 952 | + new: dict[str, Any] = { |
| 953 | + "ffmpeg": {"camera": {"cam1": {}, "cam2": {}}}, |
| 954 | + "darknet": {"object_detector": {"threshold": 0.5}}, |
| 955 | + } |
| 956 | + result = diff_config(old, new) |
| 957 | + assert "ffmpeg" in result.get_modified_components() |
| 958 | + assert "mqtt" in result.get_removed_components() |
| 959 | + assert "darknet" in result.get_added_components() |
| 960 | + |
| 961 | + def test_both_empty(self) -> None: |
| 962 | + """Test diff with empty configs.""" |
| 963 | + result = diff_config({}, {}) |
| 964 | + assert not result.has_changes |
| 965 | + |
| 966 | + def test_deep_copies_configs(self) -> None: |
| 967 | + """Test that diff_config deep copies config values.""" |
| 968 | + old: dict[str, Any] = {"ffmpeg": {"camera": {"cam1": {"host": "192.168.1.1"}}}} |
| 969 | + new: dict[str, Any] = {"mqtt": {"broker": "localhost"}} |
| 970 | + result = diff_config(old, new) |
| 971 | + |
| 972 | + old["ffmpeg"]["camera"]["cam1"]["host"] = "MUTATED" |
| 973 | + new["mqtt"]["broker"] = "MUTATED" |
| 974 | + ffmpeg = result.get_component_change("ffmpeg") |
| 975 | + assert ffmpeg and ffmpeg.old_config == { |
| 976 | + "camera": {"cam1": {"host": "192.168.1.1"}} |
| 977 | + } |
| 978 | + mqtt = result.get_component_change("mqtt") |
| 979 | + assert mqtt and mqtt.new_config == {"broker": "localhost"} |
| 980 | + |
| 981 | + def test_unchanged_components_not_included(self) -> None: |
| 982 | + """Test that components with no changes are excluded from the result.""" |
| 983 | + old: dict[str, Any] = { |
| 984 | + "ffmpeg": {"camera": {"cam1": {}}}, |
| 985 | + "mqtt": {"broker": "localhost"}, |
| 986 | + } |
| 987 | + new: dict[str, Any] = { |
| 988 | + "ffmpeg": {"camera": {"cam1": {}}}, |
| 989 | + "mqtt": {"broker": "10.0.0.1"}, |
| 990 | + } |
| 991 | + result = diff_config(old, new) |
| 992 | + assert result.get_component_change("ffmpeg") is None |
| 993 | + assert result.get_component_change("mqtt") is not None |
0 commit comments