ansible-test - Add integration test collection deps (#86437)

This commit is contained in:
Matt Clay 2026-01-21 09:40:35 -08:00 committed by GitHub
parent a0b3c7c0d6
commit a816184a6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 147 additions and 8 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- ansible-test - Support automatic loading of test collections in core integration tests.

View file

@ -0,0 +1,13 @@
from __future__ import annotations
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(argument_spec=dict())
module.exit_json(source='meta')
if __name__ == '__main__':
main()

View file

@ -0,0 +1,3 @@
- name: Set a fact indicating this collection was loaded as a dependency
set_fact:
by_meta_loaded: true

View file

@ -0,0 +1,13 @@
from __future__ import annotations
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(argument_spec=dict())
module.exit_json(source='needs/target')
if __name__ == '__main__':
main()

View file

@ -0,0 +1,5 @@
shippable/posix/group3 # runs in the distro test containers
shippable/generic/group1 # runs in the default test container
needs/target/ansible-test-collection-indirect-by-needs-target
context/controller
gather_facts/no

View file

@ -0,0 +1,2 @@
dependencies:
- ansible-test-collection-indirect-by-meta

View file

@ -0,0 +1,18 @@
- name: Verify collection dependencies executed as appropriate
assert:
that:
- by_meta_loaded
- name: Use dependency defined by alias
ansible_test.ansible_test_collection_indirect_by_needs_target.hello:
register: by_needs_target
- name: Use dependency defined by meta
ansible_test.ansible_test_collection_indirect_by_meta.hello:
register: by_meta
- name: Verify the results
assert:
that:
- by_needs_target.source == 'needs/target'
- by_meta.source == 'meta'

View file

@ -0,0 +1,4 @@
shippable/posix/group3 # runs in the distro test containers
shippable/generic/group1 # runs in the default test container
needs/target/ansible-test-collection-indirect-by-needs-target
context/controller

View file

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -eux
ansible-playbook runme.yml "${@}" -i ../../inventory

View file

@ -0,0 +1,11 @@
- hosts: testhost
gather_facts: no
tasks:
- name: Use dependency defined by alias
ansible_test.ansible_test_collection_indirect_by_needs_target.hello:
register: by_needs_target
- name: Verify the results
assert:
that:
- by_needs_target.source == 'needs/target'

View file

@ -15,6 +15,9 @@
#
# 4) Windows tests need access to the ansible.windows vendored collection.
# This script copies any of the existing collections in ANSIBLE_COLLECTIONS_PATH to the temporary directory.
#
# NOTE: This script is *NOT* compatible ansible-test's built-in collection injection feature,
# since it assumes that ANSIBLE_COLLECTIONS_PATH contains only a single path.
set -eu -o pipefail

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import collections.abc as c
import contextlib
import dataclasses
import datetime
import json
import os
@ -175,6 +176,51 @@ def get_files_needed(target_dependencies: list[IntegrationTarget]) -> list[str]:
return files_needed
def get_collection_roots_needed(target_dependencies: list[IntegrationTarget]) -> list[str]:
"""
Return a list of collection roots needed by the given list of target dependencies.
This feature is only supported for ansible-core, not collections.
To be recognized, a collection must reside in the following directory:
test/integration/targets/{target_name}/ansible_collections/ansible_test/{target_name}
If the target name has dashes, they will be replaced with underscores for the collection name.
It is an error if the collection root contains additional namespaces or collections.
This is enforced to ensure there are no naming conflicts between collection roots.
"""
if not data_context().content.is_ansible:
return []
collection_roots: list[str] = []
namespace_name = 'ansible_test'
for target_dependency in target_dependencies:
collection_root = os.path.join(data_context().content.integration_targets_path, target_dependency.name)
collection_name = target_dependency.name.replace('-', '_')
namespaces_path = os.path.join(collection_root, 'ansible_collections')
namespace_path = os.path.join(namespaces_path, namespace_name)
collection_path = os.path.join(namespace_path, collection_name)
if not os.path.isdir(collection_path):
continue
namespaces = set(os.listdir(namespaces_path))
namespaces.remove(namespace_name)
if namespaces:
raise ApplicationError(f"Additional test collection namespaces not supported: {', '.join(sorted(namespaces))}")
collections = set(os.listdir(namespace_path))
collections.remove(collection_name)
if collections:
raise ApplicationError(f"Additional test collections not supported: {', '.join(sorted(collections))}")
collection_roots.append(collection_root)
collection_roots = sorted(set(collection_roots))
return collection_roots
def check_inventory(args: IntegrationConfig, inventory_path: str) -> None:
"""Check the given inventory for issues."""
if not isinstance(args.controller, OriginConfig):
@ -289,6 +335,7 @@ def integration_test_environment(
target_dependencies = sorted([target] + list(cache.dependency_map.get(target.name, set())))
files_needed = get_files_needed(target_dependencies)
collection_roots = get_collection_roots_needed(target_dependencies)
integration_dir = os.path.join(temp_dir, data_context().content.integration_path)
targets_dir = os.path.join(temp_dir, data_context().content.integration_targets_path)
@ -336,7 +383,7 @@ def integration_test_environment(
make_dirs(os.path.dirname(file_dst))
shutil.copy2(file_src, file_dst)
yield IntegrationEnvironment(temp_dir, integration_dir, targets_dir, inventory_path, ansible_config, vars_file)
yield IntegrationEnvironment(temp_dir, integration_dir, targets_dir, inventory_path, ansible_config, vars_file, collection_roots)
finally:
if not args.explain:
remove_tree(temp_dir)
@ -628,6 +675,7 @@ def command_integration_script(
if config_path:
cmd += ['-e', '@%s' % config_path]
test_env.update_environment(env)
env.update(coverage_manager.get_environment(target.name, target.aliases))
cover_python(args, host_state.controller_profile.python, cmd, target.name, env, cwd=cwd, capture=False)
@ -747,6 +795,7 @@ def command_integration_role(
env['ANSIBLE_ROLES_PATH'] = test_env.targets_dir
test_env.update_environment(env)
env.update(coverage_manager.get_environment(target.name, target.aliases))
cover_python(args, host_state.controller_profile.python, cmd, target.name, env, cwd=cwd, capture=False)
@ -826,16 +875,25 @@ def integration_environment(
return env
@dataclasses.dataclass(frozen=True)
class IntegrationEnvironment:
"""Details about the integration environment."""
def __init__(self, test_dir: str, integration_dir: str, targets_dir: str, inventory_path: str, ansible_config: str, vars_file: str) -> None:
self.test_dir = test_dir
self.integration_dir = integration_dir
self.targets_dir = targets_dir
self.inventory_path = inventory_path
self.ansible_config = ansible_config
self.vars_file = vars_file
test_dir: str
integration_dir: str
targets_dir: str
inventory_path: str
ansible_config: str
vars_file: str
collection_roots: list[str] = dataclasses.field(default_factory=list)
def update_environment(self, env: dict[str, str]) -> None:
"""Update the given environment dictionary with the variables necessary for this integration environment."""
collections_path = value.split(':') if (value := env.get('ANSIBLE_COLLECTIONS_PATH')) else []
collections_path.extend([os.path.join(self.test_dir, root) for root in self.collection_roots])
if collections_path:
env['ANSIBLE_COLLECTIONS_PATH'] = ':'.join(collections_path)
class IntegrationCache(CommonCache):