|
|
|
@ -24,6 +24,7 @@ from lib.util import (
|
|
|
|
|
read_lines_without_comments,
|
|
|
|
|
get_available_python_versions,
|
|
|
|
|
find_python,
|
|
|
|
|
is_subdir,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
from lib.util_common import (
|
|
|
|
@ -78,7 +79,7 @@ def command_sanity(args):
|
|
|
|
|
"""
|
|
|
|
|
changes = get_changes_filter(args)
|
|
|
|
|
require = args.require + changes
|
|
|
|
|
targets = SanityTargets(args.include, args.exclude, require)
|
|
|
|
|
targets = SanityTargets.create(args.include, args.exclude, require)
|
|
|
|
|
|
|
|
|
|
if not targets.include:
|
|
|
|
|
raise AllTargetsSkipped()
|
|
|
|
@ -128,14 +129,19 @@ def command_sanity(args):
|
|
|
|
|
versions = versions[:1] or (args.python_version,)
|
|
|
|
|
|
|
|
|
|
for version in versions:
|
|
|
|
|
if isinstance(test, SanityMultipleVersion):
|
|
|
|
|
skip_version = version
|
|
|
|
|
else:
|
|
|
|
|
skip_version = None
|
|
|
|
|
|
|
|
|
|
options = ''
|
|
|
|
|
|
|
|
|
|
if test.supported_python_versions and version not in test.supported_python_versions:
|
|
|
|
|
display.warning("Skipping sanity test '%s' on unsupported Python %s." % (test.name, version))
|
|
|
|
|
result = SanitySkipped(test.name)
|
|
|
|
|
result = SanitySkipped(test.name, skip_version)
|
|
|
|
|
elif not args.python and version not in available_versions:
|
|
|
|
|
display.warning("Skipping sanity test '%s' on Python %s due to missing interpreter." % (test.name, version))
|
|
|
|
|
result = SanitySkipped(test.name)
|
|
|
|
|
result = SanitySkipped(test.name, skip_version)
|
|
|
|
|
else:
|
|
|
|
|
check_pyyaml(args, version)
|
|
|
|
|
|
|
|
|
@ -145,17 +151,42 @@ def command_sanity(args):
|
|
|
|
|
display.info("Running sanity test '%s'" % test.name)
|
|
|
|
|
|
|
|
|
|
if isinstance(test, SanityCodeSmellTest):
|
|
|
|
|
result = test.test(args, targets, version)
|
|
|
|
|
settings = test.load_processor(args)
|
|
|
|
|
elif isinstance(test, SanityMultipleVersion):
|
|
|
|
|
result = test.test(args, targets, version)
|
|
|
|
|
options = ' --python %s' % version
|
|
|
|
|
settings = test.load_processor(args, version)
|
|
|
|
|
elif isinstance(test, SanitySingleVersion):
|
|
|
|
|
result = test.test(args, targets, version)
|
|
|
|
|
settings = test.load_processor(args)
|
|
|
|
|
elif isinstance(test, SanityVersionNeutral):
|
|
|
|
|
result = test.test(args, targets)
|
|
|
|
|
settings = test.load_processor(args)
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('Unsupported test type: %s' % type(test))
|
|
|
|
|
|
|
|
|
|
if test.all_targets:
|
|
|
|
|
usable_targets = targets.targets
|
|
|
|
|
elif test.no_targets:
|
|
|
|
|
usable_targets = []
|
|
|
|
|
else:
|
|
|
|
|
usable_targets = targets.include
|
|
|
|
|
|
|
|
|
|
usable_targets = sorted(test.filter_targets(list(usable_targets)))
|
|
|
|
|
usable_targets = settings.filter_skipped_targets(usable_targets)
|
|
|
|
|
sanity_targets = SanityTargets(targets.targets, tuple(usable_targets))
|
|
|
|
|
|
|
|
|
|
if usable_targets or test.no_targets:
|
|
|
|
|
if isinstance(test, SanityCodeSmellTest):
|
|
|
|
|
result = test.test(args, sanity_targets, version)
|
|
|
|
|
elif isinstance(test, SanityMultipleVersion):
|
|
|
|
|
result = test.test(args, sanity_targets, version)
|
|
|
|
|
options = ' --python %s' % version
|
|
|
|
|
elif isinstance(test, SanitySingleVersion):
|
|
|
|
|
result = test.test(args, sanity_targets, version)
|
|
|
|
|
elif isinstance(test, SanityVersionNeutral):
|
|
|
|
|
result = test.test(args, sanity_targets)
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('Unsupported test type: %s' % type(test))
|
|
|
|
|
else:
|
|
|
|
|
result = SanitySkipped(test.name, skip_version)
|
|
|
|
|
|
|
|
|
|
result.write(args)
|
|
|
|
|
|
|
|
|
|
total += 1
|
|
|
|
@ -388,10 +419,6 @@ class SanityIgnoreProcessor:
|
|
|
|
|
self.skip_entries = self.parser.skips.get(full_name, {})
|
|
|
|
|
self.used_line_numbers = set() # type: t.Set[int]
|
|
|
|
|
|
|
|
|
|
def filter_skipped_paths(self, paths): # type: (t.List[str]) -> t.List[str]
|
|
|
|
|
"""Return the given paths, with any skipped paths filtered out."""
|
|
|
|
|
return sorted(set(paths) - set(self.skip_entries.keys()))
|
|
|
|
|
|
|
|
|
|
def filter_skipped_targets(self, targets): # type: (t.List[TestTarget]) -> t.List[TestTarget]
|
|
|
|
|
"""Return the given targets, with any skipped paths filtered out."""
|
|
|
|
|
return sorted(target for target in targets if target.path not in self.skip_entries)
|
|
|
|
@ -490,15 +517,16 @@ class SanityMessage(TestMessage):
|
|
|
|
|
|
|
|
|
|
class SanityTargets:
|
|
|
|
|
"""Sanity test target information."""
|
|
|
|
|
def __init__(self, include, exclude, require):
|
|
|
|
|
"""
|
|
|
|
|
:type include: list[str]
|
|
|
|
|
:type exclude: list[str]
|
|
|
|
|
:type require: list[str]
|
|
|
|
|
"""
|
|
|
|
|
self.all = not include
|
|
|
|
|
self.targets = tuple(sorted(walk_sanity_targets()))
|
|
|
|
|
self.include = walk_internal_targets(self.targets, include, exclude, require)
|
|
|
|
|
def __init__(self, targets, include): # type: (t.Tuple[TestTarget], t.Tuple[TestTarget]) -> None
|
|
|
|
|
self.targets = targets
|
|
|
|
|
self.include = include
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def create(include, exclude, require): # type: (t.List[str], t.List[str], t.List[str]) -> SanityTargets
|
|
|
|
|
"""Create a SanityTargets instance from the given include, exclude and require lists."""
|
|
|
|
|
_targets = tuple(sorted(walk_sanity_targets()))
|
|
|
|
|
_include = walk_internal_targets(_targets, include, exclude, require)
|
|
|
|
|
return SanityTargets(_targets, _include)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SanityTest(ABC):
|
|
|
|
@ -524,13 +552,30 @@ class SanityTest(ABC):
|
|
|
|
|
@property
|
|
|
|
|
def can_skip(self): # type: () -> bool
|
|
|
|
|
"""True if the test supports skip entries."""
|
|
|
|
|
return True
|
|
|
|
|
return not self.all_targets and not self.no_targets
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def all_targets(self): # type: () -> bool
|
|
|
|
|
"""True if test targets will not be filtered using includes, excludes, requires or changes. Mutually exclusive with no_targets."""
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def no_targets(self): # type: () -> bool
|
|
|
|
|
"""True if the test does not use test targets. Mutually exclusive with all_targets."""
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def supported_python_versions(self): # type: () -> t.Optional[t.Tuple[str, ...]]
|
|
|
|
|
"""A tuple of supported Python versions or None if the test does not depend on specific Python versions."""
|
|
|
|
|
return tuple(python_version for python_version in SUPPORTED_PYTHON_VERSIONS if python_version.startswith('3.'))
|
|
|
|
|
|
|
|
|
|
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."""
|
|
|
|
|
if self.no_targets:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
raise NotImplementedError('Sanity test "%s" must implement "filter_targets" or set "no_targets" to True.' % self.name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SanityCodeSmellTest(SanityTest):
|
|
|
|
|
"""Sanity test script."""
|
|
|
|
@ -555,27 +600,73 @@ class SanityCodeSmellTest(SanityTest):
|
|
|
|
|
self.extensions = self.config.get('extensions') # type: t.List[str]
|
|
|
|
|
self.prefixes = self.config.get('prefixes') # type: t.List[str]
|
|
|
|
|
self.files = self.config.get('files') # type: t.List[str]
|
|
|
|
|
self.always = self.config.get('always') # type: bool
|
|
|
|
|
self.text = self.config.get('text') # type: t.Optional[bool]
|
|
|
|
|
self.ignore_changes = self.config.get('ignore_changes') # type: bool
|
|
|
|
|
self.ignore_self = self.config.get('ignore_self') # type: bool
|
|
|
|
|
|
|
|
|
|
if self.ignore_changes:
|
|
|
|
|
self.always = False
|
|
|
|
|
self.__all_targets = self.config.get('all_targets') # type: bool
|
|
|
|
|
self.__no_targets = self.config.get('no_targets') # type: bool
|
|
|
|
|
else:
|
|
|
|
|
self.output = None
|
|
|
|
|
self.extensions = []
|
|
|
|
|
self.prefixes = []
|
|
|
|
|
self.files = []
|
|
|
|
|
self.always = False
|
|
|
|
|
self.text = None # type: t.Optional[bool]
|
|
|
|
|
self.ignore_changes = False
|
|
|
|
|
self.ignore_self = False
|
|
|
|
|
|
|
|
|
|
self.__all_targets = False
|
|
|
|
|
self.__no_targets = True
|
|
|
|
|
|
|
|
|
|
if self.no_targets:
|
|
|
|
|
mutually_exclusive = (
|
|
|
|
|
'extensions',
|
|
|
|
|
'prefixes',
|
|
|
|
|
'files',
|
|
|
|
|
'text',
|
|
|
|
|
'ignore_self',
|
|
|
|
|
'all_targets',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
problems = sorted(name for name in mutually_exclusive if getattr(self, name))
|
|
|
|
|
|
|
|
|
|
if problems:
|
|
|
|
|
raise ApplicationError('Sanity test "%s" option "no_targets" is mutually exclusive with options: %s' % (self.name, ', '.join(problems)))
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def can_skip(self): # type: () -> bool
|
|
|
|
|
"""True if the test supports skip entries."""
|
|
|
|
|
return not self.always
|
|
|
|
|
def all_targets(self): # type: () -> bool
|
|
|
|
|
"""True if test targets will not be filtered using includes, excludes, requires or changes. Mutually exclusive with no_targets."""
|
|
|
|
|
return self.__all_targets
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def no_targets(self): # type: () -> bool
|
|
|
|
|
"""True if the test does not use test targets. Mutually exclusive with all_targets."""
|
|
|
|
|
return self.__no_targets
|
|
|
|
|
|
|
|
|
|
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."""
|
|
|
|
|
if self.no_targets:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
if self.text is not None:
|
|
|
|
|
if self.text:
|
|
|
|
|
targets = [target for target in targets if not is_binary_file(target.path)]
|
|
|
|
|
else:
|
|
|
|
|
targets = [target for target in targets if is_binary_file(target.path)]
|
|
|
|
|
|
|
|
|
|
if self.extensions:
|
|
|
|
|
targets = [target for target in targets if os.path.splitext(target.path)[1] in self.extensions
|
|
|
|
|
or (is_subdir(target.path, 'bin') and '.py' in self.extensions)]
|
|
|
|
|
|
|
|
|
|
if self.prefixes:
|
|
|
|
|
targets = [target for target in targets if any(target.path.startswith(pre) for pre in self.prefixes)]
|
|
|
|
|
|
|
|
|
|
if self.files:
|
|
|
|
|
targets = [target for target in targets if os.path.basename(target.path) in self.files]
|
|
|
|
|
|
|
|
|
|
if self.ignore_self and data_context().content.is_ansible:
|
|
|
|
|
relative_self_path = os.path.relpath(self.path, data_context().content.root)
|
|
|
|
|
targets = [target for target in targets if target.path != relative_self_path]
|
|
|
|
|
|
|
|
|
|
return targets
|
|
|
|
|
|
|
|
|
|
def test(self, args, targets, python_version):
|
|
|
|
|
"""
|
|
|
|
@ -593,7 +684,7 @@ class SanityCodeSmellTest(SanityTest):
|
|
|
|
|
|
|
|
|
|
settings = self.load_processor(args)
|
|
|
|
|
|
|
|
|
|
paths = []
|
|
|
|
|
paths = [target.path for target in targets.include]
|
|
|
|
|
|
|
|
|
|
if self.config:
|
|
|
|
|
if self.output == 'path-line-column-message':
|
|
|
|
@ -603,40 +694,7 @@ class SanityCodeSmellTest(SanityTest):
|
|
|
|
|
else:
|
|
|
|
|
pattern = ApplicationError('Unsupported output type: %s' % self.output)
|
|
|
|
|
|
|
|
|
|
if self.ignore_changes:
|
|
|
|
|
paths = sorted(i.path for i in targets.targets)
|
|
|
|
|
else:
|
|
|
|
|
paths = sorted(i.path for i in targets.include)
|
|
|
|
|
|
|
|
|
|
if self.always:
|
|
|
|
|
paths = []
|
|
|
|
|
|
|
|
|
|
if self.text is not None:
|
|
|
|
|
if self.text:
|
|
|
|
|
paths = [p for p in paths if not is_binary_file(p)]
|
|
|
|
|
else:
|
|
|
|
|
paths = [p for p in paths if is_binary_file(p)]
|
|
|
|
|
|
|
|
|
|
if self.extensions:
|
|
|
|
|
paths = [p for p in paths if os.path.splitext(p)[1] in self.extensions or (p.startswith('bin/') and '.py' in self.extensions)]
|
|
|
|
|
|
|
|
|
|
if self.prefixes:
|
|
|
|
|
paths = [p for p in paths if any(p.startswith(pre) for pre in self.prefixes)]
|
|
|
|
|
|
|
|
|
|
if self.files:
|
|
|
|
|
paths = [p for p in paths if os.path.basename(p) in self.files]
|
|
|
|
|
|
|
|
|
|
if self.ignore_self and data_context().content.is_ansible:
|
|
|
|
|
relative_self_path = os.path.relpath(self.path, data_context().content.root)
|
|
|
|
|
|
|
|
|
|
if relative_self_path in paths:
|
|
|
|
|
paths.remove(relative_self_path)
|
|
|
|
|
|
|
|
|
|
paths = settings.filter_skipped_paths(paths)
|
|
|
|
|
|
|
|
|
|
if not paths and not self.always:
|
|
|
|
|
return SanitySkipped(self.name)
|
|
|
|
|
|
|
|
|
|
if not self.no_targets:
|
|
|
|
|
data = '\n'.join(paths)
|
|
|
|
|
|
|
|
|
|
if data:
|
|
|
|
|