--- /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 servertest provides utilities for running tests against a remote LSP
+// server.
+package servertest
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "strings"
+ "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 {
+ *connList
+
+ Addr string
+
+ ln net.Listener
+ framer jsonrpc2.Framer
+}
+
+// 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, framer jsonrpc2.Framer) *TCPServer {
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ panic(fmt.Sprintf("servertest: failed to listen: %v", err))
+ }
+ if framer == nil {
+ framer = jsonrpc2.NewHeaderStream
+ }
+ go jsonrpc2.Serve(ctx, ln, server, 0)
+ return &TCPServer{Addr: ln.Addr().String(), ln: ln, framer: framer, connList: &connList{}}
+}
+
+// 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))
+ }
+ conn := jsonrpc2.NewConn(s.framer(netConn))
+ s.add(conn)
+ return conn
+}
+
+// PipeServer is a test server that handles connections over io.Pipes.
+type PipeServer struct {
+ *connList
+ server jsonrpc2.StreamServer
+ framer jsonrpc2.Framer
+}
+
+// NewPipeServer returns a test server that can be connected to via io.Pipes.
+func NewPipeServer(ctx context.Context, server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *PipeServer {
+ if framer == nil {
+ framer = jsonrpc2.NewRawStream
+ }
+ return &PipeServer{server: server, framer: framer, connList: &connList{}}
+}
+
+// Connect creates new io.Pipes and binds them to the underlying StreamServer.
+func (s *PipeServer) Connect(ctx context.Context) jsonrpc2.Conn {
+ sPipe, cPipe := net.Pipe()
+ serverStream := s.framer(sPipe)
+ serverConn := jsonrpc2.NewConn(serverStream)
+ s.add(serverConn)
+ go s.server.ServeStream(ctx, serverConn)
+
+ clientStream := s.framer(cPipe)
+ clientConn := jsonrpc2.NewConn(clientStream)
+ s.add(clientConn)
+ return clientConn
+}
+
+// connList 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 connList struct {
+ mu sync.Mutex
+ conns []jsonrpc2.Conn
+}
+
+func (l *connList) add(conn jsonrpc2.Conn) {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.conns = append(l.conns, conn)
+}
+
+func (l *connList) Close() error {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ var errmsgs []string
+ for _, conn := range l.conns {
+ if err := conn.Close(); err != nil {
+ errmsgs = append(errmsgs, err.Error())
+ }
+ }
+ if len(errmsgs) > 0 {
+ return fmt.Errorf("closing errors:\n%s", strings.Join(errmsgs, "\n"))
+ }
+ return nil
+}