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