package app import ( "bufio" "encoding/json" "net" "sync" "testing" "time" "rickub.com/turbo-editors/turbo-core/lsp" ) // fakeLSP is a language server living in this process, at the far end of an // in-memory pipe. // // The `lsp` package has one of these too, for testing the client. This one // exists to test what the *editor* says to a server, which is a different // question and belongs here. type fakeLSP struct { stream net.Conn reader *bufio.Reader mu sync.Mutex seen map[string]int answers map[string]json.RawMessage } // frame is one JSON-RPC message, reduced to what this fake needs to look at. type frame struct { JSONRPC string `json:"jsonrpc"` ID json.RawMessage `json:"id,omitempty"` Method string `json:"method,omitempty"` Result json.RawMessage `json:"result,omitempty"` } // newFakeLanguage returns an initialised client wired to a fake server. The // client is not attached to the app yet — connectLanguage does that, so a test // can check what happens on either side of the moment it becomes ready. func newFakeLanguage(t *testing.T, _ *App) (*lsp.Client, *fakeLSP) { t.Helper() clientSide, serverSide := net.Pipe() server := &fakeLSP{ stream: serverSide, reader: bufio.NewReader(serverSide), seen: map[string]int{}, answers: map[string]json.RawMessage{}, } client := lsp.NewClient(clientSide, t.TempDir(), "Turbo Test") go client.Run() //nolint:errcheck // failures surface through the client go server.serve() t.Cleanup(func() { clientSide.Close() serverSide.Close() }) if err := client.Initialize(t.Context()); err != nil { t.Fatalf("Initialize() error = %v", err) } return client, server } // connectLanguage makes the app talk to a client, as a successful start-up // would. func connectLanguage(a *App, client *lsp.Client) { a.language.attach(client) } // serve answers every request with null and records every method it sees. func (s *fakeLSP) serve() { for { body, err := lsp.ReadMessage(s.reader) if err != nil { return } var msg frame if err := json.Unmarshal(body, &msg); err != nil { return } s.record(msg.Method) if len(msg.ID) == 0 || msg.Method == "" { continue // a notification, or an answer to something we asked } reply, err := json.Marshal(frame{JSONRPC: "2.0", ID: msg.ID, Result: s.answerTo(msg.Method)}) if err != nil { return } if err := lsp.WriteMessage(s.stream, reply); err != nil { return } } } // answerTo returns what this server has been told to answer a method with, // and null when it has been told nothing — which is what every request got // before results could be set, and what most tests still want. func (s *fakeLSP) answerTo(method string) json.RawMessage { s.mu.Lock() defer s.mu.Unlock() if answer, set := s.answers[method]; set { return answer } return json.RawMessage("null") } // setAnswer tells the server what to reply to one method. func (s *fakeLSP) setAnswer(t *testing.T, method string, result any) { t.Helper() encoded, err := json.Marshal(result) if err != nil { t.Fatalf("cannot encode the answer for %s: %v", method, err) } s.mu.Lock() defer s.mu.Unlock() s.answers[method] = encoded } // record counts one method. func (s *fakeLSP) record(method string) { if method == "" { return } s.mu.Lock() defer s.mu.Unlock() s.seen[method]++ } // methodCount returns how many times the client has sent a method. func (s *fakeLSP) methodCount(method string) int { s.mu.Lock() defer s.mu.Unlock() return s.seen[method] } // waitForMethod blocks until the server has seen a method, so a test never // races the notification it is about to assert on. func waitForMethod(t *testing.T, server *fakeLSP, method string) { t.Helper() deadline := time.After(2 * time.Second) for { if server.methodCount(method) > 0 { return } select { case <-deadline: t.Fatalf("the server never saw %q", method) case <-time.After(time.Millisecond): } } }