| // Copyright 2019 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package cmdtest contains the test suite for the command line behavior of gopls. |
| package cmdtest |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "strconv" |
| "strings" |
| "sync" |
| "testing" |
| |
| "golang.org/x/tools/go/packages/packagestest" |
| "golang.org/x/tools/internal/jsonrpc2/servertest" |
| "golang.org/x/tools/internal/lsp/cache" |
| "golang.org/x/tools/internal/lsp/cmd" |
| "golang.org/x/tools/internal/lsp/debug" |
| "golang.org/x/tools/internal/lsp/lsprpc" |
| "golang.org/x/tools/internal/lsp/protocol" |
| "golang.org/x/tools/internal/lsp/source" |
| "golang.org/x/tools/internal/lsp/tests" |
| "golang.org/x/tools/internal/span" |
| "golang.org/x/tools/internal/tool" |
| ) |
| |
| type runner struct { |
| exporter packagestest.Exporter |
| data *tests.Data |
| ctx context.Context |
| options func(*source.Options) |
| normalizers []normalizer |
| remote string |
| } |
| |
| type normalizer struct { |
| path string |
| slashed string |
| escaped string |
| fragment string |
| } |
| |
| func TestCommandLine(testdata string, options func(*source.Options)) func(*testing.T, packagestest.Exporter) { |
| return func(t *testing.T, exporter packagestest.Exporter) { |
| if stat, err := os.Stat(testdata); err != nil || !stat.IsDir() { |
| t.Skip("testdata directory not present") |
| } |
| ctx := tests.Context(t) |
| ts := NewTestServer(ctx, options) |
| data := tests.Load(t, exporter, testdata) |
| for _, datum := range data { |
| defer datum.Exported.Cleanup() |
| t.Run(tests.FormatFolderName(datum.Folder), func(t *testing.T) { |
| t.Helper() |
| tests.Run(t, NewRunner(exporter, datum, ctx, ts.Addr, options), datum) |
| }) |
| } |
| cmd.CloseTestConnections(ctx) |
| } |
| } |
| |
| func NewTestServer(ctx context.Context, options func(*source.Options)) *servertest.TCPServer { |
| ctx = debug.WithInstance(ctx, "", "") |
| cache := cache.New(ctx, options) |
| ss := lsprpc.NewStreamServer(cache, false) |
| return servertest.NewTCPServer(ctx, ss, nil) |
| } |
| |
| func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner { |
| r := &runner{ |
| exporter: exporter, |
| data: data, |
| ctx: ctx, |
| options: options, |
| normalizers: make([]normalizer, 0, len(data.Exported.Modules)), |
| remote: remote, |
| } |
| // build the path normalizing patterns |
| for _, m := range data.Exported.Modules { |
| for fragment := range m.Files { |
| n := normalizer{ |
| path: data.Exported.File(m.Name, fragment), |
| fragment: fragment, |
| } |
| if n.slashed = filepath.ToSlash(n.path); n.slashed == n.path { |
| n.slashed = "" |
| } |
| quoted := strconv.Quote(n.path) |
| if n.escaped = quoted[1 : len(quoted)-1]; n.escaped == n.path { |
| n.escaped = "" |
| } |
| r.normalizers = append(r.normalizers, n) |
| } |
| } |
| return r |
| } |
| |
| func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) { |
| //TODO: add command line completions tests when it works |
| } |
| |
| func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { |
| //TODO: add command line completions tests when it works |
| } |
| |
| func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) { |
| //TODO: add command line completions tests when it works |
| } |
| |
| func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { |
| //TODO: add command line completions tests when it works |
| } |
| |
| func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { |
| //TODO: add command line completions tests when it works |
| } |
| |
| func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { |
| //TODO: add command line completions tests when it works |
| } |
| |
| func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { |
| //TODO: add command line completions tests when it works |
| } |
| |
| func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { |
| //TODO: add command line completions tests when it works |
| } |
| |
| func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) { |
| //TODO: function extraction not supported on command line |
| } |
| |
| func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) { |
| rStdout, wStdout, err := os.Pipe() |
| if err != nil { |
| t.Fatal(err) |
| } |
| oldStdout := os.Stdout |
| rStderr, wStderr, err := os.Pipe() |
| if err != nil { |
| t.Fatal(err) |
| } |
| oldStderr := os.Stderr |
| stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{} |
| var wg sync.WaitGroup |
| wg.Add(2) |
| go func() { |
| io.Copy(stdout, rStdout) |
| wg.Done() |
| }() |
| go func() { |
| io.Copy(stderr, rStderr) |
| wg.Done() |
| }() |
| os.Stdout, os.Stderr = wStdout, wStderr |
| app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options) |
| remote := r.remote |
| err = tool.Run(tests.Context(t), |
| app, |
| append([]string{fmt.Sprintf("-remote=internal@%s", remote)}, args...)) |
| if err != nil { |
| fmt.Fprint(os.Stderr, err) |
| } |
| wStdout.Close() |
| wStderr.Close() |
| wg.Wait() |
| os.Stdout, os.Stderr = oldStdout, oldStderr |
| rStdout.Close() |
| rStderr.Close() |
| return stdout.String(), stderr.String() |
| } |
| |
| // NormalizeGoplsCmd runs the gopls command and normalizes its output. |
| func (r *runner) NormalizeGoplsCmd(t testing.TB, args ...string) (string, string) { |
| stdout, stderr := r.runGoplsCmd(t, args...) |
| return r.Normalize(stdout), r.Normalize(stderr) |
| } |
| |
| // NormalizePrefix normalizes a single path at the front of the input string. |
| func (r *runner) NormalizePrefix(s string) string { |
| for _, n := range r.normalizers { |
| if t := strings.TrimPrefix(s, n.path); t != s { |
| return n.fragment + t |
| } |
| if t := strings.TrimPrefix(s, n.slashed); t != s { |
| return n.fragment + t |
| } |
| if t := strings.TrimPrefix(s, n.escaped); t != s { |
| return n.fragment + t |
| } |
| } |
| return s |
| } |
| |
| // Normalize replaces all paths present in s with just the fragment portion |
| // this is used to make golden files not depend on the temporary paths of the files |
| func (r *runner) Normalize(s string) string { |
| type entry struct { |
| path string |
| index int |
| fragment string |
| } |
| match := make([]entry, 0, len(r.normalizers)) |
| // collect the initial state of all the matchers |
| for _, n := range r.normalizers { |
| index := strings.Index(s, n.path) |
| if index >= 0 { |
| match = append(match, entry{n.path, index, n.fragment}) |
| } |
| if n.slashed != "" { |
| index := strings.Index(s, n.slashed) |
| if index >= 0 { |
| match = append(match, entry{n.slashed, index, n.fragment}) |
| } |
| } |
| if n.escaped != "" { |
| index := strings.Index(s, n.escaped) |
| if index >= 0 { |
| match = append(match, entry{n.escaped, index, n.fragment}) |
| } |
| } |
| } |
| // result should be the same or shorter than the input |
| buf := bytes.NewBuffer(make([]byte, 0, len(s))) |
| last := 0 |
| for { |
| // find the nearest path match to the start of the buffer |
| next := -1 |
| nearest := len(s) |
| for i, c := range match { |
| if c.index >= 0 && nearest > c.index { |
| nearest = c.index |
| next = i |
| } |
| } |
| // if there are no matches, we copy the rest of the string and are done |
| if next < 0 { |
| buf.WriteString(s[last:]) |
| return buf.String() |
| } |
| // we have a match |
| n := &match[next] |
| // copy up to the start of the match |
| buf.WriteString(s[last:n.index]) |
| // skip over the filename |
| last = n.index + len(n.path) |
| // add in the fragment instead |
| buf.WriteString(n.fragment) |
| // see what the next match for this path is |
| n.index = strings.Index(s[last:], n.path) |
| if n.index >= 0 { |
| n.index += last |
| } |
| } |
| } |