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