mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 04:36:40 +00:00
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>
265 lines
8.5 KiB
Python
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()
|