computer-playbook/tests/integration/test_role_include_import_roles_exist.py

75 lines
3.1 KiB
Python

import unittest
import os
import glob
import yaml
import re
class TestIncludeImportRoleExistence(unittest.TestCase):
def setUp(self):
# Determine project root and roles directory
tests_dir = os.path.dirname(__file__)
project_root = os.path.abspath(os.path.join(tests_dir, os.pardir, os.pardir))
self.roles_dir = os.path.join(project_root, 'roles')
# Collect all .yml files in project (recursive), excluding .git and tests dirs
self.files_to_scan = []
for filepath in glob.glob(os.path.join(project_root, '**', '*.yml'), recursive=True):
# Skip .git, tests folders
if '/.git/' in filepath or '/tests/' in filepath:
continue
self.files_to_scan.append(filepath)
def _collect_includes(self, data):
"""
Recursively collect all roles referenced via include_role or import_role.
Supports scalar, block, and block-list syntax, plus templating and wildcards.
"""
roles = []
if isinstance(data, dict):
for key, val in data.items():
if key in ('include_role', 'import_role'):
# Scalar syntax: include_role: role_name
if isinstance(val, str):
roles.append(val)
# Block syntax: include_role: { name: role_name }
elif isinstance(val, dict) and 'name' in val:
roles.append(val['name'])
# Block-list syntax: include_role:
# - name: foo
# - name: bar
elif isinstance(val, list):
for item in val:
if isinstance(item, dict) and 'name' in item:
roles.append(item['name'])
else:
roles.extend(self._collect_includes(val))
elif isinstance(data, list):
for item in data:
roles.extend(self._collect_includes(item))
return roles
def test_include_import_roles_exist(self):
missing = []
for file_path in self.files_to_scan:
with open(file_path) as f:
try:
docs = list(yaml.safe_load_all(f))
except yaml.YAMLError:
self.fail(f"Failed to parse YAML in {file_path}")
for doc in docs:
for role_name in self._collect_includes(doc):
# Convert Jinja2 templates and wildcards to glob patterns
pattern = re.sub(r"\{\{.*?\}\}", '*', role_name)
glob_path = os.path.join(self.roles_dir, pattern)
# Check for matching role directories
matches = [p for p in glob.glob(glob_path) if os.path.isdir(p)]
if not matches:
missing.append((file_path, role_name))
if missing:
messages = [f"File '{fp}' references missing role '{rn}'" for fp, rn in missing]
self.fail("\n".join(messages))
if __name__ == '__main__':
unittest.main()