bisect.py revision 471aae79081b9a9097e5f57c8d6792f72853b03f
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.ArgOverride(self.default_kwargs, self.overrides)
146
147  def PreRun(self):
148    cmd = ('%s %s %s' %
149           (self.cros_pkg_setup, self.options.board, self.options.remote))
150    ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
151    if ret:
152      self.logger.LogError('Package bisector setup failed w/ error %d' % ret)
153      return 1
154    return 0
155
156  def Run(self):
157    return binary_search_state.Run(**self.default_kwargs)
158
159  def PostRun(self):
160    cmd = self.cros_pkg_cleanup % self.options.board
161    ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
162    if ret:
163      self.logger.LogError('Package bisector cleanup failed w/ error %d' % ret)
164      return 1
165    return 0
166
167
168class BisectObject(Bisector):
169  """The class for object bisection steps."""
170
171  sysroot_wrapper_setup = 'sysroot_wrapper/setup.sh'
172  sysroot_wrapper_cleanup = 'sysroot_wrapper/cleanup.sh'
173
174  def __init__(self, options, overrides):
175    super(BisectObject, self).__init__(options, overrides)
176    self.method_name = 'ChromeOS Object'
177    self.default_kwargs = {
178        'get_initial_items': 'sysroot_wrapper/get_initial_items.sh',
179        'switch_to_good': 'sysroot_wrapper/switch_to_good.sh',
180        'switch_to_bad': 'sysroot_wrapper/switch_to_bad.sh',
181        'install_script': 'sysroot_wrapper/install.sh',
182        'test_script': 'sysroot_wrapper/interactive_test.sh',
183        'noincremental': True,
184        'prune': True,
185        'file_args': True
186    }
187    self.options = options
188    if options.dir:
189      os.environ['BISECT_DIR'] = options.dir
190    self.options.dir = os.environ.get('BISECT_DIR', '/tmp/sysroot_bisect')
191
192    self.ArgOverride(self.default_kwargs, overrides)
193
194  def PreRun(self):
195    cmd = ('%s %s %s %s' % (self.sysroot_wrapper_setup, self.options.board,
196                            self.options.remote, self.options.package))
197    ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
198    if ret:
199      self.logger.LogError('Object bisector setup failed w/ error %d' % ret)
200      return 1
201
202    os.environ['BISECT_STAGE'] = 'TRIAGE'
203    return 0
204
205  def Run(self):
206    return binary_search_state.Run(**self.default_kwargs)
207
208  def PostRun(self):
209    cmd = self.sysroot_wrapper_cleanup
210    ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
211    if ret:
212      self.logger.LogError('Object bisector cleanup failed w/ error %d' % ret)
213      return 1
214    return 0
215
216
217class BisectAndroid(Bisector):
218  """The class for Android bisection steps."""
219
220  android_setup = 'android/setup.sh'
221  android_cleanup = 'android/cleanup.sh'
222  default_dir = os.path.expanduser('~/ANDROID_BISECT')
223
224  def __init__(self, options, overrides):
225    super(BisectAndroid, self).__init__(options, overrides)
226    self.method_name = 'Android'
227    self.default_kwargs = {
228        'get_initial_items': 'android/get_initial_items.sh',
229        'switch_to_good': 'android/switch_to_good.sh',
230        'switch_to_bad': 'android/switch_to_bad.sh',
231        'install_script': 'android/install.sh',
232        'test_script': 'android/interactive_test.sh',
233        'prune': True,
234        'file_args': True,
235        'noincremental': False,
236    }
237    self.options = options
238    if options.dir:
239      os.environ['BISECT_DIR'] = options.dir
240    self.options.dir = os.environ.get('BISECT_DIR', self.default_dir)
241
242    self.ArgOverride(self.default_kwargs, overrides)
243
244  def PreRun(self):
245    num_jobs = "NUM_JOBS='%s'" % self.options.num_jobs
246    device_id = ""
247    if self.options.device_id:
248      device_id = "ANDROID_SERIAL='%s'" % self.options.device_id
249
250    cmd = ('%s %s %s %s' % (num_jobs, device_id, self.android_setup,
251                            self.options.android_src))
252    ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
253    if ret:
254      self.logger.LogError('Android bisector setup failed w/ error %d' % ret)
255      return 1
256
257    os.environ['BISECT_STAGE'] = 'TRIAGE'
258    return 0
259
260  def Run(self):
261    return binary_search_state.Run(**self.default_kwargs)
262
263  def PostRun(self):
264    cmd = self.android_cleanup
265    ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
266    if ret:
267      self.logger.LogError('Android bisector cleanup failed w/ error %d' % ret)
268      return 1
269    return 0
270
271
272def Run(bisector):
273  log = logger.GetLogger()
274
275  log.LogOutput('Setting up Bisection tool')
276  ret = bisector.PreRun()
277  if ret:
278    return ret
279
280  log.LogOutput('Running Bisection tool')
281  ret = bisector.Run()
282  if ret:
283    return ret
284
285  log.LogOutput('Cleaning up Bisection tool')
286  ret = bisector.PostRun()
287  if ret:
288    return ret
289
290  return 0
291
292
293_HELP_EPILOG = """
294Run ./bisect.py {method} --help for individual method help/args
295
296------------------
297
298See README.bisect for examples on argument overriding
299
300See below for full override argument reference:
301"""
302
303
304def Main(argv):
305  override_parser = argparse.ArgumentParser(add_help=False,
306                                            argument_default=argparse.SUPPRESS,
307                                            usage='bisect.py {mode} [options]')
308  common.BuildArgParser(override_parser, override=True)
309
310  epilog = _HELP_EPILOG + override_parser.format_help()
311  parser = argparse.ArgumentParser(epilog=epilog,
312                                   formatter_class=RawTextHelpFormatter)
313  subparsers = parser.add_subparsers(title='Bisect mode',
314                                     description=('Which bisection method to '
315                                                  'use. Each method has '
316                                                  'specific setup and '
317                                                  'arguments. Please consult '
318                                                  'the README for more '
319                                                  'information.'))
320
321  parser_package = subparsers.add_parser('package')
322  parser_package.add_argument('board', help='Board to target')
323  parser_package.add_argument('remote', help='Remote machine to test on')
324  parser_package.set_defaults(handler=BisectPackage)
325
326  parser_object = subparsers.add_parser('object')
327  parser_object.add_argument('board', help='Board to target')
328  parser_object.add_argument('remote', help='Remote machine to test on')
329  parser_object.add_argument('package', help='Package to emerge and test')
330  parser_object.add_argument('--dir',
331                             help=('Bisection directory to use, sets '
332                                   '$BISECT_DIR if provided. Defaults to '
333                                   'current value of $BISECT_DIR (or '
334                                   '/tmp/sysroot_bisect if $BISECT_DIR is '
335                                   'empty).'))
336  parser_object.set_defaults(handler=BisectObject)
337
338  parser_android = subparsers.add_parser('android')
339  parser_android.add_argument('android_src', help='Path to android source tree')
340  parser_android.add_argument('--dir',
341                              help=('Bisection directory to use, sets '
342                                    '$BISECT_DIR if provided. Defaults to '
343                                    'current value of $BISECT_DIR (or '
344                                    '~/ANDROID_BISECT/ if $BISECT_DIR is '
345                                    'empty).'))
346  parser_android.add_argument('-j', '--num_jobs',
347                              type=int,
348                              default=1,
349                              help=('Number of jobs that make and various '
350                                    'scripts for bisector can spawn. Setting '
351                                    'this value too high can freeze up your '
352                                    'machine!'))
353  parser_android.add_argument('--device_id',
354                              default='',
355                              help=('Device id for device used for testing. '
356                                    'Use this if you have multiple Android '
357                                    'devices plugged into your machine.'))
358  parser_android.set_defaults(handler=BisectAndroid)
359
360  options, remaining = parser.parse_known_args(argv)
361  if remaining:
362    overrides = override_parser.parse_args(remaining)
363    overrides = vars(overrides)
364  else:
365    overrides = {}
366
367  subcmd = options.handler
368  del options.handler
369
370  bisector = subcmd(options, overrides)
371  return Run(bisector)
372
373
374if __name__ == '__main__':
375  os.chdir(os.path.dirname(__file__))
376  sys.exit(Main(sys.argv[1:]))
377