mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +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>
137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
import logging
|
|
import re
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from utils.cache.files import (
|
|
iter_project_files,
|
|
iter_project_files_with_content,
|
|
read_text,
|
|
)
|
|
from utils.cache.yaml import load_yaml_str
|
|
from utils.roles.mapping import ROLE_FILE_DEFAULTS_MAIN, ROLE_FILE_VARS_MAIN
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TestTopLevelVariableUsage(unittest.TestCase):
|
|
def setUp(self):
|
|
from . import PROJECT_ROOT
|
|
|
|
self.project_root = str(PROJECT_ROOT)
|
|
|
|
roles_prefix = str(Path(self.project_root) / "roles") + "/"
|
|
group_vars_prefix = str(Path(self.project_root) / "group_vars" / "all") + "/"
|
|
|
|
roles_vars: list[str] = []
|
|
roles_defaults: list[str] = []
|
|
group_vars: list[str] = []
|
|
for path_str in iter_project_files(extensions=(".yml",)):
|
|
if path_str.startswith(roles_prefix):
|
|
tail = path_str[len(roles_prefix) :]
|
|
if tail.endswith(f"/{ROLE_FILE_VARS_MAIN}") and tail.count("/") == 2:
|
|
roles_vars.append(path_str)
|
|
elif (
|
|
tail.endswith(f"/{ROLE_FILE_DEFAULTS_MAIN}")
|
|
and tail.count("/") == 2
|
|
):
|
|
roles_defaults.append(path_str)
|
|
elif (
|
|
path_str.startswith(group_vars_prefix)
|
|
and "/" not in path_str[len(group_vars_prefix) :]
|
|
):
|
|
group_vars.append(path_str)
|
|
|
|
self.roles_vars_paths = roles_vars + roles_defaults
|
|
self.group_vars_paths = group_vars
|
|
self.all_variable_files = self.roles_vars_paths + self.group_vars_paths
|
|
self.valid_extensions = (
|
|
".yml",
|
|
".yaml",
|
|
".j2",
|
|
".py",
|
|
".sh",
|
|
".conf",
|
|
".env",
|
|
".xml",
|
|
".html",
|
|
".txt",
|
|
)
|
|
self.ignored_top_level_keys = {
|
|
"ansible_python_interpreter",
|
|
"ansible_shell_executable",
|
|
}
|
|
|
|
def get_top_level_keys(self, file_path):
|
|
try:
|
|
data = load_yaml_str(read_text(file_path))
|
|
except yaml.YAMLError as e:
|
|
logger.warning("Failed to parse YAML file '%s': %s", file_path, e)
|
|
return []
|
|
if isinstance(data, dict):
|
|
return list(data.keys())
|
|
return []
|
|
|
|
def find_declaration_line(self, file_path, varname):
|
|
"""
|
|
Find the 1-based line number where the top-level key is actually declared.
|
|
"""
|
|
pattern = re.compile(rf"^\s*{re.escape(varname)}\s*:")
|
|
for i, line in enumerate(read_text(file_path).splitlines(), 1):
|
|
if pattern.match(line) and not line.lstrip().startswith("#"):
|
|
return i
|
|
return None
|
|
|
|
def find_usage_in_project(self, varname, definition_path):
|
|
"""
|
|
Search the whole project for varname, skipping only the single
|
|
declaration line in definition_path. Walk and file contents are
|
|
served from the process-level cache in utils.cache.files.
|
|
"""
|
|
decl_line = self.find_declaration_line(definition_path, varname)
|
|
|
|
for path, content in iter_project_files_with_content(
|
|
extensions=self.valid_extensions
|
|
):
|
|
if varname not in content:
|
|
continue
|
|
|
|
if path != definition_path or decl_line is None:
|
|
return True
|
|
|
|
for i, line in enumerate(content.splitlines(), 1):
|
|
if i == decl_line:
|
|
continue
|
|
if varname in line:
|
|
return True
|
|
return False
|
|
|
|
def test_top_level_variable_usage(self):
|
|
"""
|
|
Ensure every top-level variable in roles/*/{vars,defaults}/main.yml
|
|
and group_vars/all/*.yml is referenced somewhere in the project
|
|
(other than its own declaration line).
|
|
"""
|
|
unused = []
|
|
for varfile in self.all_variable_files:
|
|
keys = self.get_top_level_keys(varfile)
|
|
for key in keys:
|
|
if key in self.ignored_top_level_keys:
|
|
continue
|
|
if not self.find_usage_in_project(key, varfile):
|
|
unused.append((varfile, key))
|
|
|
|
if unused:
|
|
msg = "\n".join(
|
|
f"{path}: unused top-level key '{key}'" for path, key in unused
|
|
)
|
|
self.fail(
|
|
"The following top-level variables are defined but never used:\n" + msg
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|