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