gopls/internal/regtest: add a simpler API for diagnostic expectations
The number of different regtest expectations related to diagnostics has
grown significantly over time. Start to consolidate to just two
expectations: Diagnostics, which asserts on the existence of
diagnostics, and NoMatchingDiagnostics, which asserts on the
nonexistence of diagnostics. Both accept an argument list to filter the
set of diagnostics under consideration.
In a subsequent CL, NoMatchingDiagnostics will be renamed to
NoDiagnostics, once the existing expectation with that name has been
replaced.
Use this to eliminate the following expectations:
- DiagnosticAtRegexpFromSource -> Diagnostics(AtRegexp, FromSource)
- NoDiagnosticAtRegexp -> NoMatchingDiagnostics(AtRegexp)
- NoOutstandingDiagnostics -> NoMatchingDiagnostics
Updates golang/go#39384
Change-Id: I518b14eb00c9fcf62388a03efb668899939a8f82
Reviewed-on: https://go-review.googlesource.com/c/tools/+/461915
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go
index 3711386..a86dfe1 100644
--- a/gopls/internal/lsp/regtest/expectation.go
+++ b/gopls/internal/lsp/regtest/expectation.go
@@ -700,23 +700,144 @@
return desc
}
-// NoOutstandingDiagnostics asserts that the workspace has no outstanding
-// diagnostic messages.
-func NoOutstandingDiagnostics() Expectation {
+// Diagnostics asserts that there is at least one diagnostic matching the given
+// filters.
+func Diagnostics(filters ...DiagnosticFilter) Expectation {
check := func(s State) Verdict {
- for _, diags := range s.diagnostics {
- if len(diags.Diagnostics) > 0 {
+ diags := flattenDiagnostics(s)
+ for _, filter := range filters {
+ var filtered []flatDiagnostic
+ for _, d := range diags {
+ if filter.check(d.name, d.diag) {
+ filtered = append(filtered, d)
+ }
+ }
+ if len(filtered) == 0 {
+ // TODO(rfindley): if/when expectations describe their own failure, we
+ // can provide more useful information here as to which filter caused
+ // the failure.
return Unmet
}
+ diags = filtered
}
return Met
}
+ var descs []string
+ for _, filter := range filters {
+ descs = append(descs, filter.desc)
+ }
return SimpleExpectation{
check: check,
- description: "no outstanding diagnostics",
+ description: "any diagnostics " + strings.Join(descs, ", "),
}
}
+// NoMatchingDiagnostics asserts that there are no diagnostics matching the
+// given filters. Notably, if no filters are supplied this assertion checks
+// that there are no diagnostics at all, for any file.
+//
+// TODO(rfindley): replace NoDiagnostics with this, and rename.
+func NoMatchingDiagnostics(filters ...DiagnosticFilter) Expectation {
+ check := func(s State) Verdict {
+ diags := flattenDiagnostics(s)
+ for _, filter := range filters {
+ var filtered []flatDiagnostic
+ for _, d := range diags {
+ if filter.check(d.name, d.diag) {
+ filtered = append(filtered, d)
+ }
+ }
+ diags = filtered
+ }
+ if len(diags) > 0 {
+ return Unmet
+ }
+ return Met
+ }
+ var descs []string
+ for _, filter := range filters {
+ descs = append(descs, filter.desc)
+ }
+ return SimpleExpectation{
+ check: check,
+ description: "no diagnostics " + strings.Join(descs, ", "),
+ }
+}
+
+type flatDiagnostic struct {
+ name string
+ diag protocol.Diagnostic
+}
+
+func flattenDiagnostics(state State) []flatDiagnostic {
+ var result []flatDiagnostic
+ for name, diags := range state.diagnostics {
+ for _, diag := range diags.Diagnostics {
+ result = append(result, flatDiagnostic{name, diag})
+ }
+ }
+ return result
+}
+
+// -- Diagnostic filters --
+
+// A DiagnosticFilter filters the set of diagnostics, for assertion with
+// Diagnostics or NoMatchingDiagnostics.
+type DiagnosticFilter struct {
+ desc string
+ check func(name string, _ protocol.Diagnostic) bool
+}
+
+// ForFile filters to diagnostics matching the sandbox-relative file name.
+func ForFile(name string) DiagnosticFilter {
+ return DiagnosticFilter{
+ desc: fmt.Sprintf("for file %q", name),
+ check: func(diagName string, _ protocol.Diagnostic) bool {
+ return diagName == name
+ },
+ }
+}
+
+// FromSource filters to diagnostics matching the given diagnostics source.
+func FromSource(source string) DiagnosticFilter {
+ return DiagnosticFilter{
+ desc: fmt.Sprintf("with source %q", source),
+ check: func(_ string, d protocol.Diagnostic) bool {
+ return d.Source == source
+ },
+ }
+}
+
+// AtRegexp filters to diagnostics in the file with sandbox-relative path name,
+// at the first position matching the given regexp pattern.
+//
+// TODO(rfindley): pass in the editor to expectations, so that they may depend
+// on editor state and AtRegexp can be a function rather than a method.
+func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter {
+ pos := e.RegexpSearch(name, pattern)
+ return DiagnosticFilter{
+ desc: fmt.Sprintf("at the first position matching %q in %q", pattern, name),
+ check: func(diagName string, d protocol.Diagnostic) bool {
+ // TODO(rfindley): just use protocol.Position for Pos, rather than
+ // duplicating.
+ return diagName == name && d.Range.Start.Line == uint32(pos.Line) && d.Range.Start.Character == uint32(pos.Column)
+ },
+ }
+}
+
+// WithMessageContaining filters to diagnostics whose message contains the
+// given substring.
+func WithMessageContaining(substring string) DiagnosticFilter {
+ return DiagnosticFilter{
+ desc: fmt.Sprintf("with message containing %q", substring),
+ check: func(_ string, d protocol.Diagnostic) bool {
+ return strings.Contains(d.Message, substring)
+ },
+ }
+}
+
+// TODO(rfindley): eliminate all expectations below this point.
+
// NoDiagnostics asserts that either no diagnostics are sent for the
// workspace-relative path name, or empty diagnostics are sent.
func NoDiagnostics(name string) Expectation {
@@ -749,43 +870,17 @@
return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true, message: msg}
}
-// DiagnosticAtRegexpFromSource expects a diagnostic at the first position
-// matching re, from the given source.
-func (e *Env) DiagnosticAtRegexpFromSource(name, re, source string) DiagnosticExpectation {
- e.T.Helper()
- pos := e.RegexpSearch(name, re)
- return DiagnosticExpectation{path: name, pos: &pos, re: re, present: true, source: source}
-}
-
// DiagnosticAt asserts that there is a diagnostic entry at the position
// specified by line and col, for the workdir-relative path name.
func DiagnosticAt(name string, line, col int) DiagnosticExpectation {
return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: true}
}
-// NoDiagnosticAtRegexp expects that there is no diagnostic entry at the start
-// position matching the regexp search string re in the buffer specified by
-// name. Note that this currently ignores the end position.
-// This should only be used in combination with OnceMet for a given condition,
-// otherwise it may always succeed.
-func (e *Env) NoDiagnosticAtRegexp(name, re string) DiagnosticExpectation {
- e.T.Helper()
- pos := e.RegexpSearch(name, re)
- return DiagnosticExpectation{path: name, pos: &pos, re: re, present: false}
-}
-
-// NoDiagnosticWithMessage asserts that there is no diagnostic entry with the
-// given message.
-//
-// This should only be used in combination with OnceMet for a given condition,
-// otherwise it may always succeed.
-func NoDiagnosticWithMessage(name, msg string) DiagnosticExpectation {
- return DiagnosticExpectation{path: name, message: msg, present: false}
-}
-
// GoSumDiagnostic asserts that a "go.sum is out of sync" diagnostic for the
// given module (as formatted in a go.mod file, e.g. "example.com v1.0.0") is
// present.
+//
+// TODO(rfindley): remove this.
func (e *Env) GoSumDiagnostic(name, module string) Expectation {
e.T.Helper()
// In 1.16, go.sum diagnostics should appear on the relevant module. Earlier
diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go
index 8ea5e9c..13ba4e1 100644
--- a/gopls/internal/regtest/codelens/codelens_test.go
+++ b/gopls/internal/regtest/codelens/codelens_test.go
@@ -216,7 +216,7 @@
// but there may be some subtlety in timing here, where this
// should always succeed, but may not actually test the correct
// behavior.
- env.NoDiagnosticAtRegexp("b/go.mod", `require`),
+ NoMatchingDiagnostics(env.AtRegexp("b/go.mod", `require`)),
),
)
// Check for upgrades in b/go.mod and then clear them.
diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go
index 6631a92..b7ccb5b 100644
--- a/gopls/internal/regtest/diagnostics/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go
@@ -1404,7 +1404,7 @@
env.Await(
OnceMet(
InitialWorkspaceLoad,
- NoDiagnosticWithMessage("", "illegal character U+0023 '#'"),
+ NoMatchingDiagnostics(WithMessageContaining("illegal character U+0023 '#'")),
),
)
})
diff --git a/gopls/internal/regtest/misc/staticcheck_test.go b/gopls/internal/regtest/misc/staticcheck_test.go
index f2ba3cc..9242e19 100644
--- a/gopls/internal/regtest/misc/staticcheck_test.go
+++ b/gopls/internal/regtest/misc/staticcheck_test.go
@@ -64,13 +64,13 @@
Settings{"staticcheck": true},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("a/a.go")
- env.Await(
- env.DiagnosticAtRegexpFromSource("a/a.go", "sort.Slice", "sortslice"),
- env.DiagnosticAtRegexpFromSource("a/a.go", "sort.Slice.(slice)", "SA1028"),
- env.DiagnosticAtRegexpFromSource("a/a.go", "var (FooErr)", "ST1012"),
- env.DiagnosticAtRegexpFromSource("a/a.go", `"12234"`, "SA1024"),
- env.DiagnosticAtRegexpFromSource("a/a.go", "testGenerics.*(p P)", "SA4009"),
- env.DiagnosticAtRegexpFromSource("a/a.go", "q = (&\\*p)", "SA4001"),
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("a/a.go", "sort.Slice"), FromSource("sortslice")),
+ Diagnostics(env.AtRegexp("a/a.go", "sort.Slice.(slice)"), FromSource("SA1028")),
+ Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)"), FromSource("ST1012")),
+ Diagnostics(env.AtRegexp("a/a.go", `"12234"`), FromSource("SA1024")),
+ Diagnostics(env.AtRegexp("a/a.go", "testGenerics.*(p P)"), FromSource("SA4009")),
+ Diagnostics(env.AtRegexp("a/a.go", "q = (&\\*p)"), FromSource("SA4001")),
)
})
}
@@ -103,11 +103,8 @@
Settings{"staticcheck": true},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("p.go")
- env.Await(
- OnceMet(
- env.DoneWithOpen(),
- env.DiagnosticAtRegexpFromSource("p.go", ", (enabled)", "SA9008"),
- ),
+ env.AfterChange(
+ Diagnostics(env.AtRegexp("p.go", ", (enabled)"), FromSource("SA9008")),
)
})
}
diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go
index 711d596..9a65030 100644
--- a/gopls/internal/regtest/workspace/broken_test.go
+++ b/gopls/internal/regtest/workspace/broken_test.go
@@ -160,16 +160,13 @@
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("p/internal/bar/bar.go")
- env.Await(
- OnceMet(
- env.DoneWithOpen(),
- env.DiagnosticAtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\""),
- ),
+ env.AfterChange(
+ env.DiagnosticAtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\""),
)
env.OpenFile("go.mod")
env.RegexpReplace("go.mod", "mod.testx", "mod.test")
env.SaveBuffer("go.mod") // saving triggers a reload
- env.Await(NoOutstandingDiagnostics())
+ env.AfterChange(NoMatchingDiagnostics())
})
}
diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go
index 698c8aa..dffbbea 100644
--- a/gopls/internal/regtest/workspace/standalone_test.go
+++ b/gopls/internal/regtest/workspace/standalone_test.go
@@ -74,29 +74,14 @@
}
env.OpenFile("lib/lib.go")
- env.Await(
- OnceMet(
- env.DoneWithOpen(),
- NoOutstandingDiagnostics(),
- ),
- )
+ env.AfterChange(NoMatchingDiagnostics())
// Replacing C with D should not cause any workspace diagnostics, since we
// haven't yet opened the standalone file.
env.RegexpReplace("lib/lib.go", "C", "D")
- env.Await(
- OnceMet(
- env.DoneWithChange(),
- NoOutstandingDiagnostics(),
- ),
- )
+ env.AfterChange(NoMatchingDiagnostics())
env.RegexpReplace("lib/lib.go", "D", "C")
- env.Await(
- OnceMet(
- env.DoneWithChange(),
- NoOutstandingDiagnostics(),
- ),
- )
+ env.AfterChange(NoMatchingDiagnostics())
refs := env.References("lib/lib.go", env.RegexpSearch("lib/lib.go", "C"))
checkLocations("References", refs, "lib/lib.go")
@@ -106,12 +91,7 @@
// Opening the standalone file should not result in any diagnostics.
env.OpenFile("lib/ignore.go")
- env.Await(
- OnceMet(
- env.DoneWithOpen(),
- NoOutstandingDiagnostics(),
- ),
- )
+ env.AfterChange(NoMatchingDiagnostics())
// Having opened the standalone file, we should find its symbols in the
// workspace.
@@ -151,21 +131,11 @@
// Renaming "lib.C" to "lib.D" should cause a diagnostic in the standalone
// file.
env.RegexpReplace("lib/lib.go", "C", "D")
- env.Await(
- OnceMet(
- env.DoneWithChange(),
- env.DiagnosticAtRegexp("lib/ignore.go", "lib.(C)"),
- ),
- )
+ env.AfterChange(env.DiagnosticAtRegexp("lib/ignore.go", "lib.(C)"))
// Undoing the replacement should fix diagnostics
env.RegexpReplace("lib/lib.go", "D", "C")
- env.Await(
- OnceMet(
- env.DoneWithChange(),
- NoOutstandingDiagnostics(),
- ),
- )
+ env.AfterChange(NoMatchingDiagnostics())
// Now that our workspace has no errors, we should be able to find
// references and rename.
diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go
index a279b8e..49e9e27 100644
--- a/gopls/internal/regtest/workspace/workspace_test.go
+++ b/gopls/internal/regtest/workspace/workspace_test.go
@@ -272,7 +272,7 @@
env.AfterChange(
env.DiagnosticAtRegexp("moda/a/a.go", "x"),
env.DiagnosticAtRegexp("modb/b/b.go", "x"),
- env.NoDiagnosticAtRegexp("moda/a/a.go", `"b.com/b"`),
+ NoMatchingDiagnostics(env.AtRegexp("moda/a/a.go", `"b.com/b"`)),
)
})
}
@@ -702,7 +702,7 @@
// the diagnostic still shows up.
env.SetBufferContent("go.work", "go 1.18 \n\n use ./bar\n")
env.AfterChange(
- env.NoDiagnosticAtRegexp("go.work", "use"),
+ NoMatchingDiagnostics(env.AtRegexp("go.work", "use")),
)
env.SetBufferContent("go.work", "go 1.18 \n\n use ./foo\n")
env.AfterChange(
@@ -1069,7 +1069,7 @@
b
)
`)
- env.AfterChange(NoOutstandingDiagnostics())
+ env.AfterChange(NoMatchingDiagnostics())
// Removing the go.work file should put us back where we started.
env.RemoveWorkspaceFile("go.work")