cuelang.org/go@v0.10.1/internal/golangorgx/gopls/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 "cuelang.org/go/internal/golangorgx/gopls/cache" 24 "cuelang.org/go/internal/golangorgx/gopls/debug" 25 "cuelang.org/go/internal/golangorgx/gopls/lsprpc" 26 "cuelang.org/go/internal/golangorgx/gopls/protocol" 27 "cuelang.org/go/internal/golangorgx/gopls/settings" 28 "cuelang.org/go/internal/golangorgx/gopls/test/integration/fake" 29 "cuelang.org/go/internal/golangorgx/tools/jsonrpc2" 30 "cuelang.org/go/internal/golangorgx/tools/jsonrpc2/servertest" 31 "cuelang.org/go/internal/golangorgx/tools/memoize" 32 "cuelang.org/go/internal/golangorgx/tools/testenv" 33 "cuelang.org/go/internal/golangorgx/tools/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 t.Helper() 139 checkBuilder(t) 140 testenv.NeedsGoPackages(t) 141 142 tests := []struct { 143 name string 144 mode Mode 145 getServer func(func(*settings.Options)) jsonrpc2.StreamServer 146 }{ 147 {"default", Default, r.defaultServer}, 148 {"forwarded", Forwarded, r.forwardedServer}, 149 {"separate_process", SeparateProcess, r.separateProcessServer}, 150 {"experimental", Experimental, r.experimentalServer}, 151 } 152 153 for _, tc := range tests { 154 tc := tc 155 config := defaultConfig() 156 for _, opt := range opts { 157 opt.set(&config) 158 } 159 modes := r.DefaultModes 160 if config.modes != 0 { 161 modes = config.modes 162 } 163 if modes&tc.mode == 0 { 164 continue 165 } 166 167 t.Run(tc.name, func(t *testing.T) { 168 // TODO(rfindley): once jsonrpc2 shutdown is fixed, we should not leak 169 // goroutines in this test function. 170 // stacktest.NoLeak(t) 171 172 ctx := context.Background() 173 if r.Timeout != 0 { 174 var cancel context.CancelFunc 175 ctx, cancel = context.WithTimeout(ctx, r.Timeout) 176 defer cancel() 177 } else if d, ok := testenv.Deadline(t); ok { 178 timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup. 179 var cancel context.CancelFunc 180 ctx, cancel = context.WithTimeout(ctx, timeout) 181 defer cancel() 182 } 183 184 // TODO(rfindley): do we need an instance at all? Can it be removed? 185 ctx = debug.WithInstance(ctx, "off") 186 187 rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name())) 188 if err := os.MkdirAll(rootDir, 0755); err != nil { 189 t.Fatal(err) 190 } 191 192 files := fake.UnpackTxt(files) 193 if config.editor.WindowsLineEndings { 194 for name, data := range files { 195 files[name] = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")) 196 } 197 } 198 config.sandbox.Files = files 199 config.sandbox.RootDir = rootDir 200 sandbox, err := fake.NewSandbox(&config.sandbox) 201 if err != nil { 202 t.Fatal(err) 203 } 204 defer func() { 205 if !r.SkipCleanup { 206 if err := sandbox.Close(); err != nil { 207 pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) 208 t.Errorf("closing the sandbox: %v", err) 209 } 210 } 211 }() 212 213 ss := tc.getServer(r.OptionsHook) 214 215 framer := jsonrpc2.NewRawStream 216 ls := &loggingFramer{} 217 framer = ls.framer(jsonrpc2.NewRawStream) 218 ts := servertest.NewPipeServer(ss, framer) 219 220 awaiter := NewAwaiter(sandbox.Workdir) 221 const skipApplyEdits = false 222 editor, err := fake.NewEditor(sandbox, config.editor).Connect(ctx, ts, awaiter.Hooks(), skipApplyEdits) 223 if err != nil { 224 t.Fatal(err) 225 } 226 env := &Env{ 227 T: t, 228 Ctx: ctx, 229 Sandbox: sandbox, 230 Editor: editor, 231 Server: ts, 232 Awaiter: awaiter, 233 } 234 defer func() { 235 if t.Failed() && r.PrintGoroutinesOnFailure { 236 pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) 237 } 238 if t.Failed() || *printLogs { 239 ls.printBuffers(t.Name(), os.Stderr) 240 } 241 // For tests that failed due to a timeout, don't fail to shutdown 242 // because ctx is done. 243 // 244 // There is little point to setting an arbitrary timeout for closing 245 // the editor: in general we want to clean up before proceeding to the 246 // next test, and if there is a deadlock preventing closing it will 247 // eventually be handled by the `go test` timeout. 248 if err := editor.Close(xcontext.Detach(ctx)); err != nil { 249 t.Errorf("closing editor: %v", err) 250 } 251 }() 252 // Always await the initial workspace load. 253 env.Await(InitialWorkspaceLoad) 254 test(t, env) 255 }) 256 } 257 } 258 259 // longBuilders maps builders that are skipped when -short is set to a 260 // (possibly empty) justification. 261 var longBuilders = map[string]string{ 262 "openbsd-amd64-64": "golang.org/issues/42789", 263 "openbsd-386-64": "golang.org/issues/42789", 264 "openbsd-386-68": "golang.org/issues/42789", 265 "openbsd-amd64-68": "golang.org/issues/42789", 266 "darwin-amd64-10_12": "", 267 "freebsd-amd64-race": "", 268 "illumos-amd64": "", 269 "netbsd-arm-bsiegert": "", 270 "solaris-amd64-oraclerel": "", 271 "windows-arm-zx2c4": "", 272 } 273 274 func checkBuilder(t *testing.T) { 275 t.Helper() 276 builder := os.Getenv("GO_BUILDER_NAME") 277 if reason, ok := longBuilders[builder]; ok && testing.Short() { 278 if reason != "" { 279 t.Skipf("Skipping %s with -short due to %s", builder, reason) 280 } else { 281 t.Skipf("Skipping %s with -short", builder) 282 } 283 } 284 } 285 286 type loggingFramer struct { 287 mu sync.Mutex 288 buf *safeBuffer 289 } 290 291 // safeBuffer is a threadsafe buffer for logs. 292 type safeBuffer struct { 293 mu sync.Mutex 294 buf bytes.Buffer 295 } 296 297 func (b *safeBuffer) Write(p []byte) (int, error) { 298 b.mu.Lock() 299 defer b.mu.Unlock() 300 return b.buf.Write(p) 301 } 302 303 func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer { 304 return func(nc net.Conn) jsonrpc2.Stream { 305 s.mu.Lock() 306 framed := false 307 if s.buf == nil { 308 s.buf = &safeBuffer{buf: bytes.Buffer{}} 309 framed = true 310 } 311 s.mu.Unlock() 312 stream := f(nc) 313 if framed { 314 return protocol.LoggingStream(stream, s.buf) 315 } 316 return stream 317 } 318 } 319 320 func (s *loggingFramer) printBuffers(testname string, w io.Writer) { 321 s.mu.Lock() 322 defer s.mu.Unlock() 323 324 if s.buf == nil { 325 return 326 } 327 fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs for %q\n", testname) 328 s.buf.mu.Lock() 329 io.Copy(w, &s.buf.buf) 330 s.buf.mu.Unlock() 331 fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname) 332 } 333 334 // defaultServer handles the Default execution mode. 335 func (r *Runner) defaultServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { 336 return lsprpc.NewStreamServer(cache.New(r.store), false, optsHook) 337 } 338 339 // experimentalServer handles the Experimental execution mode. 340 func (r *Runner) experimentalServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { 341 options := func(o *settings.Options) { 342 optsHook(o) 343 o.EnableAllExperiments() 344 } 345 return lsprpc.NewStreamServer(cache.New(nil), false, options) 346 } 347 348 // forwardedServer handles the Forwarded execution mode. 349 func (r *Runner) forwardedServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { 350 r.tsOnce.Do(func() { 351 ctx := context.Background() 352 ctx = debug.WithInstance(ctx, "off") 353 ss := lsprpc.NewStreamServer(cache.New(nil), false, optsHook) 354 r.ts = servertest.NewTCPServer(ctx, ss, nil) 355 }) 356 return newForwarder("tcp", r.ts.Addr) 357 } 358 359 // runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running 360 // tests. It's a trick to allow tests to find a binary to use to start a gopls 361 // subprocess. 362 const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS" 363 364 // separateProcessServer handles the SeparateProcess execution mode. 365 func (r *Runner) separateProcessServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { 366 if runtime.GOOS != "linux" { 367 panic("separate process execution mode is only supported on linux") 368 } 369 370 r.startRemoteOnce.Do(func() { 371 socketDir, err := os.MkdirTemp(r.tempDir, "cuepls-test-socket") 372 if err != nil { 373 r.remoteErr = err 374 return 375 } 376 r.remoteSocket = filepath.Join(socketDir, "cuepls-test-daemon") 377 378 // The server should be killed by when the test runner exits, but to be 379 // conservative also set a listen timeout. 380 args := []string{"serve", "-listen", "unix;" + r.remoteSocket, "-listen.timeout", "1m"} 381 382 ctx, cancel := context.WithCancel(context.Background()) 383 cmd := exec.CommandContext(ctx, r.goplsPath, args...) 384 cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true") 385 386 // Start the external gopls process. This is still somewhat racy, as we 387 // don't know when gopls binds to the socket, but the gopls forwarder 388 // client has built-in retry behavior that should mostly mitigate this 389 // problem (and if it doesn't, we probably want to improve the retry 390 // behavior). 391 if err := cmd.Start(); err != nil { 392 cancel() 393 r.remoteSocket = "" 394 r.remoteErr = err 395 } else { 396 r.cancelRemote = cancel 397 // Spin off a goroutine to wait, so that we free up resources when the 398 // server exits. 399 go cmd.Wait() 400 } 401 }) 402 403 return newForwarder("unix", r.remoteSocket) 404 } 405 406 func newForwarder(network, address string) jsonrpc2.StreamServer { 407 server, err := lsprpc.NewForwarder(network+";"+address, nil) 408 if err != nil { 409 // This should never happen, as we are passing an explicit address. 410 panic(fmt.Sprintf("internal error: unable to create forwarder: %v", err)) 411 } 412 return server 413 } 414 415 // Close cleans up resource that have been allocated to this workspace. 416 func (r *Runner) Close() error { 417 var errmsgs []string 418 if r.ts != nil { 419 if err := r.ts.Close(); err != nil { 420 errmsgs = append(errmsgs, err.Error()) 421 } 422 } 423 if r.cancelRemote != nil { 424 r.cancelRemote() 425 } 426 if !r.SkipCleanup { 427 if err := os.RemoveAll(r.tempDir); err != nil { 428 errmsgs = append(errmsgs, err.Error()) 429 } 430 } 431 if len(errmsgs) > 0 { 432 return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t")) 433 } 434 return nil 435 }