ansible-test - Fix subprocess management. (#77638)

* Run code-smell sanity tests in UTF-8 Mode.
* Update subprocess use in sanity test programs.
* Use raw_command instead of run_command with always=True set.
* Add more capture=True usage.
* Don't expose stdin to subprocesses.
* Capture more output. Warn on retry.
* Add more captures.
* Capture coverage cli output.
* Capture windows and network host checks.
* Be explicit about interactive usage.
* Use a shell for non-captured, non-interactive subprocesses.
* Add integration test to assert no TTY.
* Add unit test to assert no TTY.
* Require blocking stdin/stdout/stderr.
* Use subprocess.run in ansible-core sanity tests.
* Remove unused arg.
* Be explicit with subprocess.run check=False.
* Add changelog.
pull/77639/head
Matt Clay 3 years ago committed by GitHub
parent b8793fa48d
commit 62d03c8e75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,10 @@
bugfixes:
- ansible-test - Subprocesses are now isolated from the stdin, stdout and stderr of ansible-test.
This avoids issues with subprocesses tampering with the file descriptors, such as SSH making them non-blocking.
As a result of this change, subprocess output from unit and integration tests on stderr now go to stdout.
- ansible-test - Subprocesses no longer have access to the TTY ansible-test is connected to, if any.
This maintains consistent behavior between local testing and CI systems, which typically do not provide a TTY.
Tests which require a TTY should use pexpect or another mechanism to create a PTY.
minor_changes:
- ansible-test - Blocking mode is now enforced for stdin, stdout and stderr.
If any of these are non-blocking then ansible-test will exit during startup with an error.

@ -0,0 +1,2 @@
context/controller
shippable/posix/group1

@ -0,0 +1,7 @@
#!/usr/bin/env python
import sys
assert not sys.stdin.isatty()
assert not sys.stdout.isatty()
assert not sys.stderr.isatty()

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -eux
./runme.py

@ -22,11 +22,11 @@ from .util import (
ANSIBLE_SOURCE_ROOT, ANSIBLE_SOURCE_ROOT,
ANSIBLE_TEST_TOOLS_ROOT, ANSIBLE_TEST_TOOLS_ROOT,
get_ansible_version, get_ansible_version,
raw_command,
) )
from .util_common import ( from .util_common import (
create_temp_dir, create_temp_dir,
run_command,
ResultType, ResultType,
intercept_python, intercept_python,
get_injector_path, get_injector_path,
@ -258,12 +258,12 @@ class CollectionDetailError(ApplicationError):
self.reason = reason self.reason = reason
def get_collection_detail(args, python): # type: (EnvironmentConfig, PythonConfig) -> CollectionDetail def get_collection_detail(python): # type: (PythonConfig) -> CollectionDetail
"""Return collection detail.""" """Return collection detail."""
collection = data_context().content.collection collection = data_context().content.collection
directory = os.path.join(collection.root, collection.directory) directory = os.path.join(collection.root, collection.directory)
stdout = run_command(args, [python.path, os.path.join(ANSIBLE_TEST_TOOLS_ROOT, 'collection_detail.py'), directory], capture=True, always=True)[0] stdout = raw_command([python.path, os.path.join(ANSIBLE_TEST_TOOLS_ROOT, 'collection_detail.py'), directory], capture=True)[0]
result = json.loads(stdout) result = json.loads(stdout)
error = result.get('error') error = result.get('error')

@ -100,7 +100,16 @@ def run_coverage(args, host_state, output_file, command, cmd): # type: (Coverag
cmd = ['python', '-m', 'coverage.__main__', command, '--rcfile', COVERAGE_CONFIG_PATH] + cmd cmd = ['python', '-m', 'coverage.__main__', command, '--rcfile', COVERAGE_CONFIG_PATH] + cmd
intercept_python(args, host_state.controller_profile.python, cmd, env) stdout, stderr = intercept_python(args, host_state.controller_profile.python, cmd, env, capture=True)
stdout = (stdout or '').strip()
stderr = (stderr or '').strip()
if stdout:
display.info(stdout)
if stderr:
display.warning(stderr)
def get_all_coverage_files(): # type: () -> t.List[str] def get_all_coverage_files(): # type: () -> t.List[str]

@ -18,11 +18,11 @@ from ...util import (
ANSIBLE_TEST_TOOLS_ROOT, ANSIBLE_TEST_TOOLS_ROOT,
display, display,
ApplicationError, ApplicationError,
raw_command,
) )
from ...util_common import ( from ...util_common import (
ResultType, ResultType,
run_command,
write_json_file, write_json_file,
write_json_test_results, write_json_test_results,
) )
@ -196,7 +196,7 @@ def _command_coverage_combine_powershell(args): # type: (CoverageCombineConfig)
cmd = ['pwsh', os.path.join(ANSIBLE_TEST_TOOLS_ROOT, 'coverage_stub.ps1')] cmd = ['pwsh', os.path.join(ANSIBLE_TEST_TOOLS_ROOT, 'coverage_stub.ps1')]
cmd.extend(source_paths) cmd.extend(source_paths)
stubs = json.loads(run_command(args, cmd, capture=True, always=True)[0]) stubs = json.loads(raw_command(cmd, capture=True)[0])
return dict((d['Path'], dict((line, 0) for line in d['Lines'])) for d in stubs) return dict((d['Path'], dict((line, 0) for line in d['Lines'])) for d in stubs)

@ -106,7 +106,7 @@ class CsCloudProvider(CloudProvider):
# apply work-around for OverlayFS issue # apply work-around for OverlayFS issue
# https://github.com/docker/for-linux/issues/72#issuecomment-319904698 # https://github.com/docker/for-linux/issues/72#issuecomment-319904698
docker_exec(self.args, self.DOCKER_SIMULATOR_NAME, ['find', '/var/lib/mysql', '-type', 'f', '-exec', 'touch', '{}', ';']) docker_exec(self.args, self.DOCKER_SIMULATOR_NAME, ['find', '/var/lib/mysql', '-type', 'f', '-exec', 'touch', '{}', ';'], capture=True)
if self.args.explain: if self.args.explain:
values = dict( values = dict(

@ -952,6 +952,7 @@ class SanityCodeSmellTest(SanitySingleVersion):
cmd = [python.path, self.path] cmd = [python.path, self.path]
env = ansible_environment(args, color=False) env = ansible_environment(args, color=False)
env.update(PYTHONUTF8='1') # force all code-smell sanity tests to run with Python UTF-8 Mode enabled
pattern = None pattern = None
data = None data = None

@ -141,7 +141,7 @@ class PylintTest(SanitySingleVersion):
if data_context().content.collection: if data_context().content.collection:
try: try:
collection_detail = get_collection_detail(args, python) collection_detail = get_collection_detail(python)
if not collection_detail.version: if not collection_detail.version:
display.warning('Skipping pylint collection version checks since no collection version was found.') display.warning('Skipping pylint collection version checks since no collection version was found.')

@ -121,7 +121,7 @@ class ValidateModulesTest(SanitySingleVersion):
cmd.extend(['--collection', data_context().content.collection.directory]) cmd.extend(['--collection', data_context().content.collection.directory])
try: try:
collection_detail = get_collection_detail(args, python) collection_detail = get_collection_detail(python)
if collection_detail.version: if collection_detail.version:
cmd.extend(['--collection-version', collection_detail.version]) cmd.extend(['--collection-version', collection_detail.version])

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
import sys
import typing as t import typing as t
from ...util import ( from ...util import (
@ -44,6 +45,9 @@ def command_shell(args): # type: (ShellConfig) -> None
if args.raw and isinstance(args.targets[0], ControllerConfig): if args.raw and isinstance(args.targets[0], ControllerConfig):
raise ApplicationError('The --raw option has no effect on the controller.') raise ApplicationError('The --raw option has no effect on the controller.')
if not sys.stdin.isatty():
raise ApplicationError('Standard input must be a TTY to launch a shell.')
host_state = prepare_profiles(args, skip_setup=args.raw) # shell host_state = prepare_profiles(args, skip_setup=args.raw) # shell
if args.delegate: if args.delegate:
@ -87,4 +91,4 @@ def command_shell(args): # type: (ShellConfig) -> None
else: else:
cmd = [] cmd = []
con.run(cmd) con.run(cmd, interactive=True)

@ -238,6 +238,7 @@ class ShellConfig(EnvironmentConfig):
super().__init__(args, 'shell') super().__init__(args, 'shell')
self.raw = args.raw # type: bool self.raw = args.raw # type: bool
self.interactive = True
class SanityConfig(TestConfig): class SanityConfig(TestConfig):

@ -3,7 +3,6 @@ from __future__ import annotations
import abc import abc
import shlex import shlex
import sys
import tempfile import tempfile
import typing as t import typing as t
@ -47,6 +46,7 @@ class Connection(metaclass=abc.ABCMeta):
def run(self, def run(self,
command, # type: t.List[str] command, # type: t.List[str]
capture=False, # type: bool capture=False, # type: bool
interactive=False, # type: bool
data=None, # type: t.Optional[str] data=None, # type: t.Optional[str]
stdin=None, # type: t.Optional[t.IO[bytes]] stdin=None, # type: t.Optional[t.IO[bytes]]
stdout=None, # type: t.Optional[t.IO[bytes]] stdout=None, # type: t.Optional[t.IO[bytes]]
@ -60,7 +60,7 @@ class Connection(metaclass=abc.ABCMeta):
"""Extract the given archive file stream in the specified directory.""" """Extract the given archive file stream in the specified directory."""
tar_cmd = ['tar', 'oxzf', '-', '-C', chdir] tar_cmd = ['tar', 'oxzf', '-', '-C', chdir]
retry(lambda: self.run(tar_cmd, stdin=src)) retry(lambda: self.run(tar_cmd, stdin=src, capture=True))
def create_archive(self, def create_archive(self,
chdir, # type: str chdir, # type: str
@ -82,7 +82,7 @@ class Connection(metaclass=abc.ABCMeta):
sh_cmd = ['sh', '-c', ' | '.join(' '.join(shlex.quote(cmd) for cmd in command) for command in commands)] sh_cmd = ['sh', '-c', ' | '.join(' '.join(shlex.quote(cmd) for cmd in command) for command in commands)]
retry(lambda: self.run(sh_cmd, stdout=dst)) retry(lambda: self.run(sh_cmd, stdout=dst, capture=True))
class LocalConnection(Connection): class LocalConnection(Connection):
@ -93,6 +93,7 @@ class LocalConnection(Connection):
def run(self, def run(self,
command, # type: t.List[str] command, # type: t.List[str]
capture=False, # type: bool capture=False, # type: bool
interactive=False, # type: bool
data=None, # type: t.Optional[str] data=None, # type: t.Optional[str]
stdin=None, # type: t.Optional[t.IO[bytes]] stdin=None, # type: t.Optional[t.IO[bytes]]
stdout=None, # type: t.Optional[t.IO[bytes]] stdout=None, # type: t.Optional[t.IO[bytes]]
@ -105,6 +106,7 @@ class LocalConnection(Connection):
data=data, data=data,
stdin=stdin, stdin=stdin,
stdout=stdout, stdout=stdout,
interactive=interactive,
) )
@ -131,6 +133,7 @@ class SshConnection(Connection):
def run(self, def run(self,
command, # type: t.List[str] command, # type: t.List[str]
capture=False, # type: bool capture=False, # type: bool
interactive=False, # type: bool
data=None, # type: t.Optional[str] data=None, # type: t.Optional[str]
stdin=None, # type: t.Optional[t.IO[bytes]] stdin=None, # type: t.Optional[t.IO[bytes]]
stdout=None, # type: t.Optional[t.IO[bytes]] stdout=None, # type: t.Optional[t.IO[bytes]]
@ -143,7 +146,7 @@ class SshConnection(Connection):
options.append('-q') options.append('-q')
if not data and not stdin and not stdout and sys.stdin.isatty(): if interactive:
options.append('-tt') options.append('-tt')
with tempfile.NamedTemporaryFile(prefix='ansible-test-ssh-debug-', suffix='.log') as ssh_logfile: with tempfile.NamedTemporaryFile(prefix='ansible-test-ssh-debug-', suffix='.log') as ssh_logfile:
@ -166,6 +169,7 @@ class SshConnection(Connection):
data=data, data=data,
stdin=stdin, stdin=stdin,
stdout=stdout, stdout=stdout,
interactive=interactive,
error_callback=error_callback, error_callback=error_callback,
) )
@ -209,6 +213,7 @@ class DockerConnection(Connection):
def run(self, def run(self,
command, # type: t.List[str] command, # type: t.List[str]
capture=False, # type: bool capture=False, # type: bool
interactive=False, # type: bool
data=None, # type: t.Optional[str] data=None, # type: t.Optional[str]
stdin=None, # type: t.Optional[t.IO[bytes]] stdin=None, # type: t.Optional[t.IO[bytes]]
stdout=None, # type: t.Optional[t.IO[bytes]] stdout=None, # type: t.Optional[t.IO[bytes]]
@ -219,7 +224,7 @@ class DockerConnection(Connection):
if self.user: if self.user:
options.extend(['--user', self.user]) options.extend(['--user', self.user])
if not data and not stdin and not stdout and sys.stdin.isatty(): if interactive:
options.append('-it') options.append('-it')
return docker_exec( return docker_exec(
@ -231,6 +236,7 @@ class DockerConnection(Connection):
data=data, data=data,
stdin=stdin, stdin=stdin,
stdout=stdout, stdout=stdout,
interactive=interactive,
) )
def inspect(self): # type: () -> DockerInspect def inspect(self): # type: () -> DockerInspect

@ -469,7 +469,7 @@ class SshKey:
make_dirs(os.path.dirname(key)) make_dirs(os.path.dirname(key))
if not os.path.isfile(key) or not os.path.isfile(pub): if not os.path.isfile(key) or not os.path.isfile(pub):
run_command(args, ['ssh-keygen', '-m', 'PEM', '-q', '-t', self.KEY_TYPE, '-N', '', '-f', key]) run_command(args, ['ssh-keygen', '-m', 'PEM', '-q', '-t', self.KEY_TYPE, '-N', '', '-f', key], capture=True)
if args.explain: if args.explain:
return key, pub return key, pub

@ -160,11 +160,12 @@ def delegate_command(args, host_state, exclude, require): # type: (EnvironmentC
os.path.join(content_root, ResultType.COVERAGE.relative_path), os.path.join(content_root, ResultType.COVERAGE.relative_path),
] ]
con.run(['mkdir', '-p'] + writable_dirs) con.run(['mkdir', '-p'] + writable_dirs, capture=True)
con.run(['chmod', '777'] + writable_dirs) con.run(['chmod', '777'] + writable_dirs, capture=True)
con.run(['chmod', '755', working_directory]) con.run(['chmod', '755', working_directory], capture=True)
con.run(['chmod', '644', os.path.join(content_root, args.metadata_path)]) con.run(['chmod', '644', os.path.join(content_root, args.metadata_path)], capture=True)
con.run(['useradd', pytest_user, '--create-home']) con.run(['useradd', pytest_user, '--create-home'], capture=True)
con.run(insert_options(command, options + ['--requirements-mode', 'only'])) con.run(insert_options(command, options + ['--requirements-mode', 'only']))
container = con.inspect() container = con.inspect()
@ -191,7 +192,7 @@ def delegate_command(args, host_state, exclude, require): # type: (EnvironmentC
success = False success = False
try: try:
con.run(insert_options(command, options)) con.run(insert_options(command, options), interactive=args.interactive)
success = True success = True
finally: finally:
if host_delegation: if host_delegation:

@ -279,7 +279,7 @@ def docker_pull(args, image): # type: (EnvironmentConfig, str) -> None
def docker_cp_to(args, container_id, src, dst): # type: (EnvironmentConfig, str, str, str) -> None def docker_cp_to(args, container_id, src, dst): # type: (EnvironmentConfig, str, str, str) -> None
"""Copy a file to the specified container.""" """Copy a file to the specified container."""
docker_command(args, ['cp', src, '%s:%s' % (container_id, dst)]) docker_command(args, ['cp', src, '%s:%s' % (container_id, dst)], capture=True)
def docker_run( def docker_run(
@ -514,6 +514,7 @@ def docker_exec(
capture=False, # type: bool capture=False, # type: bool
stdin=None, # type: t.Optional[t.IO[bytes]] stdin=None, # type: t.Optional[t.IO[bytes]]
stdout=None, # type: t.Optional[t.IO[bytes]] stdout=None, # type: t.Optional[t.IO[bytes]]
interactive=False, # type: bool
data=None, # type: t.Optional[str] data=None, # type: t.Optional[str]
): # type: (...) -> t.Tuple[t.Optional[str], t.Optional[str]] ): # type: (...) -> t.Tuple[t.Optional[str], t.Optional[str]]
"""Execute the given command in the specified container.""" """Execute the given command in the specified container."""
@ -523,7 +524,7 @@ def docker_exec(
if data or stdin or stdout: if data or stdin or stdout:
options.append('-i') options.append('-i')
return docker_command(args, ['exec'] + options + [container_id] + cmd, capture=capture, stdin=stdin, stdout=stdout, data=data) return docker_command(args, ['exec'] + options + [container_id] + cmd, capture=capture, stdin=stdin, stdout=stdout, interactive=interactive, data=data)
def docker_info(args): # type: (CommonConfig) -> t.Dict[str, t.Any] def docker_info(args): # type: (CommonConfig) -> t.Dict[str, t.Any]
@ -544,6 +545,7 @@ def docker_command(
capture=False, # type: bool capture=False, # type: bool
stdin=None, # type: t.Optional[t.IO[bytes]] stdin=None, # type: t.Optional[t.IO[bytes]]
stdout=None, # type: t.Optional[t.IO[bytes]] stdout=None, # type: t.Optional[t.IO[bytes]]
interactive=False, # type: bool
always=False, # type: bool always=False, # type: bool
data=None, # type: t.Optional[str] data=None, # type: t.Optional[str]
): # type: (...) -> t.Tuple[t.Optional[str], t.Optional[str]] ): # type: (...) -> t.Tuple[t.Optional[str], t.Optional[str]]
@ -552,7 +554,7 @@ def docker_command(
command = [require_docker().command] command = [require_docker().command]
if command[0] == 'podman' and _get_podman_remote(): if command[0] == 'podman' and _get_podman_remote():
command.append('--remote') command.append('--remote')
return run_command(args, command + cmd, env=env, capture=capture, stdin=stdin, stdout=stdout, always=always, data=data) return run_command(args, command + cmd, env=env, capture=capture, stdin=stdin, stdout=stdout, interactive=interactive, always=always, data=data)
def docker_environment(): # type: () -> t.Dict[str, str] def docker_environment(): # type: () -> t.Dict[str, str]

@ -484,8 +484,9 @@ class NetworkRemoteProfile(RemoteProfile[NetworkRemoteConfig]):
for dummy in range(1, 90): for dummy in range(1, 90):
try: try:
intercept_python(self.args, self.args.controller_python, cmd, env) intercept_python(self.args, self.args.controller_python, cmd, env, capture=True)
except SubprocessError: except SubprocessError as ex:
display.warning(str(ex))
time.sleep(10) time.sleep(10)
else: else:
return return
@ -717,8 +718,9 @@ class WindowsRemoteProfile(RemoteProfile[WindowsRemoteConfig]):
for dummy in range(1, 120): for dummy in range(1, 120):
try: try:
intercept_python(self.args, self.args.controller_python, cmd, env) intercept_python(self.args, self.args.controller_python, cmd, env, capture=True)
except SubprocessError: except SubprocessError as ex:
display.warning(str(ex))
time.sleep(10) time.sleep(10)
else: else:
return return

@ -261,6 +261,7 @@ def raw_command(
explain=False, # type: bool explain=False, # type: bool
stdin=None, # type: t.Optional[t.Union[t.IO[bytes], int]] stdin=None, # type: t.Optional[t.Union[t.IO[bytes], int]]
stdout=None, # type: t.Optional[t.Union[t.IO[bytes], int]] stdout=None, # type: t.Optional[t.Union[t.IO[bytes], int]]
interactive=False, # type: bool
cmd_verbosity=1, # type: int cmd_verbosity=1, # type: int
str_errors='strict', # type: str str_errors='strict', # type: str
error_callback=None, # type: t.Optional[t.Callable[[SubprocessError], None]] error_callback=None, # type: t.Optional[t.Callable[[SubprocessError], None]]
@ -274,6 +275,14 @@ def raw_command(
cmd = list(cmd) cmd = list(cmd)
if not capture and not interactive:
# When not capturing stdout/stderr and not running interactively, send subprocess stdout/stderr through a shell to the current stdout.
# Additionally send output through a pipe to `tee` to block access to the current TTY, if any.
# This prevents subprocesses from sharing stdin/stdout/stderr with the current process or each other.
# Doing so allows subprocesses to safely make changes to their file handles, such as making them non-blocking (ssh does this).
# This also maintains consistency between local testing and CI systems, which typically do not provide a TTY.
cmd = ['/bin/sh', '-c', f'{shlex.join(cmd)} 2>&1 | tee']
escaped_cmd = ' '.join(shlex.quote(c) for c in cmd) escaped_cmd = ' '.join(shlex.quote(c) for c in cmd)
display.info('Run command: %s' % escaped_cmd, verbosity=cmd_verbosity, truncate=True) display.info('Run command: %s' % escaped_cmd, verbosity=cmd_verbosity, truncate=True)
@ -298,6 +307,10 @@ def raw_command(
elif data is not None: elif data is not None:
stdin = subprocess.PIPE stdin = subprocess.PIPE
communicate = True communicate = True
elif interactive:
pass # allow the subprocess access to our stdin
else:
stdin = subprocess.DEVNULL
if stdout: if stdout:
communicate = True communicate = True
@ -654,12 +667,15 @@ class MissingEnvironmentVariable(ApplicationError):
self.name = name self.name = name
def retry(func, ex_type=SubprocessError, sleep=10, attempts=10): def retry(func, ex_type=SubprocessError, sleep=10, attempts=10, warn=True):
"""Retry the specified function on failure.""" """Retry the specified function on failure."""
for dummy in range(1, attempts): for dummy in range(1, attempts):
try: try:
return func() return func()
except ex_type: except ex_type as ex:
if warn:
display.warning(str(ex))
time.sleep(sleep) time.sleep(sleep)
return func() return func()

@ -126,6 +126,7 @@ class CommonConfig:
"""Configuration common to all commands.""" """Configuration common to all commands."""
def __init__(self, args, command): # type: (t.Any, str) -> None def __init__(self, args, command): # type: (t.Any, str) -> None
self.command = command self.command = command
self.interactive = False
self.success = None # type: t.Optional[bool] self.success = None # type: t.Optional[bool]
self.color = args.color # type: bool self.color = args.color # type: bool
@ -406,13 +407,14 @@ def run_command(
always=False, # type: bool always=False, # type: bool
stdin=None, # type: t.Optional[t.IO[bytes]] stdin=None, # type: t.Optional[t.IO[bytes]]
stdout=None, # type: t.Optional[t.IO[bytes]] stdout=None, # type: t.Optional[t.IO[bytes]]
interactive=False, # type: bool
cmd_verbosity=1, # type: int cmd_verbosity=1, # type: int
str_errors='strict', # type: str str_errors='strict', # type: str
error_callback=None, # type: t.Optional[t.Callable[[SubprocessError], None]] error_callback=None, # type: t.Optional[t.Callable[[SubprocessError], None]]
): # type: (...) -> t.Tuple[t.Optional[str], t.Optional[str]] ): # type: (...) -> t.Tuple[t.Optional[str], t.Optional[str]]
"""Run the specified command and return stdout and stderr as a tuple.""" """Run the specified command and return stdout and stderr as a tuple."""
explain = args.explain and not always explain = args.explain and not always
return raw_command(cmd, capture=capture, env=env, data=data, cwd=cwd, explain=explain, stdin=stdin, stdout=stdout, return raw_command(cmd, capture=capture, env=env, data=data, cwd=cwd, explain=explain, stdin=stdin, stdout=stdout, interactive=interactive,
cmd_verbosity=cmd_verbosity, str_errors=str_errors, error_callback=error_callback) cmd_verbosity=cmd_verbosity, str_errors=str_errors, error_callback=error_callback)

@ -20,6 +20,7 @@ from .util import (
remove_tree, remove_tree,
ApplicationError, ApplicationError,
str_to_version, str_to_version,
raw_command,
) )
from .util_common import ( from .util_common import (
@ -92,7 +93,7 @@ def create_virtual_environment(args, # type: EnvironmentConfig
# creating a virtual environment using 'venv' when running in a virtual environment created by 'virtualenv' results # creating a virtual environment using 'venv' when running in a virtual environment created by 'virtualenv' results
# in a copy of the original virtual environment instead of creation of a new one # in a copy of the original virtual environment instead of creation of a new one
# avoid this issue by only using "real" python interpreters to invoke 'venv' # avoid this issue by only using "real" python interpreters to invoke 'venv'
for real_python in iterate_real_pythons(args, python.version): for real_python in iterate_real_pythons(python.version):
if run_venv(args, real_python, system_site_packages, pip, path): if run_venv(args, real_python, system_site_packages, pip, path):
display.info('Created Python %s virtual environment using "venv": %s' % (python.version, path), verbosity=1) display.info('Created Python %s virtual environment using "venv": %s' % (python.version, path), verbosity=1)
return True return True
@ -128,7 +129,7 @@ def create_virtual_environment(args, # type: EnvironmentConfig
return False return False
def iterate_real_pythons(args, version): # type: (EnvironmentConfig, str) -> t.Iterable[str] def iterate_real_pythons(version): # type: (str) -> t.Iterable[str]
""" """
Iterate through available real python interpreters of the requested version. Iterate through available real python interpreters of the requested version.
The current interpreter will be checked and then the path will be searched. The current interpreter will be checked and then the path will be searched.
@ -138,7 +139,7 @@ def iterate_real_pythons(args, version): # type: (EnvironmentConfig, str) -> t.
if version_info == sys.version_info[:len(version_info)]: if version_info == sys.version_info[:len(version_info)]:
current_python = sys.executable current_python = sys.executable
real_prefix = get_python_real_prefix(args, current_python) real_prefix = get_python_real_prefix(current_python)
if real_prefix: if real_prefix:
current_python = find_python(version, os.path.join(real_prefix, 'bin')) current_python = find_python(version, os.path.join(real_prefix, 'bin'))
@ -159,7 +160,7 @@ def iterate_real_pythons(args, version): # type: (EnvironmentConfig, str) -> t.
if found_python == current_python: if found_python == current_python:
return return
real_prefix = get_python_real_prefix(args, found_python) real_prefix = get_python_real_prefix(found_python)
if real_prefix: if real_prefix:
found_python = find_python(version, os.path.join(real_prefix, 'bin')) found_python = find_python(version, os.path.join(real_prefix, 'bin'))
@ -168,12 +169,12 @@ def iterate_real_pythons(args, version): # type: (EnvironmentConfig, str) -> t.
yield found_python yield found_python
def get_python_real_prefix(args, python_path): # type: (EnvironmentConfig, str) -> t.Optional[str] def get_python_real_prefix(python_path): # type: (str) -> t.Optional[str]
""" """
Return the real prefix of the specified interpreter or None if the interpreter is not a virtual environment created by 'virtualenv'. Return the real prefix of the specified interpreter or None if the interpreter is not a virtual environment created by 'virtualenv'.
""" """
cmd = [python_path, os.path.join(os.path.join(ANSIBLE_TEST_TARGET_TOOLS_ROOT, 'virtualenvcheck.py'))] cmd = [python_path, os.path.join(os.path.join(ANSIBLE_TEST_TARGET_TOOLS_ROOT, 'virtualenvcheck.py'))]
check_result = json.loads(run_command(args, cmd, capture=True, always=True)[0]) check_result = json.loads(raw_command(cmd, capture=True)[0])
real_prefix = check_result['real_prefix'] real_prefix = check_result['real_prefix']
return real_prefix return real_prefix

@ -47,7 +47,11 @@ def main():
env = os.environ.copy() env = os.environ.copy()
env.update(PYTHONPATH='%s:%s' % (os.path.join(os.path.dirname(__file__), 'changelog'), env['PYTHONPATH'])) env.update(PYTHONPATH='%s:%s' % (os.path.join(os.path.dirname(__file__), 'changelog'), env['PYTHONPATH']))
subprocess.call(cmd, env=env) # ignore the return code, rely on the output instead # ignore the return code, rely on the output instead
process = subprocess.run(cmd, stdin=subprocess.DEVNULL, capture_output=True, text=True, env=env, check=False)
sys.stdout.write(process.stdout)
sys.stderr.write(process.stderr)
if __name__ == '__main__': if __name__ == '__main__':

@ -436,14 +436,13 @@ class ModuleValidator(Validator):
base_path = self._get_base_branch_module_path() base_path = self._get_base_branch_module_path()
command = ['git', 'show', '%s:%s' % (self.base_branch, base_path or self.path)] command = ['git', 'show', '%s:%s' % (self.base_branch, base_path or self.path)]
p = subprocess.Popen(command, stdout=subprocess.PIPE, p = subprocess.run(command, stdin=subprocess.DEVNULL, capture_output=True, check=False)
stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if int(p.returncode) != 0: if int(p.returncode) != 0:
return None return None
t = tempfile.NamedTemporaryFile(delete=False) t = tempfile.NamedTemporaryFile(delete=False)
t.write(stdout) t.write(p.stdout)
t.close() t.close()
return t.name return t.name
@ -2456,11 +2455,12 @@ class GitCache:
@staticmethod @staticmethod
def _git(args): def _git(args):
cmd = ['git'] + args cmd = ['git'] + args
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p = subprocess.run(cmd, stdin=subprocess.DEVNULL, capture_output=True, text=True, check=False)
stdout, stderr = p.communicate()
if p.returncode != 0: if p.returncode != 0:
raise GitError(stderr, p.returncode) raise GitError(p.stderr, p.returncode)
return stdout.decode('utf-8').splitlines()
return p.stdout.splitlines()
class GitError(Exception): class GitError(Exception):

@ -122,14 +122,12 @@ def get_ps_argument_spec(filename, collection):
}) })
script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ps_argspec.ps1') script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ps_argspec.ps1')
proc = subprocess.Popen(['pwsh', script_path, util_manifest], stdout=subprocess.PIPE, stderr=subprocess.PIPE, proc = subprocess.run(['pwsh', script_path, util_manifest], stdin=subprocess.DEVNULL, capture_output=True, text=True, check=False)
shell=False)
stdout, stderr = proc.communicate()
if proc.returncode != 0: if proc.returncode != 0:
raise AnsibleModuleImportError("STDOUT:\n%s\nSTDERR:\n%s" % (stdout.decode('utf-8'), stderr.decode('utf-8'))) raise AnsibleModuleImportError("STDOUT:\n%s\nSTDERR:\n%s" % (proc.stdout, proc.stderr))
kwargs = json.loads(stdout) kwargs = json.loads(proc.stdout)
# the validate-modules code expects the options spec to be under the argument_spec key not options as set in PS # the validate-modules code expects the options spec to be under the argument_spec key not options as set in PS
kwargs['argument_spec'] = kwargs.pop('options', {}) kwargs['argument_spec'] = kwargs.pop('options', {})

@ -27,6 +27,9 @@ def main(args=None):
raise SystemExit('This version of ansible-test cannot be executed with Python version %s. Supported Python versions are: %s' % ( raise SystemExit('This version of ansible-test cannot be executed with Python version %s. Supported Python versions are: %s' % (
version_to_str(sys.version_info[:3]), ', '.join(CONTROLLER_PYTHON_VERSIONS))) version_to_str(sys.version_info[:3]), ', '.join(CONTROLLER_PYTHON_VERSIONS)))
if any(not os.get_blocking(handle.fileno()) for handle in (sys.stdin, sys.stdout, sys.stderr)):
raise SystemExit('Standard input, output and error file handles must be blocking to run ansible-test.')
# noinspection PyProtectedMember # noinspection PyProtectedMember
from ansible_test._internal import main as cli_main from ansible_test._internal import main as cli_main

@ -29,13 +29,12 @@ def main():
try: try:
cmd = ['make', 'core_singlehtmldocs'] cmd = ['make', 'core_singlehtmldocs']
sphinx = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=docs_dir) sphinx = subprocess.run(cmd, stdin=subprocess.DEVNULL, capture_output=True, cwd=docs_dir, check=False, text=True)
stdout, stderr = sphinx.communicate()
finally: finally:
shutil.move(tmp, requirements_txt) shutil.move(tmp, requirements_txt)
stdout = stdout.decode('utf-8') stdout = sphinx.stdout
stderr = stderr.decode('utf-8') stderr = sphinx.stderr
if sphinx.returncode != 0: if sphinx.returncode != 0:
sys.stderr.write("Command '%s' failed with status code: %d\n" % (' '.join(cmd), sphinx.returncode)) sys.stderr.write("Command '%s' failed with status code: %d\n" % (' '.join(cmd), sphinx.returncode))

@ -172,14 +172,15 @@ def clean_repository(file_list):
def create_sdist(tmp_dir): def create_sdist(tmp_dir):
"""Create an sdist in the repository""" """Create an sdist in the repository"""
create = subprocess.Popen( create = subprocess.run(
['make', 'snapshot', 'SDIST_DIR=%s' % tmp_dir], ['make', 'snapshot', 'SDIST_DIR=%s' % tmp_dir],
stdout=subprocess.PIPE, stdin=subprocess.DEVNULL,
stderr=subprocess.PIPE, capture_output=True,
universal_newlines=True, text=True,
check=False,
) )
stderr = create.communicate()[1] stderr = create.stderr
if create.returncode != 0: if create.returncode != 0:
raise Exception('make snapshot failed:\n%s' % stderr) raise Exception('make snapshot failed:\n%s' % stderr)
@ -220,15 +221,16 @@ def extract_sdist(sdist_path, tmp_dir):
def install_sdist(tmp_dir, sdist_dir): def install_sdist(tmp_dir, sdist_dir):
"""Install the extracted sdist into the temporary directory""" """Install the extracted sdist into the temporary directory"""
install = subprocess.Popen( install = subprocess.run(
['python', 'setup.py', 'install', '--root=%s' % tmp_dir], ['python', 'setup.py', 'install', '--root=%s' % tmp_dir],
stdout=subprocess.PIPE, stdin=subprocess.DEVNULL,
stderr=subprocess.PIPE, capture_output=True,
universal_newlines=True, text=True,
cwd=os.path.join(tmp_dir, sdist_dir), cwd=os.path.join(tmp_dir, sdist_dir),
check=False,
) )
stdout, stderr = install.communicate() stdout, stderr = install.stdout, install.stderr
if install.returncode != 0: if install.returncode != 0:
raise Exception('sdist install failed:\n%s' % stderr) raise Exception('sdist install failed:\n%s' % stderr)

@ -0,0 +1,7 @@
import sys
def test_no_tty():
assert not sys.stdin.isatty()
assert not sys.stdout.isatty()
assert not sys.stderr.isatty()
Loading…
Cancel
Save