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