@ -11,12 +11,7 @@ import tempfile
import time
import textwrap
import functools
import shutil
import stat
import pipes
import random
import string
import atexit
import hashlib
import lib . pytar
@ -45,11 +40,12 @@ from lib.util import (
SubprocessError ,
display ,
run_command ,
common_environment ,
intercept_command ,
remove_tree ,
make_dirs ,
is_shippable ,
is_binary_file ,
find_pip ,
find_executable ,
raw_command ,
)
@ -110,8 +106,6 @@ SUPPORTED_PYTHON_VERSIONS = (
COMPILE_PYTHON_VERSIONS = SUPPORTED_PYTHON_VERSIONS
coverage_path = ' ' # pylint: disable=locally-disabled, invalid-name
def check_startup ( ) :
""" Checks to perform at startup before running commands. """
@ -163,23 +157,27 @@ def install_command_requirements(args):
if args . junit :
packages . append ( ' junit-xml ' )
commands = [ generate_pip_install ( args . command , packages = packages ) ]
pip = find_pip ( version = args . python_version )
commands = [ generate_pip_install ( pip , args . command , packages = packages ) ]
if isinstance ( args , IntegrationConfig ) :
for cloud_platform in get_cloud_platforms ( args ) :
commands . append ( generate_pip_install ( ' %s .cloud. %s ' % ( args . command , cloud_platform ) ) )
commands . append ( generate_pip_install ( pip , ' %s .cloud. %s ' % ( args . command , cloud_platform ) ) )
commands = [ cmd for cmd in commands if cmd ]
# only look for changes when more than one requirements file is needed
detect_pip_changes = len ( commands ) > 1
# first pass to install requirements, changes expected unless environment is already set up
changes = run_pip_commands ( args , commands, detect_pip_changes )
changes = run_pip_commands ( args , pip, commands, detect_pip_changes )
if not changes :
return # no changes means we can stop early
# second pass to check for conflicts in requirements, changes are not expected here
changes = run_pip_commands ( args , commands, detect_pip_changes )
changes = run_pip_commands ( args , pip, commands, detect_pip_changes )
if not changes :
return # no changes means no conflicts
@ -188,16 +186,17 @@ def install_command_requirements(args):
' \n ' . join ( ( ' ' . join ( pipes . quote ( c ) for c in cmd ) for cmd in changes ) ) )
def run_pip_commands ( args , commands, detect_pip_changes = False ) :
def run_pip_commands ( args , pip, commands, detect_pip_changes = False ) :
"""
: type args : EnvironmentConfig
: type pip : str
: type commands : list [ list [ str ] ]
: type detect_pip_changes : bool
: rtype : list [ list [ str ] ]
"""
changes = [ ]
after_list = pip_list ( args ) if detect_pip_changes else None
after_list = pip_list ( args , pip ) if detect_pip_changes else None
for cmd in commands :
if not cmd :
@ -217,10 +216,10 @@ def run_pip_commands(args, commands, detect_pip_changes=False):
# AttributeError: 'Requirement' object has no attribute 'project_name'
# See: https://bugs.launchpad.net/ubuntu/xenial/+source/python-pip/+bug/1626258
# Upgrading pip works around the issue.
run_command ( args , [ ' pip ' , ' install ' , ' --upgrade ' , ' pip ' ] )
run_command ( args , [ pip , ' install ' , ' --upgrade ' , ' pip ' ] )
run_command ( args , cmd )
after_list = pip_list ( args ) if detect_pip_changes else None
after_list = pip_list ( args , pip ) if detect_pip_changes else None
if before_list != after_list :
changes . append ( cmd )
@ -228,12 +227,13 @@ def run_pip_commands(args, commands, detect_pip_changes=False):
return changes
def pip_list ( args ):
def pip_list ( args , pip ):
"""
: type args : EnvironmentConfig
: type pip : str
: rtype : str
"""
stdout , _ = run_command ( args , [ ' pip ' , ' list ' ] , capture = True , always = True )
stdout , _ = run_command ( args , [ pip , ' list ' ] , capture = True )
return stdout
@ -244,14 +244,14 @@ def generate_egg_info(args):
if os . path . isdir ( ' lib/ansible.egg-info ' ) :
return
run_command ( args , [ ' python ' , ' setup.py ' , ' egg_info ' ] , capture = args . verbosity < 3 )
run_command ( args , [ ' python %s ' % args . python_version , ' setup.py ' , ' egg_info ' ] , capture = args . verbosity < 3 )
def generate_pip_install ( command, package s= None , extra s= None ) :
def generate_pip_install ( pip, command, package s= None ) :
"""
: type pip : str
: type command : str
: type packages : list [ str ] | None
: type extras : list [ str ] | None
: rtype : list [ str ] | None
"""
constraints = ' test/runner/requirements/constraints.txt '
@ -259,15 +259,8 @@ def generate_pip_install(command, packages=None, extras=None):
options = [ ]
requirements_list = [ requirements ]
if extras :
for extra in extras :
requirements_list . append ( ' test/runner/requirements/ %s . %s .txt ' % ( command , extra ) )
for requirements in requirements_list :
if os . path . exists ( requirements ) and os . path . getsize ( requirements ) :
options + = [ ' -r ' , requirements ]
if os . path . exists ( requirements ) and os . path . getsize ( requirements ) :
options + = [ ' -r ' , requirements ]
if packages :
options + = packages
@ -275,7 +268,7 @@ def generate_pip_install(command, packages=None, extras=None):
if not options :
return None
return [ ' pip ' , ' install ' , ' --disable-pip-version-check ' , ' -c ' , constraints ] + options
return [ pip , ' install ' , ' --disable-pip-version-check ' , ' -c ' , constraints ] + options
def command_shell ( args ) :
@ -323,31 +316,24 @@ def command_network_integration(args):
)
all_targets = tuple ( walk_network_integration_targets ( include_hidden = True ) )
internal_targets = command_integration_filter ( args , all_targets )
platform_targets = set ( a for t in internal_targets for a in t . aliases if a . startswith ( ' network/ ' ) )
internal_targets = command_integration_filter ( args , all_targets , init_callback = network_init )
if args . platform :
configs = dict ( ( config [ ' platform_version ' ] , config ) for config in args . metadata . instance_config )
instances = [ ] # type: list [lib.thread.WrappedThread]
# generate an ssh key (if needed) up front once, instead of for each instance
SshKey ( args )
for platform_version in args . platform :
platform , version = platform_version . split ( ' / ' , 1 )
platform_target = ' network/ %s / ' % platform
config = configs . get ( platform_version )
if platform_target not in platform_targets and ' network/basics/ ' not in platform_targets :
display . warning ( ' Skipping " %s " because selected tests do not target the " %s " platform. ' % (
platform_version , platform ) )
if not config :
continue
instance = lib . thread . WrappedThread ( functools . partial ( network_run , args , platform , version ))
instance = lib . thread . WrappedThread ( functools . partial ( network_run , args , platform , version , config ))
instance . daemon = True
instance . start ( )
instances . append ( instance )
install_command_requirements ( args )
while any ( instance . is_alive ( ) for instance in instances ) :
time . sleep ( 1 )
@ -359,22 +345,71 @@ def command_network_integration(args):
if not args . explain :
with open ( filename , ' w ' ) as inventory_fd :
inventory_fd . write ( inventory )
else :
install_command_requirements ( args )
command_integration_filtered ( args , internal_targets , all_targets )
def network_run ( args , platform , version ) :
def network_init ( args , internal_targets ) :
"""
: type args : NetworkIntegrationConfig
: type internal_targets : tuple [ IntegrationTarget ]
"""
if not args . platform :
return
if args . metadata . instance_config is not None :
return
platform_targets = set ( a for t in internal_targets for a in t . aliases if a . startswith ( ' network/ ' ) )
instances = [ ] # type: list [lib.thread.WrappedThread]
# generate an ssh key (if needed) up front once, instead of for each instance
SshKey ( args )
for platform_version in args . platform :
platform , version = platform_version . split ( ' / ' , 1 )
platform_target = ' network/ %s / ' % platform
if platform_target not in platform_targets and ' network/basics/ ' not in platform_targets :
display . warning ( ' Skipping " %s " because selected tests do not target the " %s " platform. ' % (
platform_version , platform ) )
continue
instance = lib . thread . WrappedThread ( functools . partial ( network_start , args , platform , version ) )
instance . daemon = True
instance . start ( )
instances . append ( instance )
while any ( instance . is_alive ( ) for instance in instances ) :
time . sleep ( 1 )
args . metadata . instance_config = [ instance . wait_for_result ( ) for instance in instances ]
def network_start ( args , platform , version ) :
"""
: type args : NetworkIntegrationConfig
: type platform : str
: type version : str
: rtype : AnsibleCoreCI
"""
core_ci = AnsibleCoreCI ( args , platform , version , stage = args . remote_stage )
core_ci . start ( )
return core_ci . save ( )
def network_run ( args , platform , version , config ) :
"""
: type args : NetworkIntegrationConfig
: type platform : str
: type version : str
: type config : dict [ str , str ]
: rtype : AnsibleCoreCI
"""
core_ci = AnsibleCoreCI ( args , platform , version , stage = args . remote_stage , load = False )
core_ci . load ( config )
core_ci . wait ( )
manage = ManageNetworkCI ( core_ci )
@ -431,19 +466,20 @@ def command_windows_integration(args):
raise ApplicationError ( ' Use the --windows option or provide an inventory file (see %s .template). ' % filename )
all_targets = tuple ( walk_windows_integration_targets ( include_hidden = True ) )
internal_targets = command_integration_filter ( args , all_targets )
internal_targets = command_integration_filter ( args , all_targets , init_callback = windows_init )
if args . windows :
configs = dict ( ( config [ ' platform_version ' ] , config ) for config in args . metadata . instance_config )
instances = [ ] # type: list [lib.thread.WrappedThread]
for version in args . windows :
instance = lib . thread . WrappedThread ( functools . partial ( windows_run , args , version ) )
config = configs [ ' windows/ %s ' % version ]
instance = lib . thread . WrappedThread ( functools . partial ( windows_run , args , version , config ) )
instance . daemon = True
instance . start ( )
instances . append ( instance )
install_command_requirements ( args )
while any ( instance . is_alive ( ) for instance in instances ) :
time . sleep ( 1 )
@ -455,16 +491,36 @@ def command_windows_integration(args):
if not args . explain :
with open ( filename , ' w ' ) as inventory_fd :
inventory_fd . write ( inventory )
else :
install_command_requirements ( args )
try :
command_integration_filtered ( args , internal_targets , all_targets )
finally :
pass
command_integration_filtered ( args , internal_targets , all_targets )
def windows_init ( args , internal_targets ) : # pylint: disable=locally-disabled, unused-argument
"""
: type args : WindowsIntegrationConfig
: type internal_targets : tuple [ IntegrationTarget ]
"""
if not args . windows :
return
if args . metadata . instance_config is not None :
return
instances = [ ] # type: list [lib.thread.WrappedThread]
for version in args . windows :
instance = lib . thread . WrappedThread ( functools . partial ( windows_start , args , version ) )
instance . daemon = True
instance . start ( )
instances . append ( instance )
while any ( instance . is_alive ( ) for instance in instances ) :
time . sleep ( 1 )
def windows_run ( args , version ) :
args . metadata . instance_config = [ instance . wait_for_result ( ) for instance in instances ]
def windows_start ( args , version ) :
"""
: type args : WindowsIntegrationConfig
: type version : str
@ -472,6 +528,19 @@ def windows_run(args, version):
"""
core_ci = AnsibleCoreCI ( args , ' windows ' , version , stage = args . remote_stage )
core_ci . start ( )
return core_ci . save ( )
def windows_run ( args , version , config ) :
"""
: type args : WindowsIntegrationConfig
: type version : str
: type config : dict [ str , str ]
: rtype : AnsibleCoreCI
"""
core_ci = AnsibleCoreCI ( args , ' windows ' , version , stage = args . remote_stage , load = False )
core_ci . load ( config )
core_ci . wait ( )
manage = ManageWindowsCI ( core_ci )
@ -525,10 +594,11 @@ def windows_inventory(remotes):
return inventory
def command_integration_filter ( args , targets ):
def command_integration_filter ( args , targets , init_callback = None ):
"""
: type args : IntegrationConfig
: type targets : collections . Iterable [ IntegrationTarget ]
: type init_callback : ( IntegrationConfig , tuple [ IntegrationTarget ] ) - > None
: rtype : tuple [ IntegrationTarget ]
"""
targets = tuple ( target for target in targets if ' hidden/ ' not in target . aliases )
@ -551,6 +621,9 @@ def command_integration_filter(args, targets):
if args . start_at and not any ( t . name == args . start_at for t in internal_targets ) :
raise ApplicationError ( ' Start at target matches nothing: %s ' % args . start_at )
if init_callback :
init_callback ( args , internal_targets )
cloud_init ( args , internal_targets )
if args . delegate :
@ -880,7 +953,7 @@ def command_units(args):
for version in SUPPORTED_PYTHON_VERSIONS :
# run all versions unless version given, in which case run only that version
if args . python and version != args . python :
if args . python and version != args . python _version :
continue
env = ansible_environment ( args )
@ -940,7 +1013,7 @@ def command_compile(args):
for version in COMPILE_PYTHON_VERSIONS :
# run all versions unless version given, in which case run only that version
if args . python and version != args . python :
if args . python and version != args . python _version :
continue
display . info ( ' Compile with Python %s ' % version )
@ -1027,104 +1100,6 @@ def compile_version(args, python_version, include, exclude):
return TestSuccess ( command , test , python_version = python_version )
def intercept_command ( args , cmd , target_name , capture = False , env = None , data = None , cwd = None , python_version = None , path = None ) :
"""
: type args : TestConfig
: type cmd : collections . Iterable [ str ]
: type target_name : str
: type capture : bool
: type env : dict [ str , str ] | None
: type data : str | None
: type cwd : str | None
: type python_version : str | None
: type path : str | None
: rtype : str | None , str | None
"""
if not env :
env = common_environment ( )
cmd = list ( cmd )
inject_path = get_coverage_path ( args )
config_path = os . path . join ( inject_path , ' injector.json ' )
version = python_version or args . python_version
interpreter = find_executable ( ' python %s ' % version , path = path )
coverage_file = os . path . abspath ( os . path . join ( inject_path , ' .. ' , ' output ' , ' %s = %s = %s = %s =coverage ' % (
args . command , target_name , args . coverage_label or ' local- %s ' % version , ' python- %s ' % version ) ) )
env [ ' PATH ' ] = inject_path + os . pathsep + env [ ' PATH ' ]
env [ ' ANSIBLE_TEST_PYTHON_VERSION ' ] = version
env [ ' ANSIBLE_TEST_PYTHON_INTERPRETER ' ] = interpreter
config = dict (
python_interpreter = interpreter ,
coverage_file = coverage_file if args . coverage else None ,
)
if not args . explain :
with open ( config_path , ' w ' ) as config_fd :
json . dump ( config , config_fd , indent = 4 , sort_keys = True )
return run_command ( args , cmd , capture = capture , env = env , data = data , cwd = cwd )
def get_coverage_path ( args ) :
"""
: type args : TestConfig
: rtype : str
"""
global coverage_path # pylint: disable=locally-disabled, global-statement, invalid-name
if coverage_path :
return os . path . join ( coverage_path , ' coverage ' )
prefix = ' ansible-test-coverage- '
tmp_dir = ' /tmp '
if args . explain :
return os . path . join ( tmp_dir , ' %s tmp ' % prefix , ' coverage ' )
src = os . path . abspath ( os . path . join ( os . getcwd ( ) , ' test/runner/injector/ ' ) )
coverage_path = tempfile . mkdtemp ( ' ' , prefix , dir = tmp_dir )
os . chmod ( coverage_path , stat . S_IRWXU | stat . S_IRGRP | stat . S_IXGRP | stat . S_IROTH | stat . S_IXOTH )
shutil . copytree ( src , os . path . join ( coverage_path , ' coverage ' ) )
shutil . copy ( ' .coveragerc ' , os . path . join ( coverage_path , ' coverage ' , ' .coveragerc ' ) )
for root , dir_names , file_names in os . walk ( coverage_path ) :
for name in dir_names + file_names :
os . chmod ( os . path . join ( root , name ) , stat . S_IRWXU | stat . S_IRGRP | stat . S_IXGRP | stat . S_IROTH | stat . S_IXOTH )
for directory in ' output ' , ' logs ' :
os . mkdir ( os . path . join ( coverage_path , directory ) )
os . chmod ( os . path . join ( coverage_path , directory ) , stat . S_IRWXU | stat . S_IRWXG | stat . S_IRWXO )
atexit . register ( cleanup_coverage_dir )
return os . path . join ( coverage_path , ' coverage ' )
def cleanup_coverage_dir ( ) :
""" Copy over coverage data from temporary directory and purge temporary directory. """
output_dir = os . path . join ( coverage_path , ' output ' )
for filename in os . listdir ( output_dir ) :
src = os . path . join ( output_dir , filename )
dst = os . path . join ( os . getcwd ( ) , ' test ' , ' results ' , ' coverage ' )
shutil . copy ( src , dst )
logs_dir = os . path . join ( coverage_path , ' logs ' )
for filename in os . listdir ( logs_dir ) :
random_suffix = ' ' . join ( random . choice ( string . ascii_letters + string . digits ) for _ in range ( 8 ) )
new_name = ' %s . %s .log ' % ( os . path . splitext ( os . path . basename ( filename ) ) [ 0 ] , random_suffix )
src = os . path . join ( logs_dir , filename )
dst = os . path . join ( os . getcwd ( ) , ' test ' , ' results ' , ' logs ' , new_name )
shutil . copy ( src , dst )
shutil . rmtree ( coverage_path )
def get_changes_filter ( args ) :
"""
: type args : TestConfig
@ -1306,12 +1281,16 @@ def get_integration_local_filter(args, targets):
% ( skip . rstrip ( ' / ' ) , ' , ' . join ( skipped ) ) )
if args . python_version . startswith ( ' 3 ' ) :
skip = ' skip/python3/ '
skipped = [ target . name for target in targets if skip in target . aliases ]
if skipped :
exclude . append ( skip )
display . warning ( ' Excluding tests marked " %s " which are not yet supported on python 3: %s '
% ( skip . rstrip ( ' / ' ) , ' , ' . join ( skipped ) ) )
python_version = 3
else :
python_version = 2
skip = ' skip/python %d / ' % python_version
skipped = [ target . name for target in targets if skip in target . aliases ]
if skipped :
exclude . append ( skip )
display . warning ( ' Excluding tests marked " %s " which are not supported on python %d : %s '
% ( skip . rstrip ( ' / ' ) , python_version , ' , ' . join ( skipped ) ) )
return exclude
@ -1332,13 +1311,26 @@ def get_integration_docker_filter(args, targets):
display . warning ( ' Excluding tests marked " %s " which require --docker-privileged to run under docker: %s '
% ( skip . rstrip ( ' / ' ) , ' , ' . join ( skipped ) ) )
python_version = 2 # images are expected to default to python 2 unless otherwise specified
if args . docker . endswith ( ' py3 ' ) :
skip = ' skip/python3/ '
skipped = [ target . name for target in targets if skip in target . aliases ]
if skipped :
exclude . append ( skip )
display . warning ( ' Excluding tests marked " %s " which are not yet supported on python 3: %s '
% ( skip . rstrip ( ' / ' ) , ' , ' . join ( skipped ) ) )
python_version = 3 # docker images ending in 'py3' are expected to default to python 3
if args . docker . endswith ( ' :default ' ) :
python_version = 3 # docker images tagged 'default' are expected to default to python 3
if args . python : # specifying a numeric --python option overrides the default python
if args . python . startswith ( ' 3 ' ) :
python_version = 3
elif args . python . startswith ( ' 2 ' ) :
python_version = 2
skip = ' skip/python %d / ' % python_version
skipped = [ target . name for target in targets if skip in target . aliases ]
if skipped :
exclude . append ( skip )
display . warning ( ' Excluding tests marked " %s " which are not supported on python %d : %s '
% ( skip . rstrip ( ' / ' ) , python_version , ' , ' . join ( skipped ) ) )
return exclude
@ -1359,9 +1351,18 @@ def get_integration_remote_filter(args, targets):
skipped = [ target . name for target in targets if skip in target . aliases ]
if skipped :
exclude . append ( skip )
display . warning ( ' Excluding tests marked " %s " which are not yet supported on %s : %s '
display . warning ( ' Excluding tests marked " %s " which are not supported on %s : %s '
% ( skip . rstrip ( ' / ' ) , platform , ' , ' . join ( skipped ) ) )
python_version = 2 # remotes are expected to default to python 2
skip = ' skip/python %d / ' % python_version
skipped = [ target . name for target in targets if skip in target . aliases ]
if skipped :
exclude . append ( skip )
display . warning ( ' Excluding tests marked " %s " which are not supported on python %d : %s '
% ( skip . rstrip ( ' / ' ) , python_version , ' , ' . join ( skipped ) ) )
return exclude