golang.org/x/tools/gopls@v0.15.3/internal/test/integration/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 integration
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"net"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"runtime"
    17  	"runtime/pprof"
    18  	"strings"
    19  	"sync"
    20  	"testing"
    21  	"time"
    22  
    23  	"golang.org/x/tools/gopls/internal/cache"
    24  	"golang.org/x/tools/gopls/internal/debug"
    25  	"golang.org/x/tools/gopls/internal/lsprpc"
    26  	"golang.org/x/tools/gopls/internal/protocol"
    27  	"golang.org/x/tools/gopls/internal/settings"
    28  	"golang.org/x/tools/gopls/internal/test/integration/fake"
    29  	"golang.org/x/tools/internal/jsonrpc2"
    30  	"golang.org/x/tools/internal/jsonrpc2/servertest"
    31  	"golang.org/x/tools/internal/memoize"
    32  	"golang.org/x/tools/internal/testenv"
    33  	"golang.org/x/tools/internal/xcontext"
    34  )
    35  
    36  // Mode is a bitmask that defines for which execution modes a test should run.
    37  //
    38  // Each mode controls several aspects of gopls' configuration:
    39  //   - Which server options to use for gopls sessions
    40  //   - Whether to use a shared cache
    41  //   - Whether to use a shared server
    42  //   - Whether to run the server in-process or in a separate process
    43  //
    44  // The behavior of each mode with respect to these aspects is summarized below.
    45  // TODO(rfindley, cleanup): rather than using arbitrary names for these modes,
    46  // we can compose them explicitly out of the features described here, allowing
    47  // individual tests more freedom in constructing problematic execution modes.
    48  // For example, a test could assert on a certain behavior when running with
    49  // experimental options on a separate process. Moreover, we could unify 'Modes'
    50  // with 'Options', and use RunMultiple rather than a hard-coded loop through
    51  // modes.
    52  //
    53  // Mode            | Options      | Shared Cache? | Shared Server? | In-process?
    54  // ---------------------------------------------------------------------------
    55  // Default         | Default      | Y             | N              | Y
    56  // Forwarded       | Default      | Y             | Y              | Y
    57  // SeparateProcess | Default      | Y             | Y              | N
    58  // Experimental    | Experimental | N             | N              | Y
    59  type Mode int
    60  
    61  const (
    62  	// Default mode runs gopls with the default options, communicating over pipes
    63  	// to emulate the lsp sidecar execution mode, which communicates over
    64  	// stdin/stdout.
    65  	//
    66  	// It uses separate servers for each test, but a shared cache, to avoid
    67  	// duplicating work when processing GOROOT.
    68  	Default Mode = 1 << iota
    69  
    70  	// Forwarded uses the default options, but forwards connections to a shared
    71  	// in-process gopls server.
    72  	Forwarded
    73  
    74  	// SeparateProcess uses the default options, but forwards connection to an
    75  	// external gopls daemon.
    76  	//
    77  	// Only supported on GOOS=linux.
    78  	SeparateProcess
    79  
    80  	// Experimental enables all of the experimental configurations that are
    81  	// being developed, and runs gopls in sidecar mode.
    82  	//
    83  	// It uses a separate cache for each test, to exercise races that may only
    84  	// appear with cache misses.
    85  	Experimental
    86  )
    87  
    88  func (m Mode) String() string {
    89  	switch m {
    90  	case Default:
    91  		return "default"
    92  	case Forwarded:
    93  		return "forwarded"
    94  	case SeparateProcess:
    95  		return "separate process"
    96  	case Experimental:
    97  		return "experimental"
    98  	default:
    99  		return "unknown mode"
   100  	}
   101  }
   102  
   103  // A Runner runs tests in gopls execution environments, as specified by its
   104  // modes. For modes that share state (for example, a shared cache or common
   105  // remote), any tests that execute on the same Runner will share the same
   106  // state.
   107  type Runner struct {
   108  	// Configuration
   109  	DefaultModes             Mode                    // modes to run for each test
   110  	Timeout                  time.Duration           // per-test timeout, if set
   111  	PrintGoroutinesOnFailure bool                    // whether to dump goroutines on test failure
   112  	SkipCleanup              bool                    // if set, don't delete test data directories when the test exits
   113  	OptionsHook              func(*settings.Options) // if set, use these options when creating gopls sessions
   114  
   115  	// Immutable state shared across test invocations
   116  	goplsPath string         // path to the gopls executable (for SeparateProcess mode)
   117  	tempDir   string         // shared parent temp directory
   118  	store     *memoize.Store // shared store
   119  
   120  	// Lazily allocated resources
   121  	tsOnce sync.Once
   122  	ts     *servertest.TCPServer // shared in-process test server ("forwarded" mode)
   123  
   124  	startRemoteOnce sync.Once
   125  	remoteSocket    string // unix domain socket for shared daemon ("separate process" mode)
   126  	remoteErr       error
   127  	cancelRemote    func()
   128  }
   129  
   130  type TestFunc func(t *testing.T, env *Env)
   131  
   132  // Run executes the test function in the default configured gopls execution
   133  // modes. For each a test run, a new workspace is created containing the
   134  // un-txtared files specified by filedata.
   135  func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) {
   136  	// TODO(rfindley): this function has gotten overly complicated, and warrants
   137  	// refactoring.
   138  
   139  	if !runFromMain {
   140  		// Main performs various setup precondition checks.
   141  		// While it could theoretically be made OK for a Runner to be used outside
   142  		// of Main, it is simpler to enforce that we only use the Runner from
   143  		// integration test suites.
   144  		t.Fatal("integration.Runner.Run must be run from integration.Main")
   145  	}
   146  
   147  	tests := []struct {
   148  		name      string
   149  		mode      Mode
   150  		getServer func(func(*settings.Options)) jsonrpc2.StreamServer
   151  	}{
   152  		{"default", Default, r.defaultServer},
   153  		{"forwarded", Forwarded, r.forwardedServer},
   154  		{"separate_process", SeparateProcess, r.separateProcessServer},
   155  		{"experimental", Experimental, r.experimentalServer},
   156  	}
   157  
   158  	for _, tc := range tests {
   159  		tc := tc
   160  		config := defaultConfig()
   161  		for _, opt := range opts {
   162  			opt.set(&config)
   163  		}
   164  		modes := r.DefaultModes
   165  		if config.modes != 0 {
   166  			modes = config.modes
   167  		}
   168  		if modes&tc.mode == 0 {
   169  			continue
   170  		}
   171  
   172  		t.Run(tc.name, func(t *testing.T) {
   173  			// TODO(rfindley): once jsonrpc2 shutdown is fixed, we should not leak
   174  			// goroutines in this test function.
   175  			// stacktest.NoLeak(t)
   176  
   177  			ctx := context.Background()
   178  			if r.Timeout != 0 {
   179  				var cancel context.CancelFunc
   180  				ctx, cancel = context.WithTimeout(ctx, r.Timeout)
   181  				defer cancel()
   182  			} else if d, ok := testenv.Deadline(t); ok {
   183  				timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup.
   184  				var cancel context.CancelFunc
   185  				ctx, cancel = context.WithTimeout(ctx, timeout)
   186  				defer cancel()
   187  			}
   188  
   189  			// TODO(rfindley): do we need an instance at all? Can it be removed?
   190  			ctx = debug.WithInstance(ctx, "off")
   191  
   192  			rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name()))
   193  			if err := os.MkdirAll(rootDir, 0755); err != nil {
   194  				t.Fatal(err)
   195  			}
   196  
   197  			files := fake.UnpackTxt(files)
   198  			if config.editor.WindowsLineEndings {
   199  				for name, data := range files {
   200  					files[name] = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n"))
   201  				}
   202  			}
   203  			config.sandbox.Files = files
   204  			config.sandbox.RootDir = rootDir
   205  			sandbox, err := fake.NewSandbox(&config.sandbox)
   206  			if err != nil {
   207  				t.Fatal(err)
   208  			}
   209  			defer func() {
   210  				if !r.SkipCleanup {
   211  					if err := sandbox.Close(); err != nil {
   212  						pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
   213  						t.Errorf("closing the sandbox: %v", err)
   214  					}
   215  				}
   216  			}()
   217  
   218  			ss := tc.getServer(r.OptionsHook)
   219  
   220  			framer := jsonrpc2.NewRawStream
   221  			ls := &loggingFramer{}
   222  			framer = ls.framer(jsonrpc2.NewRawStream)
   223  			ts := servertest.NewPipeServer(ss, framer)
   224  
   225  			awaiter := NewAwaiter(sandbox.Workdir)
   226  			const skipApplyEdits = false
   227  			editor, err := fake.NewEditor(sandbox, config.editor).Connect(ctx, ts, awaiter.Hooks(), skipApplyEdits)
   228  			if err != nil {
   229  				t.Fatal(err)
   230  			}
   231  			env := &Env{
   232  				T:       t,
   233  				Ctx:     ctx,
   234  				Sandbox: sandbox,
   235  				Editor:  editor,
   236  				Server:  ts,
   237  				Awaiter: awaiter,
   238  			}
   239  			defer func() {
   240  				if t.Failed() && r.PrintGoroutinesOnFailure {
   241  					pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
   242  				}
   243  				if t.Failed() || *printLogs {
   244  					ls.printBuffers(t.Name(), os.Stderr)
   245  				}
   246  				// For tests that failed due to a timeout, don't fail to shutdown
   247  				// because ctx is done.
   248  				//
   249  				// There is little point to setting an arbitrary timeout for closing
   250  				// the editor: in general we want to clean up before proceeding to the
   251  				// next test, and if there is a deadlock preventing closing it will
   252  				// eventually be handled by the `go test` timeout.
   253  				if err := editor.Close(xcontext.Detach(ctx)); err != nil {
   254  					t.Errorf("closing editor: %v", err)
   255  				}
   256  			}()
   257  			// Always await the initial workspace load.
   258  			env.Await(InitialWorkspaceLoad)
   259  			test(t, env)
   260  		})
   261  	}
   262  }
   263  
   264  // longBuilders maps builders that are skipped when -short is set to a
   265  // (possibly empty) justification.
   266  var longBuilders = map[string]string{
   267  	"openbsd-amd64-64":        "golang.org/issues/42789",
   268  	"openbsd-386-64":          "golang.org/issues/42789",
   269  	"openbsd-386-68":          "golang.org/issues/42789",
   270  	"openbsd-amd64-68":        "golang.org/issues/42789",
   271  	"darwin-amd64-10_12":      "",
   272  	"freebsd-amd64-race":      "",
   273  	"illumos-amd64":           "",
   274  	"netbsd-arm-bsiegert":     "",
   275  	"solaris-amd64-oraclerel": "",
   276  	"windows-arm-zx2c4":       "",
   277  }
   278  
   279  // TODO(rfindley): inline into Main.
   280  func checkBuilder() string {
   281  	builder := os.Getenv("GO_BUILDER_NAME")
   282  	if reason, ok := longBuilders[builder]; ok && testing.Short() {
   283  		if reason != "" {
   284  			return fmt.Sprintf("skipping %s with -short due to %s", builder, reason)
   285  		} else {
   286  			return fmt.Sprintf("skipping %s with -short", builder)
   287  		}
   288  	}
   289  	return ""
   290  }
   291  
   292  type loggingFramer struct {
   293  	mu  sync.Mutex
   294  	buf *safeBuffer
   295  }
   296  
   297  // safeBuffer is a threadsafe buffer for logs.
   298  type safeBuffer struct {
   299  	mu  sync.Mutex
   300  	buf bytes.Buffer
   301  }
   302  
   303  func (b *safeBuffer) Write(p []byte) (int, error) {
   304  	b.mu.Lock()
   305  	defer b.mu.Unlock()
   306  	return b.buf.Write(p)
   307  }
   308  
   309  func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer {
   310  	return func(nc net.Conn) jsonrpc2.Stream {
   311  		s.mu.Lock()
   312  		framed := false
   313  		if s.buf == nil {
   314  			s.buf = &safeBuffer{buf: bytes.Buffer{}}
   315  			framed = true
   316  		}
   317  		s.mu.Unlock()
   318  		stream := f(nc)
   319  		if framed {
   320  			return protocol.LoggingStream(stream, s.buf)
   321  		}
   322  		return stream
   323  	}
   324  }
   325  
   326  func (s *loggingFramer) printBuffers(testname string, w io.Writer) {
   327  	s.mu.Lock()
   328  	defer s.mu.Unlock()
   329  
   330  	if s.buf == nil {
   331  		return
   332  	}
   333  	fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs for %q\n", testname)
   334  	s.buf.mu.Lock()
   335  	io.Copy(w, &s.buf.buf)
   336  	s.buf.mu.Unlock()
   337  	fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname)
   338  }
   339  
   340  // defaultServer handles the Default execution mode.
   341  func (r *Runner) defaultServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer {
   342  	return lsprpc.NewStreamServer(cache.New(r.store), false, optsHook)
   343  }
   344  
   345  // experimentalServer handles the Experimental execution mode.
   346  func (r *Runner) experimentalServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer {
   347  	options := func(o *settings.Options) {
   348  		optsHook(o)
   349  		o.EnableAllExperiments()
   350  	}
   351  	return lsprpc.NewStreamServer(cache.New(nil), false, options)
   352  }
   353  
   354  // forwardedServer handles the Forwarded execution mode.
   355  func (r *Runner) forwardedServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer {
   356  	r.tsOnce.Do(func() {
   357  		ctx := context.Background()
   358  		ctx = debug.WithInstance(ctx, "off")
   359  		ss := lsprpc.NewStreamServer(cache.New(nil), false, optsHook)
   360  		r.ts = servertest.NewTCPServer(ctx, ss, nil)
   361  	})
   362  	return newForwarder("tcp", r.ts.Addr)
   363  }
   364  
   365  // runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running
   366  // tests. It's a trick to allow tests to find a binary to use to start a gopls
   367  // subprocess.
   368  const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS"
   369  
   370  // separateProcessServer handles the SeparateProcess execution mode.
   371  func (r *Runner) separateProcessServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer {
   372  	if runtime.GOOS != "linux" {
   373  		panic("separate process execution mode is only supported on linux")
   374  	}
   375  
   376  	r.startRemoteOnce.Do(func() {
   377  		socketDir, err := os.MkdirTemp(r.tempDir, "gopls-test-socket")
   378  		if err != nil {
   379  			r.remoteErr = err
   380  			return
   381  		}
   382  		r.remoteSocket = filepath.Join(socketDir, "gopls-test-daemon")
   383  
   384  		// The server should be killed by when the test runner exits, but to be
   385  		// conservative also set a listen timeout.
   386  		args := []string{"serve", "-listen", "unix;" + r.remoteSocket, "-listen.timeout", "1m"}
   387  
   388  		ctx, cancel := context.WithCancel(context.Background())
   389  		cmd := exec.CommandContext(ctx, r.goplsPath, args...)
   390  		cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true")
   391  
   392  		// Start the external gopls process. This is still somewhat racy, as we
   393  		// don't know when gopls binds to the socket, but the gopls forwarder
   394  		// client has built-in retry behavior that should mostly mitigate this
   395  		// problem (and if it doesn't, we probably want to improve the retry
   396  		// behavior).
   397  		if err := cmd.Start(); err != nil {
   398  			cancel()
   399  			r.remoteSocket = ""
   400  			r.remoteErr = err
   401  		} else {
   402  			r.cancelRemote = cancel
   403  			// Spin off a goroutine to wait, so that we free up resources when the
   404  			// server exits.
   405  			go cmd.Wait()
   406  		}
   407  	})
   408  
   409  	return newForwarder("unix", r.remoteSocket)
   410  }
   411  
   412  func newForwarder(network, address string) jsonrpc2.StreamServer {
   413  	server, err := lsprpc.NewForwarder(network+";"+address, nil)
   414  	if err != nil {
   415  		// This should never happen, as we are passing an explicit address.
   416  		panic(fmt.Sprintf("internal error: unable to create forwarder: %v", err))
   417  	}
   418  	return server
   419  }
   420  
   421  // Close cleans up resource that have been allocated to this workspace.
   422  func (r *Runner) Close() error {
   423  	var errmsgs []string
   424  	if r.ts != nil {
   425  		if err := r.ts.Close(); err != nil {
   426  			errmsgs = append(errmsgs, err.Error())
   427  		}
   428  	}
   429  	if r.cancelRemote != nil {
   430  		r.cancelRemote()
   431  	}
   432  	if !r.SkipCleanup {
   433  		if err := os.RemoveAll(r.tempDir); err != nil {
   434  			errmsgs = append(errmsgs, err.Error())
   435  		}
   436  	}
   437  	if len(errmsgs) > 0 {
   438  		return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t"))
   439  	}
   440  	return nil
   441  }