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