返回 DeepSeek-Reasonix
lsp_test.go
根目录 / internal / lsp / lsp_test.go
1 package lsp
2
3 import (
4 "bufio"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "runtime"
10 "strings"
11 "testing"
12 "time"
13 )
14
15 func TestDefaultSpecsInvariants(t *testing.T) {
16 seen := map[string]string{}
17 for lang, s := range DefaultSpecs() {
18 if s.Command == "" || s.LanguageID == "" || len(s.Extensions) == 0 {
19 t.Errorf("lang %q: incomplete spec %+v", lang, s)
20 }
21 for _, ext := range s.Extensions {
22 if prev, dup := seen[ext]; dup {
23 t.Errorf("extension %q claimed by both %q and %q", ext, prev, lang)
24 }
25 seen[ext] = lang
26 }
27 }
28 if seen[".go"] != "go" || seen[".rs"] != "rust" || seen[".cpp"] != "cpp" || seen[".cs"] != "csharp" {
29 t.Errorf("unexpected routing: %v", seen)
30 }
31 }
32
33 func TestExtensionRouting(t *testing.T) {
34 m := NewManager(t.TempDir(), map[string]ServerSpec{
35 "elixir": {Command: "no-such-elixir-ls-xyz", LanguageID: "elixir", Extensions: []string{".ex", ".exs"}, InstallHint: "mix archive.install"},
36 })
37 defer m.Close()
38
39 if _, err := m.resolve("a.ex"); !errors.As(err, new(*notInstalledError)) {
40 t.Fatalf("configured-but-missing language should yield notInstalledError, got %v", err)
41 }
42 _, err := m.resolve("a.go")
43 if err == nil || !strings.Contains(err.Error(), "no language server") {
44 t.Fatalf("unconfigured extension should report no server, got %v", err)
45 }
46 }
47
48 func TestConnBidirectional(t *testing.T) {
49 caR, caW := io.Pipe()
50 acR, acW := io.Pipe()
51 // Close the writers at the end so both readLoop goroutines see EOF and exit
52 // (in production the subprocess pipe EOFs on kill; here nothing else closes it).
53 defer caW.Close()
54 defer acW.Close()
55
56 notif := make(chan string, 4)
57 var client *conn
58 client = newConn(caW, acR,
59 func(method string, _ json.RawMessage) { notif <- method },
60 func(id int64, _ string, _ json.RawMessage) { _ = client.reply(id, map[string]any{"ok": true}) })
61
62 var server *conn
63 server = newConn(acW, caR,
64 func(string, json.RawMessage) {},
65 func(id int64, method string, _ json.RawMessage) { _ = server.reply(id, map[string]any{"echo": method}) })
66
67 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
68 defer cancel()
69
70 res, err := client.call(ctx, "ping", map[string]any{"x": 1})
71 if err != nil {
72 t.Fatalf("client call: %v", err)
73 }
74 if !strings.Contains(string(res), `"echo":"ping"`) {
75 t.Fatalf("unexpected response: %s", res)
76 }
77
78 if err := server.notify("textDocument/publishDiagnostics", map[string]any{}); err != nil {
79 t.Fatalf("server notify: %v", err)
80 }
81 select {
82 case m := <-notif:
83 if m != "textDocument/publishDiagnostics" {
84 t.Fatalf("notify method = %q", m)
85 }
86 case <-ctx.Done():
87 t.Fatal("notification not delivered")
88 }
89
90 sres, err := server.call(ctx, "workspace/configuration", nil)
91 if err != nil {
92 t.Fatalf("server→client call: %v", err)
93 }
94 if !strings.Contains(string(sres), `"ok":true`) {
95 t.Fatalf("server→client reply: %s", sres)
96 }
97 }
98
99 func TestReadFrame(t *testing.T) {
100 in := "Content-Length: 17\r\nContent-Type: x\r\n\r\n" + `{"jsonrpc":"2.0"}` + "Content-Length: 2\r\n\r\n{}"
101 r := bufio.NewReader(strings.NewReader(in))
102 first, err := readFrame(r)
103 if err != nil || string(first) != `{"jsonrpc":"2.0"}` {
104 t.Fatalf("first frame = %q, err %v", first, err)
105 }
106 second, err := readFrame(r)
107 if err != nil || string(second) != `{}` {
108 t.Fatalf("second frame = %q, err %v", second, err)
109 }
110 if _, err := readFrame(r); err == nil {
111 t.Fatal("expected EOF on third read")
112 }
113 }
114
115 func TestURIRoundtrip(t *testing.T) {
116 paths := []string{"/home/u/a b.go", "/x/y.rs"}
117 if runtime.GOOS == "windows" {
118 paths = []string{`C:\Users\u\a b.go`, `D:\x\y.rs`}
119 }
120 for _, p := range paths {
121 uri := pathToURI(p)
122 if !strings.HasPrefix(uri, "file://") {
123 t.Errorf("%q → %q is not a file URI", p, uri)
124 }
125 if got := uriToPath(uri); got != p {
126 t.Errorf("roundtrip %q → %q", p, got)
127 }
128 }
129 }
130
131 func TestLocateEncoding(t *testing.T) {
132 content := "package x\nαβ foo()\n" // line 2 has two 2-byte runes then a space
133 u16, err := locate(content, 2, "foo", encodingUTF16)
134 if err != nil {
135 t.Fatal(err)
136 }
137 if u16.Line != 1 || u16.Character != 3 {
138 t.Errorf("utf16 pos = %+v, want line 1 char 3", u16)
139 }
140 u8, err := locate(content, 2, "foo", encodingUTF8)
141 if err != nil {
142 t.Fatal(err)
143 }
144 if u8.Character != 5 {
145 t.Errorf("utf8 char = %d, want 5", u8.Character)
146 }
147 if _, err := locate(content, 2, "missing", encodingUTF16); err == nil {
148 t.Error("expected not-found error")
149 }
150 }
151
152 func TestParseLocations(t *testing.T) {
153 single := `{"uri":"file:///a","range":{"start":{"line":1,"character":0},"end":{"line":1,"character":2}}}`
154 if got := parseLocations(json.RawMessage(single)); len(got) != 1 || got[0].URI != "file:///a" {
155 t.Errorf("single: %+v", got)
156 }
157 arr := `[{"uri":"file:///a","range":{}},{"uri":"file:///b","range":{}}]`
158 if got := parseLocations(json.RawMessage(arr)); len(got) != 2 {
159 t.Errorf("array: %+v", got)
160 }
161 link := `[{"targetUri":"file:///c","targetRange":{"start":{"line":2,"character":0},"end":{"line":2,"character":1}}}]`
162 got := parseLocations(json.RawMessage(link))
163 if len(got) != 1 || got[0].URI != "file:///c" || got[0].Range.Start.Line != 2 {
164 t.Errorf("locationlink: %+v", got)
165 }
166 if parseLocations(json.RawMessage("null")) != nil {
167 t.Error("null should yield nil")
168 }
169 }
170
171 func TestParseHover(t *testing.T) {
172 markup := `{"contents":{"kind":"markdown","value":"func F()"}}`
173 if got := parseHover(json.RawMessage(markup)); got != "func F()" {
174 t.Errorf("markup hover = %q", got)
175 }
176 marked := `{"contents":[{"language":"go","value":"func F()"},"docs"]}`
177 if got := parseHover(json.RawMessage(marked)); got != "func F()\ndocs" {
178 t.Errorf("marked array hover = %q", got)
179 }
180 if got := parseHover(json.RawMessage(`{"contents":""}`)); got != "" {
181 t.Errorf("empty hover = %q", got)
182 }
183 }
184
184 lines GO