mirror of
https://github.com/golang/go
synced 2024-11-18 13:14:47 -07:00
af4cc2cd81
To match the actual gopls binary, use hooks.Options when creating the regtest server. Add a test for staticcheck diagnostics to leverage this. For golang/go#39384 Change-Id: I52837c2b12bb586a2530343bdfae5172b08df49c Reviewed-on: https://go-review.googlesource.com/c/tools/+/252683 Run-TryBot: Robert Findley <rfindley@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
423 lines
12 KiB
Go
423 lines
12 KiB
Go
// Copyright 2020 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 regtest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime/pprof"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/tools/gopls/internal/hooks"
|
|
"golang.org/x/tools/internal/jsonrpc2"
|
|
"golang.org/x/tools/internal/jsonrpc2/servertest"
|
|
"golang.org/x/tools/internal/lsp/cache"
|
|
"golang.org/x/tools/internal/lsp/debug"
|
|
"golang.org/x/tools/internal/lsp/fake"
|
|
"golang.org/x/tools/internal/lsp/lsprpc"
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
)
|
|
|
|
// Mode is a bitmask that defines for which execution modes a test should run.
|
|
type Mode int
|
|
|
|
const (
|
|
// Singleton mode uses a separate in-process gopls instance for each test,
|
|
// and communicates over pipes to mimic the gopls sidecar execution mode,
|
|
// which communicates over stdin/stderr.
|
|
Singleton Mode = 1 << iota
|
|
|
|
// Forwarded forwards connections to a shared in-process gopls instance.
|
|
Forwarded
|
|
// SeparateProcess forwards connection to a shared separate gopls process.
|
|
SeparateProcess
|
|
// NormalModes are the global default execution modes, when unmodified by
|
|
// test flags or by individual test options.
|
|
NormalModes = Singleton | Forwarded
|
|
)
|
|
|
|
// A Runner runs tests in gopls execution environments, as specified by its
|
|
// modes. For modes that share state (for example, a shared cache or common
|
|
// remote), any tests that execute on the same Runner will share the same
|
|
// state.
|
|
type Runner struct {
|
|
DefaultModes Mode
|
|
Timeout time.Duration
|
|
GoplsPath string
|
|
PrintGoroutinesOnFailure bool
|
|
TempDir string
|
|
SkipCleanup bool
|
|
|
|
mu sync.Mutex
|
|
ts *servertest.TCPServer
|
|
socketDir string
|
|
// closers is a queue of clean-up functions to run at the end of the entire
|
|
// test suite.
|
|
closers []io.Closer
|
|
}
|
|
|
|
type runConfig struct {
|
|
editor fake.EditorConfig
|
|
sandbox fake.SandboxConfig
|
|
modes Mode
|
|
timeout time.Duration
|
|
debugAddr string
|
|
skipLogs bool
|
|
skipHooks bool
|
|
}
|
|
|
|
func (r *Runner) defaultConfig() *runConfig {
|
|
return &runConfig{
|
|
modes: r.DefaultModes,
|
|
timeout: r.Timeout,
|
|
}
|
|
}
|
|
|
|
// A RunOption augments the behavior of the test runner.
|
|
type RunOption interface {
|
|
set(*runConfig)
|
|
}
|
|
|
|
type optionSetter func(*runConfig)
|
|
|
|
func (f optionSetter) set(opts *runConfig) {
|
|
f(opts)
|
|
}
|
|
|
|
// WithTimeout configures a custom timeout for this test run.
|
|
func WithTimeout(d time.Duration) RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.timeout = d
|
|
})
|
|
}
|
|
|
|
// WithProxyFiles configures a file proxy using the given txtar-encoded string.
|
|
func WithProxyFiles(txt string) RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.sandbox.ProxyFiles = txt
|
|
})
|
|
}
|
|
|
|
// WithModes configures the execution modes that the test should run in.
|
|
func WithModes(modes Mode) RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.modes = modes
|
|
})
|
|
}
|
|
|
|
// WithEditorConfig configures the editor's LSP session.
|
|
func WithEditorConfig(config fake.EditorConfig) RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.editor = config
|
|
})
|
|
}
|
|
|
|
// WithoutWorkspaceFolders prevents workspace folders from being sent as part
|
|
// of the sandbox's initialization. It is used to simulate opening a single
|
|
// file in the editor, without a workspace root. In that case, the client sends
|
|
// neither workspace folders nor a root URI.
|
|
func WithoutWorkspaceFolders() RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.editor.WithoutWorkspaceFolders = true
|
|
})
|
|
}
|
|
|
|
// WithRootPath specifies the rootURI of the workspace folder opened in the
|
|
// editor. By default, the sandbox opens the top-level directory, but some
|
|
// tests need to check other cases.
|
|
func WithRootPath(path string) RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.editor.EditorRootPath = path
|
|
})
|
|
}
|
|
|
|
// InGOPATH configures the workspace working directory to be GOPATH, rather
|
|
// than a separate working directory for use with modules.
|
|
func InGOPATH() RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.sandbox.InGoPath = true
|
|
})
|
|
}
|
|
|
|
// WithDebugAddress configures a debug server bound to addr. This option is
|
|
// currently only supported when executing in Singleton mode. It is intended to
|
|
// be used for long-running stress tests.
|
|
func WithDebugAddress(addr string) RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.debugAddr = addr
|
|
})
|
|
}
|
|
|
|
// SkipLogs skips the buffering of logs during test execution. It is intended
|
|
// for long-running stress tests.
|
|
func SkipLogs() RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.skipLogs = true
|
|
})
|
|
}
|
|
|
|
// InExistingDir runs the test in a pre-existing directory. If set, no initial
|
|
// files may be passed to the runner. It is intended for long-running stress
|
|
// tests.
|
|
func InExistingDir(dir string) RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.sandbox.Workdir = dir
|
|
})
|
|
}
|
|
|
|
// SkipHooks allows for disabling the test runner's client hooks that are used
|
|
// for instrumenting expectations (tracking diagnostics, logs, work done,
|
|
// etc.). It is intended for performance-sensitive stress tests or benchmarks.
|
|
func SkipHooks(skip bool) RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.skipHooks = skip
|
|
})
|
|
}
|
|
|
|
// WithGOPROXY configures the test environment to have an explicit proxy value.
|
|
// This is intended for stress tests -- to ensure their isolation, regtests
|
|
// should instead use WithProxyFiles.
|
|
func WithGOPROXY(goproxy string) RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.sandbox.GOPROXY = goproxy
|
|
})
|
|
}
|
|
|
|
// WithLimitWorkspaceScope sets the LimitWorkspaceScope configuration.
|
|
func WithLimitWorkspaceScope() RunOption {
|
|
return optionSetter(func(opts *runConfig) {
|
|
opts.editor.LimitWorkspaceScope = true
|
|
})
|
|
}
|
|
|
|
type TestFunc func(t *testing.T, env *Env)
|
|
|
|
// Run executes the test function in the default configured gopls execution
|
|
// modes. For each a test run, a new workspace is created containing the
|
|
// un-txtared files specified by filedata.
|
|
func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) {
|
|
t.Helper()
|
|
|
|
tests := []struct {
|
|
name string
|
|
mode Mode
|
|
getServer func(context.Context, *testing.T) jsonrpc2.StreamServer
|
|
}{
|
|
{"singleton", Singleton, singletonServer},
|
|
{"forwarded", Forwarded, r.forwardedServer},
|
|
{"separate_process", SeparateProcess, r.separateProcessServer},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
config := r.defaultConfig()
|
|
for _, opt := range opts {
|
|
opt.set(config)
|
|
}
|
|
if config.modes&tc.mode == 0 {
|
|
continue
|
|
}
|
|
if config.debugAddr != "" && tc.mode != Singleton {
|
|
// Debugging is useful for running stress tests, but since the daemon has
|
|
// likely already been started, it would be too late to debug.
|
|
t.Fatalf("debugging regtest servers only works in Singleton mode, "+
|
|
"got debug addr %q and mode %v", config.debugAddr, tc.mode)
|
|
}
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), config.timeout)
|
|
defer cancel()
|
|
ctx = debug.WithInstance(ctx, "", "off")
|
|
if config.debugAddr != "" {
|
|
di := debug.GetInstance(ctx)
|
|
di.DebugAddress = config.debugAddr
|
|
di.Serve(ctx)
|
|
di.MonitorMemory(ctx)
|
|
}
|
|
|
|
tempDir := filepath.Join(r.TempDir, filepath.FromSlash(t.Name()))
|
|
if err := os.MkdirAll(tempDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
config.sandbox.Files = files
|
|
config.sandbox.RootDir = tempDir
|
|
sandbox, err := fake.NewSandbox(&config.sandbox)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Deferring the closure of ws until the end of the entire test suite
|
|
// has, in testing, given the LSP server time to properly shutdown and
|
|
// release any file locks held in workspace, which is a problem on
|
|
// Windows. This may still be flaky however, and in the future we need a
|
|
// better solution to ensure that all Go processes started by gopls have
|
|
// exited before we clean up.
|
|
r.AddCloser(sandbox)
|
|
ss := tc.getServer(ctx, t)
|
|
framer := jsonrpc2.NewRawStream
|
|
ls := &loggingFramer{}
|
|
if !config.skipLogs {
|
|
framer = ls.framer(jsonrpc2.NewRawStream)
|
|
}
|
|
ts := servertest.NewPipeServer(ctx, ss, framer)
|
|
env := NewEnv(ctx, t, sandbox, ts, config.editor, !config.skipHooks)
|
|
defer func() {
|
|
if t.Failed() && r.PrintGoroutinesOnFailure {
|
|
pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
|
|
}
|
|
if t.Failed() || testing.Verbose() {
|
|
ls.printBuffers(t.Name(), os.Stderr)
|
|
}
|
|
env.CloseEditor()
|
|
}()
|
|
test(t, env)
|
|
})
|
|
}
|
|
}
|
|
|
|
type loggingFramer struct {
|
|
mu sync.Mutex
|
|
buffers []*bytes.Buffer
|
|
}
|
|
|
|
func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer {
|
|
return func(nc net.Conn) jsonrpc2.Stream {
|
|
s.mu.Lock()
|
|
var buf bytes.Buffer
|
|
s.buffers = append(s.buffers, &buf)
|
|
s.mu.Unlock()
|
|
stream := f(nc)
|
|
return protocol.LoggingStream(stream, &buf)
|
|
}
|
|
}
|
|
|
|
func (s *loggingFramer) printBuffers(testname string, w io.Writer) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
for i, buf := range s.buffers {
|
|
fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs %d of %d for %q\n", i+1, len(s.buffers), testname)
|
|
// Re-buffer buf to avoid a data rate (io.Copy mutates src).
|
|
writeBuf := bytes.NewBuffer(buf.Bytes())
|
|
io.Copy(w, writeBuf)
|
|
fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs %d of %d for %q\n", i+1, len(s.buffers), testname)
|
|
}
|
|
}
|
|
|
|
func singletonServer(ctx context.Context, t *testing.T) jsonrpc2.StreamServer {
|
|
return lsprpc.NewStreamServer(cache.New(ctx, hooks.Options), false)
|
|
}
|
|
|
|
func (r *Runner) forwardedServer(ctx context.Context, t *testing.T) jsonrpc2.StreamServer {
|
|
ts := r.getTestServer()
|
|
return lsprpc.NewForwarder("tcp", ts.Addr)
|
|
}
|
|
|
|
// getTestServer gets the shared test server instance to connect to, or creates
|
|
// one if it doesn't exist.
|
|
func (r *Runner) getTestServer() *servertest.TCPServer {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.ts == nil {
|
|
ctx := context.Background()
|
|
ctx = debug.WithInstance(ctx, "", "off")
|
|
ss := lsprpc.NewStreamServer(cache.New(ctx, hooks.Options), false)
|
|
r.ts = servertest.NewTCPServer(ctx, ss, nil)
|
|
}
|
|
return r.ts
|
|
}
|
|
|
|
func (r *Runner) separateProcessServer(ctx context.Context, t *testing.T) jsonrpc2.StreamServer {
|
|
// TODO(rfindley): can we use the autostart behavior here, instead of
|
|
// pre-starting the remote?
|
|
socket := r.getRemoteSocket(t)
|
|
return lsprpc.NewForwarder("unix", socket)
|
|
}
|
|
|
|
// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running
|
|
// tests. It's a trick to allow tests to find a binary to use to start a gopls
|
|
// subprocess.
|
|
const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS"
|
|
|
|
func (r *Runner) getRemoteSocket(t *testing.T) string {
|
|
t.Helper()
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
const daemonFile = "gopls-test-daemon"
|
|
if r.socketDir != "" {
|
|
return filepath.Join(r.socketDir, daemonFile)
|
|
}
|
|
|
|
if r.GoplsPath == "" {
|
|
t.Fatal("cannot run tests with a separate process unless a path to a gopls binary is configured")
|
|
}
|
|
var err error
|
|
r.socketDir, err = ioutil.TempDir(r.TempDir, "gopls-regtest-socket")
|
|
if err != nil {
|
|
t.Fatalf("creating tempdir: %v", err)
|
|
}
|
|
socket := filepath.Join(r.socketDir, daemonFile)
|
|
args := []string{"serve", "-listen", "unix;" + socket, "-listen.timeout", "10s"}
|
|
cmd := exec.Command(r.GoplsPath, args...)
|
|
cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true")
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
go func() {
|
|
if err := cmd.Run(); err != nil {
|
|
panic(fmt.Sprintf("error running external gopls: %v\nstderr:\n%s", err, stderr.String()))
|
|
}
|
|
}()
|
|
return socket
|
|
}
|
|
|
|
// AddCloser schedules a closer to be closed at the end of the test run. This
|
|
// is useful for Windows in particular, as
|
|
func (r *Runner) AddCloser(closer io.Closer) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.closers = append(r.closers, closer)
|
|
}
|
|
|
|
// Close cleans up resource that have been allocated to this workspace.
|
|
func (r *Runner) Close() error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
var errmsgs []string
|
|
if r.ts != nil {
|
|
if err := r.ts.Close(); err != nil {
|
|
errmsgs = append(errmsgs, err.Error())
|
|
}
|
|
}
|
|
if r.socketDir != "" {
|
|
if err := os.RemoveAll(r.socketDir); err != nil {
|
|
errmsgs = append(errmsgs, err.Error())
|
|
}
|
|
}
|
|
if !r.SkipCleanup {
|
|
for _, closer := range r.closers {
|
|
if err := closer.Close(); err != nil {
|
|
errmsgs = append(errmsgs, err.Error())
|
|
}
|
|
}
|
|
if err := os.RemoveAll(r.TempDir); err != nil {
|
|
errmsgs = append(errmsgs, err.Error())
|
|
}
|
|
}
|
|
if len(errmsgs) > 0 {
|
|
return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t"))
|
|
}
|
|
return nil
|
|
}
|