Skip to content

Commit 1ac1960

Browse files
committed
[UVT] add update-verify-tests.py
Adds a python script to automatically take output from a failed clang -verify test and update the test case(s) to expect the new behaviour.
1 parent c1c4251 commit 1ac1960

File tree

1 file changed

+321
-0
lines changed

1 file changed

+321
-0
lines changed

clang/utils/update-verify-tests.py

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import sys
2+
import re
3+
4+
"""
5+
Pipe output from clang's -verify into this script to have the test case updated to expect the actual diagnostic output.
6+
When inserting new expected-* checks it will place them on the line before the location of the diagnostic, with an @+1,
7+
or @+N for some N if there are multiple diagnostics emitted on the same line. If the current checks are using @-N for
8+
this line, the new check will follow that convention also.
9+
Existing checks will be left untouched as much as possible, including their location and whitespace content, to minimize
10+
diffs. If inaccurate their count will be updated, or the check removed entirely.
11+
12+
Missing features:
13+
- custom prefix support (-verify=my-prefix)
14+
- multiple prefixes on the same line (-verify=my-prefix,my-other-prefix)
15+
- multiple prefixes on separate RUN lines (RUN: -verify=my-prefix\nRUN: -verify my-other-prefix)
16+
- regexes with expected-*-re: existing ones will be left untouched if accurate, but the script will abort if there are any
17+
diagnostic mismatches on the same line.
18+
- multiple checks targeting the same line are supported, but a line may only contain one check
19+
- if multiple checks targeting the same line are failing the script is not guaranteed to produce a minimal diff
20+
21+
Example usage:
22+
build/bin/llvm-lit clang/test/Sema/ --no-progress-bar -v | python3 update-verify-tests.py
23+
"""
24+
25+
class KnownException(Exception):
26+
pass
27+
28+
def parse_error_category(s):
29+
parts = s.split("diagnostics")
30+
diag_category = parts[0]
31+
category_parts = parts[0].strip().strip("'").split("-")
32+
expected = category_parts[0]
33+
if expected != "expected":
34+
raise Exception(f"expected 'expected', but found '{expected}'. Custom verify prefixes are not supported.")
35+
diag_category = category_parts[1]
36+
if "seen but not expected" in parts[1]:
37+
seen = True
38+
elif "expected but not seen" in parts[1]:
39+
seen = False
40+
else:
41+
raise KnownException(f"unexpected category '{parts[1]}'")
42+
return (diag_category, seen)
43+
44+
diag_error_re = re.compile(r"File (\S+) Line (\d+): (.+)")
45+
diag_error_re2 = re.compile(r"File \S+ Line \d+ \(directive at (\S+):(\d+)\): (.+)")
46+
def parse_diag_error(s):
47+
m = diag_error_re2.match(s)
48+
if not m:
49+
m = diag_error_re.match(s)
50+
if not m:
51+
return None
52+
return (m.group(1), int(m.group(2)), m.group(3))
53+
54+
class Line:
55+
def __init__(self, content, line_n):
56+
self.content = content
57+
self.diag = None
58+
self.line_n = line_n
59+
self.related_diags = []
60+
self.targeting_diags = []
61+
def update_line_n(self, n):
62+
if self.diag and not self.diag.line_is_absolute:
63+
self.diag.orig_target_line_n += n - self.line_n
64+
self.line_n = n
65+
for diag in self.targeting_diags:
66+
if diag.line_is_absolute:
67+
diag.orig_target_line_n = n
68+
else:
69+
diag.orig_target_line_n = n - diag.line.line_n
70+
for diag in self.related_diags:
71+
if not diag.line_is_absolute:
72+
pass
73+
def render(self):
74+
if not self.diag:
75+
return self.content
76+
assert("{{DIAG}}" in self.content)
77+
res = self.content.replace("{{DIAG}}", self.diag.render())
78+
if not res.strip():
79+
return ""
80+
return res
81+
82+
class Diag:
83+
def __init__(self, diag_content, category, targeted_line_n, line_is_absolute, count, line, is_re, whitespace_strings):
84+
self.diag_content = diag_content
85+
self.category = category
86+
self.orig_target_line_n = targeted_line_n
87+
self.line_is_absolute = line_is_absolute
88+
self.count = count
89+
self.line = line
90+
self.target = None
91+
self.is_re = is_re
92+
self.absolute_target()
93+
self.whitespace_strings = whitespace_strings
94+
95+
def add(self):
96+
if targeted_line > 0:
97+
targeted_line += 1
98+
elif targeted_line < 0:
99+
targeted_line -= 1
100+
101+
def absolute_target(self):
102+
if self.line_is_absolute:
103+
res = self.orig_target_line_n
104+
else:
105+
res = self.line.line_n + self.orig_target_line_n
106+
if self.target:
107+
assert(self.line.line_n == res)
108+
return res
109+
110+
def relative_target(self):
111+
return self.absolute_target() - self.line.line_n
112+
113+
def render(self):
114+
assert(self.count >= 0)
115+
if self.count == 0:
116+
return ""
117+
line_location_s = ""
118+
if self.relative_target() != 0:
119+
if self.line_is_absolute:
120+
line_location_s = f"@{self.absolute_target()}"
121+
elif self.relative_target() > 0:
122+
line_location_s = f"@+{self.relative_target()}"
123+
else:
124+
line_location_s = f"@{self.relative_target()}" # the minus sign is implicit
125+
count_s = "" if self.count == 1 else f"{self.count}"
126+
re_s = "-re" if self.is_re else ""
127+
if self.whitespace_strings:
128+
whitespace1_s = self.whitespace_strings[0]
129+
whitespace2_s = self.whitespace_strings[1]
130+
whitespace3_s = self.whitespace_strings[2]
131+
whitespace4_s = self.whitespace_strings[3]
132+
else:
133+
whitespace1_s = " "
134+
whitespace2_s = ""
135+
whitespace3_s = ""
136+
whitespace4_s = ""
137+
if count_s and not whitespace3_s:
138+
whitespace3_s = " "
139+
return f"//{whitespace1_s}expected-{self.category}{re_s}{whitespace2_s}{line_location_s}{whitespace3_s}{count_s}{whitespace4_s}{{{{{self.diag_content}}}}}"
140+
141+
expected_diag_re = re.compile(r"//(\s*)expected-(note|warning|error)(-re)?(\s*)(@[+-]?\d+)?(\s*)(\d+)?(\s*)\{\{(.*)\}\}")
142+
def parse_diag(line, filename, lines):
143+
s = line.content
144+
ms = expected_diag_re.findall(s)
145+
if not ms:
146+
return None
147+
if len(ms) > 1:
148+
print(f"multiple diags on line {filename}:{line.line_n}. Aborting due to missing implementation.")
149+
sys.exit(1)
150+
[whitespace1_s, category_s, re_s, whitespace2_s, target_line_s, whitespace3_s, count_s, whitespace4_s, diag_s] = ms[0]
151+
if not target_line_s:
152+
target_line_n = 0
153+
is_absolute = False
154+
elif target_line_s.startswith("@+"):
155+
target_line_n = int(target_line_s[2:])
156+
is_absolute = False
157+
elif target_line_s.startswith("@-"):
158+
target_line_n = int(target_line_s[1:])
159+
is_absolute = False
160+
else:
161+
target_line_n = int(target_line_s[1:])
162+
is_absolute = True
163+
count = int(count_s) if count_s else 1
164+
line.content = expected_diag_re.sub("{{DIAG}}", s)
165+
166+
return Diag(diag_s, category_s, target_line_n, is_absolute, count, line, bool(re_s), [whitespace1_s, whitespace2_s, whitespace3_s, whitespace4_s])
167+
168+
def link_line_diags(lines, diag):
169+
line_n = diag.line.line_n
170+
target_line_n = diag.absolute_target()
171+
step = 1 if target_line_n < line_n else -1
172+
for i in range(target_line_n, line_n, step):
173+
lines[i-1].related_diags.append(diag)
174+
175+
def add_line(new_line, lines):
176+
lines.insert(new_line.line_n-1, new_line)
177+
for i in range(new_line.line_n, len(lines)):
178+
line = lines[i]
179+
assert(line.line_n == i)
180+
line.update_line_n(i+1)
181+
assert(all(line.line_n == i+1 for i, line in enumerate(lines)))
182+
183+
indent_re = re.compile(r"\s*")
184+
def get_indent(s):
185+
return indent_re.match(s).group(0)
186+
187+
def add_diag(line_n, diag_s, diag_category, lines):
188+
target = lines[line_n - 1]
189+
for other in target.targeting_diags:
190+
if other.is_re:
191+
raise KnownException("mismatching diag on line with regex matcher. Skipping due to missing implementation")
192+
reverse = True if [other for other in target.targeting_diags if other.relative_target() < 0] else False
193+
194+
targeting = [other for other in target.targeting_diags if not other.line_is_absolute]
195+
targeting.sort(reverse=reverse, key=lambda d: d.relative_target())
196+
prev_offset = 0
197+
prev_line = target
198+
direction = -1 if reverse else 1
199+
for d in targeting:
200+
if d.relative_target() != prev_offset + direction:
201+
break
202+
prev_offset = d.relative_target()
203+
prev_line = d.line
204+
total_offset = prev_offset - 1 if reverse else prev_offset + 1
205+
if reverse:
206+
new_line_n = prev_line.line_n + 1
207+
else:
208+
new_line_n = prev_line.line_n
209+
assert(new_line_n == line_n + (not reverse) - total_offset)
210+
211+
new_line = Line(get_indent(prev_line.content) + "{{DIAG}}\n", new_line_n)
212+
new_line.related_diags = list(prev_line.related_diags)
213+
add_line(new_line, lines)
214+
215+
new_diag = Diag(diag_s, diag_category, total_offset, False, 1, new_line, False, None)
216+
new_line.diag = new_diag
217+
new_diag.target_line = target
218+
assert(type(new_diag) != str)
219+
target.targeting_diags.append(new_diag)
220+
link_line_diags(lines, new_diag)
221+
222+
updated_test_files = set()
223+
def update_test_file(filename, diag_errors):
224+
print(f"updating test file {filename}")
225+
if filename in updated_test_files:
226+
print(f"{filename} already updated, but got new output - expect incorrect results")
227+
else:
228+
updated_test_files.add(filename)
229+
with open(filename, 'r') as f:
230+
lines = [Line(line, i+1) for i, line in enumerate(f.readlines())]
231+
for line in lines:
232+
diag = parse_diag(line, filename, lines)
233+
if diag:
234+
line.diag = diag
235+
diag.target_line = lines[diag.absolute_target() - 1]
236+
link_line_diags(lines, diag)
237+
lines[diag.absolute_target() - 1].targeting_diags.append(diag)
238+
239+
for (line_n, diag_s, diag_category, seen) in diag_errors:
240+
if seen:
241+
continue
242+
# this is a diagnostic expected but not seen
243+
assert(lines[line_n - 1].diag)
244+
if diag_s != lines[line_n - 1].diag.diag_content:
245+
raise KnownException(f"{filename}:{line_n} - found diag {lines[line_n - 1].diag.diag_content} but expected {diag_s}")
246+
if diag_category != lines[line_n - 1].diag.category:
247+
raise KnownException(f"{filename}:{line_n} - found {lines[line_n - 1].diag.category} diag but expected {diag_category}")
248+
lines[line_n - 1].diag.count -= 1
249+
diag_errors_left = []
250+
diag_errors.sort(reverse=True, key=lambda t: t[0])
251+
for (line_n, diag_s, diag_category, seen) in diag_errors:
252+
if not seen:
253+
continue
254+
target = lines[line_n - 1]
255+
other_diags = [d for d in target.targeting_diags if d.diag_content == diag_s and d.category == diag_category]
256+
other_diag = other_diags[0] if other_diags else None
257+
if other_diag:
258+
other_diag.count += 1
259+
else:
260+
diag_errors_left.append((line_n, diag_s, diag_category))
261+
for (line_n, diag_s, diag_category) in diag_errors_left:
262+
add_diag(line_n, diag_s, diag_category, lines)
263+
with open(filename, 'w') as f:
264+
for line in lines:
265+
f.write(line.render())
266+
267+
def update_test_files(errors):
268+
errors_by_file = {}
269+
for ((filename, line, diag_s), (diag_category, seen)) in errors:
270+
if filename not in errors_by_file:
271+
errors_by_file[filename] = []
272+
errors_by_file[filename].append((line, diag_s, diag_category, seen))
273+
for filename, diag_errors in errors_by_file.items():
274+
try:
275+
update_test_file(filename, diag_errors)
276+
except KnownException as e:
277+
print(f"{filename} - ERROR: {e}")
278+
print("continuing...")
279+
curr = []
280+
curr_category = None
281+
curr_run_line = None
282+
lines_since_run = []
283+
for line in sys.stdin.readlines():
284+
lines_since_run.append(line)
285+
try:
286+
if line.startswith("RUN:"):
287+
if curr:
288+
update_test_files(curr)
289+
curr = []
290+
lines_since_run = [line]
291+
curr_run_line = line
292+
else:
293+
for line in lines_since_run:
294+
print(line, end="")
295+
print("====================")
296+
print("no mismatching diagnostics found since last RUN line")
297+
continue
298+
if line.startswith("error: "):
299+
if "no expected directives found" in line:
300+
print(f"no expected directives found for RUN line '{curr_run_line.strip()}'. Add 'expected-no-diagnostics' manually if this is intended.")
301+
continue
302+
curr_category = parse_error_category(line[len("error: "):])
303+
continue
304+
305+
diag_error = parse_diag_error(line.strip())
306+
if diag_error:
307+
curr.append((diag_error, curr_category))
308+
except Exception as e:
309+
for line in lines_since_run:
310+
print(line, end="")
311+
print("====================")
312+
print(e)
313+
sys.exit(1)
314+
if curr:
315+
update_test_files(curr)
316+
print("done!")
317+
else:
318+
for line in lines_since_run:
319+
print(line, end="")
320+
print("====================")
321+
print("no mismatching diagnostics found")

0 commit comments

Comments
 (0)