computer-playbook/tests/integration/test_uppercase_constant_vars_unique.py

132 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""
Integration test: ensure every ALL-CAPS variable is defined only once project-wide.
Scope (by design):
- group_vars/**/*.yml
- roles/*/vars/*.yml
- roles/*/defaults/*.yml
- roles/*/defauls/*.yml # included on purpose in case of folder typos
A variable is considered a “constant” if its key matches: ^[A-Z0-9_]+$
If a constant is declared more than once across the scanned files, the test fails
with a clear message explaining that such constants must be defined only once.
"""
import os
import glob
import unittest
import re
from collections import defaultdict
try:
import yaml
except Exception as e: # pragma: no cover
raise SystemExit(
"PyYAML is required for this test. Install with: pip install pyyaml"
) from e
UPPER_CONST_RE = re.compile(r"^[A-Z0-9_]+$")
def _iter_yaml_files():
"""Yield all YAML file paths in the intended scope."""
patterns = [
os.path.join("group_vars", "**", "*.yml"),
os.path.join("roles", "*", "vars", "*.yml"),
os.path.join("roles", "*", "defaults", "*.yml"),
os.path.join("roles", "*", "defauls", "*.yml"), # intentionally included
]
seen = set()
for pattern in patterns:
for path in glob.glob(pattern, recursive=True):
# Normalize and deduplicate
norm = os.path.normpath(path)
if norm not in seen and os.path.isfile(norm):
seen.add(norm)
yield norm
def _extract_uppercase_keys_from_mapping(mapping):
"""
Recursively extract ALL-CAPS keys from any YAML mapping.
Returns a set of keys found in this mapping (deduplicated for the file).
"""
found = set()
def walk(node):
if isinstance(node, dict):
for k, v in node.items():
# Only consider string keys
if isinstance(k, str) and UPPER_CONST_RE.match(k):
found.add(k)
# Recurse into values to catch nested mappings too
walk(v)
elif isinstance(node, list):
for item in node:
walk(item)
walk(mapping)
return found
class TestUppercaseConstantVarsUnique(unittest.TestCase):
def test_uppercase_constants_unique(self):
# Track where each constant is defined
constant_to_files = defaultdict(set)
# Track YAML parse errors to fail fast with a helpful message
parse_errors = []
yaml_files = list(_iter_yaml_files())
for yml in yaml_files:
try:
with open(yml, "r", encoding="utf-8") as f:
docs = list(yaml.safe_load_all(f))
except Exception as e:
parse_errors.append(f"{yml}: {e}")
continue
# Some files may be empty or contain only comments
if not docs:
continue
# Collect ALL-CAPS keys for this file (dedup per file)
file_constants = set()
for doc in docs:
if isinstance(doc, dict):
file_constants |= _extract_uppercase_keys_from_mapping(doc)
# Non-mapping documents (e.g., lists/None) are ignored
for const in file_constants:
constant_to_files[const].add(yml)
# Fail if YAML parsing had errors
if parse_errors:
self.fail(
"YAML parsing failed for one or more files:\n"
+ "\n".join(f"- {err}" for err in parse_errors)
)
# Find duplicates (same constant in more than one file)
duplicates = {c: sorted(files) for c, files in constant_to_files.items() if len(files) > 1}
if duplicates:
msg_lines = [
"Found constants defined more than once. "
"ALL-CAPS variables are treated as constants and must be defined only once project-wide.\n"
"Please consolidate each duplicated constant into a single authoritative location (e.g., one vars/defaults file).",
"",
]
for const, files in sorted(duplicates.items()):
msg_lines.append(f"* {const} defined in {len(files)} files:")
for f in files:
msg_lines.append(f" - {f}")
msg_lines.append("") # spacer
self.fail("\n".join(msg_lines))
if __name__ == "__main__":
unittest.main()