Skip to content

Commit 741f65b

Browse files
committed
internal/lsp/lsprpc: add a forwarder handler
Add a forwarder handler that alters messages before forwarding, for now, it just intercepts the "exit" message. Also, make it easier to write regression tests for a shared gopls instance, by adding a helper that instantiates two connected environments, and only runs in the shared execution modes. Updates golang/go#36879 Updates golang/go#34111 Change-Id: I7673f72ab71b5c7fd6ad65d274c15132a942e06a Reviewed-on: https://go-review.googlesource.com/c/tools/+/218778 Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Heschi Kreinick <[email protected]>
1 parent ca8c272 commit 741f65b

File tree

6 files changed

+139
-35
lines changed

6 files changed

+139
-35
lines changed

internal/lsp/general.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,17 @@ func (s *Server) shutdown(ctx context.Context) error {
272272
return nil
273273
}
274274

275+
// ServerExitFunc is used to exit when requested by the client. It is mutable
276+
// for testing purposes.
277+
var ServerExitFunc = os.Exit
278+
275279
func (s *Server) exit(ctx context.Context) error {
276280
s.stateMu.Lock()
277281
defer s.stateMu.Unlock()
278282
if s.state != serverShutDown {
279-
os.Exit(1)
283+
ServerExitFunc(1)
280284
}
281-
os.Exit(0)
285+
ServerExitFunc(0)
282286
return nil
283287
}
284288

internal/lsp/lsprpc/lsprpc.go

+26
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"context"
1111
"fmt"
1212
"net"
13+
"os"
1314

1415
"golang.org/x/sync/errgroup"
1516
"golang.org/x/tools/internal/jsonrpc2"
@@ -93,6 +94,7 @@ func (f *Forwarder) ServeStream(ctx context.Context, stream jsonrpc2.Stream) err
9394
serverConn.AddHandler(protocol.Canceller{})
9495
clientConn.AddHandler(protocol.ServerHandler(server))
9596
clientConn.AddHandler(protocol.Canceller{})
97+
clientConn.AddHandler(forwarderHandler{})
9698
if f.withTelemetry {
9799
clientConn.AddHandler(telemetryHandler{})
98100
}
@@ -106,3 +108,27 @@ func (f *Forwarder) ServeStream(ctx context.Context, stream jsonrpc2.Stream) err
106108
})
107109
return g.Wait()
108110
}
111+
112+
// ForwarderExitFunc is used to exit the forwarder process. It is mutable for
113+
// testing purposes.
114+
var ForwarderExitFunc = os.Exit
115+
116+
// forwarderHandler intercepts 'exit' messages to prevent the shared gopls
117+
// instance from exiting. In the future it may also intercept 'shutdown' to
118+
// provide more graceful shutdown of the client connection.
119+
type forwarderHandler struct {
120+
jsonrpc2.EmptyHandler
121+
}
122+
123+
func (forwarderHandler) Deliver(ctx context.Context, r *jsonrpc2.Request, delivered bool) bool {
124+
// TODO(golang.org/issues/34111): we should more gracefully disconnect here,
125+
// once that process exists.
126+
if r.Method == "exit" {
127+
ForwarderExitFunc(0)
128+
// Still return true here to prevent the message from being delivered: in
129+
// tests, ForwarderExitFunc may be overridden to something that doesn't
130+
// exit the process.
131+
return true
132+
}
133+
return false
134+
}

internal/lsp/regtest/diagnostics_test.go

-22
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,6 @@ func TestDiagnosticErrorInEditedFile(t *testing.T) {
3737
})
3838
}
3939

