+++ /dev/null
-// 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"
- "golang.org/x/tools/internal/lsp/source"
-)
-
-// 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
- // Experimental enables all of the experimental configurations that are
- // being developed. Currently, it enables the workspace module.
- Experimental
- // WithoutExperiments are the modes that run without experimental features,
- // like the workspace module. These should be used for tests that only work
- // in the default modes.
- WithoutExperiments = Singleton | Forwarded
- // NormalModes are the global default execution modes, when unmodified by
- // test flags or by individual test options.
- NormalModes = Singleton | Experimental
-)
-
-// 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
- })
-}
-
-// EditorConfig is a RunOption option that configured the regtest editor.
-type EditorConfig fake.EditorConfig
-
-func (c EditorConfig) set(opts *runConfig) {
- opts.editor = fake.EditorConfig(c)
-}
-
-// 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},
- {"experimental_workspace_module", Experimental, experimentalWorkspaceModule},
- }
-
- 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()
- }()
- // Always await the initial workspace load.
- env.Await(InitialWorkspaceLoad)
- test(t, env)
- })
- }
-}
-
-type loggingFramer struct {
- mu sync.Mutex
- buffers []*safeBuffer
-}
-
-// safeBuffer is a threadsafe buffer for logs.
-type safeBuffer struct {
- mu sync.Mutex
- buf bytes.Buffer
-}
-
-func (b *safeBuffer) Write(p []byte) (int, error) {
- b.mu.Lock()
- defer b.mu.Unlock()
- return b.buf.Write(p)
-}
-
-func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer {
- return func(nc net.Conn) jsonrpc2.Stream {
- s.mu.Lock()
- buf := &safeBuffer{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)
- buf.mu.Lock()
- io.Copy(w, &buf.buf)
- buf.mu.Unlock()
- 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 experimentalWorkspaceModule(ctx context.Context, t *testing.T) jsonrpc2.StreamServer {
- options := func(o *source.Options) {
- hooks.Options(o)
- o.ExperimentalWorkspaceModule = true
- }
- return lsprpc.NewStreamServer(cache.New(ctx, 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
-}