blob: 5a0ec184427381d394bb19673f52bc9e25e0f028 [file] [log] [blame] [edit]
#!/bin/env vpython3
# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Command-line interface of the UTR
Using a specified builder name, this tool can build and/or launch a test the
same way it's done on the bots. See the README.md in //tools/utr/ for more info.
Any additional args passed at the end of the invocation will be passed down
as-is to all triggered tests. Example uses:
- vpython3 run.py -B $BUCKET -b $BUILDER -t $TEST compile
- vpython3 run.py -B $BUCKET -b $BUILDER -t $TEST compile-and-test
- vpython3 run.py -B $BUCKET -b $BUILDER -t $TEST test --
--gtest_filter=Test.Case
"""
import argparse
import logging
import os
import pathlib
import random
import re
import sys
import builders
import cipd
import recipe
from rich.logging import RichHandler
_THIS_DIR = pathlib.Path(__file__).resolve().parent
_SRC_DIR = _THIS_DIR.parents[1]
_SURVEY_LINK = 'https://forms.gle/tA41evzW5goqR5WF9'
def maybe_print_survey_link():
# Only print the link every 5% of runs
if random.random() < 0.05:
logging.info('Help us improve by sharing your feedback in this short '
'survey: %s' % _SURVEY_LINK)
def add_common_args(parser):
parser.add_argument('--verbose',
'-v',
dest='verbosity',
default=0,
action='count',
help='Enable additional runtime logging. Pass multiple '
'times for increased logging.')
parser.add_argument('--force',
'-f',
action='store_true',
help='Skip all prompts about config mismatches.')
parser.add_argument('--test',
'-t',
action='append',
default=[],
dest='tests',
help='Name of test suite(s) to replicate. Pass multiple '
'times for multiple tests. Optional with the "compile" '
'run mode which will compile "all".')
parser.add_argument('--builder',
'-b',
required=True,
help='Name of the builder we want to replicate.')
parser.add_argument(
'--project',
'-p',
help="Name of the project of the builder. Note: if you're on a release "
'branch, you can exclude the milestone part of the name (eg: you can '
'pass "chrome" instead of "chrome-m123"). Will attempt to automatically '
'determine if not specified.')
parser.add_argument(
'--bucket',
'-B',
help='Name of the bucket of the builder. Will attempt to automatically '
'determine if not specified.')
parser.add_argument(
'--build-dir',
'--out-dir',
'-o',
type=pathlib.Path,
help='Path to the build dir to use for compilation and/or for invoking '
'test binaries. Will use a build dir in //out/ named after the builder '
'if not specified: //out/UTR${{builder_name}}')
parser.add_argument(
'--recipe-dir',
'--recipe-path',
'-r',
type=pathlib.Path,
help='Path to override the recipe bundle with a local bundle. To create '
'a bundle locally, run `./recipes.py bundle` in your desired recipe '
'checkout. This creates a dir called "bundle" that can be pointed to '
'with this arg.')
parser.add_argument('--reuse-task',
type=str,
help='Ruse the cas digest of the provided swarming task')
parser.add_argument(
'--no-siso',
action='store_true',
help='Disables the use of siso ("use_siso" GN arg) in the compile, as '
"well as the use of siso when isolating tests. Will use the builder's "
'settings if not specified.')
# This only applies to test commands but making it common lets us avoid
# breaking up flag args with positional args
parser.add_argument('--dimension',
'-d',
action='append',
default=[],
dest='dimensions',
help='Custom swarming dimensions to apply to all the '
'tests to be run. These should be in the form '
'"{key}={value}". To remove an existing dimension leave '
'the value empty: "{key}="')
parser.add_argument(
'--shards',
type=int,
help='Shard count override to use for all swarming tests. Increasing '
'shard count can make a suite finish more quickly. However, this '
'will change the batching of test cases which may expose failures '
'caused by test cases that implicitly depend on running in the same '
'batch as others.')
def add_compile_args(parser):
parser.add_argument(
'--no-rbe',
action='store_true',
help='Disables the use of rbe ("use_remoteexec" GN arg) in the compile. '
"Will use the builder's settings if not specified.")
parser.add_argument(
'--no-coverage-instrumentation',
action='store_true',
help='Skips instrumenting code-coverage, even if the builder is '
'configured to instrument. Instrumentation can inflate both build sizes '
"and runtimes. But some failures may only occur when it's enabled.")
def add_test_args(parser):
parser.add_argument(
'additional_test_args',
nargs='*',
help='The args listed here will be appended to the test cmd-lines.')
def parse_args(args=None):
"""Parse cmd line args.
Args:
args: Cmd line args to parse. Only passed in unittests. Otherwise uses argv.
Returns:
An argparse.ArgumentParser.
"""
parser = argparse.ArgumentParser(
description=__doc__,
# Custom formatter to preserve line breaks in the docstring
formatter_class=argparse.RawDescriptionHelpFormatter)
add_common_args(parser)
subparsers = parser.add_subparsers(dest='run_mode')
compile_subp = subparsers.add_parser('compile',
aliases=['build'],
help='Only compiles.')
add_compile_args(compile_subp)
test_subp = subparsers.add_parser('test', help='Only run/trigger tests.')
add_test_args(test_subp)
compile_and_test_subp = subparsers.add_parser(
'compile-and-test',
aliases=['build-and-test', 'run'],
help='Both compile and run/trigger tests.')
add_compile_args(compile_and_test_subp)
add_test_args(compile_and_test_subp)
args = parser.parse_args(args)
if not args.run_mode:
parser.print_help()
parser.error('Please select a run_mode: compile,test,compile-and-test')
if args.reuse_task and args.run_mode != 'test':
parser.print_help()
parser.error('reuse-task is only compatible with "test"')
if args.run_mode == 'compile':
if args.dimensions:
parser.error(
'Dimensions flags (-d) are only applicable to run modes that run '
'tests: test or compile-and-test.')
if args.shards:
parser.error(
'Shards flag (--shards) is only applicable when running tests via '
'"test" or "compile-and-test" run modes.')
if args.dimensions and any(not re.match(r'^[^=]+=.*$', d)
for d in args.dimensions):
parser.error('Dimensions flags (-d) must be in the format {key}={value} or '
'{key}= to remove an existing dimenion.')
if not args.tests:
# Only compile mode should default to compile all
if args.run_mode != 'compile':
parser.print_help()
parser.error('Please provide a test to run')
if args.project:
if re.fullmatch(r'chromium(-m\d+)?', args.project):
args.project = 'chromium'
elif re.fullmatch(r'chrome(-m\d+)?', args.project):
args.project = 'chrome'
else:
parser.error(
f'Unknown project: "{args.project}". Please select "chrome" or '
'"chromium".')
return args
def main():
args = parse_args()
logging.basicConfig(level=logging.DEBUG if args.verbosity else logging.INFO,
format='%(message)s',
handlers=[
RichHandler(show_time=False,
show_level=False,
show_path=False,
markup=True)
])
cipd_bin_path = _SRC_DIR.joinpath('third_party', 'depot_tools', '.cipd_bin')
if not cipd_bin_path.exists():
logging.warning(
".cipd_bin folder not found. 'gclient sync' may need to be run")
else:
os.environ["PATH"] = str(cipd_bin_path) + os.pathsep + os.environ["PATH"]
if not recipe.check_luci_context_auth():
return 1
builder_props, project = builders.find_builder_props(
args.builder, bucket_name=args.bucket, project_name=args.project)
if not builder_props:
return 1
build_dir = args.build_dir
if not args.build_dir:
build_dir = _SRC_DIR.joinpath('out', 'UTR' + '_'.join(args.builder.split()))
logging.info('[cyan]Using the following build dir:[/]')
logging.getLogger('basic_logger').info(build_dir)
logging.info('')
if not args.recipe_dir:
recipes_path = cipd.fetch_recipe_bundle(project,
args.verbosity).joinpath('recipes')
else:
recipes_path = args.recipe_dir.joinpath('recipes')
skip_compile = args.run_mode == 'test'
skip_test = args.run_mode == 'compile'
recipe_runner = recipe.LegacyRunner(
recipes_path,
builder_props,
project,
args.bucket,
args.builder,
args.tests,
skip_compile,
skip_test,
args.force,
build_dir,
additional_test_args=None if skip_test else args.additional_test_args,
swarming_dimensions=args.dimensions,
swarming_shards=args.shards,
reuse_task=args.reuse_task,
skip_coverage=not skip_compile and args.no_coverage_instrumentation,
no_rbe=not skip_compile and args.no_rbe,
no_siso=args.no_siso,
)
exit_code, error_msg = recipe_runner.run_recipe(
filter_stdout=args.verbosity < 2)
if error_msg:
logging.error('\nUTR failure:')
logging.error(error_msg)
maybe_print_survey_link()
return exit_code
if __name__ == '__main__':
sys.exit(main())