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