mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-07 03:15:56 +00:00
- new utils.inventory.bundle_apps resolver aggregates and dedupes role
groups from inventories/bundles/{servers,workstations}/<name>/inventory.yml
- new make targets deploy-bundles (fresh-purged) and redeploy-bundles
(reuse-kept), driven by BUNDLES=... env, wrapped by
scripts/tests/deploy/local/deploy/{bundles,redeploy-bundles}.sh
- unit tests for the resolver under tests/unit/utils/inventory/
- document the new targets in docs/contributing/tools/make.md,
docs/contributing/actions/testing.md, inventories/bundles/README.md
- collapse multi-line Makefile comments to one-liners for consistency
- disable nextcloud bbb plugin auto-enable (commented out)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
177 lines
6.0 KiB
Python
177 lines
6.0 KiB
Python
"""Unit tests for `utils.inventory.bundle_apps`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import unittest
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from unittest.mock import patch
|
|
|
|
from utils.cache.yaml import _reset_cache_for_tests
|
|
from utils.inventory import bundle_apps
|
|
|
|
|
|
class TestSplitCsv(unittest.TestCase):
|
|
def test_splits_and_trims(self):
|
|
self.assertEqual(
|
|
bundle_apps._split_csv(" a , b,c "),
|
|
["a", "b", "c"],
|
|
)
|
|
|
|
def test_drops_empty_segments(self):
|
|
self.assertEqual(bundle_apps._split_csv("a,,b,"), ["a", "b"])
|
|
|
|
def test_empty_string(self):
|
|
self.assertEqual(bundle_apps._split_csv(""), [])
|
|
|
|
|
|
class _BundleFsMixin:
|
|
"""Build a fake inventories/bundles tree and patch SEARCH_DIRS."""
|
|
|
|
def setUp(self) -> None: # type: ignore[override]
|
|
_reset_cache_for_tests()
|
|
self._tmp = TemporaryDirectory()
|
|
self.addCleanup(self._tmp.cleanup) # type: ignore[attr-defined]
|
|
root = Path(self._tmp.name)
|
|
self.servers = root / "servers"
|
|
self.workstations = root / "workstations"
|
|
self.servers.mkdir()
|
|
self.workstations.mkdir()
|
|
patcher = patch.object(
|
|
bundle_apps,
|
|
"SEARCH_DIRS",
|
|
(self.servers, self.workstations),
|
|
)
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop) # type: ignore[attr-defined]
|
|
|
|
def write_bundle(self, base: Path, name: str, apps: list[str]) -> Path:
|
|
bundle_dir = base / name
|
|
bundle_dir.mkdir()
|
|
inv = bundle_dir / "inventory.yml"
|
|
children = "".join(f" {a}:\n" for a in apps) or " {}\n"
|
|
inv.write_text(f"all:\n children:\n{children}", encoding="utf-8")
|
|
return inv
|
|
|
|
|
|
class TestLocate(_BundleFsMixin, unittest.TestCase):
|
|
def test_finds_in_servers(self):
|
|
self.write_bundle(self.servers, "alpha", ["a"])
|
|
self.assertEqual(
|
|
bundle_apps._locate("alpha"),
|
|
self.servers / "alpha" / "inventory.yml",
|
|
)
|
|
|
|
def test_finds_in_workstations(self):
|
|
self.write_bundle(self.workstations, "beta", ["b"])
|
|
self.assertEqual(
|
|
bundle_apps._locate("beta"),
|
|
self.workstations / "beta" / "inventory.yml",
|
|
)
|
|
|
|
def test_servers_wins_over_workstations(self):
|
|
self.write_bundle(self.servers, "dup", ["s"])
|
|
self.write_bundle(self.workstations, "dup", ["w"])
|
|
self.assertEqual(
|
|
bundle_apps._locate("dup"),
|
|
self.servers / "dup" / "inventory.yml",
|
|
)
|
|
|
|
def test_missing_returns_none(self):
|
|
self.assertIsNone(bundle_apps._locate("ghost"))
|
|
|
|
|
|
class TestResolve(_BundleFsMixin, unittest.TestCase):
|
|
def test_returns_apps_in_declaration_order(self):
|
|
self.write_bundle(self.servers, "edu", ["app_b", "app_a", "app_c"])
|
|
self.assertEqual(
|
|
bundle_apps.resolve(["edu"]),
|
|
["app_b", "app_a", "app_c"],
|
|
)
|
|
|
|
def test_deduplicates_across_bundles(self):
|
|
self.write_bundle(self.servers, "one", ["shared", "uniq1"])
|
|
self.write_bundle(self.workstations, "two", ["uniq2", "shared"])
|
|
self.assertEqual(
|
|
bundle_apps.resolve(["one", "two"]),
|
|
["shared", "uniq1", "uniq2"],
|
|
)
|
|
|
|
def test_missing_bundle_raises(self):
|
|
self.write_bundle(self.servers, "present", ["a"])
|
|
with self.assertRaises(FileNotFoundError) as ctx:
|
|
bundle_apps.resolve(["present", "absent"])
|
|
self.assertIn("absent", str(ctx.exception))
|
|
self.assertNotIn("present", str(ctx.exception))
|
|
|
|
def test_multiple_missing_listed(self):
|
|
with self.assertRaises(FileNotFoundError) as ctx:
|
|
bundle_apps.resolve(["x", "y"])
|
|
msg = str(ctx.exception)
|
|
self.assertIn("x", msg)
|
|
self.assertIn("y", msg)
|
|
|
|
def test_empty_children_yields_empty_list(self):
|
|
path = self.servers / "empty" / "inventory.yml"
|
|
path.parent.mkdir()
|
|
path.write_text("all:\n children: {}\n", encoding="utf-8")
|
|
self.assertEqual(bundle_apps.resolve(["empty"]), [])
|
|
|
|
def test_missing_all_key_tolerated(self):
|
|
path = self.servers / "weird" / "inventory.yml"
|
|
path.parent.mkdir()
|
|
path.write_text("other: 1\n", encoding="utf-8")
|
|
self.assertEqual(bundle_apps.resolve(["weird"]), [])
|
|
|
|
def test_null_children_tolerated(self):
|
|
path = self.servers / "nullkids" / "inventory.yml"
|
|
path.parent.mkdir()
|
|
path.write_text("all:\n children: ~\n", encoding="utf-8")
|
|
self.assertEqual(bundle_apps.resolve(["nullkids"]), [])
|
|
|
|
|
|
class TestMain(_BundleFsMixin, unittest.TestCase):
|
|
def _run(self, argv):
|
|
out = io.StringIO()
|
|
err = io.StringIO()
|
|
with patch("sys.stdout", out), patch("sys.stderr", err):
|
|
code = bundle_apps.main(argv)
|
|
return code, out.getvalue(), err.getvalue()
|
|
|
|
def test_success_prints_csv(self):
|
|
self.write_bundle(self.servers, "edu", ["nextcloud", "openwebui"])
|
|
code, out, err = self._run(["edu"])
|
|
self.assertEqual(code, 0)
|
|
self.assertEqual(out, "nextcloud,openwebui\n")
|
|
self.assertEqual(err, "")
|
|
|
|
def test_multiple_bundles_csv_arg(self):
|
|
self.write_bundle(self.servers, "a", ["x"])
|
|
self.write_bundle(self.workstations, "b", ["y"])
|
|
code, out, _ = self._run(["a,b"])
|
|
self.assertEqual(code, 0)
|
|
self.assertEqual(out, "x,y\n")
|
|
|
|
def test_blank_arg_returns_2(self):
|
|
code, _, err = self._run([" , "])
|
|
self.assertEqual(code, 2)
|
|
self.assertIn("no bundle names", err)
|
|
|
|
def test_missing_bundle_returns_2(self):
|
|
code, _, err = self._run(["ghost"])
|
|
self.assertEqual(code, 2)
|
|
self.assertIn("ghost", err)
|
|
|
|
def test_no_apps_resolved_returns_2(self):
|
|
path = self.servers / "empty" / "inventory.yml"
|
|
path.parent.mkdir()
|
|
path.write_text("all:\n children: {}\n", encoding="utf-8")
|
|
code, _, err = self._run(["empty"])
|
|
self.assertEqual(code, 2)
|
|
self.assertIn("no role groups", err)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|