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"""Performs bisection on LLVM based off a .JSON file.""" 7 8import enum 9import json 10import os 11import subprocess 12import sys 13import time 14import traceback 15 16import chroot 17import llvm_bisection 18import update_tryjob_status 19 20 21# Used to re-try for 'llvm_bisection.py' to attempt to launch more tryjobs. 22BISECTION_RETRY_TIME_SECS = 10 * 60 23 24# Wait time to then poll each tryjob whose 'status' value is 'pending'. 25POLL_RETRY_TIME_SECS = 30 * 60 26 27# The number of attempts for 'llvm_bisection.py' to launch more tryjobs. 28# 29# It is reset (break out of the `for` loop/ exit the program) if successfully 30# launched more tryjobs or bisection is finished (no more revisions between 31# start and end of the bisection). 32BISECTION_ATTEMPTS = 3 33 34# The limit for updating all tryjobs whose 'status' is 'pending'. 35# 36# If the time that has passed for polling exceeds this value, then the program 37# will exit with the appropriate exit code. 38POLLING_LIMIT_SECS = 18 * 60 * 60 39 40 41class BuilderStatus(enum.Enum): 42 """Actual values given via 'cros buildresult'.""" 43 44 PASS = "pass" 45 FAIL = "fail" 46 RUNNING = "running" 47 48 49# Writing a dict with `.value`s spelled out makes `black`'s style conflict with 50# `cros lint`'s diagnostics. 51builder_status_mapping = { 52 a.value: b.value 53 for a, b in ( 54 (BuilderStatus.PASS, update_tryjob_status.TryjobStatus.GOOD), 55 (BuilderStatus.FAIL, update_tryjob_status.TryjobStatus.BAD), 56 (BuilderStatus.RUNNING, update_tryjob_status.TryjobStatus.PENDING), 57 ) 58} 59 60 61def GetBuildResult(chromeos_path, buildbucket_id): 62 """Returns the conversion of the result of 'cros buildresult'.""" 63 64 # Calls 'cros buildresult' to get the status of the tryjob. 65 try: 66 tryjob_json = subprocess.check_output( 67 [ 68 "cros", 69 "buildresult", 70 "--buildbucket-id", 71 str(buildbucket_id), 72 "--report", 73 "json", 74 ], 75 cwd=chromeos_path, 76 stderr=subprocess.STDOUT, 77 encoding="utf-8", 78 ) 79 except subprocess.CalledProcessError as err: 80 if "No build found. Perhaps not started" not in err.output: 81 raise 82 return None 83 84 tryjob_content = json.loads(tryjob_json) 85 86 build_result = str(tryjob_content["%d" % buildbucket_id]["status"]) 87 88 # The string returned by 'cros buildresult' might not be in the mapping. 89 if build_result not in builder_status_mapping: 90 raise ValueError( 91 '"cros buildresult" return value is invalid: %s' % build_result 92 ) 93 94 return builder_status_mapping[build_result] 95 96 97def main(): 98 """Bisects LLVM using the result of `cros buildresult` of each tryjob. 99 100 Raises: 101 AssertionError: The script was run inside the chroot. 102 """ 103 104 chroot.VerifyOutsideChroot() 105 106 args_output = llvm_bisection.GetCommandLineArgs() 107 108 chroot.VerifyChromeOSRoot(args_output.chromeos_path) 109 110 if os.path.isfile(args_output.last_tested): 111 print("Resuming bisection for %s" % args_output.last_tested) 112 else: 113 print("Starting a new bisection for %s" % args_output.last_tested) 114 115 while True: 116 # Update the status of existing tryjobs 117 if os.path.isfile(args_output.last_tested): 118 update_start_time = time.time() 119 with open(args_output.last_tested, encoding="utf-8") as json_file: 120 json_dict = json.load(json_file) 121 while True: 122 print( 123 '\nAttempting to update all tryjobs whose "status" is ' 124 '"pending":' 125 ) 126 print("-" * 40) 127 128 completed = True 129 for tryjob in json_dict["jobs"]: 130 if ( 131 tryjob["status"] 132 == update_tryjob_status.TryjobStatus.PENDING.value 133 ): 134 status = GetBuildResult( 135 args_output.chromeos_path, tryjob["buildbucket_id"] 136 ) 137 if status: 138 tryjob["status"] = status 139 else: 140 completed = False 141 142 print("-" * 40) 143 144 # Proceed to the next step if all the existing tryjobs have 145 # completed. 146 if completed: 147 break 148 149 delta_time = time.time() - update_start_time 150 151 if delta_time > POLLING_LIMIT_SECS: 152 # Something is wrong with updating the tryjobs's 'status' 153 # via `cros buildresult` (e.g. network issue, etc.). 154 sys.exit("Failed to update pending tryjobs.") 155 156 print("-" * 40) 157 print("Sleeping for %d minutes." % (POLL_RETRY_TIME_SECS // 60)) 158 time.sleep(POLL_RETRY_TIME_SECS) 159 160 # There should always be update from the tryjobs launched in the 161 # last iteration. 162 temp_filename = "%s.new" % args_output.last_tested 163 with open(temp_filename, "w", encoding="utf-8") as temp_file: 164 json.dump( 165 json_dict, temp_file, indent=4, separators=(",", ": ") 166 ) 167 os.rename(temp_filename, args_output.last_tested) 168 169 # Launch more tryjobs. 170 bisection_complete = ( 171 llvm_bisection.BisectionExitStatus.BISECTION_COMPLETE.value 172 ) 173 for cur_try in range(1, BISECTION_ATTEMPTS + 1): 174 try: 175 print("\nAttempting to launch more tryjobs if possible:") 176 print("-" * 40) 177 178 bisection_ret = llvm_bisection.main(args_output) 179 180 print("-" * 40) 181 182 # Stop if the bisection has completed. 183 if bisection_ret == bisection_complete: 184 sys.exit(0) 185 186 # Successfully launched more tryjobs. 187 break 188 except Exception: 189 traceback.print_exc() 190 191 print("-" * 40) 192 193 # Exceeded the number of times to launch more tryjobs. 194 if cur_try == BISECTION_ATTEMPTS: 195 sys.exit("Unable to continue bisection.") 196 197 num_retries_left = BISECTION_ATTEMPTS - cur_try 198 199 print( 200 "Retries left to continue bisection %d." % num_retries_left 201 ) 202 203 print( 204 "Sleeping for %d minutes." 205 % (BISECTION_RETRY_TIME_SECS // 60) 206 ) 207 time.sleep(BISECTION_RETRY_TIME_SECS) 208 209 210if __name__ == "__main__": 211 main() 212