@ -20,6 +20,7 @@ from ..target import (
)
)
from . . util import (
from . . util import (
ANSIBLE_TEST_DATA_ROOT ,
SubprocessError ,
SubprocessError ,
remove_tree ,
remove_tree ,
display ,
display ,
@ -27,6 +28,8 @@ from ..util import (
is_subdir ,
is_subdir ,
generate_pip_command ,
generate_pip_command ,
find_python ,
find_python ,
get_hash ,
REMOTE_ONLY_PYTHON_VERSIONS ,
)
)
from . . util_common import (
from . . util_common import (
@ -41,6 +44,7 @@ from ..ansible_util import (
from . . executor import (
from . . executor import (
generate_pip_install ,
generate_pip_install ,
install_cryptography ,
)
)
from . . config import (
from . . config import (
@ -60,12 +64,21 @@ from ..data import (
)
)
def _get_module_test ( module_restrictions ) : # type: (bool) -> t.Callable[[str], bool]
""" Create a predicate which tests whether a path can be used by modules or not. """
module_path = data_context ( ) . content . module_path
module_utils_path = data_context ( ) . content . module_utils_path
if module_restrictions :
return lambda path : is_subdir ( path , module_path ) or is_subdir ( path , module_utils_path )
return lambda path : not ( is_subdir ( path , module_path ) or is_subdir ( path , module_utils_path ) )
class ImportTest ( SanityMultipleVersion ) :
class ImportTest ( SanityMultipleVersion ) :
""" Sanity test for proper import exception handling. """
""" Sanity test for proper import exception handling. """
def filter_targets ( self , targets ) : # type: (t.List[TestTarget]) -> t.List[TestTarget]
def filter_targets ( self , targets ) : # type: (t.List[TestTarget]) -> t.List[TestTarget]
""" Return the given list of test targets, filtered to include only those relevant for the test. """
""" Return the given list of test targets, filtered to include only those relevant for the test. """
return [ target for target in targets if os . path . splitext ( target . path ) [ 1 ] == ' .py ' and
return [ target for target in targets if os . path . splitext ( target . path ) [ 1 ] == ' .py ' and
( is_subdir ( target . path , data_context ( ) . content . module_path ) or is_subdir ( target . path , data_context ( ) . content . module_utils_path ) ) ]
any ( is_subdir ( target . path , path) for path in data_context ( ) . content . plugin_paths . values ( ) ) ]
def test ( self , args , targets , python_version ) :
def test ( self , args , targets , python_version ) :
"""
"""
@ -92,91 +105,112 @@ class ImportTest(SanityMultipleVersion):
temp_root = os . path . join ( ResultType . TMP . path , ' sanity ' , ' import ' )
temp_root = os . path . join ( ResultType . TMP . path , ' sanity ' , ' import ' )
# create a clean virtual environment to minimize the available imports beyond the python standard library
messages = [ ]
virtual_environment_path = os . path . join ( temp_root , ' minimal-py %s ' % python_version . replace ( ' . ' , ' ' ) )
virtual_environment_bin = os . path . join ( virtual_environment_path , ' bin ' )
for import_type , test , add_ansible_requirements in (
( ' module ' , _get_module_test ( True ) , False ) ,
( ' plugin ' , _get_module_test ( False ) , True ) ,
) :
if import_type == ' plugin ' and python_version in REMOTE_ONLY_PYTHON_VERSIONS :
continue
data = ' \n ' . join ( [ path for path in paths if test ( path ) ] )
if not data :
continue
requirements_file = None
remove_tree ( virtual_environment_path )
# create a clean virtual environment to minimize the available imports beyond the python standard library
virtual_environment_dirname = ' minimal-py %s ' % python_version . replace ( ' . ' , ' ' )
if add_ansible_requirements :
requirements_file = os . path . join ( ANSIBLE_TEST_DATA_ROOT , ' requirements ' , ' sanity.import-plugins.txt ' )
virtual_environment_dirname + = ' -requirements- %s ' % get_hash ( requirements_file )
virtual_environment_path = os . path . join ( temp_root , virtual_environment_dirname )
virtual_environment_bin = os . path . join ( virtual_environment_path , ' bin ' )
if not create_virtual_environment ( args , python_version , virtual_environment_path ) :
remove_tree ( virtual_environment_path )
display . warning ( " Skipping sanity test ' %s ' on Python %s due to missing virtual environment support. " % ( self . name , python_version ) )
return SanitySkipped ( self . name , python_version )
# add the importer to our virtual environment so it can be accessed through the coverage injector
if not create_virtual_environment ( args , python_version , virtual_environment_path ) :
importer_path = os . path . join ( virtual_environment_bin , ' importer.py ' )
display . warning ( " Skipping sanity test ' %s ' on Python %s due to missing virtual environment support. " % ( self . name , python_version ) )
yaml_to_json_path = os . path . join ( virtual_environment_bin , ' yaml_to_json.py ' )
return SanitySkipped ( self . name , python_version )
if not args . explain :
os . symlink ( os . path . abspath ( os . path . join ( SANITY_ROOT , ' import ' , ' importer.py ' ) ) , importer_path )
os . symlink ( os . path . abspath ( os . path . join ( SANITY_ROOT , ' import ' , ' yaml_to_json.py ' ) ) , yaml_to_json_path )
# activate the virtual environment
# add the importer to our virtual environment so it can be accessed through the coverage injector
env [ ' PATH ' ] = ' %s : %s ' % ( virtual_environment_bin , env [ ' PATH ' ] )
importer_path = os . path . join ( virtual_environment_bin , ' importer.py ' )
yaml_to_json_path = os . path . join ( virtual_environment_bin , ' yaml_to_json.py ' )
if not args . explain :
os . symlink ( os . path . abspath ( os . path . join ( SANITY_ROOT , ' import ' , ' importer.py ' ) ) , importer_path )
os . symlink ( os . path . abspath ( os . path . join ( SANITY_ROOT , ' import ' , ' yaml_to_json.py ' ) ) , yaml_to_json_path )
env . update (
# activate the virtual environment
SANITY_TEMP_PATH = ResultType . TMP . path ,
env [ ' PATH ' ] = ' %s : %s ' % ( virtual_environment_bin , env [ ' PATH ' ] )
)
if data_context ( ) . content . collection :
env . update (
env . update (
SANITY_ COLLECTION_FULL_NAME= data_context ( ) . content . collection . full_name ,
SANITY_ TEMP_PATH= ResultType . TMP . path ,
SANITY_ EXTERNAL_PYTHON= python ,
SANITY_ IMPORTER_TYPE= import_type ,
)
)
virtualenv_python = os . path . join ( virtual_environment_bin , ' python ' )
if data_context ( ) . content . collection :
virtualenv_pip = generate_pip_command ( virtualenv_python )
env . update (
SANITY_COLLECTION_FULL_NAME = data_context ( ) . content . collection . full_name ,
SANITY_EXTERNAL_PYTHON = python ,
)
# make sure coverage is available in the virtual environment if needed
virtualenv_python = os . path . join ( virtual_environment_bin , ' python ' )
if args . coverage :
virtualenv_pip = generate_pip_command ( virtualenv_python )
run_command ( args , generate_pip_install ( virtualenv_pip , ' ' , packages = [ ' setuptools ' ] ) , env = env , capture = capture_pip )
run_command ( args , generate_pip_install ( virtualenv_pip , ' ' , packages = [ ' coverage ' ] ) , env = env , capture = capture_pip )
try :
# make sure requirements are installed if needed
# In some environments pkg_resources is installed as a separate pip package which needs to be removed.
if requirements_file :
# For example, using Python 3.8 on Ubuntu 18.04 a virtualenv is created with only pip and setuptools.
install_cryptography ( args , virtualenv_python , python_version , virtualenv_pip )
# However, a venv is created with an additional pkg-resources package which is independent of setuptools.
run_command ( args , generate_pip_install ( virtualenv_pip , ' sanity ' , context = ' import-plugins ' ) , env = env , capture = capture_pip )
# Making sure pkg-resources is removed preserves the import test consistency between venv and virtualenv.
# Additionally, in the above example, the pyparsing package vendored with pkg-resources is out-of-date and generates deprecation warnings.
# Thus it is important to remove pkg-resources to prevent system installed packages from generating deprecation warnings.
run_command ( args , virtualenv_pip + [ ' uninstall ' , ' --disable-pip-version-check ' , ' -y ' , ' pkg-resources ' ] , env = env , capture = capture_pip )
except SubprocessError :
pass
run_command ( args , virtualenv_pip + [ ' uninstall ' , ' --disable-pip-version-check ' , ' -y ' , ' setuptools ' ] , env = env , capture = capture_pip )
# make sure coverage is available in the virtual environment if needed
run_command ( args , virtualenv_pip + [ ' uninstall ' , ' --disable-pip-version-check ' , ' -y ' , ' pip ' ] , env = env , capture = capture_pip )
if args . coverage :
run_command ( args , generate_pip_install ( virtualenv_pip , ' ' , packages = [ ' setuptools ' ] ) , env = env , capture = capture_pip )
run_command ( args , generate_pip_install ( virtualenv_pip , ' ' , packages = [ ' coverage ' ] ) , env = env , capture = capture_pip )
cmd = [ ' importer.py ' ]
try :
# In some environments pkg_resources is installed as a separate pip package which needs to be removed.
# For example, using Python 3.8 on Ubuntu 18.04 a virtualenv is created with only pip and setuptools.
# However, a venv is created with an additional pkg-resources package which is independent of setuptools.
# Making sure pkg-resources is removed preserves the import test consistency between venv and virtualenv.
# Additionally, in the above example, the pyparsing package vendored with pkg-resources is out-of-date and generates deprecation warnings.
# Thus it is important to remove pkg-resources to prevent system installed packages from generating deprecation warnings.
run_command ( args , virtualenv_pip + [ ' uninstall ' , ' --disable-pip-version-check ' , ' -y ' , ' pkg-resources ' ] , env = env , capture = capture_pip )
except SubprocessError :
pass
data = ' \n ' . join ( paths )
run_command ( args , virtualenv_pip + [ ' uninstall ' , ' --disable-pip-version-check ' , ' -y ' , ' setuptools ' ] , env = env , capture = capture_pip )
run_command ( args , virtualenv_pip + [ ' uninstall ' , ' --disable-pip-version-check ' , ' -y ' , ' pip ' ] , env = env , capture = capture_pip )
display . info ( data , verbosity = 4 )
display . info ( import_type + ' : ' + data , verbosity = 4 )
results = [ ]
cmd = [ ' importer.py ' ]
try :
try :
with coverage_context ( args ) :
with coverage_context ( args ) :
stdout , stderr = intercept_command ( args , cmd , self . name , env , capture = True , data = data , python_version = python_version ,
stdout , stderr = intercept_command ( args , cmd , self . name , env , capture = True , data = data , python_version = python_version ,
virtualenv = virtualenv_python )
virtualenv = virtualenv_python )
if stdout or stderr :
if stdout or stderr :
raise SubprocessError ( cmd , stdout = stdout , stderr = stderr )
raise SubprocessError ( cmd , stdout = stdout , stderr = stderr )
except SubprocessError as ex :
except SubprocessError as ex :
if ex . status != 10 or ex . stderr or not ex . stdout :
if ex . status != 10 or ex . stderr or not ex . stdout :
raise
raise
pattern = r ' ^(?P<path>[^:]*):(?P<line>[0-9]+):(?P<column>[0-9]+): (?P<message>.*)$ '
pattern = r ' ^(?P<path>[^:]*):(?P<line>[0-9]+):(?P<column>[0-9]+): (?P<message>.*)$ '
results = parse_to_list_of_dict ( pattern , ex . stdout )
parsed = parse_to_list_of_dict ( pattern , ex . stdout )
relative_temp_root = os . path . relpath ( temp_root , data_context ( ) . content . root ) + os . path . sep
relative_temp_root = os . path . relpath ( temp_root , data_context ( ) . content . root ) + os . path . sep
results = [ SanityMessage (
messages + = [ SanityMessage (
message = r [ ' message ' ] ,
message = r [ ' message ' ] ,
path = os . path . relpath ( r [ ' path ' ] , relative_temp_root ) if r [ ' path ' ] . startswith ( relative_temp_root ) else r [ ' path ' ] ,
path = os . path . relpath ( r [ ' path ' ] , relative_temp_root ) if r [ ' path ' ] . startswith ( relative_temp_root ) else r [ ' path ' ] ,
line = int ( r [ ' line ' ] ) ,
line = int ( r [ ' line ' ] ) ,
column = int ( r [ ' column ' ] ) ,
column = int ( r [ ' column ' ] ) ,
) for r in results ]
) for r in parsed ]
results = settings . process_errors ( result s, paths )
results = settings . process_errors ( message s, paths )
if results :
if results :
return SanityFailure ( self . name , messages = results , python_version = python_version )
return SanityFailure ( self . name , messages = results , python_version = python_version )