|
| 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