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"""Updates the status of a tryjob.""" 7*760c253cSXin Li 8*760c253cSXin Liimport argparse 9*760c253cSXin Liimport enum 10*760c253cSXin Liimport json 11*760c253cSXin Liimport os 12*760c253cSXin Liimport subprocess 13*760c253cSXin Liimport sys 14*760c253cSXin Li 15*760c253cSXin Liimport chroot 16*760c253cSXin Liimport test_helpers 17*760c253cSXin Li 18*760c253cSXin Li 19*760c253cSXin Liclass TryjobStatus(enum.Enum): 20*760c253cSXin Li """Values for the 'status' field of a tryjob.""" 21*760c253cSXin Li 22*760c253cSXin Li GOOD = "good" 23*760c253cSXin Li BAD = "bad" 24*760c253cSXin Li PENDING = "pending" 25*760c253cSXin Li SKIP = "skip" 26*760c253cSXin Li 27*760c253cSXin Li # Executes the script passed into the command line (this script's exit code 28*760c253cSXin Li # determines the 'status' value of the tryjob). 29*760c253cSXin Li CUSTOM_SCRIPT = "custom_script" 30*760c253cSXin Li 31*760c253cSXin Li 32*760c253cSXin Liclass CustomScriptStatus(enum.Enum): 33*760c253cSXin Li """Exit code values of a custom script.""" 34*760c253cSXin Li 35*760c253cSXin Li # NOTE: Not using 1 for 'bad' because the custom script can raise an 36*760c253cSXin Li # exception which would cause the exit code of the script to be 1, so the 37*760c253cSXin Li # tryjob's 'status' would be updated when there is an exception. 38*760c253cSXin Li # 39*760c253cSXin Li # Exit codes are as follows: 40*760c253cSXin Li # 0: 'good' 41*760c253cSXin Li # 124: 'bad' 42*760c253cSXin Li # 125: 'skip' 43*760c253cSXin Li GOOD = 0 44*760c253cSXin Li BAD = 124 45*760c253cSXin Li SKIP = 125 46*760c253cSXin Li 47*760c253cSXin Li 48*760c253cSXin Licustom_script_exit_value_mapping = { 49*760c253cSXin Li CustomScriptStatus.GOOD.value: TryjobStatus.GOOD.value, 50*760c253cSXin Li CustomScriptStatus.BAD.value: TryjobStatus.BAD.value, 51*760c253cSXin Li CustomScriptStatus.SKIP.value: TryjobStatus.SKIP.value, 52*760c253cSXin Li} 53*760c253cSXin Li 54*760c253cSXin Li 55*760c253cSXin Lidef GetCommandLineArgs(): 56*760c253cSXin Li """Parses the command line for the command line arguments.""" 57*760c253cSXin Li 58*760c253cSXin Li # Default absoute path to the chroot if not specified. 59*760c253cSXin Li cros_root = os.path.expanduser("~") 60*760c253cSXin Li cros_root = os.path.join(cros_root, "chromiumos") 61*760c253cSXin Li 62*760c253cSXin Li # Create parser and add optional command-line arguments. 63*760c253cSXin Li parser = argparse.ArgumentParser( 64*760c253cSXin Li description="Updates the status of a tryjob." 65*760c253cSXin Li ) 66*760c253cSXin Li 67*760c253cSXin Li # Add argument for the JSON file to use for the update of a tryjob. 68*760c253cSXin Li parser.add_argument( 69*760c253cSXin Li "--status_file", 70*760c253cSXin Li required=True, 71*760c253cSXin Li help="The absolute path to the JSON file that contains the tryjobs " 72*760c253cSXin Li "used for bisecting LLVM.", 73*760c253cSXin Li ) 74*760c253cSXin Li 75*760c253cSXin Li # Add argument that sets the 'status' field to that value. 76*760c253cSXin Li parser.add_argument( 77*760c253cSXin Li "--set_status", 78*760c253cSXin Li required=True, 79*760c253cSXin Li choices=[tryjob_status.value for tryjob_status in TryjobStatus], 80*760c253cSXin Li help='Sets the "status" field of the tryjob.', 81*760c253cSXin Li ) 82*760c253cSXin Li 83*760c253cSXin Li # Add argument that determines which revision to search for in the list of 84*760c253cSXin Li # tryjobs. 85*760c253cSXin Li parser.add_argument( 86*760c253cSXin Li "--revision", 87*760c253cSXin Li required=True, 88*760c253cSXin Li type=int, 89*760c253cSXin Li help="The revision to set its status.", 90*760c253cSXin Li ) 91*760c253cSXin Li 92*760c253cSXin Li # Add argument for the custom script to execute for the 'custom_script' 93*760c253cSXin Li # option in '--set_status'. 94*760c253cSXin Li parser.add_argument( 95*760c253cSXin Li "--custom_script", 96*760c253cSXin Li help="The absolute path to the custom script to execute (its exit code " 97*760c253cSXin Li 'should be %d for "good", %d for "bad", or %d for "skip")' 98*760c253cSXin Li % ( 99*760c253cSXin Li CustomScriptStatus.GOOD.value, 100*760c253cSXin Li CustomScriptStatus.BAD.value, 101*760c253cSXin Li CustomScriptStatus.SKIP.value, 102*760c253cSXin Li ), 103*760c253cSXin Li ) 104*760c253cSXin Li 105*760c253cSXin Li args_output = parser.parse_args() 106*760c253cSXin Li 107*760c253cSXin Li if not ( 108*760c253cSXin Li os.path.isfile( 109*760c253cSXin Li args_output.status_file 110*760c253cSXin Li and not args_output.status_file.endswith(".json") 111*760c253cSXin Li ) 112*760c253cSXin Li ): 113*760c253cSXin Li raise ValueError( 114*760c253cSXin Li 'File does not exist or does not ending in ".json" ' 115*760c253cSXin Li ": %s" % args_output.status_file 116*760c253cSXin Li ) 117*760c253cSXin Li 118*760c253cSXin Li if ( 119*760c253cSXin Li args_output.set_status == TryjobStatus.CUSTOM_SCRIPT.value 120*760c253cSXin Li and not args_output.custom_script 121*760c253cSXin Li ): 122*760c253cSXin Li raise ValueError( 123*760c253cSXin Li "Please provide the absolute path to the script to " "execute." 124*760c253cSXin Li ) 125*760c253cSXin Li 126*760c253cSXin Li return args_output 127*760c253cSXin Li 128*760c253cSXin Li 129*760c253cSXin Lidef FindTryjobIndex(revision, tryjobs_list): 130*760c253cSXin Li """Searches the list of tryjob dictionaries to find 'revision'. 131*760c253cSXin Li 132*760c253cSXin Li Uses the key 'rev' for each dictionary and compares the value against 133*760c253cSXin Li 'revision.' 134*760c253cSXin Li 135*760c253cSXin Li Args: 136*760c253cSXin Li revision: The revision to search for in the tryjobs. 137*760c253cSXin Li tryjobs_list: A list of tryjob dictionaries of the format: 138*760c253cSXin Li { 139*760c253cSXin Li 'rev' : [REVISION], 140*760c253cSXin Li 'url' : [URL_OF_CL], 141*760c253cSXin Li 'cl' : [CL_NUMBER], 142*760c253cSXin Li 'link' : [TRYJOB_LINK], 143*760c253cSXin Li 'status' : [TRYJOB_STATUS], 144*760c253cSXin Li 'buildbucket_id': [BUILDBUCKET_ID] 145*760c253cSXin Li } 146*760c253cSXin Li 147*760c253cSXin Li Returns: 148*760c253cSXin Li The index within the list or None to indicate it was not found. 149*760c253cSXin Li """ 150*760c253cSXin Li 151*760c253cSXin Li for cur_index, cur_tryjob_dict in enumerate(tryjobs_list): 152*760c253cSXin Li if cur_tryjob_dict["rev"] == revision: 153*760c253cSXin Li return cur_index 154*760c253cSXin Li 155*760c253cSXin Li return None 156*760c253cSXin Li 157*760c253cSXin Li 158*760c253cSXin Lidef GetCustomScriptResult(custom_script, status_file, tryjob_contents): 159*760c253cSXin Li """Returns the conversion of the exit code of the custom script. 160*760c253cSXin Li 161*760c253cSXin Li Args: 162*760c253cSXin Li custom_script: Absolute path to the script to be executed. 163*760c253cSXin Li status_file: Absolute path to the file that contains information about 164*760c253cSXin Li the bisection of LLVM. 165*760c253cSXin Li tryjob_contents: A dictionary of the contents of the tryjob (e.g. 166*760c253cSXin Li 'status', 'url', 'link', 'buildbucket_id', etc.). 167*760c253cSXin Li 168*760c253cSXin Li Returns: 169*760c253cSXin Li The exit code conversion to either return 'good', 'bad', or 'skip'. 170*760c253cSXin Li 171*760c253cSXin Li Raises: 172*760c253cSXin Li ValueError: The custom script failed to provide the correct exit code. 173*760c253cSXin Li """ 174*760c253cSXin Li 175*760c253cSXin Li # Create a temporary file to write the contents of the tryjob at index 176*760c253cSXin Li # 'tryjob_index' (the temporary file path will be passed into the custom 177*760c253cSXin Li # script as a command line argument). 178*760c253cSXin Li with test_helpers.CreateTemporaryJsonFile() as temp_json_file: 179*760c253cSXin Li with open(temp_json_file, "w", encoding="utf-8") as tryjob_file: 180*760c253cSXin Li json.dump( 181*760c253cSXin Li tryjob_contents, tryjob_file, indent=4, separators=(",", ": ") 182*760c253cSXin Li ) 183*760c253cSXin Li 184*760c253cSXin Li exec_script_cmd = [custom_script, temp_json_file] 185*760c253cSXin Li 186*760c253cSXin Li # Execute the custom script to get the exit code. 187*760c253cSXin Li with subprocess.Popen( 188*760c253cSXin Li exec_script_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE 189*760c253cSXin Li ) as exec_script_cmd_obj: 190*760c253cSXin Li _, stderr = exec_script_cmd_obj.communicate() 191*760c253cSXin Li 192*760c253cSXin Li # Invalid exit code by the custom script. 193*760c253cSXin Li if ( 194*760c253cSXin Li exec_script_cmd_obj.returncode 195*760c253cSXin Li not in custom_script_exit_value_mapping 196*760c253cSXin Li ): 197*760c253cSXin Li # Save the .JSON file to the directory of 'status_file'. 198*760c253cSXin Li name_of_json_file = os.path.join( 199*760c253cSXin Li os.path.dirname(status_file), os.path.basename(temp_json_file) 200*760c253cSXin Li ) 201*760c253cSXin Li 202*760c253cSXin Li os.rename(temp_json_file, name_of_json_file) 203*760c253cSXin Li 204*760c253cSXin Li raise ValueError( 205*760c253cSXin Li "Custom script %s exit code %d did not match " 206*760c253cSXin Li 'any of the expected exit codes: %d for "good", %d ' 207*760c253cSXin Li 'for "bad", or %d for "skip".\nPlease check %s for information ' 208*760c253cSXin Li "about the tryjob: %s" 209*760c253cSXin Li % ( 210*760c253cSXin Li custom_script, 211*760c253cSXin Li exec_script_cmd_obj.returncode, 212*760c253cSXin Li CustomScriptStatus.GOOD.value, 213*760c253cSXin Li CustomScriptStatus.BAD.value, 214*760c253cSXin Li CustomScriptStatus.SKIP.value, 215*760c253cSXin Li name_of_json_file, 216*760c253cSXin Li stderr, 217*760c253cSXin Li ) 218*760c253cSXin Li ) 219*760c253cSXin Li 220*760c253cSXin Li return custom_script_exit_value_mapping[exec_script_cmd_obj.returncode] 221*760c253cSXin Li 222*760c253cSXin Li 223*760c253cSXin Lidef UpdateTryjobStatus(revision, set_status, status_file, custom_script): 224*760c253cSXin Li """Updates a tryjob's 'status' field based off of 'set_status'. 225*760c253cSXin Li 226*760c253cSXin Li Args: 227*760c253cSXin Li revision: The revision associated with the tryjob. 228*760c253cSXin Li set_status: What to update the 'status' field to. 229*760c253cSXin Li Ex: TryjobStatus.Good, TryjobStatus.BAD, TryjobStatus.PENDING, or 230*760c253cSXin Li TryjobStatus. 231*760c253cSXin Li status_file: The .JSON file that contains the tryjobs. 232*760c253cSXin Li custom_script: The absolute path to a script that will be executed 233*760c253cSXin Li which will determine the 'status' value of the tryjob. 234*760c253cSXin Li """ 235*760c253cSXin Li 236*760c253cSXin Li # Format of 'bisect_contents': 237*760c253cSXin Li # { 238*760c253cSXin Li # 'start': [START_REVISION_OF_BISECTION] 239*760c253cSXin Li # 'end': [END_REVISION_OF_BISECTION] 240*760c253cSXin Li # 'jobs' : [ 241*760c253cSXin Li # {[TRYJOB_INFORMATION]}, 242*760c253cSXin Li # {[TRYJOB_INFORMATION]}, 243*760c253cSXin Li # ..., 244*760c253cSXin Li # {[TRYJOB_INFORMATION]} 245*760c253cSXin Li # ] 246*760c253cSXin Li # } 247*760c253cSXin Li with open(status_file, encoding="utf-8") as tryjobs: 248*760c253cSXin Li bisect_contents = json.load(tryjobs) 249*760c253cSXin Li 250*760c253cSXin Li if not bisect_contents["jobs"]: 251*760c253cSXin Li sys.exit("No tryjobs in %s" % status_file) 252*760c253cSXin Li 253*760c253cSXin Li tryjob_index = FindTryjobIndex(revision, bisect_contents["jobs"]) 254*760c253cSXin Li 255*760c253cSXin Li # 'FindTryjobIndex()' returns None if the revision was not found. 256*760c253cSXin Li if tryjob_index is None: 257*760c253cSXin Li raise ValueError( 258*760c253cSXin Li "Unable to find tryjob for %d in %s" % (revision, status_file) 259*760c253cSXin Li ) 260*760c253cSXin Li 261*760c253cSXin Li # Set 'status' depending on 'set_status' for the tryjob. 262*760c253cSXin Li if set_status == TryjobStatus.GOOD: 263*760c253cSXin Li bisect_contents["jobs"][tryjob_index][ 264*760c253cSXin Li "status" 265*760c253cSXin Li ] = TryjobStatus.GOOD.value 266*760c253cSXin Li elif set_status == TryjobStatus.BAD: 267*760c253cSXin Li bisect_contents["jobs"][tryjob_index]["status"] = TryjobStatus.BAD.value 268*760c253cSXin Li elif set_status == TryjobStatus.PENDING: 269*760c253cSXin Li bisect_contents["jobs"][tryjob_index][ 270*760c253cSXin Li "status" 271*760c253cSXin Li ] = TryjobStatus.PENDING.value 272*760c253cSXin Li elif set_status == TryjobStatus.SKIP: 273*760c253cSXin Li bisect_contents["jobs"][tryjob_index][ 274*760c253cSXin Li "status" 275*760c253cSXin Li ] = TryjobStatus.SKIP.value 276*760c253cSXin Li elif set_status == TryjobStatus.CUSTOM_SCRIPT: 277*760c253cSXin Li bisect_contents["jobs"][tryjob_index]["status"] = GetCustomScriptResult( 278*760c253cSXin Li custom_script, status_file, bisect_contents["jobs"][tryjob_index] 279*760c253cSXin Li ) 280*760c253cSXin Li else: 281*760c253cSXin Li raise ValueError( 282*760c253cSXin Li 'Invalid "set_status" option provided: %s' % set_status 283*760c253cSXin Li ) 284*760c253cSXin Li 285*760c253cSXin Li with open(status_file, "w", encoding="utf-8") as update_tryjobs: 286*760c253cSXin Li json.dump( 287*760c253cSXin Li bisect_contents, update_tryjobs, indent=4, separators=(",", ": ") 288*760c253cSXin Li ) 289*760c253cSXin Li 290*760c253cSXin Li 291*760c253cSXin Lidef main(): 292*760c253cSXin Li """Updates the status of a tryjob.""" 293*760c253cSXin Li 294*760c253cSXin Li chroot.VerifyOutsideChroot() 295*760c253cSXin Li 296*760c253cSXin Li args_output = GetCommandLineArgs() 297*760c253cSXin Li 298*760c253cSXin Li UpdateTryjobStatus( 299*760c253cSXin Li args_output.revision, 300*760c253cSXin Li TryjobStatus(args_output.set_status), 301*760c253cSXin Li args_output.status_file, 302*760c253cSXin Li args_output.custom_script, 303*760c253cSXin Li ) 304*760c253cSXin Li 305*760c253cSXin Li 306*760c253cSXin Liif __name__ == "__main__": 307*760c253cSXin Li main() 308