Files
computer-playbook/tests/unit/utils/roles/validation/test_resources.py
T
kevinveenbirkenbachandClaude Opus 4.7 f6c8c85b1b test(lint): role-file-spot guard + 139-file mapping migration
Add tests/lint/repository/test_role_file_spot.py: AST-walks every .py
file under the production trees and tests/, flags any reference to a
role file (standalone literal, f-string segment, pathlib `/` chain,
`+` concat chain, os.path.join / Path.joinpath / posixpath.join /
ntpath.join call) whose value or joined trailing operands match a
ROLE_FILE_* constant in utils.roles.mapping. The flag set is
auto-discovered from the module so adding a new constant extends
coverage without further edits. ROLE_FILE_README is exempt as too
generic. Per-line `# nocheck: role-file-spot` (same-or-above) is the
documented opt-out, catalog row added in
docs/contributing/actions/testing/suppression.md.

Migrate 139 .py files (377 occurrences) to use the constants:

- 135 files via direct constant import (`from utils.roles.mapping
  import ROLE_FILE_*`), with the symbol used in pathlib chains, in
  os.path.join / Path.joinpath calls, in f-string interpolation, and
  in glob patterns.
- 4 files via `# nocheck: role-file-spot` where the literal lives in
  argparse help text or in an allowlist tuple meant to declare the
  literal path.

Adjust utils/roles/dependency_resolver.py to use lazy-formatting
positional args in the logger.exception call (G003-clean) instead of
string concatenation against the constant.

Drop a redundant `if cond: return True; return False` guard in the
new test for `return cond` (SIM103-clean).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:21:22 +02:00

265 lines
8.5 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:
# min_storage <= required_storage -> keep
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, "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="""
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="""
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 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_services_file_skips_silently(self) -> None:
# Role dir exists but meta/services.yml 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_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, "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_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, "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="""
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),
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, "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()