blob: ef1048bb817c73199bc6c7d475a2594cde95eb31 [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2020 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.
"""The unified package/object bisecting tool."""
from __future__ import print_function
import abc
import argparse
from argparse import RawTextHelpFormatter
import os
import sys
from binary_search_tool import binary_search_state
from binary_search_tool import common
from cros_utils import command_executer
from cros_utils import logger
class Bisector(object, metaclass=abc.ABCMeta):
"""The abstract base class for Bisectors."""
def __init__(self, options, overrides=None):
"""Constructor for Bisector abstract base class
Args:
options: positional arguments for specific mode (board, remote, etc.)
overrides: optional dict of overrides for argument defaults
"""
self.options = options
self.overrides = overrides
if not overrides:
self.overrides = {}
self.logger = logger.GetLogger()
self.ce = command_executer.GetCommandExecuter()
def _PrettyPrintArgs(self, args, overrides):
"""Output arguments in a nice, human readable format
Will print and log all arguments for the bisecting tool and make note of
which arguments have been overridden.
Example output:
./run_bisect.py package daisy 172.17.211.184 -I "" -t cros_pkg/my_test.sh
Performing ChromeOS Package bisection
Method Config:
board : daisy
remote : 172.17.211.184
Bisection Config: (* = overridden)
get_initial_items : cros_pkg/get_initial_items.sh
switch_to_good : cros_pkg/switch_to_good.sh
switch_to_bad : cros_pkg/switch_to_bad.sh
* test_setup_script :
* test_script : cros_pkg/my_test.sh
prune : True
noincremental : False
file_args : True
Args:
args: The args to be given to binary_search_state.Run. This represents
how the bisection tool will run (with overridden arguments already
added in).
overrides: The dict of overriden arguments provided by the user. This is
provided so the user can be told which arguments were
overriden and with what value.
"""
# Output method config (board, remote, etc.)
options = vars(self.options)
out = '\nPerforming %s bisection\n' % self.method_name
out += 'Method Config:\n'
max_key_len = max([len(str(x)) for x in options.keys()])
for key in sorted(options):
val = options[key]
key_str = str(key).rjust(max_key_len)
val_str = str(val)
out += ' %s : %s\n' % (key_str, val_str)
# Output bisection config (scripts, prune, etc.)
out += '\nBisection Config: (* = overridden)\n'
max_key_len = max([len(str(x)) for x in args.keys()])
# Print args in common._ArgsDict order
args_order = [x['dest'] for x in common.GetArgsDict().values()]
for key in sorted(args, key=args_order.index):
val = args[key]
key_str = str(key).rjust(max_key_len)
val_str = str(val)
changed_str = '*' if key in overrides else ' '
out += ' %s %s : %s\n' % (changed_str, key_str, val_str)
out += '\n'
self.logger.LogOutput(out)
def ArgOverride(self, args, overrides, pretty_print=True):
"""Override arguments based on given overrides and provide nice output
Args:
args: dict of arguments to be passed to binary_search_state.Run (runs
dict.update, causing args to be mutated).
overrides: dict of arguments to update args with
pretty_print: if True print out args/overrides to user in pretty format
"""
args.update(overrides)
if pretty_print:
self._PrettyPrintArgs(args, overrides)
@abc.abstractmethod
def PreRun(self):
pass
@abc.abstractmethod
def Run(self):
pass
@abc.abstractmethod
def PostRun(self):
pass
class BisectPackage(Bisector):
"""The class for package bisection steps."""
cros_pkg_setup = 'cros_pkg/setup.sh'
cros_pkg_cleanup = 'cros_pkg/%s_cleanup.sh'
def __init__(self, options, overrides):
super(BisectPackage, self).__init__(options, overrides)
self.method_name = 'ChromeOS Package'
self.default_kwargs = {
'get_initial_items': 'cros_pkg/get_initial_items.sh',
'switch_to_good': 'cros_pkg/switch_to_good.sh',
'switch_to_bad': 'cros_pkg/switch_to_bad.sh',
'test_setup_script': 'cros_pkg/test_setup.sh',
'test_script': 'cros_pkg/interactive_test.sh',
'noincremental': False,
'prune': True,
'file_args': True
}
self.setup_cmd = ('%s %s %s' % (self.cros_pkg_setup, self.options.board,
self.options.remote))
self.ArgOverride(self.default_kwargs, self.overrides)
def PreRun(self):
ret, _, _ = self.ce.RunCommandWExceptionCleanup(
self.setup_cmd, print_to_console=True)
if ret:
self.logger.LogError('Package bisector setup failed w/ error %d' % ret)
return 1
return 0
def Run(self):
return binary_search_state.Run(**self.default_kwargs)
def PostRun(self):
cmd = self.cros_pkg_cleanup % self.options.board
ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
if ret:
self.logger.LogError('Package bisector cleanup failed w/ error %d' % ret)
return 1
self.logger.LogOutput(('Cleanup successful! To restore the bisection '
'environment run the following:\n'
' cd %s; %s') % (os.getcwd(), self.setup_cmd))
return 0
class BisectObject(Bisector):
"""The class for object bisection steps."""
sysroot_wrapper_setup = 'sysroot_wrapper/setup.sh'
sysroot_wrapper_cleanup = 'sysroot_wrapper/cleanup.sh'
def __init__(self, options, overrides):
super(BisectObject, self).__init__(options, overrides)
self.method_name = 'ChromeOS Object'
self.default_kwargs = {
'get_initial_items': 'sysroot_wrapper/get_initial_items.sh',
'switch_to_good': 'sysroot_wrapper/switch_to_good.sh',
'switch_to_bad': 'sysroot_wrapper/switch_to_bad.sh',
'test_setup_script': 'sysroot_wrapper/test_setup.sh',
'test_script': 'sysroot_wrapper/interactive_test.sh',
'noincremental': False,
'prune': True,
'file_args': True
}
self.options = options
if options.dir:
os.environ['BISECT_DIR'] = options.dir
self.options.dir = os.environ.get('BISECT_DIR', '/tmp/sysroot_bisect')
self.setup_cmd = (
'%s %s %s %s' % (self.sysroot_wrapper_setup, self.options.board,
self.options.remote, self.options.package))
self.ArgOverride(self.default_kwargs, overrides)
def PreRun(self):
ret, _, _ = self.ce.RunCommandWExceptionCleanup(
self.setup_cmd, print_to_console=True)
if ret:
self.logger.LogError('Object bisector setup failed w/ error %d' % ret)
return 1
os.environ['BISECT_STAGE'] = 'TRIAGE'
return 0
def Run(self):
return binary_search_state.Run(**self.default_kwargs)
def PostRun(self):
cmd = self.sysroot_wrapper_cleanup
ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
if ret:
self.logger.LogError('Object bisector cleanup failed w/ error %d' % ret)
return 1
self.logger.LogOutput(('Cleanup successful! To restore the bisection '
'environment run the following:\n'
' cd %s; %s') % (os.getcwd(), self.setup_cmd))
return 0
class BisectAndroid(Bisector):
"""The class for Android bisection steps."""
android_setup = 'android/setup.sh'
android_cleanup = 'android/cleanup.sh'
default_dir = os.path.expanduser('~/ANDROID_BISECT')
def __init__(self, options, overrides):
super(BisectAndroid, self).__init__(options, overrides)
self.method_name = 'Android'
self.default_kwargs = {
'get_initial_items': 'android/get_initial_items.sh',
'switch_to_good': 'android/switch_to_good.sh',
'switch_to_bad': 'android/switch_to_bad.sh',
'test_setup_script': 'android/test_setup.sh',
'test_script': 'android/interactive_test.sh',
'prune': True,
'file_args': True,
'noincremental': False,
}
self.options = options
if options.dir:
os.environ['BISECT_DIR'] = options.dir
self.options.dir = os.environ.get('BISECT_DIR', self.default_dir)
num_jobs = "NUM_JOBS='%s'" % self.options.num_jobs
device_id = ''
if self.options.device_id:
device_id = "ANDROID_SERIAL='%s'" % self.options.device_id
self.setup_cmd = ('%s %s %s %s' % (num_jobs, device_id, self.android_setup,
self.options.android_src))
self.ArgOverride(self.default_kwargs, overrides)
def PreRun(self):
ret, _, _ = self.ce.RunCommandWExceptionCleanup(
self.setup_cmd, print_to_console=True)
if ret:
self.logger.LogError('Android bisector setup failed w/ error %d' % ret)
return 1
os.environ['BISECT_STAGE'] = 'TRIAGE'
return 0
def Run(self):
return binary_search_state.Run(**self.default_kwargs)
def PostRun(self):
cmd = self.android_cleanup
ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
if ret:
self.logger.LogError('Android bisector cleanup failed w/ error %d' % ret)
return 1
self.logger.LogOutput(('Cleanup successful! To restore the bisection '
'environment run the following:\n'
' cd %s; %s') % (os.getcwd(), self.setup_cmd))
return 0
def Run(bisector):
log = logger.GetLogger()
log.LogOutput('Setting up Bisection tool')
ret = bisector.PreRun()
if ret:
return ret
log.LogOutput('Running Bisection tool')
ret = bisector.Run()
if ret:
return ret
log.LogOutput('Cleaning up Bisection tool')
ret = bisector.PostRun()
if ret:
return ret
return 0
_HELP_EPILOG = """
Run ./run_bisect.py {method} --help for individual method help/args
------------------
See README.bisect for examples on argument overriding
See below for full override argument reference:
"""
def Main(argv):
override_parser = argparse.ArgumentParser(
add_help=False,
argument_default=argparse.SUPPRESS,
usage='run_bisect.py {mode} [options]')
common.BuildArgParser(override_parser, override=True)
epilog = _HELP_EPILOG + override_parser.format_help()
parser = argparse.ArgumentParser(
epilog=epilog, formatter_class=RawTextHelpFormatter)
subparsers = parser.add_subparsers(
title='Bisect mode',
description=('Which bisection method to '
'use. Each method has '
'specific setup and '
'arguments. Please consult '
'the README for more '
'information.'))
parser_package = subparsers.add_parser('package')
parser_package.add_argument('board', help='Board to target')
parser_package.add_argument('remote', help='Remote machine to test on')
parser_package.set_defaults(handler=BisectPackage)
parser_object = subparsers.add_parser('object')
parser_object.add_argument('board', help='Board to target')
parser_object.add_argument('remote', help='Remote machine to test on')
parser_object.add_argument('package', help='Package to emerge and test')
parser_object.add_argument(
'--dir',
help=('Bisection directory to use, sets '
'$BISECT_DIR if provided. Defaults to '
'current value of $BISECT_DIR (or '
'/tmp/sysroot_bisect if $BISECT_DIR is '
'empty).'))
parser_object.set_defaults(handler=BisectObject)
parser_android = subparsers.add_parser('android')
parser_android.add_argument('android_src', help='Path to android source tree')
parser_android.add_argument(
'--dir',
help=('Bisection directory to use, sets '
'$BISECT_DIR if provided. Defaults to '
'current value of $BISECT_DIR (or '
'~/ANDROID_BISECT/ if $BISECT_DIR is '
'empty).'))
parser_android.add_argument(
'-j',
'--num_jobs',
type=int,
default=1,
help=('Number of jobs that make and various '
'scripts for bisector can spawn. Setting '
'this value too high can freeze up your '
'machine!'))
parser_android.add_argument(
'--device_id',
default='',
help=('Device id for device used for testing. '
'Use this if you have multiple Android '
'devices plugged into your machine.'))
parser_android.set_defaults(handler=BisectAndroid)
options, remaining = parser.parse_known_args(argv)
if remaining:
overrides = override_parser.parse_args(remaining)
overrides = vars(overrides)
else:
overrides = {}
subcmd = options.handler
del options.handler
bisector = subcmd(options, overrides)
return Run(bisector)
if __name__ == '__main__':
os.chdir(os.path.dirname(__file__))
sys.exit(Main(sys.argv[1:]))