1#!/usr/bin/python3 -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 # [bytes] ==> [str] 112 new_useflags = [ 113 c.decode() if isinstance(c, bytes) else c 114 for c in child.communicate()[0].splitlines() 115 ] 116 if child.returncode == 0: 117 useflags = useflags.union(new_useflags) 118 return useflags 119 120 121def CheckSuites(ctrl_data, test_name, useflags): 122 """ 123 Check that any test in a SUITE is also in an ebuild. 124 125 Throws a ControlFileCheckerError if a test within a SUITE 126 does not appear in an ebuild. For purposes of this check, 127 the psuedo-suite "manual" does not require a test to be 128 in an ebuild. 129 130 @param ctrl_data: The control_data object for a test. 131 @param test_name: A string with the name of the test. 132 @param useflags: Set of all use flags from autotest packages. 133 134 @returns: None 135 """ 136 if (hasattr(ctrl_data, 'suite') and ctrl_data.suite and 137 ctrl_data.suite != 'manual'): 138 # To handle the case where a developer has cros_workon'd 139 # e.g. autotest-tests on one particular board, and has the 140 # test listed only in the -9999 ebuild, we have to query all 141 # the equery-* board-wrappers until we find one. We ALSO have 142 # to check plain 'equery', to handle the case where e.g. a 143 # developer who has never run setup_board, and has no 144 # wrappers, is making a quick edit to some existing control 145 # file already enabled in the stable ebuild. 146 for flag in useflags: 147 if flag.startswith('-') or flag.startswith('+'): 148 flag = flag[1:] 149 if flag == 'tests_%s' % test_name: 150 return 151 raise ControlFileCheckerError( 152 'No ebuild entry for %s. To fix, please do the following: 1. ' 153 'Add your new test to one of the ebuilds referenced by ' 154 'autotest-all. 2. cros_workon --board=<board> start ' 155 '<your_ebuild>. 3. emerge-<board> <your_ebuild>' % test_name) 156 157 158def CheckValidAttr(ctrl_data, attr_allowlist, bvt_allowlist, test_name): 159 """ 160 Check whether ATTRIBUTES are in the allowlist. 161 162 Throw a ControlFileCheckerError if tags in ATTRIBUTES don't exist in the 163 allowlist. 164 165 @param ctrl_data: The control_data object for a test. 166 @param attr_allowlist: allowlist set parsed from the attribute_allowlist. 167 @param bvt_allowlist: allowlist set parsed from the bvt_allowlist. 168 @param test_name: A string with the name of the test. 169 170 @returns: None 171 """ 172 if not (attr_allowlist >= ctrl_data.attributes): 173 attribute_diff = ctrl_data.attributes - attr_allowlist 174 raise ControlFileCheckerError( 175 'Attribute(s): %s not in the allowlist in control file for test ' 176 'named %s. If this is a new attribute, please add it into ' 177 'AUTOTEST_DIR/site_utils/attribute_allowlist.txt file' % 178 (attribute_diff, test_name)) 179 if ctrl_data.attributes & BVT_ATTRS: 180 for pattern in bvt_allowlist: 181 if fnmatch.fnmatch(test_name, pattern): 182 break 183 else: 184 raise ControlFileCheckerError( 185 '%s not in the BVT allowlist. New BVT tests should be written ' 186 'in Tast, not in Autotest. See: %s' % 187 (test_name, TAST_PSA_URL)) 188 189 190def CheckSuiteLineRemoved(ctrl_file_path): 191 """ 192 Check whether the SUITE line has been removed since it is obsolete. 193 194 @param ctrl_file_path: The path to the control file. 195 196 @raises: ControlFileCheckerError if check fails. 197 """ 198 with open(ctrl_file_path, 'r') as f: 199 for line in f.readlines(): 200 if line.startswith('SUITE'): 201 raise ControlFileCheckerError( 202 'SUITE is an obsolete variable, please remove it from %s. ' 203 'Instead, add suite:<your_suite> to the ATTRIBUTES field.' 204 % ctrl_file_path) 205 206 207def CheckRetry(ctrl_data, test_name): 208 """ 209 Check that any test in SUITES_NEED_RETRY has turned on retry. 210 211 @param ctrl_data: The control_data object for a test. 212 @param test_name: A string with the name of the test. 213 214 @raises: ControlFileCheckerError if check fails. 215 """ 216 if hasattr(ctrl_data, 'suite') and ctrl_data.suite: 217 suites = set(x.strip() for x in ctrl_data.suite.split(',') if x.strip()) 218 if ctrl_data.job_retries < 2 and SUITES_NEED_RETRY.intersection(suites): 219 raise ControlFileCheckerError( 220 'Setting JOB_RETRIES to 2 or greater for test in ' 221 '%s is recommended. Please set it in the control ' 222 'file for %s.' % (' or '.join(SUITES_NEED_RETRY), test_name)) 223 224 225def CheckDependencies(ctrl_data, test_name): 226 """ 227 Check if any dependencies of a test is required 228 229 @param ctrl_data: The control_data object for a test. 230 @param test_name: A string with the name of the test. 231 232 @raises: ControlFileCheckerError if check fails. 233 """ 234 if test_name.startswith(TESTS_NEED_ARC): 235 if not DEPENDENCY_ARC in ctrl_data.dependencies: 236 raise ControlFileCheckerError( 237 'DEPENDENCIES = \'arc\' for %s is needed' % test_name) 238 239 240def main(): 241 """ 242 Checks if all control files that are a part of this commit conform to the 243 ChromeOS autotest guidelines. 244 """ 245 parser = argparse.ArgumentParser(description='Process overlay arguments.') 246 parser.add_argument('--overlay', default=None, help='the overlay directory path') 247 args = parser.parse_args() 248 file_list = os.environ.get('PRESUBMIT_FILES') 249 if file_list is None: 250 raise ControlFileCheckerError('Expected a list of presubmit files in ' 251 'the PRESUBMIT_FILES environment variable.') 252 253 # Parse the allowlist set from file, hardcode the filepath to the allowlist. 254 path_attr_allowlist = os.path.join(common.autotest_dir, 255 'site_utils/attribute_allowlist.txt') 256 with open(path_attr_allowlist, 'r') as f: 257 attr_allowlist = { 258 line.strip() 259 for line in f.readlines() if line.strip() 260 } 261 262 path_bvt_allowlist = os.path.join(common.autotest_dir, 263 'site_utils/bvt_allowlist.txt') 264 with open(path_bvt_allowlist, 'r') as f: 265 bvt_allowlist = { 266 line.strip() 267 for line in f.readlines() if line.strip() 268 } 269 270 # Delay getting the useflags. The call takes long time, so init useflags 271 # only when needed, i.e., the script needs to check any control file. 272 useflags = None 273 for file_path in file_list.split('\n'): 274 control_file = re.search(r'.*/control(?:\..+)?$', file_path) 275 if control_file: 276 ctrl_file_path = control_file.group(0) 277 CheckSuiteLineRemoved(ctrl_file_path) 278 ctrl_data = control_data.parse_control(ctrl_file_path, 279 raise_warnings=True) 280 test_name = os.path.basename(os.path.split(file_path)[0]) 281 try: 282 reporting_utils.BugTemplate.validate_bug_template( 283 ctrl_data.bug_template) 284 except AttributeError: 285 # The control file may not have bug template defined. 286 pass 287 288 if not useflags: 289 useflags = GetUseFlags(args.overlay) 290 CheckSuites(ctrl_data, test_name, useflags) 291 CheckValidAttr(ctrl_data, attr_allowlist, bvt_allowlist, test_name) 292 CheckRetry(ctrl_data, test_name) 293 CheckDependencies(ctrl_data, test_name) 294 295if __name__ == '__main__': 296 main() 297