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