1#!/usr/bin/env 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' % 197 (self.sysroot_wrapper_setup, self.options.board, 198 self.options.remote, 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