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 binary search wrapper.""" 8 9from __future__ import division 10from __future__ import print_function 11 12import argparse 13import contextlib 14import errno 15import math 16import os 17import pickle 18import re 19import shutil 20import sys 21import tempfile 22import time 23 24# Adds cros_utils to PYTHONPATH 25from binary_search_tool import binary_search_perforce 26from binary_search_tool import common 27from binary_search_tool import pass_mapping 28 29# Now we do import from cros_utils 30from cros_utils import command_executer 31from cros_utils import logger 32 33GOOD_SET_VAR = 'BISECT_GOOD_SET' 34BAD_SET_VAR = 'BISECT_BAD_SET' 35 36STATE_FILE = '%s.state' % sys.argv[0] 37HIDDEN_STATE_FILE = os.path.join( 38 os.path.dirname(STATE_FILE), '.%s' % os.path.basename(STATE_FILE)) 39 40 41@contextlib.contextmanager 42def SetFile(env_var, items): 43 """Generate set files that can be used by switch/test scripts. 44 45 Generate temporary set file (good/bad) holding contents of good/bad items for 46 the current binary search iteration. Store the name of each file as an 47 environment variable so all child processes can access it. 48 49 This function is a contextmanager, meaning it's meant to be used with the 50 "with" statement in Python. This is so cleanup and setup happens automatically 51 and cleanly. Execution of the outer "with" statement happens at the "yield" 52 statement. 53 54 Args: 55 env_var: What environment variable to store the file name in. 56 items: What items are in this set. 57 """ 58 with tempfile.NamedTemporaryFile('w', encoding='utf-8') as f: 59 os.environ[env_var] = f.name 60 f.write('\n'.join(items)) 61 f.flush() 62 yield 63 64 65class BinarySearchState(object): 66 """The binary search state class.""" 67 68 def __init__(self, get_initial_items, switch_to_good, switch_to_bad, 69 test_setup_script, test_script, incremental, prune, pass_bisect, 70 ir_diff, iterations, prune_iterations, verify, file_args, 71 verbose): 72 """BinarySearchState constructor, see Run for full args documentation.""" 73 self.get_initial_items = get_initial_items 74 self.switch_to_good = switch_to_good 75 self.switch_to_bad = switch_to_bad 76 self.test_setup_script = test_setup_script 77 self.test_script = test_script 78 self.incremental = incremental 79 self.prune = prune 80 self.pass_bisect = pass_bisect 81 self.ir_diff = ir_diff 82 self.iterations = iterations 83 self.prune_iterations = prune_iterations 84 self.verify = verify 85 self.file_args = file_args 86 self.verbose = verbose 87 88 self.l = logger.GetLogger() 89 self.ce = command_executer.GetCommandExecuter() 90 91 self.resumed = False 92 self.prune_cycles = 0 93 self.search_cycles = 0 94 self.binary_search = None 95 self.all_items = None 96 self.cmd_script = None 97 self.mode = None 98 self.PopulateItemsUsingCommand(self.get_initial_items) 99 self.currently_good_items = set() 100 self.currently_bad_items = set() 101 self.found_items = set() 102 self.known_good = set() 103 104 self.start_time = time.time() 105 106 def SwitchToGood(self, item_list): 107 """Switch given items to "good" set.""" 108 if self.incremental: 109 self.l.LogOutput( 110 'Incremental set. Wanted to switch %s to good' % str(item_list), 111 print_to_console=self.verbose) 112 incremental_items = [ 113 item for item in item_list if item not in self.currently_good_items 114 ] 115 item_list = incremental_items 116 self.l.LogOutput( 117 'Incremental set. Actually switching %s to good' % str(item_list), 118 print_to_console=self.verbose) 119 120 if not item_list: 121 return 122 123 self.l.LogOutput( 124 'Switching %s to good' % str(item_list), print_to_console=self.verbose) 125 self.RunSwitchScript(self.switch_to_good, item_list) 126 self.currently_good_items = self.currently_good_items.union(set(item_list)) 127 self.currently_bad_items.difference_update(set(item_list)) 128 129 def SwitchToBad(self, item_list): 130 """Switch given items to "bad" set.""" 131 if self.incremental: 132 self.l.LogOutput( 133 'Incremental set. Wanted to switch %s to bad' % str(item_list), 134 print_to_console=self.verbose) 135 incremental_items = [ 136 item for item in item_list if item not in self.currently_bad_items 137 ] 138 item_list = incremental_items 139 self.l.LogOutput( 140 'Incremental set. Actually switching %s to bad' % str(item_list), 141 print_to_console=self.verbose) 142 143 if not item_list: 144 return 145 146 self.l.LogOutput( 147 'Switching %s to bad' % str(item_list), print_to_console=self.verbose) 148 self.RunSwitchScript(self.switch_to_bad, item_list) 149 self.currently_bad_items = self.currently_bad_items.union(set(item_list)) 150 self.currently_good_items.difference_update(set(item_list)) 151 152 def RunSwitchScript(self, switch_script, item_list): 153 """Pass given items to switch script. 154 155 Args: 156 switch_script: path to switch script 157 item_list: list of all items to be switched 158 """ 159 if self.file_args: 160 with tempfile.NamedTemporaryFile('w', encoding='utf-8') as f: 161 f.write('\n'.join(item_list)) 162 f.flush() 163 command = '%s %s' % (switch_script, f.name) 164 ret, _, _ = self.ce.RunCommandWExceptionCleanup( 165 command, print_to_console=self.verbose) 166 else: 167 command = '%s %s' % (switch_script, ' '.join(item_list)) 168 try: 169 ret, _, _ = self.ce.RunCommandWExceptionCleanup( 170 command, print_to_console=self.verbose) 171 except OSError as e: 172 if e.errno == errno.E2BIG: 173 raise RuntimeError('Too many arguments for switch script! Use ' 174 '--file_args') 175 assert ret == 0, 'Switch script %s returned %d' % (switch_script, ret) 176 177 def TestScript(self): 178 """Run test script and return exit code from script.""" 179 command = self.test_script 180 ret, _, _ = self.ce.RunCommandWExceptionCleanup(command) 181 return ret 182 183 def TestSetupScript(self): 184 """Run test setup script and return exit code from script.""" 185 if not self.test_setup_script: 186 return 0 187 188 command = self.test_setup_script 189 ret, _, _ = self.ce.RunCommandWExceptionCleanup(command) 190 return ret 191 192 def GenerateBadCommandScript(self, bad_items): 193 """Generate command line script for building bad item.""" 194 assert not self.prune, 'Prune must be false if pass_bisect is set.' 195 assert len(bad_items) == 1, 'Pruning is off, but number of bad ' \ 196 'items found was not 1.' 197 item = list(bad_items)[0] 198 command = '%s %s' % (self.pass_bisect, item) 199 ret, _, _ = self.ce.RunCommandWExceptionCleanup( 200 command, print_to_console=self.verbose) 201 return ret 202 203 def DoVerify(self): 204 """Verify correctness of test environment. 205 206 Verify that a "good" set of items produces a "good" result and that a "bad" 207 set of items produces a "bad" result. To be run directly before running 208 DoSearch. If verify is False this step is skipped. 209 """ 210 if not self.verify: 211 return 212 213 self.l.LogOutput('VERIFICATION') 214 self.l.LogOutput('Beginning tests to verify good/bad sets\n') 215 216 self._OutputProgress('Verifying items from GOOD set\n') 217 with SetFile(GOOD_SET_VAR, self.all_items), SetFile(BAD_SET_VAR, []): 218 self.l.LogOutput('Resetting all items to good to verify.') 219 self.SwitchToGood(self.all_items) 220 status = self.TestSetupScript() 221 assert status == 0, 'When reset_to_good, test setup should succeed.' 222 status = self.TestScript() 223 assert status == 0, 'When reset_to_good, status should be 0.' 224 225 self._OutputProgress('Verifying items from BAD set\n') 226 with SetFile(GOOD_SET_VAR, []), SetFile(BAD_SET_VAR, self.all_items): 227 self.l.LogOutput('Resetting all items to bad to verify.') 228 self.SwitchToBad(self.all_items) 229 status = self.TestSetupScript() 230 # The following assumption is not true; a bad image might not 231 # successfully push onto a device. 232 # assert status == 0, 'When reset_to_bad, test setup should succeed.' 233 if status == 0: 234 status = self.TestScript() 235 assert status == 1, 'When reset_to_bad, status should be 1.' 236 237 def DoSearchBadItems(self): 238 """Perform full search for bad items. 239 240 Perform full search until prune_iterations number of bad items are found. 241 """ 242 while (True and len(self.all_items) > 1 and 243 self.prune_cycles < self.prune_iterations): 244 terminated = self.DoBinarySearchBadItems() 245 self.prune_cycles += 1 246 if not terminated: 247 break 248 # Prune is set. 249 prune_index = self.binary_search.current 250 251 # If found item is last item, no new items can be found 252 if prune_index == len(self.all_items) - 1: 253 self.l.LogOutput('First bad item is the last item. Breaking.') 254 self.l.LogOutput('Bad items are: %s' % self.all_items[-1]) 255 self.found_items.add(self.all_items[-1]) 256 break 257 258 # If already seen item we have no new bad items to find, finish up 259 if self.all_items[prune_index] in self.found_items: 260 self.l.LogOutput( 261 'Found item already found before: %s.' % 262 self.all_items[prune_index], 263 print_to_console=self.verbose) 264 self.l.LogOutput('No more bad items remaining. Done searching.') 265 self.l.LogOutput('Bad items are: %s' % ' '.join(self.found_items)) 266 break 267 268 new_all_items = list(self.all_items) 269 # Move prune item to the end of the list. 270 new_all_items.append(new_all_items.pop(prune_index)) 271 self.found_items.add(new_all_items[-1]) 272 273 # Everything below newly found bad item is now known to be a good item. 274 # Take these good items out of the equation to save time on the next 275 # search. We save these known good items so they are still sent to the 276 # switch_to_good script. 277 if prune_index: 278 self.known_good.update(new_all_items[:prune_index]) 279 new_all_items = new_all_items[prune_index:] 280 281 self.l.LogOutput( 282 'Old list: %s. New list: %s' % (str(self.all_items), 283 str(new_all_items)), 284 print_to_console=self.verbose) 285 286 if not self.prune: 287 self.l.LogOutput('Not continuning further, --prune is not set') 288 break 289 # FIXME: Do we need to Convert the currently good items to bad 290 self.PopulateItemsUsingList(new_all_items) 291 292 # If pass level bisecting is set, generate a script which contains command 293 # line options to rebuild bad item. 294 if self.pass_bisect: 295 status = self.GenerateBadCommandScript(self.found_items) 296 if status == 0: 297 self.cmd_script = os.path.join( 298 os.path.dirname(self.pass_bisect), 'cmd_script.sh') 299 self.l.LogOutput('Command script generated at %s.' % self.cmd_script) 300 else: 301 raise RuntimeError('Error while generating command script.') 302 303 def DoBinarySearchBadItems(self): 304 """Perform single iteration of binary search.""" 305 # If in resume mode don't reset search_cycles 306 if not self.resumed: 307 self.search_cycles = 0 308 else: 309 self.resumed = False 310 311 terminated = False 312 while self.search_cycles < self.iterations and not terminated: 313 self.SaveState() 314 self.OutputIterationProgressBadItem() 315 316 self.search_cycles += 1 317 [bad_items, good_items] = self.GetNextItems() 318 319 with SetFile(GOOD_SET_VAR, good_items), SetFile(BAD_SET_VAR, bad_items): 320 # TODO: bad_items should come first. 321 self.SwitchToGood(good_items) 322 self.SwitchToBad(bad_items) 323 status = self.TestSetupScript() 324 if status == 0: 325 status = self.TestScript() 326 terminated = self.binary_search.SetStatus(status) 327 328 if terminated: 329 self.l.LogOutput('Terminated!', print_to_console=self.verbose) 330 if not terminated: 331 self.l.LogOutput('Ran out of iterations searching...') 332 self.l.LogOutput(str(self), print_to_console=self.verbose) 333 return terminated 334 335 def CollectPassName(self, pass_info): 336 """Mapping opt-bisect output of pass info to debugcounter name.""" 337 self.l.LogOutput('Pass info: %s' % pass_info, print_to_console=self.verbose) 338 339 for desc in pass_mapping.pass_name: 340 if desc in pass_info: 341 return pass_mapping.pass_name[desc] 342 343 # If pass not found, return None 344 return None 345 346 def BuildWithPassLimit(self, limit, generate_ir=False): 347 """Rebuild bad item with pass level bisect limit 348 349 Run command line script generated by GenerateBadCommandScript(), with 350 pass level limit flags. 351 352 Returns: 353 pass_num: current number of the pass, or total number of passes if 354 limit set to -1. 355 pass_name: The debugcounter name of current limit pass. 356 """ 357 os.environ['LIMIT_FLAGS'] = '-mllvm -opt-bisect-limit=' + str(limit) 358 if generate_ir: 359 os.environ['LIMIT_FLAGS'] += ' -S -emit-llvm' 360 self.l.LogOutput( 361 'Limit flags: %s' % os.environ['LIMIT_FLAGS'], 362 print_to_console=self.verbose) 363 command = self.cmd_script 364 _, _, msg = self.ce.RunCommandWOutput(command, print_to_console=False) 365 366 # Massages we get will be like this: 367 # BISECT: running pass (9) <Pass Description> on <function> (<file>) 368 # BISECT: running pass (10) <Pass Description> on <module> (<file>) 369 # BISECT: NOT running pass (11) <Pass Description> on <SCG> (<file>) 370 # BISECT: NOT running pass (12) <Pass Description> on <SCG> (<file>) 371 # We want to get the pass description of last running pass, to have 372 # transformation level bisect on it. 373 if 'BISECT: ' not in msg: 374 raise RuntimeError('No bisect info printed, OptBisect may not be ' 375 'supported by the compiler.') 376 377 lines = msg.split('\n') 378 pass_num = 0 379 last_pass = '' 380 for l in lines: 381 if 'running pass' in l: 382 # For situation of limit==-1, we want the total number of passes 383 if limit != -1 and 'BISECT: NOT ' in l: 384 break 385 pass_num += 1 386 last_pass = l 387 if limit not in (-1, pass_num): 388 raise ValueError('[Error] While building, limit number does not match.') 389 return pass_num, self.CollectPassName(last_pass) 390 391 def BuildWithTransformLimit(self, 392 limit, 393 pass_name=None, 394 pass_limit=-1, 395 generate_ir=False): 396 """Rebuild bad item with transformation level bisect limit 397 398 Run command line script generated by GenerateBadCommandScript(), with 399 pass level limit flags and transformation level limit flags. 400 401 Args: 402 limit: transformation level limit for bad item. 403 pass_name: name of bad pass debugcounter from pass level bisect result. 404 pass_limit: pass level limit from pass level bisect result. 405 generate_ir: Whether to generate IR comparison. 406 407 Returns: 408 Total number of transformations if limit set to -1, else return 0. 409 """ 410 counter_name = pass_name 411 412 os.environ['LIMIT_FLAGS'] = '-mllvm -opt-bisect-limit=' + \ 413 str(pass_limit) + \ 414 ' -mllvm -debug-counter=' + counter_name + \ 415 '-count=' + str(limit) + \ 416 ' -mllvm -print-debug-counter' 417 if generate_ir: 418 os.environ['LIMIT_FLAGS'] += ' -S -emit-llvm' 419 self.l.LogOutput( 420 'Limit flags: %s' % os.environ['LIMIT_FLAGS'], 421 print_to_console=self.verbose) 422 command = self.cmd_script 423 _, _, msg = self.ce.RunCommandWOutput(command, print_to_console=False) 424 425 if 'Counters and values:' not in msg: 426 # Print pass level IR diff only if transformation level bisection does 427 # not work. 428 if self.ir_diff: 429 self.PrintIRDiff(pass_limit) 430 raise RuntimeError('No bisect info printed, DebugCounter may not be ' 431 'supported by the compiler.') 432 433 # With debugcounter enabled, there will be DebugCounter counting info in 434 # the output. 435 lines = msg.split('\n') 436 for l in lines: 437 if pass_name in l: 438 # Output of debugcounter will be like: 439 # instcombine-visit: {10, 0, 20} 440 # dce-transform: {1, 0, -1} 441 # which indicates {Count, Skip, StopAfter}. 442 # The last number should be the limit we set. 443 # We want the first number as the total transformation count. 444 # Split each line by ,|{|} and we can get l_list as: 445 # ['instcombine: ', '10', '0', '20', ''] 446 # and we will need the second item in it. 447 l_list = re.split(',|{|}', l) 448 count = int(l_list[1]) 449 if limit == -1: 450 return count 451 # The returned value is only useful when limit == -1, which shows total 452 # transformation count. 453 return 0 454 455 def PrintIRDiff(self, pass_index, pass_name=None, trans_index=-1): 456 bad_item = list(self.found_items)[0] 457 self.l.LogOutput( 458 'IR difference before and after bad pass/transformation:', 459 print_to_console=self.verbose) 460 461 if trans_index == -1: 462 # Pass level IR diff 463 self.BuildWithPassLimit(pass_index, self.ir_diff) 464 good_ir = os.path.join(tempfile.tempdir, 'good.s') 465 shutil.copyfile(bad_item, good_ir) 466 pass_index += 1 467 self.BuildWithPassLimit(pass_index, self.ir_diff) 468 else: 469 # Transformation level IR diff 470 self.BuildWithTransformLimit(trans_index, pass_name, pass_index, 471 self.ir_diff) 472 good_ir = os.path.join(tempfile.tempdir, 'good.s') 473 shutil.copyfile(bad_item, good_ir) 474 trans_index += 1 475 self.BuildWithTransformLimit(trans_index, pass_name, pass_index, 476 self.ir_diff) 477 478 bad_ir = os.path.join(tempfile.tempdir, 'bad.s') 479 shutil.copyfile(bad_item, bad_ir) 480 481 command = 'diff %s %s' % (good_ir, bad_ir) 482 _, _, _ = self.ce.RunCommandWOutput(command, print_to_console=self.verbose) 483 484 def DoSearchBadPass(self): 485 """Perform full search for bad pass of bad item.""" 486 logger.GetLogger().LogOutput('Starting to bisect bad pass for bad item.') 487 488 # Pass level bisection 489 self.mode = 'pass' 490 self.binary_search = binary_search_perforce.BinarySearcherForPass( 491 logger_to_set=self.l) 492 self.binary_search.total, _ = self.BuildWithPassLimit(-1) 493 logger.GetLogger().LogOutput( 494 'Total %s number: %d' % (self.mode, self.binary_search.total)) 495 496 pass_index, pass_name = self.DoBinarySearchBadPass() 497 498 if (not pass_name and pass_index == 0): 499 raise ValueError('Bisecting passes cannot reproduce good result.') 500 logger.GetLogger().LogOutput('Bad pass found: %s.' % pass_name) 501 502 # Transformation level bisection. 503 logger.GetLogger().LogOutput('Starting to bisect at transformation level.') 504 505 self.mode = 'transform' 506 self.binary_search = binary_search_perforce.BinarySearcherForPass( 507 logger_to_set=self.l) 508 self.binary_search.total = self.BuildWithTransformLimit( 509 -1, pass_name, pass_index) 510 logger.GetLogger().LogOutput( 511 'Total %s number: %d' % (self.mode, self.binary_search.total)) 512 513 trans_index, _ = self.DoBinarySearchBadPass(pass_index, pass_name) 514 if trans_index == 0: 515 raise ValueError('Bisecting %s cannot reproduce good result.' % pass_name) 516 517 if self.ir_diff: 518 self.PrintIRDiff(pass_index, pass_name, trans_index) 519 520 logger.GetLogger().LogOutput( 521 'Bisection result for bad item %s:\n' 522 'Bad pass: %s at number %d\n' 523 'Bad transformation number: %d' % (self.found_items, pass_name, 524 pass_index, trans_index)) 525 526 def DoBinarySearchBadPass(self, pass_index=-1, pass_name=None): 527 """Perform single iteration of binary search at pass level 528 529 Args: 530 pass_index: Works for transformation level bisection, indicates the limit 531 number of pass from pass level bisecting result. 532 pass_name: Works for transformation level bisection, indicates 533 DebugCounter name of the bad pass from pass level bisecting 534 result. 535 536 Returns: 537 index: Index of problematic pass/transformation. 538 pass_name: Works for pass level bisection, returns DebugCounter name for 539 bad pass. 540 """ 541 # If in resume mode don't reset search_cycles 542 if not self.resumed: 543 self.search_cycles = 0 544 else: 545 self.resumed = False 546 547 terminated = False 548 index = 0 549 while self.search_cycles < self.iterations and not terminated: 550 self.SaveState() 551 self.OutputIterationProgressBadPass() 552 553 self.search_cycles += 1 554 current = self.binary_search.GetNext() 555 556 if self.mode == 'pass': 557 index, pass_name = self.BuildWithPassLimit(current) 558 else: 559 self.BuildWithTransformLimit(current, pass_name, pass_index) 560 index = current 561 562 # TODO: Newly generated object should not directly replace original 563 # one, need to put it somewhere and symbol link original one to it. 564 # Will update cmd_script to do it. 565 566 status = self.TestSetupScript() 567 assert status == 0, 'Test setup should succeed.' 568 status = self.TestScript() 569 terminated = self.binary_search.SetStatus(status) 570 571 if terminated: 572 self.l.LogOutput('Terminated!', print_to_console=self.verbose) 573 if not terminated: 574 self.l.LogOutput('Ran out of iterations searching...') 575 self.l.LogOutput(str(self), print_to_console=self.verbose) 576 return index, pass_name 577 578 def PopulateItemsUsingCommand(self, command): 579 """Update all_items and binary search logic from executable. 580 581 This method is mainly required for enumerating the initial list of items 582 from the get_initial_items script. 583 584 Args: 585 command: path to executable that will enumerate items. 586 """ 587 ce = command_executer.GetCommandExecuter() 588 _, out, _ = ce.RunCommandWExceptionCleanup( 589 command, return_output=True, print_to_console=self.verbose) 590 all_items = out.split() 591 self.PopulateItemsUsingList(all_items) 592 593 def PopulateItemsUsingList(self, all_items): 594 """Update all_items and binary searching logic from list. 595 596 Args: 597 all_items: new list of all_items 598 """ 599 self.all_items = all_items 600 self.binary_search = binary_search_perforce.BinarySearcher( 601 logger_to_set=self.l) 602 self.binary_search.SetSortedList(self.all_items) 603 604 def SaveState(self): 605 """Save state to STATE_FILE. 606 607 SaveState will create a new unique, hidden state file to hold data from 608 object. Then atomically overwrite the STATE_FILE symlink to point to the 609 new data. 610 611 Raises: 612 OSError if STATE_FILE already exists but is not a symlink. 613 """ 614 ce, l = self.ce, self.l 615 self.ce, self.l, self.binary_search.logger = None, None, None 616 old_state = None 617 618 _, path = tempfile.mkstemp(prefix=HIDDEN_STATE_FILE, dir='.') 619 with open(path, 'wb') as f: 620 pickle.dump(self, f) 621 622 if os.path.exists(STATE_FILE): 623 if os.path.islink(STATE_FILE): 624 old_state = os.readlink(STATE_FILE) 625 else: 626 raise OSError(('%s already exists and is not a symlink!\n' 627 'State file saved to %s' % (STATE_FILE, path))) 628 629 # Create new link and atomically overwrite old link 630 temp_link = '%s.link' % HIDDEN_STATE_FILE 631 os.symlink(path, temp_link) 632 os.rename(temp_link, STATE_FILE) 633 634 if old_state: 635 os.remove(old_state) 636 637 self.ce, self.l, self.binary_search.logger = ce, l, l 638 639 @classmethod 640 def LoadState(cls): 641 """Create BinarySearchState object from STATE_FILE.""" 642 if not os.path.isfile(STATE_FILE): 643 return None 644 try: 645 with open(STATE_FILE, 'rb') as f: 646 bss = pickle.load(f) 647 bss.l = logger.GetLogger() 648 bss.ce = command_executer.GetCommandExecuter() 649 bss.binary_search.logger = bss.l 650 bss.start_time = time.time() 651 652 # Set resumed to be True so we can enter DoBinarySearch without the 653 # method resetting our current search_cycles to 0. 654 bss.resumed = True 655 656 # Set currently_good_items and currently_bad_items to empty so that the 657 # first iteration after resuming will always be non-incremental. This 658 # is just in case the environment changes, the user makes manual 659 # changes, or a previous switch_script corrupted the environment. 660 bss.currently_good_items = set() 661 bss.currently_bad_items = set() 662 663 binary_search_perforce.verbose = bss.verbose 664 return bss 665 except Exception: 666 return None 667 668 def RemoveState(self): 669 """Remove STATE_FILE and its symlinked data from file system.""" 670 if os.path.exists(STATE_FILE): 671 if os.path.islink(STATE_FILE): 672 real_file = os.readlink(STATE_FILE) 673 os.remove(real_file) 674 os.remove(STATE_FILE) 675 676 def GetNextItems(self): 677 """Get next items for binary search based on result of the last test run.""" 678 border_item = self.binary_search.GetNext() 679 index = self.all_items.index(border_item) 680 681 next_bad_items = self.all_items[:index + 1] 682 next_good_items = self.all_items[index + 1:] + list(self.known_good) 683 684 return [next_bad_items, next_good_items] 685 686 def ElapsedTimeString(self): 687 """Return h m s format of elapsed time since execution has started.""" 688 diff = int(time.time() - self.start_time) 689 seconds = diff % 60 690 minutes = (diff // 60) % 60 691 hours = diff // (60 * 60) 692 693 seconds = str(seconds).rjust(2) 694 minutes = str(minutes).rjust(2) 695 hours = str(hours).rjust(2) 696 697 return '%sh %sm %ss' % (hours, minutes, seconds) 698 699 def _OutputProgress(self, progress_text): 700 """Output current progress of binary search to console and logs. 701 702 Args: 703 progress_text: The progress to display to the user. 704 """ 705 progress = ('\n***** PROGRESS (elapsed time: %s) *****\n' 706 '%s' 707 '************************************************') 708 progress = progress % (self.ElapsedTimeString(), progress_text) 709 self.l.LogOutput(progress) 710 711 def OutputIterationProgressBadItem(self): 712 out = ('Search %d of estimated %d.\n' 713 'Prune %d of max %d.\n' 714 'Current bad items found:\n' 715 '%s\n') 716 out = out % (self.search_cycles + 1, 717 math.ceil(math.log(len(self.all_items), 2)), self.prune_cycles 718 + 1, self.prune_iterations, ', '.join(self.found_items)) 719 self._OutputProgress(out) 720 721 def OutputIterationProgressBadPass(self): 722 out = ('Search %d of estimated %d.\n' 'Current limit: %s\n') 723 out = out % (self.search_cycles + 1, 724 math.ceil(math.log(self.binary_search.total, 2)), 725 self.binary_search.current) 726 self._OutputProgress(out) 727 728 def __str__(self): 729 ret = '' 730 ret += 'all: %s\n' % str(self.all_items) 731 ret += 'currently_good: %s\n' % str(self.currently_good_items) 732 ret += 'currently_bad: %s\n' % str(self.currently_bad_items) 733 ret += str(self.binary_search) 734 return ret 735 736 737class MockBinarySearchState(BinarySearchState): 738 """Mock class for BinarySearchState.""" 739 740 def __init__(self, **kwargs): 741 # Initialize all arguments to None 742 default_kwargs = { 743 'get_initial_items': 'echo "1"', 744 'switch_to_good': None, 745 'switch_to_bad': None, 746 'test_setup_script': None, 747 'test_script': None, 748 'incremental': True, 749 'prune': False, 750 'pass_bisect': None, 751 'ir_diff': False, 752 'iterations': 50, 753 'prune_iterations': 100, 754 'verify': True, 755 'file_args': False, 756 'verbose': False 757 } 758 default_kwargs.update(kwargs) 759 super(MockBinarySearchState, self).__init__(**default_kwargs) 760 761 762def _CanonicalizeScript(script_name): 763 """Return canonical path to script. 764 765 Args: 766 script_name: Relative or absolute path to script 767 768 Returns: 769 Canonicalized script path 770 """ 771 script_name = os.path.expanduser(script_name) 772 if not script_name.startswith('/'): 773 return os.path.join('.', script_name) 774 775 776def Run(get_initial_items, 777 switch_to_good, 778 switch_to_bad, 779 test_script, 780 test_setup_script=None, 781 iterations=50, 782 prune=False, 783 pass_bisect=None, 784 ir_diff=False, 785 noincremental=False, 786 file_args=False, 787 verify=True, 788 prune_iterations=100, 789 verbose=False, 790 resume=False): 791 """Run binary search tool. 792 793 Equivalent to running through terminal. 794 795 Args: 796 get_initial_items: Script to enumerate all items being binary searched 797 switch_to_good: Script that will take items as input and switch them to good 798 set 799 switch_to_bad: Script that will take items as input and switch them to bad 800 set 801 test_script: Script that will determine if the current combination of good 802 and bad items make a "good" or "bad" result. 803 test_setup_script: Script to do necessary setup (building, compilation, 804 etc.) for test_script. 805 iterations: How many binary search iterations to run before exiting. 806 prune: If False the binary search tool will stop when the first bad item is 807 found. Otherwise then binary search tool will continue searching until all 808 bad items are found (or prune_iterations is reached). 809 pass_bisect: Script that takes single bad item from POPULATE_BAD and returns 810 the compiler command used to generate the bad item. This will turn on 811 pass/ transformation level bisection for the bad item. Requires that 812 'prune' be set to False, and needs support of `-opt-bisect-limit`(pass) 813 and `-print-debug-counter`(transformation) from LLVM. 814 ir_diff: Whether to print IR differences before and after bad 815 pass/transformation to verbose output. Defaults to False, only works when 816 pass_bisect is enabled. 817 noincremental: Whether to send "diffs" of good/bad items to switch scripts. 818 file_args: If True then arguments to switch scripts will be a file name 819 containing a newline separated list of the items to switch. 820 verify: If True, run tests to ensure initial good/bad sets actually produce 821 a good/bad result. 822 prune_iterations: Max number of bad items to search for. 823 verbose: If True will print extra debug information to user. 824 resume: If True will resume using STATE_FILE. 825 826 Returns: 827 0 for success, error otherwise 828 """ 829 # Notice that all the argument checks are in the Run() function rather than 830 # in the Main() function. It is not common to do so but some wrappers are 831 # going to call Run() directly and bypass checks in Main() function. 832 if resume: 833 logger.GetLogger().LogOutput('Resuming from %s' % STATE_FILE) 834 bss = BinarySearchState.LoadState() 835 if not bss: 836 logger.GetLogger().LogOutput( 837 '%s is not a valid binary_search_tool state file, cannot resume!' % 838 STATE_FILE) 839 return 1 840 logger.GetLogger().LogOutput('Note: resuming from previous state, ' 841 'ignoring given options and loading saved ' 842 'options instead.') 843 else: 844 if not (get_initial_items and switch_to_good and switch_to_bad and 845 test_script): 846 logger.GetLogger().LogOutput('The following options are required: ' 847 '[-i, -g, -b, -t] | [-r]') 848 return 1 849 if pass_bisect and prune: 850 logger.GetLogger().LogOutput('"--pass_bisect" only works when ' 851 '"--prune" is set to be False.') 852 return 1 853 if not pass_bisect and ir_diff: 854 logger.GetLogger().LogOutput('"--ir_diff" only works when ' 855 '"--pass_bisect" is enabled.') 856 857 switch_to_good = _CanonicalizeScript(switch_to_good) 858 switch_to_bad = _CanonicalizeScript(switch_to_bad) 859 if test_setup_script: 860 test_setup_script = _CanonicalizeScript(test_setup_script) 861 if pass_bisect: 862 pass_bisect = _CanonicalizeScript(pass_bisect) 863 test_script = _CanonicalizeScript(test_script) 864 get_initial_items = _CanonicalizeScript(get_initial_items) 865 incremental = not noincremental 866 867 binary_search_perforce.verbose = verbose 868 869 bss = BinarySearchState(get_initial_items, switch_to_good, switch_to_bad, 870 test_setup_script, test_script, incremental, prune, 871 pass_bisect, ir_diff, iterations, prune_iterations, 872 verify, file_args, verbose) 873 bss.DoVerify() 874 875 bss.DoSearchBadItems() 876 if pass_bisect: 877 bss.DoSearchBadPass() 878 bss.RemoveState() 879 logger.GetLogger().LogOutput( 880 'Total execution time: %s' % bss.ElapsedTimeString()) 881 882 return 0 883 884 885def Main(argv): 886 """The main function.""" 887 # Common initializations 888 889 parser = argparse.ArgumentParser() 890 common.BuildArgParser(parser) 891 logger.GetLogger().LogOutput(' '.join(argv)) 892 options = parser.parse_args(argv) 893 894 # Get dictionary of all options 895 args = vars(options) 896 return Run(**args) 897 898 899if __name__ == '__main__': 900 sys.exit(Main(sys.argv[1:])) 901