diff --git a/test/sanity/import/importer.py b/test/sanity/import/importer.py index 0c81eab42d6..0e51527e76f 100755 --- a/test/sanity/import/importer.py +++ b/test/sanity/import/importer.py @@ -15,6 +15,28 @@ try: except ImportError: from io import StringIO +import ansible.module_utils.basic +import ansible.module_utils.common.removed + + +class ImporterAnsibleModuleException(Exception): + """Exception thrown during initialization of ImporterAnsibleModule.""" + pass + + +class ImporterAnsibleModule(object): + """Replacement for AnsibleModule to support import testing.""" + def __init__(self, *args, **kwargs): + raise ImporterAnsibleModuleException() + + +# stop Ansible module execution during AnsibleModule instantiation +ansible.module_utils.basic.AnsibleModule = ImporterAnsibleModule +# no-op for _load_params since it may be called before instantiating AnsibleModule +ansible.module_utils.basic._load_params = lambda *args, **kwargs: {} +# no-op for removed_module since it is called in place of AnsibleModule instantiation +ansible.module_utils.common.removed.removed_module = lambda *args, **kwargs: None + def main(): """Main program function.""" @@ -22,58 +44,87 @@ def main(): messages = set() for path in sys.argv[1:]: - capture = Capture() - try: - with open(path, 'r') as module_fd: - with capture_output(capture): - imp.load_module('module_import_test', module_fd, os.path.abspath(path), ('.py', 'r', imp.PY_SOURCE)) - - capture_report(path, capture, messages) - except BaseException as ex: # pylint: disable=locally-disabled, broad-except - capture_report(path, capture, messages) - - exc_type, _, exc_tb = sys.exc_info() - message = str(ex) - results = list(reversed(traceback.extract_tb(exc_tb))) - source = None - line = 0 - offset = 0 - - for result in results: - if result[0].startswith(base_dir): - source = result[0][len(base_dir) + 1:].replace('test/sanity/import/', '') - line = result[1] or 0 - break - - if not source: - # If none of our source files are found in the traceback, report the file we were testing. - # I haven't been able to come up with a test case that encounters this issue yet. - source = path - message += ' (in %s:%d)' % (results[-1][0], results[-1][1] or 0) - elif isinstance(ex, SyntaxError): - if ex.filename.endswith(path): # pylint: disable=locally-disabled, no-member - # A SyntaxError in the source we're importing will have the correct path, line and offset. - # However, the traceback will report the path to this importer.py script instead. - # We'll use the details from the SyntaxError in this case, as it's more accurate. - source = path - line = ex.lineno or 0 # pylint: disable=locally-disabled, no-member - offset = ex.offset or 0 # pylint: disable=locally-disabled, no-member - message = str(ex) - - # Hack to remove the filename and line number from the message, if present. - message = message.replace(' (%s, line %d)' % (os.path.basename(path), line), '') - - message = re.sub(r'\n *', ': ', message) - error = '%s:%d:%d: %s: %s' % (source, line, offset, exc_type.__name__, message) - - if error not in messages: - messages.add(error) - print(error) + test_python_module(path, base_dir, messages, False) + test_python_module(path, base_dir, messages, True) if messages: exit(10) +def test_python_module(path, base_dir, messages, ansible_module): + if ansible_module: + # importing modules with __main__ under Python 2.6 exits with status code 1 + if sys.version_info < (2, 7): + return + + # only run __main__ protected code for Ansible modules + if not path.startswith('lib/ansible/modules/'): + return + + # async_wrapper is not an Ansible module + if path == 'lib/ansible/modules/utilities/logic/async_wrapper.py': + return + + # run code protected by __name__ conditional + name = '__main__' + # show the Ansible module responsible for the exception, even if it was thrown in module_utils + filter_dir = os.path.join(base_dir, 'lib/ansible/modules') + else: + # do not run code protected by __name__ conditional + name = 'module_import_test' + # show the Ansible file responsible for the exception, even if it was thrown in 3rd party code + filter_dir = base_dir + + capture = Capture() + try: + with open(path, 'r') as module_fd: + with capture_output(capture): + imp.load_module(name, module_fd, os.path.abspath(path), ('.py', 'r', imp.PY_SOURCE)) + + capture_report(path, capture, messages) + except ImporterAnsibleModuleException: + # module instantiated AnsibleModule without raising an exception + pass + except BaseException as ex: # pylint: disable=locally-disabled, broad-except + capture_report(path, capture, messages) + + exc_type, _, exc_tb = sys.exc_info() + message = str(ex) + results = list(reversed(traceback.extract_tb(exc_tb))) + source = None + line = 0 + offset = 0 + + for result in results: + if result[0].startswith(filter_dir): + source = result[0][len(base_dir) + 1:].replace('test/sanity/import/', '') + line = result[1] or 0 + break + + if not source: + # If none of our source files are found in the traceback, report the file we were testing. + # I haven't been able to come up with a test case that encounters this issue yet. + source = path + message += ' (in %s:%d)' % (results[-1][0], results[-1][1] or 0) + elif isinstance(ex, SyntaxError): + if ex.filename.endswith(path): # pylint: disable=locally-disabled, no-member + # A SyntaxError in the source we're importing will have the correct path, line and offset. + # However, the traceback will report the path to this importer.py script instead. + # We'll use the details from the SyntaxError in this case, as it's more accurate. + source = path + line = ex.lineno or 0 # pylint: disable=locally-disabled, no-member + offset = ex.offset or 0 # pylint: disable=locally-disabled, no-member + message = str(ex) + + # Hack to remove the filename and line number from the message, if present. + message = message.replace(' (%s, line %d)' % (os.path.basename(path), line), '') + + message = re.sub(r'\n *', ': ', message) + error = '%s:%d:%d: %s: %s' % (source, line, offset, exc_type.__name__, message) + + report_message(error, messages) + + class Capture(object): """Captured output and/or exception.""" def __init__(self): @@ -89,11 +140,19 @@ def capture_report(path, capture, messages): """ if capture.stdout.getvalue(): message = '%s:%d:%d: %s: %s' % (path, 0, 0, 'Output', 'Import resulted in output to stdout.') - messages.add(message) - print(message) + report_message(message, messages) if capture.stderr.getvalue(): message = '%s:%d:%d: %s: %s' % (path, 0, 0, 'Output', 'Import resulted in output to stderr.') + report_message(message, messages) + + +def report_message(message, messages): + """Report message if not already reported. + :type message: str + :type messages: set[str] + """ + if message not in messages: messages.add(message) print(message) diff --git a/test/sanity/import/skip.txt b/test/sanity/import/skip.txt index 50d24cb3d8e..6b077b5b004 100644 --- a/test/sanity/import/skip.txt +++ b/test/sanity/import/skip.txt @@ -1,5 +1,14 @@ +lib/ansible/modules/cloud/azure/azure_rm_storageaccount.py lib/ansible/modules/cloud/webfaction/webfaction_app.py lib/ansible/modules/cloud/webfaction/webfaction_db.py lib/ansible/modules/cloud/webfaction/webfaction_domain.py lib/ansible/modules/cloud/webfaction/webfaction_mailbox.py lib/ansible/modules/cloud/webfaction/webfaction_site.py +lib/ansible/modules/clustering/k8s/k8s_raw.py +lib/ansible/modules/clustering/k8s/k8s_scale.py +lib/ansible/modules/clustering/openshift/openshift_raw.py +lib/ansible/modules/clustering/openshift/openshift_scale.py +lib/ansible/modules/network/avi/avi_gslbservice_patch_member.py +lib/ansible/modules/network/radware/vdirect_commit.py +lib/ansible/modules/network/radware/vdirect_file.py +lib/ansible/modules/network/radware/vdirect_runnable.py