Files
computer-playbook/tests/unit/utils/roles/test_resource_validation.py
T
Environment TestandClaude Sonnet 4.6 9ea2ad6ff1 refactor(docker): introduce utils/docker/image_discovery as single SPOT for image enumeration
- Move iter_role_images + all helpers + ImageRef from cli/mirror/util.py and
  cli/mirror/model.py into utils/docker/image_discovery.py; add source_file field
  to ImageRef to distinguish config/main.yml vs vars/main.yml entries
- Delete cli/mirror/util.py; update all callers to import from utils.docker.image_discovery
- Move utils into subpackages: utils/docker/ (image_ref, paths_utils, hadolint) and
  utils/roles/ (dependency_resolver, resource_validation); update all imports
- Reorganise tests into subpackages: tests/lint/docker/dockerfile/, tests/integration/docker/,
  tests/unit/utils/docker/, tests/unit/utils/roles/; fix parents[N] offsets after relocation
- tests/lint/docker/test_image_versions.py now uses iter_role_images (same SPOT as mirror)
- Add vars/main.yml images.<name>.{image,version} support for non-application roles
  (test-e2e-playwright, sys-ctl-hlth-csp); eliminate hardcoded duplicates in defaults/tasks
- Add web-app-matrix matrix-chatgpt-bot and web-app-mini-qr mini-qr entries in config/main.yml;
  replace hardcoded image strings in vars/templates with lookup('config', ...) references
- Fix sync/__main__.py log line to use correct registry prefix for non-Docker-Hub images
- Update docs/contributing/code/role/dockerfile.md with image declaration rules and SPOT reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:32:31 +02:00

275 lines
8.6 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 import resource_validation 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 / "config").mkdir(parents=True, exist_ok=True)
cfg_path = role_dir / "config" / "main.yml"
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:
# min_storage <= required_storage -> keep
self._write_role_config(
"web-app-demo",
yaml_text="""
compose:
services:
demo:
min_storage: 5G
""".lstrip(),
)
with (
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "get_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:
# min_storage > required_storage -> filtered out
self._write_role_config(
"web-app-demo",
yaml_text="""
compose:
services:
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, "get_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:
# Missing min_storage -> treat as 0GB -> keep
self._write_role_config(
"web-app-demo",
yaml_text="""
compose:
services:
demo: {}
""".lstrip(),
)
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "get_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 compose.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, "get_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, [])
# Ensure absolute path is shown
self.assertIn(
str((self.roles_root / "web-app-does-not-exist").resolve()), err.getvalue()
)
def test_missing_config_file_skips_silently(self) -> None:
# Role dir exists but config missing
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, "get_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_compose_service_entry_keeps_role_without_warning(self) -> None:
self._write_role_config(
"web-app-demo",
yaml_text="""
compose:
services:
other: {}
""".lstrip(),
)
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "get_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_compose_section_keeps_role_without_warning(self) -> None:
self._write_role_config(
"web-app-demo",
yaml_text="""
server:
domains: {}
""".lstrip(),
)
err = io.StringIO()
with (
redirect_stdout(err),
patch.object(rrv, "_roles_root", return_value=self.roles_root),
patch.object(rrv, "get_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, "get_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="""
compose:
services:
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, "get_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):
with 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="""
compose:
services:
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, "get_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()