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	"io"
12	"io/ioutil"
13	"net"
14	"os"
15	"os/exec"
16	"path/filepath"
17	"runtime/pprof"
18	"strings"
19	"sync"
20	"testing"
21	"time"
22
23	"golang.org/x/tools/internal/jsonrpc2"
24	"golang.org/x/tools/internal/jsonrpc2/servertest"
25	"golang.org/x/tools/internal/lsp/cache"
26	"golang.org/x/tools/internal/lsp/debug"
27	"golang.org/x/tools/internal/lsp/fake"
28	"golang.org/x/tools/internal/lsp/lsprpc"
29	"golang.org/x/tools/internal/lsp/protocol"
30)
31
32// Mode is a bitmask that defines for which execution modes a test should run.
33type Mode int
34
35const (
36	// Singleton mode uses a separate in-process gopls instance for each test,
37	// and communicates over pipes to mimic the gopls sidecar execution mode,
38	// which communicates over stdin/stderr.
39	Singleton Mode = 1 << iota
40
41	// Forwarded forwards connections to a shared in-process gopls instance.
42	Forwarded
43	// SeparateProcess forwards connection to a shared separate gopls process.
44	SeparateProcess
45	// NormalModes are the global default execution modes, when unmodified by
46	// test flags or by individual test options.
47	NormalModes = Singleton | Forwarded
48)
49
50// A Runner runs tests in gopls execution environments, as specified by its
51// modes. For modes that share state (for example, a shared cache or common
52// remote), any tests that execute on the same Runner will share the same
53// state.
54type Runner struct {
55	DefaultModes             Mode
56	Timeout                  time.Duration
57	GoplsPath                string
58	PrintGoroutinesOnFailure bool
59	TempDir                  string
60	SkipCleanup              bool
61
62	mu        sync.Mutex
63	ts        *servertest.TCPServer
64	socketDir string
65	// closers is a queue of clean-up functions to run at the end of the entire
66	// test suite.
67	closers []io.Closer
68}
69
70type runConfig struct {
71	editor    fake.EditorConfig
72	sandbox   fake.SandboxConfig
73	modes     Mode
74	timeout   time.Duration
75	debugAddr string
76	skipLogs  bool
77	skipHooks bool
78}
79
80func (r *Runner) defaultConfig() *runConfig {
81	return &runConfig{
82		modes:   r.DefaultModes,
83		timeout: r.Timeout,
84	}
85}
86
87// A RunOption augments the behavior of the test runner.
88type RunOption interface {
89	set(*runConfig)
90}
91
92type optionSetter func(*runConfig)
93
94func (f optionSetter) set(opts *runConfig) {
95	f(opts)
96}
97
98// WithTimeout configures a custom timeout for this test run.
99func WithTimeout(d time.Duration) RunOption {
100	return optionSetter(func(opts *runConfig) {
101		opts.timeout = d
102	})
103}
104
105// WithProxyFiles configures a file proxy using the given txtar-encoded string.
106func WithProxyFiles(txt string) RunOption {
107	return optionSetter(func(opts *runConfig) {
108		opts.sandbox.ProxyFiles = txt
109	})
110}
111
112// WithModes configures the execution modes that the test should run in.
113func WithModes(modes Mode) RunOption {
114	return optionSetter(func(opts *runConfig) {
115		opts.modes = modes
116	})
117}
118
119// WithEditorConfig configures the editor's LSP session.
120func WithEditorConfig(config fake.EditorConfig) RunOption {
121	return optionSetter(func(opts *runConfig) {
122		opts.editor = config
123	})
124}
125
126// WithoutWorkspaceFolders prevents workspace folders from being sent as part
127// of the sandbox's initialization. It is used to simulate opening a single
128// file in the editor, without a workspace root. In that case, the client sends
129// neither workspace folders nor a root URI.
130func WithoutWorkspaceFolders() RunOption {
131	return optionSetter(func(opts *runConfig) {
132		opts.editor.WithoutWorkspaceFolders = true
133	})
134}
135
136// WithRootPath specifies the rootURI of the workspace folder opened in the
137// editor. By default, the sandbox opens the top-level directory, but some
138// tests need to check other cases.
139func WithRootPath(path string) RunOption {
140	return optionSetter(func(opts *runConfig) {
141		opts.editor.EditorRootPath = path
142	})
143}
144
145// InGOPATH configures the workspace working directory to be GOPATH, rather
146// than a separate working directory for use with modules.
147func InGOPATH() RunOption {
148	return optionSetter(func(opts *runConfig) {
149		opts.sandbox.InGoPath = true
150	})
151}
152
153// WithDebugAddress configures a debug server bound to addr. This option is
154// currently only supported when executing in Singleton mode. It is intended to
155// be used for long-running stress tests.
156func WithDebugAddress(addr string) RunOption {
157	return optionSetter(func(opts *runConfig) {
158		opts.debugAddr = addr
159	})
160}
161
162// SkipLogs skips the buffering of logs during test execution. It is intended
163// for long-running stress tests.
164func SkipLogs() RunOption {
165	return optionSetter(func(opts *runConfig) {
166		opts.skipLogs = true
167	})
168}
169
170// InExistingDir runs the test in a pre-existing directory. If set, no initial
171// files may be passed to the runner. It is intended for long-running stress
172// tests.
173func InExistingDir(dir string) RunOption {
174	return optionSetter(func(opts *runConfig) {
175		opts.sandbox.Workdir = dir
176	})
177}
178
179// NoHooks disables the test runner's client hooks that are used for
180// instrumenting expectations (tracking diagnostics, logs, work done, etc.). It
181// is intended for performance-sensitive stress tests.
182func NoHooks() RunOption {
183	return optionSetter(func(opts *runConfig) {
184		opts.skipHooks = true
185	})
186}
187
188// WithGOPROXY configures the test environment to have an explicit proxy value.
189// This is intended for stress tests -- to ensure their isolation, regtests
190// should instead use WithProxyFiles.
191func WithGOPROXY(goproxy string) RunOption {
192	return optionSetter(func(opts *runConfig) {
193		opts.sandbox.GOPROXY = goproxy
194	})
195}
196
197type TestFunc func(t *testing.T, env *Env)
198
199// Run executes the test function in the default configured gopls execution
200// modes. For each a test run, a new workspace is created containing the
201// un-txtared files specified by filedata.
202func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) {
203	t.Helper()
204
205	tests := []struct {
206		name      string
207		mode      Mode
208		getServer func(context.Context, *testing.T) jsonrpc2.StreamServer
209	}{
210		{"singleton", Singleton, singletonServer},
211		{"forwarded", Forwarded, r.forwardedServer},
212		{"separate_process", SeparateProcess, r.separateProcessServer},
213	}
214
215	for _, tc := range tests {
216		tc := tc
217		config := r.defaultConfig()
218		for _, opt := range opts {
219			opt.set(config)
220		}
221		if config.modes&tc.mode == 0 {
222			continue
223		}
224		if config.debugAddr != "" && tc.mode != Singleton {
225			// Debugging is useful for running stress tests, but since the daemon has
226			// likely already been started, it would be too late to debug.
227			t.Fatalf("debugging regtest servers only works in Singleton mode, "+
228				"got debug addr %q and mode %v", config.debugAddr, tc.mode)
229		}
230
231		t.Run(tc.name, func(t *testing.T) {
232			ctx, cancel := context.WithTimeout(context.Background(), config.timeout)
233			defer cancel()
234			ctx = debug.WithInstance(ctx, "", "off")
235			if config.debugAddr != "" {
236				di := debug.GetInstance(ctx)
237				di.DebugAddress = config.debugAddr
238				di.Serve(ctx)
239				di.MonitorMemory(ctx)
240			}
241
242			tempDir := filepath.Join(r.TempDir, filepath.FromSlash(t.Name()))
243			if err := os.MkdirAll(tempDir, 0755); err != nil {
244				t.Fatal(err)
245			}
246			config.sandbox.Files = files
247			config.sandbox.RootDir = tempDir
248			sandbox, err := fake.NewSandbox(&config.sandbox)
249			if err != nil {
250				t.Fatal(err)
251			}
252			// Deferring the closure of ws until the end of the entire test suite
253			// has, in testing, given the LSP server time to properly shutdown and
254			// release any file locks held in workspace, which is a problem on
255			// Windows. This may still be flaky however, and in the future we need a
256			// better solution to ensure that all Go processes started by gopls have
257			// exited before we clean up.
258			r.AddCloser(sandbox)
259			ss := tc.getServer(ctx, t)
260			framer := jsonrpc2.NewRawStream
261			ls := &loggingFramer{}
262			if !config.skipLogs {
263				framer = ls.framer(jsonrpc2.NewRawStream)
264			}
265			ts := servertest.NewPipeServer(ctx, ss, framer)
266			env := NewEnv(ctx, t, sandbox, ts, config.editor, !config.skipHooks)
267			defer func() {
268				if t.Failed() && r.PrintGoroutinesOnFailure {
269					pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
270				}
271				if t.Failed() || testing.Verbose() {
272					ls.printBuffers(t.Name(), os.Stderr)
273				}
274				env.CloseEditor()
275			}()
276			test(t, env)
277		})
278	}
279}
280
281type loggingFramer struct {
282	mu      sync.Mutex
283	buffers []*bytes.Buffer
284}
285
286func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer {
287	return func(nc net.Conn) jsonrpc2.Stream {
288		s.mu.Lock()
289		var buf bytes.Buffer
290		s.buffers = append(s.buffers, &buf)
291		s.mu.Unlock()
292		stream := f(nc)
293		return protocol.LoggingStream(stream, &buf)
294	}
295}
296
297func (s *loggingFramer) printBuffers(testname string, w io.Writer) {
298	s.mu.Lock()
299	defer s.mu.Unlock()
300
301	for i, buf := range s.buffers {
302		fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs %d of %d for %q\n", i+1, len(s.buffers), testname)
303		// Re-buffer buf to avoid a data rate (io.Copy mutates src).
304		writeBuf := bytes.NewBuffer(buf.Bytes())
305		io.Copy(w, writeBuf)
306		fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs %d of %d for %q\n", i+1, len(s.buffers), testname)
307	}
308}
309
310func singletonServer(ctx context.Context, t *testing.T) jsonrpc2.StreamServer {
311	return lsprpc.NewStreamServer(cache.New(ctx, nil), false)
312}
313
314func (r *Runner) forwardedServer(ctx context.Context, t *testing.T) jsonrpc2.StreamServer {
315	ts := r.getTestServer()
316	return lsprpc.NewForwarder("tcp", ts.Addr)
317}
318
319// getTestServer gets the shared test server instance to connect to, or creates
320// one if it doesn't exist.
321func (r *Runner) getTestServer() *servertest.TCPServer {
322	r.mu.Lock()
323	defer r.mu.Unlock()
324	if r.ts == nil {
325		ctx := context.Background()
326		ctx = debug.WithInstance(ctx, "", "off")
327		ss := lsprpc.NewStreamServer(cache.New(ctx, nil), false)
328		r.ts = servertest.NewTCPServer(ctx, ss, nil)
329	}
330	return r.ts
331}
332
333func (r *Runner) separateProcessServer(ctx context.Context, t *testing.T) jsonrpc2.StreamServer {
334	// TODO(rfindley): can we use the autostart behavior here, instead of
335	// pre-starting the remote?
336	socket := r.getRemoteSocket(t)
337	return lsprpc.NewForwarder("unix", socket)
338}
339
340// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running
341// tests. It's a trick to allow tests to find a binary to use to start a gopls
342// subprocess.
343const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS"
344
345func (r *Runner) getRemoteSocket(t *testing.T) string {
346	t.Helper()
347	r.mu.Lock()
348	defer r.mu.Unlock()
349	const daemonFile = "gopls-test-daemon"
350	if r.socketDir != "" {
351		return filepath.Join(r.socketDir, daemonFile)
352	}
353
354	if r.GoplsPath == "" {
355		t.Fatal("cannot run tests with a separate process unless a path to a gopls binary is configured")
356	}
357	var err error
358	r.socketDir, err = ioutil.TempDir(r.TempDir, "gopls-regtest-socket")
359	if err != nil {
360		t.Fatalf("creating tempdir: %v", err)
361	}
362	socket := filepath.Join(r.socketDir, daemonFile)
363	args := []string{"serve", "-listen", "unix;" + socket, "-listen.timeout", "10s"}
364	cmd := exec.Command(r.GoplsPath, args...)
365	cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true")
366	var stderr bytes.Buffer
367	cmd.Stderr = &stderr
368	go func() {
369		if err := cmd.Run(); err != nil {
370			panic(fmt.Sprintf("error running external gopls: %v\nstderr:\n%s", err, stderr.String()))
371		}
372	}()
373	return socket
374}
375
376// AddCloser schedules a closer to be closed at the end of the test run. This
377// is useful for Windows in particular, as
378func (r *Runner) AddCloser(closer io.Closer) {
379	r.mu.Lock()
380	defer r.mu.Unlock()
381	r.closers = append(r.closers, closer)
382}
383
384// Close cleans up resource that have been allocated to this workspace.
385func (r *Runner) Close() error {
386	r.mu.Lock()
387	defer r.mu.Unlock()
388
389	var errmsgs []string
390	if r.ts != nil {
391		if err := r.ts.Close(); err != nil {
392			errmsgs = append(errmsgs, err.Error())
393		}
394	}
395	if r.socketDir != "" {
396		if err := os.RemoveAll(r.socketDir); err != nil {
397			errmsgs = append(errmsgs, err.Error())
398		}
399	}
400	if !r.SkipCleanup {
401		for _, closer := range r.closers {
402			if err := closer.Close(); err != nil {
403				errmsgs = append(errmsgs, err.Error())
404			}
405		}
406		if err := os.RemoveAll(r.TempDir); err != nil {
407			errmsgs = append(errmsgs, err.Error())
408		}
409	}
410	if len(errmsgs) > 0 {
411		return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t"))
412	}
413	return nil
414}
415