1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright 2020 The ChromiumOS Authors 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 9 10import abc 11import argparse 12from argparse import RawTextHelpFormatter 13import os 14import shlex 15import sys 16 17from binary_search_tool import binary_search_state 18from binary_search_tool import common 19from cros_utils import command_executer 20from cros_utils import logger 21 22 23class Bisector(object, metaclass=abc.ABCMeta): 24 """The abstract base class for Bisectors.""" 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 ./run_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().values()] 87 for key in sorted(args, key=args_order.index): 88 val = args[key] 89 key_str = str(key).rjust(max_key_len) 90 val_str = str(val) 91 changed_str = "*" if key in overrides else " " 92 93 out += " %s %s : %s\n" % (changed_str, key_str, val_str) 94 95 out += "\n" 96 self.logger.LogOutput(out) 97 98 def ArgOverride(self, args, overrides, pretty_print=True): 99 """Override arguments based on given overrides and provide nice output 100 101 Args: 102 args: dict of arguments to be passed to binary_search_state.Run (runs 103 dict.update, causing args to be mutated). 104 overrides: dict of arguments to update args with 105 pretty_print: if True print out args/overrides to user in pretty format 106 """ 107 args.update(overrides) 108 if pretty_print: 109 self._PrettyPrintArgs(args, overrides) 110 111 @abc.abstractmethod 112 def PreRun(self): 113 pass 114 115 @abc.abstractmethod 116 def Run(self): 117 pass 118 119 @abc.abstractmethod 120 def PostRun(self): 121 pass 122 123 124class BisectPackage(Bisector): 125 """The class for package bisection steps.""" 126 127 cros_pkg_setup = "cros_pkg/setup.sh" 128 cros_pkg_cleanup = "cros_pkg/%s_cleanup.sh" 129 130 def __init__(self, options, overrides): 131 super(BisectPackage, self).__init__(options, overrides) 132 self.method_name = "ChromeOS Package" 133 self.default_kwargs = { 134 "get_initial_items": "cros_pkg/get_initial_items.sh", 135 "switch_to_good": "cros_pkg/switch_to_good.sh", 136 "switch_to_bad": "cros_pkg/switch_to_bad.sh", 137 "test_setup_script": "cros_pkg/test_setup.sh", 138 "test_script": "cros_pkg/interactive_test.sh", 139 "noincremental": False, 140 "prune": True, 141 "file_args": True, 142 } 143 self.setup_cmd = " ".join( 144 (self.cros_pkg_setup, self.options.board, self.options.remote) 145 ) 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 ) 152 if ret: 153 self.logger.LogError( 154 "Package bisector setup failed w/ error %d" % ret 155 ) 156 return 1 157 return 0 158 159 def Run(self): 160 return binary_search_state.Run(**self.default_kwargs) 161 162 def PostRun(self): 163 cmd = self.cros_pkg_cleanup % self.options.board 164 ret, _, _ = self.ce.RunCommandWExceptionCleanup( 165 cmd, print_to_console=True 166 ) 167 if ret: 168 self.logger.LogError( 169 "Package bisector cleanup failed w/ error %d" % ret 170 ) 171 return 1 172 173 self.logger.LogOutput( 174 ( 175 "Cleanup successful! To restore the bisection " 176 "environment run the following:\n" 177 " cd %s; %s" 178 ) 179 % (os.getcwd(), self.setup_cmd) 180 ) 181 return 0 182 183 184class BisectObject(Bisector): 185 """The class for object bisection steps.""" 186 187 sysroot_wrapper_setup = "sysroot_wrapper/setup.sh" 188 sysroot_wrapper_cleanup = "sysroot_wrapper/cleanup.sh" 189 190 def __init__(self, options, overrides): 191 super(BisectObject, self).__init__(options, overrides) 192 self.method_name = "ChromeOS Object" 193 self.default_kwargs = { 194 "get_initial_items": "sysroot_wrapper/get_initial_items.sh", 195 "switch_to_good": "sysroot_wrapper/switch_to_good.sh", 196 "switch_to_bad": "sysroot_wrapper/switch_to_bad.sh", 197 "test_setup_script": "sysroot_wrapper/test_setup.sh", 198 "test_script": "sysroot_wrapper/interactive_test.sh", 199 "noincremental": False, 200 "prune": True, 201 "file_args": True, 202 } 203 self.options = options 204 if options.dir: 205 os.environ["BISECT_DIR"] = options.dir 206 self.options.dir = os.environ.get("BISECT_DIR", "/tmp/sysroot_bisect") 207 self.setup_cmd = " ".join( 208 ( 209 self.sysroot_wrapper_setup, 210 self.options.board, 211 self.options.remote, 212 self.options.package, 213 str(self.options.reboot).lower(), 214 shlex.quote(self.options.use_flags), 215 ) 216 ) 217 218 self.ArgOverride(self.default_kwargs, overrides) 219 220 def PreRun(self): 221 ret, _, _ = self.ce.RunCommandWExceptionCleanup( 222 self.setup_cmd, print_to_console=True 223 ) 224 if ret: 225 self.logger.LogError( 226 "Object bisector setup failed w/ error %d" % ret 227 ) 228 return 1 229 230 os.environ["BISECT_STAGE"] = "TRIAGE" 231 return 0 232 233 def Run(self): 234 return binary_search_state.Run(**self.default_kwargs) 235 236 def PostRun(self): 237 cmd = self.sysroot_wrapper_cleanup 238 ret, _, _ = self.ce.RunCommandWExceptionCleanup( 239 cmd, print_to_console=True 240 ) 241 if ret: 242 self.logger.LogError( 243 "Object bisector cleanup failed w/ error %d" % ret 244 ) 245 return 1 246 self.logger.LogOutput( 247 ( 248 "Cleanup successful! To restore the bisection " 249 "environment run the following:\n" 250 " cd %s; %s" 251 ) 252 % (os.getcwd(), self.setup_cmd) 253 ) 254 return 0 255 256 257class BisectAndroid(Bisector): 258 """The class for Android bisection steps.""" 259 260 android_setup = "android/setup.sh" 261 android_cleanup = "android/cleanup.sh" 262 default_dir = os.path.expanduser("~/ANDROID_BISECT") 263 264 def __init__(self, options, overrides): 265 super(BisectAndroid, self).__init__(options, overrides) 266 self.method_name = "Android" 267 self.default_kwargs = { 268 "get_initial_items": "android/get_initial_items.sh", 269 "switch_to_good": "android/switch_to_good.sh", 270 "switch_to_bad": "android/switch_to_bad.sh", 271 "test_setup_script": "android/test_setup.sh", 272 "test_script": "android/interactive_test.sh", 273 "prune": True, 274 "file_args": True, 275 "noincremental": False, 276 } 277 self.options = options 278 if options.dir: 279 os.environ["BISECT_DIR"] = options.dir 280 self.options.dir = os.environ.get("BISECT_DIR", self.default_dir) 281 282 num_jobs = "NUM_JOBS='%s'" % self.options.num_jobs 283 device_id = "" 284 if self.options.device_id: 285 device_id = "ANDROID_SERIAL='%s'" % self.options.device_id 286 287 self.setup_cmd = " ".join( 288 (num_jobs, device_id, self.android_setup, self.options.android_src) 289 ) 290 291 self.ArgOverride(self.default_kwargs, overrides) 292 293 def PreRun(self): 294 ret, _, _ = self.ce.RunCommandWExceptionCleanup( 295 self.setup_cmd, print_to_console=True 296 ) 297 if ret: 298 self.logger.LogError( 299 "Android bisector setup failed w/ error %d" % ret 300 ) 301 return 1 302 303 os.environ["BISECT_STAGE"] = "TRIAGE" 304 return 0 305 306 def Run(self): 307 return binary_search_state.Run(**self.default_kwargs) 308 309 def PostRun(self): 310 cmd = self.android_cleanup 311 ret, _, _ = self.ce.RunCommandWExceptionCleanup( 312 cmd, print_to_console=True 313 ) 314 if ret: 315 self.logger.LogError( 316 "Android bisector cleanup failed w/ error %d" % ret 317 ) 318 return 1 319 self.logger.LogOutput( 320 ( 321 "Cleanup successful! To restore the bisection " 322 "environment run the following:\n" 323 " cd %s; %s" 324 ) 325 % (os.getcwd(), self.setup_cmd) 326 ) 327 return 0 328 329 330def Run(bisector): 331 log = logger.GetLogger() 332 333 log.LogOutput("Setting up Bisection tool") 334 ret = bisector.PreRun() 335 if ret: 336 return ret 337 338 log.LogOutput("Running Bisection tool") 339 ret = bisector.Run() 340 if ret: 341 return ret 342 343 log.LogOutput("Cleaning up Bisection tool") 344 ret = bisector.PostRun() 345 if ret: 346 return ret 347 348 return 0 349 350 351_HELP_EPILOG = """ 352Run ./run_bisect.py {method} --help for individual method help/args 353 354------------------ 355 356See README.bisect for examples on argument overriding 357 358See below for full override argument reference: 359""" 360 361 362def Main(argv): 363 override_parser = argparse.ArgumentParser( 364 add_help=False, 365 argument_default=argparse.SUPPRESS, 366 usage="run_bisect.py {mode} [options]", 367 ) 368 common.BuildArgParser(override_parser, override=True) 369 370 epilog = _HELP_EPILOG + override_parser.format_help() 371 parser = argparse.ArgumentParser( 372 epilog=epilog, formatter_class=RawTextHelpFormatter 373 ) 374 subparsers = parser.add_subparsers( 375 title="Bisect mode", 376 description=( 377 "Which bisection method to " 378 "use. Each method has " 379 "specific setup and " 380 "arguments. Please consult " 381 "the README for more " 382 "information." 383 ), 384 ) 385 386 parser_package = subparsers.add_parser("package") 387 parser_package.add_argument("board", help="Board to target") 388 parser_package.add_argument("remote", help="Remote machine to test on") 389 parser_package.set_defaults(handler=BisectPackage) 390 391 parser_object = subparsers.add_parser("object") 392 parser_object.add_argument("board", help="Board to target") 393 parser_object.add_argument("remote", help="Remote machine to test on") 394 parser_object.add_argument("package", help="Package to emerge and test") 395 parser_object.add_argument( 396 "--use_flags", 397 required=False, 398 default="", 399 help="Use flags passed to emerge", 400 ) 401 parser_object.add_argument( 402 "--noreboot", 403 action="store_false", 404 dest="reboot", 405 help="Do not reboot after updating the package (default: False)", 406 ) 407 parser_object.add_argument( 408 "--dir", 409 help=( 410 "Bisection directory to use, sets " 411 "$BISECT_DIR if provided. Defaults to " 412 "current value of $BISECT_DIR (or " 413 "/tmp/sysroot_bisect if $BISECT_DIR is " 414 "empty)." 415 ), 416 ) 417 parser_object.set_defaults(handler=BisectObject) 418 419 parser_android = subparsers.add_parser("android") 420 parser_android.add_argument( 421 "android_src", help="Path to android source tree" 422 ) 423 parser_android.add_argument( 424 "--dir", 425 help=( 426 "Bisection directory to use, sets " 427 "$BISECT_DIR if provided. Defaults to " 428 "current value of $BISECT_DIR (or " 429 "~/ANDROID_BISECT/ if $BISECT_DIR is " 430 "empty)." 431 ), 432 ) 433 parser_android.add_argument( 434 "-j", 435 "--num_jobs", 436 type=int, 437 default=1, 438 help=( 439 "Number of jobs that make and various " 440 "scripts for bisector can spawn. Setting " 441 "this value too high can freeze up your " 442 "machine!" 443 ), 444 ) 445 parser_android.add_argument( 446 "--device_id", 447 default="", 448 help=( 449 "Device id for device used for testing. " 450 "Use this if you have multiple Android " 451 "devices plugged into your machine." 452 ), 453 ) 454 parser_android.set_defaults(handler=BisectAndroid) 455 456 options, remaining = parser.parse_known_args(argv) 457 if remaining: 458 overrides = override_parser.parse_args(remaining) 459 overrides = vars(overrides) 460 else: 461 overrides = {} 462 463 subcmd = options.handler 464 del options.handler 465 466 bisector = subcmd(options, overrides) 467 return Run(bisector) 468 469 470if __name__ == "__main__": 471 os.chdir(os.path.dirname(__file__)) 472 sys.exit(Main(sys.argv[1:])) 473