turbo-editors/turbo-corepublic Fork 0
v1.0.1
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 · 807 lines · 24.8 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 14h 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 6h ago17 "rickub.com/turbo-editors/turbo-core/profile"
🛟 Updated. 28d5985 k33g 14h 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 }
📦 Turbo Core — canonical paths: symbolic links resolved in URIs and document keys (moon-lsp on macOS) b91316e k33g 4h ago322 // Canonical, not as spelt: the URI was built from the canonical path
323 // (macOS's temporary directories are under a symbolic link), and what
324 // comes back is that spelling, which is the one the editor keys by.
325 if receivedPath != CanonicalPath(path) {
326 t.Errorf("the path is %q, want %q — the URI must be converted back", receivedPath, CanonicalPath(path))
🛟 Updated. 28d5985 k33g 14h ago327 }
328 case <-time.After(time.Second):
329 t.Fatal("no diagnostics arrived")
330 }
331}
332
333func TestServerMessagesAreLogged(t *testing.T) {
334 client, server := newFakeServer(t)
335 logged := make(chan string, 1)
336 client.OnLog = func(message string) { logged <- message }
337 mustInitialize(t, client)
338
339 server.notify("window/showMessage", map[string]any{"type": 3, "message": "gopls is indexing"})
340
341 select {
342 case message := <-logged:
343 if message != "gopls is indexing" {
344 t.Errorf("the logged message is %q", message)
345 }
346 case <-time.After(time.Second):
347 t.Fatal("no message arrived")
348 }
349}
350
351func TestTheClientAnswersAConfigurationRequest(t *testing.T) {
352 client, server := newFakeServer(t)
353 mustInitialize(t, client)
354
355 result, responseErr := server.request("workspace/configuration", map[string]any{
356 "items": []map[string]string{{"section": "gopls"}, {"section": "gopls"}},
357 })
358
359 if responseErr != nil {
360 t.Fatalf("the client answered with an error: %v", responseErr)
361 }
362 var settings []map[string]any
363 if err := json.Unmarshal(result, &settings); err != nil {
364 t.Fatalf("the answer is not a settings array: %v", err)
365 }
366 if len(settings) != 2 {
367 t.Errorf("the client sent %d settings objects, want one per item asked about", len(settings))
368 }
369}
370
371func TestTheClientRefusesARequestItDoesNotKnow(t *testing.T) {
372 client, server := newFakeServer(t)
373 mustInitialize(t, client)
374
375 _, responseErr := server.request("workspace/applyEdit", map[string]any{})
376
377 if responseErr == nil {
378 t.Fatal("the client accepted a request it cannot serve")
379 }
380 if responseErr.Code != CodeMethodNotFound {
381 t.Errorf("the error code is %d, want %d", responseErr.Code, CodeMethodNotFound)
382 }
383}
384
385func TestDocumentSynchronisationSendsTheRightNotifications(t *testing.T) {
386 client, server := newFakeServer(t)
387 mustInitialize(t, client)
388
389 if err := client.DidOpen("main.go", "package main"); err != nil {
390 t.Fatalf("DidOpen() error = %v", err)
391 }
392 if err := client.DidChange("main.go", "package main\n"); err != nil {
393 t.Fatalf("DidChange() error = %v", err)
394 }
395 if err := client.DidSave("main.go", "package main\n"); err != nil {
396 t.Fatalf("DidSave() error = %v", err)
397 }
398 if err := client.DidClose("main.go"); err != nil {
399 t.Fatalf("DidClose() error = %v", err)
400 }
401
402 waitForMethod(t, server, "textDocument/didClose")
403 want := []string{
404 "textDocument/didOpen", "textDocument/didChange",
405 "textDocument/didSave", "textDocument/didClose",
406 }
407 got := server.methods()[2:] // after initialize and initialized
408 for i, method := range want {
409 if got[i] != method {
410 t.Errorf("notification %d was %q, want %q", i, got[i], method)
411 }
412 }
413}
414
415func TestVersionsGoUpWithEachChange(t *testing.T) {
416 client, server := newFakeServer(t)
417 versions := make(chan int, 4)
418 server.setHandler(func(string, json.RawMessage) (any, *ResponseError) { return nil, nil })
419 mustInitialize(t, client)
420
421 // The fake server records methods, not parameters, so the versions are
422 // read back from the client's own bookkeeping instead.
423 client.DidOpen("main.go", "a") //nolint:errcheck // checked below
424 client.DidChange("main.go", "b") //nolint:errcheck
425 client.DidChange("main.go", "c") //nolint:errcheck
426
427 client.mu.Lock()
428 versions <- client.versions["main.go"]
429 client.mu.Unlock()
430
431 if got := <-versions; got != 3 {
432 t.Errorf("the document is at version %d, want 3", got)
433 }
434}
435
436func TestRequestsAfterTheConnectionClosesFail(t *testing.T) {
437 client, _ := newFakeServer(t)
438 mustInitialize(t, client)
439
440 if err := client.conn.Close(); err != nil {
441 t.Fatalf("Close() error = %v", err)
442 }
443
444 if _, err := client.Complete(t.Context(), "main.go", 0, 0, ""); err == nil {
445 t.Error("a request on a closed connection succeeded")
446 }
447}
448
449func TestPathAndURIRoundTrip(t *testing.T) {
450 path := filepath.Join(t.TempDir(), "sub dir", "main.go")
451
452 uri := PathToURI(path)
453
454 if !strings.HasPrefix(uri, "file://") {
455 t.Errorf("PathToURI() = %q, want a file URI", uri)
456 }
457 if strings.Contains(uri, " ") {
458 t.Errorf("PathToURI() = %q, want the space percent-encoded", uri)
459 }
📦 Turbo Core — canonical paths: symbolic links resolved in URIs and document keys (moon-lsp on macOS) b91316e k33g 4h ago460 // The round trip lands on the canonical spelling of the path — on macOS a
461 // temporary directory is /var/…, a link to /private/var/…, and the URI
462 // names the latter on purpose.
463 if got, want := URIToPath(uri), CanonicalPath(path); got != want {
464 t.Errorf("URIToPath(PathToURI(%q)) = %q, want %q", path, got, want)
🛟 Updated. 28d5985 k33g 14h ago465 }
466}
467
468func TestURIToPathLeavesOtherSchemesAlone(t *testing.T) {
469 if got := URIToPath("https://example.com/x"); got != "https://example.com/x" {
470 t.Errorf("URIToPath() = %q, want it unchanged", got)
471 }
472}
473
474func TestUTF16Conversion(t *testing.T) {
475 tests := []struct {
476 name string
477 line string
478 rune_ int
479 utf16 int
480 }{
481 {"ascii", "hello", 3, 3},
482 {"an accent is one unit", "héllo", 3, 3},
483 {"beyond the BMP needs two", "a𝄞bc", 2, 3},
484 {"the start of a line", "anything", 0, 0},
485 {"the end of a line", "a𝄞", 2, 3},
486 }
487
488 for _, tc := range tests {
489 t.Run(tc.name, func(t *testing.T) {
490 if got := RuneToUTF16(tc.line, tc.rune_); got != tc.utf16 {
491 t.Errorf("RuneToUTF16(%q, %d) = %d, want %d", tc.line, tc.rune_, got, tc.utf16)
492 }
493 if got := UTF16ToRune(tc.line, tc.utf16); got != tc.rune_ {
494 t.Errorf("UTF16ToRune(%q, %d) = %d, want %d", tc.line, tc.utf16, got, tc.rune_)
495 }
496 })
497 }
498}
499
500func TestUTF16ConversionClampsPastTheEnd(t *testing.T) {
501 if got := RuneToUTF16("ab", 99); got != 2 {
502 t.Errorf("RuneToUTF16() = %d, want the line's length", got)
503 }
504 if got := UTF16ToRune("ab", 99); got != 2 {
505 t.Errorf("UTF16ToRune() = %d, want the line's length", got)
506 }
507}
508
509func TestCompletionInsertion(t *testing.T) {
510 tests := []struct {
511 name string
512 item CompletionItem
513 want string
514 }{
515 {"the label when there is no insert text", CompletionItem{Label: "Println"}, "Println"},
516 {"the insert text when there is one", CompletionItem{Label: "Println", InsertText: "Println()"}, "Println()"},
517 {"a snippet's placeholders removed", CompletionItem{InsertText: "Printf(${1:format}, ${2:a})"}, "Printf(format, a)"},
518 {"a bare tab stop", CompletionItem{InsertText: "if err != nil {$0}"}, "if err != nil {}"},
519 {"an empty placeholder", CompletionItem{InsertText: "f(${1})"}, "f()"},
520 {"an unterminated placeholder", CompletionItem{InsertText: "f(${1:x"}, "f("},
521 }
522
523 for _, tc := range tests {
524 t.Run(tc.name, func(t *testing.T) {
525 if got := tc.item.Insertion(); got != tc.want {
526 t.Errorf("Insertion() = %q, want %q", got, tc.want)
527 }
528 })
529 }
530}
531
532func TestCompletionKindNames(t *testing.T) {
533 if got := KindFunction.String(); got != "func" {
534 t.Errorf("KindFunction.String() = %q", got)
535 }
536 if got := CompletionItemKind(999).String(); got != "?" {
537 t.Errorf("an unknown kind prints %q, want %q", got, "?")
538 }
539}
540
541func TestFindServerReportsWhenThereIsNone(t *testing.T) {
542 t.Setenv("PATH", t.TempDir())
543 server := profile.Server{Command: "no-such-language-server", Dirs: []string{t.TempDir()}}
544
545 _, err := FindServer(server)
546
547 if !errors.Is(err, ErrServerNotFound) {
548 t.Errorf("FindServer() error = %v, want ErrServerNotFound", err)
549 }
550}
551
552func TestFindServerSaysWhereItLooked(t *testing.T) {
553 // "not found" on its own tells nobody what to do about it; the message has
554 // to name the executable and the directories that were searched.
555 dir := t.TempDir()
556 t.Setenv("PATH", t.TempDir())
557
558 _, err := FindServer(profile.Server{Command: "rust-analyzer", Dirs: []string{dir}})
559
560 if err == nil {
561 t.Fatal("FindServer() error = nil, want a failure")
562 }
563 for _, want := range []string{"rust-analyzer", "PATH", dir} {
564 if !strings.Contains(err.Error(), want) {
565 t.Errorf("FindServer() error = %q, want it to mention %q", err, want)
566 }
567 }
568}
569
570func TestFindServerFindsTheServerInOneOfTheProfilesDirectories(t *testing.T) {
571 // The whole point of Dirs: cargo and go install put a language server
572 // somewhere that is very often not on PATH.
573 dir := t.TempDir()
574 path := filepath.Join(dir, "pretend-analyzer")
575 if err := os.WriteFile(path, []byte("#!/bin/sh\n"), 0o755); err != nil {
576 t.Fatalf("writing a stand-in server: %v", err)
577 }
578 t.Setenv("PATH", t.TempDir())
579
580 found, err := FindServer(profile.Server{Command: "pretend-analyzer", Dirs: []string{"", dir}})
581
582 if err != nil {
583 t.Fatalf("FindServer() error = %v", err)
584 }
585 if found != path {
586 t.Errorf("FindServer() = %q, want %q", found, path)
587 }
588}
589
590// mustInitialize performs the handshake, failing the test if it does not work.
591func mustInitialize(t *testing.T, client *Client) {
592 t.Helper()
593 if err := client.Initialize(t.Context()); err != nil {
594 t.Fatalf("Initialize() error = %v", err)
595 }
596}
597
598// waitForMethod blocks until the fake server has seen a method, so a test
599// never races the notification it is about to assert on.
600func waitForMethod(t *testing.T, server *fakeServer, method string) {
601 t.Helper()
602
603 deadline := time.After(2 * time.Second)
604 for {
605 for _, seen := range server.methods() {
606 if seen == method {
607 return
608 }
609 }
610 select {
611 case <-deadline:
612 t.Fatalf("the server never saw %q; it saw %v", method, server.methods())
613 case <-time.After(time.Millisecond):
614 }
615 }
616}
617
618// locationAnswerers are the four requests that answer with places in the code.
619// They share a decoder, so they share their tests.
620var locationAnswerers = []struct {
621 method string
622 ask func(*Client) ([]Location, error)
623}{
624 {"textDocument/definition", func(c *Client) ([]Location, error) {
625 return c.Definition(context.Background(), "main.go", 3, 4, "x := y")
626 }},
627 {"textDocument/typeDefinition", func(c *Client) ([]Location, error) {
628 return c.TypeDefinition(context.Background(), "main.go", 3, 4, "x := y")
629 }},
630 {"textDocument/implementation", func(c *Client) ([]Location, error) {
631 return c.Implementation(context.Background(), "main.go", 3, 4, "x := y")
632 }},
633 {"textDocument/references", func(c *Client) ([]Location, error) {
634 return c.References(context.Background(), "main.go", 3, 4, "x := y", true)
635 }},
636}
637
638func TestEveryLocationRequestSendsItsOwnMethod(t *testing.T) {
639 // One helper serves all four, so the one thing that can go wrong is a
640 // request going out under the wrong name — which a server answers with an
641 // error the caller reads as "nothing found".
642 for _, request := range locationAnswerers {
643 t.Run(request.method, func(t *testing.T) {
644 client, server := newFakeServer(t)
645 server.setHandler(func(method string, _ json.RawMessage) (any, *ResponseError) {
646 if method != request.method {
647 return nil, nil
648 }
649 return []Location{{URI: PathToURI("/tmp/other.go")}}, nil
650 })
651 mustInitialize(t, client)
652
653 locations, err := request.ask(client)
654 if err != nil {
655 t.Fatalf("error = %v", err)
656 }
657 if len(locations) != 1 {
658 t.Errorf("got %d locations, want the one the server answered to %s", len(locations), request.method)
659 }
660 })
661 }
662}
663
664func TestEveryLocationRequestReadsBothShapes(t *testing.T) {
665 // A server may answer one location as a bare object rather than an array
666 // of one. gopls does it; the specification allows it.
667 location := Location{URI: PathToURI("/tmp/other.go"), Range: Range{Start: Position{Line: 4}}}
668
669 for _, request := range locationAnswerers {
670 for _, shape := range []struct {
671 name string
672 reply any
673 want int
674 }{
675 {"an array", []Location{location, location}, 2},
676 {"a bare object", location, 1},
677 {"null", nil, 0},
678 } {
679 t.Run(request.method+"/"+shape.name, func(t *testing.T) {
680 client, server := newFakeServer(t)
681 server.setHandler(func(method string, _ json.RawMessage) (any, *ResponseError) {
682 if method != request.method {
683 return nil, nil
684 }
685 return shape.reply, nil
686 })
687 mustInitialize(t, client)
688
689 locations, err := request.ask(client)
690 if err != nil {
691 t.Fatalf("error = %v", err)
692 }
693 if len(locations) != shape.want {
694 t.Errorf("got %d locations, want %d", len(locations), shape.want)
695 }
696 })
697 }
698 }
699}
700
701func TestEveryLocationRequestRefusesBeforeTheServerIsReady(t *testing.T) {
702 for _, request := range locationAnswerers {
703 t.Run(request.method, func(t *testing.T) {
704 client, _ := newFakeServer(t)
705
706 if _, err := request.ask(client); !errors.Is(err, ErrNotReady) {
707 t.Errorf("error = %v, want ErrNotReady", err)
708 }
709 })
710 }
711}
712
713func TestReferencesSaysWhetherItWantsTheDeclaration(t *testing.T) {
714 // The one parameter that is not shared. A server that never sees it
715 // applies its own default, and the caller's choice is silently lost.
716 for _, want := range []bool{true, false} {
717 t.Run(fmt.Sprint(want), func(t *testing.T) {
718 client, server := newFakeServer(t)
719 var sent json.RawMessage
720 server.setHandler(func(method string, params json.RawMessage) (any, *ResponseError) {
721 if method == "textDocument/references" {
722 sent = params
723 }
724 return []Location{}, nil
725 })
726 mustInitialize(t, client)
727
728 if _, err := client.References(t.Context(), "main.go", 1, 0, "", want); err != nil {
729 t.Fatalf("References() error = %v", err)
730 }
731
732 var got struct {
733 Context struct {
734 IncludeDeclaration bool `json:"includeDeclaration"`
735 } `json:"context"`
736 }
737 if err := json.Unmarshal(sent, &got); err != nil {
738 t.Fatalf("the parameters do not parse: %v\n%s", err, sent)
739 }
740 if got.Context.IncludeDeclaration != want {
741 t.Errorf("includeDeclaration = %v, want %v — sent %s", got.Context.IncludeDeclaration, want, sent)
742 }
743 })
744 }
745}
746
747func TestTheClientAsksForTheCapabilitiesItUses(t *testing.T) {
748 // Claiming nothing is as wrong as claiming too much: a server may decline
749 // to answer a request the client never said it could use.
750 capabilities := clientCapabilities()
751 document, ok := capabilities["textDocument"].(map[string]any)
752 if !ok {
753 t.Fatal("no textDocument capabilities at all")
754 }
755
756 for _, want := range []string{"completion", "hover", "references", "implementation", "typeDefinition", "publishDiagnostics"} {
757 if _, declared := document[want]; !declared {
758 t.Errorf("the client never declares %q, but sends it", want)
759 }
760 }
761}
📦 Turbo Core — canonical paths: symbolic links resolved in URIs and document keys (moon-lsp on macOS) b91316e k33g 4h ago762
763func TestPathToURIResolvesSymbolicLinks(t *testing.T) {
764 // moon-lsp canonicalises the files of a package, so a document announced
765 // under a linked spelling of its path is one it knows nothing about. On
766 // macOS every temporary directory is such a spelling: /var → /private/var.
767 real := t.TempDir()
768 link := filepath.Join(t.TempDir(), "link")
769 if err := os.Symlink(real, link); err != nil {
770 t.Skipf("cannot create a symbolic link here: %v", err)
771 }
772 if err := os.WriteFile(filepath.Join(real, "main.mbt"), []byte("fn main {}\n"), 0o644); err != nil {
773 t.Fatal(err)
774 }
775
776 got := PathToURI(filepath.Join(link, "main.mbt"))
777
778 want := PathToURI(filepath.Join(real, "main.mbt"))
779 if got != want {
780 t.Errorf("PathToURI through the link = %q, want the real path's %q", got, want)
781 }
782 if strings.Contains(got, "/link/") {
783 t.Errorf("the URI still spells the link: %q", got)
784 }
785}
786
787func TestCanonicalPathOfAFileNotYetOnDiskResolvesItsDirectory(t *testing.T) {
788 // A buffer being saved under a new name has nothing to resolve, and must
789 // still be keyed the way its diagnostics will arrive once it exists.
790 real := t.TempDir()
791 link := filepath.Join(t.TempDir(), "link")
792 if err := os.Symlink(real, link); err != nil {
793 t.Skipf("cannot create a symbolic link here: %v", err)
794 }
795
796 got := CanonicalPath(filepath.Join(link, "new", "file.mbt"))
797
798 // t.TempDir itself may sit under a link (macOS again), so compare with
799 // the resolved real directory rather than with real as spelt.
800 resolvedReal, err := filepath.EvalSymlinks(real)
801 if err != nil {
802 t.Fatal(err)
803 }
804 if want := filepath.Join(resolvedReal, "new", "file.mbt"); got != want {
805 t.Errorf("CanonicalPath() = %q, want %q", got, want)
806 }
807}