turbo-editors/turbo-corepublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

fakelsp_test.go · 219 lines · 5.8 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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
	order      []string // every method, in the order it arrived
	answers    map[string]json.RawMessage
	lastOpened json.RawMessage // the params of the last textDocument/didOpen
	lastFiles  json.RawMessage // the params of the last workspace/didChangeWatchedFiles
}

// 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"`
	Params  json.RawMessage `json:"params,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 msg.Method == "textDocument/didOpen" {
			s.mu.Lock()
			s.lastOpened = msg.Params
			s.mu.Unlock()
		}
		if msg.Method == "workspace/didChangeWatchedFiles" {
			s.mu.Lock()
			s.lastFiles = msg.Params
			s.mu.Unlock()
		}

		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]++
	s.order = append(s.order, method)
}

// methodsSeen returns every method the client has sent, in order.
func (s *fakeLSP) methodsSeen() []string {
	s.mu.Lock()
	defer s.mu.Unlock()
	return append([]string(nil), s.order...)
}

// lastOpenedURI returns the URI the client announced in its most recent
// textDocument/didOpen — the spelling the server was actually given, which is
// what a server that resolves symbolic links cares about.
func (s *fakeLSP) lastOpenedURI() string {
	waitForMethodQuietly(s, "textDocument/didOpen")
	s.mu.Lock()
	defer s.mu.Unlock()
	var params struct {
		TextDocument struct {
			URI string `json:"uri"`
		} `json:"textDocument"`
	}
	_ = json.Unmarshal(s.lastOpened, &params)
	return params.TextDocument.URI
}

// lastFileEvents returns the changes the client reported in its most recent
// workspace/didChangeWatchedFiles.
func (s *fakeLSP) lastFileEvents() []lsp.FileEvent {
	waitForMethodQuietly(s, "workspace/didChangeWatchedFiles")
	s.mu.Lock()
	defer s.mu.Unlock()
	var params lsp.DidChangeWatchedFilesParams
	_ = json.Unmarshal(s.lastFiles, &params)
	return params.Changes
}

// waitForMethodQuietly is waitForMethod for a caller that reports its own
// failure: it gives up after two seconds and returns.
func waitForMethodQuietly(server *fakeLSP, method string) {
	deadline := time.After(2 * time.Second)
	for server.methodCount(method) == 0 {
		select {
		case <-deadline:
			return
		case <-time.After(time.Millisecond):
		}
	}
}

// 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):
		}
	}
}