blob: b54ba65101b42ed6cfe39475b55a445afa1ff428 [file] [log] [blame]
#!/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.
"""Runs a tryjob/tryjobs after updating the packages."""
from __future__ import print_function
import argparse
import datetime
import json
import os
import subprocess
import chroot
import failure_modes
import get_llvm_hash
import update_chromeos_llvm_hash
VALID_CQ_TRYBOTS = ['llvm', 'llvm-next', 'llvm-tot']
def GetCommandLineArgs():
"""Parses the command line for the command line arguments.
Returns:
The log level to use when retrieving the LLVM hash or google3 LLVM version,
the chroot path to use for executing chroot commands,
a list of a package or packages to update their LLVM next hash,
and the LLVM version to use when retrieving the LLVM hash.
"""
# Default path to the chroot if a path is 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='Update an LLVM hash of packages and run tests.')
# Add argument for other change lists that want to run alongside the tryjob
# which has a change list of updating a package's git hash.
parser.add_argument(
'--extra_change_lists',
type=int,
nargs='+',
default=[],
help='change lists that would like to be run alongside the change list '
'of updating the packages')
# 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 to choose between llvm and llvm-next.
parser.add_argument(
'--is_llvm_next',
action='store_true',
help='which llvm hash to update. Update LLVM_NEXT_HASH if specified. '
'Otherwise, update LLVM_HASH')
# Add argument for the absolute path to the file that contains information on
# the previous tested svn version.
parser.add_argument(
'--last_tested',
help='the absolute path to the file that contains the last tested '
'arguments.')
# Add argument for the LLVM version to use.
parser.add_argument(
'--llvm_version',
type=get_llvm_hash.is_svn_option,
required=True,
help='which git hash of LLVM to find '
'{google3, ToT, <svn_version>} '
'(default: finds the git hash of the google3 LLVM '
'version)')
# Add argument to add reviewers for the created CL.
parser.add_argument(
'--reviewers',
nargs='+',
default=[],
help='The reviewers for the package update changelist')
# Add argument for whether to display command contents to `stdout`.
parser.add_argument(
'--verbose',
action='store_true',
help='display contents of a command to the terminal '
'(default: %(default)s)')
subparsers = parser.add_subparsers(dest='subparser_name')
subparser_names = []
# Testing with the tryjobs.
tryjob_subparser = subparsers.add_parser('tryjobs')
subparser_names.append('tryjobs')
tryjob_subparser.add_argument(
'--builders',
required=True,
nargs='+',
default=[],
help='builders to use for the tryjob testing')
# Add argument for custom options for the tryjob.
tryjob_subparser.add_argument(
'--options',
required=False,
nargs='+',
default=[],
help='options to use for the tryjob testing')
# Testing with the recipe builders
recipe_subparser = subparsers.add_parser('recipe')
subparser_names.append('recipe')
recipe_subparser.add_argument(
'--options',
required=False,
nargs='+',
default=[],
help='options passed to the recipe builders')
recipe_subparser.add_argument(
'--builders',
required=True,
nargs='+',
default=[],
help='recipe builders to launch')
# Testing with CQ.
cq_subparser = subparsers.add_parser('cq')
subparser_names.append('cq')
# Add argument for specify a cq trybot to test along with other cq builders
# e.g. llvm, llvm-next or llvm-tot
cq_subparser.add_argument(
'--cq_trybot',
choices=VALID_CQ_TRYBOTS,
help='include the trybot to test together with other cq builders '
'available: %(choices)s')
args_output = parser.parse_args()
if args_output.subparser_name not in subparser_names:
parser.error('one of %s must be specified' % subparser_names)
return args_output
def UnchangedSinceLastRun(last_tested_file, arg_dict):
"""Gets the arguments used for last run
Args:
last_tested_file: The absolute path to the file that contains the
arguments for the last run.
arg_dict: The arguments used for this run.
Returns:
Return true if the arguments used for last run exist and are the
same as the arguments used for this run. Otherwise return false.
"""
if not last_tested_file:
return False
# Get the last tested svn version if the file exists.
last_arg_dict = None
try:
with open(last_tested_file) as f:
last_arg_dict = json.load(f)
except (IOError, ValueError):
return False
return arg_dict == last_arg_dict
def AddReviewers(cl, reviewers, chroot_path):
"""Add reviewers for the created CL."""
gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit')
for reviewer in reviewers:
cmd = [gerrit_abs_path, 'reviewers', str(cl), reviewer]
subprocess.check_output(cmd)
def AddLinksToCL(tests, cl, chroot_path):
"""Adds the test link(s) to the CL as a comment."""
# NOTE: Invoking `cros_sdk` does not make each tryjob link appear on its own
# line, so invoking the `gerrit` command directly instead of using `cros_sdk`
# to do it for us.
#
# FIXME: Need to figure out why `cros_sdk` does not add each tryjob link as a
# newline.
gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit')
links = ['Started the following tests:']
links.extend(test['link'] for test in tests)
add_message_cmd = [gerrit_abs_path, 'message', str(cl), '\n'.join(links)]
subprocess.check_output(add_message_cmd)
# Testing with tryjobs
def GetCurrentTimeInUTC():
"""Returns the current time via `datetime.datetime.utcnow()`."""
return datetime.datetime.utcnow()
def GetTryJobCommand(change_list, extra_change_lists, options, builder):
"""Constructs the 'tryjob' command.
Args:
change_list: The CL obtained from updating the packages.
extra_change_lists: Extra change lists that would like to be run alongside
the change list of updating the packages.
options: Options to be passed into the tryjob command.
builder: The builder to be passed into the tryjob command.
Returns:
The 'tryjob' command with the change list of updating the packages and
any extra information that was passed into the command line.
"""
tryjob_cmd = ['cros', 'tryjob', '--yes', '--json', '-g', '%d' % change_list]
if extra_change_lists:
for extra_cl in extra_change_lists:
tryjob_cmd.extend(['-g', '%d' % extra_cl])
if options:
tryjob_cmd.extend('--%s' % option for option in options)
tryjob_cmd.append(builder)
return tryjob_cmd
def RunTryJobs(cl_number, extra_change_lists, options, builders, chroot_path):
"""Runs a tryjob/tryjobs.
Args:
cl_number: The CL created by updating the packages.
extra_change_lists: Any extra change lists that would run alongside the CL
that was created by updating the packages ('cl_number').
options: Any options to be passed into the 'tryjob' command.
builders: All the builders to run the 'tryjob' with.
chroot_path: The absolute path to the chroot.
Returns:
A list that contains stdout contents of each tryjob, where stdout is
information (a hashmap) about the tryjob. The hashmap also contains stderr
if there was an error when running a tryjob.
Raises:
ValueError: Failed to submit a tryjob.
"""
# Contains the results of each builder.
tests = []
# Run tryjobs with the change list number obtained from updating the
# packages and append additional changes lists and options obtained from the
# command line.
for builder in builders:
cmd = GetTryJobCommand(cl_number, extra_change_lists, options, builder)
out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8')
test_output = json.loads(out)
tests.append({
'launch_time': str(GetCurrentTimeInUTC()),
'link': str(test_output[0]['url']),
'buildbucket_id': int(test_output[0]['buildbucket_id']),
'extra_cls': extra_change_lists,
'options': options,
'builder': [builder]
})
AddLinksToCL(tests, cl_number, chroot_path)
return tests
def StartRecipeBuilders(cl_number, extra_change_lists, options, builders,
chroot_path):
"""Launch recipe builders.
Args:
cl_number: The CL created by updating the packages.
extra_change_lists: Any extra change lists that would run alongside the CL
that was created by updating the packages ('cl_number').
options: Any options to be passed into the 'tryjob' command.
builders: All the builders to run the 'tryjob' with.
chroot_path: The absolute path to the chroot.
Returns:
A list that contains stdout contents of each builder, where stdout is
information (a hashmap) about the tryjob. The hashmap also contains stderr
if there was an error when running a tryjob.
Raises:
ValueError: Failed to start a builder.
"""
# Contains the results of each builder.
tests = []
# Launch a builders with the change list number obtained from updating the
# packages and append additional changes lists and options obtained from the
# command line.
for builder in builders:
cmd = ['bb', 'add', '-json']
if cl_number:
cmd.extend(['-cl', 'crrev.com/c/%d' % cl_number])
if extra_change_lists:
for cl in extra_change_lists:
cmd.extend(['-cl', 'crrev.com/c/%d' % cl])
if options:
cmd.extend(options)
cmd.append(builder)
out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8')
test_output = json.loads(out)
tests.append({
'launch_time': test_output['createTime'],
'link': 'http://ci.chromium.org/b/%s' % test_output['id'],
'buildbucket_id': test_output['id'],
'extra_cls': extra_change_lists,
'options': options,
'builder': [builder]
})
AddLinksToCL(tests, cl_number, chroot_path)
return tests
# Testing with CQ
def GetCQDependString(dependent_cls):
"""Get CQ dependency string e.g. `Cq-Depend: chromium:MM, chromium:NN`."""
if not dependent_cls:
return None
# Cq-Depend must start a new paragraph prefixed with "Cq-Depend".
return '\nCq-Depend: ' + ', '.join(('chromium:%s' % i) for i in dependent_cls)
def GetCQIncludeTrybotsString(trybot):
"""Get Cq-Include-Trybots string, for more llvm testings"""
if not trybot:
return None
if trybot not in VALID_CQ_TRYBOTS:
raise ValueError('%s is not a valid llvm trybot' % trybot)
# Cq-Include-Trybots must start a new paragraph prefixed
# with "Cq-Include-Trybots".
return '\nCq-Include-Trybots:chromeos/cq:cq-%s-orchestrator' % trybot
def StartCQDryRun(cl, dependent_cls, chroot_path):
"""Start CQ dry run for the changelist and dependencies."""
gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit')
cl_list = [cl]
cl_list.extend(dependent_cls)
for changes in cl_list:
cq_dry_run_cmd = [gerrit_abs_path, 'label-cq', str(changes), '1']
subprocess.check_output(cq_dry_run_cmd)
def main():
"""Updates the packages' LLVM hash and run tests.
Raises:
AssertionError: The script was run inside the chroot.
"""
chroot.VerifyOutsideChroot()
args_output = GetCommandLineArgs()
update_packages = [
'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx',
'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind'
]
patch_metadata_file = 'PATCHES.json'
svn_option = args_output.llvm_version
git_hash, svn_version = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption(
svn_option)
# There is no need to run tryjobs when all the key parameters remain unchanged
# from last time.
# If --last_tested is specified, check if the current run has the same
# arguments last time --last_tested is used.
if args_output.last_tested:
chroot_file_paths = chroot.GetChrootEbuildPaths(args_output.chroot_path,
update_packages)
arg_dict = {
'svn_version': svn_version,
'ebuilds': chroot_file_paths,
'extra_cls': args_output.extra_change_lists,
}
if args_output.subparser_name in ('tryjobs', 'recipe'):
arg_dict['builders'] = args_output.builders
arg_dict['tryjob_options'] = args_output.options
if UnchangedSinceLastRun(args_output.last_tested, arg_dict):
print('svn version (%d) matches the last tested svn version in %s' %
(svn_version, args_output.last_tested))
return
llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
if args_output.is_llvm_next:
llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
update_chromeos_llvm_hash.verbose = args_output.verbose
extra_commit_msg = None
if args_output.subparser_name == 'cq':
cq_depend_msg = GetCQDependString(args_output.extra_change_lists)
if cq_depend_msg:
extra_commit_msg = cq_depend_msg
cq_trybot_msg = GetCQIncludeTrybotsString(args_output.cq_trybot)
if cq_trybot_msg:
extra_commit_msg += cq_trybot_msg
change_list = update_chromeos_llvm_hash.UpdatePackages(
update_packages,
llvm_variant,
git_hash,
svn_version,
args_output.chroot_path,
patch_metadata_file,
failure_modes.FailureModes.DISABLE_PATCHES,
svn_option,
extra_commit_msg=extra_commit_msg)
AddReviewers(change_list.cl_number, args_output.reviewers,
args_output.chroot_path)
print('Successfully updated packages to %d' % svn_version)
print('Gerrit URL: %s' % change_list.url)
print('Change list number: %d' % change_list.cl_number)
if args_output.subparser_name == 'tryjobs':
tests = RunTryJobs(change_list.cl_number, args_output.extra_change_lists,
args_output.options, args_output.builders,
args_output.chroot_path)
print('Tests:')
for test in tests:
print(test)
elif args_output.subparser_name == 'recipe':
tests = StartRecipeBuilders(
change_list.cl_number, args_output.extra_change_lists,
args_output.options, args_output.builders, args_output.chroot_path)
print('Tests:')
for test in tests:
print(test)
else:
StartCQDryRun(change_list.cl_number, args_output.extra_change_lists,
args_output.chroot_path)
# If --last_tested is specified, record the arguments used
if args_output.last_tested:
with open(args_output.last_tested, 'w') as f:
json.dump(arg_dict, f, indent=2)
if __name__ == '__main__':
main()