[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