def get_errors(self, paths): # type: (t.List[str]) -> t.List[SanityMessage] """Return error messages related to issues with the file.""" messages = [] # unused errors unused = [] # type: t.List[t.Tuple[int, str, str]] 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)) for path in paths: path_entry = self.ignore_entries.get(path) if not path_entry: continue unused.extend((line_no, path, code) for code, line_no in path_entry.items() if line_no not in self.used_line_numbers) messages.extend(SanityMessage( code=self.code, message="Ignoring '%s' on '%s' is unnecessary" % (code, path) if self.code else "Ignoring '%s' is unnecessary" % path, path=self.parser.relative_path, line=line, column=1, confidence=calculate_best_confidence(((self.parser.path, line), (path, 0)), self.args.metadata) if self.args.metadata.changes else None, ) for line, path, code in unused) return messages
def command_sanity(args): """ :type args: SanityConfig """ changes = get_changes_filter(args) require = args.require + changes targets = SanityTargets.create(args.include, args.exclude, require) if not targets.include: raise AllTargetsSkipped() if args.delegate: raise Delegate(require=changes, exclude=args.exclude) install_command_requirements(args) tests = sanity_get_tests() if args.test: tests = [target for target in tests if target.name in args.test] else: disabled = [ target.name for target in tests if not target.enabled and not args.allow_disabled ] tests = [ target for target in tests if target.enabled or args.allow_disabled ] if disabled: display.warning( 'Skipping tests disabled by default without --allow-disabled: %s' % ', '.join(sorted(disabled))) if args.skip_test: tests = [ target for target in tests if target.name not in args.skip_test ] total = 0 failed = [] for test in tests: if args.list_tests: display.info(test.name) continue available_versions = get_available_python_versions( SUPPORTED_PYTHON_VERSIONS) if args.python: # specific version selected versions = (args.python, ) elif isinstance(test, SanityMultipleVersion): # try all supported versions for multi-version tests when a specific version has not been selected versions = test.supported_python_versions elif not test.supported_python_versions or args.python_version in test.supported_python_versions: # the test works with any version or the version we're already running versions = (args.python_version, ) else: # available versions supported by the test versions = tuple( sorted( set(available_versions) & set(test.supported_python_versions))) # use the lowest available version supported by the test or the current version as a fallback (which will be skipped) 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, 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, skip_version) else: check_pyyaml(args, version) if test.supported_python_versions: display.info("Running sanity test '%s' with Python %s" % (test.name, version)) else: display.info("Running sanity test '%s'" % test.name) if isinstance(test, SanityCodeSmellTest): settings = test.load_processor(args) elif isinstance(test, SanityMultipleVersion): settings = test.load_processor(args, version) elif isinstance(test, SanitySingleVersion): settings = test.load_processor(args) elif isinstance(test, SanityVersionNeutral): 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 = tuple() 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])) 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 if isinstance(result, SanityFailure): failed.append(result.test + options) if failed: message = 'The %d sanity test(s) listed below (out of %d) failed. See error output above for details.\n%s' % ( len(failed), total, '\n'.join(failed)) if args.failure_ok: display.error(message) else: raise ApplicationError(message)
def __init__(self, args): # type: (SanityConfig) -> None if data_context().content.collection: ansible_version = '%s.%s' % tuple( get_ansible_version(args).split('.')[:2]) ansible_label = 'Ansible %s' % ansible_version file_name = 'ignore-%s.txt' % ansible_version else: ansible_label = 'Ansible' file_name = 'ignore.txt' self.args = args self.relative_path = os.path.join('test/sanity', file_name) self.path = os.path.join(data_context().content.root, self.relative_path) self.ignores = collections.defaultdict(lambda: collections.defaultdict( dict)) # type: t.Dict[str, t.Dict[str, t.Dict[str, int]]] self.skips = collections.defaultdict(lambda: collections.defaultdict( int)) # type: t.Dict[str, t.Dict[str, int]] self.parse_errors = [] # type: t.List[t.Tuple[int, int, str]] self.file_not_found_errors = [] # type: t.List[t.Tuple[int, str]] lines = read_lines_without_comments(self.path, optional=True) targets = SanityTargets.get_targets() paths = set(target.path for target in targets) tests_by_name = {} # type: t.Dict[str, SanityTest] versioned_test_names = set() # type: t.Set[str] unversioned_test_names = {} # type: t.Dict[str, str] directories = paths_to_dirs(list(paths)) paths_by_test = {} # type: t.Dict[str, t.Set[str]] 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])) paths_by_test[test.name] = set( target.path for target in test.filter_targets(test_targets)) if isinstance(test, SanityMultipleVersion): versioned_test_names.add(test.name) tests_by_name.update( dict(('%s-%s' % (test.name, python_version), test) for python_version in test.supported_python_versions)) else: unversioned_test_names.update( dict(('%s-%s' % (test.name, python_version), test.name) for python_version in SUPPORTED_PYTHON_VERSIONS)) tests_by_name[test.name] = test for line_no, line in enumerate(lines, start=1): if not line: self.parse_errors.append( (line_no, 1, "Line cannot be empty or contain only a comment")) continue parts = line.split(' ') path = parts[0] codes = parts[1:] if not path: self.parse_errors.append( (line_no, 1, "Line cannot start with a space")) continue if path.endswith(os.path.sep): if path not in directories: self.file_not_found_errors.append((line_no, path)) continue else: if path not in paths: self.file_not_found_errors.append((line_no, path)) continue if not codes: self.parse_errors.append( (line_no, len(path), "Error code required after path")) continue code = codes[0] if not code: self.parse_errors.append( (line_no, len(path) + 1, "Error code after path cannot be empty")) continue if len(codes) > 1: self.parse_errors.append((line_no, len(path) + len(code) + 2, "Error code cannot contain spaces")) continue parts = code.split('!') code = parts[0] commands = parts[1:] parts = code.split(':') test_name = parts[0] error_codes = parts[1:] test = tests_by_name.get(test_name) if not test: unversioned_name = unversioned_test_names.get(test_name) if unversioned_name: self.parse_errors.append(( line_no, len(path) + len(unversioned_name) + 2, "Sanity test '%s' cannot use a Python version like '%s'" % (unversioned_name, test_name))) elif test_name in versioned_test_names: self.parse_errors.append(( line_no, len(path) + len(test_name) + 1, "Sanity test '%s' requires a Python version like '%s-%s'" % (test_name, test_name, args.python_version))) else: self.parse_errors.append( (line_no, len(path) + 2, "Sanity test '%s' does not exist" % test_name)) continue if path.endswith(os.path.sep) and not test.include_directories: self.parse_errors.append( (line_no, 1, "Sanity test '%s' does not support directory paths" % test_name)) continue if path not in paths_by_test[test.name] and not test.no_targets: self.parse_errors.append( (line_no, 1, "Sanity test '%s' does not test path '%s'" % (test_name, path))) continue if commands and error_codes: self.parse_errors.append( (line_no, len(path) + len(test_name) + 2, "Error code cannot contain both '!' and ':' characters")) continue if commands: command = commands[0] if len(commands) > 1: self.parse_errors.append( (line_no, len(path) + len(test_name) + len(command) + 3, "Error code cannot contain multiple '!' characters")) continue if command == 'skip': if not test.can_skip: self.parse_errors.append( (line_no, len(path) + len(test_name) + 2, "Sanity test '%s' cannot be skipped" % test_name)) continue existing_line_no = self.skips.get(test_name, {}).get(path) if existing_line_no: self.parse_errors.append(( line_no, 1, "Duplicate '%s' skip for path '%s' first found on line %d" % (test_name, path, existing_line_no))) continue self.skips[test_name][path] = line_no continue self.parse_errors.append( (line_no, len(path) + len(test_name) + 2, "Command '!%s' not recognized" % command)) continue if not test.can_ignore: self.parse_errors.append( (line_no, len(path) + 1, "Sanity test '%s' cannot be ignored" % test_name)) continue if test.error_code: if not error_codes: self.parse_errors.append( (line_no, len(path) + len(test_name) + 1, "Sanity test '%s' requires an error code" % test_name)) continue error_code = error_codes[0] if len(error_codes) > 1: self.parse_errors.append( (line_no, len(path) + len(test_name) + len(error_code) + 3, "Error code cannot contain multiple ':' characters")) continue else: if error_codes: self.parse_errors.append( (line_no, len(path) + len(test_name) + 2, "Sanity test '%s' does not support error codes" % test_name)) continue error_code = self.NO_CODE existing = self.ignores.get(test_name, {}).get(path, {}).get(error_code) if existing: if test.error_code: self.parse_errors.append(( line_no, 1, "Duplicate '%s' ignore for error code '%s' for path '%s' first found on line %d" % (test_name, error_code, path, existing))) else: self.parse_errors.append(( line_no, 1, "Duplicate '%s' ignore for path '%s' first found on line %d" % (test_name, path, existing))) continue self.ignores[test_name][path][error_code] = line_no