Files
computer-playbook/tests/integration/roles/when/test_duplication.py
kevinveenbirkenbachandClaude Opus 5 5137465c6f style: delete every comment the comments-valid policy rejects
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>
2026-08-18 03:22:02 +02:00

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()