Skip to content

Commit ed19fc7

Browse files
committed
gopls/internal/test: synchronize notifications during commands
It turns out that in the jsonrcp2 package, call responses are asynchronous to other notifications. Therefore, we must synchronize tests using progress notifications. Introduce a DelayMessages test option to reproduce these types of races. (It worked for reproducing golang/go#70342.) Fixes golang/go#70342 Change-Id: I4cfcd7675335694a47eaf1a2547be0301fc244c9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/627696 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 254baba commit ed19fc7

File tree

4 files changed

+51
-6
lines changed

4 files changed

+51
-6
lines changed

gopls/internal/server/command.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCom
5050
ctx, done := event.Start(ctx, "lsp.Server.executeCommand")
5151
defer done()
5252

53+
// For test synchronization, always create a progress notification.
54+
//
55+
// This may be in addition to user-facing progress notifications created in
56+
// the course of command execution.
57+
if s.Options().VerboseWorkDoneProgress {
58+
work := s.progress.Start(ctx, params.Command, "Verbose: running command...", nil, nil)
59+
defer work.End(ctx, "Done.")
60+
}
61+
5362
var found bool
5463
for _, name := range s.Options().SupportedCommands {
5564
if name == params.Command {

gopls/internal/test/integration/fake/editor.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import (
1010
"encoding/json"
1111
"errors"
1212
"fmt"
13+
"math/rand/v2"
1314
"os"
1415
"path"
1516
"path/filepath"
1617
"regexp"
1718
"slices"
1819
"strings"
1920
"sync"
21+
"time"
2022

2123
"golang.org/x/tools/gopls/internal/protocol"
2224
"golang.org/x/tools/gopls/internal/protocol/command"
@@ -136,6 +138,10 @@ type EditorConfig struct {
136138
// If non-nil, MessageResponder is used to respond to ShowMessageRequest
137139
// messages.
138140
MessageResponder func(params *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error)
141+
142+
// MaxMessageDelay is used for fuzzing message delivery to reproduce test
143+
// flakes.
144+
MaxMessageDelay time.Duration
139145
}
140146

141147
// NewEditor creates a new Editor.
@@ -162,10 +168,11 @@ func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, ho
162168
e.serverConn = conn
163169
e.Server = protocol.ServerDispatcher(conn)
164170
e.client = &Client{editor: e, hooks: hooks}
165-
conn.Go(bgCtx,
166-
protocol.Handlers(
167-
protocol.ClientHandler(e.client,
168-
jsonrpc2.MethodNotFound)))
171+
handler := protocol.ClientHandler(e.client, jsonrpc2.MethodNotFound)
172+
if e.config.MaxMessageDelay > 0 {
173+
handler = DelayedHandler(e.config.MaxMessageDelay, handler)
174+
}
175+
conn.Go(bgCtx, protocol.Handlers(handler))
169176

170177
if err := e.initialize(ctx); err != nil {
171178
return nil, err
@@ -174,6 +181,18 @@ func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, ho
174181
return e, nil
175182
}
176183

184+
// DelayedHandler waits [0, maxDelay) before handling each message.
185+
func DelayedHandler(maxDelay time.Duration, handler jsonrpc2.Handler) jsonrpc2.Handler {
186+
return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
187+
delay := time.Duration(rand.Int64N(int64(maxDelay)))
188+
select {
189+
case <-ctx.Done():
190+
case <-time.After(delay):
191+
}
192+
return handler(ctx, reply, req)
193+
}
194+
}
195+
177196
func (e *Editor) Stats() CallCounts {
178197
e.callsMu.Lock()
179198
defer e.callsMu.Unlock()

gopls/internal/test/integration/misc/webserver_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ const A = 1
135135
collectMessages := env.Awaiter.ListenToShownMessages()
136136
env.ExecuteCommand(params, &result)
137137

138+
// golang/go#70342: just because the command has finished does not mean
139+
// that we will have received the necessary notifications. Synchronize
140+
// using progress reports.
141+
env.Await(CompletedWork(params.Command, 1, false))
142+
138143
wantDocs, wantMessages := 0, 1
139144
if supported {
140145
wantDocs, wantMessages = 1, 0
@@ -144,10 +149,10 @@ const A = 1
144149
messages := collectMessages()
145150

146151
if gotDocs := len(docs); gotDocs != wantDocs {
147-
t.Errorf("GoDoc: got %d showDocument requests, want %d", gotDocs, wantDocs)
152+
t.Errorf("gopls.doc: got %d showDocument requests, want %d", gotDocs, wantDocs)
148153
}
149154
if gotMessages := len(messages); gotMessages != wantMessages {
150-
t.Errorf("GoDoc: got %d showMessage requests, want %d", gotMessages, wantMessages)
155+
t.Errorf("gopls.doc: got %d showMessage requests, want %d", gotMessages, wantMessages)
151156
}
152157
})
153158
})

gopls/internal/test/integration/options.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package integration
77
import (
88
"strings"
99
"testing"
10+
"time"
1011

1112
"golang.org/x/tools/gopls/internal/protocol"
1213
"golang.org/x/tools/gopls/internal/test/integration/fake"
@@ -192,3 +193,14 @@ func MessageResponder(f func(*protocol.ShowMessageRequestParams) (*protocol.Mess
192193
opts.editor.MessageResponder = f
193194
})
194195
}
196+
197+
// DelayMessages can be used to fuzz message delivery delays for the purpose of
198+
// reproducing test flakes.
199+
//
200+
// (Even though this option may be unused, keep it around to aid in debugging
201+
// future flakes.)
202+
func DelayMessages(upto time.Duration) RunOption {
203+
return optionSetter(func(opts *runConfig) {
204+
opts.editor.MaxMessageDelay = upto
205+
})
206+
}

0 commit comments

Comments
 (0)