Added variable usage test

This commit is contained in:
Kevin Veen-Birkenbach 2025-07-15 11:57:31 +02:00
parent f02ca50f88
commit fff06d52b8
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E

View File

@ -0,0 +1,97 @@
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()