blob: f54e00e1fa9877388f7035fc5b86aa902b9d44b2 [file] [log] [blame]
#!/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:]))