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.

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