binary_search_state.py revision 51ba54f3375c17e2a33c1854b635b144802457a4
1#!/usr/bin/python2 2"""The binary search wrapper.""" 3 4from __future__ import print_function 5 6import argparse 7import math 8import os 9import pickle 10import sys 11import tempfile 12 13# Programtically adding utils python path to PYTHONPATH 14if os.path.isabs(sys.argv[0]): 15 utils_pythonpath = os.path.abspath('{0}/..'.format(os.path.dirname(sys.argv[ 16 0]))) 17else: 18 wdir = os.getcwd() 19 utils_pythonpath = os.path.abspath('{0}/{1}/..'.format(wdir, os.path.dirname( 20 sys.argv[0]))) 21sys.path.append(utils_pythonpath) 22# Now we do import from utils 23from utils import command_executer 24from utils import logger 25 26import binary_search_perforce 27 28STATE_FILE = '%s.state' % sys.argv[0] 29HIDDEN_STATE_FILE = os.path.join( 30 os.path.dirname(STATE_FILE), '.%s' % os.path.basename(STATE_FILE)) 31 32 33class BinarySearchState(object): 34 """The binary search state class.""" 35 36 def __init__(self, get_initial_items, switch_to_good, switch_to_bad, 37 install_script, test_script, incremental, prune, iterations, 38 prune_iterations, verify_level, file_args, verbose): 39 self.get_initial_items = get_initial_items 40 self.switch_to_good = switch_to_good 41 self.switch_to_bad = switch_to_bad 42 self.install_script = install_script 43 self.test_script = test_script 44 self.incremental = incremental 45 self.prune = prune 46 self.iterations = iterations 47 self.prune_iterations = prune_iterations 48 self.verify_level = verify_level 49 self.file_args = file_args 50 self.verbose = verbose 51 52 self.l = logger.GetLogger() 53 self.ce = command_executer.GetCommandExecuter() 54 55 self.prune_cycles = 0 56 self.search_cycles = 0 57 self.bs = None 58 self.all_items = None 59 self.PopulateItemsUsingCommand(self.get_initial_items) 60 self.currently_good_items = set([]) 61 self.currently_bad_items = set([]) 62 self.found_items = set([]) 63 64 def SwitchToGood(self, item_list): 65 if self.incremental: 66 self.l.LogOutput('Incremental set. Wanted to switch %s to good' % 67 str(item_list), print_to_console=self.verbose) 68 incremental_items = [ 69 item for item in item_list if item not in self.currently_good_items 70 ] 71 item_list = incremental_items 72 self.l.LogOutput('Incremental set. Actually switching %s to good' % 73 str(item_list), print_to_console=self.verbose) 74 75 if not item_list: 76 return 77 78 self.l.LogOutput('Switching %s to good' % str(item_list), 79 print_to_console=self.verbose) 80 self.RunSwitchScript(self.switch_to_good, item_list) 81 self.currently_good_items = self.currently_good_items.union(set(item_list)) 82 self.currently_bad_items.difference_update(set(item_list)) 83 84 def SwitchToBad(self, item_list): 85 if self.incremental: 86 self.l.LogOutput('Incremental set. Wanted to switch %s to bad' % 87 str(item_list), print_to_console=self.verbose) 88 incremental_items = [ 89 item for item in item_list if item not in self.currently_bad_items 90 ] 91 item_list = incremental_items 92 self.l.LogOutput('Incremental set. Actually switching %s to bad' % 93 str(item_list), print_to_console=self.verbose) 94 95 if not item_list: 96 return 97 98 self.l.LogOutput('Switching %s to bad' % str(item_list), 99 print_to_console=self.verbose) 100 self.RunSwitchScript(self.switch_to_bad, item_list) 101 self.currently_bad_items = self.currently_bad_items.union(set(item_list)) 102 self.currently_good_items.difference_update(set(item_list)) 103 104 def RunSwitchScript(self, switch_script, item_list): 105 if self.file_args: 106 temp_file = tempfile.mktemp() 107 f = open(temp_file, 'wb') 108 f.write('\n'.join(item_list)) 109 f.close() 110 command = '%s %s' % (switch_script, temp_file) 111 else: 112 command = '%s %s' % (switch_script, ' '.join(item_list)) 113 ret, _, _ = self.ce.RunCommandWExceptionCleanup( 114 command, print_to_console=self.verbose) 115 assert ret == 0, 'Switch script %s returned %d' % (switch_script, ret) 116 117 def TestScript(self): 118 command = self.test_script 119 ret, _, _ = self.ce.RunCommandWExceptionCleanup(command) 120 return ret 121 122 def InstallScript(self): 123 if not self.install_script: 124 return 0 125 126 command = self.install_script 127 ret, _, _ = self.ce.RunCommandWExceptionCleanup(command) 128 return ret 129 130 def DoVerify(self): 131 self.l.LogOutput('VERIFICATION') 132 self.l.LogOutput('Beginning %d tests to verify good/bad sets\n') 133 for _ in range(int(self.verify_level)): 134 self.l.LogOutput('Resetting all items to good to verify.') 135 self.SwitchToGood(self.all_items) 136 status = self.InstallScript() 137 assert status == 0, 'When reset_to_good, install should succeed.' 138 status = self.TestScript() 139 assert status == 0, 'When reset_to_good, status should be 0.' 140 141 self.l.LogOutput('Resetting all items to bad to verify.') 142 self.SwitchToBad(self.all_items) 143 status = self.InstallScript() 144 assert status == 0, 'When reset_to_bad, install should succeed.' 145 status = self.TestScript() 146 assert status == 1, 'When reset_to_bad, status should be 1.' 147 148 def DoSearch(self): 149 num_bad_items_history = [] 150 self.prune_cycles = 0 151 while (True and 152 len(self.all_items) > 1 and 153 self.prune_cycles < self.prune_iterations): 154 self.prune_cycles += 1 155 terminated = self.DoBinarySearch() 156 if not terminated: 157 break 158 if not self.prune: 159 self.l.LogOutput('Not continuning further, --prune is not set') 160 break 161 # Prune is set. 162 prune_index = self.bs.current 163 164 if prune_index == len(self.all_items) - 1: 165 self.l.LogOutput('First bad item is the last item. Breaking.') 166 self.l.LogOutput('Bad items are: %s' % self.all_items[-1]) 167 break 168 169 num_bad_items = len(self.all_items) - prune_index 170 num_bad_items_history.append(num_bad_items) 171 172 if (num_bad_items_history[-num_bad_items:] == 173 [num_bad_items for _ in range(num_bad_items)]): 174 self.l.LogOutput('num_bad_items_history: %s for past %d iterations. ' 175 'Breaking.' % (str(num_bad_items_history), 176 num_bad_items)) 177 self.l.LogOutput('Bad items are: %s' % 178 ' '.join(self.all_items[prune_index:])) 179 break 180 181 new_all_items = list(self.all_items) 182 # Move prune item to the end of the list. 183 new_all_items.append(new_all_items.pop(prune_index)) 184 self.found_items.add(new_all_items[-1]) 185 186 if prune_index: 187 new_all_items = new_all_items[prune_index - 1:] 188 189 self.l.LogOutput('Old list: %s. New list: %s' % (str(self.all_items), 190 str(new_all_items)), 191 print_to_console=self.verbose) 192 193 # FIXME: Do we need to Convert the currently good items to bad 194 self.PopulateItemsUsingList(new_all_items) 195 196 def DoBinarySearch(self): 197 self.search_cycles = 0 198 terminated = False 199 while self.search_cycles < self.iterations and not terminated: 200 self.OutputProgress() 201 self.search_cycles += 1 202 [bad_items, good_items] = self.GetNextItems() 203 204 # TODO: bad_items should come first. 205 self.SwitchToGood(good_items) 206 self.SwitchToBad(bad_items) 207 status = self.InstallScript() 208 if status == 0: 209 status = self.TestScript() 210 else: 211 # Install script failed, treat as skipped item 212 status = 2 213 terminated = self.bs.SetStatus(status) 214 215 if terminated: 216 self.l.LogOutput('Terminated!') 217 if not terminated: 218 self.l.LogOutput('Ran out of iterations searching...') 219 self.l.LogOutput(str(self), print_to_console=self.verbose) 220 return terminated 221 222 def PopulateItemsUsingCommand(self, command): 223 ce = command_executer.GetCommandExecuter() 224 _, out, _ = ce.RunCommandWExceptionCleanup(command, 225 return_output=True, 226 print_to_console=self.verbose) 227 all_items = out.split() 228 self.PopulateItemsUsingList(all_items) 229 230 def PopulateItemsUsingList(self, all_items): 231 self.all_items = all_items 232 self.bs = binary_search_perforce.BinarySearcher(logger_to_set=self.l) 233 self.bs.SetSortedList(self.all_items) 234 235 def SaveState(self): 236 ce, l, bs = self.ce, self.l, self.bs 237 self.ce, self.l, self.bs = None, None, None 238 old_state = None 239 240 _, path = tempfile.mkstemp(prefix=HIDDEN_STATE_FILE, dir='.') 241 with open(path, 'wb') as f: 242 pickle.dump(self, f) 243 244 if os.path.exists(STATE_FILE): 245 if os.path.islink(STATE_FILE): 246 old_state = os.readlink(STATE_FILE) 247 else: 248 l.LogError(('%s already exists and is not a symlink!\n' 249 'State file saved to %s' % (STATE_FILE, path))) 250 sys.exit(1) 251 252 # Create new link and atomically overwrite old link 253 temp_link = '%s.link' % HIDDEN_STATE_FILE 254 os.symlink(path, temp_link) 255 os.rename(temp_link, STATE_FILE) 256 257 if old_state: 258 os.remove(old_state) 259 260 self.ce, self.l, self.bs = ce, l, bs 261 262 @classmethod 263 def LoadState(cls): 264 if not os.path.isfile(STATE_FILE): 265 return None 266 return pickle.load(file(STATE_FILE)) 267 268 def GetNextItems(self): 269 border_item = self.bs.GetNext() 270 index = self.all_items.index(border_item) 271 272 next_bad_items = self.all_items[:index + 1] 273 next_good_items = self.all_items[index + 1:] 274 275 return [next_bad_items, next_good_items] 276 277 def OutputProgress(self): 278 out = ('\n********** PROGRESS **********\n' 279 'Search %d of estimated %d.\n' 280 'Prune %d of max %d.\n' 281 'Current bad items found:\n' 282 '%s\n' 283 '******************************') 284 out = out % (self.search_cycles + 1, 285 math.ceil(math.log(len(self.all_items), 2)), 286 self.prune_cycles, 287 self.prune_iterations, 288 str(self.found_items)) 289 290 self.l.LogOutput(out) 291 292 def __str__(self): 293 ret = '' 294 ret += 'all: %s\n' % str(self.all_items) 295 ret += 'currently_good: %s\n' % str(self.currently_good_items) 296 ret += 'currently_bad: %s\n' % str(self.currently_bad_items) 297 ret += str(self.bs) 298 return ret 299 300 301class MockBinarySearchState(BinarySearchState): 302 """Mock class for BinarySearchState.""" 303 304 def __init__(self): 305 # Initialize all arguments to None 306 kwargs = { 307 'get_initial_items': 'echo "1"', 308 'switch_to_good': None, 309 'switch_to_bad': None, 310 'install_script': None, 311 'test_script': None, 312 'incremental': None, 313 'prune': None, 314 'iterations': None, 315 'prune_iterations': None, 316 'verify_level': None, 317 'file_args': None, 318 'verbose': None 319 } 320 super(MockBinarySearchState, self).__init__(**kwargs) 321 322 323def _CanonicalizeScript(script_name): 324 script_name = os.path.expanduser(script_name) 325 if not script_name.startswith('/'): 326 return os.path.join('.', script_name) 327 328 329def Main(argv): 330 """The main function.""" 331 # Common initializations 332 333 parser = argparse.ArgumentParser() 334 parser.add_argument('-n', 335 '--iterations', 336 dest='iterations', 337 help='Number of iterations to try in the search.', 338 default=50) 339 parser.add_argument('-i', 340 '--get_initial_items', 341 dest='get_initial_items', 342 help=('Script to run to get the initial objects. ' 343 'If your script requires user input ' 344 'the --verbose option must be used')) 345 parser.add_argument('-g', 346 '--switch_to_good', 347 dest='switch_to_good', 348 help=('Script to run to switch to good. ' 349 'If your switch script requires user input ' 350 'the --verbose option must be used')) 351 parser.add_argument('-b', 352 '--switch_to_bad', 353 dest='switch_to_bad', 354 help=('Script to run to switch to bad. ' 355 'If your switch script requires user input ' 356 'the --verbose option must be used')) 357 parser.add_argument('-I', 358 '--install_script', 359 dest='install_script', 360 default=None, 361 help=('Optional script to perform building, flashing, ' 362 'and other setup before the test script runs.')) 363 parser.add_argument('-t', 364 '--test_script', 365 dest='test_script', 366 help=('Script to run to test the ' 367 'output after packages are built.')) 368 parser.add_argument('-p', 369 '--prune', 370 dest='prune', 371 action='store_true', 372 default=False, 373 help=('Script to run to test the output after ' 374 'packages are built.')) 375 parser.add_argument('-c', 376 '--noincremental', 377 dest='noincremental', 378 action='store_true', 379 default=False, 380 help='Do not propagate good/bad changes incrementally.') 381 parser.add_argument('-f', 382 '--file_args', 383 dest='file_args', 384 action='store_true', 385 default=False, 386 help='Use a file to pass arguments to scripts.') 387 parser.add_argument('-v', 388 '--verify_level', 389 dest='verify_level', 390 default=1, 391 help=('Check binary search assumptions N times ' 392 'before starting.')) 393 parser.add_argument('-N', 394 '--prune_iterations', 395 dest='prune_iterations', 396 help='Number of prune iterations to try in the search.', 397 default=100) 398 parser.add_argument('-V', 399 '--verbose', 400 dest='verbose', 401 action='store_true', 402 help='Print full output to console.') 403 404 logger.GetLogger().LogOutput(' '.join(argv)) 405 options = parser.parse_args(argv) 406 407 if not (options.get_initial_items and options.switch_to_good and 408 options.switch_to_bad and options.test_script): 409 parser.print_help() 410 return 1 411 412 iterations = int(options.iterations) 413 switch_to_good = _CanonicalizeScript(options.switch_to_good) 414 switch_to_bad = _CanonicalizeScript(options.switch_to_bad) 415 install_script = options.install_script 416 if install_script: 417 install_script = _CanonicalizeScript(options.install_script) 418 test_script = _CanonicalizeScript(options.test_script) 419 get_initial_items = _CanonicalizeScript(options.get_initial_items) 420 prune = options.prune 421 prune_iterations = options.prune_iterations 422 verify_level = options.verify_level 423 file_args = options.file_args 424 verbose = options.verbose 425 binary_search_perforce.verbose = verbose 426 427 if options.noincremental: 428 incremental = False 429 else: 430 incremental = True 431 432 try: 433 bss = BinarySearchState(get_initial_items, switch_to_good, switch_to_bad, 434 install_script, test_script, incremental, prune, 435 iterations, prune_iterations, verify_level, 436 file_args, verbose) 437 bss.DoVerify() 438 bss.DoSearch() 439 440 except (KeyboardInterrupt, SystemExit): 441 print('C-c pressed') 442 bss.SaveState() 443 return 0 444 445 446if __name__ == '__main__': 447 sys.exit(Main(sys.argv[1:])) 448