| #!/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:])) |