Skip to content

Commit fe725d9

Browse files
committed
gopls/internal/regtest: simplify awaiting diagnostics from a change
When awaiting diagnostics, we should almost always wrap expectations in a OnceMet(precondition, ...), so that tests do not hang indefinitely if the diagnostic pass completes and the expectations are still not met. Before this change, the user must be careful to pass in the correct precondition (or combination of preconditions), else they may be susceptible to races. This change adds an AllOf combinator, and uses it to define a new DoneDiagnosingChanges expectation that waits for all anticipated diagnostic passes to complete. This should fix the race in TestUnknownRevision. We should apply a similar transformation throughout the regression test suites. To make this easier, add a shorter AfterChange helper that implements the common pattern. Fixes golang/go#55070 Change-Id: Ie0e3c4701fba7b1d10de6b43d776562d198ffac9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/448115 Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 3c8152e commit fe725d9

File tree

3 files changed

+93
-6
lines changed

3 files changed

+93
-6
lines changed

gopls/internal/lsp/regtest/expectation.go

+76
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package regtest
77
import (
88
"fmt"
99
"regexp"
10+
"sort"
1011
"strings"
1112

1213
"golang.org/x/tools/gopls/internal/lsp"
@@ -130,6 +131,33 @@ func AnyOf(anyOf ...Expectation) *SimpleExpectation {
130131
}
131132
}
132133

134+
// AllOf expects that all given expectations are met.
135+
//
136+
// TODO(rfindley): the problem with these types of combinators (OnceMet, AnyOf
137+
// and AllOf) is that we lose the information of *why* they failed: the Awaiter
138+
// is not smart enough to look inside.
139+
//
140+
// Refactor the API such that the Check function is responsible for explaining
141+
// why an expectation failed. This should allow us to significantly improve
142+
// test output: we won't need to summarize state at all, as the verdict
143+
// explanation itself should describe clearly why the expectation not met.
144+
func AllOf(allOf ...Expectation) *SimpleExpectation {
145+
check := func(s State) Verdict {
146+
verdict := Met
147+
for _, e := range allOf {
148+
if v := e.Check(s); v > verdict {
149+
verdict = v
150+
}
151+
}
152+
return verdict
153+
}
154+
description := describeExpectations(allOf...)
155+
return &SimpleExpectation{
156+
check: check,
157+
description: fmt.Sprintf("All of:\n%s", description),
158+
}
159+
}
160+
133161
// ReadDiagnostics is an 'expectation' that is used to read diagnostics
134162
// atomically. It is intended to be used with 'OnceMet'.
135163
func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) *SimpleExpectation {
@@ -218,6 +246,54 @@ func ShowMessageRequest(title string) SimpleExpectation {
218246
}
219247
}
220248

249+
// DoneDiagnosingChanges expects that diagnostics are complete from common
250+
// change notifications: didOpen, didChange, didSave, didChangeWatchedFiles,
251+
// and didClose.
252+
//
253+
// This can be used when multiple notifications may have been sent, such as
254+
// when a didChange is immediately followed by a didSave. It is insufficient to
255+
// simply await NoOutstandingWork, because the LSP client has no control over
256+
// when the server starts processing a notification. Therefore, we must keep
257+
// track of
258+
func (e *Env) DoneDiagnosingChanges() Expectation {
259+
stats := e.Editor.Stats()
260+
statsBySource := map[lsp.ModificationSource]uint64{
261+
lsp.FromDidOpen: stats.DidOpen,
262+
lsp.FromDidChange: stats.DidChange,
263+
lsp.FromDidSave: stats.DidSave,
264+
lsp.FromDidChangeWatchedFiles: stats.DidChangeWatchedFiles,
265+
lsp.FromDidClose: stats.DidClose,
266+
}
267+
268+
var expected []lsp.ModificationSource
269+
for k, v := range statsBySource {
270+
if v > 0 {
271+
expected = append(expected, k)
272+
}
273+
}
274+
275+
// Sort for stability.
276+
sort.Slice(expected, func(i, j int) bool {
277+
return expected[i] < expected[j]
278+
})
279+
280+
var all []Expectation
281+
for _, source := range expected {
282+
all = append(all, CompletedWork(lsp.DiagnosticWorkTitle(source), statsBySource[source], true))
283+
}
284+
285+
return AllOf(all...)
286+
}
287+
288+
// AfterChange expects that the given expectations will be met after all
289+
// state-changing notifications have been processed by the server.
290+
func (e *Env) AfterChange(expectations ...Expectation) Expectation {
291+
return OnceMet(
292+
e.DoneDiagnosingChanges(),
293+
expectations...,
294+
)
295+
}
296+
221297
// DoneWithOpen expects all didOpen notifications currently sent by the editor
222298
// to be completely processed.
223299
func (e *Env) DoneWithOpen() Expectation {

gopls/internal/lsp/text_synchronization.go

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const (
4040
// FromDidClose is a file modification caused by closing a file.
4141
FromDidClose
4242

43+
// TODO: add FromDidChangeConfiguration, once configuration changes cause a
44+
// new snapshot to be created.
45+
4346
// FromRegenerateCgo refers to file modifications caused by regenerating
4447
// the cgo sources for the workspace.
4548
FromRegenerateCgo

gopls/internal/regtest/modfile/modfile_test.go

+14-6
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ func main() {
633633

634634
d := protocol.PublishDiagnosticsParams{}
635635
env.Await(
636-
OnceMet(
636+
env.AfterChange(
637637
// Make sure the diagnostic mentions the new version -- the old diagnostic is in the same place.
638638
env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "[email protected]"),
639639
ReadDiagnostics("a/go.mod", &d),
@@ -646,8 +646,10 @@ func main() {
646646
env.ApplyCodeAction(qfs[0]) // Arbitrarily pick a single fix to apply. Applying all of them seems to cause trouble in this particular test.
647647
env.SaveBuffer("a/go.mod") // Save to trigger diagnostics.
648648
env.Await(
649-
EmptyDiagnostics("a/go.mod"),
650-
env.DiagnosticAtRegexp("a/main.go", "x = "),
649+
env.AfterChange(
650+
EmptyDiagnostics("a/go.mod"),
651+
env.DiagnosticAtRegexp("a/main.go", "x = "),
652+
),
651653
)
652654
})
653655
})
@@ -677,17 +679,23 @@ func main() {
677679
runner.Run(t, known, func(t *testing.T, env *Env) {
678680
env.OpenFile("a/go.mod")
679681
env.Await(
680-
env.DiagnosticAtRegexp("a/main.go", "x = "),
682+
env.AfterChange(
683+
env.DiagnosticAtRegexp("a/main.go", "x = "),
684+
),
681685
)
682686
env.RegexpReplace("a/go.mod", "v1.2.3", "v1.2.2")
683687
env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk
684688
env.Await(
685-
env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"),
689+
env.AfterChange(
690+
env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"),
691+
),
686692
)
687693
env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3")
688694
env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk
689695
env.Await(
690-
env.DiagnosticAtRegexp("a/main.go", "x = "),
696+
env.AfterChange(
697+
env.DiagnosticAtRegexp("a/main.go", "x = "),
698+
),
691699
)
692700
})
693701
})

0 commit comments

Comments
 (0)