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