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