bisect.py revision 3892aa1c459f07a5355a8aa94c7ab3c5f6e5cf39
1#!/usr/bin/python2
2"""The unified package/object bisecting tool."""
3
4from __future__ import print_function
5
6import abc
7import argparse
8import os
9import sys
10from argparse import RawTextHelpFormatter
11
12import common
13
14from cros_utils import command_executer
15from cros_utils import logger
16
17import binary_search_state
18
19
20class 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       *    install_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
126class 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        'install_script': 'cros_pkg/install.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
173class 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        'install_script': 'sysroot_wrapper/install.sh',
187        'test_script': 'sysroot_wrapper/interactive_test.sh',
188        'noincremental': True,
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' % (self.sysroot_wrapper_setup,
197                                       self.options.board, self.options.remote,
198                                       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
227class 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        'install_script': 'android/install.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
287def 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 = """
309Run ./bisect.py {method} --help for individual method help/args
310
311------------------
312
313See README.bisect for examples on argument overriding
314
315See below for full override argument reference:
316"""
317
318
319def Main(argv):
320  override_parser = argparse.ArgumentParser(add_help=False,
321                                            argument_default=argparse.SUPPRESS,
322                                            usage='bisect.py {mode} [options]')
323  common.BuildArgParser(override_parser, override=True)
324
325  epilog = _HELP_EPILOG + override_parser.format_help()
326  parser = argparse.ArgumentParser(epilog=epilog,
327                                   formatter_class=RawTextHelpFormatter)
328  subparsers = parser.add_subparsers(title='Bisect mode',
329                                     description=('Which bisection method to '
330                                                  'use. Each method has '
331                                                  'specific setup and '
332                                                  'arguments. Please consult '
333                                                  'the README for more '
334                                                  'information.'))
335
336  parser_package = subparsers.add_parser('package')
337  parser_package.add_argument('board', help='Board to target')
338  parser_package.add_argument('remote', help='Remote machine to test on')
339  parser_package.set_defaults(handler=BisectPackage)
340
341  parser_object = subparsers.add_parser('object')
342  parser_object.add_argument('board', help='Board to target')
343  parser_object.add_argument('remote', help='Remote machine to test on')
344  parser_object.add_argument('package', help='Package to emerge and test')
345  parser_object.add_argument('--dir',
346                             help=('Bisection directory to use, sets '
347                                   '$BISECT_DIR if provided. Defaults to '
348                                   'current value of $BISECT_DIR (or '
349                                   '/tmp/sysroot_bisect if $BISECT_DIR is '
350                                   'empty).'))
351  parser_object.set_defaults(handler=BisectObject)
352
353  parser_android = subparsers.add_parser('android')
354  parser_android.add_argument('android_src', help='Path to android source tree')
355  parser_android.add_argument('--dir',
356                              help=('Bisection directory to use, sets '
357                                    '$BISECT_DIR if provided. Defaults to '
358                                    'current value of $BISECT_DIR (or '
359                                    '~/ANDROID_BISECT/ if $BISECT_DIR is '
360                                    'empty).'))
361  parser_android.add_argument('-j', '--num_jobs',
362                              type=int,
363                              default=1,
364                              help=('Number of jobs that make and various '
365                                    'scripts for bisector can spawn. Setting '
366                                    'this value too high can freeze up your '
367                                    'machine!'))
368  parser_android.add_argument('--device_id',
369                              default='',
370                              help=('Device id for device used for testing. '
371                                    'Use this if you have multiple Android '
372                                    'devices plugged into your machine.'))
373  parser_android.set_defaults(handler=BisectAndroid)
374
375  options, remaining = parser.parse_known_args(argv)
376  if remaining:
377    overrides = override_parser.parse_args(remaining)
378    overrides = vars(overrides)
379  else:
380    overrides = {}
381
382  subcmd = options.handler
383  del options.handler
384
385  bisector = subcmd(options, overrides)
386  return Run(bisector)
387
388
389if __name__ == '__main__':
390  os.chdir(os.path.dirname(__file__))
391  sys.exit(Main(sys.argv[1:]))
392