| #!/usr/bin/python2 -u |
| # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """ |
| Check an autotest control file for required variables. |
| |
| This wrapper is invoked through autotest's PRESUBMIT.cfg for every commit |
| that edits a control file. |
| """ |
| |
| |
| import argparse |
| import fnmatch |
| import glob |
| import os |
| import re |
| import subprocess |
| |
| import common |
| from autotest_lib.client.common_lib import control_data |
| from autotest_lib.server.cros.dynamic_suite import reporting_utils |
| |
| |
| DEPENDENCY_ARC = 'arc' |
| SUITES_NEED_RETRY = set(['bvt-arc', 'bvt-cq', 'bvt-inline']) |
| TESTS_NEED_ARC = 'cheets_' |
| BVT_ATTRS = set( |
| ['suite:smoke', 'suite:bvt-inline', 'suite:bvt-cq', 'suite:bvt-arc']) |
| TAST_PSA_URL = ( |
| 'https://groups.google.com/a/chromium.org/d/topic/chromium-os-dev' |
| '/zH1nO7OjJ2M/discussion') |
| |
| |
| class ControlFileCheckerError(Exception): |
| """Raised when a necessary condition of this checker isn't satisfied.""" |
| |
| |
| def IsInChroot(): |
| """Return boolean indicating if we are running in the chroot.""" |
| return os.path.exists("/etc/debian_chroot") |
| |
| |
| def CommandPrefix(): |
| """Return an argv list which must appear at the start of shell commands.""" |
| if IsInChroot(): |
| return [] |
| else: |
| return ['cros_sdk', '--'] |
| |
| |
| def GetOverlayPath(overlay=None): |
| """ |
| Return the path to the overlay directory. |
| |
| If the overlay path is not given, the default chromiumos-overlay path |
| will be returned instead. |
| |
| @param overlay: The overlay repository path for autotest ebuilds. |
| |
| @return normalized absolutized path of the overlay repository. |
| """ |
| if not overlay: |
| ourpath = os.path.abspath(__file__) |
| overlay = os.path.join(os.path.dirname(ourpath), |
| "../../../../chromiumos-overlay/") |
| return os.path.normpath(overlay) |
| |
| |
| def GetAutotestTestPackages(overlay=None): |
| """ |
| Return a list of ebuilds which should be checked for test existance. |
| |
| @param overlay: The overlay repository path for autotest ebuilds. |
| |
| @return autotest packages in overlay repository. |
| """ |
| overlay = GetOverlayPath(overlay) |
| packages = glob.glob(os.path.join(overlay, "chromeos-base/autotest-*")) |
| # Return the packages list with the leading overlay path removed. |
| return [x[(len(overlay) + 1):] for x in packages] |
| |
| |
| def GetEqueryWrappers(): |
| """Return a list of all the equery variants that should be consulted.""" |
| # Note that we can't just glob.glob('/usr/local/bin/equery-*'), because |
| # we might be running outside the chroot. |
| pattern = '/usr/local/bin/equery-*' |
| cmd = CommandPrefix() + ['sh', '-c', 'echo %s' % pattern] |
| wrappers = subprocess.check_output(cmd).split() |
| # If there was no match, we get the literal pattern string echoed back. |
| if wrappers and wrappers[0] == pattern: |
| wrappers = [] |
| return ['equery'] + wrappers |
| |
| |
| def GetUseFlags(overlay=None): |
| """Get the set of all use flags from autotest packages. |
| |
| @param overlay: The overlay repository path for autotest ebuilds. |
| |
| @returns: useflags |
| """ |
| useflags = set() |
| for equery in GetEqueryWrappers(): |
| cmd_args = (CommandPrefix() + [equery, '-qC', 'uses'] + |
| GetAutotestTestPackages(overlay)) |
| child = subprocess.Popen(cmd_args, stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| new_useflags = child.communicate()[0].splitlines() |
| if child.returncode == 0: |
| useflags = useflags.union(new_useflags) |
| return useflags |
| |
| |
| def CheckSuites(ctrl_data, test_name, useflags): |
| """ |
| Check that any test in a SUITE is also in an ebuild. |
| |
| Throws a ControlFileCheckerError if a test within a SUITE |
| does not appear in an ebuild. For purposes of this check, |
| the psuedo-suite "manual" does not require a test to be |
| in an ebuild. |
| |
| @param ctrl_data: The control_data object for a test. |
| @param test_name: A string with the name of the test. |
| @param useflags: Set of all use flags from autotest packages. |
| |
| @returns: None |
| """ |
| if (hasattr(ctrl_data, 'suite') and ctrl_data.suite and |
| ctrl_data.suite != 'manual'): |
| # To handle the case where a developer has cros_workon'd |
| # e.g. autotest-tests on one particular board, and has the |
| # test listed only in the -9999 ebuild, we have to query all |
| # the equery-* board-wrappers until we find one. We ALSO have |
| # to check plain 'equery', to handle the case where e.g. a |
| # developer who has never run setup_board, and has no |
| # wrappers, is making a quick edit to some existing control |
| # file already enabled in the stable ebuild. |
| for flag in useflags: |
| if flag.startswith('-') or flag.startswith('+'): |
| flag = flag[1:] |
| if flag == 'tests_%s' % test_name: |
| return |
| raise ControlFileCheckerError( |
| 'No ebuild entry for %s. To fix, please do the following: 1. ' |
| 'Add your new test to one of the ebuilds referenced by ' |
| 'autotest-all. 2. cros_workon --board=<board> start ' |
| '<your_ebuild>. 3. emerge-<board> <your_ebuild>' % test_name) |
| |
| |
| def CheckValidAttr(ctrl_data, attr_allowlist, bvt_allowlist, test_name): |
| """ |
| Check whether ATTRIBUTES are in the allowlist. |
| |
| Throw a ControlFileCheckerError if tags in ATTRIBUTES don't exist in the |
| allowlist. |
| |
| @param ctrl_data: The control_data object for a test. |
| @param attr_allowlist: allowlist set parsed from the attribute_allowlist. |
| @param bvt_allowlist: allowlist set parsed from the bvt_allowlist. |
| @param test_name: A string with the name of the test. |
| |
| @returns: None |
| """ |
| if not (attr_allowlist >= ctrl_data.attributes): |
| attribute_diff = ctrl_data.attributes - attr_allowlist |
| raise ControlFileCheckerError( |
| 'Attribute(s): %s not in the allowlist in control file for test ' |
| 'named %s. If this is a new attribute, please add it into ' |
| 'AUTOTEST_DIR/site_utils/attribute_allowlist.txt file' % |
| (attribute_diff, test_name)) |
| if ctrl_data.attributes & BVT_ATTRS: |
| for pattern in bvt_allowlist: |
| if fnmatch.fnmatch(test_name, pattern): |
| break |
| else: |
| raise ControlFileCheckerError( |
| '%s not in the BVT allowlist. New BVT tests should be written ' |
| 'in Tast, not in Autotest. See: %s' % |
| (test_name, TAST_PSA_URL)) |
| |
| |
| def CheckSuiteLineRemoved(ctrl_file_path): |
| """ |
| Check whether the SUITE line has been removed since it is obsolete. |
| |
| @param ctrl_file_path: The path to the control file. |
| |
| @raises: ControlFileCheckerError if check fails. |
| """ |
| with open(ctrl_file_path, 'r') as f: |
| for line in f.readlines(): |
| if line.startswith('SUITE'): |
| raise ControlFileCheckerError( |
| 'SUITE is an obsolete variable, please remove it from %s. ' |
| 'Instead, add suite:<your_suite> to the ATTRIBUTES field.' |
| % ctrl_file_path) |
| |
| |
| def CheckRetry(ctrl_data, test_name): |
| """ |
| Check that any test in SUITES_NEED_RETRY has turned on retry. |
| |
| @param ctrl_data: The control_data object for a test. |
| @param test_name: A string with the name of the test. |
| |
| @raises: ControlFileCheckerError if check fails. |
| """ |
| if hasattr(ctrl_data, 'suite') and ctrl_data.suite: |
| suites = set(x.strip() for x in ctrl_data.suite.split(',') if x.strip()) |
| if ctrl_data.job_retries < 2 and SUITES_NEED_RETRY.intersection(suites): |
| raise ControlFileCheckerError( |
| 'Setting JOB_RETRIES to 2 or greater for test in ' |
| '%s is recommended. Please set it in the control ' |
| 'file for %s.' % (' or '.join(SUITES_NEED_RETRY), test_name)) |
| |
| |
| def CheckDependencies(ctrl_data, test_name): |
| """ |
| Check if any dependencies of a test is required |
| |
| @param ctrl_data: The control_data object for a test. |
| @param test_name: A string with the name of the test. |
| |
| @raises: ControlFileCheckerError if check fails. |
| """ |
| if test_name.startswith(TESTS_NEED_ARC): |
| if not DEPENDENCY_ARC in ctrl_data.dependencies: |
| raise ControlFileCheckerError( |
| 'DEPENDENCIES = \'arc\' for %s is needed' % test_name) |
| |
| |
| def main(): |
| """ |
| Checks if all control files that are a part of this commit conform to the |
| ChromeOS autotest guidelines. |
| """ |
| parser = argparse.ArgumentParser(description='Process overlay arguments.') |
| parser.add_argument('--overlay', default=None, help='the overlay directory path') |
| args = parser.parse_args() |
| file_list = os.environ.get('PRESUBMIT_FILES') |
| if file_list is None: |
| raise ControlFileCheckerError('Expected a list of presubmit files in ' |
| 'the PRESUBMIT_FILES environment variable.') |
| |
| # Parse the allowlist set from file, hardcode the filepath to the allowlist. |
| path_attr_allowlist = os.path.join(common.autotest_dir, |
| 'site_utils/attribute_allowlist.txt') |
| with open(path_attr_allowlist, 'r') as f: |
| attr_allowlist = { |
| line.strip() |
| for line in f.readlines() if line.strip() |
| } |
| |
| path_bvt_allowlist = os.path.join(common.autotest_dir, |
| 'site_utils/bvt_allowlist.txt') |
| with open(path_bvt_allowlist, 'r') as f: |
| bvt_allowlist = { |
| line.strip() |
| for line in f.readlines() if line.strip() |
| } |
| |
| # Delay getting the useflags. The call takes long time, so init useflags |
| # only when needed, i.e., the script needs to check any control file. |
| useflags = None |
| for file_path in file_list.split('\n'): |
| control_file = re.search(r'.*/control(?:\..+)?$', file_path) |
| if control_file: |
| ctrl_file_path = control_file.group(0) |
| CheckSuiteLineRemoved(ctrl_file_path) |
| ctrl_data = control_data.parse_control(ctrl_file_path, |
| raise_warnings=True) |
| test_name = os.path.basename(os.path.split(file_path)[0]) |
| try: |
| reporting_utils.BugTemplate.validate_bug_template( |
| ctrl_data.bug_template) |
| except AttributeError: |
| # The control file may not have bug template defined. |
| pass |
| |
| if not useflags: |
| useflags = GetUseFlags(args.overlay) |
| CheckSuites(ctrl_data, test_name, useflags) |
| CheckValidAttr(ctrl_data, attr_allowlist, bvt_allowlist, test_name) |
| CheckRetry(ctrl_data, test_name) |
| CheckDependencies(ctrl_data, test_name) |
| |
| |
| if __name__ == '__main__': |
| main() |