mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-07-20 23:31:10 +02:00
Optimized variable definition tester
This commit is contained in:
parent
75a5ab455e
commit
c99def5724
@ -16,26 +16,19 @@ class TestVariableDefinitions(unittest.TestCase):
|
|||||||
glob(os.path.join(self.project_root, 'roles/*/defaults/*.yml')) +
|
glob(os.path.join(self.project_root, 'roles/*/defaults/*.yml')) +
|
||||||
glob(os.path.join(self.project_root, 'group_vars/all/*.yml'))
|
glob(os.path.join(self.project_root, 'group_vars/all/*.yml'))
|
||||||
)
|
)
|
||||||
# Valid file extensions to scan for usages
|
# Valid file extensions to scan for definitions and usages
|
||||||
self.scan_extensions = {'.yml', '.j2'}
|
self.scan_extensions = {'.yml', '.j2'}
|
||||||
|
|
||||||
# Regexes
|
# Regex patterns
|
||||||
# Match simple Jinja variable usage: {{ var }} or with filters {{ var|filter }}
|
|
||||||
self.simple_var_pattern = re.compile(r"{{\s*([a-zA-Z_]\w*)\s*(?:\|[^}]*)?}}")
|
self.simple_var_pattern = re.compile(r"{{\s*([a-zA-Z_]\w*)\s*(?:\|[^}]*)?}}")
|
||||||
# Match Jinja2 set definitions: {% set var = ... %}
|
|
||||||
self.jinja_set_def = re.compile(r'{%\s*set\s+([a-zA-Z_]\w*)\s*=')
|
self.jinja_set_def = re.compile(r'{%\s*set\s+([a-zA-Z_]\w*)\s*=')
|
||||||
# Match Jinja2 for-loop variable definitions
|
|
||||||
self.jinja_for_def = re.compile(r'{%\s*for\s+([a-zA-Z_]\w*)(?:\s*,\s*([a-zA-Z_]\w*))?\s+in')
|
self.jinja_for_def = re.compile(r'{%\s*for\s+([a-zA-Z_]\w*)(?:\s*,\s*([a-zA-Z_]\w*))?\s+in')
|
||||||
# Match Ansible set_fact mapping start
|
|
||||||
self.ansible_set_fact = re.compile(r'^(?:\s*[-]\s*)?set_fact\s*:\s*$')
|
self.ansible_set_fact = re.compile(r'^(?:\s*[-]\s*)?set_fact\s*:\s*$')
|
||||||
# Match ansible vars block start
|
|
||||||
self.ansible_vars_block = re.compile(r'^(?:\s*[-]\s*)?vars\s*:\s*$')
|
self.ansible_vars_block = re.compile(r'^(?:\s*[-]\s*)?vars\s*:\s*$')
|
||||||
# Match ansible loop_control loop_var definition
|
|
||||||
self.ansible_loop_var = re.compile(r'^\s*loop_var\s*:\s*([a-zA-Z_]\w*)')
|
self.ansible_loop_var = re.compile(r'^\s*loop_var\s*:\s*([a-zA-Z_]\w*)')
|
||||||
# Match keys under a mapping block
|
|
||||||
self.mapping_key = re.compile(r'^\s*([a-zA-Z_]\w*)\s*:\s*')
|
self.mapping_key = re.compile(r'^\s*([a-zA-Z_]\w*)\s*:\s*')
|
||||||
|
|
||||||
# Build initial defined-vars set from all var definition files
|
# Initialize defined set from var files
|
||||||
self.defined = set()
|
self.defined = set()
|
||||||
for vf in self.var_files:
|
for vf in self.var_files:
|
||||||
try:
|
try:
|
||||||
@ -46,10 +39,7 @@ class TestVariableDefinitions(unittest.TestCase):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_all_used_vars_are_defined(self):
|
# Phase 1: scan all files to collect inline definitions
|
||||||
undefined_uses = []
|
|
||||||
|
|
||||||
# Walk all .yml and .j2 files
|
|
||||||
for root, _, files in os.walk(self.project_root):
|
for root, _, files in os.walk(self.project_root):
|
||||||
for fn in files:
|
for fn in files:
|
||||||
ext = os.path.splitext(fn)[1]
|
ext = os.path.splitext(fn)[1]
|
||||||
@ -58,74 +48,77 @@ class TestVariableDefinitions(unittest.TestCase):
|
|||||||
|
|
||||||
path = os.path.join(root, fn)
|
path = os.path.join(root, fn)
|
||||||
in_set_fact = False
|
in_set_fact = False
|
||||||
set_fact_indent = None
|
set_fact_indent = 0
|
||||||
in_vars_block = False
|
in_vars_block = False
|
||||||
vars_block_indent = None
|
vars_block_indent = 0
|
||||||
|
|
||||||
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
for lineno, line in enumerate(f, 1):
|
for line in f:
|
||||||
stripped = line.lstrip()
|
stripped = line.lstrip()
|
||||||
indent = len(line) - len(stripped)
|
indent = len(line) - len(stripped)
|
||||||
# Detect start of a set_fact mapping
|
# set_fact keys
|
||||||
if self.ansible_set_fact.match(stripped):
|
if self.ansible_set_fact.match(stripped):
|
||||||
in_set_fact = True
|
in_set_fact = True
|
||||||
set_fact_indent = indent
|
set_fact_indent = indent
|
||||||
continue
|
continue
|
||||||
# Inside set_fact, collect keys
|
|
||||||
if in_set_fact:
|
if in_set_fact:
|
||||||
if indent > set_fact_indent and self.mapping_key.match(stripped):
|
if indent > set_fact_indent:
|
||||||
key = self.mapping_key.match(stripped).group(1)
|
m = self.mapping_key.match(stripped)
|
||||||
self.defined.add(key)
|
if m:
|
||||||
|
self.defined.add(m.group(1))
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
in_set_fact = False
|
in_set_fact = False
|
||||||
|
# vars block keys
|
||||||
# Detect start of a vars block under a task or play
|
|
||||||
if self.ansible_vars_block.match(stripped):
|
if self.ansible_vars_block.match(stripped):
|
||||||
in_vars_block = True
|
in_vars_block = True
|
||||||
vars_block_indent = indent
|
vars_block_indent = indent
|
||||||
continue
|
continue
|
||||||
# Inside vars block, collect keys and skip usage scanning
|
|
||||||
if in_vars_block:
|
if in_vars_block:
|
||||||
if indent > vars_block_indent:
|
if indent > vars_block_indent:
|
||||||
# any mapping key under vars is a definition
|
|
||||||
m = self.mapping_key.match(stripped)
|
m = self.mapping_key.match(stripped)
|
||||||
if m:
|
if m:
|
||||||
key = m.group(1)
|
self.defined.add(m.group(1))
|
||||||
self.defined.add(key)
|
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
in_vars_block = False
|
in_vars_block = False
|
||||||
|
# loop_var
|
||||||
# Detect loop_control loop_var definitions
|
|
||||||
m_loop = self.ansible_loop_var.match(stripped)
|
m_loop = self.ansible_loop_var.match(stripped)
|
||||||
if m_loop:
|
if m_loop:
|
||||||
self.defined.add(m_loop.group(1))
|
self.defined.add(m_loop.group(1))
|
||||||
|
# jinja set
|
||||||
# collect any {% set foo = ... %} definitions
|
|
||||||
for m in self.jinja_set_def.finditer(line):
|
for m in self.jinja_set_def.finditer(line):
|
||||||
self.defined.add(m.group(1))
|
self.defined.add(m.group(1))
|
||||||
# collect any {% for var1, var2 in ... %} definitions
|
# jinja for
|
||||||
for m in self.jinja_for_def.finditer(line):
|
for m in self.jinja_for_def.finditer(line):
|
||||||
self.defined.add(m.group(1))
|
self.defined.add(m.group(1))
|
||||||
if m.group(2):
|
if m.group(2):
|
||||||
self.defined.add(m.group(2))
|
self.defined.add(m.group(2))
|
||||||
|
|
||||||
# collect simple usages only
|
def test_all_used_vars_are_defined(self):
|
||||||
|
undefined_uses = []
|
||||||
|
# Phase 2: scan all files for usages
|
||||||
|
for root, _, files in os.walk(self.project_root):
|
||||||
|
for fn in files:
|
||||||
|
ext = os.path.splitext(fn)[1]
|
||||||
|
if ext not in self.scan_extensions:
|
||||||
|
continue
|
||||||
|
path = os.path.join(root, fn)
|
||||||
|
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
for lineno, line in enumerate(f, 1):
|
||||||
for m in self.simple_var_pattern.finditer(line):
|
for m in self.simple_var_pattern.finditer(line):
|
||||||
var = m.group(1)
|
var = m.group(1)
|
||||||
# skip known Jinja builtins and whitelisted names
|
# skip builtins and whitelisted names
|
||||||
if var in ('lookup', 'role_name', 'domains', 'item', 'host_type', 'inventory_hostname', 'role_path', 'playbook_dir', 'ansible_become_password', 'inventory_dir'):
|
if var in ('lookup', 'role_name', 'domains', 'item', 'host_type',
|
||||||
|
'inventory_hostname', 'role_path', 'playbook_dir',
|
||||||
|
'ansible_become_password', 'inventory_dir'):
|
||||||
continue
|
continue
|
||||||
if var not in self.defined:
|
# skip defaults_var fallback
|
||||||
# check fallback names defaults_var or default_var
|
if var not in self.defined and \
|
||||||
if (f"defaults_{var}" in self.defined or
|
f"defaults_{var}" not in self.defined and \
|
||||||
f"default_{var}" in self.defined):
|
f"default_{var}" not in self.defined:
|
||||||
continue
|
|
||||||
undefined_uses.append(
|
undefined_uses.append(
|
||||||
f"{path}:{lineno}: '{{{{ {var} }}}}' used but not defined"
|
f"{path}:{lineno}: '{{{{ {var} }}}}' used but not defined"
|
||||||
)
|
)
|
||||||
|
|
||||||
if undefined_uses:
|
if undefined_uses:
|
||||||
self.fail(
|
self.fail(
|
||||||
"Undefined Jinja2 variables found (no fallback 'default_' or 'defaults_' key):\n" +
|
"Undefined Jinja2 variables found (no fallback 'default_' or 'defaults_' key):\n" +
|
||||||
|
Loading…
x
Reference in New Issue
Block a user