1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright 2019 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Runs a tryjob/tryjobs after updating the packages.""" 8 9from __future__ import print_function 10 11import argparse 12import datetime 13import json 14import os 15import subprocess 16 17import chroot 18import failure_modes 19import get_llvm_hash 20import update_chromeos_llvm_hash 21 22VALID_CQ_TRYBOTS = ['llvm', 'llvm-next', 'llvm-tot'] 23 24 25def GetCommandLineArgs(): 26 """Parses the command line for the command line arguments. 27 28 Returns: 29 The log level to use when retrieving the LLVM hash or google3 LLVM version, 30 the chroot path to use for executing chroot commands, 31 a list of a package or packages to update their LLVM next hash, 32 and the LLVM version to use when retrieving the LLVM hash. 33 """ 34 35 # Default path to the chroot if a path is not specified. 36 cros_root = os.path.expanduser('~') 37 cros_root = os.path.join(cros_root, 'chromiumos') 38 39 # Create parser and add optional command-line arguments. 40 parser = argparse.ArgumentParser( 41 description='Update an LLVM hash of packages and run tests.') 42 43 # Add argument for other change lists that want to run alongside the tryjob 44 # which has a change list of updating a package's git hash. 45 parser.add_argument( 46 '--extra_change_lists', 47 type=int, 48 nargs='+', 49 default=[], 50 help='change lists that would like to be run alongside the change list ' 51 'of updating the packages') 52 53 # Add argument for a specific chroot path. 54 parser.add_argument('--chroot_path', 55 default=cros_root, 56 help='the path to the chroot (default: %(default)s)') 57 58 # Add argument to choose between llvm and llvm-next. 59 parser.add_argument( 60 '--is_llvm_next', 61 action='store_true', 62 help='which llvm hash to update. Update LLVM_NEXT_HASH if specified. ' 63 'Otherwise, update LLVM_HASH') 64 65 # Add argument for the absolute path to the file that contains information on 66 # the previous tested svn version. 67 parser.add_argument( 68 '--last_tested', 69 help='the absolute path to the file that contains the last tested ' 70 'arguments.') 71 72 # Add argument for the LLVM version to use. 73 parser.add_argument('--llvm_version', 74 type=get_llvm_hash.IsSvnOption, 75 required=True, 76 help='which git hash of LLVM to find ' 77 '{google3, ToT, <svn_version>} ' 78 '(default: finds the git hash of the google3 LLVM ' 79 'version)') 80 81 # Add argument to add reviewers for the created CL. 82 parser.add_argument('--reviewers', 83 nargs='+', 84 default=[], 85 help='The reviewers for the package update changelist') 86 87 # Add argument for whether to display command contents to `stdout`. 88 parser.add_argument('--verbose', 89 action='store_true', 90 help='display contents of a command to the terminal ' 91 '(default: %(default)s)') 92 93 subparsers = parser.add_subparsers(dest='subparser_name') 94 subparser_names = [] 95 # Testing with the tryjobs. 96 tryjob_subparser = subparsers.add_parser('tryjobs') 97 subparser_names.append('tryjobs') 98 tryjob_subparser.add_argument('--builders', 99 required=True, 100 nargs='+', 101 default=[], 102 help='builders to use for the tryjob testing') 103 104 # Add argument for custom options for the tryjob. 105 tryjob_subparser.add_argument('--options', 106 required=False, 107 nargs='+', 108 default=[], 109 help='options to use for the tryjob testing') 110 111 # Testing with the recipe builders 112 recipe_subparser = subparsers.add_parser('recipe') 113 subparser_names.append('recipe') 114 recipe_subparser.add_argument('--options', 115 required=False, 116 nargs='+', 117 default=[], 118 help='options passed to the recipe builders') 119 120 recipe_subparser.add_argument('--builders', 121 required=True, 122 nargs='+', 123 default=[], 124 help='recipe builders to launch') 125 126 # Testing with CQ. 127 cq_subparser = subparsers.add_parser('cq') 128 subparser_names.append('cq') 129 130 # Add argument for specify a cq trybot to test along with other cq builders 131 # e.g. llvm, llvm-next or llvm-tot 132 cq_subparser.add_argument( 133 '--cq_trybot', 134 choices=VALID_CQ_TRYBOTS, 135 help='include the trybot to test together with other cq builders ' 136 'available: %(choices)s') 137 138 args_output = parser.parse_args() 139 140 if args_output.subparser_name not in subparser_names: 141 parser.error('one of %s must be specified' % subparser_names) 142 143 return args_output 144 145 146def UnchangedSinceLastRun(last_tested_file, arg_dict): 147 """Gets the arguments used for last run 148 149 Args: 150 last_tested_file: The absolute path to the file that contains the 151 arguments for the last run. 152 arg_dict: The arguments used for this run. 153 154 Returns: 155 Return true if the arguments used for last run exist and are the 156 same as the arguments used for this run. Otherwise return false. 157 """ 158 159 if not last_tested_file: 160 return False 161 162 # Get the last tested svn version if the file exists. 163 last_arg_dict = None 164 try: 165 with open(last_tested_file) as f: 166 last_arg_dict = json.load(f) 167 168 except (IOError, ValueError): 169 return False 170 171 return arg_dict == last_arg_dict 172 173 174def AddReviewers(cl, reviewers, chroot_path): 175 """Add reviewers for the created CL.""" 176 177 gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit') 178 for reviewer in reviewers: 179 cmd = [gerrit_abs_path, 'reviewers', str(cl), reviewer] 180 181 subprocess.check_output(cmd) 182 183 184def AddLinksToCL(tests, cl, chroot_path): 185 """Adds the test link(s) to the CL as a comment.""" 186 187 # NOTE: Invoking `cros_sdk` does not make each tryjob link appear on its own 188 # line, so invoking the `gerrit` command directly instead of using `cros_sdk` 189 # to do it for us. 190 # 191 # FIXME: Need to figure out why `cros_sdk` does not add each tryjob link as a 192 # newline. 193 gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit') 194 195 links = ['Started the following tests:'] 196 links.extend(test['link'] for test in tests) 197 198 add_message_cmd = [gerrit_abs_path, 'message', str(cl), '\n'.join(links)] 199 200 subprocess.check_output(add_message_cmd) 201 202 203# Testing with tryjobs 204def GetCurrentTimeInUTC(): 205 """Returns the current time via `datetime.datetime.utcnow()`.""" 206 return datetime.datetime.utcnow() 207 208 209def GetTryJobCommand(change_list, extra_change_lists, options, builder): 210 """Constructs the 'tryjob' command. 211 212 Args: 213 change_list: The CL obtained from updating the packages. 214 extra_change_lists: Extra change lists that would like to be run alongside 215 the change list of updating the packages. 216 options: Options to be passed into the tryjob command. 217 builder: The builder to be passed into the tryjob command. 218 219 Returns: 220 The 'tryjob' command with the change list of updating the packages and 221 any extra information that was passed into the command line. 222 """ 223 224 tryjob_cmd = ['cros', 'tryjob', '--yes', '--json', '-g', '%d' % change_list] 225 226 if extra_change_lists: 227 for extra_cl in extra_change_lists: 228 tryjob_cmd.extend(['-g', '%d' % extra_cl]) 229 230 if options: 231 tryjob_cmd.extend('--%s' % option for option in options) 232 233 tryjob_cmd.append(builder) 234 235 return tryjob_cmd 236 237 238def RunTryJobs(cl_number, extra_change_lists, options, builders, chroot_path): 239 """Runs a tryjob/tryjobs. 240 241 Args: 242 cl_number: The CL created by updating the packages. 243 extra_change_lists: Any extra change lists that would run alongside the CL 244 that was created by updating the packages ('cl_number'). 245 options: Any options to be passed into the 'tryjob' command. 246 builders: All the builders to run the 'tryjob' with. 247 chroot_path: The absolute path to the chroot. 248 249 Returns: 250 A list that contains stdout contents of each tryjob, where stdout is 251 information (a hashmap) about the tryjob. The hashmap also contains stderr 252 if there was an error when running a tryjob. 253 254 Raises: 255 ValueError: Failed to submit a tryjob. 256 """ 257 258 # Contains the results of each builder. 259 tests = [] 260 261 # Run tryjobs with the change list number obtained from updating the 262 # packages and append additional changes lists and options obtained from the 263 # command line. 264 for builder in builders: 265 cmd = GetTryJobCommand(cl_number, extra_change_lists, options, builder) 266 267 out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8') 268 269 test_output = json.loads(out) 270 271 buildbucket_id = int(test_output[0]['id']) 272 273 tests.append({ 274 'launch_time': str(GetCurrentTimeInUTC()), 275 'link': 'http://ci.chromium.org/b/%s' % buildbucket_id, 276 'buildbucket_id': buildbucket_id, 277 'extra_cls': extra_change_lists, 278 'options': options, 279 'builder': [builder] 280 }) 281 282 AddLinksToCL(tests, cl_number, chroot_path) 283 284 return tests 285 286 287def StartRecipeBuilders(cl_number, extra_change_lists, options, builders, 288 chroot_path): 289 """Launch recipe builders. 290 291 Args: 292 cl_number: The CL created by updating the packages. 293 extra_change_lists: Any extra change lists that would run alongside the CL 294 that was created by updating the packages ('cl_number'). 295 options: Any options to be passed into the 'tryjob' command. 296 builders: All the builders to run the 'tryjob' with. 297 chroot_path: The absolute path to the chroot. 298 299 Returns: 300 A list that contains stdout contents of each builder, where stdout is 301 information (a hashmap) about the tryjob. The hashmap also contains stderr 302 if there was an error when running a tryjob. 303 304 Raises: 305 ValueError: Failed to start a builder. 306 """ 307 308 # Contains the results of each builder. 309 tests = [] 310 311 # Launch a builders with the change list number obtained from updating the 312 # packages and append additional changes lists and options obtained from the 313 # command line. 314 for builder in builders: 315 cmd = ['bb', 'add', '-json'] 316 317 if cl_number: 318 cmd.extend(['-cl', 'crrev.com/c/%d' % cl_number]) 319 320 if extra_change_lists: 321 for cl in extra_change_lists: 322 cmd.extend(['-cl', 'crrev.com/c/%d' % cl]) 323 324 if options: 325 cmd.extend(options) 326 327 cmd.append(builder) 328 329 out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8') 330 331 test_output = json.loads(out) 332 333 tests.append({ 334 'launch_time': test_output['createTime'], 335 'link': 'http://ci.chromium.org/b/%s' % test_output['id'], 336 'buildbucket_id': test_output['id'], 337 'extra_cls': extra_change_lists, 338 'options': options, 339 'builder': [builder] 340 }) 341 342 AddLinksToCL(tests, cl_number, chroot_path) 343 344 return tests 345 346 347# Testing with CQ 348def GetCQDependString(dependent_cls): 349 """Get CQ dependency string e.g. `Cq-Depend: chromium:MM, chromium:NN`.""" 350 351 if not dependent_cls: 352 return None 353 354 # Cq-Depend must start a new paragraph prefixed with "Cq-Depend". 355 return '\nCq-Depend: ' + ', '.join( 356 ('chromium:%s' % i) for i in dependent_cls) 357 358 359def GetCQIncludeTrybotsString(trybot): 360 """Get Cq-Include-Trybots string, for more llvm testings""" 361 362 if not trybot: 363 return None 364 365 if trybot not in VALID_CQ_TRYBOTS: 366 raise ValueError('%s is not a valid llvm trybot' % trybot) 367 368 # Cq-Include-Trybots must start a new paragraph prefixed 369 # with "Cq-Include-Trybots". 370 return '\nCq-Include-Trybots:chromeos/cq:cq-%s-orchestrator' % trybot 371 372 373def StartCQDryRun(cl, dependent_cls, chroot_path): 374 """Start CQ dry run for the changelist and dependencies.""" 375 376 gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit') 377 378 cl_list = [cl] 379 cl_list.extend(dependent_cls) 380 381 for changes in cl_list: 382 cq_dry_run_cmd = [gerrit_abs_path, 'label-cq', str(changes), '1'] 383 384 subprocess.check_output(cq_dry_run_cmd) 385 386 387def main(): 388 """Updates the packages' LLVM hash and run tests. 389 390 Raises: 391 AssertionError: The script was run inside the chroot. 392 """ 393 394 chroot.VerifyOutsideChroot() 395 396 args_output = GetCommandLineArgs() 397 398 patch_metadata_file = 'PATCHES.json' 399 400 svn_option = args_output.llvm_version 401 402 git_hash, svn_version = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption( 403 svn_option) 404 405 # There is no need to run tryjobs when all the key parameters remain unchanged 406 # from last time. 407 408 # If --last_tested is specified, check if the current run has the same 409 # arguments last time --last_tested is used. 410 if args_output.last_tested: 411 chroot_file_paths = chroot.GetChrootEbuildPaths( 412 args_output.chroot_path, update_chromeos_llvm_hash.DEFAULT_PACKAGES) 413 arg_dict = { 414 'svn_version': svn_version, 415 'ebuilds': chroot_file_paths, 416 'extra_cls': args_output.extra_change_lists, 417 } 418 if args_output.subparser_name in ('tryjobs', 'recipe'): 419 arg_dict['builders'] = args_output.builders 420 arg_dict['tryjob_options'] = args_output.options 421 if UnchangedSinceLastRun(args_output.last_tested, arg_dict): 422 print('svn version (%d) matches the last tested svn version in %s' % 423 (svn_version, args_output.last_tested)) 424 return 425 426 llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current 427 if args_output.is_llvm_next: 428 llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next 429 update_chromeos_llvm_hash.verbose = args_output.verbose 430 extra_commit_msg = None 431 if args_output.subparser_name == 'cq': 432 cq_depend_msg = GetCQDependString(args_output.extra_change_lists) 433 if cq_depend_msg: 434 extra_commit_msg = cq_depend_msg 435 cq_trybot_msg = GetCQIncludeTrybotsString(args_output.cq_trybot) 436 if cq_trybot_msg: 437 extra_commit_msg += cq_trybot_msg 438 439 change_list = update_chromeos_llvm_hash.UpdatePackages( 440 update_chromeos_llvm_hash.DEFAULT_PACKAGES, 441 llvm_variant, 442 git_hash, 443 svn_version, 444 args_output.chroot_path, 445 patch_metadata_file, 446 failure_modes.FailureModes.DISABLE_PATCHES, 447 svn_option, 448 extra_commit_msg=extra_commit_msg) 449 450 AddReviewers(change_list.cl_number, args_output.reviewers, 451 args_output.chroot_path) 452 453 print('Successfully updated packages to %d' % svn_version) 454 print('Gerrit URL: %s' % change_list.url) 455 print('Change list number: %d' % change_list.cl_number) 456 457 if args_output.subparser_name == 'tryjobs': 458 tests = RunTryJobs(change_list.cl_number, args_output.extra_change_lists, 459 args_output.options, args_output.builders, 460 args_output.chroot_path) 461 print('Tests:') 462 for test in tests: 463 print(test) 464 elif args_output.subparser_name == 'recipe': 465 tests = StartRecipeBuilders(change_list.cl_number, 466 args_output.extra_change_lists, 467 args_output.options, args_output.builders, 468 args_output.chroot_path) 469 print('Tests:') 470 for test in tests: 471 print(test) 472 473 else: 474 StartCQDryRun(change_list.cl_number, args_output.extra_change_lists, 475 args_output.chroot_path) 476 477 # If --last_tested is specified, record the arguments used 478 if args_output.last_tested: 479 with open(args_output.last_tested, 'w') as f: 480 json.dump(arg_dict, f, indent=2) 481 482 483if __name__ == '__main__': 484 main() 485