Home | History | Annotate | Download | only in binary_search_tool
      1 #!/usr/bin/env python2
      2 """The unified package/object bisecting tool."""
      3 
      4 from __future__ import print_function
      5 
      6 import abc
      7 import argparse
      8 import os
      9 import sys
     10 from argparse import RawTextHelpFormatter
     11 
     12 import common
     13 
     14 from cros_utils import command_executer
     15 from cros_utils import logger
     16 
     17 import binary_search_state
     18 
     19 
     20 class Bisector(object):
     21   """The abstract base class for Bisectors."""
     22 
     23   # Make Bisector an abstract class
     24   __metaclass__ = abc.ABCMeta
     25 
     26   def __init__(self, options, overrides=None):
     27     """Constructor for Bisector abstract base class
     28 
     29     Args:
     30       options: positional arguments for specific mode (board, remote, etc.)
     31       overrides: optional dict of overrides for argument defaults
     32     """
     33     self.options = options
     34     self.overrides = overrides
     35     if not overrides:
     36       self.overrides = {}
     37     self.logger = logger.GetLogger()
     38     self.ce = command_executer.GetCommandExecuter()
     39 
     40   def _PrettyPrintArgs(self, args, overrides):
     41     """Output arguments in a nice, human readable format
     42 
     43     Will print and log all arguments for the bisecting tool and make note of
     44     which arguments have been overridden.
     45 
     46     Example output:
     47       ./bisect.py package daisy 172.17.211.184 -I "" -t cros_pkg/my_test.sh
     48       Performing ChromeOS Package bisection
     49       Method Config:
     50         board : daisy
     51        remote : 172.17.211.184
     52 
     53       Bisection Config: (* = overridden)
     54          get_initial_items : cros_pkg/get_initial_items.sh
     55             switch_to_good : cros_pkg/switch_to_good.sh
     56              switch_to_bad : cros_pkg/switch_to_bad.sh
     57        * test_setup_script :
     58        *       test_script : cros_pkg/my_test.sh
     59                      prune : True
     60              noincremental : False
     61                  file_args : True
     62 
     63     Args:
     64       args: The args to be given to binary_search_state.Run. This represents
     65             how the bisection tool will run (with overridden arguments already
     66             added in).
     67       overrides: The dict of overriden arguments provided by the user. This is
     68                  provided so the user can be told which arguments were
     69                  overriden and with what value.
     70     """
     71     # Output method config (board, remote, etc.)
     72     options = vars(self.options)
     73     out = '\nPerforming %s bisection\n' % self.method_name
     74     out += 'Method Config:\n'
     75     max_key_len = max([len(str(x)) for x in options.keys()])
     76     for key in sorted(options):
     77       val = options[key]
     78       key_str = str(key).rjust(max_key_len)
     79       val_str = str(val)
     80       out += ' %s : %s\n' % (key_str, val_str)
     81 
     82     # Output bisection config (scripts, prune, etc.)
     83     out += '\nBisection Config: (* = overridden)\n'
     84     max_key_len = max([len(str(x)) for x in args.keys()])
     85     # Print args in common._ArgsDict order
     86     args_order = [x['dest'] for x in common.GetArgsDict().itervalues()]
     87     compare = lambda x, y: cmp(args_order.index(x), args_order.index(y))
     88 
     89     for key in sorted(args, cmp=compare):
     90       val = args[key]
     91       key_str = str(key).rjust(max_key_len)
     92       val_str = str(val)
     93       changed_str = '*' if key in overrides else ' '
     94 
     95       out += ' %s %s : %s\n' % (changed_str, key_str, val_str)
     96 
     97     out += '\n'
     98     self.logger.LogOutput(out)
     99 
    100   def ArgOverride(self, args, overrides, pretty_print=True):
    101     """Override arguments based on given overrides and provide nice output
    102 
    103     Args:
    104       args: dict of arguments to be passed to binary_search_state.Run (runs
    105             dict.update, causing args to be mutated).
    106       overrides: dict of arguments to update args with
    107       pretty_print: if True print out args/overrides to user in pretty format
    108     """
    109     args.update(overrides)
    110     if pretty_print:
    111       self._PrettyPrintArgs(args, overrides)
    112 
    113   @abc.abstractmethod
    114   def PreRun(self):
    115     pass
    116 
    117   @abc.abstractmethod
    118   def Run(self):
    119     pass
    120 
    121   @abc.abstractmethod
    122   def PostRun(self):
    123     pass
    124 
    125 
    126 class BisectPackage(Bisector):
    127   """The class for package bisection steps."""
    128 
    129   cros_pkg_setup = 'cros_pkg/setup.sh'
    130   cros_pkg_cleanup = 'cros_pkg/%s_cleanup.sh'
    131 
    132   def __init__(self, options, overrides):
    133     super(BisectPackage, self).__init__(options, overrides)
    134     self.method_name = 'ChromeOS Package'
    135     self.default_kwargs = {
    136         'get_initial_items': 'cros_pkg/get_initial_items.sh',
    137         'switch_to_good': 'cros_pkg/switch_to_good.sh',
    138         'switch_to_bad': 'cros_pkg/switch_to_bad.sh',
    139         'test_setup_script': 'cros_pkg/test_setup.sh',
    140         'test_script': 'cros_pkg/interactive_test.sh',
    141         'noincremental': False,
    142         'prune': True,
    143         'file_args': True
    144     }
    145     self.setup_cmd = ('%s %s %s' % (self.cros_pkg_setup, self.options.board,
    146                                     self.options.remote))
    147     self.ArgOverride(self.default_kwargs, self.overrides)
    148 
    149   def PreRun(self):
    150     ret, _, _ = self.ce.RunCommandWExceptionCleanup(
    151         self.setup_cmd, print_to_console=True)
    152     if ret:
    153       self.logger.LogError('Package bisector setup failed w/ error %d' % ret)
    154       return 1
    155     return 0
    156 
    157   def Run(self):
    158     return binary_search_state.Run(**self.default_kwargs)
    159 
    160   def PostRun(self):
    161     cmd = self.cros_pkg_cleanup % self.options.board
    162     ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
    163     if ret:
    164       self.logger.LogError('Package bisector cleanup failed w/ error %d' % ret)
    165       return 1
    166 
    167     self.logger.LogOutput(('Cleanup successful! To restore the bisection '
    168                            'environment run the following:\n'
    169                            '  cd %s; %s') % (os.getcwd(), self.setup_cmd))
    170     return 0
    171 
    172 
    173 class BisectObject(Bisector):
    174   """The class for object bisection steps."""
    175 
    176   sysroot_wrapper_setup = 'sysroot_wrapper/setup.sh'
    177   sysroot_wrapper_cleanup = 'sysroot_wrapper/cleanup.sh'
    178 
    179   def __init__(self, options, overrides):
    180     super(BisectObject, self).__init__(options, overrides)
    181     self.method_name = 'ChromeOS Object'
    182     self.default_kwargs = {
    183         'get_initial_items': 'sysroot_wrapper/get_initial_items.sh',
    184         'switch_to_good': 'sysroot_wrapper/switch_to_good.sh',
    185         'switch_to_bad': 'sysroot_wrapper/switch_to_bad.sh',
    186         'test_setup_script': 'sysroot_wrapper/test_setup.sh',
    187         'test_script': 'sysroot_wrapper/interactive_test.sh',
    188         'noincremental': False,
    189         'prune': True,
    190         'file_args': True
    191     }
    192     self.options = options
    193     if options.dir:
    194       os.environ['BISECT_DIR'] = options.dir
    195     self.options.dir = os.environ.get('BISECT_DIR', '/tmp/sysroot_bisect')
    196     self.setup_cmd = ('%s %s %s %s' %
    197                       (self.sysroot_wrapper_setup, self.options.board,
    198                        self.options.remote, self.options.package))
    199 
    200     self.ArgOverride(self.default_kwargs, overrides)
    201 
    202   def PreRun(self):
    203     ret, _, _ = self.ce.RunCommandWExceptionCleanup(
    204         self.setup_cmd, print_to_console=True)
    205     if ret:
    206       self.logger.LogError('Object bisector setup failed w/ error %d' % ret)
    207       return 1
    208 
    209     os.environ['BISECT_STAGE'] = 'TRIAGE'
    210     return 0
    211 
    212   def Run(self):
    213     return binary_search_state.Run(**self.default_kwargs)
    214 
    215   def PostRun(self):
    216     cmd = self.sysroot_wrapper_cleanup
    217     ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
    218     if ret:
    219       self.logger.LogError('Object bisector cleanup failed w/ error %d' % ret)
    220       return 1
    221     self.logger.LogOutput(('Cleanup successful! To restore the bisection '
    222                            'environment run the following:\n'
    223                            '  cd %s; %s') % (os.getcwd(), self.setup_cmd))
    224     return 0
    225 
    226 
    227 class BisectAndroid(Bisector):
    228   """The class for Android bisection steps."""
    229 
    230   android_setup = 'android/setup.sh'
    231   android_cleanup = 'android/cleanup.sh'
    232   default_dir = os.path.expanduser('~/ANDROID_BISECT')
    233 
    234   def __init__(self, options, overrides):
    235     super(BisectAndroid, self).__init__(options, overrides)
    236     self.method_name = 'Android'
    237     self.default_kwargs = {
    238         'get_initial_items': 'android/get_initial_items.sh',
    239         'switch_to_good': 'android/switch_to_good.sh',
    240         'switch_to_bad': 'android/switch_to_bad.sh',
    241         'test_setup_script': 'android/test_setup.sh',
    242         'test_script': 'android/interactive_test.sh',
    243         'prune': True,
    244         'file_args': True,
    245         'noincremental': False,
    246     }
    247     self.options = options
    248     if options.dir:
    249       os.environ['BISECT_DIR'] = options.dir
    250     self.options.dir = os.environ.get('BISECT_DIR', self.default_dir)
    251 
    252     num_jobs = "NUM_JOBS='%s'" % self.options.num_jobs
    253     device_id = ''
    254     if self.options.device_id:
    255       device_id = "ANDROID_SERIAL='%s'" % self.options.device_id
    256 
    257     self.setup_cmd = ('%s %s %s %s' % (num_jobs, device_id, self.android_setup,
    258                                        self.options.android_src))
    259 
    260     self.ArgOverride(self.default_kwargs, overrides)
    261 
    262   def PreRun(self):
    263     ret, _, _ = self.ce.RunCommandWExceptionCleanup(
    264         self.setup_cmd, print_to_console=True)
    265     if ret:
    266       self.logger.LogError('Android bisector setup failed w/ error %d' % ret)
    267       return 1
    268 
    269     os.environ['BISECT_STAGE'] = 'TRIAGE'
    270     return 0
    271 
    272   def Run(self):
    273     return binary_search_state.Run(**self.default_kwargs)
    274 
    275   def PostRun(self):
    276     cmd = self.android_cleanup
    277     ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
    278     if ret:
    279       self.logger.LogError('Android bisector cleanup failed w/ error %d' % ret)
    280       return 1
    281     self.logger.LogOutput(('Cleanup successful! To restore the bisection '
    282                            'environment run the following:\n'
    283                            '  cd %s; %s') % (os.getcwd(), self.setup_cmd))
    284     return 0
    285 
    286 
    287 def Run(bisector):
    288   log = logger.GetLogger()
    289 
    290   log.LogOutput('Setting up Bisection tool')
    291   ret = bisector.PreRun()
    292   if ret:
    293     return ret
    294 
    295   log.LogOutput('Running Bisection tool')
    296   ret = bisector.Run()
    297   if ret:
    298     return ret
    299 
    300   log.LogOutput('Cleaning up Bisection tool')
    301   ret = bisector.PostRun()
    302   if ret:
    303     return ret
    304 
    305   return 0
    306 
    307 
    308 _HELP_EPILOG = """
    309 Run ./bisect.py {method} --help for individual method help/args
    310 
    311 ------------------
    312 
    313 See README.bisect for examples on argument overriding
    314 
    315 See below for full override argument reference:
    316 """
    317 
    318 
    319 def Main(argv):
    320   override_parser = argparse.ArgumentParser(
    321       add_help=False,
    322       argument_default=argparse.SUPPRESS,
    323       usage='bisect.py {mode} [options]')
    324   common.BuildArgParser(override_parser, override=True)
    325 
    326   epilog = _HELP_EPILOG + override_parser.format_help()
    327   parser = argparse.ArgumentParser(
    328       epilog=epilog, formatter_class=RawTextHelpFormatter)
    329   subparsers = parser.add_subparsers(
    330       title='Bisect mode',
    331       description=('Which bisection method to '
    332                    'use. Each method has '
    333                    'specific setup and '
    334                    'arguments. Please consult '
    335                    'the README for more '
    336                    'information.'))
    337 
    338   parser_package = subparsers.add_parser('package')
    339   parser_package.add_argument('board', help='Board to target')
    340   parser_package.add_argument('remote', help='Remote machine to test on')
    341   parser_package.set_defaults(handler=BisectPackage)
    342 
    343   parser_object = subparsers.add_parser('object')
    344   parser_object.add_argument('board', help='Board to target')
    345   parser_object.add_argument('remote', help='Remote machine to test on')
    346   parser_object.add_argument('package', help='Package to emerge and test')
    347   parser_object.add_argument(
    348       '--dir',
    349       help=('Bisection directory to use, sets '
    350             '$BISECT_DIR if provided. Defaults to '
    351             'current value of $BISECT_DIR (or '
    352             '/tmp/sysroot_bisect if $BISECT_DIR is '
    353             'empty).'))
    354   parser_object.set_defaults(handler=BisectObject)
    355 
    356   parser_android = subparsers.add_parser('android')
    357   parser_android.add_argument('android_src', help='Path to android source tree')
    358   parser_android.add_argument(
    359       '--dir',
    360       help=('Bisection directory to use, sets '
    361             '$BISECT_DIR if provided. Defaults to '
    362             'current value of $BISECT_DIR (or '
    363             '~/ANDROID_BISECT/ if $BISECT_DIR is '
    364             'empty).'))
    365   parser_android.add_argument(
    366       '-j',
    367       '--num_jobs',
    368       type=int,
    369       default=1,
    370       help=('Number of jobs that make and various '
    371             'scripts for bisector can spawn. Setting '
    372             'this value too high can freeze up your '
    373             'machine!'))
    374   parser_android.add_argument(
    375       '--device_id',
    376       default='',
    377       help=('Device id for device used for testing. '
    378             'Use this if you have multiple Android '
    379             'devices plugged into your machine.'))
    380   parser_android.set_defaults(handler=BisectAndroid)
    381 
    382   options, remaining = parser.parse_known_args(argv)
    383   if remaining:
    384     overrides = override_parser.parse_args(remaining)
    385     overrides = vars(overrides)
    386   else:
    387     overrides = {}
    388 
    389   subcmd = options.handler
    390   del options.handler
    391 
    392   bisector = subcmd(options, overrides)
    393   return Run(bisector)
    394 
    395 
    396 if __name__ == '__main__':
    397   os.chdir(os.path.dirname(__file__))
    398   sys.exit(Main(sys.argv[1:]))
    399