324 lines
11 KiB
Python
Executable File
324 lines
11 KiB
Python
Executable File
#!/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()
|