import unittest import os import yaml 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' } 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: pass 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 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()