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