r356636 - creduce-clang-crash.py: preprocess file + reduce commandline

George Burgess IV via cfe-commits cfe-commits at lists.llvm.org
Wed Mar 20 18:01:54 PDT 2019


Author: gbiv
Date: Wed Mar 20 18:01:53 2019
New Revision: 356636

URL: http://llvm.org/viewvc/llvm-project?rev=356636&view=rev
Log:
creduce-clang-crash.py: preprocess file + reduce commandline

This CL causes our creduce-clang-crash.py util to:

- try to preprocess the file before reducing
- try to remove some command line arguments
- now require a llvm bin directory, since the generated crash script
  doesn't have an absolute path for clang

It also marks it as executable, since I forgot to do that in the last
commit. :)

Patch by Amy Huang!

Differential Revision: https://reviews.llvm.org/D59440

Modified:
    cfe/trunk/utils/creduce-clang-crash.py

Modified: cfe/trunk/utils/creduce-clang-crash.py
URL: http://llvm.org/viewvc/llvm-project/cfe/trunk/utils/creduce-clang-crash.py?rev=356636&r1=356635&r2=356636&view=diff
==============================================================================
--- cfe/trunk/utils/creduce-clang-crash.py (original)
+++ cfe/trunk/utils/creduce-clang-crash.py Wed Mar 20 18:01:53 2019
@@ -1,7 +1,5 @@
 #!/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
@@ -11,108 +9,232 @@ import stat
 import sys
 import subprocess
 import pipes
+import shlex
+import tempfile
+import shutil
 from distutils.spawn import find_executable
 
-def create_test(build_script, llvm_not):
+verbose = False
+llvm_bin = None
+creduce_cmd = None
+not_cmd = None
+
+def check_file(fname):
+  if not os.path.isfile(fname):
+    sys.exit("ERROR: %s does not exist" % (fname))
+  return fname
+
+def check_cmd(cmd_name, cmd_dir, cmd_path=None):
   """
-  Create an interestingness test from the crash output.
-  Return as a string.
+  Returns absolute path to cmd_path if it is given,
+  or absolute path to cmd_dir/cmd_name.
   """
-  # 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')
+  if cmd_path:
+    cmd = find_executable(cmd_path)
+    if cmd:
+      return cmd
+    sys.exit("ERROR: executable %s not found" % (cmd_path))
+
+  cmd = find_executable(cmd_name, path=cmd_dir)
+  if cmd:
+    return cmd
+  sys.exit("ERROR: %s not found in %s" % (cmd_name, cmd_dir))
+
+def quote_cmd(cmd):
+  return ' '.join(arg if arg.startswith('$') else pipes.quote(arg)
+                  for arg in cmd)
+
+def get_crash_cmd(crash_script):
+  with open(crash_script) as f:
+    # Assume clang call is on the last line of the script
+    line = f.readlines()[-1]
+    cmd = shlex.split(line)
+
+    # Overwrite the script's clang with the user's clang path
+    new_clang = check_cmd('clang', llvm_bin)
+    cmd[0] = pipes.quote(new_clang)
+    return cmd
 
