• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 shlex
16import sys
17
18from binary_search_tool import binary_search_state
19from binary_search_tool import common
20
21from cros_utils import command_executer
22from cros_utils import logger
23
24
25class Bisector(object, metaclass=abc.ABCMeta):
26  """The abstract base class for Bisectors."""
27
28  def __init__(self, options, overrides=None):
29    """Constructor for Bisector abstract base class
30
31    Args:
32      options: positional arguments for specific mode (board, remote, etc.)
33      overrides: optional dict of overrides for argument defaults
34    """
35    self.options = options
36    self.overrides = overrides
37    if not overrides:
38      self.overrides = {}
39    self.logger = logger.GetLogger()
40    self.ce = command_executer.GetCommandExecuter()
41
42  def _PrettyPrintArgs(self, args, overrides):
43    """Output arguments in a nice, human readable format
44
45    Will print and log all arguments for the bisecting tool and make note of
46    which arguments have been overridden.
47
48    Example output:
49      ./run_bisect.py package daisy 172.17.211.184 -I "" -t cros_pkg/my_test.sh
50      Performing ChromeOS Package bisection
51      Method Config:
52        board : daisy
53       remote : 172.17.211.184
54
55      Bisection Config: (* = overridden)
56         get_initial_items : cros_pkg/get_initial_items.sh
57            switch_to_good : cros_pkg/switch_to_good.sh
58             switch_to_bad : cros_pkg/switch_to_bad.sh
59       * test_setup_script :
60       *       test_script : cros_pkg/my_test.sh
61                     prune : True
62             noincremental : False
63                 file_args : True
64
65    Args:
66      args: The args to be given to binary_search_state.Run. This represents
67            how the bisection tool will run (with overridden arguments already
68            added in).
69      overrides: The dict of overriden arguments provided by the user. This is
70                 provided so the user can be told which arguments were
71                 overriden and with what value.
72    """
73    # Output method config (board, remote, etc.)
74    options = vars(self.options)
75    out = '\nPerforming %s bisection\n' % self.method_name
76    out += 'Method Config:\n'
77    max_key_len = max([len(str(x)) for x in options.keys()])
78    for key in sorted(options):
79      val = options[key]
80      key_str = str(key).rjust(max_key_len)
81      val_str = str(val)
82      out += ' %s : %s\n' % (key_str, val_str)
83
84    # Output bisection config (scripts, prune, etc.)
85    out += '\nBisection Config: (* = overridden)\n'
86    max_key_len = max([len(str(x)) for x in args.keys()])
87    # Print args in common._ArgsDict order
88    args_order = [x['dest'] for x in common.GetArgsDict().values()]
89    for key in sorted(args, key=args_order.index):
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 = ' '.join(
146        (self.cros_pkg_setup, self.options.board, 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 = ' '.join(
197        (self.sysroot_wrapper_setup, self.options.board, self.options.remote,
198         self.options.package, str(self.options.reboot).lower(),
199         shlex.quote(self.options.use_flags)))
200
201    self.ArgOverride(self.default_kwargs, overrides)
202
203  def PreRun(self):
204    ret, _, _ = self.ce.RunCommandWExceptionCleanup(
205        self.setup_cmd, print_to_console=True)
206    if ret:
207      self.logger.LogError('Object bisector setup failed w/ error %d' % ret)
208      return 1
209
210    os.environ['BISECT_STAGE'] = 'TRIAGE'
211    return 0
212
213  def Run(self):
214    return binary_search_state.Run(**self.default_kwargs)
215
216  def PostRun(self):
217    cmd = self.sysroot_wrapper_cleanup
218    ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
219    if ret:
220      self.logger.LogError('Object bisector cleanup failed w/ error %d' % ret)
221      return 1
222    self.logger.LogOutput(('Cleanup successful! To restore the bisection '
223                           'environment run the following:\n'
224                           '  cd %s; %s') % (os.getcwd(), self.setup_cmd))
225    return 0
226
227
228class BisectAndroid(Bisector):
229  """The class for Android bisection steps."""
230
231  android_setup = 'android/setup.sh'
232  android_cleanup = 'android/cleanup.sh'
233  default_dir = os.path.expanduser('~/ANDROID_BISECT')
234
235  def __init__(self, options, overrides):
236    super(BisectAndroid, self).__init__(options, overrides)
237    self.method_name = 'Android'
238    self.default_kwargs = {
239        'get_initial_items': 'android/get_initial_items.sh',
240        'switch_to_good': 'android/switch_to_good.sh',
241        'switch_to_bad': 'android/switch_to_bad.sh',
242        'test_setup_script': 'android/test_setup.sh',
243        'test_script': 'android/interactive_test.sh',
244        'prune': True,
245        'file_args': True,
246        'noincremental': False,
247    }
248    self.options = options
249    if options.dir:
250      os.environ['BISECT_DIR'] = options.dir
251    self.options.dir = os.environ.get('BISECT_DIR', self.default_dir)
252
253    num_jobs = "NUM_JOBS='%s'" % self.options.num_jobs
254    device_id = ''
255    if self.options.device_id:
256      device_id = "ANDROID_SERIAL='%s'" % self.options.device_id
257
258    self.setup_cmd = ' '.join(
259        (num_jobs, device_id, self.android_setup, self.options.android_src))
260
261    self.ArgOverride(self.default_kwargs, overrides)
262
263  def PreRun(self):
264    ret, _, _ = self.ce.RunCommandWExceptionCleanup(
265        self.setup_cmd, print_to_console=True)
266    if ret:
267      self.logger.LogError('Android bisector setup failed w/ error %d' % ret)
268      return 1
269
270    os.environ['BISECT_STAGE'] = 'TRIAGE'
271    return 0
272
273  def Run(self):
274    return binary_search_state.Run(**self.default_kwargs)
275
276  def PostRun(self):
277    cmd = self.android_cleanup
278    ret, _, _ = self.ce.RunCommandWExceptionCleanup(cmd, print_to_console=True)
279    if ret:
280      self.logger.LogError('Android bisector cleanup failed w/ error %d' % ret)
281      return 1
282    self.logger.LogOutput(('Cleanup successful! To restore the bisection '
283                           'environment run the following:\n'
284                           '  cd %s; %s') % (os.getcwd(), self.setup_cmd))
285    return 0
286
287
288def Run(bisector):
289  log = logger.GetLogger()
290
291  log.LogOutput('Setting up Bisection tool')
292  ret = bisector.PreRun()
293  if ret:
294    return ret
295
296  log.LogOutput('Running Bisection tool')
297  ret = bisector.Run()
298  if ret:
299    return ret
300
301  log.LogOutput('Cleaning up Bisection tool')
302  ret = bisector.PostRun()
303  if ret:
304    return ret
305
306  return 0
307
308
309_HELP_EPILOG = """
310Run ./run_bisect.py {method} --help for individual method help/args
311
312------------------
313
314See README.bisect for examples on argument overriding
315
316See below for full override argument reference:
317"""
318
319
320def Main(argv):
321  override_parser = argparse.ArgumentParser(
322      add_help=False,
323      argument_default=argparse.SUPPRESS,
324      usage='run_bisect.py {mode} [options]')
325  common.BuildArgParser(override_parser, override=True)
326
327  epilog = _HELP_EPILOG + override_parser.format_help()
328  parser = argparse.ArgumentParser(
329      epilog=epilog, formatter_class=RawTextHelpFormatter)
330  subparsers = parser.add_subparsers(
331      title='Bisect mode',
332      description=('Which bisection method to '
333                   'use. Each method has '
334                   'specific setup and '
335                   'arguments. Please consult '
336                   'the README for more '
337                   'information.'))
338
339  parser_package = subparsers.add_parser('package')
340  parser_package.add_argument('board', help='Board to target')
341  parser_package.add_argument('remote', help='Remote machine to test on')
342  parser_package.set_defaults(handler=BisectPackage)
343
344  parser_object = subparsers.add_parser('object')
345  parser_object.add_argument('board', help='Board to target')
346  parser_object.add_argument('remote', help='Remote machine to test on')
347  parser_object.add_argument('package', help='Package to emerge and test')
348  parser_object.add_argument(
349      '--use_flags',
350      required=False,
351      default='',
352      help='Use flags passed to emerge')
353  parser_object.add_argument(
354      '--noreboot',
355      action='store_false',
356      dest='reboot',
357      help='Do not reboot after updating the package (default: False)')
358  parser_object.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            '/tmp/sysroot_bisect if $BISECT_DIR is '
364            'empty).'))
365  parser_object.set_defaults(handler=BisectObject)
366
367  parser_android = subparsers.add_parser('android')
368  parser_android.add_argument('android_src', help='Path to android source tree')
369  parser_android.add_argument(
370      '--dir',
371      help=('Bisection directory to use, sets '
372            '$BISECT_DIR if provided. Defaults to '
373            'current value of $BISECT_DIR (or '
374            '~/ANDROID_BISECT/ if $BISECT_DIR is '
375            'empty).'))
376  parser_android.add_argument(
377      '-j',
378      '--num_jobs',
379      type=int,
380      default=1,
381      help=('Number of jobs that make and various '
382            'scripts for bisector can spawn. Setting '
383            'this value too high can freeze up your '
384            'machine!'))
385  parser_android.add_argument(
386      '--device_id',
387      default='',
388      help=('Device id for device used for testing. '
389            'Use this if you have multiple Android '
390            'devices plugged into your machine.'))
391  parser_android.set_defaults(handler=BisectAndroid)
392
393  options, remaining = parser.parse_known_args(argv)
394  if remaining:
395    overrides = override_parser.parse_args(remaining)
396    overrides = vars(overrides)
397  else:
398    overrides = {}
399
400  subcmd = options.handler
401  del options.handler
402
403  bisector = subcmd(options, overrides)
404  return Run(bisector)
405
406
407if __name__ == '__main__':
408  os.chdir(os.path.dirname(__file__))
409  sys.exit(Main(sys.argv[1:]))
410