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