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