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