From 976c4d52cb529024400910da02cdb8e90bd92054 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 28 May 2025 15:39:52 +0200 Subject: [PATCH] Tests(inventoryStore): add tests for intersecting nix/json lists --- .../clan_lib/persist/fixtures/lists.json | 1 + .../clan_lib/persist/fixtures/lists.nix | 32 ++++++++ .../clan_lib/persist/inventory_store_test.py | 78 ++++++++++++++++++- pkgs/clan-cli/clan_lib/persist/util.py | 15 +++- 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 pkgs/clan-cli/clan_lib/persist/fixtures/lists.json create mode 100644 pkgs/clan-cli/clan_lib/persist/fixtures/lists.nix diff --git a/pkgs/clan-cli/clan_lib/persist/fixtures/lists.json b/pkgs/clan-cli/clan_lib/persist/fixtures/lists.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/pkgs/clan-cli/clan_lib/persist/fixtures/lists.json @@ -0,0 +1 @@ +{} diff --git a/pkgs/clan-cli/clan_lib/persist/fixtures/lists.nix b/pkgs/clan-cli/clan_lib/persist/fixtures/lists.nix new file mode 100644 index 000000000..8fa2a8852 --- /dev/null +++ b/pkgs/clan-cli/clan_lib/persist/fixtures/lists.nix @@ -0,0 +1,32 @@ +{ clanLib, lib, ... }: +let + eval = lib.evalModules { + modules = [ + { + # Trying to write into the default + options.empty = lib.mkOption { + type = lib.types.listOf lib.types.str; + }; + options.predefined = lib.mkOption { + type = lib.types.listOf lib.types.str; + }; + } + { + empty = [ ]; + predefined = [ + "a" + "b" + ]; + } + + # Merge the "inventory.json" + (builtins.fromJSON (builtins.readFile ./lists.json)) + ]; + }; +in +{ + clanInternals.inventoryClass.inventory = eval.config; + clanInternals.inventoryClass.introspection = clanLib.introspection.getPrios { + options = eval.options; + }; +} diff --git a/pkgs/clan-cli/clan_lib/persist/inventory_store_test.py b/pkgs/clan-cli/clan_lib/persist/inventory_store_test.py index 85d96e25d..93c729846 100644 --- a/pkgs/clan-cli/clan_lib/persist/inventory_store_test.py +++ b/pkgs/clan-cli/clan_lib/persist/inventory_store_test.py @@ -144,6 +144,7 @@ def test_read_deferred(setup_test_files: Path) -> None: store = InventoryStore( flake=MockFlake(nix_file), inventory_file_name=json_file.name, + # Needed to allow auto-transforming deferred modules _allowed_path_transforms=["foo.*"], _keys=[], # disable toplevel filtering ) @@ -168,7 +169,82 @@ def test_read_deferred(setup_test_files: Path) -> None: assert store.read() == {"foo": {"a": {}, "b": {}, "c": {"timeout": "1s"}}} - # Remove the "deferredModule" "C" along with its settings + # Remove the "deferredModle" "C" along with its settings delete_by_path(data, "foo.c") # type: ignore store.write(data, "test", commit=False) assert store.read() == {"foo": {"a": {}, "b": {}}} + + +@pytest.mark.with_core +@pytest.mark.parametrize("setup_test_files", ["lists.nix"], indirect=True) +def test_manipulate_list(setup_test_files: Path) -> None: + files = list(setup_test_files.iterdir()) + nix_file = next(f for f in files if f.suffix == ".nix") + json_file = next(f for f in files if f.suffix == ".json") + + assert nix_file.exists() + assert json_file.exists() + + store = InventoryStore( + flake=MockFlake(nix_file), + inventory_file_name=json_file.name, + _keys=[], # disable toplevel filtering + ) + + data = store.read() + + # [a, b] are static items in the list + assert data == {"empty": [], "predefined": ["a", "b"]} + + # Add a new item to the list + set_value_by_path(data, "predefined", ["a", "b", "c"]) + store.write(data, "test", commit=False) + + assert store.read() == {"empty": [], "predefined": ["c", "a", "b"]} + + # Remove an item from the list + set_value_by_path(data, "predefined", ["a", "b"]) + store.write(data, "test", commit=False) + assert store.read() == {"empty": [], "predefined": ["a", "b"]} + + # Test adding more than one of the same item + set_value_by_path(data, "empty", ["a", "b", "a"]) + + with pytest.raises(ClanError) as e: + store.write(data, "test", commit=False) + assert ( + str(e.value) + == "Key 'empty' contains list duplicates: ['a'] - List values must be unique." + ) + + assert store.read() == {"empty": [], "predefined": ["a", "b"]} + + +@pytest.mark.with_core +@pytest.mark.parametrize("setup_test_files", ["lists.nix"], indirect=True) +def test_static_list_items(setup_test_files: Path) -> None: + files = list(setup_test_files.iterdir()) + nix_file = next(f for f in files if f.suffix == ".nix") + json_file = next(f for f in files if f.suffix == ".json") + + assert nix_file.exists() + assert json_file.exists() + + store = InventoryStore( + flake=MockFlake(nix_file), + inventory_file_name=json_file.name, + _keys=[], # disable toplevel filtering + ) + + data = store.read() + assert data == {"empty": [], "predefined": ["a", "b"]} + + # Removing a nix defined item from the list throws an error + set_value_by_path(data, "predefined", ["b"]) + with pytest.raises(ClanError) as e: + store.write(data, "test", commit=False) + + assert ( + str(e.value) + == "Key 'predefined' doesn't contain items ['a'] - Deleting them is not possible, they are static values set via a .nix file" + ) diff --git a/pkgs/clan-cli/clan_lib/persist/util.py b/pkgs/clan-cli/clan_lib/persist/util.py index 3c389630e..ada021490 100644 --- a/pkgs/clan-cli/clan_lib/persist/util.py +++ b/pkgs/clan-cli/clan_lib/persist/util.py @@ -226,13 +226,26 @@ After: {new} if isinstance(new, list): duplicates = find_duplicates(new) if duplicates: - msg = f"Key '{key}' contains duplicates: {duplicates}. This not supported yet." + msg = f"Key '{key}' contains list duplicates: {duplicates} - List values must be unique." raise ClanError(msg) # List of current values persisted_data = data_dyn.get(key, []) # List including nix values all_list = data_all.get(key, []) nix_list = unmerge_lists(all_list, persisted_data) + + # every item in nix_list MUST be in new + nix_items_to_remove = list( + filter(lambda item: item not in new, nix_list) + ) + + if nix_items_to_remove: + msg = ( + f"Key '{key}' doesn't contain items {nix_items_to_remove} - " + "Deleting them is not possible, they are static values set via a .nix file" + ) + raise ClanError(msg) + if new != all_list: patchset[key] = unmerge_lists(new, nix_list) else: