github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/regtest/runner.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package regtest
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net"
    14  	"os"
    15  	"path/filepath"
    16  	"runtime/pprof"
    17  	"strings"
    18  	"sync"
    19  	"testing"
    20  	"time"
    21  
    22  	exec "golang.org/x/sys/execabs"
    23  
    24  	"github.com/jhump/golang-x-tools/internal/jsonrpc2"
    25  	"github.com/jhump/golang-x-tools/internal/jsonrpc2/servertest"
    26  	"github.com/jhump/golang-x-tools/internal/lsp/cache"
    27  	"github.com/jhump/golang-x-tools/internal/lsp/debug"
    28  	"github.com/jhump/golang-x-tools/internal/lsp/fake"
    29  	"github.com/jhump/golang-x-tools/internal/lsp/lsprpc"
    30  	"github.com/jhump/golang-x-tools/internal/lsp/protocol"
    31  	"github.com/jhump/golang-x-tools/internal/lsp/source"
    32  	"github.com/jhump/golang-x-tools/internal/testenv"
    33  	"github.com/jhump/golang-x-tools/internal/xcontext"
    34  )
    35  
    36  // Mode is a bitmask that defines for which execution modes a test should run.
    37  type Mode int
    38  
    39  const (
    40  	// Singleton mode uses a separate in-process gopls instance for each test,
    41  	// and communicates over pipes to mimic the gopls sidecar execution mode,
    42  	// which communicates over stdin/stderr.
    43  	Singleton Mode = 1 << iota
    44  	// Forwarded forwards connections to a shared in-process gopls instance.
    45  	Forwarded
    46  	// SeparateProcess forwards connection to a shared separate gopls process.
    47  	SeparateProcess
    48  	// Experimental enables all of the experimental configurations that are
    49  	// being developed.
    50  	Experimental
    51  )
    52  
    53  // A Runner runs tests in gopls execution environments, as specified by its
    54  // modes. For modes that share state (for example, a shared cache or common
    55  // remote), any tests that execute on the same Runner will share the same
    56  // state.
    57  type Runner struct {
    58  	DefaultModes             Mode
    59  	Timeout                  time.Duration
    60  	GoplsPath                string
    61  	PrintGoroutinesOnFailure bool
    62  	TempDir                  string
    63  	SkipCleanup              bool
    64  	OptionsHook              func(*source.Options)
    65  
    66  	mu        sync.Mutex
    67  	ts        *servertest.TCPServer
    68  	socketDir string
    69  	// closers is a queue of clean-up functions to run at the end of the entire
    70  	// test suite.
    71  	closers []io.Closer
    72  }
    73  
    74  type runConfig struct {
    75  	editor           fake.EditorConfig
    76  	sandbox          fake.SandboxConfig
    77  	modes            Mode
    78  	noDefaultTimeout bool
    79  	debugAddr        string
    80  	skipLogs         bool
    81  	skipHooks        bool
    82  	optionsHook      func(*source.Options)
    83  }
    84  
    85  func (r *Runner) defaultConfig() *runConfig {
    86  	return &runConfig{
    87  		modes:       r.DefaultModes,
    88  		optionsHook: r.OptionsHook,
    89  	}
    90  }
    91  
    92  // A RunOption augments the behavior of the test runner.
    93  type RunOption interface {
    94  	set(*runConfig)
    95  }
    96  
    97  type optionSetter func(*runConfig)
    98  
    99  func (f optionSetter) set(opts *runConfig) {
   100  	f(opts)
   101  }
   102  
   103  // NoDefaultTimeout removes the timeout set by the -regtest_timeout flag, for
   104  // individual tests that are expected to run longer than is reasonable for
   105  // ordinary regression tests.
   106  func NoDefaultTimeout() RunOption {
   107  	return optionSetter(func(opts *runConfig) {
   108  		opts.noDefaultTimeout = true
   109  	})
   110  }
   111  
   112  // ProxyFiles configures a file proxy using the given txtar-encoded string.
   113  func ProxyFiles(txt string) RunOption {
   114  	return optionSetter(func(opts *runConfig) {
   115  		opts.sandbox.ProxyFiles = fake.UnpackTxt(txt)
   116  	})
   117  }
   118  
   119  // Modes configures the execution modes that the test should run in.
   120  func Modes(modes Mode) RunOption {
   121  	return optionSetter(func(opts *runConfig) {
   122  		opts.modes = modes
   123  	})
   124  }
   125  
   126  // Options configures the various server and user options.
   127  func Options(hook func(*source.Options)) RunOption {
   128  	return optionSetter(func(opts *runConfig) {
   129  		old := opts.optionsHook
   130  		opts.optionsHook = func(o *source.Options) {
   131  			if old != nil {
   132  				old(o)
   133  			}
   134  			hook(o)
   135  		}
   136  	})
   137  }
   138  
   139  func SendPID() RunOption {
   140  	return optionSetter(func(opts *runConfig) {
   141  		opts.editor.SendPID = true
   142  	})
   143  }
   144  
   145  // EditorConfig is a RunOption option that configured the regtest editor.
   146  type EditorConfig fake.EditorConfig
   147  
   148  func (c EditorConfig) set(opts *runConfig) {
   149  	opts.editor = fake.EditorConfig(c)
   150  }
   151  
   152  // WorkspaceFolders configures the workdir-relative workspace folders to send
   153  // to the LSP server. By default the editor sends a single workspace folder
   154  // corresponding to the workdir root. To explicitly configure no workspace
   155  // folders, use WorkspaceFolders with no arguments.
   156  func WorkspaceFolders(relFolders ...string) RunOption {
   157  	if len(relFolders) == 0 {
   158  		// Use an empty non-nil slice to signal explicitly no folders.
   159  		relFolders = []string{}
   160  	}
   161  	return optionSetter(func(opts *runConfig) {
   162  		opts.editor.WorkspaceFolders = relFolders
   163  	})
   164  }
   165  
   166  // InGOPATH configures the workspace working directory to be GOPATH, rather
   167  // than a separate working directory for use with modules.
   168  func InGOPATH() RunOption {
   169  	return optionSetter(func(opts *runConfig) {
   170  		opts.sandbox.InGoPath = true
   171  	})
   172  }
   173  
   174  // DebugAddress configures a debug server bound to addr. This option is
   175  // currently only supported when executing in Singleton mode. It is intended to
   176  // be used for long-running stress tests.
   177  func DebugAddress(addr string) RunOption {
   178  	return optionSetter(func(opts *runConfig) {
   179  		opts.debugAddr = addr
   180  	})
   181  }
   182  
   183  // SkipLogs skips the buffering of logs during test execution. It is intended
   184  // for long-running stress tests.
   185  func SkipLogs() RunOption {
   186  	return optionSetter(func(opts *runConfig) {
   187  		opts.skipLogs = true
   188  	})
   189  }
   190  
   191  // InExistingDir runs the test in a pre-existing directory. If set, no initial
   192  // files may be passed to the runner. It is intended for long-running stress
   193  // tests.
   194  func InExistingDir(dir string) RunOption {
   195  	return optionSetter(func(opts *runConfig) {
   196  		opts.sandbox.Workdir = dir
   197  	})
   198  }
   199  
   200  // SkipHooks allows for disabling the test runner's client hooks that are used
   201  // for instrumenting expectations (tracking diagnostics, logs, work done,
   202  // etc.). It is intended for performance-sensitive stress tests or benchmarks.
   203  func SkipHooks(skip bool) RunOption {
   204  	return optionSetter(func(opts *runConfig) {
   205  		opts.skipHooks = skip
   206  	})
   207  }
   208  
   209  // GOPROXY configures the test environment to have an explicit proxy value.
   210  // This is intended for stress tests -- to ensure their isolation, regtests
   211  // should instead use WithProxyFiles.
   212  func GOPROXY(goproxy string) RunOption {
   213  	return optionSetter(func(opts *runConfig) {
   214  		opts.sandbox.GOPROXY = goproxy
   215  	})
   216  }
   217  
   218  // LimitWorkspaceScope sets the LimitWorkspaceScope configuration.
   219  func LimitWorkspaceScope() RunOption {
   220  	return optionSetter(func(opts *runConfig) {
   221  		opts.editor.LimitWorkspaceScope = true
   222  	})
   223  }
   224  
   225  type TestFunc func(t *testing.T, env *Env)
   226  
   227  // Run executes the test function in the default configured gopls execution
   228  // modes. For each a test run, a new workspace is created containing the
   229  // un-txtared files specified by filedata.
   230  func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) {
   231  	t.Helper()
   232  	checkBuilder(t)
   233  
   234  	tests := []struct {
   235  		name      string
   236  		mode      Mode
   237  		getServer func(context.Context, *testing.T, func(*source.Options)) jsonrpc2.StreamServer
   238  	}{
   239  		{"singleton", Singleton, singletonServer},
   240  		{"forwarded", Forwarded, r.forwardedServer},
   241  		{"separate_process", SeparateProcess, r.separateProcessServer},
   242  		{"experimental", Experimental, experimentalServer},
   243  	}
   244  
   245  	for _, tc := range tests {
   246  		tc := tc
   247  		config := r.defaultConfig()
   248  		for _, opt := range opts {
   249  			opt.set(config)
   250  		}
   251  		if config.modes&tc.mode == 0 {
   252  			continue
   253  		}
   254  		if config.debugAddr != "" && tc.mode != Singleton {
   255  			// Debugging is useful for running stress tests, but since the daemon has
   256  			// likely already been started, it would be too late to debug.
   257  			t.Fatalf("debugging regtest servers only works in Singleton mode, "+
   258  				"got debug addr %q and mode %v", config.debugAddr, tc.mode)
   259  		}
   260  
   261  		t.Run(tc.name, func(t *testing.T) {
   262  			ctx := context.Background()
   263  			if r.Timeout != 0 && !config.noDefaultTimeout {
   264  				var cancel context.CancelFunc
   265  				ctx, cancel = context.WithTimeout(ctx, r.Timeout)
   266  				defer cancel()
   267  			} else if d, ok := testenv.Deadline(t); ok {
   268  				timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup.
   269  				var cancel context.CancelFunc
   270  				ctx, cancel = context.WithTimeout(ctx, timeout)
   271  				defer cancel()
   272  			}
   273  
   274  			ctx = debug.WithInstance(ctx, "", "off")
   275  			if config.debugAddr != "" {
   276  				di := debug.GetInstance(ctx)
   277  				di.Serve(ctx, config.debugAddr)
   278  				di.MonitorMemory(ctx)
   279  			}
   280  
   281  			rootDir := filepath.Join(r.TempDir, filepath.FromSlash(t.Name()))
   282  			if err := os.MkdirAll(rootDir, 0755); err != nil {
   283  				t.Fatal(err)
   284  			}
   285  			files := fake.UnpackTxt(files)
   286  			if config.editor.WindowsLineEndings {
   287  				for name, data := range files {
   288  					files[name] = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n"))
   289  				}
   290  			}
   291  			config.sandbox.Files = files
   292  			config.sandbox.RootDir = rootDir
   293  			sandbox, err := fake.NewSandbox(&config.sandbox)
   294  			if err != nil {
   295  				t.Fatal(err)
   296  			}
   297  			// Deferring the closure of ws until the end of the entire test suite
   298  			// has, in testing, given the LSP server time to properly shutdown and
   299  			// release any file locks held in workspace, which is a problem on
   300  			// Windows. This may still be flaky however, and in the future we need a
   301  			// better solution to ensure that all Go processes started by gopls have
   302  			// exited before we clean up.
   303  			r.AddCloser(sandbox)
   304  			ss := tc.getServer(ctx, t, config.optionsHook)
   305  			framer := jsonrpc2.NewRawStream
   306  			ls := &loggingFramer{}
   307  			if !config.skipLogs {
   308  				framer = ls.framer(jsonrpc2.NewRawStream)
   309  			}
   310  			ts := servertest.NewPipeServer(ctx, ss, framer)
   311  			env := NewEnv(ctx, t, sandbox, ts, config.editor, !config.skipHooks)
   312  			defer func() {
   313  				if t.Failed() && r.PrintGoroutinesOnFailure {
   314  					pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
   315  				}
   316  				if t.Failed() || testing.Verbose() {
   317  					ls.printBuffers(t.Name(), os.Stderr)
   318  				}
   319  				// For tests that failed due to a timeout, don't fail to shutdown
   320  				// because ctx is done.
   321  				closeCtx, cancel := context.WithTimeout(xcontext.Detach(ctx), 5*time.Second)
   322  				defer cancel()
   323  				if err := env.Editor.Close(closeCtx); err != nil {
   324  					t.Errorf("closing editor: %v", err)
   325  				}
   326  			}()
   327  			// Always await the initial workspace load.
   328  			env.Await(InitialWorkspaceLoad)
   329  			test(t, env)
   330  		})
   331  	}
   332  }
   333  
   334  // longBuilders maps builders that are skipped when -short is set to a
   335  // (possibly empty) justification.
   336  var longBuilders = map[string]string{
   337  	"openbsd-amd64-64":        "golang.org/issues/42789",
   338  	"openbsd-386-64":          "golang.org/issues/42789",
   339  	"openbsd-386-68":          "golang.org/issues/42789",
   340  	"openbsd-amd64-68":        "golang.org/issues/42789",
   341  	"darwin-amd64-10_12":      "",
   342  	"freebsd-amd64-race":      "",
   343  	"illumos-amd64":           "",
   344  	"netbsd-arm-bsiegert":     "",
   345  	"solaris-amd64-oraclerel": "",
   346  	"windows-arm-zx2c4":       "",
   347  }
   348  
   349  func checkBuilder(t *testing.T) {
   350  	t.Helper()
   351  	builder := os.Getenv("GO_BUILDER_NAME")
   352  	if reason, ok := longBuilders[builder]; ok && testing.Short() {
   353  		if reason != "" {
   354  			t.Skipf("Skipping %s with -short due to %s", builder, reason)
   355  		} else {
   356  			t.Skipf("Skipping %s with -short", builder)
   357  		}
   358  	}
   359  }
   360  
   361  type loggingFramer struct {
   362  	mu  sync.Mutex
   363  	buf *safeBuffer
   364  }
   365  
   366  // safeBuffer is a threadsafe buffer for logs.
   367  type safeBuffer struct {
   368  	mu  sync.Mutex
   369  	buf bytes.Buffer
   370  }
   371  
   372  func (b *safeBuffer) Write(p []byte) (int, error) {
   373  	b.mu.Lock()
   374  	defer b.mu.Unlock()
   375  	return b.buf.Write(p)
   376  }
   377  
   378  func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer {
   379  	return func(nc net.Conn) jsonrpc2.Stream {
   380  		s.mu.Lock()
   381  		framed := false
   382  		if s.buf == nil {
   383  			s.buf = &safeBuffer{buf: bytes.Buffer{}}
   384  			framed = true
   385  		}
   386  		s.mu.Unlock()
   387  		stream := f(nc)
   388  		if framed {
   389  			return protocol.LoggingStream(stream, s.buf)
   390  		}
   391  		return stream
   392  	}
   393  }
   394  
   395  func (s *loggingFramer) printBuffers(testname string, w io.Writer) {
   396  	s.mu.Lock()
   397  	defer s.mu.Unlock()
   398  
   399  	if s.buf == nil {
   400  		return
   401  	}
   402  	fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs for %q\n", testname)
   403  	s.buf.mu.Lock()
   404  	io.Copy(w, &s.buf.buf)
   405  	s.buf.mu.Unlock()
   406  	fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname)
   407  }
   408  
   409  func singletonServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer {
   410  	return lsprpc.NewStreamServer(cache.New(optsHook), false)
   411  }
   412  
   413  func experimentalServer(_ context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer {
   414  	options := func(o *source.Options) {
   415  		optsHook(o)
   416  		o.EnableAllExperiments()
   417  		// ExperimentalWorkspaceModule is not (as of writing) enabled by
   418  		// source.Options.EnableAllExperiments, but we want to test it.
   419  		o.ExperimentalWorkspaceModule = true
   420  	}
   421  	return lsprpc.NewStreamServer(cache.New(options), false)
   422  }
   423  
   424  func (r *Runner) forwardedServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer {
   425  	ts := r.getTestServer(optsHook)
   426  	return newForwarder("tcp", ts.Addr)
   427  }
   428  
   429  // getTestServer gets the shared test server instance to connect to, or creates
   430  // one if it doesn't exist.
   431  func (r *Runner) getTestServer(optsHook func(*source.Options)) *servertest.TCPServer {
   432  	r.mu.Lock()
   433  	defer r.mu.Unlock()
   434  	if r.ts == nil {
   435  		ctx := context.Background()
   436  		ctx = debug.WithInstance(ctx, "", "off")
   437  		ss := lsprpc.NewStreamServer(cache.New(optsHook), false)
   438  		r.ts = servertest.NewTCPServer(ctx, ss, nil)
   439  	}
   440  	return r.ts
   441  }
   442  
   443  func (r *Runner) separateProcessServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer {
   444  	// TODO(rfindley): can we use the autostart behavior here, instead of
   445  	// pre-starting the remote?
   446  	socket := r.getRemoteSocket(t)
   447  	return newForwarder("unix", socket)
   448  }
   449  
   450  func newForwarder(network, address string) *lsprpc.Forwarder {
   451  	server, err := lsprpc.NewForwarder(network+";"+address, nil)
   452  	if err != nil {
   453  		// This should never happen, as we are passing an explicit address.
   454  		panic(fmt.Sprintf("internal error: unable to create forwarder: %v", err))
   455  	}
   456  	return server
   457  }
   458  
   459  // runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running
   460  // tests. It's a trick to allow tests to find a binary to use to start a gopls
   461  // subprocess.
   462  const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS"
   463  
   464  func (r *Runner) getRemoteSocket(t *testing.T) string {
   465  	t.Helper()
   466  	r.mu.Lock()
   467  	defer r.mu.Unlock()
   468  	const daemonFile = "gopls-test-daemon"
   469  	if r.socketDir != "" {
   470  		return filepath.Join(r.socketDir, daemonFile)
   471  	}
   472  
   473  	if r.GoplsPath == "" {
   474  		t.Fatal("cannot run tests with a separate process unless a path to a gopls binary is configured")
   475  	}
   476  	var err error
   477  	r.socketDir, err = ioutil.TempDir(r.TempDir, "gopls-regtest-socket")
   478  	if err != nil {
   479  		t.Fatalf("creating tempdir: %v", err)
   480  	}
   481  	socket := filepath.Join(r.socketDir, daemonFile)
   482  	args := []string{"serve", "-listen", "unix;" + socket, "-listen.timeout", "10s"}
   483  	cmd := exec.Command(r.GoplsPath, args...)
   484  	cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true")
   485  	var stderr bytes.Buffer
   486  	cmd.Stderr = &stderr
   487  	go func() {
   488  		if err := cmd.Run(); err != nil {
   489  			panic(fmt.Sprintf("error running external gopls: %v\nstderr:\n%s", err, stderr.String()))
   490  		}
   491  	}()
   492  	return socket
   493  }
   494  
   495  // AddCloser schedules a closer to be closed at the end of the test run. This
   496  // is useful for Windows in particular, as
   497  func (r *Runner) AddCloser(closer io.Closer) {
   498  	r.mu.Lock()
   499  	defer r.mu.Unlock()
   500  	r.closers = append(r.closers, closer)
   501  }
   502  
   503  // Close cleans up resource that have been allocated to this workspace.
   504  func (r *Runner) Close() error {
   505  	r.mu.Lock()
   506  	defer r.mu.Unlock()
   507  
   508  	var errmsgs []string
   509  	if r.ts != nil {
   510  		if err := r.ts.Close(); err != nil {
   511  			errmsgs = append(errmsgs, err.Error())
   512  		}
   513  	}
   514  	if r.socketDir != "" {
   515  		if err := os.RemoveAll(r.socketDir); err != nil {
   516  			errmsgs = append(errmsgs, err.Error())
   517  		}
   518  	}
   519  	if !r.SkipCleanup {
   520  		for _, closer := range r.closers {
   521  			if err := closer.Close(); err != nil {
   522  				errmsgs = append(errmsgs, err.Error())
   523  			}
   524  		}
   525  		if err := os.RemoveAll(r.TempDir); err != nil {
   526  			errmsgs = append(errmsgs, err.Error())
   527  		}
   528  	}
   529  	if len(errmsgs) > 0 {
   530  		return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t"))
   531  	}
   532  	return nil
   533  }