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