computer-playbook/tests/integration/test_get_app_conf_paths.py
2025-07-15 21:39:41 +02:00

186 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# tests/integration/test_get_app_conf_paths.py
import os
import re
import unittest
from pathlib import Path
import yaml # requires PyYAML
from filter_plugins.get_role import get_role
class TestGetAppConfPaths(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Setup paths
root = Path(__file__).resolve().parents[2]
cls.root = root
cls.app_config_path = root / 'group_vars' / 'all' / '04_applications.yml'
cls.user_config_path = root / 'group_vars' / 'all' / '03_users.yml'
# Load application defaults
with cls.app_config_path.open(encoding='utf-8') as f:
app_cfg = yaml.safe_load(f) or {}
cls.defaults_app = app_cfg.get('defaults_applications', {})
# Load default users
with cls.user_config_path.open(encoding='utf-8') as f:
user_cfg = yaml.safe_load(f) or {}
cls.defaults_users = user_cfg.get('default_users', {})
# Preload role schemas: map application_id -> schema dict
cls.role_schemas = {}
cls.role_for_app = {}
roles_path = str(root / 'roles')
for app_id in cls.defaults_app:
try:
role = get_role(app_id, roles_path)
cls.role_for_app[app_id] = role
schema_file = root / 'roles' / role / 'schema' / 'main.yml'
with schema_file.open(encoding='utf-8') as sf:
schema = yaml.safe_load(sf) or {}
cls.role_schemas[app_id] = schema
except Exception:
# skip apps without schema or role
continue
# Regex to find get_app_conf calls
cls.pattern = re.compile(r"get_app_conf\(\s*([^\),]+)\s*,\s*['\"]([^'\"]+)['\"]")
# Scan files once
cls.literal_paths = {} # app_id -> {path: [(file,line)...]}
cls.variable_paths = {} # path -> [(file,line)...]
for dirpath, dirs, files in os.walk(root):
# skip descending into the tests directory
if 'tests' in dirs:
dirs.remove('tests')
if 'tests' in Path(dirpath).parts:
continue
for fname in files:
# ignore .py and .sh files
if fname.endswith(('.py', '.sh')):
continue
file_path = Path(dirpath) / fname
try:
text = file_path.read_text(encoding='utf-8')
except Exception:
continue
for m in cls.pattern.finditer(text):
lineno = text.count('\n', 0, m.start()) + 1
app_arg = m.group(1).strip()
path_arg = m.group(2).strip()
# ignore any templated Jinja2 raw-blocks
if '{%' in path_arg:
continue
if (app_arg.startswith("'") and app_arg.endswith("'")) or (app_arg.startswith('"') and app_arg.endswith('"')):
app_id = app_arg.strip("'\"")
cls.literal_paths.setdefault(app_id, {}).setdefault(path_arg, []).append((file_path, lineno))
else:
cls.variable_paths.setdefault(path_arg, []).append((file_path, lineno))
def assertNested(self, mapping, dotted, context):
keys = dotted.split('.')
cur = mapping
for k in keys:
assert isinstance(cur, dict), f"{context}: expected dict at {k}"
assert k in cur, f"{context}: missing '{k}' in '{dotted}'"
cur = cur[k]
def test_literal_paths(self):
# Check each literal path exists
for app_id, paths in self.literal_paths.items():
with self.subTest(app=app_id):
self.assertIn(app_id, self.defaults_app, f"App '{app_id}' missing in defaults_applications")
for dotted, occs in paths.items():
with self.subTest(path=dotted):
self._validate(app_id, dotted, occs)
def test_variable_paths(self):
# dynamic paths: must exist somewhere
if not self.variable_paths:
self.skipTest('No dynamic get_app_conf calls')
for dotted, occs in self.variable_paths.items():
with self.subTest(path=dotted):
found = False
# credentials.*: zuerst in defaults_applications prüfen, dann im Schema
if dotted.startswith('credentials.'):
key = dotted.split('.', 1)[1]
# 1) defaults_applications[app_id].credentials
for aid, cfg in self.defaults_app.items():
creds = cfg.get('credentials', {})
if isinstance(creds, dict) and key in creds:
found = True
break
if found:
continue
# 2) role_schema.credentials
for aid, schema in self.role_schemas.items():
creds = schema.get('credentials', {})
if isinstance(creds, dict) and key in creds:
found = True
break
if found:
continue
# images.*: any images dict
if dotted.startswith('images.'):
if any(isinstance(cfg.get('images'), dict) for cfg in self.defaults_app.values()):
continue
# users.*: default_users fallback
if dotted.startswith('users.'):
subpath = dotted.split('.', 1)[1]
try:
# this will raise if the nested key doesnt exist
self.assertNested(self.defaults_users, subpath, 'default_users')
continue
except AssertionError:
pass
# application defaults
for aid, cfg in self.defaults_app.items():
try:
self.assertNested(cfg, dotted, aid)
found = True
break
except AssertionError:
pass
if not found:
file_path, lineno = occs[0]
self.fail(f"No entry for '{dotted}'; called at {file_path}:{lineno}")
def _validate(self, app_id, dotted, occs):
# try app defaults
cfg = self.defaults_app.get(app_id, {})
try:
self.assertNested(cfg, dotted, app_id)
return
except AssertionError:
pass
# users.* fallback
if dotted.startswith('users.'):
sub = dotted.split('.', 1)[1]
if sub in self.defaults_users:
return
# credentials.* fallback: defaults_applications, dann Schema
if dotted.startswith('credentials.'):
key = dotted.split('.', 1)[1]
# 1) defaults_applications[app_id].credentials
creds_cfg = cfg.get('credentials', {})
if isinstance(creds_cfg, dict) and key in creds_cfg:
return
# 2) schema
schema = self.role_schemas.get(app_id, {})
creds = schema.get('credentials', {})
self.assertIn(key, creds, f"Credential '{key}' missing for app '{app_id}'")
return
# images.* fallback
if dotted.startswith('images.'):
if isinstance(cfg.get('images'), dict):
return
# final fail
file_path, lineno = occs[0]
self.fail(f"'{dotted}' not found for '{app_id}'; called at {file_path}:{lineno}")
if __name__ == '__main__':
unittest.main()