blob: 7b3b757536f0c39e6541c11f2accf3bbe87ceaf6 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package integration
import (
"bytes"
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"runtime/pprof"
"strings"
"sync"
"testing"
"time"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/debug"
"golang.org/x/tools/gopls/internal/lsprpc"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/jsonrpc2/servertest"
"golang.org/x/tools/internal/memoize"
"golang.org/x/tools/internal/testenv"
"golang.org/x/tools/internal/xcontext"
)
// Mode is a bitmask that defines for which execution modes a test should run.
//
// Each mode controls several aspects of gopls' configuration:
// - Which server options to use for gopls sessions
// - Whether to use a shared cache
// - Whether to use a shared server
// - Whether to run the server in-process or in a separate process
//
// The behavior of each mode with respect to these aspects is summarized below.
// TODO(rfindley, cleanup): rather than using arbitrary names for these modes,
// we can compose them explicitly out of the features described here, allowing
// individual tests more freedom in constructing problematic execution modes.
// For example, a test could assert on a certain behavior when running on a
// separate process. Moreover, we could unify 'Modes' with 'Options', and use
// RunMultiple rather than a hard-coded loop through modes.
//
// Mode | Options | Shared Cache? | Shared Server? | In-process?
// ---------------------------------------------------------------------------
// Default | Default | Y | N | Y
// Forwarded | Default | Y | Y | Y
// SeparateProcess | Default | Y | Y | N
type Mode int
const (
// Default mode runs gopls with the default options, communicating over pipes
// to emulate the lsp sidecar execution mode, which communicates over
// stdin/stdout.
//
// It uses separate servers for each test, but a shared cache, to avoid
// duplicating work when processing GOROOT.
Default Mode = 1 << iota
// Forwarded uses the default options, but forwards connections to a shared
// in-process gopls server.
Forwarded
// SeparateProcess uses the default options, but forwards connection to an
// external gopls daemon.
//
// Only supported on GOOS=linux.
SeparateProcess
)
func (m Mode) String() string {
switch m {
case Default:
return "default"
case Forwarded:
return "forwarded"
case SeparateProcess:
return "separate process"
default:
return "unknown mode"
}
}
// A Runner runs tests in gopls execution environments, as specified by its
// modes. For modes that share state (for example, a shared cache or common
// remote), any tests that execute on the same Runner will share the same
// state.
type Runner struct {
// Configuration
DefaultModes Mode // modes to run for each test
Timeout time.Duration // per-test timeout, if set
PrintGoroutinesOnFailure bool // whether to dump goroutines on test failure
SkipCleanup bool // if set, don't delete test data directories when the test exits
// Immutable state shared across test invocations
goplsPath string // path to the gopls executable (for SeparateProcess mode)
tempDir string // shared parent temp directory
store *memoize.Store // shared store
// Lazily allocated resources
tsOnce sync.Once
ts *servertest.TCPServer // shared in-process test server ("forwarded" mode)
startRemoteOnce sync.Once
remoteSocket string // unix domain socket for shared daemon ("separate process" mode)
remoteErr error
cancelRemote func()
}
type TestFunc func(t *testing.T, env *Env)
// Run executes the test function in the default configured gopls execution
// modes. For each a test run, a new workspace is created containing the
// un-txtared files specified by filedata.
func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) {
// TODO(rfindley): this function has gotten overly complicated, and warrants
// refactoring.
if !runFromMain {
// Main performs various setup precondition checks.
// While it could theoretically be made OK for a Runner to be used outside
// of Main, it is simpler to enforce that we only use the Runner from
// integration test suites.
t.Fatal("integration.Runner.Run must be run from integration.Main")
}
tests := []struct {
name string
mode Mode
getServer func() jsonrpc2.StreamServer
}{
{"default", Default, r.defaultServer},
{"forwarded", Forwarded, r.forwardedServer},
{"separate_process", SeparateProcess, r.separateProcessServer},
}
for _, tc := range tests {
tc := tc
config := defaultConfig()
for _, opt := range opts {
opt.set(&config)
}
modes := r.DefaultModes
if config.modes != 0 {
modes = config.modes
}
if modes&tc.mode == 0 {
continue
}
t.Run(tc.name, func(t *testing.T) {
// TODO(rfindley): once jsonrpc2 shutdown is fixed, we should not leak
// goroutines in this test function.
// stacktest.NoLeak(t)
ctx := context.Background()
if r.Timeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.Timeout)
defer cancel()
} else if d, ok := testenv.Deadline(t); ok {
timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup.
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
// TODO(rfindley): do we need an instance at all? Can it be removed?
ctx = debug.WithInstance(ctx, "off")
rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name()))
if err := os.MkdirAll(rootDir, 0755); err != nil {
t.Fatal(err)
}
files := fake.UnpackTxt(files)
if config.editor.WindowsLineEndings {
for name, data := range files {
files[name] = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n"))
}
}
config.sandbox.Files = files
config.sandbox.RootDir = rootDir
sandbox, err := fake.NewSandbox(&config.sandbox)
if err != nil {
t.Fatal(err)
}
defer func() {
if !r.SkipCleanup {
if err := sandbox.Close(); err != nil {
pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
t.Errorf("closing the sandbox: %v", err)
}
}
}()
// Write the go.sum file for the requested directories, before starting the server.
for _, dir := range config.writeGoSum {
if err := sandbox.RunGoCommand(context.Background(), dir, "list", []string{"-mod=mod", "./..."}, []string{"GOWORK=off"}, true); err != nil {
t.Fatal(err)
}
}
ss := tc.getServer()
framer := jsonrpc2.NewRawStream
ls := &loggingFramer{}
framer = ls.framer(jsonrpc2.NewRawStream)
ts := servertest.NewPipeServer(ss, framer)
env := ConnectGoplsEnv(t, ctx, sandbox, config.editor, ts)
defer func() {
if t.Failed() && r.PrintGoroutinesOnFailure {
pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
}
if (t.Failed() && !config.noLogsOnError) || *printLogs {
ls.printBuffers(t.Name(), os.Stderr)
}
// For tests that failed due to a timeout, don't fail to shutdown
// because ctx is done.
//
// There is little point to setting an arbitrary timeout for closing
// the editor: in general we want to clean up before proceeding to the
// next test, and if there is a deadlock preventing closing it will
// eventually be handled by the `go test` timeout.
if err := env.Editor.Close(xcontext.Detach(ctx)); err != nil {
t.Errorf("closing editor: %v", err)
}
}()
// Always await the initial workspace load.
env.Await(InitialWorkspaceLoad)
test(t, env)
})
}
}
// ConnectGoplsEnv creates a new Gopls environment for the given sandbox,
// editor config, and server connector.
//
// TODO(rfindley): significantly refactor the way testing environments are
// constructed.
func ConnectGoplsEnv(t testing.TB, ctx context.Context, sandbox *fake.Sandbox, config fake.EditorConfig, connector servertest.Connector) *Env {
awaiter := NewAwaiter(sandbox.Workdir)
editor, err := fake.NewEditor(sandbox, config).Connect(ctx, connector, awaiter.Hooks())
if err != nil {
t.Fatal(err)
}
env := &Env{
T: t,
Ctx: ctx,
Sandbox: sandbox,
Server: connector,
Editor: editor,
Awaiter: awaiter,
}
return env
}
// longBuilders maps builders that are skipped when -short is set to a
// (possibly empty) justification.
var longBuilders = map[string]string{
"openbsd-amd64-64": "go.dev/issue/42789",
"openbsd-386-64": "go.dev/issue/42789",
"openbsd-386-68": "go.dev/issue/42789",
"openbsd-amd64-68": "go.dev/issue/42789",
"darwin-amd64-10_12": "",
"freebsd-amd64-race": "",
"illumos-amd64": "",
"netbsd-arm-bsiegert": "",
"solaris-amd64-oraclerel": "",
"windows-arm-zx2c4": "",
"linux-ppc64le-power9osu": "go.dev/issue/66748",
}
// TODO(rfindley): inline into Main.
func checkBuilder() string {
builder := os.Getenv("GO_BUILDER_NAME")
if reason, ok := longBuilders[builder]; ok && testing.Short() {
if reason != "" {
return fmt.Sprintf("skipping %s with -short due to %s", builder, reason)
} else {
return fmt.Sprintf("skipping %s with -short", builder)
}
}
return ""
}
type loggingFramer struct {
mu sync.Mutex
buf *safeBuffer
}
// safeBuffer is a threadsafe buffer for logs.
type safeBuffer struct {
mu sync.Mutex
buf bytes.Buffer
}
func (b *safeBuffer) Write(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.Write(p)
}
func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer {
return func(nc net.Conn) jsonrpc2.Stream {
s.mu.Lock()
framed := false
if s.buf == nil {
s.buf = &safeBuffer{buf: bytes.Buffer{}}
framed = true
}
s.mu.Unlock()
stream := f(nc)
if framed {
return protocol.LoggingStream(stream, s.buf)
}
return stream
}
}
func (s *loggingFramer) printBuffers(testname string, w io.Writer) {
s.mu.Lock()
defer s.mu.Unlock()
if s.buf == nil {
return
}
fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs for %q\n", testname)
s.buf.mu.Lock()
io.Copy(w, &s.buf.buf)
s.buf.mu.Unlock()
fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname)
}
// defaultServer handles the Default execution mode.
func (r *Runner) defaultServer() jsonrpc2.StreamServer {
return lsprpc.NewStreamServer(cache.New(r.store), false, nil)
}
// forwardedServer handles the Forwarded execution mode.
func (r *Runner) forwardedServer() jsonrpc2.StreamServer {
r.tsOnce.Do(func() {
ctx := context.Background()
ctx = debug.WithInstance(ctx, "off")
ss := lsprpc.NewStreamServer(cache.New(nil), false, nil)
r.ts = servertest.NewTCPServer(ctx, ss, nil)
})
return newForwarder("tcp", r.ts.Addr)
}
// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running
// tests. It's a trick to allow tests to find a binary to use to start a gopls
// subprocess.
const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS"
// separateProcessServer handles the SeparateProcess execution mode.
func (r *Runner) separateProcessServer() jsonrpc2.StreamServer {
if runtime.GOOS != "linux" {
panic("separate process execution mode is only supported on linux")
}
r.startRemoteOnce.Do(func() {
socketDir, err := os.MkdirTemp(r.tempDir, "gopls-test-socket")
if err != nil {
r.remoteErr = err
return
}
r.remoteSocket = filepath.Join(socketDir, "gopls-test-daemon")
// The server should be killed by when the test runner exits, but to be
// conservative also set a listen timeout.
args := []string{"serve", "-listen", "unix;" + r.remoteSocket, "-listen.timeout", "1m"}
ctx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, r.goplsPath, args...)
cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true")
// Start the external gopls process. This is still somewhat racy, as we
// don't know when gopls binds to the socket, but the gopls forwarder
// client has built-in retry behavior that should mostly mitigate this
// problem (and if it doesn't, we probably want to improve the retry
// behavior).
if err := cmd.Start(); err != nil {
cancel()
r.remoteSocket = ""
r.remoteErr = err
} else {
r.cancelRemote = cancel
// Spin off a goroutine to wait, so that we free up resources when the
// server exits.
go cmd.Wait()
}
})
return newForwarder("unix", r.remoteSocket)
}
func newForwarder(network, address string) jsonrpc2.StreamServer {
server, err := lsprpc.NewForwarder(network+";"+address, nil)
if err != nil {
// This should never happen, as we are passing an explicit address.
panic(fmt.Sprintf("internal error: unable to create forwarder: %v", err))
}
return server
}
// Close cleans up resource that have been allocated to this workspace.
func (r *Runner) Close() error {
var errmsgs []string
if r.ts != nil {
if err := r.ts.Close(); err != nil {
errmsgs = append(errmsgs, err.Error())
}
}
if r.cancelRemote != nil {
r.cancelRemote()
}
if !r.SkipCleanup {
if err := os.RemoveAll(r.tempDir); err != nil {
errmsgs = append(errmsgs, err.Error())
}
}
if len(errmsgs) > 0 {
return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t"))
}
return nil
}