mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
3529 invalid comments in 528 files, grandfathered in while the linter only saw the working-tree diff. Removal ran through the linter's own collectors, so detection and deletion cannot drift: a comment whose opener line is blank before it and whose closer line is blank after it loses all its lines, anything else loses only the comment region, and a line left holding a bare delimiter goes with it. Five files keep their comments behind a nocheck marker instead. Four are upstream configs whose inline docs are upstream's own (mautrix, systemd journald, nextcloud-oidc-login, MediaWiki). In littlejs the commented-out entries ARE the deactivation mechanism for an example tile, so deleting them would delete configuration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
168 lines
5.9 KiB
Python
168 lines
5.9 KiB
Python
import unittest
|
|
from collections.abc import Iterable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from utils.cache.files import read_text
|
|
from utils.cache.yaml import load_yaml_all_str
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
THRESHOLD = 3 # fail if the same when-condition occurs on more than this many tasks
|
|
|
|
|
|
def _normalize_when(value: Any) -> str:
|
|
"""
|
|
Normalize Ansible 'when' to a comparable string:
|
|
- If it's a list, join with ' && ' preserving order (order matters in Ansible).
|
|
- If it's a scalar, strip leading/trailing whitespace.
|
|
- Represent everything as a single-line string for stable comparison.
|
|
"""
|
|
if value is None:
|
|
return ""
|
|
if isinstance(value, list):
|
|
parts = []
|
|
for v in value:
|
|
s = "" if v is None else str(v).strip()
|
|
s = " ".join(s.split())
|
|
parts.append(s)
|
|
return " && ".join(parts)
|
|
s = str(value).strip()
|
|
return " ".join(s.split())
|
|
|
|
|
|
def _iter_tasks(node: Any) -> Iterable[dict[str, Any]]:
|
|
"""
|
|
Yield task-like dicts (those which may contain 'when') from arbitrary YAML structures.
|
|
Handles:
|
|
- Top-level lists of tasks
|
|
- Dicts that contain keys like 'block', 'rescue', 'always' (Ansible blocks)
|
|
- Nested lists/dicts recursively
|
|
We only yield a dict once as a "task" (the one that has a 'when' or looks like a task).
|
|
"""
|
|
if isinstance(node, list):
|
|
for item in node:
|
|
yield from _iter_tasks(item)
|
|
elif isinstance(node, dict):
|
|
is_task_like = any(
|
|
k in node
|
|
for k in (
|
|
"when",
|
|
"name",
|
|
"block",
|
|
"rescue",
|
|
"always",
|
|
"include_tasks",
|
|
"import_tasks",
|
|
"ansible.builtin.include_tasks",
|
|
"ansible.builtin.import_tasks",
|
|
)
|
|
)
|
|
if is_task_like:
|
|
yield node
|
|
|
|
for section in ("block", "rescue", "always"):
|
|
if section in node and isinstance(node[section], list):
|
|
for item in node[section]:
|
|
yield from _iter_tasks(item)
|
|
for k, v in node.items():
|
|
if k not in ("block", "rescue", "always") and isinstance(v, (list, dict)):
|
|
yield from _iter_tasks(v)
|
|
|
|
|
|
def _load_yaml_documents(path: Path) -> list[Any]:
|
|
"""
|
|
Load all YAML documents from a file. Best-effort parsing:
|
|
- If YAML fails due to Jinja syntax, we still raise, because a broken file
|
|
should be fixed in the repo.
|
|
"""
|
|
text = read_text(str(path))
|
|
return list(load_yaml_all_str(text)) # may return [None] if empty
|
|
|
|
|
|
def _collect_when_counts(yaml_docs: list[Any]) -> dict[str, list[tuple[str, str]]]:
|
|
"""
|
|
Return a mapping: normalized_when -> list of (task_name, hint_location)
|
|
where each entry corresponds to a task that uses that 'when'.
|
|
"""
|
|
counts: dict[str, list[tuple[str, str]]] = {}
|
|
for doc in yaml_docs:
|
|
for task in _iter_tasks(doc):
|
|
if "when" not in task:
|
|
continue
|
|
normalized = _normalize_when(task.get("when"))
|
|
if not normalized:
|
|
continue
|
|
task_name = str(task.get("name") or "<unnamed task>")
|
|
hint = None
|
|
for key in (
|
|
"include_tasks",
|
|
"import_tasks",
|
|
"ansible.builtin.include_tasks",
|
|
"ansible.builtin.import_tasks",
|
|
):
|
|
if key in task:
|
|
hint = f"{key}: {task[key]}"
|
|
break
|
|
hint_loc = hint or "task"
|
|
counts.setdefault(normalized, []).append((task_name, hint_loc))
|
|
return counts
|
|
|
|
|
|
class WhenConditionDuplicationTest(unittest.TestCase):
|
|
"""
|
|
Integration test that ensures we don't repeat the same 'when' condition
|
|
on too many tasks in a single tasks file.
|
|
|
|
Rationale:
|
|
Repeating identical 'when' across many tasks forces Ansible to evaluate
|
|
the same condition over and over, which is bad for performance. Prefer
|
|
factoring those tasks into a dedicated file and use `include_tasks`
|
|
(or a block with a single 'when') to evaluate once.
|
|
"""
|
|
|
|
def test_excessive_repeated_when_in_tasks_files(self):
|
|
repo_root = PROJECT_ROOT
|
|
tasks_globs = [
|
|
"**/tasks/**/*.yml",
|
|
"**/tasks/**/*.yaml",
|
|
]
|
|
|
|
violations: list[str] = []
|
|
|
|
for pattern in tasks_globs:
|
|
for path in repo_root.glob(pattern):
|
|
if not path.is_file():
|
|
continue
|
|
|
|
try:
|
|
docs = _load_yaml_documents(path)
|
|
except Exception as exc:
|
|
self.fail(f"Failed to parse YAML file: {path}\n{exc}")
|
|
|
|
counts = _collect_when_counts(docs)
|
|
for normalized_when, occurrences in counts.items():
|
|
if len(occurrences) > THRESHOLD:
|
|
sample = "\n".join(
|
|
f" - {tname} ({hint})" for tname, hint in occurrences[:5]
|
|
)
|
|
violations.append(
|
|
f"{path} uses the same 'when' condition more than {THRESHOLD} times "
|
|
f"({len(occurrences)} occurrences):\n"
|
|
f" WHEN: {normalized_when}\n"
|
|
f" Sample tasks:\n{sample}\n"
|
|
f"Suggestion: Group these tasks into a separate file and call it with "
|
|
f"`include_tasks`, or use a single `block` guarded by this 'when' to avoid "
|
|
f"re-evaluating the condition repeatedly."
|
|
)
|
|
|
|
if violations:
|
|
self.fail(
|
|
"Excessive duplicate 'when' conditions detected (hurts performance):\n\n"
|
|
+ "\n\n".join(violations)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|