Skip to content

Commit 46dc332

Browse files
committed
internal/lsp: instrument work done reporting to use in regtests
In order for regtests to wait until file diagnostics are complete, instrument diagnostics with verbose WorkDone reporting. In order for this to be granular enough for use, the modification source needed to be threaded through to the didModifyFiles function (which is where the diagnostic goroutine is spun off). A new expectation is added: CompletedWork, to allow specifying that a specific work item has been completed. The problem with using NoOutstandingWork was that it required a continuous chain of work to prevent the regtest from succeeding when the bug was present, meaning that by the time we have sent the didChange notification successfully the server must have started work on its behalf. This was inherently racy, and too tricky to get right. Additionally, a couple bugs are fixed: - EmptyDiagnostics is corrected to account for the case where we have received zero diagnostics for a given file. - A deadlock is fixed in Await when expectations are immediately met. Updates golang/go#36879 Fixes golang/go#32149 Change-Id: I49ee011860351eed96a3b4f6795804b57a10dc60 Reviewed-on: https://go-review.googlesource.com/c/tools/+/229777 Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
1 parent 9938a07 commit 46dc332

File tree

5 files changed

+103
-27
lines changed

5 files changed

+103
-27
lines changed

internal/lsp/diagnostics.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,12 @@ type diagnosticKey struct {
2525
func (s *Server) diagnoseDetached(snapshot source.Snapshot) {
2626
ctx := snapshot.View().BackgroundContext()
2727
ctx = xcontext.Detach(ctx)
28-
2928
reports := s.diagnose(ctx, snapshot, false)
3029
s.publishReports(ctx, snapshot, reports)
3130
}
3231

3332
func (s *Server) diagnoseSnapshot(snapshot source.Snapshot) {
3433
ctx := snapshot.View().BackgroundContext()
35-
3634
reports := s.diagnose(ctx, snapshot, false)
3735
s.publishReports(ctx, snapshot, reports)
3836
}

internal/lsp/regtest/diagnostics_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package regtest
77
import (
88
"testing"
99

10+
"golang.org/x/tools/internal/lsp"
1011
"golang.org/x/tools/internal/lsp/fake"
1112
"golang.org/x/tools/internal/lsp/protocol"
1213
)
@@ -269,13 +270,19 @@ package foo
269270
func main() {}
270271
`
271272

272-
func TestPackageChange(t *testing.T) {
273-
t.Skip("skipping due to golang.org/issues/32149")
273+
func TestPackageChange_Issue38328(t *testing.T) {
274274
runner.Run(t, packageChange, func(t *testing.T, env *Env) {
275275
env.OpenFile("a.go")
276276
env.RegexpReplace("a.go", "foo", "foox")
277-
// TODO: there should be no error
278-
env.Await(EmptyDiagnostics("a.go"))
277+
env.Await(
278+
// When the bug reported in #38328 was present, we didn't get erroneous
279+
// file diagnostics until after the didChange message generated by the
280+
// package renaming was fully processed. Therefore, in order for this
281+
// test to actually exercise the bug, we must wait until that work has
282+
// completed.
283+
EmptyDiagnostics("a.go"),
284+
CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1),
285+
)
279286
})
280287
}
281288

internal/lsp/regtest/env.go

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ func WithModes(modes EnvMode) RunOption {
203203
})
204204
}
205205

206+
// WithEnv overlays environment variables encoded by "<var>=<value" on top of
207+
// the default regtest environment.
206208
func WithEnv(env ...string) RunOption {
207209
return optionSetter(func(opts *runConfig) {
208210
opts.env = env
@@ -350,6 +352,7 @@ type State struct {
350352
// be string, though the spec allows for numeric tokens as well. When work
351353
// completes, it is deleted from this map.
352354
outstandingWork map[string]*workProgress
355+
completedWork map[string]int
353356
}
354357

355358
type workProgress struct {
@@ -416,6 +419,7 @@ func NewEnv(ctx context.Context, t *testing.T, ws *fake.Workspace, ts servertest
416419
state: State{
417420
diagnostics: make(map[string]*protocol.PublishDiagnosticsParams),
418421
outstandingWork: make(map[string]*workProgress),
422+
completedWork: make(map[string]int),
419423
},
420424
waiters: make(map[int]*condition),
421425
}
@@ -439,6 +443,7 @@ func (e *Env) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsPar
439443
func (e *Env) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error {
440444
e.mu.Lock()
441445
defer e.mu.Unlock()
446+
442447
e.state.logs = append(e.state.logs, m)
443448
e.checkConditionsLocked()
444449
return nil
@@ -447,7 +452,7 @@ func (e *Env) onLogMessage(_ context.Context, m *protocol.LogMessageParams) erro
447452
func (e *Env) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error {
448453
e.mu.Lock()
449454
defer e.mu.Unlock()
450-
// panic if we don't have a string token.
455+
451456
token := m.Token.(string)
452457
e.state.outstandingWork[token] = &workProgress{}
453458
return nil
@@ -459,7 +464,7 @@ func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error {
459464
token := m.Token.(string)
460465
work, ok := e.state.outstandingWork[token]
461466
if !ok {
462-
panic(fmt.Sprintf("got progress report for unknown report %s", token))
467+
panic(fmt.Sprintf("got progress report for unknown report %s: %v", token, m))
463468
}
464469
v := m.Value.(map[string]interface{})
465470
switch kind := v["kind"]; kind {
@@ -470,6 +475,8 @@ func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error {
470475
work.percent = pct.(float64)
471476
}
472477
case "end":
478+
title := e.state.outstandingWork[token].title
479+
e.state.completedWork[title] = e.state.completedWork[title] + 1
473480
delete(e.state.outstandingWork, token)
474481
}
475482
e.checkConditionsLocked()
@@ -567,12 +574,12 @@ func (v Verdict) String() string {
567574

568575
// SimpleExpectation holds an arbitrary check func, and implements the Expectation interface.
569576
type SimpleExpectation struct {
570-
check func(State) Verdict
577+
check func(State) (Verdict, interface{})
571578
description string
572579
}
573580

574581
// Check invokes e.check.
575-
func (e SimpleExpectation) Check(s State) Verdict {
582+
func (e SimpleExpectation) Check(s State) (Verdict, interface{}) {
576583
return e.check(s)
577584
}
578585

@@ -584,18 +591,35 @@ func (e SimpleExpectation) Description() string {
584591
// NoOutstandingWork asserts that there is no work initiated using the LSP
585592
// $/progress API that has not completed.
586593
func NoOutstandingWork() SimpleExpectation {
587-
check := func(s State) Verdict {
594+
check := func(s State) (Verdict, interface{}) {
588595
if len(s.outstandingWork) == 0 {
589-
return Met
596+
return Met, nil
590597
}
591-
return Unmet
598+
return Unmet, nil
592599
}
593600
return SimpleExpectation{
594601
check: check,
595602
description: "no outstanding work",
596603
}
597604
}
598605

606+
// CompletedWork expects a work item to have been completed >= atLeast times.
607+
//
608+
// Since the Progress API doesn't include any hidden metadata, we must use the
609+
// progress notification title to identify the work we expect to be completed.
610+
func CompletedWork(title string, atLeast int) SimpleExpectation {
611+
check := func(s State) (Verdict, interface{}) {
612+
if s.completedWork[title] >= atLeast {
613+
return Met, title
614+
}
615+
return Unmet, nil
616+
}
617+
return SimpleExpectation{
618+
check: check,
619+
description: fmt.Sprintf("completed work %q at least %d time(s)", title, atLeast),
620+
}
621+
}
622+
599623
// LogExpectation is an expectation on the log messages received by the editor
600624
// from gopls.
601625
type LogExpectation struct {
@@ -678,14 +702,16 @@ func (e DiagnosticExpectation) Description() string {
678702

679703
// EmptyDiagnostics asserts that diagnostics are empty for the
680704
// workspace-relative path name.
681-
func EmptyDiagnostics(name string) DiagnosticExpectation {
682-
isMet := func(diags *protocol.PublishDiagnosticsParams) bool {
683-
return len(diags.Diagnostics) == 0
705+
func EmptyDiagnostics(name string) Expectation {
706+
check := func(s State) (Verdict, interface{}) {
707+
if diags, ok := s.diagnostics[name]; !ok || len(diags.Diagnostics) == 0 {
708+
return Met, nil
709+
}
710+
return Unmet, nil
684711
}
685-
return DiagnosticExpectation{
686-
isMet: isMet,
712+
return SimpleExpectation{
713+
check: check,
687714
description: "empty diagnostics",
688-
path: name,
689715
}
690716
}
691717

@@ -742,6 +768,7 @@ func (e *Env) Await(expectations ...Expectation) []interface{} {
742768
// called.
743769
switch verdict, summary, metBy := checkExpectations(e.state, expectations); verdict {
744770
case Met:
771+
e.mu.Unlock()
745772
return metBy
746773
case Unmeetable:
747774
e.mu.Unlock()
@@ -771,7 +798,7 @@ func (e *Env) Await(expectations ...Expectation) []interface{} {
771798
// Debugging an unmet expectation can be tricky, so we put some effort into
772799
// nicely formatting the failure.
773800
if err != nil {
774-
e.T.Fatalf("waiting on:\n%s\nerr:%v\nstate:\n%v", err, summary, e.state)
801+
e.T.Fatalf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, e.state)
775802
}
776803
return metBy
777804
}

internal/lsp/regtest/env_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func TestProgressUpdating(t *testing.T) {
1616
e := &Env{
1717
state: State{
1818
outstandingWork: make(map[string]*workProgress),
19+
completedWork: make(map[string]int),
1920
},
2021
}
2122
ctx := context.Background()

internal/lsp/text_synchronization.go

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,37 @@ import (
1616
errors "golang.org/x/xerrors"
1717
)
1818

19+
// ModificationSource identifies the originating cause of a file modification.
20+
type ModificationSource int
21+
22+
const (
23+
// FromDidOpen is a file modification caused by opening a file.
24+
FromDidOpen = ModificationSource(iota)
25+
// FromDidChange is a file modification caused by changing a file.
26+
FromDidChange
27+
// FromDidChangeWatchedFiles is a file modification caused by a change to a watched file.
28+
FromDidChangeWatchedFiles
29+
// FromDidSave is a file modification caused by a file save.
30+
FromDidSave
31+
// FromDidClose is a file modification caused by closing a file.
32+
FromDidClose
33+
)
34+
35+
func (m ModificationSource) String() string {
36+
switch m {
37+
case FromDidOpen:
38+
return "opened files"
39+
case FromDidChange:
40+
return "changed files"
41+
case FromDidChangeWatchedFiles:
42+
return "files changed on disk"
43+
case FromDidSave:
44+
return "saved files"
45+
default:
46+
return "unknown file modification"
47+
}
48+
}
49+
1950
func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
2051
uri := params.TextDocument.URI.SpanURI()
2152
if !uri.IsFile() {
@@ -30,7 +61,7 @@ func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocume
3061
Text: []byte(params.TextDocument.Text),
3162
LanguageID: params.TextDocument.LanguageID,
3263
},
33-
})
64+
}, FromDidOpen)
3465
return err
3566
}
3667

@@ -50,7 +81,7 @@ func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDo
5081
Version: params.TextDocument.Version,
5182
Text: text,
5283
}
53-
snapshots, err := s.didModifyFiles(ctx, []source.FileModification{c})
84+
snapshots, err := s.didModifyFiles(ctx, []source.FileModification{c}, FromDidChange)
5485
if err != nil {
5586
return err
5687
}
@@ -96,7 +127,7 @@ func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.Did
96127
delete(deletions, uri)
97128
}
98129
}
99-
snapshots, err := s.didModifyFiles(ctx, modifications)
130+
snapshots, err := s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles)
100131
if err != nil {
101132
return err
102133
}
@@ -129,7 +160,7 @@ func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocume
129160
if params.Text != nil {
130161
c.Text = []byte(*params.Text)
131162
}
132-
_, err := s.didModifyFiles(ctx, []source.FileModification{c})
163+
_, err := s.didModifyFiles(ctx, []source.FileModification{c}, FromDidSave)
133164
return err
134165
}
135166

@@ -145,7 +176,7 @@ func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocu
145176
Version: -1,
146177
Text: nil,
147178
},
148-
})
179+
}, FromDidClose)
149180
if err != nil {
150181
return err
151182
}
@@ -168,7 +199,7 @@ func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocu
168199
return nil
169200
}
170201

171-
func (s *Server) didModifyFiles(ctx context.Context, modifications []source.FileModification) (map[span.URI]source.Snapshot, error) {
202+
func (s *Server) didModifyFiles(ctx context.Context, modifications []source.FileModification, cause ModificationSource) (map[span.URI]source.Snapshot, error) {
172203
snapshots, err := s.session.DidModifyFiles(ctx, modifications)
173204
if err != nil {
174205
return nil, err
@@ -228,11 +259,23 @@ func (s *Server) didModifyFiles(ctx context.Context, modifications []source.File
228259
}
229260
}
230261
}
231-
go s.diagnoseSnapshot(snapshot)
262+
go func(snapshot source.Snapshot) {
263+
if s.session.Options().VerboseWorkDoneProgress {
264+
work := s.StartWork(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil)
265+
defer work.End(ctx, "Done.")
266+
}
267+
s.diagnoseSnapshot(snapshot)
268+
}(snapshot)
232269
}
233270
return snapshotByURI, nil
234271
}
235272

273+
// DiagnosticWorkTitle returns the title of the diagnostic work resulting from a
274+
// file change originating from the given cause.
275+
func DiagnosticWorkTitle(cause ModificationSource) string {
276+
return fmt.Sprintf("diagnosing %v", cause)
277+
}
278+
236279
func (s *Server) wasFirstChange(uri span.URI) bool {
237280
if s.changedFiles == nil {
238281
s.changedFiles = make(map[span.URI]struct{})

0 commit comments

Comments
 (0)