From f956ddcc775885cb2916dc573ecc703019808c63 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 3 Jan 2025 14:56:36 +0100 Subject: [PATCH] Add extra sanity test for acme action group. --- tests/sanity/extra/action-group.json | 12 ++ tests/sanity/extra/action-group.json.license | 3 + tests/sanity/extra/action-group.py | 123 +++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 tests/sanity/extra/action-group.json create mode 100644 tests/sanity/extra/action-group.json.license create mode 100755 tests/sanity/extra/action-group.py diff --git a/tests/sanity/extra/action-group.json b/tests/sanity/extra/action-group.json new file mode 100644 index 000000000..db6a92bcb --- /dev/null +++ b/tests/sanity/extra/action-group.json @@ -0,0 +1,12 @@ +{ + "include_symlinks": true, + "prefixes": [ + "meta/runtime.yml", + "plugins/modules/", + "tests/sanity/extra/action-group." + ], + "output": "path-message", + "requirements": [ + "pyyaml" + ] +} diff --git a/tests/sanity/extra/action-group.json.license b/tests/sanity/extra/action-group.json.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/tests/sanity/extra/action-group.json.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/extra/action-group.py b/tests/sanity/extra/action-group.py new file mode 100755 index 000000000..ccd5f0ddd --- /dev/null +++ b/tests/sanity/extra/action-group.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# Copyright (c) 2024, Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +"""Make sure all modules that should show up in the action group.""" + +from __future__ import annotations + +import os +import re +import yaml + + +ACTION_GROUPS = { + # The format is as follows: + # * 'pattern': a regular expression matching all module names potentially belonging to the action group; + # * 'exclusions': a list of modules that are not part of the action group; all other modules matching 'pattern' must be part of it; + # * 'doc_fragment': the docs fragment that documents membership of the action group. + 'acme': { + 'pattern': re.compile('^acme_.*$'), + 'exclusions': [ + 'acme_ari_info', # does not support ACME account + 'acme_certificate_renewal_info', # does not support ACME account + 'acme_challenge_cert_helper', # does not support (and need) any common parameters + ], + 'doc_fragment': 'community.crypto.attributes.actiongroup_acme', + }, +} + + +def main(): + """Main entry point.""" + + # Load redirects + meta_runtime = 'meta/runtime.yml' + self_path = 'tests/sanity/extra/action-group.py' + try: + with open(meta_runtime, 'rb') as f: + data = yaml.safe_load(f) + action_groups = data['action_groups'] + except Exception as exc: + print(f'{meta_runtime}: cannot load action groups: {exc}') + return + + for action_group in action_groups: + if action_group not in ACTION_GROUPS: + print(f'{meta_runtime}: found unknown action group {action_group!r}; likely {self_path} needs updating') + for action_group, action_group_data in list(ACTION_GROUPS.items()): + if action_group not in action_groups: + print(f'{meta_runtime}: cannot find action group {action_group!r}; likely {self_path} needs updating') + + modules_directory = 'plugins/modules/' + modules_suffix = '.py' + + for file in os.listdir(modules_directory): + if not file.endswith(modules_suffix): + continue + module_name = file[:-len(modules_suffix)] + + for action_group, action_group_data in ACTION_GROUPS.items(): + action_group_content = action_groups.get(action_group) or [] + path = os.path.join(modules_directory, file) + + if not action_group_data['pattern'].match(module_name): + if module_name in action_group_content: + print(f'{path}: module is in action group {action_group!r} despite not matching its pattern as defined in {self_path}') + continue + + should_be_in_action_group = module_name not in action_group_data['exclusions'] + + if should_be_in_action_group: + if module_name not in action_group_content: + print(f'{meta_runtime}: module {module_name!r} is not part of {action_group!r} action group') + else: + action_group_content.remove(module_name) + + documentation = [] + in_docs = False + with open(path, 'r', encoding='utf-8') as f: + for line in f: + if line.startswith('DOCUMENTATION ='): + in_docs = True + elif line.startswith(("'''", '"""')) and in_docs: + in_docs = False + elif in_docs: + documentation.append(line) + if in_docs: + print(f'{path}: cannot find DOCUMENTATION end') + if not documentation: + print(f'{path}: cannot find DOCUMENTATION') + continue + + try: + docs = yaml.safe_load('\n'.join(documentation)) + if not isinstance(docs, dict): + raise Exception('is not a top-level dictionary') + except Exception as exc: + print(f'{path}: cannot load DOCUMENTATION as YAML: {exc}') + continue + + docs_fragments = docs.get('extends_documentation_fragment') or [] + is_in_action_group = action_group_data['doc_fragment'] in docs_fragments + + if should_be_in_action_group != is_in_action_group: + if should_be_in_action_group: + print( + f'{path}: module does not document itself as part of action group {action_group!r}, but it should;' + f' you need to add {action_group_data["doc_fragment"]} to "extends_documentation_fragment" in DOCUMENTATION' + ) + else: + print(f'{path}: module documents itself as part of action group {action_group!r}, but it should not be') + + for action_group, action_group_data in ACTION_GROUPS.items(): + action_group_content = action_groups.get(action_group) or [] + for module_name in action_group_content: + print( + f'{meta_runtime}: module {module_name} mentioned in {action_group!r} action group' + f' does not exist or does not match pattern defined in {self_path}' + ) + + +if __name__ == '__main__': + main()