-  # Get crash output
-  p = subprocess.Popen(build_script,
+def has_expected_output(crash_cmd, expected_output):
+  p = subprocess.Popen(crash_cmd,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT)
   crash_output, _ = p.communicate()
+  return all(msg in crash_output for msg in expected_output)
 
-  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
+def get_expected_output(crash_cmd):
+  p = subprocess.Popen(crash_cmd,
+                       stdout=subprocess.PIPE,
+                       stderr=subprocess.STDOUT)
+  crash_output, _ = p.communicate()
+
+  # 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))
+    return [assertion_match.group(1)]
   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 matches[-5:]
+
+def write_interestingness_test(testfile, crash_cmd, expected_output,
+                               file_to_reduce):
+  filename = os.path.basename(file_to_reduce)
+  if filename not in crash_cmd:
+    sys.exit("ERROR: expected %s to be in the crash command" % filename)
+
+  # Replace all instances of file_to_reduce with a command line variable
+  output = ['#!/bin/bash',
+            'if [ -z "$1" ] ; then',
+            '  f=%s' % (pipes.quote(filename)),
+            'else',
+            '  f="$1"',
+            'fi']
+  cmd = ['$f' if s == filename else s for s in crash_cmd]
+
+  output.append('%s --crash %s >& t.log || exit 1' % (pipes.quote(not_cmd),
+                                                      quote_cmd(cmd)))
+
+  for msg in expected_output:
+    output.append('grep %s t.log || exit 1' % pipes.quote(msg))
+
+  with open(testfile, 'w') as f:
+    f.write('\n'.join(output))
+  os.chmod(testfile, os.stat(testfile).st_mode | stat.S_IEXEC)
 
-  return output
+def check_interestingness(testfile, file_to_reduce):
+  testfile = os.path.abspath(testfile)
+
+  # Check that the test considers the original file interesting
+  with open(os.devnull, 'w') as devnull:
+    returncode = subprocess.call(testfile, stdout=devnull)
+  if returncode:
+    sys.exit("The interestingness test does not pass for the original file.")
+
+  # Check that an empty file is not interesting
+  _, empty_file = tempfile.mkstemp()
+  with open(os.devnull, 'w') as devnull:
+    returncode = subprocess.call([testfile, empty_file], stdout=devnull)
+  os.remove(empty_file)
+  if not returncode:
+    sys.exit("The interestingness test passes for an empty file.")
+
+def clang_preprocess(file_to_reduce, crash_cmd, expected_output):
+  _, tmpfile = tempfile.mkstemp()
+  shutil.copy(file_to_reduce, tmpfile)
+
+  cmd = crash_cmd + ['-E', '-P']
+  p = subprocess.Popen(cmd,
+                       stdout=subprocess.PIPE,
+                       stderr=subprocess.STDOUT)
+  preprocessed, _ = p.communicate()
+
+  with open(file_to_reduce, 'w') as f:
+    f.write(preprocessed)
+
+  if has_expected_output(crash_cmd, expected_output):
+    if verbose:
+      print("Successfuly preprocessed with %s" % (quote_cmd(cmd)))
+    os.remove(tmpfile)
+  else:
+    if verbose:
+      print("Failed to preprocess with %s" % (quote_cmd(cmd)))
+    shutil.move(tmpfile, file_to_reduce)
+
+
+def filter_args(args, opts_startswith=[]):
+  result = [arg for arg in args if all(not arg.startswith(a) for a in
+                                       opts_startswith)]
+  return result
+
+def try_remove_args(cmd, expected_output, msg=None, extra_arg=None, **kwargs):
+  new_cmd = filter_args(cmd, **kwargs)
+  if extra_arg and extra_arg not in new_cmd:
+    new_cmd.append(extra_arg)
+  if new_cmd != cmd and has_expected_output(new_cmd, expected_output):
+    if msg and verbose:
+      print(msg)
+    return new_cmd
+  return cmd
+
+def simplify_crash_cmd(crash_cmd, expected_output):
+  new_cmd = try_remove_args(crash_cmd, expected_output,
+                            msg="Removed debug info options",
+                            opts_startswith=["-gcodeview",
+                                             "-dwarf-column-info",
+                                             "-debug-info-kind=",
+                                             "-debugger-tuning=",
+                                             "-gdwarf"])
+  new_cmd = try_remove_args(new_cmd, expected_output,
+                            msg="Replaced -W options with -w",
+                            extra_arg='-w',
+                            opts_startswith=["-W"])
+  new_cmd = try_remove_args(new_cmd, expected_output,
+                            msg="Replaced optimization level with -O0",
+                            extra_arg="-O0",
+                            opts_startswith=["-O"])
+  return new_cmd
 
 def main():
+  global verbose
+  global llvm_bin
+  global creduce_cmd
+  global not_cmd
+
   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('crash_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.')
+                      help="Name of the file to be reduced.")
+  parser.add_argument('--llvm-bin', dest='llvm_bin', type=str,
+                      required=True, help="Path to the LLVM bin directory.")
   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.");
+                      help="The path to the `not` executable. "
+                      "By default uses the llvm-bin directory.")
   parser.add_argument('--creduce', dest='creduce', type=str,
-                      help="The path to the C-Reduce executable. "
-                      "Required if 'creduce' is not in PATH environment.");
+                      help="The path to the `creduce` executable. "
+                      "Required if `creduce` is not in PATH environment.")
+  parser.add_argument('-v', '--verbose', action='store_true')
   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
+  verbose = args.verbose
+  llvm_bin = os.path.abspath(args.llvm_bin)
+  creduce_cmd = check_cmd('creduce', None, args.creduce)
+  not_cmd = check_cmd('not', llvm_bin, args.llvm_not)
+  crash_script = check_file(args.crash_script[0])
+  file_to_reduce = check_file(args.file_to_reduce[0])
+
+  print("\nParsing the crash script and getting expected output...")
+  crash_cmd = get_crash_cmd(crash_script)
+
+  expected_output = get_expected_output(crash_cmd)
+  if len(expected_output) < 1:
+    sys.exit("ERROR: no crash was found")
+
+  print("\nSimplifying the crash command...")
+  crash_cmd = simplify_crash_cmd(crash_cmd, expected_output)
+
+  print("\nWriting interestingness test to file...")
+  testfile = os.path.splitext(file_to_reduce)[0] + '.test.sh'
+  write_interestingness_test(testfile, crash_cmd, expected_output,
+                             file_to_reduce)
+  check_interestingness(testfile, file_to_reduce)
 
-  # FIXME: try running clang preprocessor first
+  print("\nPreprocessing the file to reduce...")
+  clang_preprocess(file_to_reduce, crash_cmd, expected_output)
 
+  print("\nRunning C-Reduce...")
   try:
-    p = subprocess.Popen([creduce, testfile, file_to_reduce])
+    p = subprocess.Popen([creduce_cmd, 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()
 
+  # FIXME: reduce the clang crash command
+
 if __name__ == '__main__':
-  sys.exit(main())
+  main()




More information about the cfe-commits mailing list