[clang] [Utils] add --update-tests flag to llvm-lit (PR #97369)
Henrik G. Olsson via cfe-commits
cfe-commits at lists.llvm.org
Wed Sep 11 17:14:25 PDT 2024
https://github.com/hnrklssn updated https://github.com/llvm/llvm-project/pull/97369
>From d93d77e193f235d12d4de4a4b184c458508fa8df Mon Sep 17 00:00:00 2001
From: "Henrik G. Olsson" <h_olsson at apple.com>
Date: Mon, 1 Jul 2024 18:19:09 -0700
Subject: [PATCH 1/4] [Utils] 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.
---
clang/utils/update-verify-tests.py | 404 +++++++++++++++++++++++++++++
1 file changed, 404 insertions(+)
create mode 100644 clang/utils/update-verify-tests.py
diff --git a/clang/utils/update-verify-tests.py b/clang/utils/update-verify-tests.py
new file mode 100644
index 00000000000000..cfcfefc85e576a
--- /dev/null
+++ b/clang/utils/update-verify-tests.py
@@ -0,0 +1,404 @@
+import sys
+import re
+
+"""
+ Pipe output from clang's -verify into this script to have the test case updated to expect the actual diagnostic output.
+ When inserting new expected-* checks it will place them on the line before the location of the diagnostic, with an @+1,
+ or @+N for some N if there are multiple diagnostics emitted on the same line. If the current checks are using @-N for
+ this line, the new check will follow that convention also.
+ Existing checks will be left untouched as much as possible, including their location and whitespace content, to minimize
+ diffs. If inaccurate their count will be updated, or the check removed entirely.
+
+ Missing features:
+ - custom prefix support (-verify=my-prefix)
+ - multiple prefixes on the same line (-verify=my-prefix,my-other-prefix)
+ - multiple prefixes on separate RUN lines (RUN: -verify=my-prefix\nRUN: -verify my-other-prefix)
+ - regexes with expected-*-re: existing ones will be left untouched if accurate, but the script will abort if there are any
+ diagnostic mismatches on the same line.
+ - multiple checks targeting the same line are supported, but a line may only contain one check
+ - if multiple checks targeting the same line are failing the script is not guaranteed to produce a minimal diff
+
+Example usage:
+ build/bin/llvm-lit clang/test/Sema/ --no-progress-bar -v | python3 update-verify-tests.py
+"""
+
+
+class KnownException(Exception):
+ pass
+
+
+def parse_error_category(s):
+ parts = s.split("diagnostics")
+ diag_category = parts[0]
+ category_parts = parts[0].strip().strip("'").split("-")
+ expected = category_parts[0]
+ if expected != "expected":
+ raise Exception(
+ f"expected 'expected', but found '{expected}'. Custom verify prefixes are not supported."
+ )
+ diag_category = category_parts[1]
+ if "seen but not expected" in parts[1]:
+ seen = True
+ elif "expected but not seen" in parts[1]:
+ seen = False
+ else:
+ raise KnownException(f"unexpected category '{parts[1]}'")
+ return (diag_category, seen)
+
+
+diag_error_re = re.compile(r"File (\S+) Line (\d+): (.+)")
+diag_error_re2 = re.compile(r"File \S+ Line \d+ \(directive at (\S+):(\d+)\): (.+)")
+
+
+def parse_diag_error(s):
+ m = diag_error_re2.match(s)
+ if not m:
+ m = diag_error_re.match(s)
+ if not m:
+ return None
+ return (m.group(1), int(m.group(2)), m.group(3))
+
+
+class Line:
+ def __init__(self, content, line_n):
+ self.content = content
+ self.diag = None
+ self.line_n = line_n
+ self.related_diags = []
+ self.targeting_diags = []
+
+ def update_line_n(self, n):
+ if self.diag and not self.diag.line_is_absolute:
+ self.diag.orig_target_line_n += n - self.line_n
+ self.line_n = n
+ for diag in self.targeting_diags:
+ if diag.line_is_absolute:
+ diag.orig_target_line_n = n
+ else:
+ diag.orig_target_line_n = n - diag.line.line_n
+ for diag in self.related_diags:
+ if not diag.line_is_absolute:
+ pass
+
+ def render(self):
+ if not self.diag:
+ return self.content
+ assert "{{DIAG}}" in self.content
+ res = self.content.replace("{{DIAG}}", self.diag.render())
+ if not res.strip():
+ return ""
+ return res
+
+
+class Diag:
+ def __init__(
+ self,
+ diag_content,
+ category,
+ targeted_line_n,
+ line_is_absolute,
+ count,
+ line,
+ is_re,
+ whitespace_strings,
+ ):
+ self.diag_content = diag_content
+ self.category = category
+ self.orig_target_line_n = targeted_line_n
+ self.line_is_absolute = line_is_absolute
+ self.count = count
+ self.line = line
+ self.target = None
+ self.is_re = is_re
+ self.absolute_target()
+ self.whitespace_strings = whitespace_strings
+
+ def add(self):
+ if targeted_line > 0:
+ targeted_line += 1
+ elif targeted_line < 0:
+ targeted_line -= 1
+
+ def absolute_target(self):
+ if self.line_is_absolute:
+ res = self.orig_target_line_n
+ else:
+ res = self.line.line_n + self.orig_target_line_n
+ if self.target:
+ assert self.line.line_n == res
+ return res
+
+ def relative_target(self):
+ return self.absolute_target() - self.line.line_n
+
+ def render(self):
+ assert self.count >= 0
+ if self.count == 0:
+ return ""
+ line_location_s = ""
+ if self.relative_target() != 0:
+ if self.line_is_absolute:
+ line_location_s = f"@{self.absolute_target()}"
+ elif self.relative_target() > 0:
+ line_location_s = f"@+{self.relative_target()}"
+ else:
+ line_location_s = (
+ f"@{self.relative_target()}" # the minus sign is implicit
+ )
+ count_s = "" if self.count == 1 else f"{self.count}"
+ re_s = "-re" if self.is_re else ""
+ if self.whitespace_strings:
+ whitespace1_s = self.whitespace_strings[0]
+ whitespace2_s = self.whitespace_strings[1]
+ whitespace3_s = self.whitespace_strings[2]
+ whitespace4_s = self.whitespace_strings[3]
+ else:
+ whitespace1_s = " "
+ whitespace2_s = ""
+ whitespace3_s = ""
+ whitespace4_s = ""
+ if count_s and not whitespace3_s:
+ whitespace3_s = " "
+ return f"//{whitespace1_s}expected-{self.category}{re_s}{whitespace2_s}{line_location_s}{whitespace3_s}{count_s}{whitespace4_s}{{{{{self.diag_content}}}}}"
+
+
+expected_diag_re = re.compile(
+ r"//(\s*)expected-(note|warning|error)(-re)?(\s*)(@[+-]?\d+)?(\s*)(\d+)?(\s*)\{\{(.*)\}\}"
+)
+
+
+def parse_diag(line, filename, lines):
+ s = line.content
+ ms = expected_diag_re.findall(s)
+ if not ms:
+ return None
+ if len(ms) > 1:
+ print(
+ f"multiple diags on line {filename}:{line.line_n}. Aborting due to missing implementation."
+ )
+ sys.exit(1)
+ [
+ whitespace1_s,
+ category_s,
+ re_s,
+ whitespace2_s,
+ target_line_s,
+ whitespace3_s,
+ count_s,
+ whitespace4_s,
+ diag_s,
+ ] = ms[0]
+ if not target_line_s:
+ target_line_n = 0
+ is_absolute = False
+ elif target_line_s.startswith("@+"):
+ target_line_n = int(target_line_s[2:])
+ is_absolute = False
+ elif target_line_s.startswith("@-"):
+ target_line_n = int(target_line_s[1:])
+ is_absolute = False
+ else:
+ target_line_n = int(target_line_s[1:])
+ is_absolute = True
+ count = int(count_s) if count_s else 1
+ line.content = expected_diag_re.sub("{{DIAG}}", s)
+
+ return Diag(
+ diag_s,
+ category_s,
+ target_line_n,
+ is_absolute,
+ count,
+ line,
+ bool(re_s),
+ [whitespace1_s, whitespace2_s, whitespace3_s, whitespace4_s],
+ )
+
+
+def link_line_diags(lines, diag):
+ line_n = diag.line.line_n
+ target_line_n = diag.absolute_target()
+ step = 1 if target_line_n < line_n else -1
+ for i in range(target_line_n, line_n, step):
+ lines[i - 1].related_diags.append(diag)
+
+
+def add_line(new_line, lines):
+ lines.insert(new_line.line_n - 1, new_line)
+ for i in range(new_line.line_n, len(lines)):
+ line = lines[i]
+ assert line.line_n == i
+ line.update_line_n(i + 1)
+ assert all(line.line_n == i + 1 for i, line in enumerate(lines))
+
+
+indent_re = re.compile(r"\s*")
+
+
+def get_indent(s):
+ return indent_re.match(s).group(0)
+
+
+def add_diag(line_n, diag_s, diag_category, lines):
+ target = lines[line_n - 1]
+ for other in target.targeting_diags:
+ if other.is_re:
+ raise KnownException(
+ "mismatching diag on line with regex matcher. Skipping due to missing implementation"
+ )
+ reverse = (
+ True
+ if [other for other in target.targeting_diags if other.relative_target() < 0]
+ else False
+ )
+
+ targeting = [
+ other for other in target.targeting_diags if not other.line_is_absolute
+ ]
+ targeting.sort(reverse=reverse, key=lambda d: d.relative_target())
+ prev_offset = 0
+ prev_line = target
+ direction = -1 if reverse else 1
+ for d in targeting:
+ if d.relative_target() != prev_offset + direction:
+ break
+ prev_offset = d.relative_target()
+ prev_line = d.line
+ total_offset = prev_offset - 1 if reverse else prev_offset + 1
+ if reverse:
+ new_line_n = prev_line.line_n + 1
+ else:
+ new_line_n = prev_line.line_n
+ assert new_line_n == line_n + (not reverse) - total_offset
+
+ new_line = Line(get_indent(prev_line.content) + "{{DIAG}}\n", new_line_n)
+ new_line.related_diags = list(prev_line.related_diags)
+ add_line(new_line, lines)
+
+ new_diag = Diag(
+ diag_s, diag_category, total_offset, False, 1, new_line, False, None
+ )
+ new_line.diag = new_diag
+ new_diag.target_line = target
+ assert type(new_diag) != str
+ target.targeting_diags.append(new_diag)
+ link_line_diags(lines, new_diag)
+
+
+updated_test_files = set()
+
+
+def update_test_file(filename, diag_errors):
+ print(f"updating test file {filename}")
+ if filename in updated_test_files:
+ print(
+ f"{filename} already updated, but got new output - expect incorrect results"
+ )
+ else:
+ updated_test_files.add(filename)
+ with open(filename, "r") as f:
+ lines = [Line(line, i + 1) for i, line in enumerate(f.readlines())]
+ for line in lines:
+ diag = parse_diag(line, filename, lines)
+ if diag:
+ line.diag = diag
+ diag.target_line = lines[diag.absolute_target() - 1]
+ link_line_diags(lines, diag)
+ lines[diag.absolute_target() - 1].targeting_diags.append(diag)
+
+ for line_n, diag_s, diag_category, seen in diag_errors:
+ if seen:
+ continue
+ # this is a diagnostic expected but not seen
+ assert lines[line_n - 1].diag
+ if diag_s != lines[line_n - 1].diag.diag_content:
+ raise KnownException(
+ f"{filename}:{line_n} - found diag {lines[line_n - 1].diag.diag_content} but expected {diag_s}"
+ )
+ if diag_category != lines[line_n - 1].diag.category:
+ raise KnownException(
+ f"{filename}:{line_n} - found {lines[line_n - 1].diag.category} diag but expected {diag_category}"
+ )
+ lines[line_n - 1].diag.count -= 1
+ diag_errors_left = []
+ diag_errors.sort(reverse=True, key=lambda t: t[0])
+ for line_n, diag_s, diag_category, seen in diag_errors:
+ if not seen:
+ continue
+ target = lines[line_n - 1]
+ other_diags = [
+ d
+ for d in target.targeting_diags
+ if d.diag_content == diag_s and d.category == diag_category
+ ]
+ other_diag = other_diags[0] if other_diags else None
+ if other_diag:
+ other_diag.count += 1
+ else:
+ diag_errors_left.append((line_n, diag_s, diag_category))
+ for line_n, diag_s, diag_category in diag_errors_left:
+ add_diag(line_n, diag_s, diag_category, lines)
+ with open(filename, "w") as f:
+ for line in lines:
+ f.write(line.render())
+
+
+def update_test_files(errors):
+ errors_by_file = {}
+ for (filename, line, diag_s), (diag_category, seen) in errors:
+ if filename not in errors_by_file:
+ errors_by_file[filename] = []
+ errors_by_file[filename].append((line, diag_s, diag_category, seen))
+ for filename, diag_errors in errors_by_file.items():
+ try:
+ update_test_file(filename, diag_errors)
+ except KnownException as e:
+ print(f"{filename} - ERROR: {e}")
+ print("continuing...")
+
+
+curr = []
+curr_category = None
+curr_run_line = None
+lines_since_run = []
+for line in sys.stdin.readlines():
+ lines_since_run.append(line)
+ try:
+ if line.startswith("RUN:"):
+ if curr:
+ update_test_files(curr)
+ curr = []
+ lines_since_run = [line]
+ curr_run_line = line
+ else:
+ for line in lines_since_run:
+ print(line, end="")
+ print("====================")
+ print("no mismatching diagnostics found since last RUN line")
+ continue
+ if line.startswith("error: "):
+ if "no expected directives found" in line:
+ print(
+ f"no expected directives found for RUN line '{curr_run_line.strip()}'. Add 'expected-no-diagnostics' manually if this is intended."
+ )
+ continue
+ curr_category = parse_error_category(line[len("error: ") :])
+ continue
+
+ diag_error = parse_diag_error(line.strip())
+ if diag_error:
+ curr.append((diag_error, curr_category))
+ except Exception as e:
+ for line in lines_since_run:
+ print(line, end="")
+ print("====================")
+ print(e)
+ sys.exit(1)
+if curr:
+ update_test_files(curr)
+ print("done!")
+else:
+ for line in lines_since_run:
+ print(line, end="")
+ print("====================")
+ print("no mismatching diagnostics found")
>From e9919beb705213398ad4faaf4d2b70d6579bdf5a Mon Sep 17 00:00:00 2001
From: "Henrik G. Olsson" <h_olsson at apple.com>
Date: Tue, 27 Aug 2024 16:51:35 -0700
Subject: [PATCH 2/4] [Utils] Add testing for update-verify-tests, fix bugs
Fixes various line offset bugs. Fixes a bug where whitespace could be
emitted between 'expected-[category]' and the line location specifier,
which clang does not parse.
Now handles the cases where the file does not emit any diagnostics, or
previously contained 'expected-no-diagnostics'.
Also does further work to minimise diffs by replacing diagnostics checks
with a new one, even if the previous check was in a location that the
script would not normally emit. This is useful for the case where a
check is on the same line as the emitted diagnostic, and the diagnostic
message changes. Instead of removing the previous diagnostic and
inserting a new one with @+1, the old diagnostic text is replaced with
the new one.
---
.../Inputs/duplicate-diag.c | 8 +
.../Inputs/duplicate-diag.c.expected | 8 +
.../Inputs/infer-indentation.c | 8 +
.../Inputs/infer-indentation.c.expected | 11 +
.../Inputs/leave-existing-diags.c | 11 +
.../Inputs/leave-existing-diags.c.expected | 12 ++
.../Inputs/multiple-errors.c | 6 +
.../Inputs/multiple-errors.c.expected | 9 +
.../multiple-missing-errors-same-line.c | 8 +
...ltiple-missing-errors-same-line.c.expected | 13 ++
.../update-verify-tests/Inputs/no-checks.c | 3 +
.../Inputs/no-checks.c.expected | 4 +
.../update-verify-tests/Inputs/no-diags.c | 5 +
.../Inputs/no-diags.c.expected | 5 +
.../Inputs/no-expected-diags.c | 4 +
.../Inputs/no-expected-diags.c.expected | 4 +
.../Inputs/update-same-line.c | 4 +
.../Inputs/update-same-line.c.expected | 4 +
.../Inputs/update-single-check.c | 4 +
.../Inputs/update-single-check.c.expected | 4 +
.../update-verify-tests/duplicate-diag.test | 4 +
.../infer-indentation.test | 3 +
.../leave-existing-diags.test | 4 +
.../utils/update-verify-tests/lit.local.cfg | 25 +++
.../update-verify-tests/multiple-errors.test | 3 +
.../multiple-missing-errors-same-line.test | 3 +
.../utils/update-verify-tests/no-checks.test | 3 +
.../utils/update-verify-tests/no-diags.test | 4 +
.../no-expected-diags.test | 4 +
.../update-verify-tests/update-same-line.test | 4 +
.../update-single-check.test | 3 +
clang/utils/update-verify-tests.py | 197 ++++++++++++------
32 files changed, 325 insertions(+), 67 deletions(-)
create mode 100644 clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c.expected
create mode 100644 clang/test/utils/update-verify-tests/Inputs/infer-indentation.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/infer-indentation.c.expected
create mode 100644 clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c.expected
create mode 100644 clang/test/utils/update-verify-tests/Inputs/multiple-errors.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/multiple-errors.c.expected
create mode 100644 clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c.expected
create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-checks.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-checks.c.expected
create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-diags.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-diags.c.expected
create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c.expected
create mode 100644 clang/test/utils/update-verify-tests/Inputs/update-same-line.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/update-same-line.c.expected
create mode 100644 clang/test/utils/update-verify-tests/Inputs/update-single-check.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/update-single-check.c.expected
create mode 100644 clang/test/utils/update-verify-tests/duplicate-diag.test
create mode 100644 clang/test/utils/update-verify-tests/infer-indentation.test
create mode 100644 clang/test/utils/update-verify-tests/leave-existing-diags.test
create mode 100644 clang/test/utils/update-verify-tests/lit.local.cfg
create mode 100644 clang/test/utils/update-verify-tests/multiple-errors.test
create mode 100644 clang/test/utils/update-verify-tests/multiple-missing-errors-same-line.test
create mode 100644 clang/test/utils/update-verify-tests/no-checks.test
create mode 100644 clang/test/utils/update-verify-tests/no-diags.test
create mode 100644 clang/test/utils/update-verify-tests/no-expected-diags.test
create mode 100644 clang/test/utils/update-verify-tests/update-same-line.test
create mode 100644 clang/test/utils/update-verify-tests/update-single-check.test
diff --git a/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c b/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c
new file mode 100644
index 00000000000000..8c7e46c6eca9c1
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c
@@ -0,0 +1,8 @@
+void foo() {
+ // expected-error at +1{{use of undeclared identifier 'a'}}
+ a = 2; a = 2;
+ b = 2; b = 2;
+ // expected-error at +1 3{{use of undeclared identifier 'c'}}
+ c = 2; c = 2;
+ // expected-error 2{{asdf}}
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c.expected b/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c.expected
new file mode 100644
index 00000000000000..6214ff382f4495
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c.expected
@@ -0,0 +1,8 @@
+void foo() {
+ // expected-error at +1 2{{use of undeclared identifier 'a'}}
+ a = 2; a = 2;
+ // expected-error at +1 2{{use of undeclared identifier 'b'}}
+ b = 2; b = 2;
+ // expected-error at +1 2{{use of undeclared identifier 'c'}}
+ c = 2; c = 2;
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c b/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c
new file mode 100644
index 00000000000000..0210ac35fd5cd1
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c
@@ -0,0 +1,8 @@
+void foo() {
+ // expected-error at +1 2 {{use of undeclared identifier 'a'}}
+ a = 2; a = 2; b = 2; b = 2; c = 2;
+ // expected-error at +1 2 {{asdf}}
+ d = 2;
+ e = 2; f = 2; // expected-error 2 {{use of undeclared identifier 'e'}}
+}
+
diff --git a/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c.expected b/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c.expected
new file mode 100644
index 00000000000000..5c5aaeeef97acf
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c.expected
@@ -0,0 +1,11 @@
+void foo() {
+ // expected-error at +3 {{use of undeclared identifier 'c'}}
+ // expected-error at +2 2 {{use of undeclared identifier 'b'}}
+ // expected-error at +1 2 {{use of undeclared identifier 'a'}}
+ a = 2; a = 2; b = 2; b = 2; c = 2;
+ // expected-error at +1 {{use of undeclared identifier 'd'}}
+ d = 2;
+ // expected-error at +1 {{use of undeclared identifier 'f'}}
+ e = 2; f = 2; // expected-error {{use of undeclared identifier 'e'}}
+}
+
diff --git a/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c b/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c
new file mode 100644
index 00000000000000..1aa8d088e97273
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c
@@ -0,0 +1,11 @@
+void foo() {
+ a = 2;
+ // expected-error at -1{{use of undeclared identifier 'a'}}
+ b = 2;// expected-error{{use of undeclared identifier 'b'}}
+ c = 2;
+ // expected-error at 5{{use of undeclared identifier 'c'}}
+ d = 2; // expected-error-re{{use of {{.*}} identifier 'd'}}
+
+ e = 2; // error to trigger mismatch
+}
+
diff --git a/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c.expected b/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c.expected
new file mode 100644
index 00000000000000..6b621061bbfbbd
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c.expected
@@ -0,0 +1,12 @@
+void foo() {
+ a = 2;
+ // expected-error at -1{{use of undeclared identifier 'a'}}
+ b = 2;// expected-error{{use of undeclared identifier 'b'}}
+ c = 2;
+ // expected-error at 5{{use of undeclared identifier 'c'}}
+ d = 2; // expected-error-re{{use of {{.*}} identifier 'd'}}
+
+ // expected-error at +1{{use of undeclared identifier 'e'}}
+ e = 2; // error to trigger mismatch
+}
+
diff --git a/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c b/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c
new file mode 100644
index 00000000000000..e230e0a337bf49
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c
@@ -0,0 +1,6 @@
+void foo() {
+ a = 2;
+ b = 2;
+
+ c = 2;
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c.expected b/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c.expected
new file mode 100644
index 00000000000000..27dc1f30a26faf
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c.expected
@@ -0,0 +1,9 @@
+void foo() {
+ // expected-error at +1{{use of undeclared identifier 'a'}}
+ a = 2;
+ // expected-error at +1{{use of undeclared identifier 'b'}}
+ b = 2;
+
+ // expected-error at +1{{use of undeclared identifier 'c'}}
+ c = 2;
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c b/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c
new file mode 100644
index 00000000000000..03f723d44bbe82
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c
@@ -0,0 +1,8 @@
+void foo() {
+ a = 2; b = 2; c = 2;
+}
+
+void bar() {
+ x = 2; y = 2; z = 2;
+ // expected-error at -1{{use of undeclared identifier 'x'}}
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c.expected b/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c.expected
new file mode 100644
index 00000000000000..24b57f4353d95d
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c.expected
@@ -0,0 +1,13 @@
+void foo() {
+ // expected-error at +3{{use of undeclared identifier 'c'}}
+ // expected-error at +2{{use of undeclared identifier 'b'}}
+ // expected-error at +1{{use of undeclared identifier 'a'}}
+ a = 2; b = 2; c = 2;
+}
+
+void bar() {
+ x = 2; y = 2; z = 2;
+ // expected-error at -1{{use of undeclared identifier 'x'}}
+ // expected-error at -2{{use of undeclared identifier 'y'}}
+ // expected-error at -3{{use of undeclared identifier 'z'}}
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/no-checks.c b/clang/test/utils/update-verify-tests/Inputs/no-checks.c
new file mode 100644
index 00000000000000..8fd1f7cd333705
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/no-checks.c
@@ -0,0 +1,3 @@
+void foo() {
+ bar = 2;
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/no-checks.c.expected b/clang/test/utils/update-verify-tests/Inputs/no-checks.c.expected
new file mode 100644
index 00000000000000..e80548fbe50f2c
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/no-checks.c.expected
@@ -0,0 +1,4 @@
+void foo() {
+ // expected-error at +1{{use of undeclared identifier 'bar'}}
+ bar = 2;
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/no-diags.c b/clang/test/utils/update-verify-tests/Inputs/no-diags.c
new file mode 100644
index 00000000000000..66d169be439402
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/no-diags.c
@@ -0,0 +1,5 @@
+void foo() {
+ // expected-error at +1{{asdf}}
+ int a = 2;
+}
+
diff --git a/clang/test/utils/update-verify-tests/Inputs/no-diags.c.expected b/clang/test/utils/update-verify-tests/Inputs/no-diags.c.expected
new file mode 100644
index 00000000000000..05230284945702
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/no-diags.c.expected
@@ -0,0 +1,5 @@
+// expected-no-diagnostics
+void foo() {
+ int a = 2;
+}
+
diff --git a/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c b/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c
new file mode 100644
index 00000000000000..78b72e1357da76
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c
@@ -0,0 +1,4 @@
+// expected-no-diagnostics
+void foo() {
+ a = 2;
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c.expected b/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c.expected
new file mode 100644
index 00000000000000..d948ffce56189a
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c.expected
@@ -0,0 +1,4 @@
+void foo() {
+ // expected-error at +1{{use of undeclared identifier 'a'}}
+ a = 2;
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/update-same-line.c b/clang/test/utils/update-verify-tests/Inputs/update-same-line.c
new file mode 100644
index 00000000000000..5278ce0c57c319
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/update-same-line.c
@@ -0,0 +1,4 @@
+void foo() {
+ bar = 2; // expected-error {{asdf}}
+}
+
diff --git a/clang/test/utils/update-verify-tests/Inputs/update-same-line.c.expected b/clang/test/utils/update-verify-tests/Inputs/update-same-line.c.expected
new file mode 100644
index 00000000000000..8ba47f788319b1
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/update-same-line.c.expected
@@ -0,0 +1,4 @@
+void foo() {
+ bar = 2; // expected-error {{use of undeclared identifier 'bar'}}
+}
+
diff --git a/clang/test/utils/update-verify-tests/Inputs/update-single-check.c b/clang/test/utils/update-verify-tests/Inputs/update-single-check.c
new file mode 100644
index 00000000000000..20b011bfc3d77e
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/update-single-check.c
@@ -0,0 +1,4 @@
+void foo() {
+ // expected-error at +1{{asdf}}
+ bar = 2;
+}
diff --git a/clang/test/utils/update-verify-tests/Inputs/update-single-check.c.expected b/clang/test/utils/update-verify-tests/Inputs/update-single-check.c.expected
new file mode 100644
index 00000000000000..e80548fbe50f2c
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/update-single-check.c.expected
@@ -0,0 +1,4 @@
+void foo() {
+ // expected-error at +1{{use of undeclared identifier 'bar'}}
+ bar = 2;
+}
diff --git a/clang/test/utils/update-verify-tests/duplicate-diag.test b/clang/test/utils/update-verify-tests/duplicate-diag.test
new file mode 100644
index 00000000000000..3163ce46199c3f
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/duplicate-diag.test
@@ -0,0 +1,4 @@
+# RUN: cp %S/Inputs/duplicate-diag.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests
+# RUN: diff -u %S/Inputs/duplicate-diag.c.expected %t.c
+# RUN: %clang_cc1 -verify %t.c
+
diff --git a/clang/test/utils/update-verify-tests/infer-indentation.test b/clang/test/utils/update-verify-tests/infer-indentation.test
new file mode 100644
index 00000000000000..6ba2f5d9d505bf
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/infer-indentation.test
@@ -0,0 +1,3 @@
+# RUN: cp %S/Inputs/infer-indentation.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests
+# RUN: diff -u %S/Inputs/infer-indentation.c.expected %t.c
+# RUN: %clang_cc1 -verify %t.c
diff --git a/clang/test/utils/update-verify-tests/leave-existing-diags.test b/clang/test/utils/update-verify-tests/leave-existing-diags.test
new file mode 100644
index 00000000000000..cde690ef715a67
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/leave-existing-diags.test
@@ -0,0 +1,4 @@
+# RUN: cp %S/Inputs/leave-existing-diags.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests
+# RUN: diff -u %S/Inputs/leave-existing-diags.c.expected %t.c
+# RUN: %clang_cc1 -verify %t.c
+
diff --git a/clang/test/utils/update-verify-tests/lit.local.cfg b/clang/test/utils/update-verify-tests/lit.local.cfg
new file mode 100644
index 00000000000000..a0b6afccc25010
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/lit.local.cfg
@@ -0,0 +1,25 @@
+import lit.util
+
+# python 2.7 backwards compatibility
+try:
+ from shlex import quote as shell_quote
+except ImportError:
+ from pipes import quote as shell_quote
+
+if config.standalone_build:
+ # These tests require the update-verify-tests.py script from the clang
+ # source tree, so skip these tests if we are doing standalone builds.
+ config.unsupported = True
+else:
+ config.suffixes = [".test"]
+
+ script_path = os.path.join(
+ config.clang_src_dir, "utils", "update-verify-tests.py"
+ )
+ python = shell_quote(config.python_executable)
+ config.substitutions.append(
+ (
+ "%update-verify-tests",
+ "%s %s" % (python, shell_quote(script_path)),
+ )
+ )
diff --git a/clang/test/utils/update-verify-tests/multiple-errors.test b/clang/test/utils/update-verify-tests/multiple-errors.test
new file mode 100644
index 00000000000000..1332ef365dc863
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/multiple-errors.test
@@ -0,0 +1,3 @@
+# RUN: cp %S/Inputs/multiple-errors.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests
+# RUN: diff -u %S/Inputs/multiple-errors.c.expected %t.c
+# RUN: %clang_cc1 -verify %t.c
diff --git a/clang/test/utils/update-verify-tests/multiple-missing-errors-same-line.test b/clang/test/utils/update-verify-tests/multiple-missing-errors-same-line.test
new file mode 100644
index 00000000000000..a9c21cd77e192b
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/multiple-missing-errors-same-line.test
@@ -0,0 +1,3 @@
+# RUN: cp %S/Inputs/multiple-missing-errors-same-line.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests
+# RUN: diff -u %S/Inputs/multiple-missing-errors-same-line.c.expected %t.c
+# RUN: %clang_cc1 -verify %t.c
diff --git a/clang/test/utils/update-verify-tests/no-checks.test b/clang/test/utils/update-verify-tests/no-checks.test
new file mode 100644
index 00000000000000..f6ea91fa552be4
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/no-checks.test
@@ -0,0 +1,3 @@
+# RUN: cp %S/Inputs/no-checks.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests
+# RUN: diff -u %S/Inputs/no-checks.c.expected %t.c
+# RUN: %clang_cc1 -verify %t.c
diff --git a/clang/test/utils/update-verify-tests/no-diags.test b/clang/test/utils/update-verify-tests/no-diags.test
new file mode 100644
index 00000000000000..464fe8894253b6
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/no-diags.test
@@ -0,0 +1,4 @@
+# RUN: cp %S/Inputs/no-diags.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests
+# RUN: diff -u %S/Inputs/no-diags.c.expected %t.c
+# RUN: %clang_cc1 -verify %t.c
+
diff --git a/clang/test/utils/update-verify-tests/no-expected-diags.test b/clang/test/utils/update-verify-tests/no-expected-diags.test
new file mode 100644
index 00000000000000..75235f17a64a29
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/no-expected-diags.test
@@ -0,0 +1,4 @@
+# RUN: cp %S/Inputs/no-expected-diags.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests
+# RUN: diff -u %S/Inputs/no-expected-diags.c.expected %t.c
+# RUN: %clang_cc1 -verify %t.c
+
diff --git a/clang/test/utils/update-verify-tests/update-same-line.test b/clang/test/utils/update-verify-tests/update-same-line.test
new file mode 100644
index 00000000000000..324768eae5faac
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/update-same-line.test
@@ -0,0 +1,4 @@
+# RUN: cp %S/Inputs/update-same-line.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests
+# RUN: diff -u %S/Inputs/update-same-line.c.expected %t.c
+# RUN: %clang_cc1 -verify %t.c
+
diff --git a/clang/test/utils/update-verify-tests/update-single-check.test b/clang/test/utils/update-verify-tests/update-single-check.test
new file mode 100644
index 00000000000000..2cb1ae3bcbd3b8
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/update-single-check.test
@@ -0,0 +1,3 @@
+# RUN: cp %S/Inputs/update-single-check.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests
+# RUN: diff -u %S/Inputs/update-single-check.c.expected %t.c
+# RUN: %clang_cc1 -verify %t.c
diff --git a/clang/utils/update-verify-tests.py b/clang/utils/update-verify-tests.py
index cfcfefc85e576a..f40ce15e449f22 100644
--- a/clang/utils/update-verify-tests.py
+++ b/clang/utils/update-verify-tests.py
@@ -28,6 +28,8 @@ class KnownException(Exception):
def parse_error_category(s):
+ if "no expected directives found" in line:
+ return None
parts = s.split("diagnostics")
diag_category = parts[0]
category_parts = parts[0].strip().strip("'").split("-")
@@ -64,21 +66,10 @@ def __init__(self, content, line_n):
self.content = content
self.diag = None
self.line_n = line_n
- self.related_diags = []
self.targeting_diags = []
def update_line_n(self, n):
- if self.diag and not self.diag.line_is_absolute:
- self.diag.orig_target_line_n += n - self.line_n
self.line_n = n
- for diag in self.targeting_diags:
- if diag.line_is_absolute:
- diag.orig_target_line_n = n
- else:
- diag.orig_target_line_n = n - diag.line.line_n
- for diag in self.related_diags:
- if not diag.line_is_absolute:
- pass
def render(self):
if not self.diag:
@@ -95,16 +86,17 @@ def __init__(
self,
diag_content,
category,
- targeted_line_n,
+ parsed_target_line_n,
line_is_absolute,
count,
line,
is_re,
whitespace_strings,
+ is_from_source_file,
):
self.diag_content = diag_content
self.category = category
- self.orig_target_line_n = targeted_line_n
+ self.parsed_target_line_n = parsed_target_line_n
self.line_is_absolute = line_is_absolute
self.count = count
self.line = line
@@ -112,25 +104,50 @@ def __init__(
self.is_re = is_re
self.absolute_target()
self.whitespace_strings = whitespace_strings
+ self.is_from_source_file = is_from_source_file
+
+ def decrement_count(self):
+ self.count -= 1
+ assert self.count >= 0
+
+ def increment_count(self):
+ assert self.count >= 0
+ self.count += 1
- def add(self):
- if targeted_line > 0:
- targeted_line += 1
- elif targeted_line < 0:
- targeted_line -= 1
+ def unset_target(self):
+ assert self.target is not None
+ self.target.targeting_diags.remove(self)
+ self.target = None
+
+ def set_target(self, target):
+ if self.target:
+ self.unset_target()
+ self.target = target
+ self.target.targeting_diags.append(self)
def absolute_target(self):
- if self.line_is_absolute:
- res = self.orig_target_line_n
- else:
- res = self.line.line_n + self.orig_target_line_n
if self.target:
- assert self.line.line_n == res
- return res
+ return self.target.line_n
+ if self.line_is_absolute:
+ return self.parsed_target_line_n
+ return self.line.line_n + self.parsed_target_line_n
def relative_target(self):
return self.absolute_target() - self.line.line_n
+ def take(self, other_diag):
+ assert self.count == 0
+ assert other_diag.count > 0
+ assert other_diag.target == self.target
+ assert not other_diag.line_is_absolute
+ assert not other_diag.is_re and not self.is_re
+ self.line_is_absolute = False
+ self.diag_content = other_diag.diag_content
+ self.count = other_diag.count
+ self.category = other_diag.category
+ self.count = other_diag.count
+ other_diag.count = 0
+
def render(self):
assert self.count >= 0
if self.count == 0:
@@ -151,19 +168,25 @@ def render(self):
whitespace1_s = self.whitespace_strings[0]
whitespace2_s = self.whitespace_strings[1]
whitespace3_s = self.whitespace_strings[2]
- whitespace4_s = self.whitespace_strings[3]
else:
whitespace1_s = " "
whitespace2_s = ""
whitespace3_s = ""
- whitespace4_s = ""
- if count_s and not whitespace3_s:
- whitespace3_s = " "
- return f"//{whitespace1_s}expected-{self.category}{re_s}{whitespace2_s}{line_location_s}{whitespace3_s}{count_s}{whitespace4_s}{{{{{self.diag_content}}}}}"
-
+ if count_s and not whitespace2_s:
+ whitespace2_s = " " # required to parse correctly
+ elif not count_s and whitespace2_s == " ":
+ """ Don't emit a weird extra space.
+ However if the whitespace is something other than the
+ standard single space, let it be to avoid disrupting manual formatting.
+ The existence of a non-empty whitespace2_s implies this was parsed with
+ a count > 1 and then decremented, otherwise this whitespace would have
+ been parsed as whitespace3_s.
+ """
+ whitespace2_s = ""
+ return f"//{whitespace1_s}expected-{self.category}{re_s}{line_location_s}{whitespace2_s}{count_s}{whitespace3_s}{{{{{self.diag_content}}}}}"
expected_diag_re = re.compile(
- r"//(\s*)expected-(note|warning|error)(-re)?(\s*)(@[+-]?\d+)?(\s*)(\d+)?(\s*)\{\{(.*)\}\}"
+ r"//(\s*)expected-(note|warning|error)(-re)?(@[+-]?\d+)?(?:(\s*)(\d+))?(\s*)\{\{(.*)\}\}"
)
@@ -173,19 +196,17 @@ def parse_diag(line, filename, lines):
if not ms:
return None
if len(ms) > 1:
- print(
+ raise KnownException(
f"multiple diags on line {filename}:{line.line_n}. Aborting due to missing implementation."
)
- sys.exit(1)
[
whitespace1_s,
category_s,
re_s,
- whitespace2_s,
target_line_s,
- whitespace3_s,
+ whitespace2_s,
count_s,
- whitespace4_s,
+ whitespace3_s,
diag_s,
] = ms[0]
if not target_line_s:
@@ -211,18 +232,11 @@ def parse_diag(line, filename, lines):
count,
line,
bool(re_s),
- [whitespace1_s, whitespace2_s, whitespace3_s, whitespace4_s],
+ [whitespace1_s, whitespace2_s, whitespace3_s],
+ True,
)
-def link_line_diags(lines, diag):
- line_n = diag.line.line_n
- target_line_n = diag.absolute_target()
- step = 1 if target_line_n < line_n else -1
- for i in range(target_line_n, line_n, step):
- lines[i - 1].related_diags.append(diag)
-
-
def add_line(new_line, lines):
lines.insert(new_line.line_n - 1, new_line)
for i in range(new_line.line_n, len(lines)):
@@ -231,6 +245,14 @@ def add_line(new_line, lines):
line.update_line_n(i + 1)
assert all(line.line_n == i + 1 for i, line in enumerate(lines))
+def remove_line(old_line, lines):
+ lines.remove(old_line)
+ for i in range(old_line.line_n - 1, len(lines)):
+ line = lines[i]
+ assert line.line_n == i + 2
+ line.update_line_n(i + 1)
+ assert all(line.line_n == i + 1 for i, line in enumerate(lines))
+
indent_re = re.compile(r"\s*")
@@ -238,8 +260,11 @@ def add_line(new_line, lines):
def get_indent(s):
return indent_re.match(s).group(0)
+def orig_line_n_to_new_line_n(line_n, orig_lines):
+ return orig_lines[line_n - 1].line_n
-def add_diag(line_n, diag_s, diag_category, lines):
+def add_diag(orig_line_n, diag_s, diag_category, lines, orig_lines):
+ line_n = orig_line_n_to_new_line_n(orig_line_n, orig_lines)
target = lines[line_n - 1]
for other in target.targeting_diags:
if other.is_re:
@@ -272,18 +297,42 @@ def add_diag(line_n, diag_s, diag_category, lines):
assert new_line_n == line_n + (not reverse) - total_offset
new_line = Line(get_indent(prev_line.content) + "{{DIAG}}\n", new_line_n)
- new_line.related_diags = list(prev_line.related_diags)
add_line(new_line, lines)
+ whitespace_strings = prev_line.diag.whitespace_strings if prev_line.diag else None
new_diag = Diag(
- diag_s, diag_category, total_offset, False, 1, new_line, False, None
+ diag_s, diag_category, total_offset, False, 1, new_line, False, whitespace_strings, False,
)
new_line.diag = new_diag
- new_diag.target_line = target
- assert type(new_diag) != str
- target.targeting_diags.append(new_diag)
- link_line_diags(lines, new_diag)
+ new_diag.set_target(target)
+
+def remove_dead_diags(lines):
+ for line in lines:
+ if not line.diag or line.diag.count != 0:
+ continue
+ if line.render() == "":
+ remove_line(line, lines)
+ else:
+ assert line.diag.is_from_source_file
+ for other_diag in line.targeting_diags:
+ if other_diag.is_from_source_file or other_diag.count == 0 or other_diag.category != line.diag.category:
+ continue
+ if other_diag.is_re or line.diag.is_re:
+ continue
+ line.diag.take(other_diag)
+ remove_line(other_diag.line, lines)
+
+def has_live_diags(lines):
+ for line in lines:
+ if line.diag and line.diag.count > 0:
+ return True
+ return False
+def get_expected_no_diags_line_n(lines):
+ for line in lines:
+ if "expected-no-diagnostics" in line.content:
+ return line.line_n
+ return None
updated_test_files = set()
@@ -298,13 +347,14 @@ def update_test_file(filename, diag_errors):
updated_test_files.add(filename)
with open(filename, "r") as f:
lines = [Line(line, i + 1) for i, line in enumerate(f.readlines())]
+ orig_lines = list(lines)
+ expected_no_diags_line_n = get_expected_no_diags_line_n(orig_lines)
+
for line in lines:
diag = parse_diag(line, filename, lines)
if diag:
line.diag = diag
- diag.target_line = lines[diag.absolute_target() - 1]
- link_line_diags(lines, diag)
- lines[diag.absolute_target() - 1].targeting_diags.append(diag)
+ diag.set_target(lines[diag.absolute_target() - 1])
for line_n, diag_s, diag_category, seen in diag_errors:
if seen:
@@ -319,13 +369,13 @@ def update_test_file(filename, diag_errors):
raise KnownException(
f"{filename}:{line_n} - found {lines[line_n - 1].diag.category} diag but expected {diag_category}"
)
- lines[line_n - 1].diag.count -= 1
+ lines[line_n - 1].diag.decrement_count()
diag_errors_left = []
diag_errors.sort(reverse=True, key=lambda t: t[0])
for line_n, diag_s, diag_category, seen in diag_errors:
if not seen:
continue
- target = lines[line_n - 1]
+ target = orig_lines[line_n - 1]
other_diags = [
d
for d in target.targeting_diags
@@ -333,13 +383,17 @@ def update_test_file(filename, diag_errors):
]
other_diag = other_diags[0] if other_diags else None
if other_diag:
- other_diag.count += 1
+ other_diag.increment_count()
else:
- diag_errors_left.append((line_n, diag_s, diag_category))
- for line_n, diag_s, diag_category in diag_errors_left:
- add_diag(line_n, diag_s, diag_category, lines)
+ add_diag(line_n, diag_s, diag_category, lines, orig_lines)
+ remove_dead_diags(lines)
+ has_diags = has_live_diags(lines)
with open(filename, "w") as f:
+ if not has_diags and expected_no_diags_line_n is None:
+ f.write("// expected-no-diagnostics\n")
for line in lines:
+ if has_diags and line.line_n == expected_no_diags_line_n:
+ continue
f.write(line.render())
@@ -361,6 +415,7 @@ def update_test_files(errors):
curr_category = None
curr_run_line = None
lines_since_run = []
+skip_to_next_file = False
for line in sys.stdin.readlines():
lines_since_run.append(line)
try:
@@ -374,20 +429,28 @@ def update_test_files(errors):
for line in lines_since_run:
print(line, end="")
print("====================")
- print("no mismatching diagnostics found since last RUN line")
+ if lines_since_run:
+ print("no mismatching diagnostics found since last RUN line")
+ skip_to_next_file = False
+ continue
+ if skip_to_next_file:
continue
if line.startswith("error: "):
- if "no expected directives found" in line:
- print(
- f"no expected directives found for RUN line '{curr_run_line.strip()}'. Add 'expected-no-diagnostics' manually if this is intended."
- )
- continue
curr_category = parse_error_category(line[len("error: ") :])
continue
diag_error = parse_diag_error(line.strip())
if diag_error:
curr.append((diag_error, curr_category))
+ except KnownException as e:
+ print(f"Error while parsing: {e}")
+ if curr:
+ print("skipping to next file")
+ curr = []
+ curr_category = None
+ curr_run_line = None
+ lines_since_run = []
+ skip_to_next_file = True
except Exception as e:
for line in lines_since_run:
print(line, end="")
>From dd702ce2be48dcf225755d7228b7993b698e888a Mon Sep 17 00:00:00 2001
From: "Henrik G. Olsson" <h_olsson at apple.com>
Date: Tue, 27 Aug 2024 17:42:50 -0700
Subject: [PATCH 3/4] [Utils] Add custom prefix support to update-verify-tests
Custom prefixes can now be provided with --prefix. The default remains
'expected'.
---
.../Inputs/non-default-prefix.c | 5 +
.../Inputs/non-default-prefix.c.expected | 5 +
.../non-default-prefix.test | 4 +
clang/utils/update-verify-tests.py | 153 ++++++++++--------
4 files changed, 98 insertions(+), 69 deletions(-)
create mode 100644 clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c
create mode 100644 clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c.expected
create mode 100644 clang/test/utils/update-verify-tests/non-default-prefix.test
diff --git a/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c b/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c
new file mode 100644
index 00000000000000..3d63eaf0f1b878
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c
@@ -0,0 +1,5 @@
+void foo() {
+ a = 2; // check-error{{asdf}}
+ // expected-error at -1{ignored}}
+}
+
diff --git a/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c.expected b/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c.expected
new file mode 100644
index 00000000000000..a877f86922123d
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c.expected
@@ -0,0 +1,5 @@
+void foo() {
+ a = 2; // check-error{{use of undeclared identifier 'a'}}
+ // expected-error at -1{ignored}}
+}
+
diff --git a/clang/test/utils/update-verify-tests/non-default-prefix.test b/clang/test/utils/update-verify-tests/non-default-prefix.test
new file mode 100644
index 00000000000000..e581755a6e6038
--- /dev/null
+++ b/clang/test/utils/update-verify-tests/non-default-prefix.test
@@ -0,0 +1,4 @@
+# RUN: cp %S/Inputs/non-default-prefix.c %t.c && not %clang_cc1 -verify=check %t.c 2>&1 | %update-verify-tests --prefix check
+# RUN: diff -u %S/Inputs/non-default-prefix.c.expected %t.c
+# RUN: %clang_cc1 -verify=check %t.c
+
diff --git a/clang/utils/update-verify-tests.py b/clang/utils/update-verify-tests.py
index f40ce15e449f22..dd83a9418ddf5f 100644
--- a/clang/utils/update-verify-tests.py
+++ b/clang/utils/update-verify-tests.py
@@ -1,5 +1,6 @@
import sys
import re
+import argparse
"""
Pipe output from clang's -verify into this script to have the test case updated to expect the actual diagnostic output.
@@ -10,7 +11,6 @@
diffs. If inaccurate their count will be updated, or the check removed entirely.
Missing features:
- - custom prefix support (-verify=my-prefix)
- multiple prefixes on the same line (-verify=my-prefix,my-other-prefix)
- multiple prefixes on separate RUN lines (RUN: -verify=my-prefix\nRUN: -verify my-other-prefix)
- regexes with expected-*-re: existing ones will be left untouched if accurate, but the script will abort if there are any
@@ -27,16 +27,16 @@ class KnownException(Exception):
pass
-def parse_error_category(s):
- if "no expected directives found" in line:
+def parse_error_category(s, prefix):
+ if "no expected directives found" in s:
return None
parts = s.split("diagnostics")
diag_category = parts[0]
category_parts = parts[0].strip().strip("'").split("-")
expected = category_parts[0]
- if expected != "expected":
+ if expected != prefix:
raise Exception(
- f"expected 'expected', but found '{expected}'. Custom verify prefixes are not supported."
+ f"expected prefix '{prefix}', but found '{expected}'. Multiple verify prefixes are not supported."
)
diag_category = category_parts[1]
if "seen but not expected" in parts[1]:
@@ -84,6 +84,7 @@ def render(self):
class Diag:
def __init__(
self,
+ prefix,
diag_content,
category,
parsed_target_line_n,
@@ -94,6 +95,7 @@ def __init__(
whitespace_strings,
is_from_source_file,
):
+ self.prefix = prefix
self.diag_content = diag_content
self.category = category
self.parsed_target_line_n = parsed_target_line_n
@@ -183,14 +185,14 @@ def render(self):
been parsed as whitespace3_s.
"""
whitespace2_s = ""
- return f"//{whitespace1_s}expected-{self.category}{re_s}{line_location_s}{whitespace2_s}{count_s}{whitespace3_s}{{{{{self.diag_content}}}}}"
+ return f"//{whitespace1_s}{self.prefix}-{self.category}{re_s}{line_location_s}{whitespace2_s}{count_s}{whitespace3_s}{{{{{self.diag_content}}}}}"
expected_diag_re = re.compile(
- r"//(\s*)expected-(note|warning|error)(-re)?(@[+-]?\d+)?(?:(\s*)(\d+))?(\s*)\{\{(.*)\}\}"
+ r"//(\s*)([a-zA-Z]+)-(note|warning|error)(-re)?(@[+-]?\d+)?(?:(\s*)(\d+))?(\s*)\{\{(.*)\}\}"
)
-def parse_diag(line, filename, lines):
+def parse_diag(line, filename, lines, prefix):
s = line.content
ms = expected_diag_re.findall(s)
if not ms:
@@ -201,6 +203,7 @@ def parse_diag(line, filename, lines):
)
[
whitespace1_s,
+ check_prefix,
category_s,
re_s,
target_line_s,
@@ -209,6 +212,8 @@ def parse_diag(line, filename, lines):
whitespace3_s,
diag_s,
] = ms[0]
+ if check_prefix != prefix:
+ return None
if not target_line_s:
target_line_n = 0
is_absolute = False
@@ -225,6 +230,7 @@ def parse_diag(line, filename, lines):
line.content = expected_diag_re.sub("{{DIAG}}", s)
return Diag(
+ prefix,
diag_s,
category_s,
target_line_n,
@@ -263,7 +269,7 @@ def get_indent(s):
def orig_line_n_to_new_line_n(line_n, orig_lines):
return orig_lines[line_n - 1].line_n
-def add_diag(orig_line_n, diag_s, diag_category, lines, orig_lines):
+def add_diag(orig_line_n, diag_s, diag_category, lines, orig_lines, prefix):
line_n = orig_line_n_to_new_line_n(orig_line_n, orig_lines)
target = lines[line_n - 1]
for other in target.targeting_diags:
@@ -301,7 +307,7 @@ def add_diag(orig_line_n, diag_s, diag_category, lines, orig_lines):
whitespace_strings = prev_line.diag.whitespace_strings if prev_line.diag else None
new_diag = Diag(
- diag_s, diag_category, total_offset, False, 1, new_line, False, whitespace_strings, False,
+ prefix, diag_s, diag_category, total_offset, False, 1, new_line, False, whitespace_strings, False,
)
new_line.diag = new_diag
new_diag.set_target(target)
@@ -328,16 +334,16 @@ def has_live_diags(lines):
return True
return False
-def get_expected_no_diags_line_n(lines):
+def get_expected_no_diags_line_n(lines, prefix):
for line in lines:
- if "expected-no-diagnostics" in line.content:
+ if f"{prefix}-no-diagnostics" in line.content:
return line.line_n
return None
updated_test_files = set()
-def update_test_file(filename, diag_errors):
+def update_test_file(filename, diag_errors, prefix):
print(f"updating test file {filename}")
if filename in updated_test_files:
print(
@@ -348,10 +354,10 @@ def update_test_file(filename, diag_errors):
with open(filename, "r") as f:
lines = [Line(line, i + 1) for i, line in enumerate(f.readlines())]
orig_lines = list(lines)
- expected_no_diags_line_n = get_expected_no_diags_line_n(orig_lines)
+ expected_no_diags_line_n = get_expected_no_diags_line_n(orig_lines, prefix)
for line in lines:
- diag = parse_diag(line, filename, lines)
+ diag = parse_diag(line, filename, lines, prefix)
if diag:
line.diag = diag
diag.set_target(lines[diag.absolute_target() - 1])
@@ -385,7 +391,7 @@ def update_test_file(filename, diag_errors):
if other_diag:
other_diag.increment_count()
else:
- add_diag(line_n, diag_s, diag_category, lines, orig_lines)
+ add_diag(line_n, diag_s, diag_category, lines, orig_lines, prefix)
remove_dead_diags(lines)
has_diags = has_live_diags(lines)
with open(filename, "w") as f:
@@ -397,7 +403,7 @@ def update_test_file(filename, diag_errors):
f.write(line.render())
-def update_test_files(errors):
+def update_test_files(errors, prefix):
errors_by_file = {}
for (filename, line, diag_s), (diag_category, seen) in errors:
if filename not in errors_by_file:
@@ -405,63 +411,72 @@ def update_test_files(errors):
errors_by_file[filename].append((line, diag_s, diag_category, seen))
for filename, diag_errors in errors_by_file.items():
try:
- update_test_file(filename, diag_errors)
+ update_test_file(filename, diag_errors, prefix)
except KnownException as e:
print(f"{filename} - ERROR: {e}")
print("continuing...")
-
-curr = []
-curr_category = None
-curr_run_line = None
-lines_since_run = []
-skip_to_next_file = False
-for line in sys.stdin.readlines():
- lines_since_run.append(line)
- try:
- if line.startswith("RUN:"):
- if curr:
- update_test_files(curr)
+def check_expectations(tool_output, prefix):
+ curr = []
+ curr_category = None
+ curr_run_line = None
+ lines_since_run = []
+ skip_to_next_file = False
+ for line in tool_output:
+ lines_since_run.append(line)
+ try:
+ if line.startswith("RUN:"):
+ if curr:
+ update_test_files(curr, prefix)
+ curr = []
+ lines_since_run = [line]
+ curr_run_line = line
+ else:
+ for line in lines_since_run:
+ print(line, end="")
+ print("====================")
+ if lines_since_run:
+ print("no mismatching diagnostics found since last RUN line")
+ skip_to_next_file = False
+ continue
+ if skip_to_next_file:
+ continue
+ if line.startswith("error: "):
+ curr_category = parse_error_category(line[len("error: ") :], prefix)
+ continue
+
+ diag_error = parse_diag_error(line.strip())
+ if diag_error:
+ curr.append((diag_error, curr_category))
+ except KnownException as e:
+ print(f"Error while parsing: {e}")
+ if curr:
+ print("skipping to next file")
curr = []
- lines_since_run = [line]
- curr_run_line = line
- else:
- for line in lines_since_run:
- print(line, end="")
- print("====================")
- if lines_since_run:
- print("no mismatching diagnostics found since last RUN line")
- skip_to_next_file = False
- continue
- if skip_to_next_file:
- continue
- if line.startswith("error: "):
- curr_category = parse_error_category(line[len("error: ") :])
- continue
-
- diag_error = parse_diag_error(line.strip())
- if diag_error:
- curr.append((diag_error, curr_category))
- except KnownException as e:
- print(f"Error while parsing: {e}")
- if curr:
- print("skipping to next file")
- curr = []
- curr_category = None
- curr_run_line = None
- lines_since_run = []
- skip_to_next_file = True
- except Exception as e:
+ curr_category = None
+ curr_run_line = None
+ lines_since_run = []
+ skip_to_next_file = True
+ except Exception as e:
+ for line in lines_since_run:
+ print(line, end="")
+ print("====================")
+ print(e)
+ sys.exit(1)
+ if curr:
+ update_test_files(curr, prefix)
+ print("done!")
+ else:
for line in lines_since_run:
print(line, end="")
print("====================")
- print(e)
- sys.exit(1)
-if curr:
- update_test_files(curr)
- print("done!")
-else:
- for line in lines_since_run:
- print(line, end="")
- print("====================")
- print("no mismatching diagnostics found")
+ print("no mismatching diagnostics found")
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--prefix", default="expected", help="The prefix passed to -verify")
+ args = parser.parse_args()
+ check_expectations(sys.stdin.readlines(), args.prefix)
+
+if __name__ == "__main__":
+ main()
>From 7395ef2db8c43f41cdbed70a4fa8c06579a078fe Mon Sep 17 00:00:00 2001
From: "Henrik G. Olsson" <h_olsson at apple.com>
Date: Wed, 11 Sep 2024 17:05:43 -0700
Subject: [PATCH 4/4] [Utils] Separate update-verify-tests script from core
The script no longer attempts to be RUN line aware. Feeding raw llvm-lit
output into it _may_ still work, but given a large enough number of
test cases to update, the risk that some test case uses unsupported
features grows pretty large. This will instead be handled by adding a
lit integration to automatically invoke the core for each failing test
output. The lit integration will be landed as a separate change.
---
clang/utils/UpdateVerifyTests/core.py | 452 +++++++++++++++++++++++++
clang/utils/update-verify-tests.py | 462 +-------------------------
2 files changed, 461 insertions(+), 453 deletions(-)
create mode 100644 clang/utils/UpdateVerifyTests/core.py
diff --git a/clang/utils/UpdateVerifyTests/core.py b/clang/utils/UpdateVerifyTests/core.py
new file mode 100644
index 00000000000000..d1350cdbb698b6
--- /dev/null
+++ b/clang/utils/UpdateVerifyTests/core.py
@@ -0,0 +1,452 @@
+import sys
+import re
+
+DEBUG = False
+
+
+def dprint(*args):
+ if DEBUG:
+ print(*args, file=sys.stderr)
+
+
+class KnownException(Exception):
+ pass
+
+
+def parse_error_category(s, prefix):
+ if "no expected directives found" in s:
+ return None
+ parts = s.split("diagnostics")
+ diag_category = parts[0]
+ category_parts = parts[0].strip().strip("'").split("-")
+ expected = category_parts[0]
+ if expected != prefix:
+ raise Exception(
+ f"expected prefix '{prefix}', but found '{expected}'. Multiple verify prefixes are not supported."
+ )
+ diag_category = category_parts[1]
+ if "seen but not expected" in parts[1]:
+ seen = True
+ elif "expected but not seen" in parts[1]:
+ seen = False
+ else:
+ raise KnownException(f"unexpected category '{parts[1]}'")
+ return (diag_category, seen)
+
+
+diag_error_re = re.compile(r"File (\S+) Line (\d+): (.+)")
+diag_error_re2 = re.compile(r"File \S+ Line \d+ \(directive at (\S+):(\d+)\): (.+)")
+
+
+def parse_diag_error(s):
+ m = diag_error_re2.match(s)
+ if not m:
+ m = diag_error_re.match(s)
+ if not m:
+ return None
+ return (m.group(1), int(m.group(2)), m.group(3))
+
+
+class Line:
+ def __init__(self, content, line_n):
+ self.content = content
+ self.diag = None
+ self.line_n = line_n
+ self.targeting_diags = []
+
+ def update_line_n(self, n):
+ self.line_n = n
+
+ def render(self):
+ if not self.diag:
+ return self.content
+ assert "{{DIAG}}" in self.content
+ res = self.content.replace("{{DIAG}}", self.diag.render())
+ if not res.strip():
+ return ""
+ return res
+
+
+class Diag:
+ def __init__(
+ self,
+ prefix,
+ diag_content,
+ category,
+ parsed_target_line_n,
+ line_is_absolute,
+ count,
+ line,
+ is_re,
+ whitespace_strings,
+ is_from_source_file,
+ ):
+ self.prefix = prefix
+ self.diag_content = diag_content
+ self.category = category
+ self.parsed_target_line_n = parsed_target_line_n
+ self.line_is_absolute = line_is_absolute
+ self.count = count
+ self.line = line
+ self.target = None
+ self.is_re = is_re
+ self.absolute_target()
+ self.whitespace_strings = whitespace_strings
+ self.is_from_source_file = is_from_source_file
+
+ def decrement_count(self):
+ self.count -= 1
+ assert self.count >= 0
+
+ def increment_count(self):
+ assert self.count >= 0
+ self.count += 1
+
+ def unset_target(self):
+ assert self.target is not None
+ self.target.targeting_diags.remove(self)
+ self.target = None
+
+ def set_target(self, target):
+ if self.target:
+ self.unset_target()
+ self.target = target
+ self.target.targeting_diags.append(self)
+
+ def absolute_target(self):
+ if self.target:
+ return self.target.line_n
+ if self.line_is_absolute:
+ return self.parsed_target_line_n
+ return self.line.line_n + self.parsed_target_line_n
+
+ def relative_target(self):
+ return self.absolute_target() - self.line.line_n
+
+ def take(self, other_diag):
+ assert self.count == 0
+ assert other_diag.count > 0
+ assert other_diag.target == self.target
+ assert not other_diag.line_is_absolute
+ assert not other_diag.is_re and not self.is_re
+ self.line_is_absolute = False
+ self.diag_content = other_diag.diag_content
+ self.count = other_diag.count
+ self.category = other_diag.category
+ self.count = other_diag.count
+ other_diag.count = 0
+
+ def render(self):
+ assert self.count >= 0
+ if self.count == 0:
+ return ""
+ line_location_s = ""
+ if self.relative_target() != 0:
+ if self.line_is_absolute:
+ line_location_s = f"@{self.absolute_target()}"
+ elif self.relative_target() > 0:
+ line_location_s = f"@+{self.relative_target()}"
+ else:
+ line_location_s = (
+ f"@{self.relative_target()}" # the minus sign is implicit
+ )
+ count_s = "" if self.count == 1 else f"{self.count}"
+ re_s = "-re" if self.is_re else ""
+ if self.whitespace_strings:
+ whitespace1_s = self.whitespace_strings[0]
+ whitespace2_s = self.whitespace_strings[1]
+ whitespace3_s = self.whitespace_strings[2]
+ else:
+ whitespace1_s = " "
+ whitespace2_s = ""
+ whitespace3_s = ""
+ if count_s and not whitespace2_s:
+ whitespace2_s = " " # required to parse correctly
+ elif not count_s and whitespace2_s == " ":
+ """Don't emit a weird extra space.
+ However if the whitespace is something other than the
+ standard single space, let it be to avoid disrupting manual formatting.
+ The existence of a non-empty whitespace2_s implies this was parsed with
+ a count > 1 and then decremented, otherwise this whitespace would have
+ been parsed as whitespace3_s.
+ """
+ whitespace2_s = ""
+ return f"//{whitespace1_s}{self.prefix}-{self.category}{re_s}{line_location_s}{whitespace2_s}{count_s}{whitespace3_s}{{{{{self.diag_content}}}}}"
+
+
+expected_diag_re = re.compile(
+ r"//(\s*)([a-zA-Z]+)-(note|warning|error)(-re)?(@[+-]?\d+)?(?:(\s*)(\d+))?(\s*)\{\{(.*)\}\}"
+)
+
+
+def parse_diag(line, filename, lines, prefix):
+ s = line.content
+ ms = expected_diag_re.findall(s)
+ if not ms:
+ return None
+ if len(ms) > 1:
+ raise KnownException(
+ f"multiple diags on line {filename}:{line.line_n}. Aborting due to missing implementation."
+ )
+ [
+ whitespace1_s,
+ check_prefix,
+ category_s,
+ re_s,
+ target_line_s,
+ whitespace2_s,
+ count_s,
+ whitespace3_s,
+ diag_s,
+ ] = ms[0]
+ if check_prefix != prefix:
+ return None
+ if not target_line_s:
+ target_line_n = 0
+ is_absolute = False
+ elif target_line_s.startswith("@+"):
+ target_line_n = int(target_line_s[2:])
+ is_absolute = False
+ elif target_line_s.startswith("@-"):
+ target_line_n = int(target_line_s[1:])
+ is_absolute = False
+ else:
+ target_line_n = int(target_line_s[1:])
+ is_absolute = True
+ count = int(count_s) if count_s else 1
+ line.content = expected_diag_re.sub("{{DIAG}}", s)
+
+ return Diag(
+ prefix,
+ diag_s,
+ category_s,
+ target_line_n,
+ is_absolute,
+ count,
+ line,
+ bool(re_s),
+ [whitespace1_s, whitespace2_s, whitespace3_s],
+ True,
+ )
+
+
+def add_line(new_line, lines):
+ lines.insert(new_line.line_n - 1, new_line)
+ for i in range(new_line.line_n, len(lines)):
+ line = lines[i]
+ assert line.line_n == i
+ line.update_line_n(i + 1)
+ assert all(line.line_n == i + 1 for i, line in enumerate(lines))
+
+
+def remove_line(old_line, lines):
+ lines.remove(old_line)
+ for i in range(old_line.line_n - 1, len(lines)):
+ line = lines[i]
+ assert line.line_n == i + 2
+ line.update_line_n(i + 1)
+ assert all(line.line_n == i + 1 for i, line in enumerate(lines))
+
+
+indent_re = re.compile(r"\s*")
+
+
+def get_indent(s):
+ return indent_re.match(s).group(0)
+
+
+def orig_line_n_to_new_line_n(line_n, orig_lines):
+ return orig_lines[line_n - 1].line_n
+
+
+def add_diag(orig_line_n, diag_s, diag_category, lines, orig_lines, prefix):
+ line_n = orig_line_n_to_new_line_n(orig_line_n, orig_lines)
+ target = lines[line_n - 1]
+ for other in target.targeting_diags:
+ if other.is_re:
+ raise KnownException(
+ "mismatching diag on line with regex matcher. Skipping due to missing implementation"
+ )
+ reverse = (
+ True
+ if [other for other in target.targeting_diags if other.relative_target() < 0]
+ else False
+ )
+
+ targeting = [
+ other for other in target.targeting_diags if not other.line_is_absolute
+ ]
+ targeting.sort(reverse=reverse, key=lambda d: d.relative_target())
+ prev_offset = 0
+ prev_line = target
+ direction = -1 if reverse else 1
+ for d in targeting:
+ if d.relative_target() != prev_offset + direction:
+ break
+ prev_offset = d.relative_target()
+ prev_line = d.line
+ total_offset = prev_offset - 1 if reverse else prev_offset + 1
+ if reverse:
+ new_line_n = prev_line.line_n + 1
+ else:
+ new_line_n = prev_line.line_n
+ assert new_line_n == line_n + (not reverse) - total_offset
+
+ new_line = Line(get_indent(prev_line.content) + "{{DIAG}}\n", new_line_n)
+ add_line(new_line, lines)
+
+ whitespace_strings = prev_line.diag.whitespace_strings if prev_line.diag else None
+ new_diag = Diag(
+ prefix,
+ diag_s,
+ diag_category,
+ total_offset,
+ False,
+ 1,
+ new_line,
+ False,
+ whitespace_strings,
+ False,
+ )
+ new_line.diag = new_diag
+ new_diag.set_target(target)
+
+
+def remove_dead_diags(lines):
+ for line in lines:
+ if not line.diag or line.diag.count != 0:
+ continue
+ if line.render() == "":
+ remove_line(line, lines)
+ else:
+ assert line.diag.is_from_source_file
+ for other_diag in line.targeting_diags:
+ if (
+ other_diag.is_from_source_file
+ or other_diag.count == 0
+ or other_diag.category != line.diag.category
+ ):
+ continue
+ if other_diag.is_re or line.diag.is_re:
+ continue
+ line.diag.take(other_diag)
+ remove_line(other_diag.line, lines)
+
+
+def has_live_diags(lines):
+ for line in lines:
+ if line.diag and line.diag.count > 0:
+ return True
+ return False
+
+
+def get_expected_no_diags_line_n(lines, prefix):
+ for line in lines:
+ if f"{prefix}-no-diagnostics" in line.content:
+ return line.line_n
+ return None
+
+
+def update_test_file(filename, diag_errors, prefix, updated_test_files):
+ dprint(f"updating test file {filename}")
+ if filename in updated_test_files:
+ raise KnownException(f"{filename} already updated, but got new output")
+ else:
+ updated_test_files.add(filename)
+ with open(filename, "r") as f:
+ lines = [Line(line, i + 1) for i, line in enumerate(f.readlines())]
+ orig_lines = list(lines)
+ expected_no_diags_line_n = get_expected_no_diags_line_n(orig_lines, prefix)
+
+ for line in lines:
+ diag = parse_diag(line, filename, lines, prefix)
+ if diag:
+ line.diag = diag
+ diag.set_target(lines[diag.absolute_target() - 1])
+
+ for line_n, diag_s, diag_category, seen in diag_errors:
+ if seen:
+ continue
+ # this is a diagnostic expected but not seen
+ assert lines[line_n - 1].diag
+ if diag_s != lines[line_n - 1].diag.diag_content:
+ raise KnownException(
+ f"{filename}:{line_n} - found diag {lines[line_n - 1].diag.diag_content} but expected {diag_s}"
+ )
+ if diag_category != lines[line_n - 1].diag.category:
+ raise KnownException(
+ f"{filename}:{line_n} - found {lines[line_n - 1].diag.category} diag but expected {diag_category}"
+ )
+ lines[line_n - 1].diag.decrement_count()
+ diag_errors_left = []
+ diag_errors.sort(reverse=True, key=lambda t: t[0])
+ for line_n, diag_s, diag_category, seen in diag_errors:
+ if not seen:
+ continue
+ target = orig_lines[line_n - 1]
+ other_diags = [
+ d
+ for d in target.targeting_diags
+ if d.diag_content == diag_s and d.category == diag_category
+ ]
+ other_diag = other_diags[0] if other_diags else None
+ if other_diag:
+ other_diag.increment_count()
+ else:
+ add_diag(line_n, diag_s, diag_category, lines, orig_lines, prefix)
+ remove_dead_diags(lines)
+ has_diags = has_live_diags(lines)
+ with open(filename, "w") as f:
+ if not has_diags and expected_no_diags_line_n is None:
+ f.write("// expected-no-diagnostics\n")
+ for line in lines:
+ if has_diags and line.line_n == expected_no_diags_line_n:
+ continue
+ f.write(line.render())
+
+
+def update_test_files(errors, prefix):
+ errors_by_file = {}
+ for (filename, line, diag_s), (diag_category, seen) in errors:
+ if filename not in errors_by_file:
+ errors_by_file[filename] = []
+ errors_by_file[filename].append((line, diag_s, diag_category, seen))
+ updated_test_files = set()
+ for filename, diag_errors in errors_by_file.items():
+ try:
+ update_test_file(filename, diag_errors, prefix, updated_test_files)
+ except KnownException as e:
+ return f"Error in update-verify-tests while updating {filename}: {e}"
+ updated_files = list(updated_test_files)
+ assert updated_files
+ if len(updated_files) == 1:
+ return f"updated file {updated_files[0]}"
+ updated_files_s = "\n\t".join(updated_files)
+ return "updated files:\n\t{updated_files_s}"
+
+
+def check_expectations(tool_output, prefix):
+ """
+ The entry point function.
+ Called by the stand-alone update-verify-tests.py as well as litplugin.py.
+ """
+ curr = []
+ curr_category = None
+ try:
+ for line in tool_output:
+ if line.startswith("error: "):
+ curr_category = parse_error_category(line[len("error: ") :], prefix)
+ continue
+
+ diag_error = parse_diag_error(line.strip())
+ if diag_error:
+ curr.append((diag_error, curr_category))
+ else:
+ dprint("no match")
+ dprint(line.strip())
+ except KnownException as e:
+ return f"Error in update-verify-tests while parsing tool output: {e}"
+ if curr:
+ return update_test_files(curr, prefix)
+ else:
+ return "no mismatching diagnostics found"
diff --git a/clang/utils/update-verify-tests.py b/clang/utils/update-verify-tests.py
index dd83a9418ddf5f..e2874a8c049ef3 100644
--- a/clang/utils/update-verify-tests.py
+++ b/clang/utils/update-verify-tests.py
@@ -1,6 +1,6 @@
import sys
-import re
import argparse
+from UpdateVerifyTests.core import check_expectations
"""
Pipe output from clang's -verify into this script to have the test case updated to expect the actual diagnostic output.
@@ -19,464 +19,20 @@
- if multiple checks targeting the same line are failing the script is not guaranteed to produce a minimal diff
Example usage:
- build/bin/llvm-lit clang/test/Sema/ --no-progress-bar -v | python3 update-verify-tests.py
+ clang -verify [file] | python3 update-verify-tests.py
+ clang -verify=check [file] | python3 update-verify-tests.py --prefix check
"""
-class KnownException(Exception):
- pass
-
-
-def parse_error_category(s, prefix):
- if "no expected directives found" in s:
- return None
- parts = s.split("diagnostics")
- diag_category = parts[0]
- category_parts = parts[0].strip().strip("'").split("-")
- expected = category_parts[0]
- if expected != prefix:
- raise Exception(
- f"expected prefix '{prefix}', but found '{expected}'. Multiple verify prefixes are not supported."
- )
- diag_category = category_parts[1]
- if "seen but not expected" in parts[1]:
- seen = True
- elif "expected but not seen" in parts[1]:
- seen = False
- else:
- raise KnownException(f"unexpected category '{parts[1]}'")
- return (diag_category, seen)
-
-
-diag_error_re = re.compile(r"File (\S+) Line (\d+): (.+)")
-diag_error_re2 = re.compile(r"File \S+ Line \d+ \(directive at (\S+):(\d+)\): (.+)")
-
-
-def parse_diag_error(s):
- m = diag_error_re2.match(s)
- if not m:
- m = diag_error_re.match(s)
- if not m:
- return None
- return (m.group(1), int(m.group(2)), m.group(3))
-
-
-class Line:
- def __init__(self, content, line_n):
- self.content = content
- self.diag = None
- self.line_n = line_n
- self.targeting_diags = []
-
- def update_line_n(self, n):
- self.line_n = n
-
- def render(self):
- if not self.diag:
- return self.content
- assert "{{DIAG}}" in self.content
- res = self.content.replace("{{DIAG}}", self.diag.render())
- if not res.strip():
- return ""
- return res
-
-
-class Diag:
- def __init__(
- self,
- prefix,
- diag_content,
- category,
- parsed_target_line_n,
- line_is_absolute,
- count,
- line,
- is_re,
- whitespace_strings,
- is_from_source_file,
- ):
- self.prefix = prefix
- self.diag_content = diag_content
- self.category = category
- self.parsed_target_line_n = parsed_target_line_n
- self.line_is_absolute = line_is_absolute
- self.count = count
- self.line = line
- self.target = None
- self.is_re = is_re
- self.absolute_target()
- self.whitespace_strings = whitespace_strings
- self.is_from_source_file = is_from_source_file
-
- def decrement_count(self):
- self.count -= 1
- assert self.count >= 0
-
- def increment_count(self):
- assert self.count >= 0
- self.count += 1
-
- def unset_target(self):
- assert self.target is not None
- self.target.targeting_diags.remove(self)
- self.target = None
-
- def set_target(self, target):
- if self.target:
- self.unset_target()
- self.target = target
- self.target.targeting_diags.append(self)
-
- def absolute_target(self):
- if self.target:
- return self.target.line_n
- if self.line_is_absolute:
- return self.parsed_target_line_n
- return self.line.line_n + self.parsed_target_line_n
-
- def relative_target(self):
- return self.absolute_target() - self.line.line_n
-
- def take(self, other_diag):
- assert self.count == 0
- assert other_diag.count > 0
- assert other_diag.target == self.target
- assert not other_diag.line_is_absolute
- assert not other_diag.is_re and not self.is_re
- self.line_is_absolute = False
- self.diag_content = other_diag.diag_content
- self.count = other_diag.count
- self.category = other_diag.category
- self.count = other_diag.count
- other_diag.count = 0
-
- def render(self):
- assert self.count >= 0
- if self.count == 0:
- return ""
- line_location_s = ""
- if self.relative_target() != 0:
- if self.line_is_absolute:
- line_location_s = f"@{self.absolute_target()}"
- elif self.relative_target() > 0:
- line_location_s = f"@+{self.relative_target()}"
- else:
- line_location_s = (
- f"@{self.relative_target()}" # the minus sign is implicit
- )
- count_s = "" if self.count == 1 else f"{self.count}"
- re_s = "-re" if self.is_re else ""
- if self.whitespace_strings:
- whitespace1_s = self.whitespace_strings[0]
- whitespace2_s = self.whitespace_strings[1]
- whitespace3_s = self.whitespace_strings[2]
- else:
- whitespace1_s = " "
- whitespace2_s = ""
- whitespace3_s = ""
- if count_s and not whitespace2_s:
- whitespace2_s = " " # required to parse correctly
- elif not count_s and whitespace2_s == " ":
- """ Don't emit a weird extra space.
- However if the whitespace is something other than the
- standard single space, let it be to avoid disrupting manual formatting.
- The existence of a non-empty whitespace2_s implies this was parsed with
- a count > 1 and then decremented, otherwise this whitespace would have
- been parsed as whitespace3_s.
- """
- whitespace2_s = ""
- return f"//{whitespace1_s}{self.prefix}-{self.category}{re_s}{line_location_s}{whitespace2_s}{count_s}{whitespace3_s}{{{{{self.diag_content}}}}}"
-
-expected_diag_re = re.compile(
- r"//(\s*)([a-zA-Z]+)-(note|warning|error)(-re)?(@[+-]?\d+)?(?:(\s*)(\d+))?(\s*)\{\{(.*)\}\}"
-)
-
-
-def parse_diag(line, filename, lines, prefix):
- s = line.content
- ms = expected_diag_re.findall(s)
- if not ms:
- return None
- if len(ms) > 1:
- raise KnownException(
- f"multiple diags on line {filename}:{line.line_n}. Aborting due to missing implementation."
- )
- [
- whitespace1_s,
- check_prefix,
- category_s,
- re_s,
- target_line_s,
- whitespace2_s,
- count_s,
- whitespace3_s,
- diag_s,
- ] = ms[0]
- if check_prefix != prefix:
- return None
- if not target_line_s:
- target_line_n = 0
- is_absolute = False
- elif target_line_s.startswith("@+"):
- target_line_n = int(target_line_s[2:])
- is_absolute = False
- elif target_line_s.startswith("@-"):
- target_line_n = int(target_line_s[1:])
- is_absolute = False
- else:
- target_line_n = int(target_line_s[1:])
- is_absolute = True
- count = int(count_s) if count_s else 1
- line.content = expected_diag_re.sub("{{DIAG}}", s)
-
- return Diag(
- prefix,
- diag_s,
- category_s,
- target_line_n,
- is_absolute,
- count,
- line,
- bool(re_s),
- [whitespace1_s, whitespace2_s, whitespace3_s],
- True,
- )
-
-
-def add_line(new_line, lines):
- lines.insert(new_line.line_n - 1, new_line)
- for i in range(new_line.line_n, len(lines)):
- line = lines[i]
- assert line.line_n == i
- line.update_line_n(i + 1)
- assert all(line.line_n == i + 1 for i, line in enumerate(lines))
-
-def remove_line(old_line, lines):
- lines.remove(old_line)
- for i in range(old_line.line_n - 1, len(lines)):
- line = lines[i]
- assert line.line_n == i + 2
- line.update_line_n(i + 1)
- assert all(line.line_n == i + 1 for i, line in enumerate(lines))
-
-
-indent_re = re.compile(r"\s*")
-
-
-def get_indent(s):
- return indent_re.match(s).group(0)
-
-def orig_line_n_to_new_line_n(line_n, orig_lines):
- return orig_lines[line_n - 1].line_n
-
-def add_diag(orig_line_n, diag_s, diag_category, lines, orig_lines, prefix):
- line_n = orig_line_n_to_new_line_n(orig_line_n, orig_lines)
- target = lines[line_n - 1]
- for other in target.targeting_diags:
- if other.is_re:
- raise KnownException(
- "mismatching diag on line with regex matcher. Skipping due to missing implementation"
- )
- reverse = (
- True
- if [other for other in target.targeting_diags if other.relative_target() < 0]
- else False
- )
-
- targeting = [
- other for other in target.targeting_diags if not other.line_is_absolute
- ]
- targeting.sort(reverse=reverse, key=lambda d: d.relative_target())
- prev_offset = 0
- prev_line = target
- direction = -1 if reverse else 1
- for d in targeting:
- if d.relative_target() != prev_offset + direction:
- break
- prev_offset = d.relative_target()
- prev_line = d.line
- total_offset = prev_offset - 1 if reverse else prev_offset + 1
- if reverse:
- new_line_n = prev_line.line_n + 1
- else:
- new_line_n = prev_line.line_n
- assert new_line_n == line_n + (not reverse) - total_offset
-
- new_line = Line(get_indent(prev_line.content) + "{{DIAG}}\n", new_line_n)
- add_line(new_line, lines)
-
- whitespace_strings = prev_line.diag.whitespace_strings if prev_line.diag else None
- new_diag = Diag(
- prefix, diag_s, diag_category, total_offset, False, 1, new_line, False, whitespace_strings, False,
- )
- new_line.diag = new_diag
- new_diag.set_target(target)
-
-def remove_dead_diags(lines):
- for line in lines:
- if not line.diag or line.diag.count != 0:
- continue
- if line.render() == "":
- remove_line(line, lines)
- else:
- assert line.diag.is_from_source_file
- for other_diag in line.targeting_diags:
- if other_diag.is_from_source_file or other_diag.count == 0 or other_diag.category != line.diag.category:
- continue
- if other_diag.is_re or line.diag.is_re:
- continue
- line.diag.take(other_diag)
- remove_line(other_diag.line, lines)
-
-def has_live_diags(lines):
- for line in lines:
- if line.diag and line.diag.count > 0:
- return True
- return False
-
-def get_expected_no_diags_line_n(lines, prefix):
- for line in lines:
- if f"{prefix}-no-diagnostics" in line.content:
- return line.line_n
- return None
-
-updated_test_files = set()
-
-
-def update_test_file(filename, diag_errors, prefix):
- print(f"updating test file {filename}")
- if filename in updated_test_files:
- print(
- f"{filename} already updated, but got new output - expect incorrect results"
- )
- else:
- updated_test_files.add(filename)
- with open(filename, "r") as f:
- lines = [Line(line, i + 1) for i, line in enumerate(f.readlines())]
- orig_lines = list(lines)
- expected_no_diags_line_n = get_expected_no_diags_line_n(orig_lines, prefix)
-
- for line in lines:
- diag = parse_diag(line, filename, lines, prefix)
- if diag:
- line.diag = diag
- diag.set_target(lines[diag.absolute_target() - 1])
-
- for line_n, diag_s, diag_category, seen in diag_errors:
- if seen:
- continue
- # this is a diagnostic expected but not seen
- assert lines[line_n - 1].diag
- if diag_s != lines[line_n - 1].diag.diag_content:
- raise KnownException(
- f"{filename}:{line_n} - found diag {lines[line_n - 1].diag.diag_content} but expected {diag_s}"
- )
- if diag_category != lines[line_n - 1].diag.category:
- raise KnownException(
- f"{filename}:{line_n} - found {lines[line_n - 1].diag.category} diag but expected {diag_category}"
- )
- lines[line_n - 1].diag.decrement_count()
- diag_errors_left = []
- diag_errors.sort(reverse=True, key=lambda t: t[0])
- for line_n, diag_s, diag_category, seen in diag_errors:
- if not seen:
- continue
- target = orig_lines[line_n - 1]
- other_diags = [
- d
- for d in target.targeting_diags
- if d.diag_content == diag_s and d.category == diag_category
- ]
- other_diag = other_diags[0] if other_diags else None
- if other_diag:
- other_diag.increment_count()
- else:
- add_diag(line_n, diag_s, diag_category, lines, orig_lines, prefix)
- remove_dead_diags(lines)
- has_diags = has_live_diags(lines)
- with open(filename, "w") as f:
- if not has_diags and expected_no_diags_line_n is None:
- f.write("// expected-no-diagnostics\n")
- for line in lines:
- if has_diags and line.line_n == expected_no_diags_line_n:
- continue
- f.write(line.render())
-
-
-def update_test_files(errors, prefix):
- errors_by_file = {}
- for (filename, line, diag_s), (diag_category, seen) in errors:
- if filename not in errors_by_file:
- errors_by_file[filename] = []
- errors_by_file[filename].append((line, diag_s, diag_category, seen))
- for filename, diag_errors in errors_by_file.items():
- try:
- update_test_file(filename, diag_errors, prefix)
- except KnownException as e:
- print(f"{filename} - ERROR: {e}")
- print("continuing...")
-
-def check_expectations(tool_output, prefix):
- curr = []
- curr_category = None
- curr_run_line = None
- lines_since_run = []
- skip_to_next_file = False
- for line in tool_output:
- lines_since_run.append(line)
- try:
- if line.startswith("RUN:"):
- if curr:
- update_test_files(curr, prefix)
- curr = []
- lines_since_run = [line]
- curr_run_line = line
- else:
- for line in lines_since_run:
- print(line, end="")
- print("====================")
- if lines_since_run:
- print("no mismatching diagnostics found since last RUN line")
- skip_to_next_file = False
- continue
- if skip_to_next_file:
- continue
- if line.startswith("error: "):
- curr_category = parse_error_category(line[len("error: ") :], prefix)
- continue
-
- diag_error = parse_diag_error(line.strip())
- if diag_error:
- curr.append((diag_error, curr_category))
- except KnownException as e:
- print(f"Error while parsing: {e}")
- if curr:
- print("skipping to next file")
- curr = []
- curr_category = None
- curr_run_line = None
- lines_since_run = []
- skip_to_next_file = True
- except Exception as e:
- for line in lines_since_run:
- print(line, end="")
- print("====================")
- print(e)
- sys.exit(1)
- if curr:
- update_test_files(curr, prefix)
- print("done!")
- else:
- for line in lines_since_run:
- print(line, end="")
- print("====================")
- print("no mismatching diagnostics found")
-
def main():
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("--prefix", default="expected", help="The prefix passed to -verify")
+ parser.add_argument(
+ "--prefix", default="expected", help="The prefix passed to -verify"
+ )
args = parser.parse_args()
- check_expectations(sys.stdin.readlines(), args.prefix)
+ output = check_expectations(sys.stdin.readlines(), args.prefix)
+ print(output)
+
if __name__ == "__main__":
main()
More information about the cfe-commits
mailing list