package lsp import ( "bufio" "encoding/json" "io" "net" "sync" "testing" "time" ) // message is a JSON-RPC frame as the fake server writes and reads it. // // It is the server's own struct rather than the client's, deliberately. This // file plays the *peer*: a peer that shared the client's types could not catch // the client encoding a field wrongly, because both sides would be wrong in // the same way. type message struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id,omitempty"` Method string `json:"method,omitempty"` Params json.RawMessage `json:"params,omitempty"` Result json.RawMessage `json:"result,omitempty"` Error *ResponseError `json:"error,omitempty"` } // fakeServer is a language server living in this process, at the far end of an // in-memory pipe. // // Testing the client against it exercises the real framing, the real // concurrency and the real decoding, with no gopls to install, no subprocess // to reap and no timing to get lucky with. type fakeServer struct { t *testing.T stream net.Conn reader *bufio.Reader replies chan message // answers the client sent to our own requests mu sync.Mutex received []string // the methods the client has sent, in order // handle answers a request. Returning a nil result and a nil error sends // a null result, which is a perfectly ordinary answer. handle func(method string, params json.RawMessage) (any, *ResponseError) } // newFakeServer returns a client wired to a fake server, both already running. func newFakeServer(t *testing.T) (*Client, *fakeServer) { t.Helper() clientSide, serverSide := net.Pipe() server := &fakeServer{ t: t, stream: serverSide, reader: bufio.NewReader(serverSide), replies: make(chan message, 4), } server.handle = func(string, json.RawMessage) (any, *ResponseError) { return nil, nil } client := NewClient(clientSide, t.TempDir(), "Turbo Test") go client.Run() //nolint:errcheck // the test observes failures through the client go server.serve() t.Cleanup(func() { clientSide.Close() serverSide.Close() }) return client, server } // serve answers messages until the pipe is closed. func (s *fakeServer) serve() { for { body, err := ReadMessage(s.reader) if err != nil { return } var msg message if err := json.Unmarshal(body, &msg); err != nil { return } s.record(msg.Method) switch { case msg.Method == "": // An answer to something we asked. Only this goroutine ever reads // the stream, so the waiting test is handed the message instead of // reading it for itself. s.replies <- msg case len(msg.ID) == 0: // A notification: nothing to answer. default: if err := s.reply(msg); err != nil { return } } } } // reply answers one request through the test's handler. func (s *fakeServer) reply(msg message) error { result, responseErr := s.handler()(msg.Method, msg.Params) reply := message{JSONRPC: "2.0", ID: msg.ID, Error: responseErr} if responseErr == nil { encoded, err := json.Marshal(result) if err != nil { return err } reply.Result = encoded } body, err := json.Marshal(reply) if err != nil { return err } return WriteMessage(s.stream, body) } // handler returns the current handler under the lock. func (s *fakeServer) handler() func(string, json.RawMessage) (any, *ResponseError) { s.mu.Lock() defer s.mu.Unlock() return s.handle } // setHandler replaces the request handler. func (s *fakeServer) setHandler(h func(string, json.RawMessage) (any, *ResponseError)) { s.mu.Lock() defer s.mu.Unlock() s.handle = h } // record notes a method the client sent. func (s *fakeServer) record(method string) { if method == "" { return } s.mu.Lock() defer s.mu.Unlock() s.received = append(s.received, method) } // methods returns everything the client has sent so far. func (s *fakeServer) methods() []string { s.mu.Lock() defer s.mu.Unlock() return append([]string(nil), s.received...) } // notify pushes a notification at the client. func (s *fakeServer) notify(method string, params any) { encoded, err := json.Marshal(params) if err != nil { s.t.Errorf("encoding the notification: %v", err) return } body, err := json.Marshal(message{JSONRPC: "2.0", Method: method, Params: encoded}) if err != nil { s.t.Errorf("encoding the message: %v", err) return } if err := WriteMessage(s.stream, body); err != nil && err != io.ErrClosedPipe { s.t.Errorf("writing the notification: %v", err) } } // request asks the client something, the way gopls asks for configuration, and // returns the client's answer. func (s *fakeServer) request(method string, params any) (json.RawMessage, *ResponseError) { encodedParams, err := json.Marshal(params) if err != nil { s.t.Fatalf("encoding the parameters: %v", err) } body, err := json.Marshal(message{ JSONRPC: "2.0", ID: json.RawMessage(`9001`), Method: method, Params: encodedParams, }) if err != nil { s.t.Fatalf("encoding the request: %v", err) } if err := WriteMessage(s.stream, body); err != nil { s.t.Fatalf("writing the request: %v", err) } select { case reply := <-s.replies: return reply.Result, reply.Error case <-time.After(2 * time.Second): s.t.Fatalf("the client never answered %s", method) return nil, nil } }