mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-15 16:40:45 +02:00
Add integration test to ensure no Jinja variables are used in handler names
This test scans roles/*/handlers/main.yml and fails if a handler's 'name' contains a Jinja variable ({{ ... }}). Reason: - Handler names must be static to ensure reliable 'notify' resolution. - Dynamic names can break handler matching, cause undefined-variable errors, and produce unstable logs. Recommendation: - Keep handler names static and, if dynamic behavior is needed, use a static 'listen:' key. https://chatgpt.com/share/689b37dc-e1e4-800f-bd56-00b43c7701f6
This commit is contained in:
parent
0c4cd283c4
commit
26b29debc0
114
tests/integration/test_handler_names_static.py
Normal file
114
tests/integration/test_handler_names_static.py
Normal file
@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Integration test: ensure no Jinja variables are used in handler *names*.
|
||||
|
||||
Why this policy?
|
||||
- Handler identifiers should be stable strings. If you ever notify by handler
|
||||
name (instead of a dedicated `listen:` key), a templated name can fail to
|
||||
resolve or silently not match what `notify` referenced.
|
||||
- Even when `listen:` is used (recommended), variable-laden names make logs and
|
||||
tooling brittle and can trigger undefined-variable errors at parse/run time.
|
||||
- Keeping handler names static improves reliability, debuggability, and
|
||||
compatibility with analysis tools.
|
||||
|
||||
Allowed:
|
||||
- You may still template other fields or use `listen:` for dynamic trigger
|
||||
routing; just keep the handler’s `name` static text.
|
||||
|
||||
This test scans: roles/*/handlers/main.yml
|
||||
"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
import re
|
||||
import unittest
|
||||
|
||||
try:
|
||||
import yaml # PyYAML
|
||||
except ImportError as exc:
|
||||
raise SystemExit(
|
||||
"PyYAML is required to run this test. Install with: pip install pyyaml"
|
||||
) from exc
|
||||
|
||||
|
||||
JINJA_VAR_PATTERN = re.compile(r"{{.*?}}") # minimal check for any templating
|
||||
|
||||
|
||||
def _iter_tasks(node):
|
||||
"""
|
||||
Yield all task-like dicts from a loaded YAML node, descending into common
|
||||
task containers (`block`, `rescue`, `always`), just in case.
|
||||
"""
|
||||
if isinstance(node, dict):
|
||||
# If this dict looks like a task (has 'name' or a module key), yield it.
|
||||
if any(k in node for k in ("name", "action")):
|
||||
yield node
|
||||
|
||||
# Dive into known task containers (handlers can include blocks too).
|
||||
for key in ("block", "rescue", "always"):
|
||||
if key in node and isinstance(node[key], list):
|
||||
for item in node[key]:
|
||||
yield from _iter_tasks(item)
|
||||
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
yield from _iter_tasks(item)
|
||||
|
||||
|
||||
class StaticHandlerNamesTest(unittest.TestCase):
|
||||
"""
|
||||
Ensures handler names are static strings (no Jinja variables like {{ ... }}).
|
||||
"""
|
||||
|
||||
def test_no_templated_names_in_handlers(self):
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
pattern = os.path.join(project_root, "roles", "*", "handlers", "main.yml")
|
||||
|
||||
violations = []
|
||||
|
||||
for handler_path in sorted(glob.glob(pattern)):
|
||||
# Load possibly multi-document YAML safely
|
||||
try:
|
||||
with open(handler_path, "r", encoding="utf-8") as f:
|
||||
docs = list(yaml.safe_load_all(f))
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
except yaml.YAMLError as e:
|
||||
violations.append(
|
||||
f"{handler_path} -> YAML parse error: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
for doc in docs:
|
||||
for task in _iter_tasks(doc):
|
||||
name = task.get("name")
|
||||
if not isinstance(name, str):
|
||||
# ignore unnamed or non-string names
|
||||
continue
|
||||
if JINJA_VAR_PATTERN.search(name):
|
||||
# Compose a clear, actionable message
|
||||
listen = task.get("listen")
|
||||
listen_hint = (
|
||||
""
|
||||
if listen
|
||||
else " Consider using a static handler name and, if you need flexible triggers, add a static `listen:` key that your tasks `notify`."
|
||||
)
|
||||
violations.append(
|
||||
f"{handler_path} -> Handler name contains variables: {name!r}\n"
|
||||
"Reason: Handler names must be static. Using Jinja variables in the name "
|
||||
"can break handler resolution (when notified by name), produces unstable logs, "
|
||||
"and may cause undefined-variable errors. Keep the handler `name` constant."
|
||||
f"{listen_hint}"
|
||||
)
|
||||
|
||||
if violations:
|
||||
self.fail(
|
||||
"Templated handler names are not allowed.\n\n"
|
||||
+ "\n\n".join(violations)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user