github.com/neilgarb/delve@v1.9.2-nobreaks/cmd/dlv/dlv_test.go (about) 1 package main_test 2 3 import ( 4 "bufio" 5 "bytes" 6 "flag" 7 "fmt" 8 "go/ast" 9 "go/token" 10 "go/types" 11 "io" 12 "io/ioutil" 13 "net" 14 "os" 15 "os/exec" 16 "os/user" 17 "path/filepath" 18 "runtime" 19 "strconv" 20 "strings" 21 "testing" 22 "time" 23 24 "github.com/go-delve/delve/pkg/goversion" 25 protest "github.com/go-delve/delve/pkg/proc/test" 26 "github.com/go-delve/delve/pkg/terminal" 27 "github.com/go-delve/delve/service/dap" 28 "github.com/go-delve/delve/service/dap/daptest" 29 "github.com/go-delve/delve/service/debugger" 30 "github.com/go-delve/delve/service/rpc2" 31 godap "github.com/google/go-dap" 32 "golang.org/x/tools/go/packages" 33 ) 34 35 var testBackend string 36 var ldFlags string 37 38 func init() { 39 ldFlags = os.Getenv("CGO_LDFLAGS") 40 } 41 42 func TestMain(m *testing.M) { 43 flag.StringVar(&testBackend, "backend", "", "selects backend") 44 flag.Parse() 45 if testBackend == "" { 46 testBackend = os.Getenv("PROCTEST") 47 if testBackend == "" { 48 testBackend = "native" 49 if runtime.GOOS == "darwin" { 50 testBackend = "lldb" 51 } 52 } 53 } 54 os.Exit(protest.RunTestsWithFixtures(m)) 55 } 56 57 func assertNoError(err error, t testing.TB, s string) { 58 t.Helper() 59 if err != nil { 60 _, file, line, _ := runtime.Caller(1) 61 fname := filepath.Base(file) 62 t.Fatalf("failed assertion at %s:%d: %s - %s\n", fname, line, s, err) 63 } 64 } 65 66 func projectRoot() string { 67 wd, err := os.Getwd() 68 if err != nil { 69 panic(err) 70 } 71 72 gopaths := strings.FieldsFunc(os.Getenv("GOPATH"), func(r rune) bool { return r == os.PathListSeparator }) 73 for _, curpath := range gopaths { 74 // Detects "gopath mode" when GOPATH contains several paths ex. "d:\\dir\\gopath;f:\\dir\\gopath2" 75 if strings.Contains(wd, curpath) { 76 return filepath.Join(curpath, "src", "github.com", "go-delve", "delve") 77 } 78 } 79 val, err := exec.Command("go", "list", "-mod=", "-m", "-f", "{{ .Dir }}").Output() 80 if err != nil { 81 panic(err) // the Go tool was tested to work earlier 82 } 83 return strings.TrimSuffix(string(val), "\n") 84 } 85 86 func TestBuild(t *testing.T) { 87 const listenAddr = "127.0.0.1:40573" 88 var err error 89 90 cmd := exec.Command("go", "run", "_scripts/make.go", "build") 91 cmd.Dir = projectRoot() 92 out, err := cmd.CombinedOutput() 93 if err != nil { 94 t.Fatalf("makefile error: %v\noutput %s\n", err, string(out)) 95 } 96 97 dlvbin := filepath.Join(cmd.Dir, "dlv") 98 defer os.Remove(dlvbin) 99 100 fixtures := protest.FindFixturesDir() 101 102 buildtestdir := filepath.Join(fixtures, "buildtest") 103 104 cmd = exec.Command(dlvbin, "debug", "--headless=true", "--listen="+listenAddr, "--api-version=2", "--backend="+testBackend, "--log", "--log-output=debugger,rpc") 105 cmd.Dir = buildtestdir 106 stderr, err := cmd.StderrPipe() 107 assertNoError(err, t, "stderr pipe") 108 defer stderr.Close() 109 110 assertNoError(cmd.Start(), t, "dlv debug") 111 112 scan := bufio.NewScanner(stderr) 113 // wait for the debugger to start 114 scan.Scan() 115 t.Log(scan.Text()) 116 go func() { 117 for scan.Scan() { 118 t.Log(scan.Text()) 119 // keep pipe empty 120 } 121 }() 122 123 client := rpc2.NewClient(listenAddr) 124 state := <-client.Continue() 125 126 if !state.Exited { 127 t.Fatal("Program did not exit") 128 } 129 130 client.Detach(true) 131 cmd.Wait() 132 } 133 134 func testOutput(t *testing.T, dlvbin, output string, delveCmds []string) (stdout, stderr []byte) { 135 var stdoutBuf, stderrBuf bytes.Buffer 136 buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest") 137 138 c := []string{dlvbin, "debug", "--allow-non-terminal-interactive=true"} 139 debugbin := filepath.Join(buildtestdir, "__debug_bin") 140 if output != "" { 141 c = append(c, "--output", output) 142 if filepath.IsAbs(output) { 143 debugbin = output 144 } else { 145 debugbin = filepath.Join(buildtestdir, output) 146 } 147 } 148 cmd := exec.Command(c[0], c[1:]...) 149 cmd.Dir = buildtestdir 150 stdin, err := cmd.StdinPipe() 151 assertNoError(err, t, "stdin pipe") 152 defer stdin.Close() 153 154 cmd.Stdout = &stdoutBuf 155 cmd.Stderr = &stderrBuf 156 157 assertNoError(cmd.Start(), t, "dlv debug with output") 158 159 // Give delve some time to compile and write the binary. 160 foundIt := false 161 for wait := 0; wait < 30; wait++ { 162 _, err = os.Stat(debugbin) 163 if err == nil { 164 foundIt = true 165 break 166 } 167 168 time.Sleep(1 * time.Second) 169 } 170 if !foundIt { 171 t.Errorf("running %q: file not created: %v", delveCmds, err) 172 } 173 174 for _, c := range delveCmds { 175 fmt.Fprintf(stdin, "%s\n", c) 176 } 177 178 // ignore "dlv debug" command error, it returns 179 // errors even after successful debug session. 180 cmd.Wait() 181 stdout, stderr = stdoutBuf.Bytes(), stderrBuf.Bytes() 182 183 _, err = os.Stat(debugbin) 184 if err == nil { 185 // Sometimes delve on Windows can't remove the built binary before 186 // exiting and gets an "Access is denied" error when trying. 187 // See: https://travis-ci.com/go-delve/delve/jobs/296325131) 188 // We have added a delay to gobuild.Remove, but to avoid any test 189 // flakiness, we guard against this failure here as well. 190 if runtime.GOOS != "windows" || !strings.Contains(err.Error(), "Access is denied") { 191 t.Errorf("running %q: file %v was not deleted\nstdout is %q, stderr is %q", delveCmds, debugbin, stdout, stderr) 192 } 193 return 194 } 195 if !os.IsNotExist(err) { 196 t.Errorf("running %q: %v\nstdout is %q, stderr is %q", delveCmds, err, stdout, stderr) 197 return 198 } 199 return 200 } 201 202 func getDlvBin(t *testing.T) (string, string) { 203 // In case this was set in the environment 204 // from getDlvBinEBPF lets clear it here so 205 // we can ensure we don't get build errors 206 // depending on the test ordering. 207 os.Setenv("CGO_LDFLAGS", ldFlags) 208 return getDlvBinInternal(t) 209 } 210 211 func getDlvBinEBPF(t *testing.T) (string, string) { 212 return getDlvBinInternal(t, "-tags", "ebpf") 213 } 214 215 func getDlvBinInternal(t *testing.T, goflags ...string) (string, string) { 216 tmpdir, err := ioutil.TempDir("", "TestDlv") 217 if err != nil { 218 t.Fatal(err) 219 } 220 221 dlvbin := filepath.Join(tmpdir, "dlv.exe") 222 args := append([]string{"build", "-o", dlvbin}, goflags...) 223 args = append(args, "github.com/go-delve/delve/cmd/dlv") 224 225 out, err := exec.Command("go", args...).CombinedOutput() 226 if err != nil { 227 t.Fatalf("go build -o %v github.com/go-delve/delve/cmd/dlv: %v\n%s", dlvbin, err, string(out)) 228 } 229 230 return dlvbin, tmpdir 231 } 232 233 // TestOutput verifies that the debug executable is created in the correct path 234 // and removed after exit. 235 func TestOutput(t *testing.T) { 236 dlvbin, tmpdir := getDlvBin(t) 237 defer os.RemoveAll(tmpdir) 238 239 for _, output := range []string{"", "myownname", filepath.Join(tmpdir, "absolute.path")} { 240 testOutput(t, dlvbin, output, []string{"exit"}) 241 242 const hello = "hello world!" 243 stdout, _ := testOutput(t, dlvbin, output, []string{"continue", "exit"}) 244 if !strings.Contains(string(stdout), hello) { 245 t.Errorf("stdout %q should contain %q", stdout, hello) 246 } 247 } 248 } 249 250 // TestContinue verifies that the debugged executable starts immediately with --continue 251 func TestContinue(t *testing.T) { 252 const listenAddr = "127.0.0.1:40573" 253 254 dlvbin, tmpdir := getDlvBin(t) 255 defer os.RemoveAll(tmpdir) 256 257 buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest") 258 cmd := exec.Command(dlvbin, "debug", "--headless", "--continue", "--accept-multiclient", "--listen", listenAddr) 259 cmd.Dir = buildtestdir 260 stdout, err := cmd.StdoutPipe() 261 assertNoError(err, t, "stdout pipe") 262 defer stdout.Close() 263 264 assertNoError(cmd.Start(), t, "start headless instance") 265 266 scan := bufio.NewScanner(stdout) 267 // wait for the debugger to start 268 for scan.Scan() { 269 t.Log(scan.Text()) 270 if scan.Text() == "hello world!" { 271 break 272 } 273 } 274 275 // and detach from and kill the headless instance 276 client := rpc2.NewClient(listenAddr) 277 if err := client.Detach(true); err != nil { 278 t.Fatalf("error detaching from headless instance: %v", err) 279 } 280 cmd.Wait() 281 } 282 283 // TestChildProcessExitWhenNoDebugInfo verifies that the child process exits when dlv launch the binary without debug info 284 func TestChildProcessExitWhenNoDebugInfo(t *testing.T) { 285 noDebugFlags := protest.LinkStrip 286 // -s doesn't strip symbols on Mac, use -w instead 287 if runtime.GOOS == "darwin" { 288 noDebugFlags = protest.LinkDisableDWARF 289 } 290 291 if _, err := exec.LookPath("ps"); err != nil { 292 t.Skip("test skipped, `ps` not found") 293 } 294 295 dlvbin, tmpdir := getDlvBin(t) 296 defer os.RemoveAll(tmpdir) 297 298 fix := protest.BuildFixture("http_server", noDebugFlags) 299 300 // dlv exec the binary file and expect error. 301 out, err := exec.Command(dlvbin, "exec", "--headless", "--log", fix.Path).CombinedOutput() 302 t.Log(string(out)) 303 if err == nil { 304 t.Fatalf("Expected err when launching the binary without debug info, but got nil") 305 } 306 // Test only for dlv's prefix of the error like "could not launch process: could not open debug info" 307 if !strings.Contains(string(out), "could not launch process") || !strings.Contains(string(out), debugger.NoDebugWarning) { 308 t.Fatalf("Expected logged error 'could not launch process: ... - %s'", debugger.NoDebugWarning) 309 } 310 311 // search the running process named fix.Name 312 cmd := exec.Command("ps", "-aux") 313 stdout, err := cmd.StdoutPipe() 314 assertNoError(err, t, "stdout pipe") 315 defer stdout.Close() 316 317 assertNoError(cmd.Start(), t, "start `ps -aux`") 318 319 var foundFlag bool 320 scan := bufio.NewScanner(stdout) 321 for scan.Scan() { 322 t.Log(scan.Text()) 323 if strings.Contains(scan.Text(), fix.Name) { 324 foundFlag = true 325 break 326 } 327 } 328 cmd.Wait() 329 330 if foundFlag { 331 t.Fatalf("Expected child process exited, but found it running") 332 } 333 } 334 335 // TestRedirect verifies that redirecting stdin works 336 func TestRedirect(t *testing.T) { 337 const listenAddr = "127.0.0.1:40573" 338 339 dlvbin, tmpdir := getDlvBin(t) 340 defer os.RemoveAll(tmpdir) 341 342 catfixture := filepath.Join(protest.FindFixturesDir(), "cat.go") 343 cmd := exec.Command(dlvbin, "debug", "--headless", "--continue", "--accept-multiclient", "--listen", listenAddr, "-r", catfixture, catfixture) 344 stdout, err := cmd.StdoutPipe() 345 assertNoError(err, t, "stdout pipe") 346 defer stdout.Close() 347 348 assertNoError(cmd.Start(), t, "start headless instance") 349 350 scan := bufio.NewScanner(stdout) 351 // wait for the debugger to start 352 for scan.Scan() { 353 t.Log(scan.Text()) 354 if scan.Text() == "read \"}\"" { 355 break 356 } 357 } 358 359 // and detach from and kill the headless instance 360 client := rpc2.NewClient(listenAddr) 361 _ = client.Detach(true) 362 cmd.Wait() 363 } 364 365 const checkAutogenDocLongOutput = false 366 367 func checkAutogenDoc(t *testing.T, filename, gencommand string, generated []byte) { 368 saved := slurpFile(t, filepath.Join(projectRoot(), filename)) 369 370 saved = bytes.ReplaceAll(saved, []byte("\r\n"), []byte{'\n'}) 371 generated = bytes.ReplaceAll(generated, []byte("\r\n"), []byte{'\n'}) 372 373 if len(saved) != len(generated) { 374 if checkAutogenDocLongOutput { 375 t.Logf("generated %q saved %q\n", generated, saved) 376 } 377 diffMaybe(t, filename, generated) 378 t.Fatalf("%s: needs to be regenerated; run %s", filename, gencommand) 379 } 380 381 for i := range saved { 382 if saved[i] != generated[i] { 383 if checkAutogenDocLongOutput { 384 t.Logf("generated %q saved %q\n", generated, saved) 385 } 386 diffMaybe(t, filename, generated) 387 t.Fatalf("%s: needs to be regenerated; run %s", filename, gencommand) 388 } 389 } 390 } 391 392 func slurpFile(t *testing.T, filename string) []byte { 393 saved, err := ioutil.ReadFile(filename) 394 if err != nil { 395 t.Fatalf("Could not read %s: %v", filename, err) 396 } 397 return saved 398 } 399 400 func diffMaybe(t *testing.T, filename string, generated []byte) { 401 _, err := exec.LookPath("diff") 402 if err != nil { 403 return 404 } 405 cmd := exec.Command("diff", filename, "-") 406 cmd.Dir = projectRoot() 407 stdin, _ := cmd.StdinPipe() 408 go func() { 409 stdin.Write(generated) 410 stdin.Close() 411 }() 412 out, _ := cmd.CombinedOutput() 413 t.Logf("diff:\n%s", string(out)) 414 } 415 416 // TestGeneratedDoc tests that the autogenerated documentation has been 417 // updated. 418 func TestGeneratedDoc(t *testing.T) { 419 if strings.ToLower(os.Getenv("TRAVIS")) == "true" && runtime.GOOS == "windows" { 420 t.Skip("skipping test on Windows in CI") 421 } 422 // Checks gen-cli-docs.go 423 var generatedBuf bytes.Buffer 424 commands := terminal.DebugCommands(nil) 425 commands.WriteMarkdown(&generatedBuf) 426 checkAutogenDoc(t, "Documentation/cli/README.md", "_scripts/gen-cli-docs.go", generatedBuf.Bytes()) 427 428 // Checks gen-usage-docs.go 429 tempDir, err := ioutil.TempDir(os.TempDir(), "test-gen-doc") 430 assertNoError(err, t, "TempDir") 431 defer protest.SafeRemoveAll(tempDir) 432 cmd := exec.Command("go", "run", "_scripts/gen-usage-docs.go", tempDir) 433 cmd.Dir = projectRoot() 434 err = cmd.Run() 435 assertNoError(err, t, "go run _scripts/gen-usage-docs.go") 436 entries, err := ioutil.ReadDir(tempDir) 437 assertNoError(err, t, "ReadDir") 438 for _, doc := range entries { 439 docFilename := "Documentation/usage/" + doc.Name() 440 checkAutogenDoc(t, docFilename, "_scripts/gen-usage-docs.go", slurpFile(t, tempDir+"/"+doc.Name())) 441 } 442 443 runScript := func(args ...string) []byte { 444 a := []string{"run"} 445 a = append(a, args...) 446 cmd := exec.Command("go", a...) 447 cmd.Dir = projectRoot() 448 out, err := cmd.CombinedOutput() 449 if err != nil { 450 t.Fatalf("could not run script %v: %v (output: %q)", args, err, string(out)) 451 } 452 return out 453 } 454 455 checkAutogenDoc(t, "pkg/terminal/starbind/starlark_mapping.go", "'go generate' inside pkg/terminal/starbind", runScript("_scripts/gen-starlark-bindings.go", "go", "-")) 456 checkAutogenDoc(t, "Documentation/cli/starlark.md", "'go generate' inside pkg/terminal/starbind", runScript("_scripts/gen-starlark-bindings.go", "doc/dummy", "Documentation/cli/starlark.md")) 457 checkAutogenDoc(t, "Documentation/backend_test_health.md", "go run _scripts/gen-backend_test_health.go", runScript("_scripts/gen-backend_test_health.go", "-")) 458 checkAutogenDoc(t, "_scripts/rtype-out.txt", "go run _scripts/rtype.go report _scripts/rtype-out.txt", runScript("_scripts/rtype.go", "report")) 459 460 runScript("_scripts/rtype.go", "check") 461 } 462 463 func TestExitInInit(t *testing.T) { 464 dlvbin, tmpdir := getDlvBin(t) 465 defer os.RemoveAll(tmpdir) 466 467 buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest") 468 exitInit := filepath.Join(protest.FindFixturesDir(), "exit.init") 469 cmd := exec.Command(dlvbin, "--init", exitInit, "debug") 470 cmd.Dir = buildtestdir 471 out, err := cmd.CombinedOutput() 472 t.Logf("%q %v\n", string(out), err) 473 // dlv will exit anyway because stdin is not a tty but it will print the 474 // prompt once if the init file didn't call exit successfully. 475 if strings.Contains(string(out), "(dlv)") { 476 t.Fatal("init did not cause dlv to exit") 477 } 478 } 479 480 func getMethods(pkg *types.Package, typename string) map[string]*types.Func { 481 r := make(map[string]*types.Func) 482 mset := types.NewMethodSet(types.NewPointer(pkg.Scope().Lookup(typename).Type())) 483 for i := 0; i < mset.Len(); i++ { 484 fn := mset.At(i).Obj().(*types.Func) 485 r[fn.Name()] = fn 486 } 487 return r 488 } 489 490 func publicMethodOf(decl ast.Decl, receiver string) *ast.FuncDecl { 491 fndecl, isfunc := decl.(*ast.FuncDecl) 492 if !isfunc { 493 return nil 494 } 495 if fndecl.Name.Name[0] >= 'a' && fndecl.Name.Name[0] <= 'z' { 496 return nil 497 } 498 if fndecl.Recv == nil || len(fndecl.Recv.List) != 1 { 499 return nil 500 } 501 starexpr, isstar := fndecl.Recv.List[0].Type.(*ast.StarExpr) 502 if !isstar { 503 return nil 504 } 505 identexpr, isident := starexpr.X.(*ast.Ident) 506 if !isident || identexpr.Name != receiver { 507 return nil 508 } 509 if fndecl.Body == nil { 510 return nil 511 } 512 return fndecl 513 } 514 515 func findCallCall(fndecl *ast.FuncDecl) *ast.CallExpr { 516 for _, stmt := range fndecl.Body.List { 517 var x ast.Expr = nil 518 519 switch s := stmt.(type) { 520 case *ast.AssignStmt: 521 if len(s.Rhs) == 1 { 522 x = s.Rhs[0] 523 } 524 case *ast.ReturnStmt: 525 if len(s.Results) == 1 { 526 x = s.Results[0] 527 } 528 case *ast.ExprStmt: 529 x = s.X 530 } 531 532 callx, iscall := x.(*ast.CallExpr) 533 if !iscall { 534 continue 535 } 536 fun, issel := callx.Fun.(*ast.SelectorExpr) 537 if !issel || fun.Sel.Name != "call" { 538 continue 539 } 540 return callx 541 } 542 return nil 543 } 544 545 func qf(*types.Package) string { 546 return "" 547 } 548 549 func TestTypecheckRPC(t *testing.T) { 550 fset := &token.FileSet{} 551 cfg := &packages.Config{ 552 Mode: packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedName | packages.NeedCompiledGoFiles | packages.NeedTypes, 553 Fset: fset, 554 } 555 pkgs, err := packages.Load(cfg, "github.com/go-delve/delve/service/rpc2") 556 if err != nil { 557 t.Fatal(err) 558 } 559 var clientAst *ast.File 560 var serverMethods map[string]*types.Func 561 var info *types.Info 562 packages.Visit(pkgs, func(pkg *packages.Package) bool { 563 if pkg.PkgPath != "github.com/go-delve/delve/service/rpc2" { 564 return true 565 } 566 t.Logf("package found: %v", pkg.PkgPath) 567 serverMethods = getMethods(pkg.Types, "RPCServer") 568 info = pkg.TypesInfo 569 for i := range pkg.Syntax { 570 t.Logf("file %q", pkg.CompiledGoFiles[i]) 571 if strings.HasSuffix(pkg.CompiledGoFiles[i], string(os.PathSeparator)+"client.go") { 572 clientAst = pkg.Syntax[i] 573 break 574 } 575 } 576 return true 577 }, nil) 578 579 errcount := 0 580 581 for _, decl := range clientAst.Decls { 582 fndecl := publicMethodOf(decl, "RPCClient") 583 if fndecl == nil { 584 continue 585 } 586 587 switch fndecl.Name.Name { 588 case "Continue", "Rewind": 589 // wrappers over continueDir 590 continue 591 case "SetReturnValuesLoadConfig", "Disconnect": 592 // support functions 593 continue 594 } 595 596 if fndecl.Name.Name == "Continue" || fndecl.Name.Name == "Rewind" || fndecl.Name.Name == "DirectionCongruentContinue" { 597 // using continueDir 598 continue 599 } 600 601 callx := findCallCall(fndecl) 602 603 if callx == nil { 604 t.Errorf("%s: could not find RPC call", fset.Position(fndecl.Pos())) 605 errcount++ 606 continue 607 } 608 609 if len(callx.Args) != 3 { 610 t.Errorf("%s: wrong number of arguments for RPC call", fset.Position(callx.Pos())) 611 errcount++ 612 continue 613 } 614 615 arg0, arg0islit := callx.Args[0].(*ast.BasicLit) 616 arg1 := callx.Args[1] 617 arg2 := callx.Args[2] 618 if !arg0islit || arg0.Kind != token.STRING { 619 continue 620 } 621 name, _ := strconv.Unquote(arg0.Value) 622 serverMethod := serverMethods[name] 623 if serverMethod == nil { 624 t.Errorf("%s: could not find RPC method %q", fset.Position(callx.Pos()), name) 625 errcount++ 626 continue 627 } 628 629 params := serverMethod.Type().(*types.Signature).Params() 630 631 if a, e := info.TypeOf(arg1), params.At(0).Type(); !types.AssignableTo(a, e) { 632 t.Errorf("%s: wrong type of first argument %s, expected %s", fset.Position(callx.Pos()), types.TypeString(a, qf), types.TypeString(e, qf)) 633 errcount++ 634 continue 635 } 636 637 if !strings.HasSuffix(params.At(1).Type().String(), "/service.RPCCallback") { 638 if a, e := info.TypeOf(arg2), params.At(1).Type(); !types.AssignableTo(a, e) { 639 t.Errorf("%s: wrong type of second argument %s, expected %s", fset.Position(callx.Pos()), types.TypeString(a, qf), types.TypeString(e, qf)) 640 errcount++ 641 continue 642 } 643 } 644 645 if clit, ok := arg1.(*ast.CompositeLit); ok { 646 typ := params.At(0).Type() 647 st := typ.Underlying().(*types.Struct) 648 if len(clit.Elts) != st.NumFields() && types.TypeString(typ, qf) != "DebuggerCommand" { 649 t.Errorf("%s: wrong number of fields in first argument's literal %d, expected %d", fset.Position(callx.Pos()), len(clit.Elts), st.NumFields()) 650 errcount++ 651 continue 652 } 653 } 654 } 655 656 if errcount > 0 { 657 t.Errorf("%d errors", errcount) 658 } 659 } 660 661 // TestDAPCmd verifies that a dap server can be started and shut down. 662 func TestDAPCmd(t *testing.T) { 663 const listenAddr = "127.0.0.1:40575" 664 665 dlvbin, tmpdir := getDlvBin(t) 666 defer os.RemoveAll(tmpdir) 667 668 cmd := exec.Command(dlvbin, "dap", "--log-output=dap", "--log", "--listen", listenAddr) 669 stdout, err := cmd.StdoutPipe() 670 assertNoError(err, t, "stdout pipe") 671 defer stdout.Close() 672 stderr, err := cmd.StderrPipe() 673 assertNoError(err, t, "stderr pipe") 674 defer stderr.Close() 675 676 assertNoError(cmd.Start(), t, "start dap instance") 677 678 scanOut := bufio.NewScanner(stdout) 679 scanErr := bufio.NewScanner(stderr) 680 // Wait for the debug server to start 681 scanOut.Scan() 682 listening := "DAP server listening at: " + listenAddr 683 if scanOut.Text() != listening { 684 cmd.Process.Kill() // release the port 685 t.Fatalf("Unexpected stdout:\ngot %q\nwant %q", scanOut.Text(), listening) 686 } 687 go func() { 688 for scanErr.Scan() { 689 t.Log(scanErr.Text()) 690 } 691 }() 692 693 // Connect a client and request shutdown. 694 client := daptest.NewClient(listenAddr) 695 client.DisconnectRequest() 696 client.ExpectDisconnectResponse(t) 697 client.ExpectTerminatedEvent(t) 698 if _, err := client.ReadMessage(); err != io.EOF { 699 t.Errorf("got %q, want \"EOF\"\n", err) 700 } 701 client.Close() 702 cmd.Wait() 703 } 704 705 func TestDAPCmdWithNoDebugBinary(t *testing.T) { 706 const listenAddr = "127.0.0.1:40579" 707 708 dlvbin, tmpdir := getDlvBin(t) 709 defer os.RemoveAll(tmpdir) 710 711 cmd := exec.Command(dlvbin, "dap", "--log", "--listen", listenAddr) 712 stdout, err := cmd.StdoutPipe() 713 assertNoError(err, t, "stdout pipe") 714 defer stdout.Close() 715 stderr, err := cmd.StderrPipe() 716 assertNoError(err, t, "stderr pipe") 717 defer stderr.Close() 718 assertNoError(cmd.Start(), t, "start dap instance") 719 720 scanOut := bufio.NewScanner(stdout) 721 scanErr := bufio.NewScanner(stderr) 722 // Wait for the debug server to start 723 scanOut.Scan() 724 listening := "DAP server listening at: " + listenAddr 725 if scanOut.Text() != listening { 726 cmd.Process.Kill() // release the port 727 t.Fatalf("Unexpected stdout:\ngot %q\nwant %q", scanOut.Text(), listening) 728 } 729 go func() { // Capture logging 730 for scanErr.Scan() { 731 t.Log(scanErr.Text()) 732 } 733 }() 734 735 // Exec the stripped debuggee and expect things to fail 736 noDebugFlags := protest.LinkStrip 737 // -s doesn't strip symbols on Mac, use -w instead 738 if runtime.GOOS == "darwin" { 739 noDebugFlags = protest.LinkDisableDWARF 740 } 741 fixture := protest.BuildFixture("increment", noDebugFlags) 742 go func() { 743 for scanOut.Scan() { 744 t.Errorf("Unexpected stdout: %s", scanOut.Text()) 745 } 746 }() 747 client := daptest.NewClient(listenAddr) 748 client.LaunchRequest("exec", fixture.Path, false) 749 client.ExpectErrorResponse(t) 750 client.DisconnectRequest() 751 client.ExpectDisconnectResponse(t) 752 client.ExpectTerminatedEvent(t) 753 client.Close() 754 cmd.Wait() 755 } 756 757 func newDAPRemoteClient(t *testing.T, addr string, isDlvAttach bool, isMulti bool) *daptest.Client { 758 c := daptest.NewClient(addr) 759 c.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true}) 760 if isDlvAttach || isMulti { 761 c.ExpectCapabilitiesEventSupportTerminateDebuggee(t) 762 } 763 c.ExpectInitializedEvent(t) 764 c.ExpectAttachResponse(t) 765 c.ConfigurationDoneRequest() 766 c.ExpectStoppedEvent(t) 767 c.ExpectConfigurationDoneResponse(t) 768 return c 769 } 770 771 func TestRemoteDAPClient(t *testing.T) { 772 const listenAddr = "127.0.0.1:40576" 773 774 dlvbin, tmpdir := getDlvBin(t) 775 defer os.RemoveAll(tmpdir) 776 777 buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest") 778 cmd := exec.Command(dlvbin, "debug", "--headless", "--log-output=dap", "--log", "--listen", listenAddr) 779 cmd.Dir = buildtestdir 780 stdout, err := cmd.StdoutPipe() 781 assertNoError(err, t, "stdout pipe") 782 defer stdout.Close() 783 stderr, err := cmd.StderrPipe() 784 assertNoError(err, t, "stderr pipe") 785 defer stderr.Close() 786 assertNoError(cmd.Start(), t, "start headless instance") 787 788 scanOut := bufio.NewScanner(stdout) 789 scanErr := bufio.NewScanner(stderr) 790 // Wait for the debug server to start 791 scanOut.Scan() 792 t.Log(scanOut.Text()) 793 go func() { // Capture logging 794 for scanErr.Scan() { 795 t.Log(scanErr.Text()) 796 } 797 }() 798 799 client := newDAPRemoteClient(t, listenAddr, false, false) 800 client.ContinueRequest(1) 801 client.ExpectContinueResponse(t) 802 client.ExpectTerminatedEvent(t) 803 804 client.DisconnectRequest() 805 client.ExpectOutputEventProcessExited(t, 0) 806 client.ExpectOutputEventDetaching(t) 807 client.ExpectDisconnectResponse(t) 808 client.ExpectTerminatedEvent(t) 809 if _, err := client.ReadMessage(); err == nil { 810 t.Error("expected read error upon shutdown") 811 } 812 client.Close() 813 cmd.Wait() 814 } 815 816 func closeDAPRemoteMultiClient(t *testing.T, c *daptest.Client, expectStatus string) { 817 c.DisconnectRequest() 818 c.ExpectOutputEventClosingClient(t, expectStatus) 819 c.ExpectDisconnectResponse(t) 820 c.ExpectTerminatedEvent(t) 821 c.Close() 822 time.Sleep(10 * time.Millisecond) 823 } 824 825 func TestRemoteDAPClientMulti(t *testing.T) { 826 const listenAddr = "127.0.0.1:40577" 827 828 dlvbin, tmpdir := getDlvBin(t) 829 defer os.RemoveAll(tmpdir) 830 831 buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest") 832 cmd := exec.Command(dlvbin, "debug", "--headless", "--accept-multiclient", "--log-output=debugger", "--log", "--listen", listenAddr) 833 cmd.Dir = buildtestdir 834 stdout, err := cmd.StdoutPipe() 835 assertNoError(err, t, "stdout pipe") 836 defer stdout.Close() 837 stderr, err := cmd.StderrPipe() 838 assertNoError(err, t, "stderr pipe") 839 defer stderr.Close() 840 assertNoError(cmd.Start(), t, "start headless instance") 841 842 scanOut := bufio.NewScanner(stdout) 843 scanErr := bufio.NewScanner(stderr) 844 // Wait for the debug server to start 845 scanOut.Scan() 846 t.Log(scanOut.Text()) 847 go func() { // Capture logging 848 for scanErr.Scan() { 849 t.Log(scanErr.Text()) 850 } 851 }() 852 853 // Client 0 connects but with the wrong attach request 854 dapclient0 := daptest.NewClient(listenAddr) 855 dapclient0.AttachRequest(map[string]interface{}{"mode": "local"}) 856 dapclient0.ExpectErrorResponse(t) 857 858 // Client 1 connects and continues to main.main 859 dapclient := newDAPRemoteClient(t, listenAddr, false, true) 860 dapclient.SetFunctionBreakpointsRequest([]godap.FunctionBreakpoint{{Name: "main.main"}}) 861 dapclient.ExpectSetFunctionBreakpointsResponse(t) 862 dapclient.ContinueRequest(1) 863 dapclient.ExpectContinueResponse(t) 864 dapclient.ExpectStoppedEvent(t) 865 dapclient.CheckStopLocation(t, 1, "main.main", 5) 866 closeDAPRemoteMultiClient(t, dapclient, "halted") 867 868 // Client 2 reconnects at main.main and continues to process exit 869 dapclient2 := newDAPRemoteClient(t, listenAddr, false, true) 870 dapclient2.CheckStopLocation(t, 1, "main.main", 5) 871 dapclient2.ContinueRequest(1) 872 dapclient2.ExpectContinueResponse(t) 873 dapclient2.ExpectTerminatedEvent(t) 874 closeDAPRemoteMultiClient(t, dapclient2, "exited") 875 876 // Attach to exited processes is an error 877 dapclient3 := daptest.NewClient(listenAddr) 878 dapclient3.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true}) 879 dapclient3.ExpectErrorResponseWith(t, dap.FailedToAttach, `Process \d+ has exited with status 0`, true) 880 closeDAPRemoteMultiClient(t, dapclient3, "exited") 881 882 // But rpc clients can still connect and restart 883 rpcclient := rpc2.NewClient(listenAddr) 884 if _, err := rpcclient.Restart(false); err != nil { 885 t.Errorf("error restarting with rpc client: %v", err) 886 } 887 if err := rpcclient.Detach(true); err != nil { 888 t.Fatalf("error detaching from headless instance: %v", err) 889 } 890 cmd.Wait() 891 } 892 893 func TestRemoteDAPClientAfterContinue(t *testing.T) { 894 const listenAddr = "127.0.0.1:40578" 895 896 dlvbin, tmpdir := getDlvBin(t) 897 defer os.RemoveAll(tmpdir) 898 899 fixture := protest.BuildFixture("loopprog", 0) 900 cmd := exec.Command(dlvbin, "exec", fixture.Path, "--headless", "--continue", "--accept-multiclient", "--log-output=debugger,dap", "--log", "--listen", listenAddr) 901 stdout, err := cmd.StdoutPipe() 902 assertNoError(err, t, "stdout pipe") 903 defer stdout.Close() 904 stderr, err := cmd.StderrPipe() 905 assertNoError(err, t, "stderr pipe") 906 defer stderr.Close() 907 assertNoError(cmd.Start(), t, "start headless instance") 908 909 scanOut := bufio.NewScanner(stdout) 910 scanErr := bufio.NewScanner(stderr) 911 // Wait for the debug server to start 912 scanOut.Scan() // "API server listening..."" 913 t.Log(scanOut.Text()) 914 // Wait for the program to start 915 scanOut.Scan() // "past main" 916 t.Log(scanOut.Text()) 917 918 go func() { // Capture logging 919 for scanErr.Scan() { 920 text := scanErr.Text() 921 if strings.Contains(text, "Internal Error") { 922 t.Error("ERROR", text) 923 } else { 924 t.Log(text) 925 } 926 } 927 }() 928 929 c := newDAPRemoteClient(t, listenAddr, false, true) 930 c.ContinueRequest(1) 931 c.ExpectContinueResponse(t) 932 c.DisconnectRequest() 933 c.ExpectOutputEventClosingClient(t, "running") 934 c.ExpectDisconnectResponse(t) 935 c.ExpectTerminatedEvent(t) 936 c.Close() 937 938 c = newDAPRemoteClient(t, listenAddr, false, true) 939 c.DisconnectRequestWithKillOption(true) 940 c.ExpectOutputEventDetachingKill(t) 941 c.ExpectDisconnectResponse(t) 942 c.ExpectTerminatedEvent(t) 943 if _, err := c.ReadMessage(); err == nil { 944 t.Error("expected read error upon shutdown") 945 } 946 c.Close() 947 cmd.Wait() 948 } 949 950 // TestDAPCmdWithClient tests dlv dap --client-addr can be started and shut down. 951 func TestDAPCmdWithClient(t *testing.T) { 952 listener, err := net.Listen("tcp", ":0") 953 if err != nil { 954 t.Fatalf("cannot setup listener required for testing: %v", err) 955 } 956 defer listener.Close() 957 958 dlvbin, tmpdir := getDlvBin(t) 959 defer os.RemoveAll(tmpdir) 960 961 cmd := exec.Command(dlvbin, "dap", "--log-output=dap", "--log", "--client-addr", listener.Addr().String()) 962 buf := &bytes.Buffer{} 963 cmd.Stdin = buf 964 cmd.Stdout = buf 965 assertNoError(cmd.Start(), t, "start dlv dap process with --client-addr flag") 966 967 // Wait for the connection. 968 conn, err := listener.Accept() 969 if err != nil { 970 cmd.Process.Kill() // release the port 971 t.Fatalf("Failed to get connection: %v", err) 972 } 973 t.Log("dlv dap process dialed in successfully") 974 975 client := daptest.NewClientFromConn(conn) 976 client.InitializeRequest() 977 client.ExpectInitializeResponse(t) 978 979 // Close the connection. 980 if err := conn.Close(); err != nil { 981 cmd.Process.Kill() 982 t.Fatalf("Failed to get connection: %v", err) 983 } 984 985 // Connection close should trigger dlv-reverse command's normal exit. 986 if err := cmd.Wait(); err != nil { 987 cmd.Process.Kill() 988 t.Fatalf("command failed: %v\n%s\n%v", err, buf.Bytes(), cmd.Process.Pid) 989 } 990 } 991 992 func TestTrace(t *testing.T) { 993 dlvbin, tmpdir := getDlvBin(t) 994 defer os.RemoveAll(tmpdir) 995 996 expected := []byte("> goroutine(1): main.foo(99, 9801) => (9900)\n") 997 998 fixtures := protest.FindFixturesDir() 999 cmd := exec.Command(dlvbin, "trace", "--output", filepath.Join(tmpdir, "__debug"), filepath.Join(fixtures, "issue573.go"), "foo") 1000 rdr, err := cmd.StderrPipe() 1001 assertNoError(err, t, "stderr pipe") 1002 defer rdr.Close() 1003 1004 cmd.Dir = filepath.Join(fixtures, "buildtest") 1005 1006 assertNoError(cmd.Start(), t, "running trace") 1007 1008 output, err := ioutil.ReadAll(rdr) 1009 assertNoError(err, t, "ReadAll") 1010 1011 if !bytes.Contains(output, expected) { 1012 t.Fatalf("expected:\n%s\ngot:\n%s", string(expected), string(output)) 1013 } 1014 cmd.Wait() 1015 } 1016 1017 func TestTracePid(t *testing.T) { 1018 if runtime.GOOS == "linux" { 1019 bs, _ := ioutil.ReadFile("/proc/sys/kernel/yama/ptrace_scope") 1020 if bs == nil || strings.TrimSpace(string(bs)) != "0" { 1021 t.Logf("can not run TestAttachDetach: %v\n", bs) 1022 return 1023 } 1024 } 1025 1026 dlvbin, tmpdir := getDlvBin(t) 1027 defer os.RemoveAll(tmpdir) 1028 1029 expected := []byte("goroutine(1): main.A()\n => ()\n") 1030 1031 // make process run 1032 fix := protest.BuildFixture("issue2023", 0) 1033 targetCmd := exec.Command(fix.Path) 1034 assertNoError(targetCmd.Start(), t, "execute issue2023") 1035 1036 if targetCmd.Process == nil || targetCmd.Process.Pid == 0 { 1037 t.Fatal("expected target process runninng") 1038 } 1039 defer targetCmd.Process.Kill() 1040 1041 // dlv attach the process by pid 1042 cmd := exec.Command(dlvbin, "trace", "-p", strconv.Itoa(targetCmd.Process.Pid), "main.A") 1043 rdr, err := cmd.StderrPipe() 1044 assertNoError(err, t, "stderr pipe") 1045 defer rdr.Close() 1046 1047 assertNoError(cmd.Start(), t, "running trace") 1048 1049 output, err := ioutil.ReadAll(rdr) 1050 assertNoError(err, t, "ReadAll") 1051 1052 if !bytes.Contains(output, expected) { 1053 t.Fatalf("expected:\n%s\ngot:\n%s", string(expected), string(output)) 1054 } 1055 1056 cmd.Wait() 1057 } 1058 1059 func TestTraceBreakpointExists(t *testing.T) { 1060 dlvbin, tmpdir := getDlvBin(t) 1061 defer os.RemoveAll(tmpdir) 1062 1063 fixtures := protest.FindFixturesDir() 1064 // We always set breakpoints on some runtime functions at startup, so this would return with 1065 // a breakpoints exists error. 1066 // TODO: Perhaps we shouldn't be setting these default breakpoints in trace mode, however. 1067 cmd := exec.Command(dlvbin, "trace", "--output", filepath.Join(tmpdir, "__debug"), filepath.Join(fixtures, "issue573.go"), "runtime.*") 1068 rdr, err := cmd.StderrPipe() 1069 assertNoError(err, t, "stderr pipe") 1070 defer rdr.Close() 1071 1072 cmd.Dir = filepath.Join(fixtures, "buildtest") 1073 1074 assertNoError(cmd.Start(), t, "running trace") 1075 1076 defer cmd.Wait() 1077 1078 output, err := ioutil.ReadAll(rdr) 1079 assertNoError(err, t, "ReadAll") 1080 1081 if bytes.Contains(output, []byte("Breakpoint exists")) { 1082 t.Fatal("Breakpoint exists errors should be ignored") 1083 } 1084 } 1085 1086 func TestTracePrintStack(t *testing.T) { 1087 dlvbin, tmpdir := getDlvBin(t) 1088 defer os.RemoveAll(tmpdir) 1089 1090 fixtures := protest.FindFixturesDir() 1091 cmd := exec.Command(dlvbin, "trace", "--output", filepath.Join(tmpdir, "__debug"), "--stack", "2", filepath.Join(fixtures, "issue573.go"), "foo") 1092 rdr, err := cmd.StderrPipe() 1093 assertNoError(err, t, "stderr pipe") 1094 defer rdr.Close() 1095 1096 cmd.Dir = filepath.Join(fixtures, "buildtest") 1097 assertNoError(cmd.Start(), t, "running trace") 1098 1099 defer cmd.Wait() 1100 1101 output, err := ioutil.ReadAll(rdr) 1102 assertNoError(err, t, "ReadAll") 1103 1104 if !bytes.Contains(output, []byte("Stack:")) && !bytes.Contains(output, []byte("main.main")) { 1105 t.Fatal("stacktrace not printed") 1106 } 1107 } 1108 1109 func TestTraceEBPF(t *testing.T) { 1110 if os.Getenv("CI") == "true" { 1111 t.Skip("cannot run test in CI, requires kernel compiled with btf support") 1112 } 1113 if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" { 1114 t.Skip("not implemented on non linux/amd64 systems") 1115 } 1116 if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 16) { 1117 t.Skip("requires at least Go 1.16 to run test") 1118 } 1119 usr, err := user.Current() 1120 if err != nil { 1121 t.Fatal(err) 1122 } 1123 if usr.Uid != "0" { 1124 t.Skip("test must be run as root") 1125 } 1126 1127 dlvbin, tmpdir := getDlvBinEBPF(t) 1128 defer os.RemoveAll(tmpdir) 1129 1130 expected := []byte("> (1) main.foo(99, 9801)\n=> \"9900\"") 1131 1132 fixtures := protest.FindFixturesDir() 1133 cmd := exec.Command(dlvbin, "trace", "--ebpf", "--output", filepath.Join(tmpdir, "__debug"), filepath.Join(fixtures, "issue573.go"), "foo") 1134 rdr, err := cmd.StderrPipe() 1135 assertNoError(err, t, "stderr pipe") 1136 defer rdr.Close() 1137 1138 assertNoError(cmd.Start(), t, "running trace") 1139 1140 output, err := ioutil.ReadAll(rdr) 1141 assertNoError(err, t, "ReadAll") 1142 1143 if !bytes.Contains(output, expected) { 1144 t.Fatalf("expected:\n%s\ngot:\n%s", string(expected), string(output)) 1145 } 1146 cmd.Wait() 1147 } 1148 1149 func TestDlvTestChdir(t *testing.T) { 1150 dlvbin, tmpdir := getDlvBin(t) 1151 defer os.RemoveAll(tmpdir) 1152 1153 fixtures := protest.FindFixturesDir() 1154 cmd := exec.Command(dlvbin, "--allow-non-terminal-interactive=true", "test", filepath.Join(fixtures, "buildtest"), "--", "-test.v") 1155 cmd.Stdin = strings.NewReader("continue\nexit\n") 1156 out, err := cmd.CombinedOutput() 1157 if err != nil { 1158 t.Fatalf("error executing Delve: %v", err) 1159 } 1160 t.Logf("output: %q", out) 1161 1162 p, _ := filepath.Abs(filepath.Join(fixtures, "buildtest")) 1163 tgt := "current directory: " + p 1164 if !strings.Contains(string(out), tgt) { 1165 t.Errorf("output did not contain expected string %q", tgt) 1166 } 1167 } 1168 1169 func TestVersion(t *testing.T) { 1170 dlvbin, tmpdir := getDlvBin(t) 1171 defer os.RemoveAll(tmpdir) 1172 1173 got, err := exec.Command(dlvbin, "version", "-v").CombinedOutput() 1174 if err != nil { 1175 t.Fatalf("error executing `dlv version`: %v\n%s\n", err, got) 1176 } 1177 want1 := []byte("mod\tgithub.com/go-delve/delve") 1178 want2 := []byte("dep\tgithub.com/google/go-dap") 1179 if !bytes.Contains(got, want1) || !bytes.Contains(got, want2) { 1180 t.Errorf("got %s\nwant %v and %v in the output", got, want1, want2) 1181 } 1182 } 1183 1184 func TestStaticcheck(t *testing.T) { 1185 if goversion.VersionAfterOrEqual(runtime.Version(), 1, 18) { 1186 //TODO(aarzilli): remove this before version 1.8.0 is released 1187 t.Skip("staticcheck does not currently support Go 1.18") 1188 } 1189 _, err := exec.LookPath("staticcheck") 1190 if err != nil { 1191 t.Skip("staticcheck not installed") 1192 } 1193 // default checks minus SA1019 which complains about deprecated identifiers, which change between versions of Go. 1194 args := []string{"-tests=false", "-checks=all,-SA1019,-ST1000,-ST1003,-ST1016,-S1021,-ST1023", "github.com/go-delve/delve/..."} 1195 // * SA1019 is disabled because new deprecations get added on every version 1196 // of Go making the output of staticcheck inconsistent depending on the 1197 // version of Go used to run it. 1198 // * ST1000,ST1003,ST1016 are disabled in the default 1199 // staticcheck configuration 1200 // * S1021 "Merge variable declaration and assignment" is disabled because 1201 // where we don't do this it is a deliberate style choice. 1202 // * ST1023 "Redundant type in variable declaration" same as S1021. 1203 cmd := exec.Command("staticcheck", args...) 1204 cmd.Dir = projectRoot() 1205 cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64") 1206 out, _ := cmd.CombinedOutput() 1207 checkAutogenDoc(t, "_scripts/staticcheck-out.txt", fmt.Sprintf("staticcheck %s > _scripts/staticcheck-out.txt", strings.Join(args, " ")), out) 1208 }