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