github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/lsprpc/lsprpc_test.go (about) 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 lsprpc 6 7 import ( 8 "context" 9 "errors" 10 "regexp" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/powerman/golang-tools/internal/event" 16 "github.com/powerman/golang-tools/internal/jsonrpc2" 17 "github.com/powerman/golang-tools/internal/jsonrpc2/servertest" 18 "github.com/powerman/golang-tools/internal/lsp/cache" 19 "github.com/powerman/golang-tools/internal/lsp/debug" 20 "github.com/powerman/golang-tools/internal/lsp/fake" 21 "github.com/powerman/golang-tools/internal/lsp/protocol" 22 "github.com/powerman/golang-tools/internal/testenv" 23 ) 24 25 type FakeClient struct { 26 protocol.Client 27 28 Logs chan string 29 } 30 31 func (c FakeClient) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error { 32 c.Logs <- params.Message 33 return nil 34 } 35 36 // fakeServer is intended to be embedded in the test fakes below, to trivially 37 // implement Shutdown. 38 type fakeServer struct { 39 protocol.Server 40 } 41 42 func (fakeServer) Shutdown(ctx context.Context) error { 43 return nil 44 } 45 46 type PingServer struct{ fakeServer } 47 48 func (s PingServer) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { 49 event.Log(ctx, "ping") 50 return nil 51 } 52 53 func TestClientLogging(t *testing.T) { 54 ctx, cancel := context.WithCancel(context.Background()) 55 defer cancel() 56 57 server := PingServer{} 58 client := FakeClient{Logs: make(chan string, 10)} 59 60 ctx = debug.WithInstance(ctx, "", "") 61 ss := NewStreamServer(cache.New(nil), false) 62 ss.serverForTest = server 63 ts := servertest.NewPipeServer(ctx, ss, nil) 64 defer checkClose(t, ts.Close) 65 cc := ts.Connect(ctx) 66 cc.Go(ctx, protocol.ClientHandler(client, jsonrpc2.MethodNotFound)) 67 68 if err := protocol.ServerDispatcher(cc).DidOpen(ctx, &protocol.DidOpenTextDocumentParams{}); err != nil { 69 t.Errorf("DidOpen: %v", err) 70 } 71 72 select { 73 case got := <-client.Logs: 74 want := "ping" 75 matched, err := regexp.MatchString(want, got) 76 if err != nil { 77 t.Fatal(err) 78 } 79 if !matched { 80 t.Errorf("got log %q, want a log containing %q", got, want) 81 } 82 case <-time.After(1 * time.Second): 83 t.Error("timeout waiting for client log") 84 } 85 } 86 87 // WaitableServer instruments LSP request so that we can control their timing. 88 // The requests chosen are arbitrary: we simply needed one that blocks, and 89 // another that doesn't. 90 type WaitableServer struct { 91 fakeServer 92 93 Started chan struct{} 94 Completed chan error 95 } 96 97 func (s WaitableServer) Hover(ctx context.Context, _ *protocol.HoverParams) (_ *protocol.Hover, err error) { 98 s.Started <- struct{}{} 99 defer func() { 100 s.Completed <- err 101 }() 102 select { 103 case <-ctx.Done(): 104 return nil, errors.New("cancelled hover") 105 case <-time.After(10 * time.Second): 106 } 107 return &protocol.Hover{}, nil 108 } 109 110 func (s WaitableServer) ResolveCompletionItem(_ context.Context, item *protocol.CompletionItem) (*protocol.CompletionItem, error) { 111 return item, nil 112 } 113 114 func checkClose(t *testing.T, closer func() error) { 115 t.Helper() 116 if err := closer(); err != nil { 117 t.Errorf("closing: %v", err) 118 } 119 } 120 121 func setupForwarding(ctx context.Context, t *testing.T, s protocol.Server) (direct, forwarded servertest.Connector, cleanup func()) { 122 t.Helper() 123 serveCtx := debug.WithInstance(ctx, "", "") 124 ss := NewStreamServer(cache.New(nil), false) 125 ss.serverForTest = s 126 tsDirect := servertest.NewTCPServer(serveCtx, ss, nil) 127 128 forwarderCtx := debug.WithInstance(ctx, "", "") 129 forwarder, err := NewForwarder("tcp;"+tsDirect.Addr, nil) 130 if err != nil { 131 t.Fatal(err) 132 } 133 tsForwarded := servertest.NewPipeServer(forwarderCtx, forwarder, nil) 134 return tsDirect, tsForwarded, func() { 135 checkClose(t, tsDirect.Close) 136 checkClose(t, tsForwarded.Close) 137 } 138 } 139 140 func TestRequestCancellation(t *testing.T) { 141 ctx := context.Background() 142 server := WaitableServer{ 143 Started: make(chan struct{}), 144 Completed: make(chan error), 145 } 146 tsDirect, tsForwarded, cleanup := setupForwarding(ctx, t, server) 147 defer cleanup() 148 tests := []struct { 149 serverType string 150 ts servertest.Connector 151 }{ 152 {"direct", tsDirect}, 153 {"forwarder", tsForwarded}, 154 } 155 156 for _, test := range tests { 157 t.Run(test.serverType, func(t *testing.T) { 158 cc := test.ts.Connect(ctx) 159 sd := protocol.ServerDispatcher(cc) 160 cc.Go(ctx, 161 protocol.Handlers( 162 jsonrpc2.MethodNotFound)) 163 164 ctx := context.Background() 165 ctx, cancel := context.WithCancel(ctx) 166 167 result := make(chan error) 168 go func() { 169 _, err := sd.Hover(ctx, &protocol.HoverParams{}) 170 result <- err 171 }() 172 // Wait for the Hover request to start. 173 <-server.Started 174 cancel() 175 if err := <-result; err == nil { 176 t.Error("nil error for cancelled Hover(), want non-nil") 177 } 178 if err := <-server.Completed; err == nil || !strings.Contains(err.Error(), "cancelled hover") { 179 t.Errorf("Hover(): unexpected server-side error %v", err) 180 } 181 }) 182 } 183 } 184 185 const exampleProgram = ` 186 -- go.mod -- 187 module mod 188 189 go 1.12 190 -- main.go -- 191 package main 192 193 import "fmt" 194 195 func main() { 196 fmt.Println("Hello World.") 197 }` 198 199 func TestDebugInfoLifecycle(t *testing.T) { 200 sb, err := fake.NewSandbox(&fake.SandboxConfig{Files: fake.UnpackTxt(exampleProgram)}) 201 if err != nil { 202 t.Fatal(err) 203 } 204 defer func() { 205 if err := sb.Close(); err != nil { 206 // TODO(golang/go#38490): we can't currently make this an error because 207 // it fails on Windows: the workspace directory is still locked by a 208 // separate Go process. 209 // Once we have a reliable way to wait for proper shutdown, make this an 210 // error. 211 t.Logf("closing workspace failed: %v", err) 212 } 213 }() 214 215 baseCtx, cancel := context.WithCancel(context.Background()) 216 defer cancel() 217 clientCtx := debug.WithInstance(baseCtx, "", "") 218 serverCtx := debug.WithInstance(baseCtx, "", "") 219 220 cache := cache.New(nil) 221 ss := NewStreamServer(cache, false) 222 tsBackend := servertest.NewTCPServer(serverCtx, ss, nil) 223 224 forwarder, err := NewForwarder("tcp;"+tsBackend.Addr, nil) 225 if err != nil { 226 t.Fatal(err) 227 } 228 tsForwarder := servertest.NewPipeServer(clientCtx, forwarder, nil) 229 230 conn1 := tsForwarder.Connect(clientCtx) 231 ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, conn1, fake.ClientHooks{}) 232 if err != nil { 233 t.Fatal(err) 234 } 235 defer ed1.Close(clientCtx) 236 conn2 := tsBackend.Connect(baseCtx) 237 ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, conn2, fake.ClientHooks{}) 238 if err != nil { 239 t.Fatal(err) 240 } 241 defer ed2.Close(baseCtx) 242 243 serverDebug := debug.GetInstance(serverCtx) 244 if got, want := len(serverDebug.State.Clients()), 2; got != want { 245 t.Errorf("len(server:Clients) = %d, want %d", got, want) 246 } 247 if got, want := len(serverDebug.State.Sessions()), 2; got != want { 248 t.Errorf("len(server:Sessions) = %d, want %d", got, want) 249 } 250 clientDebug := debug.GetInstance(clientCtx) 251 if got, want := len(clientDebug.State.Servers()), 1; got != want { 252 t.Errorf("len(client:Servers) = %d, want %d", got, want) 253 } 254 // Close one of the connections to verify that the client and session were 255 // dropped. 256 if err := ed1.Close(clientCtx); err != nil { 257 t.Fatal(err) 258 } 259 /*TODO: at this point we have verified the editor is closed 260 However there is no way currently to wait for all associated go routines to 261 go away, and we need to wait for those to trigger the client drop 262 for now we just give it a little bit of time, but we need to fix this 263 in a principled way 264 */ 265 start := time.Now() 266 delay := time.Millisecond 267 const maxWait = time.Second 268 for len(serverDebug.State.Clients()) > 1 { 269 if time.Since(start) > maxWait { 270 break 271 } 272 time.Sleep(delay) 273 delay *= 2 274 } 275 if got, want := len(serverDebug.State.Clients()), 1; got != want { 276 t.Errorf("len(server:Clients) = %d, want %d", got, want) 277 } 278 if got, want := len(serverDebug.State.Sessions()), 1; got != want { 279 t.Errorf("len(server:Sessions()) = %d, want %d", got, want) 280 } 281 } 282 283 type initServer struct { 284 fakeServer 285 286 params *protocol.ParamInitialize 287 } 288 289 func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { 290 s.params = params 291 return &protocol.InitializeResult{}, nil 292 } 293 294 func TestEnvForwarding(t *testing.T) { 295 testenv.NeedsGo1Point(t, 13) 296 server := &initServer{} 297 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 298 defer cancel() 299 _, tsForwarded, cleanup := setupForwarding(ctx, t, server) 300 defer cleanup() 301 302 conn := tsForwarded.Connect(ctx) 303 conn.Go(ctx, jsonrpc2.MethodNotFound) 304 dispatch := protocol.ServerDispatcher(conn) 305 initParams := &protocol.ParamInitialize{} 306 initParams.InitializationOptions = map[string]interface{}{ 307 "env": map[string]interface{}{ 308 "GONOPROXY": "example.com", 309 }, 310 } 311 _, err := dispatch.Initialize(ctx, initParams) 312 if err != nil { 313 t.Fatal(err) 314 } 315 if server.params == nil { 316 t.Fatalf("initialize params are unset") 317 } 318 env := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{}) 319 320 // Check for an arbitrary Go variable. It should be set. 321 if _, ok := env["GOPRIVATE"]; !ok { 322 t.Errorf("Go environment variable GOPRIVATE unset in initialization options") 323 } 324 // Check that the variable present in our user config was not overwritten. 325 if v := env["GONOPROXY"]; v != "example.com" { 326 t.Errorf("GONOPROXY environment variable was overwritten") 327 } 328 } 329 330 func TestListenParsing(t *testing.T) { 331 tests := []struct { 332 input, wantNetwork, wantAddr string 333 }{ 334 {"127.0.0.1:0", "tcp", "127.0.0.1:0"}, 335 {"unix;/tmp/sock", "unix", "/tmp/sock"}, 336 {"auto", "auto", ""}, 337 {"auto;foo", "auto", "foo"}, 338 } 339 340 for _, test := range tests { 341 gotNetwork, gotAddr := ParseAddr(test.input) 342 if gotNetwork != test.wantNetwork { 343 t.Errorf("network = %q, want %q", gotNetwork, test.wantNetwork) 344 } 345 if gotAddr != test.wantAddr { 346 t.Errorf("addr = %q, want %q", gotAddr, test.wantAddr) 347 } 348 } 349 }