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