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.

lsp_test.go · 755 lines · 22.7 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 16h ago1package lsp
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "os"
12 "path/filepath"
13 "strings"
14 "testing"
15 "time"
16
📦 Turbo Core f3ade8d k33g 8h ago17 "rickub.com/turbo-editors/turbo-core/profile"
🛟 Updated. 28d5985 k33g 16h ago18)
19
20func TestFramingRoundTrip(t *testing.T) {
21 var out bytes.Buffer
22 body := []byte(`{"jsonrpc":"2.0","method":"hello"}`)
23
24 if err := WriteMessage(&out, body); err != nil {
25 t.Fatalf("WriteMessage() error = %v", err)
26 }
27
28 wantHeader := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(body))
29 if !strings.HasPrefix(out.String(), wantHeader) {
30 t.Errorf("the frame starts with %q, want %q", out.String()[:len(wantHeader)], wantHeader)
31 }
32
33 got, err := ReadMessage(bufio.NewReader(&out))
34 if err != nil {
35 t.Fatalf("ReadMessage() error = %v", err)
36 }
37 if string(got) != string(body) {
38 t.Errorf("ReadMessage() = %q, want %q", got, body)
39 }
40}
41
42func TestReadMessageReadsSeveralFramesInARow(t *testing.T) {
43 var out bytes.Buffer
44 for _, body := range []string{`{"a":1}`, `{"b":2}`, `{"c":3}`} {
45 if err := WriteMessage(&out, []byte(body)); err != nil {
46 t.Fatalf("WriteMessage() error = %v", err)
47 }
48 }
49
50 reader := bufio.NewReader(&out)
51 for _, want := range []string{`{"a":1}`, `{"b":2}`, `{"c":3}`} {
52 got, err := ReadMessage(reader)
53 if err != nil {
54 t.Fatalf("ReadMessage() error = %v", err)
55 }
56 if string(got) != want {
57 t.Errorf("ReadMessage() = %q, want %q", got, want)
58 }
59 }
60
61 if _, err := ReadMessage(reader); !errors.Is(err, io.EOF) {
62 t.Errorf("ReadMessage() at the end = %v, want io.EOF", err)
63 }
64}
65
66func TestReadMessageRejectsBadFrames(t *testing.T) {
67 tests := []struct {
68 name string
69 give string
70 }{
71 {"no Content-Length", "X-Other: 1\r\n\r\n{}"},
72 {"a length that is not a number", "Content-Length: many\r\n\r\n{}"},
73 {"a negative length", "Content-Length: -5\r\n\r\n{}"},
74 {"a body shorter than announced", "Content-Length: 100\r\n\r\n{}"},
75 }
76
77 for _, tc := range tests {
78 t.Run(tc.name, func(t *testing.T) {
79 _, err := ReadMessage(bufio.NewReader(strings.NewReader(tc.give)))
80 if err == nil {
81 t.Fatal("ReadMessage() error = nil, want a failure")
82 }
83 })
84 }
85}
86
87func TestReadMessageRefusesAnAbsurdLength(t *testing.T) {
88 frame := "Content-Length: 999999999999\r\n\r\n"
89
90 _, err := ReadMessage(bufio.NewReader(strings.NewReader(frame)))
91
92 if !errors.Is(err, ErrMessageTooLarge) {
93 t.Errorf("ReadMessage() error = %v, want ErrMessageTooLarge", err)
94 }
95}
96
97func TestInitializeSendsTheHandshakeAndMarksTheClientReady(t *testing.T) {
98 client, server := newFakeServer(t)
99
100 if client.Ready() {
101 t.Fatal("Ready() = true before the handshake")
102 }
103 if err := client.Initialize(t.Context()); err != nil {
104 t.Fatalf("Initialize() error = %v", err)
105 }
106
107 if !client.Ready() {
108 t.Error("Ready() = false after a successful handshake")
109 }
110 waitForMethod(t, server, "initialized")
111 if got := server.methods(); got[0] != "initialize" || got[1] != "initialized" {
112 t.Errorf("the server saw %v, want initialize then initialized", got)
113 }
114}
115
116func TestRequestsBeforeInitializationAreRefused(t *testing.T) {
117 client, _ := newFakeServer(t)
118
119 if _, err := client.Complete(t.Context(), "main.go", 0, 0, ""); !errors.Is(err, ErrNotReady) {
120 t.Errorf("Complete() error = %v, want ErrNotReady", err)
121 }
122 if _, err := client.Hover(t.Context(), "main.go", 0, 0, ""); !errors.Is(err, ErrNotReady) {
123 t.Errorf("Hover() error = %v, want ErrNotReady", err)
124 }
125 if _, err := client.Definition(t.Context(), "main.go", 0, 0, ""); !errors.Is(err, ErrNotReady) {
126 t.Errorf("Definition() error = %v, want ErrNotReady", err)
127 }
128}
129
130func TestCompleteReadsAListObject(t *testing.T) {
131 client, server := newFakeServer(t)
132 server.setHandler(func(method string, _ json.RawMessage) (any, *ResponseError) {
133 if method != "textDocument/completion" {
134 return nil, nil
135 }
136 return CompletionList{Items: []CompletionItem{
137 {Label: "Println", Kind: KindFunction, Detail: "func(a ...any)"},
138 {Label: "Printf", Kind: KindFunction},
139 }}, nil
140 })
141 mustInitialize(t, client)
142
143 items, err := client.Complete(t.Context(), "main.go", 3, 4, "fmt.")
144 if err != nil {
145 t.Fatalf("Complete() error = %v", err)
146 }
147
148 if len(items) != 2 {
149 t.Fatalf("Complete() returned %d items, want 2", len(items))
150 }
151 if items[0].Label != "Println" || items[0].Kind != KindFunction {
152 t.Errorf("the first item is %+v", items[0])
153 }
154}
155
156func TestCompleteReadsABareArrayToo(t *testing.T) {
157 client, server := newFakeServer(t)
158 server.setHandler(func(string, json.RawMessage) (any, *ResponseError) {
159 return []CompletionItem{{Label: "len"}}, nil
160 })
161 mustInitialize(t, client)
162
163 items, err := client.Complete(t.Context(), "main.go", 0, 0, "")
164 if err != nil {
165 t.Fatalf("Complete() error = %v", err)
166 }
167 if len(items) != 1 || items[0].Label != "len" {
168 t.Errorf("Complete() returned %+v, want one item labelled len", items)
169 }
170}
171
172func TestCompleteSendsUTF16Columns(t *testing.T) {
173 client, server := newFakeServer(t)
174 var seen Position
175 server.setHandler(func(method string, params json.RawMessage) (any, *ResponseError) {
176 if method == "textDocument/completion" {
177 var p TextDocumentPositionParams
178 json.Unmarshal(params, &p) //nolint:errcheck // a bad decode shows up as a zero position
179 seen = p.Position
180 }
181 return CompletionList{}, nil
182 })
183 mustInitialize(t, client)
184
185 // Four runes precede the cursor, but the clef needs a surrogate pair, so
186 // the protocol column is five.
187 if _, err := client.Complete(t.Context(), "main.go", 2, 4, "a𝄞bc"); err != nil {
188 t.Fatalf("Complete() error = %v", err)
189 }
190
191 if seen.Line != 2 || seen.Character != 5 {
192 t.Errorf("the server saw %+v, want {Line:2 Character:5}", seen)
193 }
194}
195
196func TestAServerErrorReachesTheCaller(t *testing.T) {
197 client, server := newFakeServer(t)
198 server.setHandler(func(method string, _ json.RawMessage) (any, *ResponseError) {
199 if method != "textDocument/completion" {
200 return nil, nil // the handshake must still succeed
201 }
202 return nil, &ResponseError{Code: -32000, Message: "no package for this file"}
203 })
204 mustInitialize(t, client)
205
206 _, err := client.Complete(t.Context(), "main.go", 0, 0, "")
207
208 var responseErr *ResponseError
209 if !errors.As(err, &responseErr) {
210 t.Fatalf("Complete() error = %v, want a *ResponseError", err)
211 }
212 if !strings.Contains(responseErr.Error(), "no package for this file") {
213 t.Errorf("the error reads %q, want the server's message in it", responseErr.Error())
214 }
215}
216
217func TestARequestGivesUpWhenItsContextDoes(t *testing.T) {
218 client, server := newFakeServer(t)
219 block := make(chan struct{})
220 t.Cleanup(func() { close(block) })
221 server.setHandler(func(method string, _ json.RawMessage) (any, *ResponseError) {
222 if method == "textDocument/completion" {
223 <-block // never answer this one
224 }
225 return CompletionList{}, nil
226 })
227 mustInitialize(t, client)
228
229 ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
230 defer cancel()
231
232 _, err := client.Complete(ctx, "main.go", 0, 0, "")
233
234 if !errors.Is(err, context.DeadlineExceeded) {
235 t.Errorf("Complete() error = %v, want the deadline to have been reached", err)
236 }
237}
238
239func TestHover(t *testing.T) {
240 client, server := newFakeServer(t)
241 server.setHandler(func(string, json.RawMessage) (any, *ResponseError) {
242 return Hover{Contents: MarkupContent{Kind: "plaintext", Value: "func len(v Type) int"}}, nil
243 })
244 mustInitialize(t, client)
245
246 got, err := client.Hover(t.Context(), "main.go", 0, 0, "len")
247 if err != nil {
248 t.Fatalf("Hover() error = %v", err)
249 }
250 if got != "func len(v Type) int" {
251 t.Errorf("Hover() = %q", got)
252 }
253}
254
255func TestHoverWithNothingToSay(t *testing.T) {
256 client, server := newFakeServer(t)
257 server.setHandler(func(string, json.RawMessage) (any, *ResponseError) { return nil, nil })
258 mustInitialize(t, client)
259
260 got, err := client.Hover(t.Context(), "main.go", 0, 0, "")
261 if err != nil {
262 t.Fatalf("Hover() error = %v", err)
263 }
264 if got != "" {
265 t.Errorf("Hover() = %q, want empty", got)
266 }
267}
268
269func TestDefinitionReadsBothShapes(t *testing.T) {
270 location := Location{URI: PathToURI("/tmp/other.go"), Range: Range{Start: Position{Line: 4}}}
271
272 tests := []struct {
273 name string
274 reply any
275 want int
276 }{
277 {"a single location", location, 1},
278 {"an array of locations", []Location{location, location}, 2},
279 {"nothing at all", nil, 0},
280 }
281
282 for _, tc := range tests {
283 t.Run(tc.name, func(t *testing.T) {
284 client, server := newFakeServer(t)
285 server.setHandler(func(string, json.RawMessage) (any, *ResponseError) { return tc.reply, nil })
286 mustInitialize(t, client)
287
288 got, err := client.Definition(t.Context(), "main.go", 0, 0, "")
289 if err != nil {
290 t.Fatalf("Definition() error = %v", err)
291 }
292 if len(got) != tc.want {
293 t.Errorf("Definition() returned %d locations, want %d", len(got), tc.want)
294 }
295 })
296 }
297}
298
299func TestDiagnosticsReachTheEditor(t *testing.T) {
300 client, server := newFakeServer(t)
301 received := make(chan []Diagnostic, 1)
302 var receivedPath string
303 client.OnDiagnostics = func(path string, diagnostics []Diagnostic) {
304 receivedPath = path
305 received <- diagnostics
306 }
307 mustInitialize(t, client)
308
309 path := filepath.Join(t.TempDir(), "main.go")
310 server.notify("textDocument/publishDiagnostics", PublishDiagnosticsParams{
311 URI: PathToURI(path),
312 Diagnostics: []Diagnostic{
313 {Message: "undefined: foo", Severity: SeverityError, Range: Range{Start: Position{Line: 3}}},
314 },
315 })
316
317 select {
318 case diagnostics := <-received:
319 if len(diagnostics) != 1 || diagnostics[0].Message != "undefined: foo" {
320 t.Errorf("the diagnostics are %+v", diagnostics)
321 }
322 if receivedPath != path {
323 t.Errorf("the path is %q, want %q — the URI must be converted back", receivedPath, path)
324 }
325 case <-time.After(time.Second):
326 t.Fatal("no diagnostics arrived")
327 }
328}
329
330func TestServerMessagesAreLogged(t *testing.T) {
331 client, server := newFakeServer(t)
332 logged := make(chan string, 1)
333 client.OnLog = func(message string) { logged <- message }
334 mustInitialize(t, client)
335
336 server.notify("window/showMessage", map[string]any{"type": 3, "message": "gopls is indexing"})
337
338 select {
339 case message := <-logged:
340 if message != "gopls is indexing" {
341 t.Errorf("the logged message is %q", message)
342 }
343 case <-time.After(time.Second):
344 t.Fatal("no message arrived")
345 }
346}
347
348func TestTheClientAnswersAConfigurationRequest(t *testing.T) {
349 client, server := newFakeServer(t)
350 mustInitialize(t, client)
351
352 result, responseErr := server.request("workspace/configuration", map[string]any{
353 "items": []map[string]string{{"section": "gopls"}, {"section": "gopls"}},
354 })
355
356 if responseErr != nil {
357 t.Fatalf("the client answered with an error: %v", responseErr)
358 }
359 var settings []map[string]any
360 if err := json.Unmarshal(result, &settings); err != nil {
361 t.Fatalf("the answer is not a settings array: %v", err)
362 }
363 if len(settings) != 2 {
364 t.Errorf("the client sent %d settings objects, want one per item asked about", len(settings))
365 }
366}
367
368func TestTheClientRefusesARequestItDoesNotKnow(t *testing.T) {
369 client, server := newFakeServer(t)
370 mustInitialize(t, client)
371
372 _, responseErr := server.request("workspace/applyEdit", map[string]any{})
373
374 if responseErr == nil {
375 t.Fatal("the client accepted a request it cannot serve")
376 }
377 if responseErr.Code != CodeMethodNotFound {
378 t.Errorf("the error code is %d, want %d", responseErr.Code, CodeMethodNotFound)
379 }
380}
381
382func TestDocumentSynchronisationSendsTheRightNotifications(t *testing.T) {
383 client, server := newFakeServer(t)
384 mustInitialize(t, client)
385
386 if err := client.DidOpen("main.go", "package main"); err != nil {
387 t.Fatalf("DidOpen() error = %v", err)
388 }
389 if err := client.DidChange("main.go", "package main\n"); err != nil {
390 t.Fatalf("DidChange() error = %v", err)
391 }
392 if err := client.DidSave("main.go", "package main\n"); err != nil {
393 t.Fatalf("DidSave() error = %v", err)
394 }
395 if err := client.DidClose("main.go"); err != nil {
396 t.Fatalf("DidClose() error = %v", err)
397 }
398
399 waitForMethod(t, server, "textDocument/didClose")
400 want := []string{
401 "textDocument/didOpen", "textDocument/didChange",
402 "textDocument/didSave", "textDocument/didClose",
403 }
404 got := server.methods()[2:] // after initialize and initialized
405 for i, method := range want {
406 if got[i] != method {
407 t.Errorf("notification %d was %q, want %q", i, got[i], method)
408 }
409 }
410}
411
412func TestVersionsGoUpWithEachChange(t *testing.T) {
413 client, server := newFakeServer(t)
414 versions := make(chan int, 4)
415 server.setHandler(func(string, json.RawMessage) (any, *ResponseError) { return nil, nil })
416 mustInitialize(t, client)
417
418 // The fake server records methods, not parameters, so the versions are
419 // read back from the client's own bookkeeping instead.
420 client.DidOpen("main.go", "a") //nolint:errcheck // checked below
421 client.DidChange("main.go", "b") //nolint:errcheck
422 client.DidChange("main.go", "c") //nolint:errcheck
423
424 client.mu.Lock()
425 versions <- client.versions["main.go"]
426 client.mu.Unlock()
427
428 if got := <-versions; got != 3 {
429 t.Errorf("the document is at version %d, want 3", got)
430 }
431}
432
433func TestRequestsAfterTheConnectionClosesFail(t *testing.T) {
434 client, _ := newFakeServer(t)
435 mustInitialize(t, client)
436
437 if err := client.conn.Close(); err != nil {
438 t.Fatalf("Close() error = %v", err)
439 }
440
441 if _, err := client.Complete(t.Context(), "main.go", 0, 0, ""); err == nil {
442 t.Error("a request on a closed connection succeeded")
443 }
444}
445
446func TestPathAndURIRoundTrip(t *testing.T) {
447 path := filepath.Join(t.TempDir(), "sub dir", "main.go")
448
449 uri := PathToURI(path)
450
451 if !strings.HasPrefix(uri, "file://") {
452 t.Errorf("PathToURI() = %q, want a file URI", uri)
453 }
454 if strings.Contains(uri, " ") {
455 t.Errorf("PathToURI() = %q, want the space percent-encoded", uri)
456 }
457 if got := URIToPath(uri); got != path {
458 t.Errorf("URIToPath(PathToURI(%q)) = %q", path, got)
459 }
460}
461
462func TestURIToPathLeavesOtherSchemesAlone(t *testing.T) {
463 if got := URIToPath("https://example.com/x"); got != "https://example.com/x" {
464 t.Errorf("URIToPath() = %q, want it unchanged", got)
465 }
466}
467
468func TestUTF16Conversion(t *testing.T) {
469 tests := []struct {
470 name string
471 line string
472 rune_ int
473 utf16 int
474 }{
475 {"ascii", "hello", 3, 3},
476 {"an accent is one unit", "héllo", 3, 3},
477 {"beyond the BMP needs two", "a𝄞bc", 2, 3},
478 {"the start of a line", "anything", 0, 0},
479 {"the end of a line", "a𝄞", 2, 3},
480 }
481
482 for _, tc := range tests {
483 t.Run(tc.name, func(t *testing.T) {
484 if got := RuneToUTF16(tc.line, tc.rune_); got != tc.utf16 {
485 t.Errorf("RuneToUTF16(%q, %d) = %d, want %d", tc.line, tc.rune_, got, tc.utf16)
486 }
487 if got := UTF16ToRune(tc.line, tc.utf16); got != tc.rune_ {
488 t.Errorf("UTF16ToRune(%q, %d) = %d, want %d", tc.line, tc.utf16, got, tc.rune_)
489 }
490 })
491 }
492}
493
494func TestUTF16ConversionClampsPastTheEnd(t *testing.T) {
495 if got := RuneToUTF16("ab", 99); got != 2 {
496 t.Errorf("RuneToUTF16() = %d, want the line's length", got)
497 }
498 if got := UTF16ToRune("ab", 99); got != 2 {
499 t.Errorf("UTF16ToRune() = %d, want the line's length", got)
500 }
501}
502
503func TestCompletionInsertion(t *testing.T) {
504 tests := []struct {
505 name string
506 item CompletionItem
507 want string
508 }{
509 {"the label when there is no insert text", CompletionItem{Label: "Println"}, "Println"},
510 {"the insert text when there is one", CompletionItem{Label: "Println", InsertText: "Println()"}, "Println()"},
511 {"a snippet's placeholders removed", CompletionItem{InsertText: "Printf(${1:format}, ${2:a})"}, "Printf(format, a)"},
512 {"a bare tab stop", CompletionItem{InsertText: "if err != nil {$0}"}, "if err != nil {}"},
513 {"an empty placeholder", CompletionItem{InsertText: "f(${1})"}, "f()"},
514 {"an unterminated placeholder", CompletionItem{InsertText: "f(${1:x"}, "f("},
515 }
516
517 for _, tc := range tests {
518 t.Run(tc.name, func(t *testing.T) {
519 if got := tc.item.Insertion(); got != tc.want {
520 t.Errorf("Insertion() = %q, want %q", got, tc.want)
521 }
522 })
523 }
524}
525
526func TestCompletionKindNames(t *testing.T) {
527 if got := KindFunction.String(); got != "func" {
528 t.Errorf("KindFunction.String() = %q", got)
529 }
530 if got := CompletionItemKind(999).String(); got != "?" {
531 t.Errorf("an unknown kind prints %q, want %q", got, "?")
532 }
533}
534
535func TestFindServerReportsWhenThereIsNone(t *testing.T) {
536 t.Setenv("PATH", t.TempDir())
537 server := profile.Server{Command: "no-such-language-server", Dirs: []string{t.TempDir()}}
538
539 _, err := FindServer(server)
540
541 if !errors.Is(err, ErrServerNotFound) {
542 t.Errorf("FindServer() error = %v, want ErrServerNotFound", err)
543 }
544}
545
546func TestFindServerSaysWhereItLooked(t *testing.T) {
547 // "not found" on its own tells nobody what to do about it; the message has
548 // to name the executable and the directories that were searched.
549 dir := t.TempDir()
550 t.Setenv("PATH", t.TempDir())
551
552 _, err := FindServer(profile.Server{Command: "rust-analyzer", Dirs: []string{dir}})
553
554 if err == nil {
555 t.Fatal("FindServer() error = nil, want a failure")
556 }
557 for _, want := range []string{"rust-analyzer", "PATH", dir} {
558 if !strings.Contains(err.Error(), want) {
559 t.Errorf("FindServer() error = %q, want it to mention %q", err, want)
560 }
561 }
562}
563
564func TestFindServerFindsTheServerInOneOfTheProfilesDirectories(t *testing.T) {
565 // The whole point of Dirs: cargo and go install put a language server
566 // somewhere that is very often not on PATH.
567 dir := t.TempDir()
568 path := filepath.Join(dir, "pretend-analyzer")
569 if err := os.WriteFile(path, []byte("#!/bin/sh\n"), 0o755); err != nil {
570 t.Fatalf("writing a stand-in server: %v", err)
571 }
572 t.Setenv("PATH", t.TempDir())
573
574 found, err := FindServer(profile.Server{Command: "pretend-analyzer", Dirs: []string{"", dir}})
575
576 if err != nil {
577 t.Fatalf("FindServer() error = %v", err)
578 }
579 if found != path {
580 t.Errorf("FindServer() = %q, want %q", found, path)
581 }
582}
583
584// mustInitialize performs the handshake, failing the test if it does not work.
585func mustInitialize(t *testing.T, client *Client) {
586 t.Helper()
587 if err := client.Initialize(t.Context()); err != nil {
588 t.Fatalf("Initialize() error = %v", err)
589 }
590}
591
592// waitForMethod blocks until the fake server has seen a method, so a test
593// never races the notification it is about to assert on.
594func waitForMethod(t *testing.T, server *fakeServer, method string) {
595 t.Helper()
596
597 deadline := time.After(2 * time.Second)
598 for {
599 for _, seen := range server.methods() {
600 if seen == method {
601 return
602 }
603 }
604 select {
605 case <-deadline:
606 t.Fatalf("the server never saw %q; it saw %v", method, server.methods())
607 case <-time.After(time.Millisecond):
608 }
609 }
610}
611
612// locationAnswerers are the four requests that answer with places in the code.
613// They share a decoder, so they share their tests.
614var locationAnswerers = []struct {
615 method string
616 ask func(*Client) ([]Location, error)
617}{
618 {"textDocument/definition", func(c *Client) ([]Location, error) {
619 return c.Definition(context.Background(), "main.go", 3, 4, "x := y")
620 }},
621 {"textDocument/typeDefinition", func(c *Client) ([]Location, error) {
622 return c.TypeDefinition(context.Background(), "main.go", 3, 4, "x := y")
623 }},
624 {"textDocument/implementation", func(c *Client) ([]Location, error) {
625 return c.Implementation(context.Background(), "main.go", 3, 4, "x := y")
626 }},
627 {"textDocument/references", func(c *Client) ([]Location, error) {
628 return c.References(context.Background(), "main.go", 3, 4, "x := y", true)
629 }},
630}
631
632func TestEveryLocationRequestSendsItsOwnMethod(t *testing.T) {
633 // One helper serves all four, so the one thing that can go wrong is a
634 // request going out under the wrong name — which a server answers with an
635 // error the caller reads as "nothing found".
636 for _, request := range locationAnswerers {
637 t.Run(request.method, func(t *testing.T) {
638 client, server := newFakeServer(t)
639 server.setHandler(func(method string, _ json.RawMessage) (any, *ResponseError) {
640 if method != request.method {
641 return nil, nil
642 }
643 return []Location{{URI: PathToURI("/tmp/other.go")}}, nil
644 })
645 mustInitialize(t, client)
646
647 locations, err := request.ask(client)
648 if err != nil {
649 t.Fatalf("error = %v", err)
650 }
651 if len(locations) != 1 {
652 t.Errorf("got %d locations, want the one the server answered to %s", len(locations), request.method)
653 }
654 })
655 }
656}
657
658func TestEveryLocationRequestReadsBothShapes(t *testing.T) {
659 // A server may answer one location as a bare object rather than an array
660 // of one. gopls does it; the specification allows it.
661 location := Location{URI: PathToURI("/tmp/other.go"), Range: Range{Start: Position{Line: 4}}}
662
663 for _, request := range locationAnswerers {
664 for _, shape := range []struct {
665 name string
666 reply any
667 want int
668 }{
669 {"an array", []Location{location, location}, 2},
670 {"a bare object", location, 1},
671 {"null", nil, 0},
672 } {
673 t.Run(request.method+"/"+shape.name, func(t *testing.T) {
674 client, server := newFakeServer(t)
675 server.setHandler(func(method string, _ json.RawMessage) (any, *ResponseError) {
676 if method != request.method {
677 return nil, nil
678 }
679 return shape.reply, nil
680 })
681 mustInitialize(t, client)
682
683 locations, err := request.ask(client)
684 if err != nil {
685 t.Fatalf("error = %v", err)
686 }
687 if len(locations) != shape.want {
688 t.Errorf("got %d locations, want %d", len(locations), shape.want)
689 }
690 })
691 }
692 }
693}
694
695func TestEveryLocationRequestRefusesBeforeTheServerIsReady(t *testing.T) {
696 for _, request := range locationAnswerers {
697 t.Run(request.method, func(t *testing.T) {
698 client, _ := newFakeServer(t)
699
700 if _, err := request.ask(client); !errors.Is(err, ErrNotReady) {
701 t.Errorf("error = %v, want ErrNotReady", err)
702 }
703 })
704 }
705}
706
707func TestReferencesSaysWhetherItWantsTheDeclaration(t *testing.T) {
708 // The one parameter that is not shared. A server that never sees it
709 // applies its own default, and the caller's choice is silently lost.
710 for _, want := range []bool{true, false} {
711 t.Run(fmt.Sprint(want), func(t *testing.T) {
712 client, server := newFakeServer(t)
713 var sent json.RawMessage
714 server.setHandler(func(method string, params json.RawMessage) (any, *ResponseError) {
715 if method == "textDocument/references" {
716 sent = params
717 }
718 return []Location{}, nil
719 })
720 mustInitialize(t, client)
721
722 if _, err := client.References(t.Context(), "main.go", 1, 0, "", want); err != nil {
723 t.Fatalf("References() error = %v", err)
724 }
725
726 var got struct {
727 Context struct {
728 IncludeDeclaration bool `json:"includeDeclaration"`
729 } `json:"context"`
730 }
731 if err := json.Unmarshal(sent, &got); err != nil {
732 t.Fatalf("the parameters do not parse: %v\n%s", err, sent)
733 }
734 if got.Context.IncludeDeclaration != want {
735 t.Errorf("includeDeclaration = %v, want %v — sent %s", got.Context.IncludeDeclaration, want, sent)
736 }
737 })
738 }
739}
740
741func TestTheClientAsksForTheCapabilitiesItUses(t *testing.T) {
742 // Claiming nothing is as wrong as claiming too much: a server may decline
743 // to answer a request the client never said it could use.
744 capabilities := clientCapabilities()
745 document, ok := capabilities["textDocument"].(map[string]any)
746 if !ok {
747 t.Fatal("no textDocument capabilities at all")
748 }
749
750 for _, want := range []string{"completion", "hover", "references", "implementation", "typeDefinition", "publishDiagnostics"} {
751 if _, declared := document[want]; !declared {
752 t.Errorf("the client never declares %q, but sends it", want)
753 }
754 }
755}