mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
import unittest
|
||
import os
|
||
import yaml
|
||
import logging
|
||
from glob import glob
|
||
import re
|
||
|
||
|
||
class TestTopLevelVariableUsage(unittest.TestCase):
|
||
def setUp(self):
|
||
self.project_root = os.path.abspath(
|
||
os.path.join(os.path.dirname(__file__), "../../")
|
||
)
|
||
# Braces werden von glob nicht unterstützt – also einzeln sammeln:
|
||
self.roles_vars_paths = glob(
|
||
os.path.join(self.project_root, "roles/*/vars/main.yml")
|
||
) + glob(os.path.join(self.project_root, "roles/*/defaults/main.yml"))
|
||
self.group_vars_paths = glob(
|
||
os.path.join(self.project_root, "group_vars/all/*.yml")
|
||
)
|
||
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",
|
||
}
|
||
# Global Ansible runtime knobs are consumed by Ansible itself and may not
|
||
# appear as plain string references inside this repository.
|
||
self.ignored_top_level_keys = {
|
||
"ansible_python_interpreter",
|
||
"ansible_shell_executable",
|
||
}
|
||
|
||
def get_top_level_keys(self, file_path):
|
||
with open(file_path, "r") as f:
|
||
try:
|
||
data = yaml.safe_load(f)
|
||
if isinstance(data, dict):
|
||
return list(data.keys())
|
||
except yaml.YAMLError as e:
|
||
logging.warning("Failed to parse YAML file '%s': %s", file_path, e)
|
||
return []
|
||
|
||
def find_declaration_line(self, file_path, varname):
|
||
"""
|
||
Findet die Zeilennummer (1-basiert), in der der Top-Level-Key wirklich deklariert wird.
|
||
"""
|
||
pattern = re.compile(rf"^\s*{re.escape(varname)}\s*:")
|
||
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
||
for i, line in enumerate(f, 1):
|
||
if pattern.match(line) and not line.lstrip().startswith("#"):
|
||
return i
|
||
return None
|
||
|
||
def find_usage_in_project(self, varname, definition_path):
|
||
"""
|
||
Sucht im gesamten Projekt nach varname, überspringt dabei
|
||
nur die eine Deklarationszeile in definition_path.
|
||
"""
|
||
decl_line = self.find_declaration_line(definition_path, varname)
|
||
|
||
for root, _, files in os.walk(self.project_root):
|
||
for fn in files:
|
||
path = os.path.join(root, fn)
|
||
ext = os.path.splitext(path)[1]
|
||
if ext not in self.valid_extensions:
|
||
continue
|
||
try:
|
||
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
||
for i, line in enumerate(f, 1):
|
||
if (
|
||
path == definition_path
|
||
and decl_line is not None
|
||
and i == decl_line
|
||
):
|
||
# genau die Deklarationszeile überspringen
|
||
continue
|
||
if varname in line:
|
||
return True
|
||
except Exception:
|
||
continue
|
||
return False
|
||
|
||
def test_top_level_variable_usage(self):
|
||
"""
|
||
Stellt sicher, dass jede Top-Level-Variable in roles/*/{vars,defaults}/main.yml
|
||
und group_vars/all/*.yml irgendwo im Projekt (außer in ihrer eigenen
|
||
Deklarationszeile) verwendet wird.
|
||
"""
|
||
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()
|