cuelang.org/go@v0.10.1/internal/golangorgx/gopls/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  	"cuelang.org/go/internal/golangorgx/gopls/cache"
    24  	"cuelang.org/go/internal/golangorgx/gopls/debug"
    25  	"cuelang.org/go/internal/golangorgx/gopls/lsprpc"
    26  	"cuelang.org/go/internal/golangorgx/gopls/protocol"
    27  	"cuelang.org/go/internal/golangorgx/gopls/settings"
    28  	"cuelang.org/go/internal/golangorgx/gopls/test/integration/fake"
    29  	"cuelang.org/go/internal/golangorgx/tools/jsonrpc2"
    30  	"cuelang.org/go/internal/golangorgx/tools/jsonrpc2/servertest"
    31  	"cuelang.org/go/internal/golangorgx/tools/memoize"
    32  	"cuelang.org/go/internal/golangorgx/tools/testenv"
    33  	"cuelang.org/go/internal/golangorgx/tools/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  	t.Helper()
   139  	checkBuilder(t)
   140  	testenv.NeedsGoPackages(t)
   141  
   142  	tests := []struct {
   143  		name      string
   144  		mode      Mode
   145  		getServer func(func(*settings.Options)) jsonrpc2.StreamServer
   146  	}{
   147  		{"default", Default, r.defaultServer},
   148  		{"forwarded", Forwarded, r.forwardedServer},
   149  		{"separate_process", SeparateProcess, r.separateProcessServer},
   150  		{"experimental", Experimental, r.experimentalServer},
   151  	}
   152  
   153  	for _, tc := range tests {
   154  		tc := tc
   155  		config := defaultConfig()
   156  		for _, opt := range opts {
   157  			opt.set(&config)
   158  		}
   159  		modes := r.DefaultModes
   160  		if config.modes != 0 {
   161  			modes = config.modes
   162  		}
   163  		if modes&tc.mode == 0 {
   164  			continue
   165  		}
   166  
   167  		t.Run(tc.name, func(t *testing.T) {
   168  			// TODO(rfindley): once jsonrpc2 shutdown is fixed, we should not leak
   169  			// goroutines in this test function.
   170  			// stacktest.NoLeak(t)
   171  
   172  			ctx := context.Background()
   173  			if r.Timeout != 0 {
   174  				var cancel context.CancelFunc
   175  				ctx, cancel = context.WithTimeout(ctx, r.Timeout)
   176  				defer cancel()
   177  			} else if d, ok := testenv.Deadline(t); ok {
   178  				timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup.
   179  				var cancel context.CancelFunc
   180  				ctx, cancel = context.WithTimeout(ctx, timeout)
   181  				defer cancel()
   182  			}
   183  
   184  			// TODO(rfindley): do we need an instance at all? Can it be removed?
   185  			ctx = debug.WithInstance(ctx, "off")
   186  
   187  			rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name()))
   188  			if err := os.MkdirAll(rootDir, 0755); err != nil {
   189  				t.Fatal(err)
   190  			}
   191  
   192  			files := fake.UnpackTxt(files)
   193  			if config.editor.WindowsLineEndings {
   194  				for name, data := range files {
   195  					files[name] = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n"))
   196  				}
   197  			}
   198  			config.sandbox.Files = files
   199  			config.sandbox.RootDir = rootDir
   200  			sandbox, err := fake.NewSandbox(&config.sandbox)
   201  			if err != nil {
   202  				t.Fatal(err)
   203  			}
   204  			defer func() {
   205  				if !r.SkipCleanup {
   206  					if err := sandbox.Close(); err != nil {
   207  						pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
   208  						t.Errorf("closing the sandbox: %v", err)
   209  					}
   210  				}
   211  			}()
   212  
   213  			ss := tc.getServer(r.OptionsHook)
   214  
   215  			framer := jsonrpc2.NewRawStream
   216  			ls := &loggingFramer{}
   217  			framer = ls.framer(jsonrpc2.NewRawStream)
   218  			ts := servertest.NewPipeServer(ss, framer)
   219  
   220  			awaiter := NewAwaiter(sandbox.Workdir)
   221  			const skipApplyEdits = false
   222  			editor, err := fake.NewEditor(sandbox, config.editor).Connect(ctx, ts, awaiter.Hooks(), skipApplyEdits)
   223  			if err != nil {
   224  				t.Fatal(err)
   225  			}
   226  			env := &Env{
   227  				T:       t,
   228  				Ctx:     ctx,
   229  				Sandbox: sandbox,
   230  				Editor:  editor,
   231  				Server:  ts,
   232  				Awaiter: awaiter,
   233  			}
   234  			defer func() {
   235  				if t.Failed() && r.PrintGoroutinesOnFailure {
   236  					pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
   237  				}
   238  				if t.Failed() || *printLogs {
   239  					ls.printBuffers(t.Name(), os.Stderr)
   240  				}
   241  				// For tests that failed due to a timeout, don't fail to shutdown
   242  				// because ctx is done.
   243  				//
   244  				// There is little point to setting an arbitrary timeout for closing
   245  				// the editor: in general we want to clean up before proceeding to the
   246  				// next test, and if there is a deadlock preventing closing it will
   247  				// eventually be handled by the `go test` timeout.
   248  				if err := editor.Close(xcontext.Detach(ctx)); err != nil {
   249  					t.Errorf("closing editor: %v", err)
   250  				}
   251  			}()
   252  			// Always await the initial workspace load.
   253  			env.Await(InitialWorkspaceLoad)
   254  			test(t, env)
   255  		})
   256  	}
   257  }
   258  
   259  // longBuilders maps builders that are skipped when -short is set to a
   260  // (possibly empty) justification.
   261  var longBuilders = map[string]string{
   262  	"openbsd-amd64-64":        "golang.org/issues/42789",
   263  	"openbsd-386-64":          "golang.org/issues/42789",
   264  	"openbsd-386-68":          "golang.org/issues/42789",
   265  	"openbsd-amd64-68":        "golang.org/issues/42789",
   266  	"darwin-amd64-10_12":      "",
   267  	"freebsd-amd64-race":      "",
   268  	"illumos-amd64":           "",
   269  	"netbsd-arm-bsiegert":     "",
   270  	"solaris-amd64-oraclerel": "",
   271  	"windows-arm-zx2c4":       "",
   272  }
   273  
   274  func checkBuilder(t *testing.T) {
   275  	t.Helper()
   276  	builder := os.Getenv("GO_BUILDER_NAME")
   277  	if reason, ok := longBuilders[builder]; ok && testing.Short() {
   278  		if reason != "" {
   279  			t.Skipf("Skipping %s with -short due to %s", builder, reason)
   280  		} else {
   281  			t.Skipf("Skipping %s with -short", builder)
   282  		}
   283  	}
   284  }
   285  
   286  type loggingFramer struct {
   287  	mu  sync.Mutex
   288  	buf *safeBuffer
   289  }
   290  
   291  // safeBuffer is a threadsafe buffer for logs.
   292  type safeBuffer struct {
   293  	mu  sync.Mutex
   294  	buf bytes.Buffer
   295  }
   296  
   297  func (b *safeBuffer) Write(p []byte) (int, error) {
   298  	b.mu.Lock()
   299  	defer b.mu.Unlock()
   300  	return b.buf.Write(p)
   301  }
   302  
   303  func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer {
   304  	return func(nc net.Conn) jsonrpc2.Stream {
   305  		s.mu.Lock()
   306  		framed := false
   307  		if s.buf == nil {
   308  			s.buf = &safeBuffer{buf: bytes.Buffer{}}
   309  			framed = true
   310  		}
   311  		s.mu.Unlock()
   312  		stream := f(nc)
   313  		if framed {
   314  			return protocol.LoggingStream(stream, s.buf)
   315  		}
   316  		return stream
   317  	}
   318  }
   319  
   320  func (s *loggingFramer) printBuffers(testname string, w io.Writer) {
   321  	s.mu.Lock()
   322  	defer s.mu.Unlock()
   323  
   324  	if s.buf == nil {
   325  		return
   326  	}
   327  	fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs for %q\n", testname)
   328  	s.buf.mu.Lock()
   329  	io.Copy(w, &s.buf.buf)
   330  	s.buf.mu.Unlock()
   331  	fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname)
   332  }
   333  
   334  // defaultServer handles the Default execution mode.
   335  func (r *Runner) defaultServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer {
   336  	return lsprpc.NewStreamServer(cache.New(r.store), false, optsHook)
   337  }
   338  
   339  // experimentalServer handles the Experimental execution mode.
   340  func (r *Runner) experimentalServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer {
   341  	options := func(o *settings.Options) {
   342  		optsHook(o)
   343  		o.EnableAllExperiments()
   344  	}
   345  	return lsprpc.NewStreamServer(cache.New(nil), false, options)
   346  }
   347  
   348  // forwardedServer handles the Forwarded execution mode.
   349  func (r *Runner) forwardedServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer {
   350  	r.tsOnce.Do(func() {
   351  		ctx := context.Background()
   352  		ctx = debug.WithInstance(ctx, "off")
   353  		ss := lsprpc.NewStreamServer(cache.New(nil), false, optsHook)
   354  		r.ts = servertest.NewTCPServer(ctx, ss, nil)
   355  	})
   356  	return newForwarder("tcp", r.ts.Addr)
   357  }
   358  
   359  // runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running
   360  // tests. It's a trick to allow tests to find a binary to use to start a gopls
   361  // subprocess.
   362  const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS"
   363  
   364  // separateProcessServer handles the SeparateProcess execution mode.
   365  func (r *Runner) separateProcessServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer {
   366  	if runtime.GOOS != "linux" {
   367  		panic("separate process execution mode is only supported on linux")
   368  	}
   369  
   370  	r.startRemoteOnce.Do(func() {
   371  		socketDir, err := os.MkdirTemp(r.tempDir, "cuepls-test-socket")
   372  		if err != nil {
   373  			r.remoteErr = err
   374  			return
   375  		}
   376  		r.remoteSocket = filepath.Join(socketDir, "cuepls-test-daemon")
   377  
   378  		// The server should be killed by when the test runner exits, but to be
   379  		// conservative also set a listen timeout.
   380  		args := []string{"serve", "-listen", "unix;" + r.remoteSocket, "-listen.timeout", "1m"}
   381  
   382  		ctx, cancel := context.WithCancel(context.Background())
   383  		cmd := exec.CommandContext(ctx, r.goplsPath, args...)
   384  		cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true")
   385  
   386  		// Start the external gopls process. This is still somewhat racy, as we
   387  		// don't know when gopls binds to the socket, but the gopls forwarder
   388  		// client has built-in retry behavior that should mostly mitigate this
   389  		// problem (and if it doesn't, we probably want to improve the retry
   390  		// behavior).
   391  		if err := cmd.Start(); err != nil {
   392  			cancel()
   393  			r.remoteSocket = ""
   394  			r.remoteErr = err
   395  		} else {
   396  			r.cancelRemote = cancel
   397  			// Spin off a goroutine to wait, so that we free up resources when the
   398  			// server exits.
   399  			go cmd.Wait()
   400  		}
   401  	})
   402  
   403  	return newForwarder("unix", r.remoteSocket)
   404  }
   405  
   406  func newForwarder(network, address string) jsonrpc2.StreamServer {
   407  	server, err := lsprpc.NewForwarder(network+";"+address, nil)
   408  	if err != nil {
   409  		// This should never happen, as we are passing an explicit address.
   410  		panic(fmt.Sprintf("internal error: unable to create forwarder: %v", err))
   411  	}
   412  	return server
   413  }
   414  
   415  // Close cleans up resource that have been allocated to this workspace.
   416  func (r *Runner) Close() error {
   417  	var errmsgs []string
   418  	if r.ts != nil {
   419  		if err := r.ts.Close(); err != nil {
   420  			errmsgs = append(errmsgs, err.Error())
   421  		}
   422  	}
   423  	if r.cancelRemote != nil {
   424  		r.cancelRemote()
   425  	}
   426  	if !r.SkipCleanup {
   427  		if err := os.RemoveAll(r.tempDir); err != nil {
   428  			errmsgs = append(errmsgs, err.Error())
   429  		}
   430  	}
   431  	if len(errmsgs) > 0 {
   432  		return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t"))
   433  	}
   434  	return nil
   435  }