mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
Four tests were guarding two rules. Both duplicates are gone and what they were meant to check now lives with the rule it belongs to. The handler name/length pair: a 120-character limit sat over the same files as an 80-character one, so it could never fire first, and outside that overlap it matched `name:` keys that were no task at all -- a volume name, a var. It found nothing in either half. Deleted; the surviving lint instead grows the scope it was missing, `roles/*/handlers/**` and `playbooks/**`, because a handler is a task, and the dispatch input descriptions, because a form line is read like a job name. The templated-handler-name pair is worse: the second one skipped every document with `if not isinstance(doc, dict)`, and a handlers file is a list. It has been vacuously green for its whole life. Its intent -- every `handlers/*.yml`, not just `main.yml` -- is now what the surviving test does. The notify/listen contract itself gains the rule that made the rename in a8dd7f1 safe: a notify must land on a `listen:` topic, never on a display name, and both spell out as [a-z_-]+. Following `import_tasks` chains by hand is gone with it; scanning every project YAML sees the same handlers, plus the ones a role hands to a generic renderer through a data structure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
62 lines
2.2 KiB
Python
62 lines
2.2 KiB
Python
"""Handler entry points carry a static name.
|
|
|
|
An entry that declares `listen:` is the notification target of a role, and its
|
|
name is what an operator reads in the play output and greps for in a failed
|
|
run. Templating that name buys nothing -- the `listen:` topic already carries
|
|
every dynamic decision -- and costs a stable log line, plus an
|
|
undefined-variable error whenever the play reaches the handler with the
|
|
variable unset.
|
|
|
|
Scope: every `roles/*/handlers/*.yml`, not only `main.yml`, so a role that
|
|
splits its handlers by deploy mode is covered too. Sub-tasks *inside* an
|
|
included handler file are exempt: nothing notifies them, so their names are
|
|
free to name the thing they act on (`Connect '{{ CONTAINER }}' to ...`).
|
|
|
|
Which topic a notify may bind to, and how those topics are spelled, is the
|
|
subject of `test_invoked.py`.
|
|
"""
|
|
|
|
import re
|
|
import unittest
|
|
|
|
from utils.cache.yaml import load_yaml_all
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
JINJA_VAR_PATTERN = re.compile(r"{{.*?}}")
|
|
|
|
|
|
def handler_entries():
|
|
"""Every ``listen:``-bearing entry of every handlers file, with its path."""
|
|
for path in sorted((PROJECT_ROOT / "roles").glob("*/handlers/*.yml")):
|
|
try:
|
|
docs = list(load_yaml_all(str(path)))
|
|
except Exception as error:
|
|
yield path, {"name": f"YAML parse error: {error}", "listen": True}
|
|
continue
|
|
for doc in docs or []:
|
|
for entry in doc or []:
|
|
if isinstance(entry, dict) and "listen" in entry:
|
|
yield path, entry
|
|
|
|
|
|
class StaticHandlerNamesTest(unittest.TestCase):
|
|
def test_no_templated_names_in_handlers(self) -> None:
|
|
violations = [
|
|
f"{path.relative_to(PROJECT_ROOT).as_posix()}: {entry['name']!r}"
|
|
for path, entry in handler_entries()
|
|
if isinstance(entry.get("name"), str)
|
|
and JINJA_VAR_PATTERN.search(entry["name"])
|
|
]
|
|
self.assertEqual(
|
|
violations,
|
|
[],
|
|
"Handler entry point(s) with a templated name. Keep the name a "
|
|
"constant and let the static 'listen:' topic carry the routing:\n"
|
|
+ "\n".join(f" {v}" for v in violations),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|