| #!/usr/bin/env python2 |
| |
| import argparse |
| import math |
| import os |
| import re |
| import signal |
| import subprocess |
| |
| class Runner(object): |
| def __init__(self, input_cmd, timeout, comma_join, template, find_all): |
| self._input_cmd = input_cmd |
| self._timeout = timeout |
| self._num_tries = 0 |
| self._comma_join = comma_join |
| self._template = template |
| self._find_all = find_all |
| |
| def estimate(self, included_ranges): |
| result = 0 |
| for i in included_ranges: |
| if isinstance(i, int): |
| result += 1 |
| else: |
| if i[1] - i[0] > 2: |
| result += int(math.log(i[1] - i[0], 2)) |
| else: |
| result += (i[1] - i[0]) |
| if self._find_all: |
| return 2 * result |
| else: |
| return result |
| |
| def Run(self, included_ranges): |
| def timeout_handler(signum, frame): |
| raise RuntimeError('Timeout') |
| |
| self._num_tries += 1 |
| cmd_addition = '' |
| for i in included_ranges: |
| if isinstance(i, int): |
| range_str = str(i) |
| else: |
| range_str = '{start}:{end}'.format(start=i[0], end=i[1]) |
| if self._comma_join: |
| cmd_addition += ',' + range_str |
| else: |
| cmd_addition += ' -i ' + range_str |
| |
| if self._template: |
| cmd = cmd_addition.join(re.split(r'%i' ,self._input_cmd)) |
| else: |
| cmd = self._input_cmd + cmd_addition |
| |
| print cmd |
| p = subprocess.Popen(cmd, shell = True, cwd = None, |
| stdout = subprocess.PIPE, stderr = subprocess.PIPE, env = None) |
| if self._timeout != -1: |
| signal.signal(signal.SIGALRM, timeout_handler) |
| signal.alarm(self._timeout) |
| |
| try: |
| _, _ = p.communicate() |
| if self._timeout != -1: |
| signal.alarm(0) |
| except: |
| try: |
| os.kill(p.pid, signal.SIGKILL) |
| except OSError: |
| pass |
| print 'Timeout' |
| return -9 |
| print '===Return Code===: ' + str(p.returncode) |
| print '===Remaining Steps (approx)===: ' \ |
| + str(self.estimate(included_ranges)) |
| return p.returncode |
| |
| def flatten(tree): |
| if isinstance(tree, list): |
| result = [] |
| for node in tree: |
| result.extend(flatten(node)) |
| return result |
| else: |
| return [tree] # leaf |
| |
| def find_failures(runner, current_interval, include_ranges, find_all): |
| if current_interval[0] == current_interval[1]: |
| return [] |
| mid = (current_interval[0] + current_interval[1]) / 2 |
| |
| first_half = (current_interval[0], mid) |
| second_half = (mid, current_interval[1]) |
| |
| exit_code_2 = 0 |
| |
| exit_code_1 = runner.Run([first_half] + include_ranges) |
| if find_all or exit_code_1 == 0: |
| exit_code_2 = runner.Run([second_half] + include_ranges) |
| |
| if exit_code_1 == 0 and exit_code_2 == 0: |
| # Whole range fails but both halves pass |
| # So, some conjunction of functions cause a failure, but none individually. |
| partial_result = flatten(find_failures(runner, first_half, [second_half] |
| + include_ranges, find_all)) |
| # Heavy list concatenation, but this is insignificant compared to the |
| # process run times |
| partial_result.extend(flatten(find_failures(runner, second_half, |
| partial_result + include_ranges, find_all))) |
| return [partial_result] |
| else: |
| result = [] |
| if exit_code_1 != 0: |
| if first_half[1] == first_half[0] + 1: |
| result.append(first_half[0]) |
| else: |
| result.extend(find_failures(runner, first_half, |
| include_ranges, find_all)) |
| if exit_code_2 != 0: |
| if second_half[1] == second_half[0] + 1: |
| result.append(second_half[0]) |
| else: |
| result.extend(find_failures(runner, second_half, |
| include_ranges, find_all)) |
| return result |
| |
| |
| def main(): |
| ''' |
| Helper Script for Automating Bisection Debugging |
| |
| Example Invocation: |
| bisection-tool.py --cmd 'bisection-test.py -c 2x3' --end 1000 --timeout 60 |
| |
| This will invoke 'bisection-test.py -c 2x3' starting with the range -i 0:1000 |
| If that fails, it will subdivide the range (initially 0:500 and 500:1000) |
| recursively to pinpoint a combination of singletons that are needed to cause |
| the input to return a non zero exit code or timeout. |
| |
| For investigating an error in the generated code: |
| bisection-tool.py --cmd './pydir/szbuild_spec2k.py --run 188.ammp' |
| |
| For Subzero itself crashing, |
| bisection-tool.py --cmd 'pnacl-sz -translate-only=' --comma-join=1 |
| The --comma-join flag ensures the ranges are formatted in the manner pnacl-sz |
| expects. |
| |
| If the range specification is not to be appended on the input: |
| bisection-tool.py --cmd 'echo %i; cmd-main %i; cmd-post' --template=1 |
| |
| ''' |
| argparser = argparse.ArgumentParser(main.__doc__) |
| argparser.add_argument('--cmd', required=True, dest='cmd', |
| help='Runnable command') |
| |
| argparser.add_argument('--start', dest='start', default=0, |
| help='Start of initial range') |
| |
| argparser.add_argument('--end', dest='end', default=50000, |
| help='End of initial range') |
| |
| argparser.add_argument('--timeout', dest='timeout', default=60, |
| help='Timeout for each invocation of the input') |
| |
| argparser.add_argument('--all', type=int, choices=[0,1], default=1, |
| dest='all', help='Find all failures') |
| |
| argparser.add_argument('--comma-join', type=int, choices=[0,1], default=0, |
| dest='comma_join', help='Use comma to join ranges') |
| |
| argparser.add_argument('--template', type=int, choices=[0,1], default=0, |
| dest='template', |
| help='Replace %%i in the cmd string with the ranges') |
| |
| |
| args = argparser.parse_args() |
| |
| fail_list = [] |
| |
| initial_range = (int(args.start), int(args.end)) |
| timeout = int(args.timeout) |
| runner = Runner(args.cmd, timeout, args.comma_join, args.template, args.all) |
| if runner.Run([initial_range]) != 0: |
| fail_list = find_failures(runner, initial_range, [], args.all) |
| else: |
| print 'Pass' |
| # The whole input range works, maybe check subzero build flags? |
| # Also consider widening the initial range (control with --start and --end) |
| |
| if fail_list: |
| print 'Failing Items:' |
| for fail in fail_list: |
| if isinstance(fail, list): |
| fail.sort() |
| print '[' + ','.join(str(x) for x in fail) + ']' |
| else: |
| print fail |
| print 'Number of tries: ' + str(runner._num_tries) |
| |
| if __name__ == '__main__': |
| main() |