1#!/usr/bin/env python3 2# Copyright 2019 The ChromiumOS Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Modifies a tryjob based off of arguments.""" 7 8import argparse 9import enum 10import json 11import os 12from pathlib import Path 13import sys 14from typing import Dict, Iterable, List, Union 15 16import chroot 17import failure_modes 18import get_llvm_hash 19import git 20import update_chromeos_llvm_hash 21import update_packages_and_run_tests 22import update_tryjob_status 23 24 25class ModifyTryjob(enum.Enum): 26 """Options to modify a tryjob.""" 27 28 REMOVE = "remove" 29 RELAUNCH = "relaunch" 30 ADD = "add" 31 32 33def GetCommandLineArgs() -> argparse.Namespace: 34 """Parses the command line for the command line arguments.""" 35 36 # Default path to the chroot if a path is not specified. 37 cros_root = os.path.expanduser("~") 38 cros_root = os.path.join(cros_root, "chromiumos") 39 40 # Create parser and add optional command-line arguments. 41 parser = argparse.ArgumentParser( 42 description="Removes, relaunches, or adds a tryjob." 43 ) 44 45 # Add argument for the JSON file to use for the update of a tryjob. 46 parser.add_argument( 47 "--status_file", 48 required=True, 49 help="The absolute path to the JSON file that contains the tryjobs " 50 "used for bisecting LLVM.", 51 ) 52 53 # Add argument that determines what action to take on the revision 54 # specified. 55 parser.add_argument( 56 "--modify_tryjob", 57 required=True, 58 choices=[modify_tryjob.value for modify_tryjob in ModifyTryjob], 59 help="What action to perform on the tryjob.", 60 ) 61 62 # Add argument that determines which revision to search for in the list of 63 # tryjobs. 64 parser.add_argument( 65 "--revision", 66 required=True, 67 type=int, 68 help="The revision to either remove or relaunch.", 69 ) 70 71 # Add argument for other change lists that want to run alongside the 72 # tryjob. 73 parser.add_argument( 74 "--extra_change_lists", 75 type=int, 76 nargs="+", 77 help="change lists that would like to be run alongside the change list " 78 "of updating the packages", 79 ) 80 81 # Add argument for custom options for the tryjob. 82 parser.add_argument( 83 "--options", 84 required=False, 85 nargs="+", 86 help="options to use for the tryjob testing", 87 ) 88 89 # Add argument for the builder to use for the tryjob. 90 parser.add_argument( 91 "--builder", help="builder to use for the tryjob testing" 92 ) 93 94 # Add argument for a specific chroot path. 95 parser.add_argument( 96 "--chromeos_path", 97 default=cros_root, 98 help="the path to the chroot (default: %(default)s)", 99 ) 100 101 args_output = parser.parse_args() 102 103 if not os.path.isfile( 104 args_output.status_file 105 ) or not args_output.status_file.endswith(".json"): 106 raise ValueError( 107 'File does not exist or does not ending in ".json" ' 108 ": %s" % args_output.status_file 109 ) 110 111 if ( 112 args_output.modify_tryjob == ModifyTryjob.ADD.value 113 and not args_output.builder 114 ): 115 raise ValueError("A builder is required for adding a tryjob.") 116 elif ( 117 args_output.modify_tryjob != ModifyTryjob.ADD.value 118 and args_output.builder 119 ): 120 raise ValueError( 121 "Specifying a builder is only available when adding a " "tryjob." 122 ) 123 124 return args_output 125 126 127def GetCLAfterUpdatingPackages( 128 packages: Iterable[str], 129 git_hash: str, 130 svn_version: int, 131 chromeos_path: Union[Path, str], 132 svn_option: Union[int, str], 133) -> git.CommitContents: 134 """Updates the packages' LLVM_NEXT.""" 135 136 change_list = update_chromeos_llvm_hash.UpdatePackages( 137 packages=packages, 138 manifest_packages=[], 139 llvm_variant=update_chromeos_llvm_hash.LLVMVariant.next, 140 git_hash=git_hash, 141 svn_version=svn_version, 142 chroot_opts=update_chromeos_llvm_hash.ChrootOpts(Path(chromeos_path)), 143 mode=failure_modes.FailureModes.DISABLE_PATCHES, 144 git_hash_source=svn_option, 145 extra_commit_msg_lines=None, 146 ) 147 148 # We are calling UpdatePackages with upload_changes=True, in 149 # which case it should always return a git.CommitContents value. 150 assert change_list is not None 151 print("\nSuccessfully updated packages to %d" % svn_version) 152 print("Gerrit URL: %s" % change_list.url) 153 print("Change list number: %d" % change_list.cl_number) 154 155 return change_list 156 157 158def CreateNewTryjobEntryForBisection( 159 cl: int, 160 extra_cls: List[int], 161 options: List[str], 162 builder: str, 163 chromeos_path: Union[Path, str], 164 cl_url: str, 165 revision, 166) -> Dict: 167 """Submits a tryjob and adds additional information.""" 168 169 # Get the tryjob results after submitting the tryjob. 170 # Format of 'tryjob_results': 171 # [ 172 # { 173 # 'link' : [TRYJOB_LINK], 174 # 'buildbucket_id' : [BUILDBUCKET_ID], 175 # 'extra_cls' : [EXTRA_CLS_LIST], 176 # 'options' : [EXTRA_OPTIONS_LIST], 177 # 'builder' : [BUILDER_AS_A_LIST] 178 # } 179 # ] 180 tryjob_results = update_packages_and_run_tests.RunTryJobs( 181 cl, extra_cls, options, [builder], chromeos_path 182 ) 183 print("\nTryjob:") 184 print(tryjob_results[0]) 185 186 # Add necessary information about the tryjob. 187 tryjob_results[0]["url"] = cl_url 188 tryjob_results[0]["rev"] = revision 189 tryjob_results[0][ 190 "status" 191 ] = update_tryjob_status.TryjobStatus.PENDING.value 192 tryjob_results[0]["cl"] = cl 193 194 return tryjob_results[0] 195 196 197def AddTryjob( 198 packages: Iterable[str], 199 git_hash: str, 200 revision: int, 201 chromeos_path: Union[Path, str], 202 extra_cls: List[int], 203 options: List[str], 204 builder: str, 205 svn_option: Union[int, str], 206): 207 """Submits a tryjob.""" 208 209 change_list = GetCLAfterUpdatingPackages( 210 packages, 211 git_hash, 212 revision, 213 chromeos_path, 214 svn_option, 215 ) 216 217 tryjob_dict = CreateNewTryjobEntryForBisection( 218 change_list.cl_number, 219 extra_cls, 220 options, 221 builder, 222 chromeos_path, 223 change_list.url, 224 revision, 225 ) 226 227 return tryjob_dict 228 229 230def PerformTryjobModification( 231 revision: int, 232 modify_tryjob: ModifyTryjob, 233 status_file: Union[Path, str], 234 extra_cls: List[int], 235 options: List[str], 236 builder: str, 237 chromeos_path: Union[Path, str], 238) -> None: 239 """Removes, relaunches, or adds a tryjob. 240 241 Args: 242 revision: The revision associated with the tryjob. 243 modify_tryjob: What action to take on the tryjob. 244 Ex: ModifyTryjob.REMOVE, ModifyTryjob.RELAUNCH, ModifyTryjob.ADD 245 status_file: The .JSON file that contains the tryjobs. 246 extra_cls: Extra change lists to be run alongside tryjob 247 options: Extra options to pass into 'cros tryjob'. 248 builder: The builder to use for 'cros tryjob'. 249 chromeos_path: The absolute path to the chromeos checkout. 250 """ 251 252 # Format of 'bisect_contents': 253 # { 254 # 'start': [START_REVISION_OF_BISECTION] 255 # 'end': [END_REVISION_OF_BISECTION] 256 # 'jobs' : [ 257 # {[TRYJOB_INFORMATION]}, 258 # {[TRYJOB_INFORMATION]}, 259 # ..., 260 # {[TRYJOB_INFORMATION]} 261 # ] 262 # } 263 with open(status_file, encoding="utf-8") as tryjobs: 264 bisect_contents = json.load(tryjobs) 265 266 if not bisect_contents["jobs"] and modify_tryjob != ModifyTryjob.ADD: 267 sys.exit("No tryjobs in %s" % status_file) 268 269 tryjob_index = update_tryjob_status.FindTryjobIndex( 270 revision, bisect_contents["jobs"] 271 ) 272 273 # 'FindTryjobIndex()' returns None if the tryjob was not found. 274 if tryjob_index is None and modify_tryjob != ModifyTryjob.ADD: 275 raise ValueError( 276 "Unable to find tryjob for %d in %s" % (revision, status_file) 277 ) 278 279 # Determine the action to take based off of 'modify_tryjob'. 280 if modify_tryjob == ModifyTryjob.REMOVE: 281 del bisect_contents["jobs"][tryjob_index] 282 283 print("Successfully deleted the tryjob of revision %d" % revision) 284 elif modify_tryjob == ModifyTryjob.RELAUNCH: 285 # Need to update the tryjob link and buildbucket ID. 286 tryjob_results = update_packages_and_run_tests.RunTryJobs( 287 bisect_contents["jobs"][tryjob_index]["cl"], 288 bisect_contents["jobs"][tryjob_index]["extra_cls"], 289 bisect_contents["jobs"][tryjob_index]["options"], 290 bisect_contents["jobs"][tryjob_index]["builder"], 291 chromeos_path, 292 ) 293 294 bisect_contents["jobs"][tryjob_index][ 295 "status" 296 ] = update_tryjob_status.TryjobStatus.PENDING.value 297 bisect_contents["jobs"][tryjob_index]["link"] = tryjob_results[0][ 298 "link" 299 ] 300 bisect_contents["jobs"][tryjob_index][ 301 "buildbucket_id" 302 ] = tryjob_results[0]["buildbucket_id"] 303 304 print( 305 "Successfully relaunched the tryjob for revision %d and updated " 306 "the tryjob link to %s" % (revision, tryjob_results[0]["link"]) 307 ) 308 elif modify_tryjob == ModifyTryjob.ADD: 309 # Tryjob exists already. 310 if tryjob_index is not None: 311 raise ValueError( 312 "Tryjob already exists (index is %d) in %s." 313 % (tryjob_index, status_file) 314 ) 315 316 # Make sure the revision is within the bounds of the start and end of 317 # the bisection. 318 elif bisect_contents["start"] < revision < bisect_contents["end"]: 319 ( 320 git_hash, 321 revision, 322 ) = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption(revision) 323 324 tryjob_dict = AddTryjob( 325 update_chromeos_llvm_hash.DEFAULT_PACKAGES, 326 git_hash, 327 revision, 328 chromeos_path, 329 extra_cls, 330 options, 331 builder, 332 revision, 333 ) 334 335 bisect_contents["jobs"].append(tryjob_dict) 336 337 print("Successfully added tryjob of revision %d" % revision) 338 else: 339 raise ValueError("Failed to add tryjob to %s" % status_file) 340 else: 341 raise ValueError( 342 'Invalid "modify_tryjob" option provided: %s' % modify_tryjob 343 ) 344 345 with open(status_file, "w", encoding="utf-8") as update_tryjobs: 346 json.dump( 347 bisect_contents, update_tryjobs, indent=4, separators=(",", ": ") 348 ) 349 350 351def main() -> None: 352 """Removes, relaunches, or adds a tryjob.""" 353 354 chroot.VerifyOutsideChroot() 355 356 args_output = GetCommandLineArgs() 357 358 chroot.VerifyChromeOSRoot(args_output.chromeos_path) 359 360 PerformTryjobModification( 361 args_output.revision, 362 ModifyTryjob(args_output.modify_tryjob), 363 args_output.status_file, 364 args_output.extra_change_lists, 365 args_output.options, 366 args_output.builder, 367 args_output.chromeos_path, 368 ) 369 370 371if __name__ == "__main__": 372 main() 373