--- /dev/null
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package lsprpc
+
+import (
+ "context"
+ "regexp"
+ "sync"
+ "testing"
+ "time"
+
+ "golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/jsonrpc2"
+ "golang.org/x/tools/internal/jsonrpc2/servertest"
+ "golang.org/x/tools/internal/lsp/cache"
+ "golang.org/x/tools/internal/lsp/debug"
+ "golang.org/x/tools/internal/lsp/fake"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/testenv"
+)
+
+type fakeClient struct {
+ protocol.Client
+
+ logs chan string
+}
+
+func (c fakeClient) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error {
+ c.logs <- params.Message
+ return nil
+}
+
+// fakeServer is intended to be embedded in the test fakes below, to trivially
+// implement Shutdown.
+type fakeServer struct {
+ protocol.Server
+}
+
+func (fakeServer) Shutdown(ctx context.Context) error {
+ return nil
+}
+
+type pingServer struct{ fakeServer }
+
+func (s pingServer) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
+ event.Log(ctx, "ping")
+ return nil
+}
+
+func TestClientLogging(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ server := pingServer{}
+ client := fakeClient{logs: make(chan string, 10)}
+
+ ctx = debug.WithInstance(ctx, "", "")
+ ss := NewStreamServer(cache.New(ctx, nil), false)
+ ss.serverForTest = server
+ ts := servertest.NewPipeServer(ctx, ss, nil)
+ defer checkClose(t, ts.Close)
+ cc := ts.Connect(ctx)
+ cc.Go(ctx, protocol.ClientHandler(client, jsonrpc2.MethodNotFound))
+
+ protocol.ServerDispatcher(cc).DidOpen(ctx, &protocol.DidOpenTextDocumentParams{})
+
+ select {
+ case got := <-client.logs:
+ want := "ping"
+ matched, err := regexp.MatchString(want, got)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !matched {
+ t.Errorf("got log %q, want a log containing %q", got, want)
+ }
+ case <-time.After(1 * time.Second):
+ t.Error("timeout waiting for client log")
+ }
+}
+
+// waitableServer instruments LSP request so that we can control their timing.
+// The requests chosen are arbitrary: we simply needed one that blocks, and
+// another that doesn't.
+type waitableServer struct {
+ fakeServer
+
+ started chan struct{}
+}
+
+func (s waitableServer) Hover(ctx context.Context, _ *protocol.HoverParams) (*protocol.Hover, error) {
+ s.started <- struct{}{}
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-time.After(200 * time.Millisecond):
+ }
+ return &protocol.Hover{}, nil
+}
+
+func (s waitableServer) Resolve(_ context.Context, item *protocol.CompletionItem) (*protocol.CompletionItem, error) {
+ return item, nil
+}
+
+func checkClose(t *testing.T, closer func() error) {
+ t.Helper()
+ if err := closer(); err != nil {
+ t.Errorf("closing: %v", err)
+ }
+}
+
+func setupForwarding(ctx context.Context, t *testing.T, s protocol.Server) (direct, forwarded servertest.Connector, cleanup func()) {
+ t.Helper()
+ serveCtx := debug.WithInstance(ctx, "", "")
+ ss := NewStreamServer(cache.New(serveCtx, nil), false)
+ ss.serverForTest = s
+ tsDirect := servertest.NewTCPServer(serveCtx, ss, nil)
+
+ forwarderCtx := debug.WithInstance(ctx, "", "")
+ forwarder := NewForwarder("tcp", tsDirect.Addr)
+ tsForwarded := servertest.NewPipeServer(forwarderCtx, forwarder, nil)
+ return tsDirect, tsForwarded, func() {
+ checkClose(t, tsDirect.Close)
+ checkClose(t, tsForwarded.Close)
+ }
+}
+
+func TestRequestCancellation(t *testing.T) {
+ ctx := context.Background()
+ server := waitableServer{
+ started: make(chan struct{}),
+ }
+ tsDirect, tsForwarded, cleanup := setupForwarding(ctx, t, server)
+ defer cleanup()
+ tests := []struct {
+ serverType string
+ ts servertest.Connector
+ }{
+ {"direct", tsDirect},
+ {"forwarder", tsForwarded},
+ }
+
+ for _, test := range tests {
+ t.Run(test.serverType, func(t *testing.T) {
+ cc := test.ts.Connect(ctx)
+ sd := protocol.ServerDispatcher(cc)
+ cc.Go(ctx,
+ protocol.Handlers(
+ jsonrpc2.MethodNotFound))
+
+ ctx := context.Background()
+ ctx1, cancel1 := context.WithCancel(ctx)
+ var (
+ err1, err2 error
+ wg sync.WaitGroup
+ )
+ wg.Add(2)
+ go func() {
+ defer wg.Done()
+ _, err1 = sd.Hover(ctx1, &protocol.HoverParams{})
+ }()
+ go func() {
+ defer wg.Done()
+ _, err2 = sd.Resolve(ctx, &protocol.CompletionItem{})
+ }()
+ // Wait for the Hover request to start.
+ <-server.started
+ cancel1()
+ wg.Wait()
+ if err1 == nil {
+ t.Errorf("cancelled Hover(): got nil err")
+ }
+ if err2 != nil {
+ t.Errorf("uncancelled Hover(): err: %v", err2)
+ }
+ if _, err := sd.Resolve(ctx, &protocol.CompletionItem{}); err != nil {
+ t.Errorf("subsequent Hover(): %v", err)
+ }
+ })
+ }
+}
+
+const exampleProgram = `
+-- go.mod --
+module mod
+
+go 1.12
+-- main.go --
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello World.")
+}`
+
+func TestDebugInfoLifecycle(t *testing.T) {
+ sb, err := fake.NewSandbox(&fake.SandboxConfig{Files: exampleProgram})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() {
+ if err := sb.Close(); err != nil {
+ // TODO(golang/go#38490): we can't currently make this an error because
+ // it fails on Windows: the workspace directory is still locked by a
+ // separate Go process.
+ // Once we have a reliable way to wait for proper shutdown, make this an
+ // error.
+ t.Logf("closing workspace failed: %v", err)
+ }
+ }()
+
+ baseCtx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ clientCtx := debug.WithInstance(baseCtx, "", "")
+ serverCtx := debug.WithInstance(baseCtx, "", "")
+
+ cache := cache.New(serverCtx, nil)
+ ss := NewStreamServer(cache, false)
+ tsBackend := servertest.NewTCPServer(serverCtx, ss, nil)
+
+ forwarder := NewForwarder("tcp", tsBackend.Addr)
+ tsForwarder := servertest.NewPipeServer(clientCtx, forwarder, nil)
+
+ conn1 := tsForwarder.Connect(clientCtx)
+ ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, conn1, fake.ClientHooks{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ed1.Close(clientCtx)
+ conn2 := tsBackend.Connect(baseCtx)
+ ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, conn2, fake.ClientHooks{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ed2.Close(baseCtx)
+
+ serverDebug := debug.GetInstance(serverCtx)
+ if got, want := len(serverDebug.State.Clients()), 2; got != want {
+ t.Errorf("len(server:Clients) = %d, want %d", got, want)
+ }
+ if got, want := len(serverDebug.State.Sessions()), 2; got != want {
+ t.Errorf("len(server:Sessions) = %d, want %d", got, want)
+ }
+ clientDebug := debug.GetInstance(clientCtx)
+ if got, want := len(clientDebug.State.Servers()), 1; got != want {
+ t.Errorf("len(client:Servers) = %d, want %d", got, want)
+ }
+ // Close one of the connections to verify that the client and session were
+ // dropped.
+ if err := ed1.Close(clientCtx); err != nil {
+ t.Fatal(err)
+ }
+ /*TODO: at this point we have verified the editor is closed
+ However there is no way currently to wait for all associated go routines to
+ go away, and we need to wait for those to trigger the client drop
+ for now we just give it a little bit of time, but we need to fix this
+ in a principled way
+ */
+ start := time.Now()
+ delay := time.Millisecond
+ const maxWait = time.Second
+ for len(serverDebug.State.Clients()) > 1 {
+ if time.Since(start) > maxWait {
+ break
+ }
+ time.Sleep(delay)
+ delay *= 2
+ }
+ if got, want := len(serverDebug.State.Clients()), 1; got != want {
+ t.Errorf("len(server:Clients) = %d, want %d", got, want)
+ }
+ if got, want := len(serverDebug.State.Sessions()), 1; got != want {
+ t.Errorf("len(server:Sessions()) = %d, want %d", got, want)
+ }
+}
+
+type initServer struct {
+ fakeServer
+
+ params *protocol.ParamInitialize
+}
+
+func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) {
+ s.params = params
+ return &protocol.InitializeResult{}, nil
+}
+
+func TestEnvForwarding(t *testing.T) {
+ testenv.NeedsGo1Point(t, 13)
+ server := &initServer{}
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ _, tsForwarded, cleanup := setupForwarding(ctx, t, server)
+ defer cleanup()
+
+ conn := tsForwarded.Connect(ctx)
+ conn.Go(ctx, jsonrpc2.MethodNotFound)
+ dispatch := protocol.ServerDispatcher(conn)
+ initParams := &protocol.ParamInitialize{}
+ initParams.InitializationOptions = map[string]interface{}{
+ "env": map[string]interface{}{
+ "GONOPROXY": "example.com",
+ },
+ }
+ _, err := dispatch.Initialize(ctx, initParams)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if server.params == nil {
+ t.Fatalf("initialize params are unset")
+ }
+ env := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{})
+
+ // Check for an arbitrary Go variable. It should be set.
+ if _, ok := env["GOPRIVATE"]; !ok {
+ t.Errorf("Go environment variable GOPRIVATE unset in initialization options")
+ }
+ // Check that the variable present in our user config was not overwritten.
+ if v := env["GONOPROXY"]; v != "example.com" {
+ t.Errorf("GONOPROXY environment variable was overwritten")
+ }
+}