#!/usr/bin/env python3 """A script to generate FileCheck statements for 'opt' regression tests. This script is a utility to update LLVM opt test cases with new FileCheck patterns. It can either update all of the tests in the file or a single test function. Example usage: # Default to using `opt` as found in your PATH. $ update_test_checks.py test/foo.ll # Override the path lookup. $ update_test_checks.py --tool-binary=../bin/opt test/foo.ll # Use a custom tool instead of `opt`. $ update_test_checks.py --tool=yourtool test/foo.ll Workflow: 1. Make a compiler patch that requires updating some number of FileCheck lines in regression test files. 2. Save the patch and revert it from your local work area. 3. Update the RUN-lines in the affected regression tests to look canonical. Example: "; RUN: opt < %s -instcombine -S | FileCheck %s" 4. Refresh the FileCheck lines for either the entire file or select functions by running this script. 5. Commit the fresh baseline of checks. 6. Apply your patch from step 1 and rebuild your local binaries. 7. Re-run this script on affected regression tests. 8. Check the diffs to ensure the script has done something reasonable. 9. Submit a patch including the regression test diffs for review. """ from __future__ import print_function import argparse import os # Used to advertise this file's name ("autogenerated_note"). import re import sys from UpdateTestChecks import common def main(): from argparse import RawTextHelpFormatter parser = argparse.ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter) parser.add_argument('--tool', default='opt', help='The name of the tool used to generate the test case (defaults to "opt")') parser.add_argument('--tool-binary', '--opt-binary', help='The tool binary used to generate the test case') parser.add_argument( '--function', help='The function in the test file to update') parser.add_argument('-p', '--preserve-names', action='store_true', help='Do not scrub IR names') parser.add_argument('--function-signature', action='store_true', help='Keep function signature information around for the check line') parser.add_argument('--scrub-attributes', action='store_true', help='Remove attribute annotations (#0) from the end of check line') parser.add_argument('--check-attributes', action='store_true', help='Check "Function Attributes" for functions') parser.add_argument('--check-globals', action='store_true', help='Check global entries (global variables, metadata, attribute sets, ...) for functions') parser.add_argument('tests', nargs='+') initial_args = common.parse_commandline_args(parser) script_name = os.path.basename(__file__) if initial_args.tool_binary: tool_basename = os.path.basename(initial_args.tool_binary) if not re.match(r'^%s(-\d+)?(\.exe)?$' % (initial_args.tool), tool_basename): common.error('Unexpected tool name: ' + tool_basename) sys.exit(1) tool_basename = initial_args.tool for ti in common.itertests(initial_args.tests, parser, script_name='utils/' + script_name): # If requested we scrub trailing attribute annotations, e.g., '#0', together with whitespaces if ti.args.scrub_attributes: common.SCRUB_TRAILING_WHITESPACE_TEST_RE = common.SCRUB_TRAILING_WHITESPACE_AND_ATTRIBUTES_RE else: common.SCRUB_TRAILING_WHITESPACE_TEST_RE = common.SCRUB_TRAILING_WHITESPACE_RE prefix_list = [] for l in ti.run_lines: if '|' not in l: common.warn('Skipping unparseable RUN line: ' + l) continue commands = [cmd.strip() for cmd in l.split('|')] assert len(commands) >= 2 preprocess_cmd = None if len(commands) > 2: preprocess_cmd = " | ".join(commands[:-2]) tool_cmd = commands[-2] filecheck_cmd = commands[-1] common.verify_filecheck_prefixes(filecheck_cmd) if not tool_cmd.startswith(tool_basename + ' '): common.warn('Skipping non-%s RUN line: %s' % (tool_basename, l)) continue if not filecheck_cmd.startswith('FileCheck '): common.warn('Skipping non-FileChecked RUN line: ' + l) continue tool_cmd_args = tool_cmd[len(tool_basename):].strip() tool_cmd_args = tool_cmd_args.replace('< %s', '').replace('%s', '').strip() check_prefixes = [item for m in common.CHECK_PREFIX_RE.finditer(filecheck_cmd) for item in m.group(1).split(',')] if not check_prefixes: check_prefixes = ['CHECK'] # FIXME: We should use multiple check prefixes to common check lines. For # now, we just ignore all but the last. prefix_list.append((check_prefixes, tool_cmd_args, preprocess_cmd)) global_vars_seen_dict = {} builder = common.FunctionTestBuilder( run_list=prefix_list, flags=ti.args, scrubber_args=[], path=ti.path) tool_binary = ti.args.tool_binary if not tool_binary: tool_binary = tool_basename for prefixes, tool_args, preprocess_cmd in prefix_list: common.debug('Extracted tool cmd: ' + tool_basename + ' ' + tool_args) common.debug('Extracted FileCheck prefixes: ' + str(prefixes)) raw_tool_output = common.invoke_tool(tool_binary, tool_args, ti.path, preprocess_cmd=preprocess_cmd, verbose=ti.args.verbose) builder.process_run_line(common.OPT_FUNCTION_RE, common.scrub_body, raw_tool_output, prefixes, False) builder.processed_prefixes(prefixes) func_dict = builder.finish_and_get_func_dict() is_in_function = False is_in_function_start = False has_checked_pre_function_globals = False prefix_set = set([prefix for prefixes, _, _ in prefix_list for prefix in prefixes]) common.debug('Rewriting FileCheck prefixes:', str(prefix_set)) output_lines = [] include_generated_funcs = common.find_arg_in_test(ti, lambda args: ti.args.include_generated_funcs, '--include-generated-funcs', True) if include_generated_funcs: # Generate the appropriate checks for each function. We need to emit # these in the order according to the generated output so that CHECK-LABEL # works properly. func_order provides that. # We can't predict where various passes might insert functions so we can't # be sure the input function order is maintained. Therefore, first spit # out all the source lines. common.dump_input_lines(output_lines, ti, prefix_set, ';') args = ti.args if args.check_globals: common.add_global_checks(builder.global_var_dict(), ';', prefix_list, output_lines, global_vars_seen_dict, args.preserve_names, True) # Now generate all the checks. common.add_checks_at_end(output_lines, prefix_list, builder.func_order(), ';', lambda my_output_lines, prefixes, func: common.add_ir_checks(my_output_lines, ';', prefixes, func_dict, func, False, args.function_signature, global_vars_seen_dict, is_filtered=builder.is_filtered())) else: # "Normal" mode. for input_line_info in ti.iterlines(output_lines): input_line = input_line_info.line args = input_line_info.args if is_in_function_start: if input_line == '': continue if input_line.lstrip().startswith(';'): m = common.CHECK_RE.match(input_line) if not m or m.group(1) not in prefix_set: output_lines.append(input_line) continue # Print out the various check lines here. common.add_ir_checks(output_lines, ';', prefix_list, func_dict, func_name, args.preserve_names, args.function_signature, global_vars_seen_dict, is_filtered=builder.is_filtered()) is_in_function_start = False m = common.IR_FUNCTION_RE.match(input_line) if m and not has_checked_pre_function_globals: if args.check_globals: common.add_global_checks(builder.global_var_dict(), ';', prefix_list, output_lines, global_vars_seen_dict, args.preserve_names, True) has_checked_pre_function_globals = True if common.should_add_line_to_output(input_line, prefix_set, not is_in_function): # This input line of the function body will go as-is into the output. # Except make leading whitespace uniform: 2 spaces. input_line = common.SCRUB_LEADING_WHITESPACE_RE.sub(r' ', input_line) output_lines.append(input_line) if input_line.strip() == '}': is_in_function = False continue if is_in_function: continue m = common.IR_FUNCTION_RE.match(input_line) if not m: continue func_name = m.group(1) if args.function is not None and func_name != args.function: # When filtering on a specific function, skip all others. continue is_in_function = is_in_function_start = True if args.check_globals: common.add_global_checks(builder.global_var_dict(), ';', prefix_list, output_lines, global_vars_seen_dict, args.preserve_names, False) common.debug('Writing %d lines to %s...' % (len(output_lines), ti.path)) with open(ti.path, 'wb') as f: f.writelines(['{}\n'.format(l).encode('utf-8') for l in output_lines]) if __name__ == '__main__': main()