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