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 argparse 15import glob 16import os 17import re 18import subprocess 19 20import common 21from autotest_lib.client.common_lib import control_data 22from autotest_lib.server.cros.dynamic_suite import reporting_utils 23 24 25DEPENDENCY_ARC = 'arc' 26SUITES_NEED_RETRY = set(['bvt-arc', 'bvt-cq', 'bvt-inline']) 27TESTS_NEED_ARC = 'cheets_' 28 29 30class ControlFileCheckerError(Exception): 31 """Raised when a necessary condition of this checker isn't satisfied.""" 32 33 34def IsInChroot(): 35 """Return boolean indicating if we are running in the chroot.""" 36 return os.path.exists("/etc/debian_chroot") 37 38 39def 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 47def 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 65def 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 79def 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 92def 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 111def 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 148def 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 170def 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 187def 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 205def 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 220def 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 265if __name__ == '__main__': 266 main() 267