| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # Copyright 2019 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Updates the status of a tryjob.""" |
| |
| from __future__ import print_function |
| |
| import argparse |
| import enum |
| import json |
| import os |
| import subprocess |
| import sys |
| |
| import chroot |
| from subprocess_helpers import ChrootRunCommand |
| from test_helpers import CreateTemporaryJsonFile |
| |
| |
| class TryjobStatus(enum.Enum): |
| """Values for the 'status' field of a tryjob.""" |
| |
| GOOD = 'good' |
| BAD = 'bad' |
| PENDING = 'pending' |
| SKIP = 'skip' |
| |
| # Executes the script passed into the command line (this script's exit code |
| # determines the 'status' value of the tryjob). |
| CUSTOM_SCRIPT = 'custom_script' |
| |
| # Uses the result returned by 'cros buildresult'. |
| AUTO = 'auto' |
| |
| |
| class BuilderStatus(enum.Enum): |
| """Actual values given via 'cros buildresult'.""" |
| |
| PASS = 'pass' |
| FAIL = 'fail' |
| RUNNING = 'running' |
| |
| |
| class CustomScriptStatus(enum.Enum): |
| """Exit code values of a custom script.""" |
| |
| # NOTE: Not using 1 for 'bad' because the custom script can raise an |
| # exception which would cause the exit code of the script to be 1, so the |
| # tryjob's 'status' would be updated when there is an exception. |
| # |
| # Exit codes are as follows: |
| # 0: 'good' |
| # 124: 'bad' |
| # 125: 'skip' |
| GOOD = 0 |
| BAD = 124 |
| SKIP = 125 |
| |
| |
| custom_script_exit_value_mapping = { |
| CustomScriptStatus.GOOD.value: TryjobStatus.GOOD.value, |
| CustomScriptStatus.BAD.value: TryjobStatus.BAD.value, |
| CustomScriptStatus.SKIP.value: TryjobStatus.SKIP.value |
| } |
| |
| builder_status_mapping = { |
| BuilderStatus.PASS.value: TryjobStatus.GOOD.value, |
| BuilderStatus.FAIL.value: TryjobStatus.BAD.value, |
| BuilderStatus.RUNNING.value: TryjobStatus.PENDING.value |
| } |
| |
| |
| def GetCommandLineArgs(): |
| """Parses the command line for the command line arguments.""" |
| |
| # Default absoute path to the chroot if not specified. |
| cros_root = os.path.expanduser('~') |
| cros_root = os.path.join(cros_root, 'chromiumos') |
| |
| # Create parser and add optional command-line arguments. |
| parser = argparse.ArgumentParser( |
| description='Updates the status of a tryjob.') |
| |
| # Add argument for the JSON file to use for the update of a tryjob. |
| parser.add_argument( |
| '--status_file', |
| required=True, |
| help='The absolute path to the JSON file that contains the tryjobs used ' |
| 'for bisecting LLVM.') |
| |
| # Add argument that sets the 'status' field to that value. |
| parser.add_argument( |
| '--set_status', |
| required=True, |
| choices=[tryjob_status.value for tryjob_status in TryjobStatus], |
| help='Sets the "status" field of the tryjob.') |
| |
| # Add argument that determines which revision to search for in the list of |
| # tryjobs. |
| parser.add_argument( |
| '--revision', |
| required=True, |
| type=int, |
| help='The revision to set its status.') |
| |
| # Add argument for a specific chroot path. |
| parser.add_argument( |
| '--chroot_path', |
| default=cros_root, |
| help='the path to the chroot (default: %(default)s)') |
| |
| # Add argument for the custom script to execute for the 'custom_script' |
| # option in '--set_status'. |
| parser.add_argument( |
| '--custom_script', |
| help='The absolute path to the custom script to execute (its exit code ' |
| 'should be %d for "good", %d for "bad", or %d for "skip")' % |
| (CustomScriptStatus.GOOD.value, CustomScriptStatus.BAD.value, |
| CustomScriptStatus.SKIP.value)) |
| |
| args_output = parser.parse_args() |
| |
| if not os.path.isfile(args_output.status_file) or \ |
| not args_output.status_file.endswith('.json'): |
| raise ValueError('File does not exist or does not ending in ".json" ' |
| ': %s' % args_output.status_file) |
| |
| if args_output.set_status == TryjobStatus.CUSTOM_SCRIPT.value and \ |
| not args_output.custom_script: |
| raise ValueError('Please provide the absolute path to the script to ' |
| 'execute.') |
| |
| return args_output |
| |
| |
| def FindTryjobIndex(revision, tryjobs_list): |
| """Searches the list of tryjob dictionaries to find 'revision'. |
| |
| Uses the key 'rev' for each dictionary and compares the value against |
| 'revision.' |
| |
| Args: |
| revision: The revision to search for in the tryjobs. |
| tryjobs_list: A list of tryjob dictionaries of the format: |
| { |
| 'rev' : [REVISION], |
| 'url' : [URL_OF_CL], |
| 'cl' : [CL_NUMBER], |
| 'link' : [TRYJOB_LINK], |
| 'status' : [TRYJOB_STATUS], |
| 'buildbucket_id': [BUILDBUCKET_ID] |
| } |
| |
| Returns: |
| The index within the list or None to indicate it was not found. |
| """ |
| |
| for cur_index, cur_tryjob_dict in enumerate(tryjobs_list): |
| if cur_tryjob_dict['rev'] == revision: |
| return cur_index |
| |
| return None |
| |
| |
| def GetStatusFromCrosBuildResult(chroot_path, buildbucket_id): |
| """Retrieves the 'status' using 'cros buildresult'.""" |
| |
| get_buildbucket_id_cmd = [ |
| 'cros', 'buildresult', '--buildbucket-id', |
| str(buildbucket_id), '--report', 'json' |
| ] |
| |
| tryjob_json = ChrootRunCommand(chroot_path, get_buildbucket_id_cmd) |
| |
| tryjob_contents = json.loads(tryjob_json) |
| |
| return str(tryjob_contents['%d' % buildbucket_id]['status']) |
| |
| |
| def GetAutoResult(chroot_path, buildbucket_id): |
| """Returns the conversion of the result of 'cros buildresult'.""" |
| |
| # Calls 'cros buildresult' to get the status of the tryjob. |
| build_result = GetStatusFromCrosBuildResult(chroot_path, buildbucket_id) |
| |
| # The string returned by 'cros buildresult' might not be in the mapping. |
| if build_result not in builder_status_mapping: |
| raise ValueError( |
| '"cros buildresult" return value is invalid: %s' % build_result) |
| |
| return builder_status_mapping[build_result] |
| |
| |
| def GetCustomScriptResult(custom_script, status_file, tryjob_contents): |
| """Returns the conversion of the exit code of the custom script. |
| |
| Args: |
| custom_script: Absolute path to the script to be executed. |
| status_file: Absolute path to the file that contains information about the |
| bisection of LLVM. |
| tryjob_contents: A dictionary of the contents of the tryjob (e.g. 'status', |
| 'url', 'link', 'buildbucket_id', etc.). |
| |
| Returns: |
| The exit code conversion to either return 'good', 'bad', or 'skip'. |
| |
| Raises: |
| ValueError: The custom script failed to provide the correct exit code. |
| """ |
| |
| # Create a temporary file to write the contents of the tryjob at index |
| # 'tryjob_index' (the temporary file path will be passed into the custom |
| # script as a command line argument). |
| with CreateTemporaryJsonFile() as temp_json_file: |
| with open(temp_json_file, 'w') as tryjob_file: |
| json.dump(tryjob_contents, tryjob_file, indent=4, separators=(',', ': ')) |
| |
| exec_script_cmd = [custom_script, temp_json_file] |
| |
| # Execute the custom script to get the exit code. |
| exec_script_cmd_obj = subprocess.Popen( |
| exec_script_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| _, stderr = exec_script_cmd_obj.communicate() |
| |
| # Invalid exit code by the custom script. |
| if exec_script_cmd_obj.returncode not in custom_script_exit_value_mapping: |
| # Save the .JSON file to the directory of 'status_file'. |
| name_of_json_file = os.path.join( |
| os.path.dirname(status_file), os.path.basename(temp_json_file)) |
| |
| os.rename(temp_json_file, name_of_json_file) |
| |
| raise ValueError( |
| 'Custom script %s exit code %d did not match ' |
| 'any of the expected exit codes: %d for "good", %d ' |
| 'for "bad", or %d for "skip".\nPlease check %s for information ' |
| 'about the tryjob: %s' % |
| (custom_script, exec_script_cmd_obj.returncode, |
| CustomScriptStatus.GOOD.value, CustomScriptStatus.BAD.value, |
| CustomScriptStatus.SKIP.value, name_of_json_file, stderr)) |
| |
| return custom_script_exit_value_mapping[exec_script_cmd_obj.returncode] |
| |
| |
| def UpdateTryjobStatus(revision, set_status, status_file, chroot_path, |
| custom_script): |
| """Updates a tryjob's 'status' field based off of 'set_status'. |
| |
| Args: |
| revision: The revision associated with the tryjob. |
| set_status: What to update the 'status' field to. |
| Ex: TryjobStatus.Good, TryjobStatus.BAD, TryjobStatus.PENDING, or |
| TryjobStatus.AUTO where TryjobStatus.AUTO uses the result of |
| 'cros buildresult'. |
| status_file: The .JSON file that contains the tryjobs. |
| chroot_path: The absolute path to the chroot (used by 'cros buildresult'). |
| custom_script: The absolute path to a script that will be executed which |
| will determine the 'status' value of the tryjob. |
| """ |
| |
| # Format of 'bisect_contents': |
| # { |
| # 'start': [START_REVISION_OF_BISECTION] |
| # 'end': [END_REVISION_OF_BISECTION] |
| # 'jobs' : [ |
| # {[TRYJOB_INFORMATION]}, |
| # {[TRYJOB_INFORMATION]}, |
| # ..., |
| # {[TRYJOB_INFORMATION]} |
| # ] |
| # } |
| with open(status_file) as tryjobs: |
| bisect_contents = json.load(tryjobs) |
| |
| if not bisect_contents['jobs']: |
| sys.exit('No tryjobs in %s' % status_file) |
| |
| tryjob_index = FindTryjobIndex(revision, bisect_contents['jobs']) |
| |
| # 'FindTryjobIndex()' returns None if the revision was not found. |
| if tryjob_index is None: |
| raise ValueError( |
| 'Unable to find tryjob for %d in %s' % (revision, status_file)) |
| |
| # Set 'status' depending on 'set_status' for the tryjob. |
| if set_status == TryjobStatus.GOOD: |
| bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.GOOD.value |
| elif set_status == TryjobStatus.BAD: |
| bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.BAD.value |
| elif set_status == TryjobStatus.PENDING: |
| bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.PENDING.value |
| elif set_status == TryjobStatus.AUTO: |
| bisect_contents['jobs'][tryjob_index]['status'] = GetAutoResult( |
| chroot_path, bisect_contents['jobs'][tryjob_index]['buildbucket_id']) |
| elif set_status == TryjobStatus.SKIP: |
| bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.SKIP.value |
| elif set_status == TryjobStatus.CUSTOM_SCRIPT: |
| bisect_contents['jobs'][tryjob_index]['status'] = GetCustomScriptResult( |
| custom_script, status_file, bisect_contents['jobs'][tryjob_index]) |
| else: |
| raise ValueError('Invalid "set_status" option provided: %s' % set_status) |
| |
| with open(status_file, 'w') as update_tryjobs: |
| json.dump(bisect_contents, update_tryjobs, indent=4, separators=(',', ': ')) |
| |
| |
| def main(): |
| """Updates the status of a tryjob.""" |
| |
| chroot.VerifyOutsideChroot() |
| |
| args_output = GetCommandLineArgs() |
| |
| UpdateTryjobStatus(args_output.revision, TryjobStatus(args_output.set_status), |
| args_output.status_file, args_output.chroot_path, |
| args_output.custom_script) |
| |
| |
| if __name__ == '__main__': |
| main() |