blob: cc0ea8623a813e889a486659c1a2fa1abaefdbc0 [file] [log] [blame]
// 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"
"flag"
"fmt"
"io"
"os"
"runtime"
"sync"
"testing"
"golang.org/x/tools/gopls/internal/lsp/cache"
"golang.org/x/tools/gopls/internal/lsp/cmd"
"golang.org/x/tools/gopls/internal/lsp/debug"
"golang.org/x/tools/gopls/internal/lsp/lsprpc"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/tests"
"golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/internal/jsonrpc2/servertest"
"golang.org/x/tools/internal/tool"
)
// TestCommandLine runs the marker tests in files beneath testdata/ using
// implementations of each of the marker operations (e.g. @hover) that
// call the main function of the gopls command within this process.
func TestCommandLine(t *testing.T, testdata string, options func(*source.Options)) {
// On Android, the testdata directory is not copied to the runner.
if runtime.GOOS == "android" {
t.Skip("testdata directory not present on android")
}
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(nil, nil)
ss := lsprpc.NewStreamServer(cache, false, options)
return servertest.NewTCPServer(ctx, ss, nil)
}
// runner implements tests.Tests by invoking the gopls command.
//
// TODO(golang/go#54845): We don't plan to implement all the methods
// of tests.Test. Indeed, we'd like to delete the methods that are
// implemented because the two problems they solve are best addressed
// in other ways:
//
// 1. They provide coverage of the behavior of the server, but this
// coverage is almost identical to the coverage provided by
// executing the same tests by making LSP RPCs directly (see
// lsp_test), and the latter is more efficient. When they do
// differ, it is a pain for maintainers.
//
// 2. They provide coverage of the client-side code of the
// command-line tool, which turns arguments into an LSP request and
// prints the results. But this coverage could be more directly and
// efficiently achieved by running a small number of tests tailored
// to exercise the client-side code, not the server behavior.
//
// Once that's done, tests.Tests would have only a single
// implementation (LSP), and we could refactor the marker tests
// so that they more closely resemble self-contained regtests,
// as described in #54845.
type runner struct {
data *tests.Data
ctx context.Context
options func(*source.Options)
normalizers []tests.Normalizer
remote string
}
func newRunner(data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner {
return &runner{
data: data,
ctx: ctx,
options: options,
normalizers: tests.CollectNormalizers(data.Exported),
remote: remote,
}
}
// runGoplsCmd returns the stdout and stderr of a gopls command.
//
// It does not fork+exec gopls, but in effect calls its main function,
// and thus the subcommand's Application.Run method, within this process.
//
// Stdout and stderr are temporarily redirected: not concurrency-safe!
//
// The "exit code" is printed to stderr but not returned.
// Invalid flags cause process exit.
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
s := flag.NewFlagSet(app.Name(), flag.ExitOnError)
err = tool.Run(tests.Context(t), s,
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)
}
func (r *runner) Normalize(s string) string {
return tests.Normalize(s, r.normalizers)
}
// Unimplemented methods of tests.Tests (see comment at runner):
func (*runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {}
func (*runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
}
func (*runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) {
}
func (*runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
}
func (*runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
}
func (*runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
}
func (*runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
}
func (*runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
}
func (*runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {}
func (*runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) {}
func (*runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {}
func (*runner) Hover(t *testing.T, spn span.Span, info string) {}
func (*runner) InlayHints(t *testing.T, spn span.Span) {}
func (*runner) SelectionRanges(t *testing.T, spn span.Span) {}