Home | History | Annotate | Download | only in presubmit_hooks
      1 #!/usr/bin/python -u
      2 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """
      7 Check an autotest control file for required variables.
      8 
      9 This wrapper is invoked through autotest's PRESUBMIT.cfg for every commit
     10 that edits a control file.
     11 """
     12 
     13 
     14 import argparse
     15 import glob
     16 import os
     17 import re
     18 import subprocess
     19 
     20 import common
     21 from autotest_lib.client.common_lib import control_data
     22 from autotest_lib.server.cros.dynamic_suite import reporting_utils
     23 
     24 
     25 DEPENDENCY_ARC = 'arc'
     26 SUITES_NEED_RETRY = set(['bvt-arc', 'bvt-cq', 'bvt-inline'])
     27 TESTS_NEED_ARC = 'cheets_'
     28 
     29 
     30 class ControlFileCheckerError(Exception):
     31     """Raised when a necessary condition of this checker isn't satisfied."""
     32 
     33 
     34 def IsInChroot():
     35     """Return boolean indicating if we are running in the chroot."""
     36     return os.path.exists("/etc/debian_chroot")
     37 
     38 
     39 def CommandPrefix():
     40     """Return an argv list which must appear at the start of shell commands."""
     41     if IsInChroot():
     42         return []
     43     else:
     44         return ['cros_sdk', '--']
     45 
     46 
     47 def GetOverlayPath(overlay=None):
     48     """
     49     Return the path to the overlay directory.
     50 
     51     If the overlay path is not given, the default chromiumos-overlay path
     52     will be returned instead.
     53 
     54     @param overlay: The overlay repository path for autotest ebuilds.
     55 
     56     @return normalized absolutized path of the overlay repository.
     57     """
     58     if not overlay:
     59         ourpath = os.path.abspath(__file__)
     60         overlay = os.path.join(os.path.dirname(ourpath),
     61                                "../../../../chromiumos-overlay/")
     62     return os.path.normpath(overlay)
     63 
     64 
     65 def GetAutotestTestPackages(overlay=None):
     66     """
     67     Return a list of ebuilds which should be checked for test existance.
     68 
     69     @param overlay: The overlay repository path for autotest ebuilds.
     70 
     71     @return autotest packages in overlay repository.
     72     """
     73     overlay = GetOverlayPath(overlay)
     74     packages = glob.glob(os.path.join(overlay, "chromeos-base/autotest-*"))
     75     # Return the packages list with the leading overlay path removed.
     76     return [x[(len(overlay) + 1):] for x in packages]
     77 
     78 
     79 def GetEqueryWrappers():
     80     """Return a list of all the equery variants that should be consulted."""
     81     # Note that we can't just glob.glob('/usr/local/bin/equery-*'), because
     82     # we might be running outside the chroot.
     83     pattern = '/usr/local/bin/equery-*'
     84     cmd = CommandPrefix() + ['sh', '-c', 'echo %s' % pattern]
     85     wrappers = subprocess.check_output(cmd).split()
     86     # If there was no match, we get the literal pattern string echoed back.
     87     if wrappers and wrappers[0] == pattern:
     88         wrappers = []
     89     return ['equery'] + wrappers
     90 
     91 
     92 def GetUseFlags(overlay=None):
     93     """Get the set of all use flags from autotest packages.
     94 
     95     @param overlay: The overlay repository path for autotest ebuilds.
     96 
     97     @returns: useflags
     98     """
     99     useflags = set()
    100     for equery in GetEqueryWrappers():
    101         cmd_args = (CommandPrefix() + [equery, '-qC', 'uses'] +
    102                     GetAutotestTestPackages(overlay))
    103         child = subprocess.Popen(cmd_args, stdout=subprocess.PIPE,
    104                                  stderr=subprocess.PIPE)
    105         new_useflags = child.communicate()[0].splitlines()
    106         if child.returncode == 0:
    107             useflags = useflags.union(new_useflags)
    108     return useflags
    109 
    110 
    111 def CheckSuites(ctrl_data, test_name, useflags):
    112     """
    113     Check that any test in a SUITE is also in an ebuild.
    114 
    115     Throws a ControlFileCheckerError if a test within a SUITE
    116     does not appear in an ebuild. For purposes of this check,
    117     the psuedo-suite "manual" does not require a test to be
    118     in an ebuild.
    119 
    120     @param ctrl_data: The control_data object for a test.
    121     @param test_name: A string with the name of the test.
    122     @param useflags: Set of all use flags from autotest packages.
    123 
    124     @returns: None
    125     """
    126     if (hasattr(ctrl_data, 'suite') and ctrl_data.suite and
    127         ctrl_data.suite != 'manual'):
    128         # To handle the case where a developer has cros_workon'd
    129         # e.g. autotest-tests on one particular board, and has the
    130         # test listed only in the -9999 ebuild, we have to query all
    131         # the equery-* board-wrappers until we find one. We ALSO have
    132         # to check plain 'equery', to handle the case where e.g. a
    133         # developer who has never run setup_board, and has no
    134         # wrappers, is making a quick edit to some existing control
    135         # file already enabled in the stable ebuild.
    136         for flag in useflags:
    137             if flag.startswith('-') or flag.startswith('+'):
    138                 flag = flag[1:]
    139             if flag == 'tests_%s' % test_name:
    140                 return
    141         raise ControlFileCheckerError(
    142                 'No ebuild entry for %s. To fix, please do the following: 1. '
    143                 'Add your new test to one of the ebuilds referenced by '
    144                 'autotest-all. 2. cros_workon --board=<board> start '
    145                 '<your_ebuild>. 3. emerge-<board> <your_ebuild>' % test_name)
    146 
    147 
    148 def CheckValidAttr(ctrl_data, whitelist, test_name):
    149     """
    150     Check whether ATTRIBUTES are in the whitelist.
    151 
    152     Throw a ControlFileCheckerError if tags in ATTRIBUTES don't exist in the
    153     whitelist.
    154 
    155     @param ctrl_data: The control_data object for a test.
    156     @param whitelist: whitelist set parsed from the attribute_whitelist file.
    157     @param test_name: A string with the name of the test.
    158 
    159     @returns: None
    160     """
    161     if not (whitelist >= ctrl_data.attributes):
    162         attribute_diff = ctrl_data.attributes - whitelist
    163         raise ControlFileCheckerError(
    164             'Attribute(s): %s not in the whitelist in control file for test '
    165             'named %s. If this is a new attribute, please add it into '
    166             'AUTOTEST_DIR/site_utils/attribute_whitelist.txt file'
    167             % (attribute_diff, test_name))
    168 
    169 
    170 def CheckSuiteLineRemoved(ctrl_file_path):
    171     """
    172     Check whether the SUITE line has been removed since it is obsolete.
    173 
    174     @param ctrl_file_path: The path to the control file.
    175 
    176     @raises: ControlFileCheckerError if check fails.
    177     """
    178     with open(ctrl_file_path, 'r') as f:
    179         for line in f.readlines():
    180             if line.startswith('SUITE'):
    181                 raise ControlFileCheckerError(
    182                     'SUITE is an obsolete variable, please remove it from %s. '
    183                     'Instead, add suite:<your_suite> to the ATTRIBUTES field.'
    184                     % ctrl_file_path)
    185 
    186 
    187 def CheckRetry(ctrl_data, test_name):
    188     """
    189     Check that any test in SUITES_NEED_RETRY has turned on retry.
    190 
    191     @param ctrl_data: The control_data object for a test.
    192     @param test_name: A string with the name of the test.
    193 
    194     @raises: ControlFileCheckerError if check fails.
    195     """
    196     if hasattr(ctrl_data, 'suite') and ctrl_data.suite:
    197         suites = set(x.strip() for x in ctrl_data.suite.split(',') if x.strip())
    198         if ctrl_data.job_retries < 2 and SUITES_NEED_RETRY.intersection(suites):
    199             raise ControlFileCheckerError(
    200                 'Setting JOB_RETRIES to 2 or greater for test in '
    201                 '%s is recommended. Please set it in the control '
    202                 'file for %s.' % (' or '.join(SUITES_NEED_RETRY), test_name))
    203 
    204 
    205 def CheckDependencies(ctrl_data, test_name):
    206     """
    207     Check if any dependencies of a test is required
    208 
    209     @param ctrl_data: The control_data object for a test.
    210     @param test_name: A string with the name of the test.
    211 
    212     @raises: ControlFileCheckerError if check fails.
    213     """
    214     if test_name.startswith(TESTS_NEED_ARC):
    215         if not DEPENDENCY_ARC in ctrl_data.dependencies:
    216             raise ControlFileCheckerError(
    217                     'DEPENDENCIES = \'arc\' for %s is needed' % test_name)
    218 
    219 
    220 def main():
    221     """
    222     Checks if all control files that are a part of this commit conform to the
    223     ChromeOS autotest guidelines.
    224     """
    225     parser = argparse.ArgumentParser(description='Process overlay arguments.')
    226     parser.add_argument('--overlay', default=None, help='the overlay directory path')
    227     args = parser.parse_args()
    228     file_list = os.environ.get('PRESUBMIT_FILES')
    229     if file_list is None:
    230         raise ControlFileCheckerError('Expected a list of presubmit files in '
    231             'the PRESUBMIT_FILES environment variable.')
    232 
    233     # Parse the whitelist set from file, hardcode the filepath to the whitelist.
    234     path_whitelist = os.path.join(common.autotest_dir,
    235                                   'site_utils/attribute_whitelist.txt')
    236     with open(path_whitelist, 'r') as f:
    237         whitelist = {line.strip() for line in f.readlines() if line.strip()}
    238 
    239     # Delay getting the useflags. The call takes long time, so init useflags
    240     # only when needed, i.e., the script needs to check any control file.
    241     useflags = None
    242     for file_path in file_list.split('\n'):
    243         control_file = re.search(r'.*/control(?:\.\w+)?$', file_path)
    244         if control_file:
    245             ctrl_file_path = control_file.group(0)
    246             CheckSuiteLineRemoved(ctrl_file_path)
    247             ctrl_data = control_data.parse_control(ctrl_file_path,
    248                                                    raise_warnings=True)
    249             test_name = os.path.basename(os.path.split(file_path)[0])
    250             try:
    251                 reporting_utils.BugTemplate.validate_bug_template(
    252                         ctrl_data.bug_template)
    253             except AttributeError:
    254                 # The control file may not have bug template defined.
    255                 pass
    256 
    257             if not useflags:
    258                 useflags = GetUseFlags(args.overlay)
    259             CheckSuites(ctrl_data, test_name, useflags)
    260             CheckValidAttr(ctrl_data, whitelist, test_name)
    261             CheckRetry(ctrl_data, test_name)
    262             CheckDependencies(ctrl_data, test_name)
    263 
    264 
    265 if __name__ == '__main__':
    266     main()
    267