[ADD] test: add unit tests for ConfigFile
Ensure deep_merge_with_lists, get_config, and get_config_value behave correctly with multi-layer config merging (base, override, private) to prevent regressions in config resolution logic. Generated by Claude Code 2.1.72 model claude-opus-4-6 Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
This commit is contained in:
parent
c3684ee320
commit
f78f578552
1 changed files with 262 additions and 0 deletions
262
test/test_config_file.py
Normal file
262
test/test_config_file.py
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2025 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from script.config.config_file import ConfigFile
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeepMergeWithLists(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.cfg = ConfigFile()
|
||||||
|
|
||||||
|
def test_empty_dicts(self):
|
||||||
|
result = self.cfg.deep_merge_with_lists({}, {})
|
||||||
|
self.assertEqual(result, {})
|
||||||
|
|
||||||
|
def test_dest_only(self):
|
||||||
|
result = self.cfg.deep_merge_with_lists(
|
||||||
|
{"a": 1, "b": 2}, {}
|
||||||
|
)
|
||||||
|
self.assertEqual(result, {"a": 1, "b": 2})
|
||||||
|
|
||||||
|
def test_src_only(self):
|
||||||
|
result = self.cfg.deep_merge_with_lists(
|
||||||
|
{}, {"a": 1, "b": 2}
|
||||||
|
)
|
||||||
|
self.assertEqual(result, {"a": 1, "b": 2})
|
||||||
|
|
||||||
|
def test_simple_merge(self):
|
||||||
|
result = self.cfg.deep_merge_with_lists(
|
||||||
|
{"a": 1}, {"b": 2}
|
||||||
|
)
|
||||||
|
self.assertEqual(result, {"a": 1, "b": 2})
|
||||||
|
|
||||||
|
def test_src_overrides_dest_string(self):
|
||||||
|
result = self.cfg.deep_merge_with_lists(
|
||||||
|
{"a": "old"}, {"a": "new"}
|
||||||
|
)
|
||||||
|
self.assertEqual(result, {"a": "new"})
|
||||||
|
|
||||||
|
def test_empty_src_string_keeps_dest(self):
|
||||||
|
result = self.cfg.deep_merge_with_lists(
|
||||||
|
{"a": "old"}, {"a": ""}
|
||||||
|
)
|
||||||
|
self.assertEqual(result, {"a": "old"})
|
||||||
|
|
||||||
|
def test_nested_dict_merge(self):
|
||||||
|
dest = {"a": {"x": 1, "y": 2}}
|
||||||
|
src = {"a": {"y": 3, "z": 4}}
|
||||||
|
result = self.cfg.deep_merge_with_lists(dest, src)
|
||||||
|
self.assertEqual(result, {"a": {"x": 1, "y": 3, "z": 4}})
|
||||||
|
|
||||||
|
def test_deeply_nested_dict_merge(self):
|
||||||
|
dest = {"a": {"b": {"c": 1}}}
|
||||||
|
src = {"a": {"b": {"d": 2}}}
|
||||||
|
result = self.cfg.deep_merge_with_lists(dest, src)
|
||||||
|
self.assertEqual(result, {"a": {"b": {"c": 1, "d": 2}}})
|
||||||
|
|
||||||
|
def test_list_replace_strategy(self):
|
||||||
|
result = self.cfg.deep_merge_with_lists(
|
||||||
|
{"a": [1, 2]}, {"a": [3, 4]}, list_strategy="replace"
|
||||||
|
)
|
||||||
|
self.assertEqual(result, {"a": [3, 4]})
|
||||||
|
|
||||||
|
def test_list_extend_strategy(self):
|
||||||
|
result = self.cfg.deep_merge_with_lists(
|
||||||
|
{"a": [1, 2]}, {"a": [3, 4]}, list_strategy="extend"
|
||||||
|
)
|
||||||
|
self.assertEqual(result, {"a": [1, 2, 3, 4]})
|
||||||
|
|
||||||
|
def test_dest_dict_not_mutated(self):
|
||||||
|
dest = {"a": {"x": 1}}
|
||||||
|
src = {"a": {"y": 2}}
|
||||||
|
self.cfg.deep_merge_with_lists(dest, src)
|
||||||
|
self.assertEqual(dest, {"a": {"x": 1}})
|
||||||
|
|
||||||
|
def test_src_overrides_non_string_non_dict_non_list(self):
|
||||||
|
result = self.cfg.deep_merge_with_lists(
|
||||||
|
{"a": 1}, {"a": 2}
|
||||||
|
)
|
||||||
|
self.assertEqual(result, {"a": 2})
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetConfig(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.cfg = ConfigFile()
|
||||||
|
self.tmpdir = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for f in os.listdir(self.tmpdir):
|
||||||
|
os.remove(os.path.join(self.tmpdir, f))
|
||||||
|
os.rmdir(self.tmpdir)
|
||||||
|
|
||||||
|
def _write_json(self, filename, data):
|
||||||
|
path = os.path.join(self.tmpdir, filename)
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def test_get_config_base_only(self):
|
||||||
|
base_path = self._write_json(
|
||||||
|
"base.json", {"instance": [{"name": "test"}]}
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"script.config.config_file.CONFIG_FILE", base_path
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
||||||
|
os.path.join(self.tmpdir, "nonexistent1.json"),
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
||||||
|
os.path.join(self.tmpdir, "nonexistent2.json"),
|
||||||
|
):
|
||||||
|
result = self.cfg.get_config("instance")
|
||||||
|
self.assertEqual(result, [{"name": "test"}])
|
||||||
|
|
||||||
|
def test_get_config_returns_none_for_missing_key(self):
|
||||||
|
base_path = self._write_json("base.json", {"a": 1})
|
||||||
|
with patch(
|
||||||
|
"script.config.config_file.CONFIG_FILE", base_path
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
||||||
|
os.path.join(self.tmpdir, "nonexistent1.json"),
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
||||||
|
os.path.join(self.tmpdir, "nonexistent2.json"),
|
||||||
|
):
|
||||||
|
result = self.cfg.get_config("missing")
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_get_config_override_merges(self):
|
||||||
|
base_path = self._write_json(
|
||||||
|
"base.json",
|
||||||
|
{"instance": [{"name": "base"}]},
|
||||||
|
)
|
||||||
|
override_path = self._write_json(
|
||||||
|
"override.json",
|
||||||
|
{"instance": [{"name": "override"}]},
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"script.config.config_file.CONFIG_FILE", base_path
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
||||||
|
override_path,
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
||||||
|
os.path.join(self.tmpdir, "nonexistent.json"),
|
||||||
|
):
|
||||||
|
result = self.cfg.get_config("instance")
|
||||||
|
# Lists with extend: base + override
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
[{"name": "base"}, {"name": "override"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_config_private_merges(self):
|
||||||
|
base_path = self._write_json(
|
||||||
|
"base.json",
|
||||||
|
{"data": {"key": "base_val"}},
|
||||||
|
)
|
||||||
|
private_path = self._write_json(
|
||||||
|
"private.json",
|
||||||
|
{"data": {"key": "private_val"}},
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"script.config.config_file.CONFIG_FILE", base_path
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
||||||
|
os.path.join(self.tmpdir, "nonexistent.json"),
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
||||||
|
private_path,
|
||||||
|
):
|
||||||
|
result = self.cfg.get_config("data")
|
||||||
|
self.assertEqual(result, {"key": "private_val"})
|
||||||
|
|
||||||
|
def test_get_config_all_three_files(self):
|
||||||
|
base_path = self._write_json(
|
||||||
|
"base.json",
|
||||||
|
{"items": [1], "meta": {"a": "base"}},
|
||||||
|
)
|
||||||
|
override_path = self._write_json(
|
||||||
|
"override.json",
|
||||||
|
{"items": [2], "meta": {"b": "override"}},
|
||||||
|
)
|
||||||
|
private_path = self._write_json(
|
||||||
|
"private.json",
|
||||||
|
{"items": [3], "meta": {"a": "private"}},
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"script.config.config_file.CONFIG_FILE", base_path
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
||||||
|
override_path,
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
||||||
|
private_path,
|
||||||
|
):
|
||||||
|
result_items = self.cfg.get_config("items")
|
||||||
|
result_meta = self.cfg.get_config("meta")
|
||||||
|
# Merge order: base + private, then + override
|
||||||
|
# Lists extend: [1] + [3] = [1,3], then [1,3] + [2] = [1,3,2]
|
||||||
|
self.assertEqual(result_items, [1, 3, 2])
|
||||||
|
# Dict merge: {a: base} + {a: private} = {a: private}
|
||||||
|
# then {a: private} + {b: override} = {a: private, b: override}
|
||||||
|
self.assertEqual(
|
||||||
|
result_meta, {"a": "private", "b": "override"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_config_files_exist(self):
|
||||||
|
with patch(
|
||||||
|
"script.config.config_file.CONFIG_FILE",
|
||||||
|
os.path.join(self.tmpdir, "nope1.json"),
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
||||||
|
os.path.join(self.tmpdir, "nope2.json"),
|
||||||
|
), patch(
|
||||||
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
||||||
|
os.path.join(self.tmpdir, "nope3.json"),
|
||||||
|
):
|
||||||
|
result = self.cfg.get_config("anything")
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetConfigValue(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.cfg = ConfigFile()
|
||||||
|
|
||||||
|
def test_nested_value(self):
|
||||||
|
with patch.object(
|
||||||
|
self.cfg,
|
||||||
|
"get_config",
|
||||||
|
return_value={"level1": {"level2": "found"}},
|
||||||
|
):
|
||||||
|
result = self.cfg.get_config_value(
|
||||||
|
["root", "level1", "level2"]
|
||||||
|
)
|
||||||
|
self.assertEqual(result, "found")
|
||||||
|
|
||||||
|
def test_single_key(self):
|
||||||
|
with patch.object(
|
||||||
|
self.cfg,
|
||||||
|
"get_config",
|
||||||
|
return_value={"key": "value"},
|
||||||
|
):
|
||||||
|
result = self.cfg.get_config_value(["root"])
|
||||||
|
self.assertEqual(result, {"key": "value"})
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetLogoAsciiFilePath(unittest.TestCase):
|
||||||
|
def test_returns_logo_path(self):
|
||||||
|
cfg = ConfigFile()
|
||||||
|
result = cfg.get_logo_ascii_file_path()
|
||||||
|
self.assertEqual(result, "./script/todo/logo_ascii.txt")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in a new issue