Sanity test updates for collections support. (#61248)

* Run no-unwanted-files sanity test only on Ansible.

Since collections should be able to use binary modules there is not really any limit on what could exist in a collection `plugins` directory.

* Add support for symlinks in sanity target lists:

- Sanity tests that need to analyze symlinks can do so using the supplied target list.
- Tests that analyze directories will now only look at symlinks if requested.
- Directory symlinks will now be seen as directories instead of files.

* Enable symlinks on filename based sanity tests.

Sanity tests that evalulate filenames instead of content should include symlinks.

* Update symlinks sanity test.

Use the sanity test target list now that it can include symlinks.
pull/61252/head
Matt Clay 5 years ago committed by GitHub
parent 5434bf74c6
commit 6ec0b4ec86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,5 @@
{
"include_directories": true,
"include_symlinks": true,
"output": "path-message"
}

@ -1,4 +1,5 @@
{
"no_targets": true,
"include_directories": true,
"include_symlinks": true,
"output": "path-message"
}

@ -3,34 +3,19 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import sys
def main():
skip_dirs = set([
'.tox',
])
for path in sys.argv[1:] or sys.stdin.read().splitlines():
if not os.path.islink(path.rstrip(os.path.sep)):
continue
for root, dirs, files in os.walk('.'):
for skip_dir in skip_dirs:
if skip_dir in dirs:
dirs.remove(skip_dir)
if not os.path.exists(path):
print('%s: broken symlinks are not allowed' % path)
if root == '.':
root = ''
elif root.startswith('./'):
root = root[2:]
for file in files:
path = os.path.join(root, file)
if not os.path.exists(path):
print('%s: broken symlinks are not allowed' % path)
for directory in dirs:
path = os.path.join(root, directory)
if os.path.islink(path):
print('%s: symlinks to directories are not allowed' % path)
if path.endswith(os.path.sep):
print('%s: symlinks to directories are not allowed' % path)
if __name__ == '__main__':

@ -58,7 +58,9 @@ def command_coverage_combine(args):
groups = {}
if args.all or args.stub:
sources = sorted(os.path.abspath(target.path) for target in walk_compile_targets())
# excludes symlinks of regular files to avoid reporting on the same file multiple times
# in the future it would be nice to merge any coverage for symlinks into the real files
sources = sorted(os.path.abspath(target.path) for target in walk_compile_targets(include_symlinks=False))
else:
sources = []

@ -25,17 +25,27 @@ class Layout:
): # type: (...) -> None
self.root = root
self.__paths = paths
self.__tree = paths_to_tree(paths)
self.__paths = paths # contains both file paths and symlinked directory paths (ending with os.path.sep)
self.__files = [path for path in paths if not path.endswith(os.path.sep)] # contains only file paths
self.__paths_tree = paths_to_tree(self.__paths)
self.__files_tree = paths_to_tree(self.__files)
def all_files(self): # type: () -> t.List[str]
def all_files(self, include_symlinked_directories=False): # type: (bool) -> t.List[str]
"""Return a list of all file paths."""
return self.__paths
if include_symlinked_directories:
return self.__paths
def walk_files(self, directory): # type: (str) -> t.List[str]
return self.__files
def walk_files(self, directory, include_symlinked_directories=False): # type: (str, bool) -> t.List[str]
"""Return a list of file paths found recursively under the given directory."""
if include_symlinked_directories:
tree = self.__paths_tree
else:
tree = self.__files_tree
parts = directory.rstrip(os.sep).split(os.sep)
item = get_tree_item(self.__tree, parts)
item = get_tree_item(tree, parts)
if not item:
return []
@ -54,13 +64,13 @@ class Layout:
def get_dirs(self, directory): # type: (str) -> t.List[str]
"""Return a list directory paths found directly under the given directory."""
parts = directory.rstrip(os.sep).split(os.sep)
item = get_tree_item(self.__tree, parts)
item = get_tree_item(self.__files_tree, parts)
return [os.path.join(directory, key) for key in item[0].keys()] if item else []
def get_files(self, directory): # type: (str) -> t.List[str]
"""Return a list of file paths found directly under the given directory."""
parts = directory.rstrip(os.sep).split(os.sep)
item = get_tree_item(self.__tree, parts)
item = get_tree_item(self.__files_tree, parts)
return item[1] if item else []

@ -10,6 +10,10 @@ from ...git import (
Git,
)
from ...util import (
to_bytes,
)
from . import (
SourceProvider,
)
@ -30,4 +34,7 @@ class GitSource(SourceProvider):
deleted_paths = git.get_file_names(['--deleted'])
paths = sorted(set(paths) - set(deleted_paths))
# directory symlinks are reported by git as regular files but they need to be treated as directories
paths = [path + os.path.sep if os.path.isdir(to_bytes(path)) else path for path in paths]
return paths

