| #!/usr/bin/env python |
| """Calls C-Reduce to create a minimal reproducer for clang crashes. |
| |
| Requires C-Reduce and not (part of LLVM utils) to be installed. |
| """ |
| |
| from argparse import ArgumentParser |
| import os |
| import re |
| import stat |
| import sys |
| import subprocess |
| import pipes |
| from distutils.spawn import find_executable |
| |
| def create_test(build_script, llvm_not): |
| """ |
| Create an interestingness test from the crash output. |
| Return as a string. |
| """ |
| # Get clang call from build script |
| # Assumes the call is the last line of the script |
| with open(build_script) as f: |
| cmd = f.readlines()[-1].rstrip('\n\r') |
| |
| # Get crash output |
| p = subprocess.Popen(build_script, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT) |
| crash_output, _ = p.communicate() |
| |
| output = ['#!/bin/bash'] |
| output.append('%s --crash %s >& t.log || exit 1' % (pipes.quote(llvm_not), |
| cmd)) |
| |
| # Add messages from crash output to the test |
| # If there is an Assertion failure, use that; otherwise use the |
| # last five stack trace functions |
| assertion_re = r'Assertion `([^\']+)\' failed' |
| assertion_match = re.search(assertion_re, crash_output) |
| if assertion_match: |
| msg = assertion_match.group(1) |
| output.append('grep %s t.log || exit 1' % pipes.quote(msg)) |
| else: |
| stacktrace_re = r'#[0-9]+\s+0[xX][0-9a-fA-F]+\s*([^(]+)\(' |
| matches = re.findall(stacktrace_re, crash_output) |
| del matches[:-5] |
| output += ['grep %s t.log || exit 1' % pipes.quote(msg) for msg in matches] |
| |
| return output |
| |
| def main(): |
| parser = ArgumentParser(description=__doc__) |
| parser.add_argument('build_script', type=str, nargs=1, |
| help='Name of the script that generates the crash.') |
| parser.add_argument('file_to_reduce', type=str, nargs=1, |
| help='Name of the file to be reduced.') |
| parser.add_argument('-o', '--output', dest='output', type=str, |
| help='Name of the output file for the reduction. Optional.') |
| parser.add_argument('--llvm-not', dest='llvm_not', type=str, |
| help="The path to the llvm-not executable. " |
| "Required if 'not' is not in PATH environment."); |
| parser.add_argument('--creduce', dest='creduce', type=str, |
| help="The path to the C-Reduce executable. " |
| "Required if 'creduce' is not in PATH environment."); |
| args = parser.parse_args() |
| |
| build_script = os.path.abspath(args.build_script[0]) |
| file_to_reduce = os.path.abspath(args.file_to_reduce[0]) |
| llvm_not = (find_executable(args.llvm_not) if args.llvm_not else |
| find_executable('not')) |
| creduce = (find_executable(args.creduce) if args.creduce else |
| find_executable('creduce')) |
| |
| if not os.path.isfile(build_script): |
| print(("ERROR: input file '%s' does not exist") % build_script) |
| return 1 |
| |
| if not os.path.isfile(file_to_reduce): |
| print(("ERROR: input file '%s' does not exist") % file_to_reduce) |
| return 1 |
| |
| if not llvm_not: |
| parser.print_help() |
| return 1 |
| |
| if not creduce: |
| parser.print_help() |
| return 1 |
| |
| # Write interestingness test to file |
| test_contents = create_test(build_script, llvm_not) |
| testname, _ = os.path.splitext(file_to_reduce) |
| testfile = testname + '.test.sh' |
| with open(testfile, 'w') as f: |
| f.write('\n'.join(test_contents)) |
| os.chmod(testfile, os.stat(testfile).st_mode | stat.S_IEXEC) |
| |
| # Confirm that the interestingness test passes |
| try: |
| with open(os.devnull, 'w') as devnull: |
| subprocess.check_call(testfile, stdout=devnull) |
| except subprocess.CalledProcessError: |
| print("For some reason the interestingness test does not return zero") |
| return 1 |
| |
| # FIXME: try running clang preprocessor first |
| |
| try: |
| p = subprocess.Popen([creduce, testfile, file_to_reduce]) |
| p.communicate() |
| except KeyboardInterrupt: |
| # Hack to kill C-Reduce because it jumps into its own pgid |
| print('\n\nctrl-c detected, killed creduce') |
| p.kill() |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |