返回 DeepSeek-Reasonix
manual_test.go
根目录 / internal / lsp / manual_test.go
1 //go:build manual
2
3 package lsp
4
5 import (
6 "context"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13 )
14
15 // TestLanguageServers drives each installed mainstream server against a per-
16 // language fixture under testdata/lsp. Servers not on PATH are skipped. Run with:
17 //
18 // go test -tags manual -run TestLanguageServers -v ./internal/lsp/
19 func TestLanguageServers(t *testing.T) {
20 specs := DefaultSpecs()
21 cases := []struct {
22 lang, dir, file, callNeedle, symbol string
23 }{
24 {"go", "golang", "main.go", "_ = greet(", "greet"},
25 {"rust", "rust", filepath.Join("src", "main.rs"), "let _ = greet(", "greet"},
26 {"typescript", "typescript", "index.ts", "const msg = greet(", "greet"},
27 {"python", "python", "main.py", "msg = greet(", "greet"},
28 {"bash", "bash", "script.sh", `greet "world"`, "greet"},
29 }
30 for _, tc := range cases {
31 t.Run(tc.lang, func(t *testing.T) {
32 spec := specs[tc.lang]
33 if _, err := exec.LookPath(spec.Command); err != nil {
34 t.Skipf("%s not on PATH (%s)", spec.Command, spec.InstallHint)
35 }
36 root, err := filepath.Abs(filepath.Join("testdata", "lsp", tc.dir))
37 if err != nil {
38 t.Fatal(err)
39 }
40 m := NewManager(root, specs)
41 defer m.Close()
42 ctx, cancel := context.WithTimeout(context.Background(), 200*time.Second)
43 defer cancel()
44
45 line := findLine(t, root, tc.file, tc.callNeedle)
46 def := defWithRetry(t, ctx, m, tc.file, line, tc.symbol, 150*time.Second)
47 t.Logf("[%s] definition → %s", tc.lang, def)
48 if strings.Contains(def, "no definition") || !strings.Contains(def, filepath.Base(tc.file)) {
49 t.Errorf("%s: definition did not resolve into %s: %s", tc.lang, tc.file, def)
50 }
51
52 if hov, err := m.Hover(ctx, tc.file, line, tc.symbol); err != nil {
53 t.Errorf("%s hover: %v", tc.lang, err)
54 } else {
55 t.Logf("[%s] hover → %s", tc.lang, oneLine(hov))
56 }
57 if diag, err := m.Diagnostics(ctx, tc.file); err != nil {
58 t.Errorf("%s diagnostics: %v", tc.lang, err)
59 } else {
60 t.Logf("[%s] diagnostics → %s", tc.lang, oneLine(diag))
61 }
62 })
63 }
64 }
65
66 // defWithRetry polls Definition until it resolves or the budget elapses — some
67 // servers (rust-analyzer) keep indexing for a while after initialize returns.
68 func defWithRetry(t *testing.T, ctx context.Context, m *Manager, file string, line int, symbol string, budget time.Duration) string {
69 end := time.Now().Add(budget)
70 var last string
71 for {
72 d, err := m.Definition(ctx, file, line, symbol)
73 if err != nil {
74 // ContentModified means the server is still indexing — retry per LSP §-32801.
75 if strings.Contains(err.Error(), "content modified") && time.Now().Before(end) {
76 time.Sleep(time.Second)
77 continue
78 }
79 t.Fatalf("definition: %v", err)
80 }
81 last = d
82 if !strings.Contains(d, "no definition") || time.Now().After(end) {
83 return last
84 }
85 time.Sleep(500 * time.Millisecond)
86 }
87 }
88
89 func oneLine(s string) string {
90 s = strings.TrimSpace(s)
91 if i := strings.IndexByte(s, '\n'); i >= 0 {
92 return s[:i] + " …"
93 }
94 return s
95 }
96
97 // TestGoplsSmoke drives a real gopls against this module's own source. Run with:
98 //
99 // go test -tags manual -run TestGoplsSmoke -v ./internal/lsp/
100 func TestGoplsSmoke(t *testing.T) {
101 root := moduleRoot(t)
102 m := NewManager(root, DefaultSpecs())
103 defer m.Close()
104
105 ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
106 defer cancel()
107
108 managerRel := filepath.Join("internal", "lsp", "manager.go")
109 callLine := findLine(t, root, managerRel, "err := locate(")
110
111 def, err := m.Definition(ctx, managerRel, callLine, "locate")
112 if err != nil {
113 t.Fatalf("definition: %v", err)
114 }
115 t.Logf("definition →\n%s", def)
116 if !strings.Contains(def, "position.go") {
117 t.Errorf("definition of locate should land in position.go, got:\n%s", def)
118 }
119
120 hov, err := m.Hover(ctx, managerRel, callLine, "locate")
121 if err != nil {
122 t.Fatalf("hover: %v", err)
123 }
124 t.Logf("hover →\n%s", hov)
125 if !strings.Contains(hov, "func locate") {
126 t.Errorf("hover should show locate's signature, got:\n%s", hov)
127 }
128
129 diag, err := m.Diagnostics(ctx, filepath.Join("internal", "lsp", "jsonrpc.go"))
130 if err != nil {
131 t.Fatalf("diagnostics: %v", err)
132 }
133 t.Logf("diagnostics → %s", diag)
134 if !strings.Contains(diag, "no diagnostics") {
135 t.Errorf("clean file should have no diagnostics, got: %s", diag)
136 }
137 }
138
139 func moduleRoot(t *testing.T) string {
140 dir, err := os.Getwd()
141 if err != nil {
142 t.Fatal(err)
143 }
144 for {
145 if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
146 return dir
147 }
148 parent := filepath.Dir(dir)
149 if parent == dir {
150 t.Fatal("go.mod not found")
151 }
152 dir = parent
153 }
154 }
155
156 func findLine(t *testing.T, root, rel, needle string) int {
157 b, err := os.ReadFile(filepath.Join(root, rel))
158 if err != nil {
159 t.Fatal(err)
160 }
161 for i, line := range strings.Split(string(b), "\n") {
162 if strings.Contains(line, needle) {
163 return i + 1
164 }
165 }
166 t.Fatalf("needle %q not found in %s", needle, rel)
167 return 0
168 }
169
169 lines GO