| // 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 servertest provides utilities for running tests against a remote LSP |
| // server. |
| package servertest |
| |
| import ( |
| "context" |
| "fmt" |
| "io" |
| "net" |
| "sync" |
| |
| "golang.org/x/tools/internal/jsonrpc2" |
| ) |
| |
| // Connector is the interface used to connect to a server. |
| type Connector interface { |
| Connect(context.Context) *jsonrpc2.Conn |
| } |
| |
| // TCPServer is a helper for executing tests against a remote jsonrpc2 |
| // connection. Once initialized, its Addr field may be used to connect a |
| // jsonrpc2 client. |
| type TCPServer struct { |
| Addr string |
| |
| ln net.Listener |
| cls *closerList |
| } |
| |
| // NewTCPServer returns a new test server listening on local tcp port and |
| // serving incoming jsonrpc2 streams using the provided stream server. It |
| // panics on any error. |
| func NewTCPServer(ctx context.Context, server jsonrpc2.StreamServer) *TCPServer { |
| ln, err := net.Listen("tcp", "127.0.0.1:0") |
| if err != nil { |
| panic(fmt.Sprintf("servertest: failed to listen: %v", err)) |
| } |
| go jsonrpc2.Serve(ctx, ln, server, 0) |
| return &TCPServer{Addr: ln.Addr().String(), ln: ln, cls: &closerList{}} |
| } |
| |
| // Connect dials the test server and returns a jsonrpc2 Connection that is |
| // ready for use. |
| func (s *TCPServer) Connect(ctx context.Context) *jsonrpc2.Conn { |
| netConn, err := net.Dial("tcp", s.Addr) |
| if err != nil { |
| panic(fmt.Sprintf("servertest: failed to connect to test instance: %v", err)) |
| } |
| s.cls.add(func() { |
| netConn.Close() |
| }) |
| conn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn, netConn)) |
| go conn.Run(ctx) |
| return conn |
| } |
| |
| // Close closes all connected pipes. |
| func (s *TCPServer) Close() error { |
| s.cls.closeAll() |
| return nil |
| } |
| |
| // PipeServer is a test server that handles connections over io.Pipes. |
| type PipeServer struct { |
| server jsonrpc2.StreamServer |
| cls *closerList |
| } |
| |
| // NewPipeServer returns a test server that can be connected to via io.Pipes. |
| func NewPipeServer(ctx context.Context, server jsonrpc2.StreamServer) *PipeServer { |
| return &PipeServer{server: server, cls: &closerList{}} |
| } |
| |
| // Connect creates new io.Pipes and binds them to the underlying StreamServer. |
| func (s *PipeServer) Connect(ctx context.Context) *jsonrpc2.Conn { |
| // Pipes connect like this: |
| // Client🡒(sWriter)🡒(sReader)🡒Server |
| // 🡔(cReader)🡐(cWriter)🡗 |
| sReader, sWriter := io.Pipe() |
| cReader, cWriter := io.Pipe() |
| s.cls.add(func() { |
| sReader.Close() |
| sWriter.Close() |
| cReader.Close() |
| cWriter.Close() |
| }) |
| serverStream := jsonrpc2.NewStream(sReader, cWriter) |
| go s.server.ServeStream(ctx, serverStream) |
| |
| clientStream := jsonrpc2.NewStream(cReader, sWriter) |
| clientConn := jsonrpc2.NewConn(clientStream) |
| go clientConn.Run(ctx) |
| return clientConn |
| } |
| |
| // Close closes all connected pipes. |
| func (s *PipeServer) Close() error { |
| s.cls.closeAll() |
| return nil |
| } |
| |
| // closerList tracks closers to run when a testserver is closed. This is a |
| // convenience, so that callers don't have to worry about closing each |
| // connection. |
| type closerList struct { |
| mu sync.Mutex |
| closers []func() |
| } |
| |
| func (l *closerList) add(closer func()) { |
| l.mu.Lock() |
| defer l.mu.Unlock() |
| l.closers = append(l.closers, closer) |
| } |
| |
| func (l *closerList) closeAll() { |
| l.mu.Lock() |
| defer l.mu.Unlock() |
| for _, closer := range l.closers { |
| closer() |
| } |
| } |