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