Files
computer-playbook/tests/unit/python/utils/roles/validation/test_resources.py
T
kevinveenbirkenbachandClaude Opus 5 a05071fe78 refactor(roles): let a role declare its entity name and drop the get_ prefix
The entity name keys a role's instance directory, its compose project and its container name, and was derived only by stripping the longest matching category prefix. Two roles under different categories whose names share a suffix therefore collapse onto one entity and overwrite each other's rendered stack, which no provides: can repair.

A literal entity_name in vars/main.yml now wins over the derivation. web-app-seaweedfs-console takes its shorter name back and declares the entity seaweedfs-console, which frees the seaweedfs key in its own services map for the engine consumer and removes the provides:, covers: and alias entries that stood in for it. A new lint pairs the declaration with the primary key it has to match, because a drift between them would drop the role from the provider set in silence.

Compared across all 288 roles, exactly one entity changes, and it restores what main already keeps on disk, so no instance directory moves.

The function and the filter lose their get_ prefix. Two lints read that prefix as the discriminator between the variable and the filter that fills it; both discriminate on the pipe now. Ansible's templar resolves a filter and a same-named variable independently, verified against ansible-core 2.20.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 19:40:53 +02:00

260 lines
8.2 KiB
Python

from __future__ import annotations
import io
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
from utils.roles.mapping import ROLE_FILE_META_SERVICES
from utils.roles.validation import resources as rrv
class TestFilterRolesByMinStorage(unittest.TestCase):
def setUp(self) -> None:
self.tmp = TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.roles_root = self.root / "roles"
self.roles_root.mkdir(parents=True, exist_ok=True)
def _write_role_config(self, role_name: str, *, yaml_text: str) -> Path:
role_dir = self.roles_root / role_name
(role_dir / "meta").mkdir(parents=True, exist_ok=True)
cfg_path = role_dir / ROLE_FILE_META_SERVICES
cfg_path.write_text(yaml_text, encoding="utf-8")
return cfg_path
def test_keeps_role_when_min_storage_is_within_required_storage(self) -> None:
self._write_role_config(
"web-app-demo",
yaml_text="""
demo:
min_storage: 5G
""".lstrip(),
)
with (
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "entity_name", return_value="demo"),
):
kept = rrv.filter_roles_by_min_storage(
role_names=["web-app-demo"],
required_storage="10G",
emit_warnings=False,
)
self.assertEqual(kept, ["web-app-demo"])
def test_filters_out_role_when_min_storage_exceeds_required_storage(self) -> None:
self._write_role_config(
"web-app-demo",
yaml_text="""
demo:
min_storage: 50G
""".lstrip(),
)
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "entity_name", return_value="demo"),
):
kept = rrv.filter_roles_by_min_storage(
role_names=["web-app-demo"],
required_storage="10G",
emit_warnings=True,
)
self.assertEqual(kept, [])
self.assertIn("requires", err.getvalue())
def test_missing_key_treats_as_zero_and_keeps_role(self) -> None:
self._write_role_config(
"web-app-demo",
yaml_text="""
demo: {}
""".lstrip(),
)
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "entity_name", return_value="demo"),
):
kept = rrv.filter_roles_by_min_storage(
role_names=["web-app-demo"],
required_storage="1G",
emit_warnings=True,
)
self.assertEqual(kept, ["web-app-demo"])
self.assertIn("Missing key services.demo.min_storage", err.getvalue())
def test_missing_role_directory_emits_absolute_path_warning(self) -> None:
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "entity_name", return_value="demo"),
):
kept = rrv.filter_roles_by_min_storage(
role_names=["web-app-does-not-exist"],
required_storage="1G",
emit_warnings=True,
)
self.assertEqual(kept, [])
self.assertIn(
str((self.roles_root / "web-app-does-not-exist").resolve()), err.getvalue()
)
def test_missing_services_file_skips_silently(self) -> None:
role_dir = self.roles_root / "web-app-demo"
role_dir.mkdir(parents=True, exist_ok=True)
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "entity_name", return_value="demo"),
):
kept = rrv.filter_roles_by_min_storage(
role_names=["web-app-demo"],
required_storage="1G",
emit_warnings=True,
)
self.assertEqual(kept, [])
self.assertEqual(err.getvalue(), "")
def test_missing_service_entry_keeps_role_without_warning(self) -> None:
self._write_role_config(
"web-app-demo",
yaml_text="""
other: {}
""".lstrip(),
)
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "entity_name", return_value="demo"),
):
kept = rrv.filter_roles_by_min_storage(
role_names=["web-app-demo"],
required_storage="1G",
emit_warnings=True,
)
self.assertEqual(kept, ["web-app-demo"])
self.assertEqual(err.getvalue(), "")
def test_missing_primary_entity_keeps_role_without_warning(self) -> None:
self._write_role_config(
"web-app-demo",
yaml_text="""
not_demo: {}
""".lstrip(),
)
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "entity_name", return_value="demo"),
):
kept = rrv.filter_roles_by_min_storage(
role_names=["web-app-demo"],
required_storage="1G",
emit_warnings=True,
)
self.assertEqual(kept, ["web-app-demo"])
self.assertEqual(err.getvalue(), "")
def test_invalid_yaml_emits_warning(self) -> None:
self._write_role_config("web-app-demo", yaml_text=": this is not yaml")
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "entity_name", return_value="demo"),
):
kept = rrv.filter_roles_by_min_storage(
role_names=["web-app-demo"],
required_storage="1G",
emit_warnings=True,
)
self.assertEqual(kept, [])
self.assertIn("Failed to parse YAML:", err.getvalue())
def test_invalid_min_storage_value_emits_warning(self) -> None:
self._write_role_config(
"web-app-demo",
yaml_text="""
demo:
min_storage: "nope"
""".lstrip(),
)
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "entity_name", return_value="demo"),
):
kept = rrv.filter_roles_by_min_storage(
role_names=["web-app-demo"],
required_storage="10G",
emit_warnings=True,
)
self.assertEqual(kept, [])
self.assertIn("Invalid min_storage value", err.getvalue())
def test_invalid_required_storage_raises_value_error(self) -> None:
with (
patch.object(rrv, "_roles_root", return_value=self.roles_root),
self.assertRaises(ValueError),
):
rrv.filter_roles_by_min_storage(
role_names=["web-app-demo"],
required_storage="not-a-size",
emit_warnings=False,
)
def test_entity_name_missing_emits_warning_and_skips(self) -> None:
self._write_role_config(
"web-app-demo",
yaml_text="""
demo:
min_storage: 1G
""".lstrip(),
)
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "entity_name", return_value=""),
):
kept = rrv.filter_roles_by_min_storage(
role_names=["web-app-demo"],
required_storage="10G",
emit_warnings=True,
)
self.assertEqual(kept, [])
self.assertIn("Could not derive entity_name", err.getvalue())
if __name__ == "__main__":
unittest.main()