blob: 1d489383cf4b9f9732ff250087e6b0525d8cc405 [file] [log] [blame]
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# Copyright 2018 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.
"""Diagnose ChromeOS autotest regressions.
This is integrated bisection utility. Given ChromeOS, Chrome, Android source
tree, and necessary parameters, this script can determine which components to
bisect, and hopefully output the culprit CL of regression.
Sometimes the script failed to figure out the final CL for various reasons, it
will cut down the search range as narrow as it can.
"""
from __future__ import print_function
import argparse
import fnmatch
import glob
import logging
import os
from bisect_kit import cli
from bisect_kit import common
from bisect_kit import configure
from bisect_kit import core
from bisect_kit import cros_lab_util
from bisect_kit import cros_util
from bisect_kit import diagnoser_cros
from bisect_kit import util
import setup_cros_bisect
logger = logging.getLogger(__name__)
# What chrome binaries to build for given autotest.
# This dict is created manually by inspecting output of
# 'grep -r ChromeBinaryTest autotest/files/client/site_tests'
# If you change this dict, build_and_deploy_chrome_helper.sh may need update
# as well.
CHROME_BINARIES_OF_TEST = {
'graphics_Chrome.ozone_gl_unittests': ['ozone_gl_unittests'],
'security_SandboxLinuxUnittests': ['sandbox_linux_unittests'],
'video_HangoutHardwarePerf*': [
'video_decode_accelerator_unittest',
'video_encode_accelerator_unittest',
],
'video_JDAPerf*': ['jpeg_decode_accelerator_unittest'],
'video_JEAPerf': ['jpeg_encode_accelerator_unittest'],
'video_JpegDecodeAccelerator': ['jpeg_decode_accelerator_unittest'],
'video_JpegEncodeAccelerator': ['jpeg_encode_accelerator_unittest'],
'video_VDAPerf': ['video_decode_accelerator_unittest'],
'video_VDASanity': ['video_decode_accelerator_unittest'],
'video_VEAPerf': ['video_encode_accelerator_unittest'],
'video_VideoDecodeAccelerator*': ['video_decode_accelerator_unittest'],
'video_VideoEncodeAccelerator*': ['video_encode_accelerator_unittest'],
}
class DiagnoseStates(core.States):
"""Diagnose states."""
def init(self, config):
self.set_data(dict(config=config))
@property
def config(self):
return self.data['config']
def grab_dut(config):
# Assume "DEPENDENCIES" is identical between the period of
# `old` and `new` version.
autotest_dir = os.path.join(config['chromeos_root'],
cros_util.prebuilt_autotest_dir)
info = cros_util.get_autotest_test_info(autotest_dir, config['test_name'])
assert info
reason = 'bisect-kit: %s' % config['session']
if config.get('allocated_dut'):
host_name = cros_lab_util.dut_host_name(config['allocated_dut'])
logger.info('try to allocate the same host (%s) as last run', host_name)
host = cros_lab_util.allocate_host(reason, host=host_name)
else:
extra_labels = []
dependencies = info.variables.get('DEPENDENCIES', '')
for label in dependencies.split(','):
label = label.strip()
# Skip non-machine labels
if label in ['cleanup-reboot']:
continue
extra_labels.append(label)
host = cros_lab_util.allocate_host(
reason,
model=config['model'],
sku=config['sku'],
extra_labels=extra_labels)
if not host:
logger.error('unable to allocate dut')
return None
logger.info('allocated host %s', host)
return host
def may_depend_on_extra_chrome_binaries(autotest_dir, test_name):
info = cros_util.get_autotest_test_info(autotest_dir, test_name)
assert info
dirpath = os.path.dirname(info.path)
for pypath in glob.glob(os.path.join(dirpath, '*.py')):
if 'ChromeBinaryTest' in open(pypath).read():
return True
return False
def determine_chrome_binaries(chromeos_root, test_name):
chrome_binaries = None
for name_pattern, binaries in CHROME_BINARIES_OF_TEST.items():
if fnmatch.fnmatch(test_name, name_pattern):
chrome_binaries = binaries
break
autotest_dir = os.path.join(chromeos_root, cros_util.prebuilt_autotest_dir)
if chrome_binaries:
logger.info('This test depends on chrome binary: %s', chrome_binaries)
elif may_depend_on_extra_chrome_binaries(autotest_dir, test_name):
logger.warning(
'%s code used ChromeBinaryTest but the binary is unknown; '
'please update CHROME_BINARIES_OF_TEST table', test_name)
return chrome_binaries
class DiagnoseCommandLine(object):
"""Diagnose command line interface."""
def __init__(self):
common.init()
self.argument_parser = self.create_argument_parser()
self.states = None
@property
def config(self):
return self.states.config
def check_options(self, opts, path_factory):
if not opts.chromeos_mirror:
opts.chromeos_mirror = path_factory.get_chromeos_mirror()
logger.info('chromeos_mirror = %s', opts.chromeos_mirror)
if not opts.chromeos_root:
opts.chromeos_root = path_factory.get_chromeos_tree()
logger.info('chromeos_root = %s', opts.chromeos_root)
if not opts.chrome_mirror:
opts.chrome_mirror = path_factory.get_chrome_cache()
logger.info('chrome_mirror = %s', opts.chrome_mirror)
if not opts.chrome_root:
opts.chrome_root = path_factory.get_chrome_tree()
logger.info('chrome_root = %s', opts.chrome_root)
if opts.dut == cros_lab_util.LAB_DUT:
if not opts.model and not opts.sku:
self.argument_parser.error(
'either --model or --sku need to be specified if DUT is "%s"' %
cros_lab_util.LAB_DUT)
# Board name cannot be deduced from auto allocated devices because they
# may be provisioned with image of unexpected board.
if not opts.board:
self.argument_parser.error('--board need to be specified if DUT is "%s"'
% cros_lab_util.LAB_DUT)
else:
if not opts.board:
opts.board = cros_util.query_dut_board(opts.dut)
if cros_util.is_cros_short_version(opts.old):
opts.old = cros_util.version_to_full(opts.board, opts.old)
if cros_util.is_cros_short_version(opts.new):
opts.new = cros_util.version_to_full(opts.board, opts.new)
if opts.metric:
if opts.old_value is None:
self.argument_parser.error('--old_value is not provided')
if opts.new_value is None:
self.argument_parser.error('--new_value is not provided')
else:
if opts.old_value is not None:
self.argument_parser.error(
'--old_value is provided but --metric is not')
if opts.new_value is not None:
self.argument_parser.error(
'--new_value is provided but --metric is not')
def cmd_init(self, opts):
path_factory = setup_cros_bisect.DefaultProjectPathFactory(
opts.mirror_base, opts.work_base, opts.session)
self.check_options(opts, path_factory)
config = dict(
session=opts.session,
mirror_base=opts.mirror_base,
work_base=opts.work_base,
chromeos_root=opts.chromeos_root,
chromeos_mirror=opts.chromeos_mirror,
chrome_root=opts.chrome_root,
chrome_mirror=opts.chrome_mirror,
android_root=opts.android_root,
android_mirror=opts.android_mirror,
dut=opts.dut,
model=opts.model,
sku=opts.sku,
board=opts.board,
old=opts.old,
new=opts.new,
test_name=opts.test_name,
metric=opts.metric,
old_value=opts.old_value,
new_value=opts.new_value,
noisy=opts.noisy,
test_that_args=opts.args,
always_reflash=opts.always_reflash,
)
self.states.init(config)
# Unpack old autotest prebuilt, assume following information don't change
# between versions:
# - what chrome binaries to run
# - dependency labels for DUT allocation
common_switch_cmd, _common_eval_cmd = self._build_cmds()
util.check_call(*(common_switch_cmd + [self.config['old']]))
self.states.save()
def _build_cmds(self):
# prebuilt version will be specified later.
common_switch_cmd = [
'./switch_autotest_prebuilt.py',
'--chromeos_root', self.config['chromeos_root'],
'--test_name', self.config['test_name'],
'--board', self.config['board'],
] # yapf: disable
common_eval_cmd = [
'./eval_cros_autotest.py',
'--chromeos_root', self.config['chromeos_root'],
'--test_name', self.config['test_name'],
] # yapf: disable
if self.config['metric']:
common_eval_cmd += [
'--metric', self.config['metric'],
'--old_value', str(self.config['old_value']),
'--new_value', str(self.config['new_value']),
] # yapf: disable
if self.config['test_that_args']:
common_eval_cmd += ['--args', self.config['test_that_args']]
return common_switch_cmd, common_eval_cmd
def cmd_run(self, opts):
del opts # unused
self.states.load()
path_factory = setup_cros_bisect.DefaultProjectPathFactory(
self.config['mirror_base'], self.config['work_base'],
self.config['session'])
common_switch_cmd, common_eval_cmd = self._build_cmds()
chrome_binaries = determine_chrome_binaries(self.config['chromeos_root'],
self.config['test_name'])
with cros_lab_util.dut_manager(self.config['dut'],
lambda: grab_dut(self.config)) as dut:
if not dut:
raise core.ExecutionFatalError('unable to allocate DUT')
assert cros_util.is_dut(dut)
self.config['allocated_dut'] = dut
self.states.save()
common_eval_cmd.append(dut)
diagnoser = diagnoser_cros.CrosDiagnoser(
self.config['session'], path_factory, self.config['chromeos_root'],
self.config['chromeos_mirror'], self.config['android_root'],
self.config['android_mirror'], self.config['chrome_root'],
self.config['chrome_mirror'], self.config['board'],
self.config['noisy'], dut)
eval_cmd = common_eval_cmd + ['--prebuilt', '--reinstall']
# Do not specify version for autotest prebuilt switching here. The trick
# is that version number is obtained via bisector's environment variable
# CROS_VERSION.
extra_switch_cmd = common_switch_cmd
if not diagnoser.narrow_down_chromeos_prebuilt(
self.config['old'],
self.config['new'],
eval_cmd,
extra_switch_cmd=extra_switch_cmd):
return
diagnoser.switch_chromeos_to_old(force=self.config['always_reflash'])
util.check_call(*(common_switch_cmd + [diagnoser.cros_old]))
util.check_call('ssh', dut, 'rm', '-rf', '/usr/local/autotest')
if diagnoser.narrow_down_android(eval_cmd) is not None:
return
# Assume it's ok to leave random version of android prebuilt on DUT.
# Don't --reinstall to keep chrome binaries override.
eval_cmd = common_eval_cmd + ['--prebuilt']
if diagnoser.narrow_down_chrome(
eval_cmd, chrome_binaries=chrome_binaries) is not None:
return
eval_cmd = common_eval_cmd + ['--reinstall']
if diagnoser.narrow_down_chromeos_localbuild(eval_cmd):
logger.info('%s done', __file__)
def create_argument_parser(self):
parser = argparse.ArgumentParser()
common.add_common_arguments(parser)
parser.add_argument('--session_base', default='bisect.sessions')
parser.add_argument('--session', help='Session name', required=True)
subparsers = parser.add_subparsers(
dest='command', title='commands', metavar='<command>')
parser_init = subparsers.add_parser('init', help='Initialize')
group = parser_init.add_argument_group(
title='Source tree path options',
description='''
Specify the paths of chromeos/chrome/android mirror and checkout. They
have the same default values as setup_cros_bisect.py, so usually you can
omit them and it just works.
''')
group.add_argument(
'--mirror_base',
metavar='MIRROR_BASE',
default=configure.get('MIRROR_BASE',
setup_cros_bisect.DEFAULT_MIRROR_BASE),
help='Directory for mirrors (default: %(default)s)')
group.add_argument(
'--work_base',
metavar='WORK_BASE',
default=configure.get('WORK_BASE', setup_cros_bisect.DEFAULT_WORK_BASE),
help='Directory for bisection working directories '
'(default: %(default)s)')
group.add_argument(
'--chromeos_root',
metavar='CHROMEOS_ROOT',
type=cli.argtype_dir_path,
default=configure.get('CHROMEOS_ROOT'),
help='ChromeOS tree root')
group.add_argument(
'--chromeos_mirror',
type=cli.argtype_dir_path,
default=configure.get('CHROMEOS_MIRROR'),
help='ChromeOS repo mirror path')
group.add_argument(
'--android_root',
metavar='ANDROID_ROOT',
type=cli.argtype_dir_path,
default=configure.get('ANDROID_ROOT'),
help='Android tree root')
group.add_argument(
'--android_mirror',
type=cli.argtype_dir_path,
default=configure.get('ANDROID_MIRROR'),
help='Android repo mirror path')
group.add_argument(
'--chrome_root',
metavar='CHROME_ROOT',
type=cli.argtype_dir_path,
default=configure.get('CHROME_ROOT'),
help='Chrome tree root')
group.add_argument(
'--chrome_mirror',
metavar='CHROME_MIRROR',
type=cli.argtype_dir_path,
default=configure.get('CHROME_MIRROR'),
help="chrome's gclient cache dir")
group = parser_init.add_argument_group(title='DUT allocation options')
group.add_argument(
'--dut',
metavar='DUT',
required=True,
help='Address of DUT (Device Under Test). If "%s", DUT will be '
'automatically allocated from the lab' % cros_lab_util.LAB_DUT)
group.add_argument(
'--model',
metavar='MODEL',
help='"model" criteria if DUT is auto allocated from the lab')
group.add_argument(
'--sku',
metavar='SKU',
help='"sku" criteria if DUT is auto allocated from the lab')
group = parser_init.add_argument_group(title='Essential options')
group.add_argument(
'--board',
metavar='BOARD',
default=configure.get('BOARD'),
help='ChromeOS board name; auto detected if DUT is not auto allocated')
group.add_argument(
'--old',
type=cros_util.argtype_cros_version,
required=True,
help='ChromeOS version with old behavior')
group.add_argument(
'--new',
type=cros_util.argtype_cros_version,
required=True,
help='ChromeOS version with new behavior')
group.add_argument('--test_name', required=True, help='Test name')
group = parser_init.add_argument_group(title='Options for benchmark test')
group.add_argument('--metric', help='Metric name of benchmark test')
group.add_argument(
'--old_value',
type=float,
help='For benchmark test, old value of metric')
group.add_argument(
'--new_value',
type=float,
help='For benchmark test, new value of metric')
group = parser_init.add_argument_group(title='Options passed to test_that')
group.add_argument(
'--args',
help='Extra args passed to "test_that --args"; Overrides the default')
group = parser_init.add_argument_group(title='Bisect behavior options')
group.add_argument(
'--noisy',
help='Enable noisy binary search. Example value: "old=1/10,new=2/3"')
group.add_argument(
'--always_reflash',
action='store_true',
help='Do not trust ChromeOS version number of DUT and always reflash. '
'This is usually only needed when resume because previous bisect was '
'interrupted and the DUT may be in an unexpected state')
parser_init.set_defaults(func=self.cmd_init)
parser_run = subparsers.add_parser('run', help='Start auto bisection')
parser_run.set_defaults(func=self.cmd_run)
return parser
def main(self, args=None):
opts = self.argument_parser.parse_args(args)
common.config_logging(opts)
session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
session_file = os.path.join(session_base, opts.session,
self.__class__.__name__)
self.states = DiagnoseStates(session_file)
opts.func(opts)
if __name__ == '__main__':
DiagnoseCommandLine().main()