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