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       * 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
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        '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
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        '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' % (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        '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
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(
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
396if __name__ == '__main__':
397  os.chdir(os.path.dirname(__file__))
398  sys.exit(Main(sys.argv[1:]))
399