mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-10-21 05:26:09 +00:00
feat(cli/build/defaults): ensure deterministic alphabetical sorting for applications and users
- Added sorting by application key and user key before YAML output. - Ensures stable and reproducible file generation across runs. - Added comprehensive unit tests verifying key order and output stability. See: https://chatgpt.com/share/68ef4778-a848-800f-a50b-a46a3b878797
This commit is contained in:
@@ -108,6 +108,89 @@ class TestGenerateDefaultApplications(unittest.TestCase):
|
||||
self.assertIn("nocfgdirapp", apps)
|
||||
self.assertEqual(apps["nocfgdirapp"], {})
|
||||
|
||||
def test_applications_sorted_by_key(self):
|
||||
"""
|
||||
Ensure that defaults_applications keys are written in alphabetical order.
|
||||
"""
|
||||
# Create several roles in non-sorted order
|
||||
for name, cfg in [
|
||||
("web-app-zeta", {"vars_id": "zeta", "cfg": "z: 1\n"}),
|
||||
("web-app-alpha", {"vars_id": "alpha", "cfg": "a: 1\n"}),
|
||||
("web-app-mu", {"vars_id": "mu", "cfg": "m: 1\n"}),
|
||||
]:
|
||||
role = self.roles_dir / name
|
||||
(role / "vars").mkdir(parents=True, exist_ok=True)
|
||||
(role / "config").mkdir(parents=True, exist_ok=True)
|
||||
(role / "vars" / "main.yml").write_text(f"application_id: {cfg['vars_id']}\n")
|
||||
(role / "config" / "main.yml").write_text(cfg["cfg"])
|
||||
|
||||
# Run generator
|
||||
result = subprocess.run(
|
||||
["python3", str(self.script_path),
|
||||
"--roles-dir", str(self.roles_dir),
|
||||
"--output-file", str(self.output_file)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
|
||||
# Validate order of keys in YAML
|
||||
data = yaml.safe_load(self.output_file.read_text())
|
||||
apps = data.get("defaults_applications", {})
|
||||
# dict preserves insertion order in Python 3.7+, PyYAML keeps document order
|
||||
keys_in_file = list(apps.keys())
|
||||
|
||||
self.assertEqual(
|
||||
keys_in_file,
|
||||
sorted(keys_in_file),
|
||||
msg=f"Applications are not sorted: {keys_in_file}"
|
||||
)
|
||||
# Sanity: all expected apps present
|
||||
for app in ("alpha", "mu", "zeta", "testapp"):
|
||||
self.assertIn(app, apps)
|
||||
|
||||
|
||||
def test_sorting_is_stable_across_runs(self):
|
||||
"""
|
||||
Running the generator multiple times yields identical content (stable sort).
|
||||
"""
|
||||
# Create a couple more roles (unsorted)
|
||||
for name, appid in [
|
||||
("web-app-beta", "beta"),
|
||||
("web-app-delta", "delta"),
|
||||
]:
|
||||
role = self.roles_dir / name
|
||||
(role / "vars").mkdir(parents=True, exist_ok=True)
|
||||
(role / "config").mkdir(parents=True, exist_ok=True)
|
||||
(role / "vars" / "main.yml").write_text(f"application_id: {appid}\n")
|
||||
(role / "config" / "main.yml").write_text("key: value\n")
|
||||
|
||||
# First run
|
||||
result1 = subprocess.run(
|
||||
["python3", str(self.script_path),
|
||||
"--roles-dir", str(self.roles_dir),
|
||||
"--output-file", str(self.output_file)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(result1.returncode, 0, msg=result1.stderr)
|
||||
content_run1 = self.output_file.read_text()
|
||||
|
||||
# Second run (simulate potential filesystem order differences by touching dirs)
|
||||
for p in self.roles_dir.iterdir():
|
||||
os.utime(p, None)
|
||||
|
||||
result2 = subprocess.run(
|
||||
["python3", str(self.script_path),
|
||||
"--roles-dir", str(self.roles_dir),
|
||||
"--output-file", str(self.output_file)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(result2.returncode, 0, msg=result2.stderr)
|
||||
content_run2 = self.output_file.read_text()
|
||||
|
||||
self.assertEqual(
|
||||
content_run1, content_run2,
|
||||
msg="Output differs between runs; sorting should be stable."
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
@@ -132,5 +132,122 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
finally:
|
||||
shutil.rmtree(tmp)
|
||||
|
||||
def test_cli_users_sorted_by_key(self):
|
||||
"""
|
||||
Ensure that default_users keys are written in alphabetical order.
|
||||
"""
|
||||
import tempfile
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
tmpdir = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
roles_dir = tmpdir / "roles"
|
||||
roles_dir.mkdir()
|
||||
|
||||
# Create multiple roles with users in unsorted order
|
||||
for role, users_map in [
|
||||
("role-zeta", {"zeta": {"email": "z@ex"}}),
|
||||
("role-alpha", {"alpha": {"email": "a@ex"}}),
|
||||
("role-mu", {"mu": {"email": "m@ex"}}),
|
||||
("role-beta", {"beta": {"email": "b@ex"}}),
|
||||
]:
|
||||
(roles_dir / role / "users").mkdir(parents=True, exist_ok=True)
|
||||
with open(roles_dir / role / "users" / "main.yml", "w") as f:
|
||||
yaml.safe_dump({"users": users_map}, f)
|
||||
|
||||
out_file = tmpdir / "users.yml"
|
||||
|
||||
# Resolve script path like in other tests (relative to repo root)
|
||||
script_path = Path(__file__).resolve().parents[5] / "cli" / "build" / "defaults" / "users.py"
|
||||
|
||||
# Run generator
|
||||
result = subprocess.run(
|
||||
["python3", str(script_path),
|
||||
"--roles-dir", str(roles_dir),
|
||||
"--output", str(out_file)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
self.assertTrue(out_file.exists(), "Output file was not created.")
|
||||
|
||||
data = yaml.safe_load(out_file.read_text())
|
||||
self.assertIn("default_users", data)
|
||||
users_map = data["default_users"]
|
||||
keys_in_file = list(users_map.keys())
|
||||
|
||||
# Expect alphabetical order
|
||||
self.assertEqual(
|
||||
keys_in_file, sorted(keys_in_file),
|
||||
msg=f"Users are not sorted alphabetically: {keys_in_file}"
|
||||
)
|
||||
# Sanity: all expected keys present
|
||||
for k in ["alpha", "beta", "mu", "zeta"]:
|
||||
self.assertIn(k, users_map)
|
||||
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def test_cli_users_sorting_stable_across_runs(self):
|
||||
"""
|
||||
Running the generator multiple times yields identical content (stable sort).
|
||||
"""
|
||||
import tempfile
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
tmpdir = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
roles_dir = tmpdir / "roles"
|
||||
roles_dir.mkdir()
|
||||
|
||||
# Unsorted creation order on purpose
|
||||
cases = [
|
||||
("role-d", {"duser": {"email": "d@ex"}}),
|
||||
("role-a", {"auser": {"email": "a@ex"}}),
|
||||
("role-c", {"cuser": {"email": "c@ex"}}),
|
||||
("role-b", {"buser": {"email": "b@ex"}}),
|
||||
]
|
||||
for role, users_map in cases:
|
||||
(roles_dir / role / "users").mkdir(parents=True, exist_ok=True)
|
||||
with open(roles_dir / role / "users" / "main.yml", "w") as f:
|
||||
yaml.safe_dump({"users": users_map}, f)
|
||||
|
||||
out_file = tmpdir / "users.yml"
|
||||
script_path = Path(__file__).resolve().parents[5] / "cli" / "build" / "defaults" / "users.py"
|
||||
|
||||
# First run
|
||||
r1 = subprocess.run(
|
||||
["python3", str(script_path),
|
||||
"--roles-dir", str(roles_dir),
|
||||
"--output", str(out_file)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(r1.returncode, 0, msg=r1.stderr)
|
||||
content1 = out_file.read_text()
|
||||
|
||||
# Touch dirs to shuffle filesystem mtimes
|
||||
for p in roles_dir.iterdir():
|
||||
os.utime(p, None)
|
||||
|
||||
# Second run
|
||||
r2 = subprocess.run(
|
||||
["python3", str(script_path),
|
||||
"--roles-dir", str(roles_dir),
|
||||
"--output", str(out_file)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(r2.returncode, 0, msg=r2.stderr)
|
||||
content2 = out_file.read_text()
|
||||
|
||||
self.assertEqual(
|
||||
content1, content2,
|
||||
msg="Output differs between runs; user sorting should be stable."
|
||||
)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
Reference in New Issue
Block a user