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