@ -38,4 +38,6 @@ class InstalledSource(SourceProvider):
paths.extend([os.path.join(rel_root, file_name) for file_name in file_names
if not os.path.splitext(file_name)[1] in kill_extensions])
# NOTE: directory symlinks are ignored as there should be no directory symlinks for an install
return paths

@ -10,6 +10,10 @@ from ...constants import (
TIMEOUT_PATH,
)
from ...util import (
to_bytes,
)
from . import (
SourceProvider,
)
@ -75,4 +79,7 @@ class UnversionedSource(SourceProvider):
paths.extend([os.path.join(rel_root, file_name) for file_name in file_names
if not os.path.splitext(file_name)[1] in kill_extensions and file_name not in kill_files])
# include directory symlinks since they will not be traversed and would otherwise go undetected
paths.extend([os.path.join(rel_root, dir_name) + os.path.sep for dir_name in dir_names if os.path.islink(to_bytes(dir_name))])
return paths

@ -163,6 +163,8 @@ def command_sanity(args):
else:
raise Exception('Unsupported test type: %s' % type(test))
all_targets = targets.targets
if test.all_targets:
usable_targets = targets.targets
elif test.no_targets:
@ -170,12 +172,12 @@ def command_sanity(args):
else:
usable_targets = targets.include
if test.include_directories:
usable_targets += tuple(TestTarget(path, None, None, '') for path in paths_to_dirs([target.path for target in usable_targets]))
all_targets = SanityTargets.filter_and_inject_targets(test, all_targets)
usable_targets = SanityTargets.filter_and_inject_targets(test, usable_targets)
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))
sanity_targets = SanityTargets(tuple(all_targets), tuple(usable_targets))
if usable_targets or test.no_targets:
if isinstance(test, SanityCodeSmellTest):
@ -266,10 +268,7 @@ class SanityIgnoreParser:
display.info('Read %d sanity test ignore line(s) for %s from: %s' % (len(lines), ansible_label, self.relative_path), verbosity=1)
for test in sanity_get_tests():
test_targets = list(targets)
if test.include_directories:
test_targets += tuple(TestTarget(path, None, None, '') for path in paths_to_dirs([target.path for target in test_targets]))
test_targets = SanityTargets.filter_and_inject_targets(test, targets)
paths_by_test[test.name] = set(target.path for target in test.filter_targets(test_targets))
@ -488,10 +487,9 @@ class SanityIgnoreProcessor:
if self.test.no_targets or self.test.all_targets:
# tests which do not accept a target list, or which use all targets, always return all possible errors, so all ignores can be checked
paths = [target.path for target in SanityTargets.get_targets()]
if self.test.include_directories:
paths.extend(paths_to_dirs(paths))
targets = SanityTargets.get_targets()
test_targets = SanityTargets.filter_and_inject_targets(self.test, targets)
paths = [target.path for target in test_targets]
for path in paths:
path_entry = self.ignore_entries.get(path)
@ -562,6 +560,29 @@ class SanityTargets:
_include = walk_internal_targets(_targets, include, exclude, require)
return SanityTargets(_targets, _include)
@staticmethod
def filter_and_inject_targets(test, targets): # type: (SanityTest, t.Iterable[TestTarget]) -> t.List[TestTarget]
"""Filter and inject targets based on test requirements and the given target list."""
test_targets = list(targets)
if not test.include_symlinks:
# remove all symlinks unless supported by the test
test_targets = [target for target in test_targets if not target.symlink]
if not test.include_directories or not test.include_symlinks:
# exclude symlinked directories unless supported by the test
test_targets = [target for target in test_targets if not target.path.endswith(os.path.sep)]
if test.include_directories:
# include directories containing any of the included files
test_targets += tuple(TestTarget(path, None, None, '') for path in paths_to_dirs([target.path for target in test_targets]))
if not test.include_symlinks:
# remove all directory symlinks unless supported by the test
test_targets = [target for target in test_targets if not target.symlink]
return test_targets
@staticmethod
def get_targets(): # type: () -> t.Tuple[TestTarget, ...]
"""Return a tuple of sanity test targets. Uses a cached version when available."""
@ -613,6 +634,11 @@ class SanityTest(ABC):
"""True if the test targets should include directories."""
return False
@property
def include_symlinks(self): # type: () -> bool
"""True if the test targets should include symlinks."""
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."""
@ -655,6 +681,7 @@ class SanityCodeSmellTest(SanityTest):
self.__all_targets = self.config.get('all_targets') # type: bool
self.__no_targets = self.config.get('no_targets') # type: bool
self.__include_directories = self.config.get('include_directories') # type: bool
self.__include_symlinks = self.config.get('include_symlinks') # type: bool
else:
self.output = None
self.extensions = []
@ -666,6 +693,7 @@ class SanityCodeSmellTest(SanityTest):
self.__all_targets = False
self.__no_targets = True
self.__include_directories = False
self.__include_symlinks = False
if self.no_targets:
mutually_exclusive = (
@ -676,6 +704,7 @@ class SanityCodeSmellTest(SanityTest):
'ignore_self',
'all_targets',
'include_directories',
'include_symlinks',
)
problems = sorted(name for name in mutually_exclusive if getattr(self, name))
@ -698,6 +727,11 @@ class SanityCodeSmellTest(SanityTest):
"""True if the test targets should include directories."""
return self.__include_directories
@property
def include_symlinks(self): # type: () -> bool
"""True if the test targets should include symlinks."""
return self.__include_symlinks
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:

@ -172,18 +172,19 @@ def walk_units_targets():
return walk_test_targets(path=data_context().content.unit_path, module_path=data_context().content.unit_module_path, extensions=('.py',), prefix='test_')
def walk_compile_targets():
def walk_compile_targets(include_symlinks=True):
"""
:type include_symlinks: bool
:rtype: collections.Iterable[TestTarget]
"""
return walk_test_targets(module_path=data_context().content.module_path, extensions=('.py',), extra_dirs=('bin',))
return walk_test_targets(module_path=data_context().content.module_path, extensions=('.py',), extra_dirs=('bin',), include_symlinks=include_symlinks)
def walk_sanity_targets():
"""
:rtype: collections.Iterable[TestTarget]
"""
return walk_test_targets(module_path=data_context().content.module_path)
return walk_test_targets(module_path=data_context().content.module_path, include_symlinks=True, include_symlinked_directories=True)
def walk_posix_integration_targets(include_hidden=False):
@ -245,19 +246,21 @@ def load_integration_prefixes():
return prefixes
def walk_test_targets(path=None, module_path=None, extensions=None, prefix=None, extra_dirs=None):
def walk_test_targets(path=None, module_path=None, extensions=None, prefix=None, extra_dirs=None, include_symlinks=False, include_symlinked_directories=False):
"""
:type path: str | None
:type module_path: str | None
:type extensions: tuple[str] | None
:type prefix: str | None
:type extra_dirs: tuple[str] | None
:type include_symlinks: bool
:type include_symlinked_directories: bool
:rtype: collections.Iterable[TestTarget]
"""
if path:
file_paths = data_context().content.walk_files(path)
file_paths = data_context().content.walk_files(path, include_symlinked_directories=include_symlinked_directories)
else:
file_paths = data_context().content.all_files()
file_paths = data_context().content.all_files(include_symlinked_directories=include_symlinked_directories)
for file_path in file_paths:
name, ext = os.path.splitext(os.path.basename(file_path))
@ -268,12 +271,12 @@ def walk_test_targets(path=None, module_path=None, extensions=None, prefix=None,
if prefix and not name.startswith(prefix):
continue
if os.path.islink(to_bytes(file_path)):
# special case to allow a symlink of ansible_release.py -> ../release.py
if file_path != 'lib/ansible/module_utils/ansible_release.py':
continue
symlink = os.path.islink(to_bytes(file_path.rstrip(os.path.sep)))
if symlink and not include_symlinks:
continue
yield TestTarget(file_path, module_path, prefix, path)
yield TestTarget(file_path, module_path, prefix, path, symlink)
file_paths = []
@ -283,10 +286,12 @@ def walk_test_targets(path=None, module_path=None, extensions=None, prefix=None,
file_paths.append(file_path)
for file_path in file_paths:
if os.path.islink(to_bytes(file_path)):
symlink = os.path.islink(to_bytes(file_path.rstrip(os.path.sep)))
if symlink and not include_symlinks:
continue
yield TestTarget(file_path, module_path, prefix, path)
yield TestTarget(file_path, module_path, prefix, path, symlink)
def analyze_integration_target_dependencies(integration_targets):
@ -315,7 +320,7 @@ def analyze_integration_target_dependencies(integration_targets):
# this use case is supported, but discouraged
for target in integration_targets:
for path in data_context().content.walk_files(target.path):
if not os.path.islink(path):
if not os.path.islink(to_bytes(path.rstrip(os.path.sep))):
continue
real_link_path = os.path.realpath(path)
@ -442,18 +447,23 @@ class DirectoryTarget(CompletionTarget):
class TestTarget(CompletionTarget):
"""Generic test target."""
def __init__(self, path, module_path, module_prefix, base_path):
def __init__(self, path, module_path, module_prefix, base_path, symlink=None):
"""
:type path: str
:type module_path: str | None
:type module_prefix: str | None
:type base_path: str
:type symlink: bool | None
"""
super(TestTarget, self).__init__()
if symlink is None:
symlink = os.path.islink(to_bytes(path.rstrip(os.path.sep)))
self.name = path
self.path = path
self.base_path = base_path + '/' if base_path else None
self.symlink = symlink
name, ext = os.path.splitext(os.path.basename(self.path))

@ -1,4 +1,5 @@
{
"include_symlinks": true,
"prefixes": [
"test/runner/",
"test/sanity/ansible-doc/",

Loading…
Cancel
Save