diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-03-16 14:15:45 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-03-16 14:15:45 +0000 |
commit | cd3c7908c2ca750b27d330b4d257edc6818c4a5d (patch) | |
tree | 194d7b0e539d014393564a256bec571e18d6533a /gopls/internal/lsp/lsprpc/lsprpc_test.go | |
parent | 3225eca48f7ce16eb31b2dd5a170806c1214a49e (diff) | |
parent | 09c5a32afc5b66f28f166a68afe1fc71afbf9b73 (diff) | |
download | golang-x-tools-cd3c7908c2ca750b27d330b4d257edc6818c4a5d.tar.gz |
Snap for 9757917 from 09c5a32afc5b66f28f166a68afe1fc71afbf9b73 to build-tools-releasebuild-tools-release
Change-Id: If48e809642d94de846f47e34b88e446095e21aa5
Diffstat (limited to 'gopls/internal/lsp/lsprpc/lsprpc_test.go')
-rw-r--r-- | gopls/internal/lsp/lsprpc/lsprpc_test.go | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/gopls/internal/lsp/lsprpc/lsprpc_test.go b/gopls/internal/lsp/lsprpc/lsprpc_test.go new file mode 100644 index 000000000..3ec1a13d2 --- /dev/null +++ b/gopls/internal/lsp/lsprpc/lsprpc_test.go @@ -0,0 +1,345 @@ +// 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" + "errors" + "regexp" + "strings" + "testing" + "time" + + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" +) + +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(nil), false, nil) + ss.serverForTest = server + ts := servertest.NewPipeServer(ss, nil) + defer checkClose(t, ts.Close) + cc := ts.Connect(ctx) + cc.Go(ctx, protocol.ClientHandler(client, jsonrpc2.MethodNotFound)) + + if err := protocol.ServerDispatcher(cc).DidOpen(ctx, &protocol.DidOpenTextDocumentParams{}); err != nil { + t.Errorf("DidOpen: %v", err) + } + + 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{} + Completed chan error +} + +func (s WaitableServer) Hover(ctx context.Context, _ *protocol.HoverParams) (_ *protocol.Hover, err error) { + s.Started <- struct{}{} + defer func() { + s.Completed <- err + }() + select { + case <-ctx.Done(): + return nil, errors.New("cancelled hover") + case <-time.After(10 * time.Second): + } + return &protocol.Hover{}, nil +} + +func (s WaitableServer) ResolveCompletionItem(_ 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(nil), false, nil) + ss.serverForTest = s + tsDirect := servertest.NewTCPServer(serveCtx, ss, nil) + + forwarder, err := NewForwarder("tcp;"+tsDirect.Addr, nil) + if err != nil { + t.Fatal(err) + } + tsForwarded := servertest.NewPipeServer(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{}), + Completed: make(chan error), + } + 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() + ctx, cancel := context.WithCancel(ctx) + + result := make(chan error) + go func() { + _, err := sd.Hover(ctx, &protocol.HoverParams{}) + result <- err + }() + // Wait for the Hover request to start. + <-server.Started + cancel() + if err := <-result; err == nil { + t.Error("nil error for cancelled Hover(), want non-nil") + } + if err := <-server.Completed; err == nil || !strings.Contains(err.Error(), "cancelled hover") { + t.Errorf("Hover(): unexpected server-side error %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: fake.UnpackTxt(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(nil) + ss := NewStreamServer(cache, false, nil) + tsBackend := servertest.NewTCPServer(serverCtx, ss, nil) + + forwarder, err := NewForwarder("tcp;"+tsBackend.Addr, nil) + if err != nil { + t.Fatal(err) + } + tsForwarder := servertest.NewPipeServer(forwarder, nil) + + const skipApplyEdits = false + ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, tsForwarder, fake.ClientHooks{}, skipApplyEdits) + if err != nil { + t.Fatal(err) + } + defer ed1.Close(clientCtx) + ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, tsBackend, fake.ClientHooks{}, skipApplyEdits) + 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) { + ctx := context.Background() + + server := &initServer{} + _, 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") + } +} + +func TestListenParsing(t *testing.T) { + tests := []struct { + input, wantNetwork, wantAddr string + }{ + {"127.0.0.1:0", "tcp", "127.0.0.1:0"}, + {"unix;/tmp/sock", "unix", "/tmp/sock"}, + {"auto", "auto", ""}, + {"auto;foo", "auto", "foo"}, + } + + for _, test := range tests { + gotNetwork, gotAddr := ParseAddr(test.input) + if gotNetwork != test.wantNetwork { + t.Errorf("network = %q, want %q", gotNetwork, test.wantNetwork) + } + if gotAddr != test.wantAddr { + t.Errorf("addr = %q, want %q", gotAddr, test.wantAddr) + } + } +} |