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