40-
func TestSimultaneousEdits(t *testing.T) {
41-
t.Parallel()
42-
runner.Run(t, exampleProgram, func(ctx context.Context, t *testing.T, env1 *Env) {
43-
// Create a second test session connected to the same workspace and server
44-
// as the first.
45-
env2 := NewEnv(ctx, t, env1.W, env1.Server)
46-
47-
// In editor #1, break fmt.Println as before.
48-
edit1 := fake.NewEdit(5, 11, 5, 12, "")
49-
env1.OpenFile("main.go")
50-
env1.EditBuffer("main.go", edit1)
51-
// In editor #2 remove the closing brace.
52-
edit2 := fake.NewEdit(6, 0, 6, 1, "")
53-
env2.OpenFile("main.go")
54-
env2.EditBuffer("main.go", edit2)
55-
56-
// Now check that we got different diagnostics in each environment.
57-
env1.Await(DiagnosticAt("main.go", 5, 5))
58-
env2.Await(DiagnosticAt("main.go", 7, 0))
59-
})
60-
}
61-
6240
const brokenFile = `package main
6341
6442
const Foo = "abc

internal/lsp/regtest/env.go

+23-11
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ const (
4040
// remote), any tests that execute on the same Runner will share the same
4141
// state.
4242
type Runner struct {
43-
ts *servertest.TCPServer
44-
modes EnvMode
45-
timeout time.Duration
43+
ts *servertest.TCPServer
44+
defaultModes EnvMode
45+
timeout time.Duration
4646
}
4747

4848
// NewTestRunner creates a Runner with its shared state initialized, ready to
@@ -51,9 +51,9 @@ func NewTestRunner(modes EnvMode, testTimeout time.Duration) *Runner {
5151
ss := lsprpc.NewStreamServer(cache.New(nil), false)
5252
ts := servertest.NewTCPServer(context.Background(), ss)
5353
return &Runner{
54-
ts: ts,
55-
modes: modes,
56-
timeout: testTimeout,
54+
ts: ts,
55+
defaultModes: modes,
56+
timeout: testTimeout,
5757
}
5858
}
5959

@@ -62,12 +62,17 @@ func (r *Runner) Close() error {
6262
return r.ts.Close()
6363
}
6464

65-
// Run executes the test function in in all configured gopls execution modes.
66-
// For each a test run, a new workspace is created containing the un-txtared
67-
// files specified by filedata.
65+
// Run executes the test function in the default configured gopls execution
66+
// modes. For each a test run, a new workspace is created containing the
67+
// un-txtared files specified by filedata.
6868
func (r *Runner) Run(t *testing.T, filedata string, test func(context.Context, *testing.T, *Env)) {
6969
t.Helper()
70+
r.RunInMode(r.defaultModes, t, filedata, test)
71+
}
7072

73+
// RunInMode runs the test in the execution modes specified by the modes bitmask.
74+
func (r *Runner) RunInMode(modes EnvMode, t *testing.T, filedata string, test func(ctx context.Context, t *testing.T, e *Env)) {
75+
t.Helper()
7176
tests := []struct {
7277
name string
7378
mode EnvMode
@@ -80,7 +85,7 @@ func (r *Runner) Run(t *testing.T, filedata string, test func(context.Context, *
8085

8186
for _, tc := range tests {
8287
tc := tc
83-
if r.modes&tc.mode == 0 {
88+
if modes&tc.mode == 0 {
8489
continue
8590
}
8691
t.Run(tc.name, func(t *testing.T) {
@@ -231,10 +236,17 @@ func (e *Env) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsPar
231236
close(condition.met)
232237
}
233238
}
234-
235239
return nil
236240
}
237241

242+
// CloseEditor shuts down the editor, calling t.Fatal on any error.
243+
func (e *Env) CloseEditor() {
244+
e.t.Helper()
245+
if err := e.E.ShutdownAndExit(e.ctx); err != nil {
246+
e.t.Fatal(err)
247+
}
248+
}
249+
238250
func meetsCondition(m map[string]*protocol.PublishDiagnosticsParams, expectations []DiagnosticExpectation) bool {
239251
for _, e := range expectations {
240252
if !e.IsMet(m) {

internal/lsp/regtest/reg_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,29 @@
55
package regtest
66

77
import (
8+
"fmt"
89
"os"
910
"testing"
1011
"time"
12+
13+
"golang.org/x/tools/internal/lsp"
14+
"golang.org/x/tools/internal/lsp/lsprpc"
1115
)
1216

1317
var runner *Runner
1418

1519
func TestMain(m *testing.M) {
20+
// Override functions that would shut down the test process
21+
defer func(lspExit, forwarderExit func(code int)) {
22+
lsp.ServerExitFunc = lspExit
23+
lsprpc.ForwarderExitFunc = forwarderExit
24+
}(lsp.ServerExitFunc, lsprpc.ForwarderExitFunc)
25+
// None of these regtests should be able to shut down a server process.
26+
lsp.ServerExitFunc = func(code int) {
27+
panic(fmt.Sprintf("LSP server exited with code %d", code))
28+
}
29+
// We don't want our forwarders to exit, but it's OK if they would have.
30+
lsprpc.ForwarderExitFunc = func(code int) {}
1631
runner = NewTestRunner(AllModes, 30*time.Second)
1732
defer runner.Close()
1833
os.Exit(m.Run())

internal/lsp/regtest/shared_test.go

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package regtest
6+
7+
import (
8+
"context"
9+
"testing"
10+
11+
"golang.org/x/tools/internal/lsp/fake"
12+
)
13+
14+
const sharedProgram = `
15+
-- go.mod --
16+
module mod
17+
18+
go 1.12
19+
-- main.go --
20+
package main
21+
22+
import "fmt"
23+
24+
func main() {
25+
fmt.Println("Hello World.")
26+
}`
27+
28+
func runShared(t *testing.T, program string, testFunc func(ctx context.Context, t *testing.T, env1 *Env, env2 *Env)) {
29+
runner.RunInMode(Forwarded, t, sharedProgram, func(ctx context.Context, t *testing.T, env1 *Env) {
30+
// Create a second test session connected to the same workspace and server
31+
// as the first.
32+
env2 := NewEnv(ctx, t, env1.W, env1.Server)
33+
testFunc(ctx, t, env1, env2)
34+
})
35+
}
36+
37+
func TestSimultaneousEdits(t *testing.T) {
38+
t.Parallel()
39+
runner.Run(t, exampleProgram, func(ctx context.Context, t *testing.T, env1 *Env) {
40+
// Create a second test session connected to the same workspace and server
41+
// as the first.
42+
env2 := NewEnv(ctx, t, env1.W, env1.Server)
43+
44+
// In editor #1, break fmt.Println as before.
45+
edit1 := fake.NewEdit(5, 11, 5, 12, "")
46+
env1.OpenFile("main.go")
47+
env1.EditBuffer("main.go", edit1)
48+
// In editor #2 remove the closing brace.
49+
edit2 := fake.NewEdit(6, 0, 6, 1, "")
50+
env2.OpenFile("main.go")
51+
env2.EditBuffer("main.go", edit2)
52+
53+
// Now check that we got different diagnostics in each environment.
54+
env1.Await(DiagnosticAt("main.go", 5, 5))
55+
env2.Await(DiagnosticAt("main.go", 7, 0))
56+
})
57+
}
58+
59+
func TestShutdown(t *testing.T) {
60+
t.Parallel()
61+
runShared(t, sharedProgram, func(ctx context.Context, t *testing.T, env1 *Env, env2 *Env) {
62+
env1.CloseEditor()
63+
// Now make an edit in editor #2 to trigger diagnostics.
64+
edit2 := fake.NewEdit(6, 0, 6, 1, "")
65+
env2.OpenFile("main.go")
66+
env2.EditBuffer("main.go", edit2)
67+
env2.Await(DiagnosticAt("main.go", 7, 0))
68+
})
69+
}

0 commit comments

Comments
 (0)