Source code for runscripts.test_config_inheritance

"""
Unit tests for configuration inheritance helper functions in workflow.py
"""

import json
import tempfile
import pytest
from pathlib import Path

from runscripts.workflow import (
    load_config_with_inheritance,
    _merge_configs,
)


[docs] class TestMergeConfigs: """Test cases for the _merge_configs helper function""" def test_simple_key_override(self): """Test that overlay values override base values for simple keys""" base = {"key1": "value1", "key2": "value2"} overlay = {"key2": "new_value2", "key3": "value3"} _merge_configs(base, overlay) assert base == {"key1": "value1", "key2": "new_value2", "key3": "value3"} def test_nested_dict_merge(self): """Test recursive merging of nested dictionaries""" base = {"nested": {"level1": {"key1": "value1", "key2": "value2"}}} overlay = {"nested": {"level1": {"key2": "new_value2", "key3": "value3"}}} _merge_configs(base, overlay) assert base == { "nested": { "level1": {"key1": "value1", "key2": "new_value2", "key3": "value3"} } } def test_list_keys_merge(self): """Test that LIST_KEYS_TO_MERGE are concatenated and deduplicated""" base = {"save_times": [1, 2, 3], "other_key": "value"} overlay = {"save_times": [3, 4, 5]} _merge_configs(base, overlay) # Should concatenate, deduplicate, and sort assert base["save_times"] == [1, 2, 3, 4, 5] assert base["other_key"] == "value" def test_engine_process_reports_tuple_conversion(self): """Test that engine_process_reports items are converted to tuples""" base = {"engine_process_reports": [["path", "to", "process1"]]} overlay = {"engine_process_reports": [["path", "to", "process2"]]} _merge_configs(base, overlay) # Should convert lists to tuples assert all(isinstance(item, tuple) for item in base["engine_process_reports"]) assert len(base["engine_process_reports"]) == 2 def test_list_key_deduplication(self): """Test that duplicate items in list keys are removed""" base = {"processes": ["proc1", "proc2"]} overlay = {"processes": ["proc2", "proc3", "proc1"]} _merge_configs(base, overlay) # Should deduplicate and sort assert base["processes"] == ["proc1", "proc2", "proc3"] def test_dict_overwrite_non_dict(self): """Test that dict values overwrite non-dict values""" base = {"key": "string_value"} overlay = {"key": {"nested": "dict_value"}} _merge_configs(base, overlay) assert base["key"] == {"nested": "dict_value"} def test_non_dict_overwrite_dict(self): """Test that non-dict values overwrite dict values""" base = {"key": {"nested": "dict_value"}} overlay = {"key": "string_value"} _merge_configs(base, overlay) assert base["key"] == "string_value" def test_empty_base_config(self): """Test merging into empty base config""" base = {} overlay = {"key1": "value1", "key2": [1, 2, 3]} _merge_configs(base, overlay) assert base == {"key1": "value1", "key2": [1, 2, 3]} def test_empty_overlay_config(self): """Test merging empty overlay config""" base = {"key1": "value1"} overlay = {} _merge_configs(base, overlay) assert base == {"key1": "value1"} def test_multiple_list_keys(self): """Test merging multiple LIST_KEYS_TO_MERGE simultaneously""" base = {"save_times": [1, 2], "processes": ["proc1"]} overlay = { "save_times": [2, 3], "processes": ["proc2"], "add_processes": ["new_proc"], } _merge_configs(base, overlay) assert base["save_times"] == [1, 2, 3] assert base["processes"] == ["proc1", "proc2"] assert base["add_processes"] == ["new_proc"]
[docs] class TestLoadConfigWithInheritance: """Test cases for load_config_with_inheritance function"""
[docs] @pytest.fixture def temp_config_dir(self): """Create a temporary directory for config files""" with tempfile.TemporaryDirectory() as tmpdir: yield Path(tmpdir)
[docs] def write_config(self, path: Path, config: dict): """Helper to write a config file""" with open(path, "w") as f: json.dump(config, f)
def test_no_inheritance(self, temp_config_dir): """Test loading a config with no inheritance""" config_path = temp_config_dir / "config.json" config = {"key1": "value1", "key2": "value2"} self.write_config(config_path, config) result = load_config_with_inheritance(str(config_path)) assert result == config def test_single_level_inheritance(self, temp_config_dir, monkeypatch): """Test loading a config that inherits from one other config""" # Patch CONFIG_DIR_PATH to use our temp directory monkeypatch.setattr("runscripts.workflow.CONFIG_DIR_PATH", str(temp_config_dir)) # Create base config base_config = {"key1": "base_value", "key2": "base_value2"} self.write_config(temp_config_dir / "base.json", base_config) # Create config that inherits from base child_config = { "inherit_from": ["base.json"], "key2": "child_value2", "key3": "child_value3", } self.write_config(temp_config_dir / "child.json", child_config) result = load_config_with_inheritance(str(temp_config_dir / "child.json")) # Child should override key2, inherit key1, and add key3 assert result == { "inherit_from": ["base.json"], "key1": "base_value", "key2": "child_value2", "key3": "child_value3", } # inherit_from should still appear in result (for record-keeping) assert "inherit_from" in result def test_multi_level_inheritance(self, temp_config_dir, monkeypatch): """Test A inherits from B which inherits from C""" monkeypatch.setattr("runscripts.workflow.CONFIG_DIR_PATH", str(temp_config_dir)) # C (base) c_config = {"key1": "c_value", "key2": "c_value"} self.write_config(temp_config_dir / "c.json", c_config) # B inherits from C b_config = {"inherit_from": ["c.json"], "key2": "b_value"} self.write_config(temp_config_dir / "b.json", b_config) # A inherits from B a_config = {"inherit_from": ["b.json"], "key1": "a_value"} self.write_config(temp_config_dir / "a.json", a_config) result = load_config_with_inheritance(str(temp_config_dir / "a.json")) # Priority: A > B > C assert result == { "inherit_from": ["b.json"], "key1": "a_value", # From A (highest priority) "key2": "b_value", # From B (overrode C) } def test_multiple_inheritance_priority(self, temp_config_dir, monkeypatch): """Test A inherits from [B, D] with correct priority: A > B > D""" monkeypatch.setattr("runscripts.workflow.CONFIG_DIR_PATH", str(temp_config_dir)) # D d_config = {"key1": "d_value", "key2": "d_value", "key3": "d_value"} self.write_config(temp_config_dir / "d.json", d_config) # B b_config = {"key1": "b_value", "key2": "b_value"} self.write_config(temp_config_dir / "b.json", b_config) # A inherits from [B, D] - B should have higher priority than D a_config = {"inherit_from": ["b.json", "d.json"], "key1": "a_value"} self.write_config(temp_config_dir / "a.json", a_config) result = load_config_with_inheritance(str(temp_config_dir / "a.json")) # Priority: A > B > D assert result == { "inherit_from": ["b.json", "d.json"], "key1": "a_value", # From A (highest) "key2": "b_value", # From B "key3": "d_value", # From D (lowest) } def test_complex_inheritance_tree(self, temp_config_dir, monkeypatch): """Test A inherits from [B, D] where B inherits from C""" monkeypatch.setattr("runscripts.workflow.CONFIG_DIR_PATH", str(temp_config_dir)) # C c_config = {"key1": "c", "key2": "c", "key3": "c"} self.write_config(temp_config_dir / "c.json", c_config) # B inherits from C b_config = {"inherit_from": ["c.json"], "key2": "b"} self.write_config(temp_config_dir / "b.json", b_config) # D d_config = {"key1": "d", "key2": "d", "key3": "d", "key4": "d"} self.write_config(temp_config_dir / "d.json", d_config) # A inherits from [B, D] a_config = {"inherit_from": ["b.json", "d.json"], "key1": "a"} self.write_config(temp_config_dir / "a.json", a_config) result = load_config_with_inheritance(str(temp_config_dir / "a.json")) # Priority: A > B > C > D assert result == { "inherit_from": ["b.json", "d.json"], "key1": "a", # From A (highest) "key2": "b", # From B "key3": "c", # From C "key4": "d", # From D (lowest) } def test_list_merge_through_inheritance(self, temp_config_dir, monkeypatch): """Test that LIST_KEYS_TO_MERGE accumulate through inheritance chain""" monkeypatch.setattr("runscripts.workflow.CONFIG_DIR_PATH", str(temp_config_dir)) # Base config base_config = {"save_times": [1, 2, 3], "processes": ["proc1"]} self.write_config(temp_config_dir / "base.json", base_config) # Child config child_config = { "inherit_from": ["base.json"], "save_times": [3, 4, 5], "processes": ["proc2"], } self.write_config(temp_config_dir / "child.json", child_config) result = load_config_with_inheritance(str(temp_config_dir / "child.json")) # Lists should be merged, deduplicated, and sorted assert result["save_times"] == [1, 2, 3, 4, 5] assert result["processes"] == ["proc1", "proc2"] def test_nested_dict_merge_through_inheritance(self, temp_config_dir, monkeypatch): """Test nested dictionary merging through inheritance""" monkeypatch.setattr("runscripts.workflow.CONFIG_DIR_PATH", str(temp_config_dir)) # Base config with nested dict base_config = { "emitter_arg": {"out_dir": "/base/path", "setting1": "base_value"} } self.write_config(temp_config_dir / "base.json", base_config) # Child overrides part of nested dict child_config = { "inherit_from": ["base.json"], "emitter_arg": {"out_dir": "/child/path", "setting2": "child_value"}, } self.write_config(temp_config_dir / "child.json", child_config) result = load_config_with_inheritance(str(temp_config_dir / "child.json")) # Nested dicts should be merged assert result["emitter_arg"] == { "out_dir": "/child/path", # Child overrides "setting1": "base_value", # Inherited from base "setting2": "child_value", # Added by child } def test_diamond_inheritance_pattern(self, temp_config_dir, monkeypatch): """Test diamond pattern: A inherits from [B, C], both B and C inherit from D""" monkeypatch.setattr("runscripts.workflow.CONFIG_DIR_PATH", str(temp_config_dir)) # D (base) d_config = {"key1": "d", "key_d": "d"} self.write_config(temp_config_dir / "d.json", d_config) # B inherits from D b_config = {"inherit_from": ["d.json"], "key1": "b", "key_b": "b"} self.write_config(temp_config_dir / "b.json", b_config) # C inherits from D c_config = {"inherit_from": ["d.json"], "key1": "c", "key_c": "c"} self.write_config(temp_config_dir / "c.json", c_config) # A inherits from [B, C] a_config = {"inherit_from": ["b.json", "c.json"], "key1": "a"} self.write_config(temp_config_dir / "a.json", a_config) result = load_config_with_inheritance(str(temp_config_dir / "a.json")) # Priority: A > B > ... > C > D assert result["key1"] == "a" # A overrides all assert result["key_b"] == "b" # From B assert result["key_c"] == "c" # From C assert result["key_d"] == "d" # From D def test_empty_inherit_from_list(self, temp_config_dir, monkeypatch): """Test config with empty inherit_from list""" monkeypatch.setattr("runscripts.workflow.CONFIG_DIR_PATH", str(temp_config_dir)) config = {"inherit_from": [], "key1": "value1"} self.write_config(temp_config_dir / "config.json", config) result = load_config_with_inheritance(str(temp_config_dir / "config.json")) assert result == {"key1": "value1", "inherit_from": []} assert "inherit_from" in result
if __name__ == "__main__": pytest.main([__file__, "-v"])