summaryrefslogtreecommitdiff
path: root/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py')
-rw-r--r--test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py197
1 files changed, 163 insertions, 34 deletions
diff --git a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py
index 25c61798..2b92a56c 100644
--- a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py
+++ b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py
@@ -33,6 +33,9 @@ from collections.abc import Mapping
from contextlib import contextmanager
from fnmatch import fnmatch
+from antsibull_docs_parser import dom
+from antsibull_docs_parser.parser import parse, Context
+
import yaml
from voluptuous.humanize import humanize_error
@@ -63,6 +66,7 @@ setup_collection_loader()
from ansible import __version__ as ansible_version
from ansible.executor.module_common import REPLACER_WINDOWS, NEW_STYLE_PYTHON_MODULE_RE
+from ansible.module_utils.common.collections import is_iterable
from ansible.module_utils.common.parameters import DEFAULT_TYPE_VALIDATORS
from ansible.module_utils.compat.version import StrictVersion, LooseVersion
from ansible.module_utils.basic import to_bytes
@@ -74,9 +78,13 @@ from ansible.utils.version import SemanticVersion
from .module_args import AnsibleModuleImportError, AnsibleModuleNotInitialized, get_argument_spec
-from .schema import ansible_module_kwargs_schema, doc_schema, return_schema
+from .schema import (
+ ansible_module_kwargs_schema,
+ doc_schema,
+ return_schema,
+)
-from .utils import CaptureStd, NoArgsAnsibleModule, compare_unordered_lists, is_empty, parse_yaml, parse_isodate
+from .utils import CaptureStd, NoArgsAnsibleModule, compare_unordered_lists, parse_yaml, parse_isodate
if PY3:
@@ -297,8 +305,6 @@ class ModuleValidator(Validator):
# win_dsc is a dynamic arg spec, the docs won't ever match
PS_ARG_VALIDATE_REJECTLIST = frozenset(('win_dsc.ps1', ))
- ACCEPTLIST_FUTURE_IMPORTS = frozenset(('absolute_import', 'division', 'print_function'))
-
def __init__(self, path, git_cache: GitCache, analyze_arg_spec=False, collection=None, collection_version=None,
reporter=None, routing=None, plugin_type='module'):
super(ModuleValidator, self).__init__(reporter=reporter or Reporter())
@@ -401,13 +407,10 @@ class ModuleValidator(Validator):
if isinstance(child, ast.Expr) and isinstance(child.value, ast.Constant) and isinstance(child.value.value, str):
continue
- # allowed from __future__ imports
+ # allow __future__ imports (the specific allowed imports are checked by other sanity tests)
if isinstance(child, ast.ImportFrom) and child.module == '__future__':
- for future_import in child.names:
- if future_import.name not in self.ACCEPTLIST_FUTURE_IMPORTS:
- break
- else:
- continue
+ continue
+
return False
return True
except AttributeError:
@@ -636,29 +639,21 @@ class ModuleValidator(Validator):
)
def _ensure_imports_below_docs(self, doc_info, first_callable):
- min_doc_line = min(doc_info[key]['lineno'] for key in doc_info)
+ doc_line_numbers = [lineno for lineno in (doc_info[key]['lineno'] for key in doc_info) if lineno > 0]
+
+ min_doc_line = min(doc_line_numbers) if doc_line_numbers else None
max_doc_line = max(doc_info[key]['end_lineno'] for key in doc_info)
import_lines = []
for child in self.ast.body:
if isinstance(child, (ast.Import, ast.ImportFrom)):
+ # allow __future__ imports (the specific allowed imports are checked by other sanity tests)
if isinstance(child, ast.ImportFrom) and child.module == '__future__':
- # allowed from __future__ imports
- for future_import in child.names:
- if future_import.name not in self.ACCEPTLIST_FUTURE_IMPORTS:
- self.reporter.error(
- path=self.object_path,
- code='illegal-future-imports',
- msg=('Only the following from __future__ imports are allowed: %s'
- % ', '.join(self.ACCEPTLIST_FUTURE_IMPORTS)),
- line=child.lineno
- )
- break
- else: # for-else. If we didn't find a problem nad break out of the loop, then this is a legal import
- continue
+ continue
+
import_lines.append(child.lineno)
- if child.lineno < min_doc_line:
+ if min_doc_line and child.lineno < min_doc_line:
self.reporter.error(
path=self.object_path,
code='import-before-documentation',
@@ -675,7 +670,7 @@ class ModuleValidator(Validator):
for grandchild in bodies:
if isinstance(grandchild, (ast.Import, ast.ImportFrom)):
import_lines.append(grandchild.lineno)
- if grandchild.lineno < min_doc_line:
+ if min_doc_line and grandchild.lineno < min_doc_line:
self.reporter.error(
path=self.object_path,
code='import-before-documentation',
@@ -813,22 +808,22 @@ class ModuleValidator(Validator):
continue
if grandchild.id == 'DOCUMENTATION':
- docs['DOCUMENTATION']['value'] = child.value.s
+ docs['DOCUMENTATION']['value'] = child.value.value
docs['DOCUMENTATION']['lineno'] = child.lineno
docs['DOCUMENTATION']['end_lineno'] = (
- child.lineno + len(child.value.s.splitlines())
+ child.lineno + len(child.value.value.splitlines())
)
elif grandchild.id == 'EXAMPLES':
- docs['EXAMPLES']['value'] = child.value.s
+ docs['EXAMPLES']['value'] = child.value.value
docs['EXAMPLES']['lineno'] = child.lineno
docs['EXAMPLES']['end_lineno'] = (
- child.lineno + len(child.value.s.splitlines())
+ child.lineno + len(child.value.value.splitlines())
)
elif grandchild.id == 'RETURN':
- docs['RETURN']['value'] = child.value.s
+ docs['RETURN']['value'] = child.value.value
docs['RETURN']['lineno'] = child.lineno
docs['RETURN']['end_lineno'] = (
- child.lineno + len(child.value.s.splitlines())
+ child.lineno + len(child.value.value.splitlines())
)
return docs
@@ -1041,6 +1036,8 @@ class ModuleValidator(Validator):
'invalid-documentation',
)
+ self._validate_all_semantic_markup(doc, returns)
+
if not self.collection:
existing_doc = self._check_for_new_args(doc)
self._check_version_added(doc, existing_doc)
@@ -1166,6 +1163,113 @@ class ModuleValidator(Validator):
return doc_info, doc
+ def _check_sem_option(self, part: dom.OptionNamePart, current_plugin: dom.PluginIdentifier) -> None:
+ if part.plugin is None or part.plugin != current_plugin:
+ return
+ if part.entrypoint is not None:
+ return
+ if tuple(part.link) not in self._all_options:
+ self.reporter.error(
+ path=self.object_path,
+ code='invalid-documentation-markup',
+ msg='Directive "%s" contains a non-existing option "%s"' % (part.source, part.name)
+ )
+
+ def _check_sem_return_value(self, part: dom.ReturnValuePart, current_plugin: dom.PluginIdentifier) -> None:
+ if part.plugin is None or part.plugin != current_plugin:
+ return
+ if part.entrypoint is not None:
+ return
+ if tuple(part.link) not in self._all_return_values:
+ self.reporter.error(
+ path=self.object_path,
+ code='invalid-documentation-markup',
+ msg='Directive "%s" contains a non-existing return value "%s"' % (part.source, part.name)
+ )
+
+ def _validate_semantic_markup(self, object) -> None:
+ # Make sure we operate on strings
+ if is_iterable(object):
+ for entry in object:
+ self._validate_semantic_markup(entry)
+ return
+ if not isinstance(object, string_types):
+ return
+
+ if self.collection:
+ fqcn = f'{self.collection_name}.{self.name}'
+ else:
+ fqcn = f'ansible.builtin.{self.name}'
+ current_plugin = dom.PluginIdentifier(fqcn=fqcn, type=self.plugin_type)
+ for par in parse(object, Context(current_plugin=current_plugin), errors='message', add_source=True):
+ for part in par:
+ # Errors are already covered during schema validation, we only check for option and
+ # return value references
+ if part.type == dom.PartType.OPTION_NAME:
+ self._check_sem_option(part, current_plugin)
+ if part.type == dom.PartType.RETURN_VALUE:
+ self._check_sem_return_value(part, current_plugin)
+
+ def _validate_semantic_markup_collect(self, destination, sub_key, data, all_paths):
+ if not isinstance(data, dict):
+ return
+ for key, value in data.items():
+ if not isinstance(value, dict):
+ continue
+ keys = {key}
+ if is_iterable(value.get('aliases')):
+ keys.update(value['aliases'])
+ new_paths = [path + [key] for path in all_paths for key in keys]
+ destination.update([tuple(path) for path in new_paths])
+ self._validate_semantic_markup_collect(destination, sub_key, value.get(sub_key), new_paths)
+
+ def _validate_semantic_markup_options(self, options):
+ if not isinstance(options, dict):
+ return
+ for key, value in options.items():
+ self._validate_semantic_markup(value.get('description'))
+ self._validate_semantic_markup_options(value.get('suboptions'))
+
+ def _validate_semantic_markup_return_values(self, return_vars):
+ if not isinstance(return_vars, dict):
+ return
+ for key, value in return_vars.items():
+ self._validate_semantic_markup(value.get('description'))
+ self._validate_semantic_markup(value.get('returned'))
+ self._validate_semantic_markup_return_values(value.get('contains'))
+
+ def _validate_all_semantic_markup(self, docs, return_docs):
+ if not isinstance(docs, dict):
+ docs = {}
+ if not isinstance(return_docs, dict):
+ return_docs = {}
+
+ self._all_options = set()
+ self._all_return_values = set()
+ self._validate_semantic_markup_collect(self._all_options, 'suboptions', docs.get('options'), [[]])
+ self._validate_semantic_markup_collect(self._all_return_values, 'contains', return_docs, [[]])
+
+ for string_keys in ('short_description', 'description', 'notes', 'requirements', 'todo'):
+ self._validate_semantic_markup(docs.get(string_keys))
+
+ if is_iterable(docs.get('seealso')):
+ for entry in docs.get('seealso'):
+ if isinstance(entry, dict):
+ self._validate_semantic_markup(entry.get('description'))
+
+ if isinstance(docs.get('attributes'), dict):
+ for entry in docs.get('attributes').values():
+ if isinstance(entry, dict):
+ for key in ('description', 'details'):
+ self._validate_semantic_markup(entry.get(key))
+
+ if isinstance(docs.get('deprecated'), dict):
+ for key in ('why', 'alternative'):
+ self._validate_semantic_markup(docs.get('deprecated').get(key))
+
+ self._validate_semantic_markup_options(docs.get('options'))
+ self._validate_semantic_markup_return_values(return_docs)
+
def _check_version_added(self, doc, existing_doc):
version_added_raw = doc.get('version_added')
try:
@@ -1233,6 +1337,31 @@ class ModuleValidator(Validator):
self._validate_argument_spec(docs, spec, kwargs)
+ if isinstance(docs, Mapping) and isinstance(docs.get('attributes'), Mapping):
+ if isinstance(docs['attributes'].get('check_mode'), Mapping):
+ support_value = docs['attributes']['check_mode'].get('support')
+ if not kwargs.get('supports_check_mode', False):
+ if support_value != 'none':
+ self.reporter.error(
+ path=self.object_path,
+ code='attributes-check-mode',
+ msg="The module does not declare support for check mode, but the check_mode attribute's"
+ " support value is '%s' and not 'none'" % support_value
+ )
+ else:
+ if support_value not in ('full', 'partial', 'N/A'):
+ self.reporter.error(
+ path=self.object_path,
+ code='attributes-check-mode',
+ msg="The module does declare support for check mode, but the check_mode attribute's support value is '%s'" % support_value
+ )
+ if support_value in ('partial', 'N/A') and docs['attributes']['check_mode'].get('details') in (None, '', []):
+ self.reporter.error(
+ path=self.object_path,
+ code='attributes-check-mode-details',
+ msg="The module declares it does not fully support check mode, but has no details on what exactly that means"
+ )
+
def _validate_list_of_module_args(self, name, terms, spec, context):
if terms is None:
return
@@ -1748,7 +1877,7 @@ class ModuleValidator(Validator):
)
arg_default = None
- if 'default' in data and not is_empty(data['default']):
+ if 'default' in data and data['default'] is not None:
try:
with CaptureStd():
arg_default = _type_checker(data['default'])
@@ -1789,7 +1918,7 @@ class ModuleValidator(Validator):
try:
doc_default = None
- if 'default' in doc_options_arg and not is_empty(doc_options_arg['default']):
+ if 'default' in doc_options_arg and doc_options_arg['default'] is not None:
with CaptureStd():
doc_default = _type_checker(doc_options_arg['default'])
except (Exception, SystemExit):