llvm.org GIT mirror llvm / ecf84c1
Add a new utility script that helps update very simple regression tests. This script is currently specific to x86 and limited to use with very small regression or feature tests using 'llc' and 'FileCheck' in a reasonably canonical way. It is in no way general purpose or robust at this point. However, it works quite well for simple examples. Here is the intended workflow: - Make a change that requires updating N test files and M functions' assertions within those files. - Stash the change. - Update those N test files' RUN-lines to look "canonical"[1]. - Refresh the FileCheck lines for either the entire file or select functions by running this script. - The script will parse the RUN lines and run the 'llc' binary you give it according to each line, collecting the asm. - It will then annotate each function with the appropriate FileCheck comments to check every instruction from the start of the first basic block to the last return. - There will be numerous cases where the script either fails to remove the old lines, or inserts checks which need to be manually editted, but the manual edits tend to be deletions or replacements of registers with FileCheck variables which are fast manual edits. - A common pattern is to have the script insert complete checking of every instruction, and then edit it down to only check the relevant ones. - Be careful to do all of these cleanups though! The script is designed to make transferring and formatting the asm output of llc into a test case fast, it is *not* designed to be authoratitive about what constitutes a good test! - Commit the nice fresh baseline of checks. - Unstash your change and rebuild llc. - Re-run script to regenerate the FileCheck annotations - Remember to re-cleanup these annotations!!! - Check the diff to make sure this is sane, checking the things you expected it to, and check that the newly updated tests actually pass. - Profit! Also, I'm *terrible* at writing Python, and frankly I didn't spend a lot of time making this script beautiful or well engineered. But it's useful to me and may be useful to others so I thought I'd send it out. http://reviews.llvm.org/D5546 git-svn-id: https://llvm.org/svn/llvm-project/llvm/trunk@225618 91177308-0d34-0410-b5e6-96231b3b80d8 Chandler Carruth 4 years ago
1 changed file(s) with 207 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 #!/usr/bin/env python2.7
1
2 """A test case update script.
3
4 This script is a utility to update LLVM X86 'llc' based test cases with new
5 FileCheck patterns. It can either update all of the tests in the file or
6 a single test function.
7 """
8
9 import argparse
10 import itertools
11 import string
12 import subprocess
13 import sys
14 import tempfile
15 import re
16
17
18 def llc(args, cmd_args, ir):
19 with open(ir) as ir_file:
20 stdout = subprocess.check_output(args.llc_binary + ' ' + cmd_args,
21 shell=True, stdin=ir_file)
22 return stdout
23
24
25 ASM_SCRUB_WHITESPACE_RE = re.compile(r'(?!^(| \w))[ \t]+', flags=re.M)
26 ASM_SCRUB_SHUFFLES_RE = (
27 re.compile(
28 r'^(\s*\w+) [^#\n]+#+ ((?:[xyz]mm\d+|mem) = .*)$',
29 flags=re.M))
30 ASM_SCRUB_SP_RE = re.compile(r'\d+\(%(esp|rsp)\)')
31 ASM_SCRUB_RIP_RE = re.compile(r'[.\w]+\(%rip\)')
32 ASM_SCRUB_KILL_COMMENT_RE = re.compile(r'^ *#+ +kill:.*\n')
33
34
35 def scrub_asm(asm):
36 # Scrub runs of whitespace out of the assembly, but leave the leading
37 # whitespace in place.
38 asm = ASM_SCRUB_WHITESPACE_RE.sub(r' ', asm)
39 # Expand the tabs used for indentation.
40 asm = string.expandtabs(asm, 2)
41 # Detect shuffle asm comments and hide the operands in favor of the comments.
42 asm = ASM_SCRUB_SHUFFLES_RE.sub(r'\1 {{.*#+}} \2', asm)
43 # Generically match the stack offset of a memory operand.
44 asm = ASM_SCRUB_SP_RE.sub(r'{{[0-9]+}}(%\1)', asm)
45 # Generically match a RIP-relative memory operand.
46 asm = ASM_SCRUB_RIP_RE.sub(r'{{.*}}(%rip)', asm)
47 # Strip kill operands inserted into the asm.
48 asm = ASM_SCRUB_KILL_COMMENT_RE.sub('', asm)
49 return asm
50
51
52 def main():
53 parser = argparse.ArgumentParser(description=__doc__)
54 parser.add_argument('-v', '--verbose', action='store_true',
55 help='Show verbose output')
56 parser.add_argument('--llc-binary', default='llc',
57 help='The "llc" binary to use to generate the test case')
58 parser.add_argument(
59 '--function', help='The function in the test file to update')
60 parser.add_argument('tests', nargs='+')
61 args = parser.parse_args()
62
63 run_line_re = re.compile('^\s*;\s*RUN:\s*(.*)$')
64 ir_function_re = re.compile('^\s*define\s+(?:internal\s+)?[^@]*@(\w+)\s*\(')
65 asm_function_re = re.compile(
66 r'^_?(?P[^:]+):[ \t]*#+[ \t]*@(?P=f)\n[^:]*?'
67 r'(?P^##?[ \t]+[^:]+:.*?)\s*'
68 r'^\s*(?:[^:\n]+?:\s*\n\s*\.size|\.cfi_endproc|\.globl|\.(?:sub)?section)',
69 flags=(re.M | re.S))
70 check_prefix_re = re.compile('--check-prefix=(\S+)')
71 check_re = re.compile(r'^\s*;\s*([^:]+?)(?:-NEXT|-NOT|-DAG|-LABEL)?:')
72
73 for test in args.tests:
74 if args.verbose:
75 print >>sys.stderr, 'Scanning for RUN lines in test file: %s' % (test,)
76 with open(test) as f:
77 test_lines = [l.rstrip() for l in f]
78
79 run_lines = [m.group(1)
80 for m in [run_line_re.match(l) for l in test_lines] if m]
81 if args.verbose:
82 print >>sys.stderr, 'Found %d RUN lines:' % (len(run_lines),)
83 for l in run_lines:
84 print >>sys.stderr, ' RUN: ' + l
85
86 checks = []
87 for l in run_lines:
88 (llc_cmd, filecheck_cmd) = tuple([cmd.strip() for cmd in l.split('|', 1)])
89 if not llc_cmd.startswith('llc '):
90 print >>sys.stderr, 'WARNING: Skipping non-llc RUN line: ' + l
91 continue
92
93 if not filecheck_cmd.startswith('FileCheck '):
94 print >>sys.stderr, 'WARNING: Skipping non-FileChecked RUN line: ' + l
95 continue
96
97 llc_cmd_args = llc_cmd[len('llc'):].strip()
98 llc_cmd_args = llc_cmd_args.replace('< %s', '').replace('%s', '').strip()
99
100 check_prefixes = [m.group(1)
101 for m in check_prefix_re.finditer(filecheck_cmd)]
102 if not check_prefixes:
103 check_prefixes = ['CHECK']
104
105 # FIXME: We should use multiple check prefixes to common check lines. For
106 # now, we just ignore all but the last.
107 checks.append((check_prefixes, llc_cmd_args))
108
109 asm = {}
110 for prefixes, _ in checks:
111 for prefix in prefixes:
112 asm.update({prefix: dict()})
113 for prefixes, llc_args in checks:
114 if args.verbose:
115 print >>sys.stderr, 'Extracted LLC cmd: llc ' + llc_args
116 print >>sys.stderr, 'Extracted FileCheck prefixes: ' + str(prefixes)
117 raw_asm = llc(args, llc_args, test)
118 # Build up a dictionary of all the function bodies.
119 for m in asm_function_re.finditer(raw_asm):
120 if not m:
121 continue
122 f = m.group('f')
123 f_asm = scrub_asm(m.group('body'))
124 if args.verbose:
125 print >>sys.stderr, 'Processing asm for function: ' + f
126 for l in f_asm.splitlines():
127 print >>sys.stderr, ' ' + l
128 for prefix in prefixes:
129 if f in asm[prefix] and asm[prefix][f] != f_asm:
130 if prefix == prefixes[-1]:
131 print >>sys.stderr, ('WARNING: Found conflicting asm under the '
132 'same prefix!')
133 else:
134 asm[prefix][f] = None
135 continue
136
137 asm[prefix][f] = f_asm
138
139 is_in_function = False
140 is_in_function_start = False
141 prefix_set = set([prefix for prefixes, _ in checks for prefix in prefixes])
142 if args.verbose:
143 print >>sys.stderr, 'Rewriting FileCheck prefixes: %s' % (prefix_set,)
144 fixed_lines = []
145 for l in test_lines:
146 if is_in_function_start:
147 if l.lstrip().startswith(';'):
148 m = check_re.match(l)
149 if not m or m.group(1) not in prefix_set:
150 fixed_lines.append(l)
151 continue
152
153 # Print out the various check lines here
154 printed_prefixes = []
155 for prefixes, _ in checks:
156 for prefix in prefixes:
157 if prefix in printed_prefixes:
158 break
159 if not asm[prefix][name]:
160 continue
161 if len(printed_prefixes) != 0:
162 fixed_lines.append(';')
163 printed_prefixes.append(prefix)
164 fixed_lines.append('; %s-LABEL: %s:' % (prefix, name))
165 asm_lines = asm[prefix][name].splitlines()
166 fixed_lines.append('; %s: %s' % (prefix, asm_lines[0]))
167 for asm_line in asm_lines[1:]:
168 fixed_lines.append('; %s-NEXT: %s' % (prefix, asm_line))
169 break
170 is_in_function_start = False
171
172 if is_in_function:
173 # Skip any blank comment lines in the IR.
174 if l.strip() == ';':
175 continue
176 # And skip any CHECK lines. We'll build our own.
177 m = check_re.match(l)
178 if m and m.group(1) in prefix_set:
179 continue
180 # Collect the remaining lines in the function body and look for the end
181 # of the function.
182 fixed_lines.append(l)
183 if l.strip() == '}':
184 is_in_function = False
185 continue
186
187 fixed_lines.append(l)
188
189 m = ir_function_re.match(l)
190 if not m:
191 continue
192 name = m.group(1)
193 if args.function is not None and name != args.function:
194 # When filtering on a specific function, skip all others.
195 continue
196 is_in_function = is_in_function_start = True
197
198 if args.verbose:
199 print>>sys.stderr, 'Writing %d fixed lines to %s...' % (
200 len(fixed_lines), test)
201 with open(test, 'w') as f:
202 f.writelines([l + '\n' for l in fixed_lines])
203
204
205 if __name__ == '__main__':
206 main()