--- /dev/null
+// 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/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 {
+ 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(t *testing.T, testdata string, options func(*source.Options)) {
+ // On Android, the testdata directory is not copied to the runner.
+ if stat, err := os.Stat(testdata); err != nil || !stat.IsDir() {
+ t.Skip("testdata directory not present")
+ }
+ tests.RunTests(t, testdata, false, func(t *testing.T, datum *tests.Data) {
+ ctx := tests.Context(t)
+ ts := NewTestServer(ctx, options)
+ tests.Run(t, NewRunner(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(data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner {
+ r := &runner{
+ 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
+ }
+ }
+}