#!/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()