turbo-editors/turbo-corepublic Fork 0
v1.0.0
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.

🛟 Updated. 28d5985 · on v1.0.0 · k33g · 16h ago
fakeserver_test.go · 200 lines · 5.2 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
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
	}
}