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