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