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