bfa004930a
* add subdir support to collection loading * collections may now load plugins from subdirs under a plugin type or roles dir, eg `ns.coll.subdir1.subdir2.myrole`->ns.coll's roles/subdir1/subdir2/myrole, `ns.coll.subdir1.mymodule`->ns.coll's plugins/modules/subdir1/mymodule.py * centralize parsing/validation in AnsibleCollectionRef class * fix issues loading Jinja2 plugins from multiple sources * resolves #59462, #59890, * sanity test fixes * string fixes * add changelog entry
215 lines
9.7 KiB
Python
215 lines
9.7 KiB
Python
from __future__ import (absolute_import, division, print_function)
|
|
__metaclass__ = type
|
|
|
|
import os
|
|
import pytest
|
|
import re
|
|
import sys
|
|
|
|
from ansible.utils.collection_loader import AnsibleCollectionRef
|
|
|
|
|
|
def test_import_from_collection(monkeypatch):
|
|
collection_root = os.path.join(os.path.dirname(__file__), 'fixtures', 'collections')
|
|
collection_path = os.path.join(collection_root, 'ansible_collections/my_namespace/my_collection/plugins/module_utils/my_util.py')
|
|
|
|
# the trace we're expecting to be generated when running the code below:
|
|
# answer = question()
|
|
expected_trace_log = [
|
|
(collection_path, 5, 'call'),
|
|
(collection_path, 6, 'line'),
|
|
(collection_path, 6, 'return'),
|
|
]
|
|
|
|
# define the collection root before any ansible code has been loaded
|
|
# otherwise config will have already been loaded and changing the environment will have no effect
|
|
monkeypatch.setenv('ANSIBLE_COLLECTIONS_PATHS', collection_root)
|
|
|
|
from ansible.utils.collection_loader import AnsibleCollectionLoader
|
|
|
|
# zap the singleton collection loader instance if it exists
|
|
AnsibleCollectionLoader._Singleton__instance = None
|
|
|
|
for index in [idx for idx, obj in enumerate(sys.meta_path) if isinstance(obj, AnsibleCollectionLoader)]:
|
|
# replace any existing collection loaders that may exist
|
|
# since these were loaded during unit test collection
|
|
# they will not have the correct configuration
|
|
sys.meta_path[index] = AnsibleCollectionLoader()
|
|
|
|
# make sure the collection loader is installed
|
|
# this will be a no-op if the collection loader is already installed
|
|
# which will depend on whether or not any tests being run imported ansible.plugins.loader during unit test collection
|
|
from ansible.plugins.loader import _configure_collection_loader
|
|
_configure_collection_loader() # currently redundant, the import above already calls this
|
|
|
|
from ansible_collections.my_namespace.my_collection.plugins.module_utils.my_util import question
|
|
|
|
original_trace_function = sys.gettrace()
|
|
trace_log = []
|
|
|
|
if original_trace_function:
|
|
# enable tracing while preserving the existing trace function (coverage)
|
|
def my_trace_function(frame, event, arg):
|
|
trace_log.append((frame.f_code.co_filename, frame.f_lineno, event))
|
|
|
|
# the original trace function expects to have itself set as the trace function
|
|
sys.settrace(original_trace_function)
|
|
# call the original trace function
|
|
original_trace_function(frame, event, arg)
|
|
# restore our trace function
|
|
sys.settrace(my_trace_function)
|
|
|
|
return my_trace_function
|
|
else:
|
|
# no existing trace function, so our trace function is much simpler
|
|
def my_trace_function(frame, event, arg):
|
|
trace_log.append((frame.f_code.co_filename, frame.f_lineno, event))
|
|
|
|
return my_trace_function
|
|
|
|
sys.settrace(my_trace_function)
|
|
|
|
try:
|
|
# run a minimal amount of code while the trace is running
|
|
# adding more code here, including use of a context manager, will add more to our trace
|
|
answer = question()
|
|
finally:
|
|
sys.settrace(original_trace_function)
|
|
|
|
# make sure 'import ... as ...' works on builtin synthetic collections
|
|
# the following import is not supported (it tries to find module_utils in ansible.plugins)
|
|
# import ansible_collections.ansible.builtin.plugins.module_utils as c1
|
|
import ansible_collections.ansible.builtin.plugins.action as c2
|
|
import ansible_collections.ansible.builtin.plugins as c3
|
|
import ansible_collections.ansible.builtin as c4
|
|
import ansible_collections.ansible as c5
|
|
import ansible_collections as c6
|
|
|
|
# make sure 'import ...' works on builtin synthetic collections
|
|
import ansible_collections.ansible.builtin.plugins.module_utils
|
|
|
|
import ansible_collections.ansible.builtin.plugins.action
|
|
assert ansible_collections.ansible.builtin.plugins.action == c3.action == c2
|
|
|
|
import ansible_collections.ansible.builtin.plugins
|
|
assert ansible_collections.ansible.builtin.plugins == c4.plugins == c3
|
|
|
|
import ansible_collections.ansible.builtin
|
|
assert ansible_collections.ansible.builtin == c5.builtin == c4
|
|
|
|
import ansible_collections.ansible
|
|
assert ansible_collections.ansible == c6.ansible == c5
|
|
|
|
import ansible_collections
|
|
assert ansible_collections == c6
|
|
|
|
# make sure 'from ... import ...' works on builtin synthetic collections
|
|
from ansible_collections.ansible import builtin
|
|
from ansible_collections.ansible.builtin import plugins
|
|
assert builtin.plugins == plugins
|
|
|
|
from ansible_collections.ansible.builtin.plugins import action
|
|
from ansible_collections.ansible.builtin.plugins.action import command
|
|
assert action.command == command
|
|
|
|
from ansible_collections.ansible.builtin.plugins.module_utils import basic
|
|
from ansible_collections.ansible.builtin.plugins.module_utils.basic import AnsibleModule
|
|
assert basic.AnsibleModule == AnsibleModule
|
|
|
|
# make sure relative imports work from collections code
|
|
# these require __package__ to be set correctly
|
|
import ansible_collections.my_namespace.my_collection.plugins.module_utils.my_other_util
|
|
import ansible_collections.my_namespace.my_collection.plugins.action.my_action
|
|
|
|
# verify that code loaded from a collection does not inherit __future__ statements from the collection loader
|
|
if sys.version_info[0] == 2:
|
|
# if the collection code inherits the division future feature from the collection loader this will fail
|
|
assert answer == 1
|
|
else:
|
|
assert answer == 1.5
|
|
|
|
# verify that the filename and line number reported by the trace is correct
|
|
# this makes sure that collection loading preserves file paths and line numbers
|
|
assert trace_log == expected_trace_log
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'ref,ref_type,expected_collection,expected_subdirs,expected_resource,expected_python_pkg_name',
|
|
[
|
|
('ns.coll.myaction', 'action', 'ns.coll', '', 'myaction', 'ansible_collections.ns.coll.plugins.action'),
|
|
('ns.coll.subdir1.subdir2.myaction', 'action', 'ns.coll', 'subdir1.subdir2', 'myaction', 'ansible_collections.ns.coll.plugins.action.subdir1.subdir2'),
|
|
('ns.coll.myrole', 'role', 'ns.coll', '', 'myrole', 'ansible_collections.ns.coll.roles.myrole'),
|
|
('ns.coll.subdir1.subdir2.myrole', 'role', 'ns.coll', 'subdir1.subdir2', 'myrole', 'ansible_collections.ns.coll.roles.subdir1.subdir2.myrole'),
|
|
])
|
|
def test_fqcr_parsing_valid(ref, ref_type, expected_collection,
|
|
expected_subdirs, expected_resource, expected_python_pkg_name):
|
|
assert AnsibleCollectionRef.is_valid_fqcr(ref, ref_type)
|
|
|
|
r = AnsibleCollectionRef.from_fqcr(ref, ref_type)
|
|
assert r.collection == expected_collection
|
|
assert r.subdirs == expected_subdirs
|
|
assert r.resource == expected_resource
|
|
assert r.n_python_package_name == expected_python_pkg_name
|
|
|
|
r = AnsibleCollectionRef.try_parse_fqcr(ref, ref_type)
|
|
assert r.collection == expected_collection
|
|
assert r.subdirs == expected_subdirs
|
|
assert r.resource == expected_resource
|
|
assert r.n_python_package_name == expected_python_pkg_name
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'ref,ref_type,expected_error_type,expected_error_expression',
|
|
[
|
|
('no_dots_at_all_action', 'action', ValueError, 'is not a valid collection reference'),
|
|
('no_nscoll.myaction', 'action', ValueError, 'is not a valid collection reference'),
|
|
('ns.coll.myaction', 'bogus', ValueError, 'invalid collection ref_type'),
|
|
])
|
|
def test_fqcr_parsing_invalid(ref, ref_type, expected_error_type, expected_error_expression):
|
|
assert not AnsibleCollectionRef.is_valid_fqcr(ref, ref_type)
|
|
|
|
with pytest.raises(expected_error_type) as curerr:
|
|
AnsibleCollectionRef.from_fqcr(ref, ref_type)
|
|
|
|
assert re.search(expected_error_expression, str(curerr.value))
|
|
|
|
r = AnsibleCollectionRef.try_parse_fqcr(ref, ref_type)
|
|
assert r is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'name,subdirs,resource,ref_type,python_pkg_name',
|
|
[
|
|
('ns.coll', None, 'res', 'doc_fragments', 'ansible_collections.ns.coll.plugins.doc_fragments'),
|
|
('ns.coll', 'subdir1', 'res', 'doc_fragments', 'ansible_collections.ns.coll.plugins.doc_fragments.subdir1'),
|
|
('ns.coll', 'subdir1.subdir2', 'res', 'action', 'ansible_collections.ns.coll.plugins.action.subdir1.subdir2'),
|
|
])
|
|
def test_collectionref_components_valid(name, subdirs, resource, ref_type, python_pkg_name):
|
|
x = AnsibleCollectionRef(name, subdirs, resource, ref_type)
|
|
|
|
assert x.collection == name
|
|
if subdirs:
|
|
assert x.subdirs == subdirs
|
|
else:
|
|
assert x.subdirs == ''
|
|
|
|
assert x.resource == resource
|
|
assert x.ref_type == ref_type
|
|
assert x.n_python_package_name == python_pkg_name
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'name,subdirs,resource,ref_type,expected_error_type,expected_error_expression',
|
|
[
|
|
('bad_ns', '', 'resource', 'action', ValueError, 'invalid collection name'),
|
|
('ns.coll.', '', 'resource', 'action', ValueError, 'invalid collection name'),
|
|
('ns.coll', 'badsubdir#', 'resource', 'action', ValueError, 'invalid subdirs entry'),
|
|
('ns.coll', 'badsubdir.', 'resource', 'action', ValueError, 'invalid subdirs entry'),
|
|
('ns.coll', '.badsubdir', 'resource', 'action', ValueError, 'invalid subdirs entry'),
|
|
('ns.coll', '', 'resource', 'bogus', ValueError, 'invalid collection ref_type'),
|
|
])
|
|
def test_collectionref_components_invalid(name, subdirs, resource, ref_type, expected_error_type, expected_error_expression):
|
|
with pytest.raises(expected_error_type) as curerr:
|
|
AnsibleCollectionRef(name, subdirs, resource, ref_type)
|
|
|
|
assert re.search(expected_error_expression, str(curerr.value))
|