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