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