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