| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # Copyright 2020 The ChromiumOS Authors |
| # 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.""" |
| |
| |
| import abc |
| import argparse |
| from argparse import RawTextHelpFormatter |
| import os |
| import shlex |
| 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 = " ".join( |
| (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 = " ".join( |
| ( |
| self.sysroot_wrapper_setup, |
| self.options.board, |
| self.options.remote, |
| self.options.package, |
| str(self.options.reboot).lower(), |
| shlex.quote(self.options.use_flags), |
| ) |
| ) |
| |
| 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 = " ".join( |
| (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( |
| "--use_flags", |
| required=False, |
| default="", |
| help="Use flags passed to emerge", |
| ) |
| parser_object.add_argument( |
| "--noreboot", |
| action="store_false", |
| dest="reboot", |
| help="Do not reboot after updating the package (default: False)", |
| ) |
| 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:])) |