1// Copyright 2019 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// Package cmdtest contains the test suite for the command line behavior of gopls. 6package cmdtest 7 8import ( 9 "bytes" 10 "context" 11 "fmt" 12 "io" 13 "os" 14 "path/filepath" 15 "strconv" 16 "strings" 17 "sync" 18 "testing" 19 20 "golang.org/x/tools/go/packages/packagestest" 21 "golang.org/x/tools/internal/lsp/cmd" 22 "golang.org/x/tools/internal/lsp/protocol" 23 "golang.org/x/tools/internal/lsp/source" 24 "golang.org/x/tools/internal/lsp/tests" 25 "golang.org/x/tools/internal/span" 26 "golang.org/x/tools/internal/tool" 27) 28 29type runner struct { 30 exporter packagestest.Exporter 31 data *tests.Data 32 ctx context.Context 33 options func(*source.Options) 34 normalizers []normalizer 35 remote string 36} 37 38type normalizer struct { 39 path string 40 slashed string 41 escaped string 42 fragment string 43} 44 45func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner { 46 r := &runner{ 47 exporter: exporter, 48 data: data, 49 ctx: ctx, 50 options: options, 51 normalizers: make([]normalizer, 0, len(data.Exported.Modules)), 52 remote: remote, 53 } 54 // build the path normalizing patterns 55 for _, m := range data.Exported.Modules { 56 for fragment := range m.Files { 57 n := normalizer{ 58 path: data.Exported.File(m.Name, fragment), 59 fragment: fragment, 60 } 61 if n.slashed = filepath.ToSlash(n.path); n.slashed == n.path { 62 n.slashed = "" 63 } 64 quoted := strconv.Quote(n.path) 65 if n.escaped = quoted[1 : len(quoted)-1]; n.escaped == n.path { 66 n.escaped = "" 67 } 68 r.normalizers = append(r.normalizers, n) 69 } 70 } 71 return r 72} 73 74func (r *runner) CodeLens(t *testing.T, spn span.Span, want []protocol.CodeLens) { 75 //TODO: add command line completions tests when it works 76} 77 78func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { 79 //TODO: add command line completions tests when it works 80} 81 82func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) { 83 //TODO: add command line completions tests when it works 84} 85 86func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { 87 //TODO: add command line completions tests when it works 88} 89 90func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { 91 //TODO: add command line completions tests when it works 92} 93 94func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { 95 //TODO: add command line completions tests when it works 96} 97 98func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { 99 //TODO: add command line completions tests when it works 100} 101 102func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { 103 //TODO: add command line completions tests when it works 104} 105 106func (r *runner) WorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) { 107 //TODO: add command line workspace symbol tests when it works 108} 109 110func (r *runner) FuzzyWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) { 111 //TODO: add command line workspace symbol tests when it works 112} 113 114func (r *runner) CaseSensitiveWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) { 115 //TODO: add command line workspace symbol tests when it works 116} 117 118func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) { 119 rStdout, wStdout, err := os.Pipe() 120 if err != nil { 121 t.Fatal(err) 122 } 123 oldStdout := os.Stdout 124 rStderr, wStderr, err := os.Pipe() 125 if err != nil { 126 t.Fatal(err) 127 } 128 oldStderr := os.Stderr 129 stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{} 130 var wg sync.WaitGroup 131 wg.Add(2) 132 go func() { 133 io.Copy(stdout, rStdout) 134 wg.Done() 135 }() 136 go func() { 137 io.Copy(stderr, rStderr) 138 wg.Done() 139 }() 140 os.Stdout, os.Stderr = wStdout, wStderr 141 app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options) 142 remote := r.remote 143 err = tool.Run(tests.Context(t), 144 app, 145 append([]string{fmt.Sprintf("-remote=internal@%s", remote)}, args...)) 146 if err != nil { 147 fmt.Fprint(os.Stderr, err) 148 } 149 wStdout.Close() 150 wStderr.Close() 151 wg.Wait() 152 os.Stdout, os.Stderr = oldStdout, oldStderr 153 rStdout.Close() 154 rStderr.Close() 155 return stdout.String(), stderr.String() 156} 157 158// NormalizeGoplsCmd runs the gopls command and normalizes its output. 159func (r *runner) NormalizeGoplsCmd(t testing.TB, args ...string) (string, string) { 160 stdout, stderr := r.runGoplsCmd(t, args...) 161 return r.Normalize(stdout), r.Normalize(stderr) 162} 163 164// NormalizePrefix normalizes a single path at the front of the input string. 165func (r *runner) NormalizePrefix(s string) string { 166 for _, n := range r.normalizers { 167 if t := strings.TrimPrefix(s, n.path); t != s { 168 return n.fragment + t 169 } 170 if t := strings.TrimPrefix(s, n.slashed); t != s { 171 return n.fragment + t 172 } 173 if t := strings.TrimPrefix(s, n.escaped); t != s { 174 return n.fragment + t 175 } 176 } 177 return s 178} 179 180// Normalize replaces all paths present in s with just the fragment portion 181// this is used to make golden files not depend on the temporary paths of the files 182func (r *runner) Normalize(s string) string { 183 type entry struct { 184 path string 185 index int 186 fragment string 187 } 188 match := make([]entry, 0, len(r.normalizers)) 189 // collect the initial state of all the matchers 190 for _, n := range r.normalizers { 191 index := strings.Index(s, n.path) 192 if index >= 0 { 193 match = append(match, entry{n.path, index, n.fragment}) 194 } 195 if n.slashed != "" { 196 index := strings.Index(s, n.slashed) 197 if index >= 0 { 198 match = append(match, entry{n.slashed, index, n.fragment}) 199 } 200 } 201 if n.escaped != "" { 202 index := strings.Index(s, n.escaped) 203 if index >= 0 { 204 match = append(match, entry{n.escaped, index, n.fragment}) 205 } 206 } 207 } 208 // result should be the same or shorter than the input 209 buf := bytes.NewBuffer(make([]byte, 0, len(s))) 210 last := 0 211 for { 212 // find the nearest path match to the start of the buffer 213 next := -1 214 nearest := len(s) 215 for i, c := range match { 216 if c.index >= 0 && nearest > c.index { 217 nearest = c.index 218 next = i 219 } 220 } 221 // if there are no matches, we copy the rest of the string and are done 222 if next < 0 { 223 buf.WriteString(s[last:]) 224 return buf.String() 225 } 226 // we have a match 227 n := &match[next] 228 // copy up to the start of the match 229 buf.WriteString(s[last:n.index]) 230 // skip over the filename 231 last = n.index + len(n.path) 232 // add in the fragment instead 233 buf.WriteString(n.fragment) 234 // see what the next match for this path is 235 n.index = strings.Index(s[last:], n.path) 236 if n.index >= 0 { 237 n.index += last 238 } 239 } 240} 241