import os import yaml import unittest from pathlib import Path from collections import Counter ROLES_DIR = Path(__file__).resolve().parent.parent.parent / "roles" class TestDomainsStructure(unittest.TestCase): def test_domains_keys_types_and_uniqueness(self): """Ensure that under 'domains' only 'canonical' and 'aliases' keys exist, 'aliases' is a list of strings, 'canonical' is either a list of strings or a dict with string values, and no domain is defined more than once across all roles.""" failed_roles = [] all_domains = [] for role_path in ROLES_DIR.iterdir(): if not role_path.is_dir(): continue vars_dir = role_path / "vars" if not vars_dir.exists(): continue for vars_file in vars_dir.glob("*.yml"): try: with open(vars_file, 'r') as f: data = yaml.safe_load(f) or {} except yaml.YAMLError as e: failed_roles.append((role_path.name, vars_file.name, f"YAML error: {e}")) continue if 'domains' not in data: continue domains = data['domains'] if not isinstance(domains, dict): failed_roles.append((role_path.name, vars_file.name, "'domains' should be a dict")) continue # Check allowed keys allowed_keys = {'canonical', 'aliases'} extra_keys = set(domains.keys()) - allowed_keys if extra_keys: failed_roles.append((role_path.name, vars_file.name, f"Unexpected keys in 'domains': {extra_keys}")) # Validate and collect 'aliases' if 'aliases' in domains: aliases = domains['aliases'] if not isinstance(aliases, list) or not all(isinstance(item, str) for item in aliases): failed_roles.append((role_path.name, vars_file.name, "'aliases' must be a list of strings")) else: all_domains.extend(aliases) # Validate and collect 'canonical' if 'canonical' in domains: canonical = domains['canonical'] if isinstance(canonical, list): if not all(isinstance(item, str) for item in canonical): failed_roles.append((role_path.name, vars_file.name, "'canonical' list items must be strings")) else: all_domains.extend(canonical) elif isinstance(canonical, dict): if not all(isinstance(k, str) and isinstance(v, str) for k, v in canonical.items()): failed_roles.append((role_path.name, vars_file.name, "All keys and values in 'canonical' dict must be strings")) else: all_domains.extend(canonical.values()) else: failed_roles.append((role_path.name, vars_file.name, "'canonical' must be a list or a dict")) # Check for duplicate domains across all roles duplicates = [domain for domain, count in Counter(all_domains).items() if count > 1] if duplicates: failed_roles.append(("GLOBAL", "", f"Duplicate domain entries found: {duplicates}")) if failed_roles: messages = [] for role, file, reason in failed_roles: entry = f"{role}/{file}: {reason}" if file else f"{role}: {reason}" messages.append(entry) self.fail("Domain structure errors found:\n" + "\n".join(messages)) if __name__ == "__main__": unittest.main()