[stable-2.9] Fix issues with ansible-test --venv option. (#62033)

* Fix ansible-test venv activation.

When using the ansible-test --venv option, an execv wrapper for each python interpreter is now used instead of a symbolic link.

* Fix ansible-test execv wrapper generation.

Use the currently running Python interpreter for the shebang in the execv wrapper instead of the selected interpreter.

This allows the wrapper to work when the selected interpreter is a script instead of a binary.

* Fix ansible-test sanity requirements install.

When running sanity tests on multiple Python versions, install requirements for all versions used instead of only the default version.

* Fix ansible-test --venv when installed.

When running ansible-test from an install, the --venv delegation option needs to make sure the ansible-test code is available in the created virtual environment.

Exposing system site packages does not work because the virtual environment may be for a different Python version than the one on which ansible-test is installed.
(cherry picked from commit c77ab11051)

Co-authored-by: Matt Clay <matt@mystile.com>
pull/61899/head
Matt Clay 5 years ago committed by Toshio Kuratomi
parent 427da1d213
commit b3fbc3c156

@ -0,0 +1,2 @@
bugfixes:
- ansible-test now properly handles creation of Python execv wrappers when the selected interpreter is a script

@ -0,0 +1,2 @@
bugfixes:
- ansible-test now properly installs requirements for multiple Python versions when running sanity tests

@ -0,0 +1,2 @@
bugfixes:
- ansible-test now properly activates virtual environments created using the --venv option

@ -0,0 +1,2 @@
bugfixes:
- ansible-test now properly registers its own code in a virtual environment when running from an install

@ -48,12 +48,16 @@ from .util import (
display, display,
ANSIBLE_BIN_PATH, ANSIBLE_BIN_PATH,
ANSIBLE_TEST_DATA_ROOT, ANSIBLE_TEST_DATA_ROOT,
ANSIBLE_LIB_ROOT,
ANSIBLE_TEST_ROOT,
tempdir, tempdir,
make_dirs,
) )
from .util_common import ( from .util_common import (
run_command, run_command,
ResultType, ResultType,
create_interpreter_wrapper,
) )
from .docker_util import ( from .docker_util import (
@ -244,7 +248,7 @@ def delegate_venv(args, # type: EnvironmentConfig
with tempdir() as inject_path: with tempdir() as inject_path:
for version, path in venvs.items(): for version, path in venvs.items():
os.symlink(os.path.join(path, 'bin', 'python'), os.path.join(inject_path, 'python%s' % version)) create_interpreter_wrapper(os.path.join(path, 'bin', 'python'), os.path.join(inject_path, 'python%s' % version))
python_interpreter = os.path.join(inject_path, 'python%s' % args.python_version) python_interpreter = os.path.join(inject_path, 'python%s' % args.python_version)
@ -255,8 +259,15 @@ def delegate_venv(args, # type: EnvironmentConfig
cmd += ['--coverage-label', 'venv'] cmd += ['--coverage-label', 'venv']
env = common_environment() env = common_environment()
with tempdir() as library_path:
# expose ansible and ansible_test to the virtual environment (only required when running from an install)
os.symlink(ANSIBLE_LIB_ROOT, os.path.join(library_path, 'ansible'))
os.symlink(ANSIBLE_TEST_ROOT, os.path.join(library_path, 'ansible_test'))
env.update( env.update(
PATH=inject_path + os.pathsep + env['PATH'], PATH=inject_path + os.pathsep + env['PATH'],
PYTHONPATH=library_path,
) )
run_command(args, cmd, env=env) run_command(args, cmd, env=env)

@ -89,8 +89,6 @@ def command_sanity(args):
if args.delegate: if args.delegate:
raise Delegate(require=changes, exclude=args.exclude) raise Delegate(require=changes, exclude=args.exclude)
install_command_requirements(args)
tests = sanity_get_tests() tests = sanity_get_tests()
if args.test: if args.test:
@ -108,6 +106,8 @@ def command_sanity(args):
total = 0 total = 0
failed = [] failed = []
requirements_installed = set() # type: t.Set[str]
for test in tests: for test in tests:
if args.list_tests: if args.list_tests:
display.info(test.name) display.info(test.name)
@ -180,6 +180,10 @@ def command_sanity(args):
sanity_targets = SanityTargets(tuple(all_targets), tuple(usable_targets)) sanity_targets = SanityTargets(tuple(all_targets), tuple(usable_targets))
if usable_targets or test.no_targets: if usable_targets or test.no_targets:
if version not in requirements_installed:
requirements_installed.add(version)
install_command_requirements(args, version)
if isinstance(test, SanityCodeSmellTest): if isinstance(test, SanityCodeSmellTest):
result = test.test(args, sanity_targets, version) result = test.test(args, sanity_targets, version)
elif isinstance(test, SanityMultipleVersion): elif isinstance(test, SanityMultipleVersion):

@ -7,6 +7,7 @@ import contextlib
import json import json
import os import os
import shutil import shutil
import sys
import tempfile import tempfile
import textwrap import textwrap
@ -204,6 +205,24 @@ def get_python_path(args, interpreter):
else: else:
display.info('Injecting "%s" as a execv wrapper for the "%s" interpreter.' % (injected_interpreter, interpreter), verbosity=1) display.info('Injecting "%s" as a execv wrapper for the "%s" interpreter.' % (injected_interpreter, interpreter), verbosity=1)
create_interpreter_wrapper(interpreter, injected_interpreter)
os.chmod(python_path, MODE_DIRECTORY)
if not PYTHON_PATHS:
atexit.register(cleanup_python_paths)
PYTHON_PATHS[interpreter] = python_path
return python_path
def create_interpreter_wrapper(interpreter, injected_interpreter): # type: (str, str) -> None
"""Create a wrapper for the given Python interpreter at the specified path."""
# sys.executable is used for the shebang to guarantee it is a binary instead of a script
# injected_interpreter could be a script from the system or our own wrapper created for the --venv option
shebang_interpreter = sys.executable
code = textwrap.dedent(''' code = textwrap.dedent('''
#!%s #!%s
@ -215,21 +234,12 @@ def get_python_path(args, interpreter):
python = '%s' python = '%s'
execv(python, [python] + argv[1:]) execv(python, [python] + argv[1:])
''' % (interpreter, interpreter)).lstrip() ''' % (shebang_interpreter, interpreter)).lstrip()
write_text_file(injected_interpreter, code) write_text_file(injected_interpreter, code)
os.chmod(injected_interpreter, MODE_FILE_EXECUTE) os.chmod(injected_interpreter, MODE_FILE_EXECUTE)
os.chmod(python_path, MODE_DIRECTORY)
if not PYTHON_PATHS:
atexit.register(cleanup_python_paths)
PYTHON_PATHS[interpreter] = python_path
return python_path
def cleanup_python_paths(): def cleanup_python_paths():
"""Clean up all temporary python directories.""" """Clean up all temporary python directories."""

Loading…
Cancel
Save