golang.org/x/tools/gopls@v0.15.3/internal/test/integration/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 integration 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "io" 12 "net" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "runtime" 17 "runtime/pprof" 18 "strings" 19 "sync" 20 "testing" 21 "time" 22 23 "golang.org/x/tools/gopls/internal/cache" 24 "golang.org/x/tools/gopls/internal/debug" 25 "golang.org/x/tools/gopls/internal/lsprpc" 26 "golang.org/x/tools/gopls/internal/protocol" 27 "golang.org/x/tools/gopls/internal/settings" 28 "golang.org/x/tools/gopls/internal/test/integration/fake" 29 "golang.org/x/tools/internal/jsonrpc2" 30 "golang.org/x/tools/internal/jsonrpc2/servertest" 31 "golang.org/x/tools/internal/memoize" 32 "golang.org/x/tools/internal/testenv" 33 "golang.org/x/tools/internal/xcontext" 34 ) 35 36 // Mode is a bitmask that defines for which execution modes a test should run. 37 // 38 // Each mode controls several aspects of gopls' configuration: 39 // - Which server options to use for gopls sessions 40 // - Whether to use a shared cache 41 // - Whether to use a shared server 42 // - Whether to run the server in-process or in a separate process 43 // 44 // The behavior of each mode with respect to these aspects is summarized below. 45 // TODO(rfindley, cleanup): rather than using arbitrary names for these modes, 46 // we can compose them explicitly out of the features described here, allowing 47 // individual tests more freedom in constructing problematic execution modes. 48 // For example, a test could assert on a certain behavior when running with 49 // experimental options on a separate process. Moreover, we could unify 'Modes' 50 // with 'Options', and use RunMultiple rather than a hard-coded loop through 51 // modes. 52 // 53 // Mode | Options | Shared Cache? | Shared Server? | In-process? 54 // --------------------------------------------------------------------------- 55 // Default | Default | Y | N | Y 56 // Forwarded | Default | Y | Y | Y 57 // SeparateProcess | Default | Y | Y | N 58 // Experimental | Experimental | N | N | Y 59 type Mode int 60 61 const ( 62 // Default mode runs gopls with the default options, communicating over pipes 63 // to emulate the lsp sidecar execution mode, which communicates over 64 // stdin/stdout. 65 // 66 // It uses separate servers for each test, but a shared cache, to avoid 67 // duplicating work when processing GOROOT. 68 Default Mode = 1 << iota 69 70 // Forwarded uses the default options, but forwards connections to a shared 71 // in-process gopls server. 72 Forwarded 73 74 // SeparateProcess uses the default options, but forwards connection to an 75 // external gopls daemon. 76 // 77 // Only supported on GOOS=linux. 78 SeparateProcess 79 80 // Experimental enables all of the experimental configurations that are 81 // being developed, and runs gopls in sidecar mode. 82 // 83 // It uses a separate cache for each test, to exercise races that may only 84 // appear with cache misses. 85 Experimental 86 ) 87 88 func (m Mode) String() string { 89 switch m { 90 case Default: 91 return "default" 92 case Forwarded: 93 return "forwarded" 94 case SeparateProcess: 95 return "separate process" 96 case Experimental: 97 return "experimental" 98 default: 99 return "unknown mode" 100 } 101 } 102 103 // A Runner runs tests in gopls execution environments, as specified by its 104 // modes. For modes that share state (for example, a shared cache or common 105 // remote), any tests that execute on the same Runner will share the same 106 // state. 107 type Runner struct { 108 // Configuration 109 DefaultModes Mode // modes to run for each test 110 Timeout time.Duration // per-test timeout, if set 111 PrintGoroutinesOnFailure bool // whether to dump goroutines on test failure 112 SkipCleanup bool // if set, don't delete test data directories when the test exits 113 OptionsHook func(*settings.Options) // if set, use these options when creating gopls sessions 114 115 // Immutable state shared across test invocations 116 goplsPath string // path to the gopls executable (for SeparateProcess mode) 117 tempDir string // shared parent temp directory 118 store *memoize.Store // shared store 119 120 // Lazily allocated resources 121 tsOnce sync.Once 122 ts *servertest.TCPServer // shared in-process test server ("forwarded" mode) 123 124 startRemoteOnce sync.Once 125 remoteSocket string // unix domain socket for shared daemon ("separate process" mode) 126 remoteErr error 127 cancelRemote func() 128 } 129 130 type TestFunc func(t *testing.T, env *Env) 131 132 // Run executes the test function in the default configured gopls execution 133 // modes. For each a test run, a new workspace is created containing the 134 // un-txtared files specified by filedata. 135 func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) { 136 // TODO(rfindley): this function has gotten overly complicated, and warrants 137 // refactoring. 138 139 if !runFromMain { 140 // Main performs various setup precondition checks. 141 // While it could theoretically be made OK for a Runner to be used outside 142 // of Main, it is simpler to enforce that we only use the Runner from 143 // integration test suites. 144 t.Fatal("integration.Runner.Run must be run from integration.Main") 145 } 146 147 tests := []struct { 148 name string 149 mode Mode 150 getServer func(func(*settings.Options)) jsonrpc2.StreamServer 151 }{ 152 {"default", Default, r.defaultServer}, 153 {"forwarded", Forwarded, r.forwardedServer}, 154 {"separate_process", SeparateProcess, r.separateProcessServer}, 155 {"experimental", Experimental, r.experimentalServer}, 156 } 157 158 for _, tc := range tests { 159 tc := tc 160 config := defaultConfig() 161 for _, opt := range opts { 162 opt.set(&config) 163 } 164 modes := r.DefaultModes 165 if config.modes != 0 { 166 modes = config.modes 167 } 168 if modes&tc.mode == 0 { 169 continue 170 } 171 172 t.Run(tc.name, func(t *testing.T) { 173 // TODO(rfindley): once jsonrpc2 shutdown is fixed, we should not leak 174 // goroutines in this test function. 175 // stacktest.NoLeak(t) 176 177 ctx := context.Background() 178 if r.Timeout != 0 { 179 var cancel context.CancelFunc 180 ctx, cancel = context.WithTimeout(ctx, r.Timeout) 181 defer cancel() 182 } else if d, ok := testenv.Deadline(t); ok { 183 timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup. 184 var cancel context.CancelFunc 185 ctx, cancel = context.WithTimeout(ctx, timeout) 186 defer cancel() 187 } 188 189 // TODO(rfindley): do we need an instance at all? Can it be removed? 190 ctx = debug.WithInstance(ctx, "off") 191 192 rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name())) 193 if err := os.MkdirAll(rootDir, 0755); err != nil { 194 t.Fatal(err) 195 } 196 197 files := fake.UnpackTxt(files) 198 if config.editor.WindowsLineEndings { 199 for name, data := range files { 200 files[name] = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")) 201 } 202 } 203 config.sandbox.Files = files 204 config.sandbox.RootDir = rootDir 205 sandbox, err := fake.NewSandbox(&config.sandbox) 206 if err != nil { 207 t.Fatal(err) 208 } 209 defer func() { 210 if !r.SkipCleanup { 211 if err := sandbox.Close(); err != nil { 212 pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) 213 t.Errorf("closing the sandbox: %v", err) 214 } 215 } 216 }() 217 218 ss := tc.getServer(r.OptionsHook) 219 220 framer := jsonrpc2.NewRawStream 221 ls := &loggingFramer{} 222 framer = ls.framer(jsonrpc2.NewRawStream) 223 ts := servertest.NewPipeServer(ss, framer) 224 225 awaiter := NewAwaiter(sandbox.Workdir) 226 const skipApplyEdits = false 227 editor, err := fake.NewEditor(sandbox, config.editor).Connect(ctx, ts, awaiter.Hooks(), skipApplyEdits) 228 if err != nil { 229 t.Fatal(err) 230 } 231 env := &Env{ 232 T: t, 233 Ctx: ctx, 234 Sandbox: sandbox, 235 Editor: editor, 236 Server: ts, 237 Awaiter: awaiter, 238 } 239 defer func() { 240 if t.Failed() && r.PrintGoroutinesOnFailure { 241 pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) 242 } 243 if t.Failed() || *printLogs { 244 ls.printBuffers(t.Name(), os.Stderr) 245 } 246 // For tests that failed due to a timeout, don't fail to shutdown 247 // because ctx is done. 248 // 249 // There is little point to setting an arbitrary timeout for closing 250 // the editor: in general we want to clean up before proceeding to the 251 // next test, and if there is a deadlock preventing closing it will 252 // eventually be handled by the `go test` timeout. 253 if err := editor.Close(xcontext.Detach(ctx)); err != nil { 254 t.Errorf("closing editor: %v", err) 255 } 256 }() 257 // Always await the initial workspace load. 258 env.Await(InitialWorkspaceLoad) 259 test(t, env) 260 }) 261 } 262 } 263 264 // longBuilders maps builders that are skipped when -short is set to a 265 // (possibly empty) justification. 266 var longBuilders = map[string]string{ 267 "openbsd-amd64-64": "golang.org/issues/42789", 268 "openbsd-386-64": "golang.org/issues/42789", 269 "openbsd-386-68": "golang.org/issues/42789", 270 "openbsd-amd64-68": "golang.org/issues/42789", 271 "darwin-amd64-10_12": "", 272 "freebsd-amd64-race": "", 273 "illumos-amd64": "", 274 "netbsd-arm-bsiegert": "", 275 "solaris-amd64-oraclerel": "", 276 "windows-arm-zx2c4": "", 277 } 278 279 // TODO(rfindley): inline into Main. 280 func checkBuilder() string { 281 builder := os.Getenv("GO_BUILDER_NAME") 282 if reason, ok := longBuilders[builder]; ok && testing.Short() { 283 if reason != "" { 284 return fmt.Sprintf("skipping %s with -short due to %s", builder, reason) 285 } else { 286 return fmt.Sprintf("skipping %s with -short", builder) 287 } 288 } 289 return "" 290 } 291 292 type loggingFramer struct { 293 mu sync.Mutex 294 buf *safeBuffer 295 } 296 297 // safeBuffer is a threadsafe buffer for logs. 298 type safeBuffer struct { 299 mu sync.Mutex 300 buf bytes.Buffer 301 } 302 303 func (b *safeBuffer) Write(p []byte) (int, error) { 304 b.mu.Lock() 305 defer b.mu.Unlock() 306 return b.buf.Write(p) 307 } 308 309 func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer { 310 return func(nc net.Conn) jsonrpc2.Stream { 311 s.mu.Lock() 312 framed := false 313 if s.buf == nil { 314 s.buf = &safeBuffer{buf: bytes.Buffer{}} 315 framed = true 316 } 317 s.mu.Unlock() 318 stream := f(nc) 319 if framed { 320 return protocol.LoggingStream(stream, s.buf) 321 } 322 return stream 323 } 324 } 325 326 func (s *loggingFramer) printBuffers(testname string, w io.Writer) { 327 s.mu.Lock() 328 defer s.mu.Unlock() 329 330 if s.buf == nil { 331 return 332 } 333 fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs for %q\n", testname) 334 s.buf.mu.Lock() 335 io.Copy(w, &s.buf.buf) 336 s.buf.mu.Unlock() 337 fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname) 338 } 339 340 // defaultServer handles the Default execution mode. 341 func (r *Runner) defaultServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { 342 return lsprpc.NewStreamServer(cache.New(r.store), false, optsHook) 343 } 344 345 // experimentalServer handles the Experimental execution mode. 346 func (r *Runner) experimentalServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { 347 options := func(o *settings.Options) { 348 optsHook(o) 349 o.EnableAllExperiments() 350 } 351 return lsprpc.NewStreamServer(cache.New(nil), false, options) 352 } 353 354 // forwardedServer handles the Forwarded execution mode. 355 func (r *Runner) forwardedServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { 356 r.tsOnce.Do(func() { 357 ctx := context.Background() 358 ctx = debug.WithInstance(ctx, "off") 359 ss := lsprpc.NewStreamServer(cache.New(nil), false, optsHook) 360 r.ts = servertest.NewTCPServer(ctx, ss, nil) 361 }) 362 return newForwarder("tcp", r.ts.Addr) 363 } 364 365 // runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running 366 // tests. It's a trick to allow tests to find a binary to use to start a gopls 367 // subprocess. 368 const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS" 369 370 // separateProcessServer handles the SeparateProcess execution mode. 371 func (r *Runner) separateProcessServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { 372 if runtime.GOOS != "linux" { 373 panic("separate process execution mode is only supported on linux") 374 } 375 376 r.startRemoteOnce.Do(func() { 377 socketDir, err := os.MkdirTemp(r.tempDir, "gopls-test-socket") 378 if err != nil { 379 r.remoteErr = err 380 return 381 } 382 r.remoteSocket = filepath.Join(socketDir, "gopls-test-daemon") 383 384 // The server should be killed by when the test runner exits, but to be 385 // conservative also set a listen timeout. 386 args := []string{"serve", "-listen", "unix;" + r.remoteSocket, "-listen.timeout", "1m"} 387 388 ctx, cancel := context.WithCancel(context.Background()) 389 cmd := exec.CommandContext(ctx, r.goplsPath, args...) 390 cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true") 391 392 // Start the external gopls process. This is still somewhat racy, as we 393 // don't know when gopls binds to the socket, but the gopls forwarder 394 // client has built-in retry behavior that should mostly mitigate this 395 // problem (and if it doesn't, we probably want to improve the retry 396 // behavior). 397 if err := cmd.Start(); err != nil { 398 cancel() 399 r.remoteSocket = "" 400 r.remoteErr = err 401 } else { 402 r.cancelRemote = cancel 403 // Spin off a goroutine to wait, so that we free up resources when the 404 // server exits. 405 go cmd.Wait() 406 } 407 }) 408 409 return newForwarder("unix", r.remoteSocket) 410 } 411 412 func newForwarder(network, address string) jsonrpc2.StreamServer { 413 server, err := lsprpc.NewForwarder(network+";"+address, nil) 414 if err != nil { 415 // This should never happen, as we are passing an explicit address. 416 panic(fmt.Sprintf("internal error: unable to create forwarder: %v", err)) 417 } 418 return server 419 } 420 421 // Close cleans up resource that have been allocated to this workspace. 422 func (r *Runner) Close() error { 423 var errmsgs []string 424 if r.ts != nil { 425 if err := r.ts.Close(); err != nil { 426 errmsgs = append(errmsgs, err.Error()) 427 } 428 } 429 if r.cancelRemote != nil { 430 r.cancelRemote() 431 } 432 if !r.SkipCleanup { 433 if err := os.RemoveAll(r.tempDir); err != nil { 434 errmsgs = append(errmsgs, err.Error()) 435 } 436 } 437 if len(errmsgs) > 0 { 438 return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t")) 439 } 440 return nil 441 }