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