github.com/gagliardetto/golang-go@v0.0.0-20201020153340-53909ea70814/cmd/compile/internal/ssa/debug_test.go (about) 1 // Copyright 2017 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 ssa_test 6 7 import ( 8 "bytes" 9 "flag" 10 "fmt" 11 "github.com/gagliardetto/golang-go/not-internal/testenv" 12 "io" 13 "io/ioutil" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "regexp" 18 "runtime" 19 "strconv" 20 "strings" 21 "testing" 22 "time" 23 ) 24 25 var ( 26 update = flag.Bool("u", false, "update test reference files") 27 verbose = flag.Bool("v", false, "print debugger interactions (very verbose)") 28 dryrun = flag.Bool("n", false, "just print the command line and first debugging bits") 29 useGdb = flag.Bool("g", false, "use Gdb instead of Delve (dlv), use gdb reference files") 30 force = flag.Bool("f", false, "force run under not linux-amd64; also do not use tempdir") 31 repeats = flag.Bool("r", false, "detect repeats in debug steps and don't ignore them") 32 inlines = flag.Bool("i", false, "do inlining for gdb (makes testing flaky till inlining info is correct)") 33 ) 34 35 var ( 36 hexRe = regexp.MustCompile("0x[a-zA-Z0-9]+") 37 numRe = regexp.MustCompile("-?[0-9]+") 38 stringRe = regexp.MustCompile("\"([^\\\"]|(\\.))*\"") 39 leadingDollarNumberRe = regexp.MustCompile("^[$][0-9]+") 40 optOutGdbRe = regexp.MustCompile("[<]optimized out[>]") 41 numberColonRe = regexp.MustCompile("^ *[0-9]+:") 42 ) 43 44 var gdb = "gdb" // Might be "ggdb" on Darwin, because gdb no longer part of XCode 45 var debugger = "dlv" // For naming files, etc. 46 47 var gogcflags = os.Getenv("GO_GCFLAGS") 48 49 // optimizedLibs usually means "not running in a noopt test builder". 50 var optimizedLibs = (!strings.Contains(gogcflags, "-N") && !strings.Contains(gogcflags, "-l")) 51 52 // TestNexting go-builds a file, then uses a debugger (default gdb, optionally delve) 53 // to next through the generated executable, recording each line landed at, and 54 // then compares those lines with reference file(s). 55 // Flag -u updates the reference file(s). 56 // Flag -d changes the debugger to delve (and uses delve-specific reference files) 57 // Flag -v is ever-so-slightly verbose. 58 // Flag -n is for dry-run, and prints the shell and first debug commands. 59 // 60 // Because this test (combined with existing compiler deficiencies) is flaky, 61 // for gdb-based testing by default inlining is disabled 62 // (otherwise output depends on library internals) 63 // and for both gdb and dlv by default repeated lines in the next stream are ignored 64 // (because this appears to be timing-dependent in gdb, and the cleanest fix is in code common to gdb and dlv). 65 // 66 // Also by default, any source code outside of .../testdata/ is not mentioned 67 // in the debugging histories. This deals both with inlined library code once 68 // the compiler is generating clean inline records, and also deals with 69 // runtime code between return from main and process exit. This is hidden 70 // so that those files (in the runtime/library) can change without affecting 71 // this test. 72 // 73 // These choices can be reversed with -i (inlining on) and -r (repeats detected) which 74 // will also cause their own failures against the expected outputs. Note that if the compiler 75 // and debugger were behaving properly, the inlined code and repeated lines would not appear, 76 // so the expected output is closer to what we hope to see, though it also encodes all our 77 // current bugs. 78 // 79 // The file being tested may contain comments of the form 80 // //DBG-TAG=(v1,v2,v3) 81 // where DBG = {gdb,dlv} and TAG={dbg,opt} 82 // each variable may optionally be followed by a / and one or more of S,A,N,O 83 // to indicate normalization of Strings, (hex) addresses, and numbers. 84 // "O" is an explicit indication that we expect it to be optimized out. 85 // For example: 86 /* 87 if len(os.Args) > 1 { //gdb-dbg=(hist/A,cannedInput/A) //dlv-dbg=(hist/A,cannedInput/A) 88 */ 89 // TODO: not implemented for Delve yet, but this is the plan 90 // 91 // After a compiler change that causes a difference in the debug behavior, check 92 // to see if it is sensible or not, and if it is, update the reference files with 93 // go test debug_test.go -args -u 94 // (for Delve) 95 // go test debug_test.go -args -u -d 96 97 func TestNexting(t *testing.T) { 98 skipReasons := "" // Many possible skip reasons, list all that apply 99 if testing.Short() { 100 skipReasons = "not run in short mode; " 101 } 102 testenv.MustHaveGoBuild(t) 103 104 if *useGdb && !*force && !(runtime.GOOS == "linux" && runtime.GOARCH == "amd64") { 105 // Running gdb on OSX/darwin is very flaky. 106 // Sometimes it is called ggdb, depending on how it is installed. 107 // It also sometimes requires an admin password typed into a dialog box. 108 // Various architectures tend to differ slightly sometimes, and keeping them 109 // all in sync is a pain for people who don't have them all at hand, 110 // so limit testing to amd64 (for now) 111 skipReasons += "not run when testing gdb (-g) unless forced (-f) or linux-amd64" 112 } 113 114 if *useGdb { 115 debugger = "gdb" 116 _, err := exec.LookPath(gdb) 117 if err != nil { 118 if runtime.GOOS != "darwin" { 119 skipReasons += "not run because gdb not on path; " 120 } else { 121 // On Darwin, MacPorts installs gdb as "ggdb". 122 _, err = exec.LookPath("ggdb") 123 if err != nil { 124 skipReasons += "not run because gdb (and also ggdb) request by -g option not on path; " 125 } else { 126 gdb = "ggdb" 127 } 128 } 129 } 130 } else { // Delve 131 debugger = "dlv" 132 _, err := exec.LookPath("dlv") 133 if err != nil { 134 skipReasons += "not run because dlv not on path; " 135 } 136 } 137 138 if skipReasons != "" { 139 t.Skip(skipReasons[:len(skipReasons)-2]) 140 } 141 142 optFlags := "" // Whatever flags are needed to test debugging of optimized code. 143 dbgFlags := "-N -l" 144 if *useGdb && !*inlines { 145 // For gdb (default), disable inlining so that a compiler test does not depend on library code. 146 // TODO: Technically not necessary in 1.10 and later, but it causes a largish regression that needs investigation. 147 optFlags += " -l" 148 } 149 150 moreargs := []string{} 151 if *useGdb && (runtime.GOOS == "darwin" || runtime.GOOS == "windows") { 152 // gdb and lldb on Darwin do not deal with compressed dwarf. 153 // also, Windows. 154 moreargs = append(moreargs, "-ldflags=-compressdwarf=false") 155 } 156 157 subTest(t, debugger+"-dbg", "hist", dbgFlags, moreargs...) 158 subTest(t, debugger+"-dbg", "scopes", dbgFlags, moreargs...) 159 subTest(t, debugger+"-dbg", "i22558", dbgFlags, moreargs...) 160 161 subTest(t, debugger+"-dbg-race", "i22600", dbgFlags, append(moreargs, "-race")...) 162 163 optSubTest(t, debugger+"-opt", "hist", optFlags, 1000, moreargs...) 164 optSubTest(t, debugger+"-opt", "scopes", optFlags, 1000, moreargs...) 165 166 // Was optSubtest, this test is observed flaky on Linux in Docker on (busy) macOS, probably because of timing 167 // glitches in this harness. 168 // TODO get rid of timing glitches in this harness. 169 skipSubTest(t, debugger+"-opt", "infloop", optFlags, 10, moreargs...) 170 171 } 172 173 // subTest creates a subtest that compiles basename.go with the specified gcflags and additional compiler arguments, 174 // then runs the debugger on the resulting binary, with any comment-specified actions matching tag triggered. 175 func subTest(t *testing.T, tag string, basename string, gcflags string, moreargs ...string) { 176 t.Run(tag+"-"+basename, func(t *testing.T) { 177 if t.Name() == "TestNexting/gdb-dbg-i22558" { 178 testenv.SkipFlaky(t, 31263) 179 } 180 testNexting(t, basename, tag, gcflags, 1000, moreargs...) 181 }) 182 } 183 184 // skipSubTest is the same as subTest except that it skips the test if execution is not forced (-f) 185 func skipSubTest(t *testing.T, tag string, basename string, gcflags string, count int, moreargs ...string) { 186 t.Run(tag+"-"+basename, func(t *testing.T) { 187 if *force { 188 testNexting(t, basename, tag, gcflags, count, moreargs...) 189 } else { 190 t.Skip("skipping flaky test becaused not forced (-f)") 191 } 192 }) 193 } 194 195 // optSubTest is the same as subTest except that it skips the test if the runtime and libraries 196 // were not compiled with optimization turned on. (The skip may not be necessary with Go 1.10 and later) 197 func optSubTest(t *testing.T, tag string, basename string, gcflags string, count int, moreargs ...string) { 198 // If optimized test is run with unoptimized libraries (compiled with -N -l), it is very likely to fail. 199 // This occurs in the noopt builders (for example). 200 t.Run(tag+"-"+basename, func(t *testing.T) { 201 if *force || optimizedLibs { 202 testNexting(t, basename, tag, gcflags, count, moreargs...) 203 } else { 204 t.Skip("skipping for unoptimized stdlib/runtime") 205 } 206 }) 207 } 208 209 func testNexting(t *testing.T, base, tag, gcflags string, count int, moreArgs ...string) { 210 // (1) In testdata, build sample.go into test-sample.<tag> 211 // (2) Run debugger gathering a history 212 // (3) Read expected history from testdata/sample.<tag>.nexts 213 // optionally, write out testdata/sample.<tag>.nexts 214 215 testbase := filepath.Join("testdata", base) + "." + tag 216 tmpbase := filepath.Join("testdata", "test-"+base+"."+tag) 217 218 // Use a temporary directory unless -f is specified 219 if !*force { 220 tmpdir, err := ioutil.TempDir("", "debug_test") 221 if err != nil { 222 panic(fmt.Sprintf("Problem creating TempDir, error %v\n", err)) 223 } 224 tmpbase = filepath.Join(tmpdir, "test-"+base+"."+tag) 225 if *verbose { 226 fmt.Printf("Tempdir is %s\n", tmpdir) 227 } 228 defer os.RemoveAll(tmpdir) 229 } 230 exe := tmpbase 231 232 runGoArgs := []string{"build", "-o", exe, "-gcflags=all=" + gcflags} 233 runGoArgs = append(runGoArgs, moreArgs...) 234 runGoArgs = append(runGoArgs, filepath.Join("testdata", base+".go")) 235 236 runGo(t, "", runGoArgs...) 237 238 nextlog := testbase + ".nexts" 239 tmplog := tmpbase + ".nexts" 240 var dbg dbgr 241 if *useGdb { 242 dbg = newGdb(tag, exe) 243 } else { 244 dbg = newDelve(tag, exe) 245 } 246 h1 := runDbgr(dbg, count) 247 if *dryrun { 248 fmt.Printf("# Tag for above is %s\n", dbg.tag()) 249 return 250 } 251 if *update { 252 h1.write(nextlog) 253 } else { 254 h0 := &nextHist{} 255 h0.read(nextlog) 256 if !h0.equals(h1) { 257 // Be very noisy about exactly what's wrong to simplify debugging. 258 h1.write(tmplog) 259 cmd := exec.Command("diff", "-u", nextlog, tmplog) 260 line := asCommandLine("", cmd) 261 bytes, err := cmd.CombinedOutput() 262 if err != nil && len(bytes) == 0 { 263 t.Fatalf("step/next histories differ, diff command %s failed with error=%v", line, err) 264 } 265 t.Fatalf("step/next histories differ, diff=\n%s", string(bytes)) 266 } 267 } 268 } 269 270 type dbgr interface { 271 start() 272 stepnext(s string) bool // step or next, possible with parameter, gets line etc. returns true for success, false for unsure response 273 quit() 274 hist() *nextHist 275 tag() string 276 } 277 278 func runDbgr(dbg dbgr, maxNext int) *nextHist { 279 dbg.start() 280 if *dryrun { 281 return nil 282 } 283 for i := 0; i < maxNext; i++ { 284 if !dbg.stepnext("n") { 285 break 286 } 287 } 288 dbg.quit() 289 h := dbg.hist() 290 return h 291 } 292 293 func runGo(t *testing.T, dir string, args ...string) string { 294 var stdout, stderr bytes.Buffer 295 cmd := exec.Command(testenv.GoToolPath(t), args...) 296 cmd.Dir = dir 297 if *dryrun { 298 fmt.Printf("%s\n", asCommandLine("", cmd)) 299 return "" 300 } 301 cmd.Stdout = &stdout 302 cmd.Stderr = &stderr 303 304 if err := cmd.Run(); err != nil { 305 t.Fatalf("error running cmd (%s): %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String()) 306 } 307 308 if s := stderr.String(); s != "" { 309 t.Fatalf("Stderr = %s\nWant empty", s) 310 } 311 312 return stdout.String() 313 } 314 315 // tstring provides two strings, o (stdout) and e (stderr) 316 type tstring struct { 317 o string 318 e string 319 } 320 321 func (t tstring) String() string { 322 return t.o + t.e 323 } 324 325 type pos struct { 326 line uint32 327 file uint8 // Artifact of plans to implement differencing instead of calling out to diff. 328 } 329 330 type nextHist struct { 331 f2i map[string]uint8 332 fs []string 333 ps []pos 334 texts []string 335 vars [][]string 336 } 337 338 func (h *nextHist) write(filename string) { 339 file, err := os.Create(filename) 340 if err != nil { 341 panic(fmt.Sprintf("Problem opening %s, error %v\n", filename, err)) 342 } 343 defer file.Close() 344 var lastfile uint8 345 for i, x := range h.texts { 346 p := h.ps[i] 347 if lastfile != p.file { 348 fmt.Fprintf(file, " %s\n", h.fs[p.file-1]) 349 lastfile = p.file 350 } 351 fmt.Fprintf(file, "%d:%s\n", p.line, x) 352 // TODO, normalize between gdb and dlv into a common, comparable format. 353 for _, y := range h.vars[i] { 354 y = strings.TrimSpace(y) 355 fmt.Fprintf(file, "%s\n", y) 356 } 357 } 358 file.Close() 359 } 360 361 func (h *nextHist) read(filename string) { 362 h.f2i = make(map[string]uint8) 363 bytes, err := ioutil.ReadFile(filename) 364 if err != nil { 365 panic(fmt.Sprintf("Problem reading %s, error %v\n", filename, err)) 366 } 367 var lastfile string 368 lines := strings.Split(string(bytes), "\n") 369 for i, l := range lines { 370 if len(l) > 0 && l[0] != '#' { 371 if l[0] == ' ' { 372 // file -- first two characters expected to be " " 373 lastfile = strings.TrimSpace(l) 374 } else if numberColonRe.MatchString(l) { 375 // line number -- <number>:<line> 376 colonPos := strings.Index(l, ":") 377 if colonPos == -1 { 378 panic(fmt.Sprintf("Line %d (%s) in file %s expected to contain '<number>:' but does not.\n", i+1, l, filename)) 379 } 380 h.add(lastfile, l[0:colonPos], l[colonPos+1:]) 381 } else { 382 h.addVar(l) 383 } 384 } 385 } 386 } 387 388 // add appends file (name), line (number) and text (string) to the history, 389 // provided that the file+line combo does not repeat the previous position, 390 // and provided that the file is within the testdata directory. The return 391 // value indicates whether the append occurred. 392 func (h *nextHist) add(file, line, text string) bool { 393 // Only record source code in testdata unless the inlines flag is set 394 if !*inlines && !strings.Contains(file, "/testdata/") { 395 return false 396 } 397 fi := h.f2i[file] 398 if fi == 0 { 399 h.fs = append(h.fs, file) 400 fi = uint8(len(h.fs)) 401 h.f2i[file] = fi 402 } 403 404 line = strings.TrimSpace(line) 405 var li int 406 var err error 407 if line != "" { 408 li, err = strconv.Atoi(line) 409 if err != nil { 410 panic(fmt.Sprintf("Non-numeric line: %s, error %v\n", line, err)) 411 } 412 } 413 l := len(h.ps) 414 p := pos{line: uint32(li), file: fi} 415 416 if l == 0 || *repeats || h.ps[l-1] != p { 417 h.ps = append(h.ps, p) 418 h.texts = append(h.texts, text) 419 h.vars = append(h.vars, []string{}) 420 return true 421 } 422 return false 423 } 424 425 func (h *nextHist) addVar(text string) { 426 l := len(h.texts) 427 h.vars[l-1] = append(h.vars[l-1], text) 428 } 429 430 func invertMapSU8(hf2i map[string]uint8) map[uint8]string { 431 hi2f := make(map[uint8]string) 432 for hs, i := range hf2i { 433 hi2f[i] = hs 434 } 435 return hi2f 436 } 437 438 func (h *nextHist) equals(k *nextHist) bool { 439 if len(h.f2i) != len(k.f2i) { 440 return false 441 } 442 if len(h.ps) != len(k.ps) { 443 return false 444 } 445 hi2f := invertMapSU8(h.f2i) 446 ki2f := invertMapSU8(k.f2i) 447 448 for i, hs := range hi2f { 449 if hs != ki2f[i] { 450 return false 451 } 452 } 453 454 for i, x := range h.ps { 455 if k.ps[i] != x { 456 return false 457 } 458 } 459 460 for i, hv := range h.vars { 461 kv := k.vars[i] 462 if len(hv) != len(kv) { 463 return false 464 } 465 for j, hvt := range hv { 466 if hvt != kv[j] { 467 return false 468 } 469 } 470 } 471 472 return true 473 } 474 475 // canonFileName strips everything before "/src/" from a filename. 476 // This makes file names portable across different machines, 477 // home directories, and temporary directories. 478 func canonFileName(f string) string { 479 i := strings.Index(f, "/src/") 480 if i != -1 { 481 f = f[i+1:] 482 } 483 return f 484 } 485 486 /* Delve */ 487 488 type delveState struct { 489 cmd *exec.Cmd 490 tagg string 491 *ioState 492 atLineRe *regexp.Regexp // "\n =>" 493 funcFileLinePCre *regexp.Regexp // "^> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)" 494 line string 495 file string 496 function string 497 } 498 499 func newDelve(tag, executable string, args ...string) dbgr { 500 cmd := exec.Command("dlv", "exec", executable) 501 cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb") 502 if len(args) > 0 { 503 cmd.Args = append(cmd.Args, "--") 504 cmd.Args = append(cmd.Args, args...) 505 } 506 s := &delveState{tagg: tag, cmd: cmd} 507 // HAHA Delve has control characters embedded to change the color of the => and the line number 508 // that would be '(\\x1b\\[[0-9;]+m)?' OR TERM=dumb 509 s.atLineRe = regexp.MustCompile("\n=>[[:space:]]+[0-9]+:(.*)") 510 s.funcFileLinePCre = regexp.MustCompile("> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)[)]\n") 511 s.ioState = newIoState(s.cmd) 512 return s 513 } 514 515 func (s *delveState) tag() string { 516 return s.tagg 517 } 518 519 func (s *delveState) stepnext(ss string) bool { 520 x := s.ioState.writeReadExpect(ss+"\n", "[(]dlv[)] ") 521 excerpts := s.atLineRe.FindStringSubmatch(x.o) 522 locations := s.funcFileLinePCre.FindStringSubmatch(x.o) 523 excerpt := "" 524 if len(excerpts) > 1 { 525 excerpt = excerpts[1] 526 } 527 if len(locations) > 0 { 528 fn := canonFileName(locations[2]) 529 if *verbose { 530 if s.file != fn { 531 fmt.Printf("%s\n", locations[2]) // don't canonocalize verbose logging 532 } 533 fmt.Printf(" %s\n", locations[3]) 534 } 535 s.line = locations[3] 536 s.file = fn 537 s.function = locations[1] 538 s.ioState.history.add(s.file, s.line, excerpt) 539 // TODO: here is where variable processing will be added. See gdbState.stepnext as a guide. 540 // Adding this may require some amount of normalization so that logs are comparable. 541 return true 542 } 543 if *verbose { 544 fmt.Printf("DID NOT MATCH EXPECTED NEXT OUTPUT\nO='%s'\nE='%s'\n", x.o, x.e) 545 } 546 return false 547 } 548 549 func (s *delveState) start() { 550 if *dryrun { 551 fmt.Printf("%s\n", asCommandLine("", s.cmd)) 552 fmt.Printf("b main.test\n") 553 fmt.Printf("c\n") 554 return 555 } 556 err := s.cmd.Start() 557 if err != nil { 558 line := asCommandLine("", s.cmd) 559 panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err)) 560 } 561 s.ioState.readExpecting(-1, 5000, "Type 'help' for list of commands.") 562 s.ioState.writeReadExpect("b main.test\n", "[(]dlv[)] ") 563 s.stepnext("c") 564 } 565 566 func (s *delveState) quit() { 567 expect("", s.ioState.writeRead("q\n")) 568 } 569 570 /* Gdb */ 571 572 type gdbState struct { 573 cmd *exec.Cmd 574 tagg string 575 args []string 576 *ioState 577 atLineRe *regexp.Regexp 578 funcFileLinePCre *regexp.Regexp 579 line string 580 file string 581 function string 582 } 583 584 func newGdb(tag, executable string, args ...string) dbgr { 585 // Turn off shell, necessary for Darwin apparently 586 cmd := exec.Command(gdb, "-nx", 587 "-iex", fmt.Sprintf("add-auto-load-safe-path %s/src/runtime", runtime.GOROOT()), 588 "-ex", "set startup-with-shell off", executable) 589 cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb") 590 s := &gdbState{tagg: tag, cmd: cmd, args: args} 591 s.atLineRe = regexp.MustCompile("(^|\n)([0-9]+)(.*)") 592 s.funcFileLinePCre = regexp.MustCompile( 593 "([^ ]+) [(][^)]*[)][ \\t\\n]+at ([^:]+):([0-9]+)") 594 // runtime.main () at /Users/drchase/GoogleDrive/work/go/src/runtime/proc.go:201 595 // function file line 596 // Thread 2 hit Breakpoint 1, main.main () at /Users/drchase/GoogleDrive/work/debug/hist.go:18 597 s.ioState = newIoState(s.cmd) 598 return s 599 } 600 601 func (s *gdbState) tag() string { 602 return s.tagg 603 } 604 605 func (s *gdbState) start() { 606 run := "run" 607 for _, a := range s.args { 608 run += " " + a // Can't quote args for gdb, it will pass them through including the quotes 609 } 610 if *dryrun { 611 fmt.Printf("%s\n", asCommandLine("", s.cmd)) 612 fmt.Printf("tbreak main.test\n") 613 fmt.Printf("%s\n", run) 614 return 615 } 616 err := s.cmd.Start() 617 if err != nil { 618 line := asCommandLine("", s.cmd) 619 panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err)) 620 } 621 s.ioState.readSimpleExpecting("[(]gdb[)] ") 622 x := s.ioState.writeReadExpect("b main.test\n", "[(]gdb[)] ") 623 expect("Breakpoint [0-9]+ at", x) 624 s.stepnext(run) 625 } 626 627 func (s *gdbState) stepnext(ss string) bool { 628 x := s.ioState.writeReadExpect(ss+"\n", "[(]gdb[)] ") 629 excerpts := s.atLineRe.FindStringSubmatch(x.o) 630 locations := s.funcFileLinePCre.FindStringSubmatch(x.o) 631 excerpt := "" 632 addedLine := false 633 if len(excerpts) == 0 && len(locations) == 0 { 634 if *verbose { 635 fmt.Printf("DID NOT MATCH %s", x.o) 636 } 637 return false 638 } 639 if len(excerpts) > 0 { 640 excerpt = excerpts[3] 641 } 642 if len(locations) > 0 { 643 fn := canonFileName(locations[2]) 644 if *verbose { 645 if s.file != fn { 646 fmt.Printf("%s\n", locations[2]) 647 } 648 fmt.Printf(" %s\n", locations[3]) 649 } 650 s.line = locations[3] 651 s.file = fn 652 s.function = locations[1] 653 addedLine = s.ioState.history.add(s.file, s.line, excerpt) 654 } 655 if len(excerpts) > 0 { 656 if *verbose { 657 fmt.Printf(" %s\n", excerpts[2]) 658 } 659 s.line = excerpts[2] 660 addedLine = s.ioState.history.add(s.file, s.line, excerpt) 661 } 662 663 if !addedLine { 664 // True if this was a repeat line 665 return true 666 } 667 // Look for //gdb-<tag>=(v1,v2,v3) and print v1, v2, v3 668 vars := varsToPrint(excerpt, "//"+s.tag()+"=(") 669 for _, v := range vars { 670 response := printVariableAndNormalize(v, func(v string) string { 671 return s.ioState.writeReadExpect("p "+v+"\n", "[(]gdb[)] ").String() 672 }) 673 s.ioState.history.addVar(response) 674 } 675 return true 676 } 677 678 // printVariableAndNormalize extracts any slash-indicated normalizing requests from the variable 679 // name, then uses printer to get the value of the variable from the debugger, and then 680 // normalizes and returns the response. 681 func printVariableAndNormalize(v string, printer func(v string) string) string { 682 slashIndex := strings.Index(v, "/") 683 substitutions := "" 684 if slashIndex != -1 { 685 substitutions = v[slashIndex:] 686 v = v[:slashIndex] 687 } 688 response := printer(v) 689 // expect something like "$1 = ..." 690 dollar := strings.Index(response, "$") 691 cr := strings.Index(response, "\n") 692 693 if dollar == -1 { // some not entirely expected response, whine and carry on. 694 if cr == -1 { 695 response = strings.TrimSpace(response) // discards trailing newline 696 response = strings.Replace(response, "\n", "<BR>", -1) 697 return "$ Malformed response " + response 698 } 699 response = strings.TrimSpace(response[:cr]) 700 return "$ " + response 701 } 702 if cr == -1 { 703 cr = len(response) 704 } 705 // Convert the leading $<number> into the variable name to enhance readability 706 // and reduce scope of diffs if an earlier print-variable is added. 707 response = strings.TrimSpace(response[dollar:cr]) 708 response = leadingDollarNumberRe.ReplaceAllString(response, v) 709 710 // Normalize value as requested. 711 if strings.Contains(substitutions, "A") { 712 response = hexRe.ReplaceAllString(response, "<A>") 713 } 714 if strings.Contains(substitutions, "N") { 715 response = numRe.ReplaceAllString(response, "<N>") 716 } 717 if strings.Contains(substitutions, "S") { 718 response = stringRe.ReplaceAllString(response, "<S>") 719 } 720 if strings.Contains(substitutions, "O") { 721 response = optOutGdbRe.ReplaceAllString(response, "<Optimized out, as expected>") 722 } 723 return response 724 } 725 726 // varsToPrint takes a source code line, and extracts the comma-separated variable names 727 // found between lookfor and the next ")". 728 // For example, if line includes "... //gdb-foo=(v1,v2,v3)" and 729 // lookfor="//gdb-foo=(", then varsToPrint returns ["v1", "v2", "v3"] 730 func varsToPrint(line, lookfor string) []string { 731 var vars []string 732 if strings.Contains(line, lookfor) { 733 x := line[strings.Index(line, lookfor)+len(lookfor):] 734 end := strings.Index(x, ")") 735 if end == -1 { 736 panic(fmt.Sprintf("Saw variable list begin %s in %s but no closing ')'", lookfor, line)) 737 } 738 vars = strings.Split(x[:end], ",") 739 for i, y := range vars { 740 vars[i] = strings.TrimSpace(y) 741 } 742 } 743 return vars 744 } 745 746 func (s *gdbState) quit() { 747 response := s.ioState.writeRead("q\n") 748 if strings.Contains(response.o, "Quit anyway? (y or n)") { 749 defer func() { 750 if r := recover(); r != nil { 751 if s, ok := r.(string); !(ok && strings.Contains(s, "'Y\n'")) { 752 // Not the panic that was expected. 753 fmt.Printf("Expected a broken pipe panic, but saw the following panic instead") 754 panic(r) 755 } 756 } 757 }() 758 s.ioState.writeRead("Y\n") 759 } 760 } 761 762 type ioState struct { 763 stdout io.ReadCloser 764 stderr io.ReadCloser 765 stdin io.WriteCloser 766 outChan chan string 767 errChan chan string 768 last tstring // Output of previous step 769 history *nextHist 770 } 771 772 func newIoState(cmd *exec.Cmd) *ioState { 773 var err error 774 s := &ioState{} 775 s.history = &nextHist{} 776 s.history.f2i = make(map[string]uint8) 777 s.stdout, err = cmd.StdoutPipe() 778 line := asCommandLine("", cmd) 779 if err != nil { 780 panic(fmt.Sprintf("There was an error [stdoutpipe] running '%s', %v\n", line, err)) 781 } 782 s.stderr, err = cmd.StderrPipe() 783 if err != nil { 784 panic(fmt.Sprintf("There was an error [stdouterr] running '%s', %v\n", line, err)) 785 } 786 s.stdin, err = cmd.StdinPipe() 787 if err != nil { 788 panic(fmt.Sprintf("There was an error [stdinpipe] running '%s', %v\n", line, err)) 789 } 790 791 s.outChan = make(chan string, 1) 792 s.errChan = make(chan string, 1) 793 go func() { 794 buffer := make([]byte, 4096) 795 for { 796 n, err := s.stdout.Read(buffer) 797 if n > 0 { 798 s.outChan <- string(buffer[0:n]) 799 } 800 if err == io.EOF || n == 0 { 801 break 802 } 803 if err != nil { 804 fmt.Printf("Saw an error forwarding stdout") 805 break 806 } 807 } 808 close(s.outChan) 809 s.stdout.Close() 810 }() 811 812 go func() { 813 buffer := make([]byte, 4096) 814 for { 815 n, err := s.stderr.Read(buffer) 816 if n > 0 { 817 s.errChan <- string(buffer[0:n]) 818 } 819 if err == io.EOF || n == 0 { 820 break 821 } 822 if err != nil { 823 fmt.Printf("Saw an error forwarding stderr") 824 break 825 } 826 } 827 close(s.errChan) 828 s.stderr.Close() 829 }() 830 return s 831 } 832 833 func (s *ioState) hist() *nextHist { 834 return s.history 835 } 836 837 // writeRead writes ss, then reads stdout and stderr, waiting 500ms to 838 // be sure all the output has appeared. 839 func (s *ioState) writeRead(ss string) tstring { 840 if *verbose { 841 fmt.Printf("=> %s", ss) 842 } 843 _, err := io.WriteString(s.stdin, ss) 844 if err != nil { 845 panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err)) 846 } 847 return s.readExpecting(-1, 500, "") 848 } 849 850 // writeReadExpect writes ss, then reads stdout and stderr until something 851 // that matches expectRE appears. expectRE should not be "" 852 func (s *ioState) writeReadExpect(ss, expectRE string) tstring { 853 if *verbose { 854 fmt.Printf("=> %s", ss) 855 } 856 if expectRE == "" { 857 panic("expectRE should not be empty; use .* instead") 858 } 859 _, err := io.WriteString(s.stdin, ss) 860 if err != nil { 861 panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err)) 862 } 863 return s.readSimpleExpecting(expectRE) 864 } 865 866 func (s *ioState) readExpecting(millis, interlineTimeout int, expectedRE string) tstring { 867 timeout := time.Millisecond * time.Duration(millis) 868 interline := time.Millisecond * time.Duration(interlineTimeout) 869 s.last = tstring{} 870 var re *regexp.Regexp 871 if expectedRE != "" { 872 re = regexp.MustCompile(expectedRE) 873 } 874 loop: 875 for { 876 var timer <-chan time.Time 877 if timeout > 0 { 878 timer = time.After(timeout) 879 } 880 select { 881 case x, ok := <-s.outChan: 882 if !ok { 883 s.outChan = nil 884 } 885 s.last.o += x 886 case x, ok := <-s.errChan: 887 if !ok { 888 s.errChan = nil 889 } 890 s.last.e += x 891 case <-timer: 892 break loop 893 } 894 if re != nil { 895 if re.MatchString(s.last.o) { 896 break 897 } 898 if re.MatchString(s.last.e) { 899 break 900 } 901 } 902 timeout = interline 903 } 904 if *verbose { 905 fmt.Printf("<= %s%s", s.last.o, s.last.e) 906 } 907 return s.last 908 } 909 910 func (s *ioState) readSimpleExpecting(expectedRE string) tstring { 911 s.last = tstring{} 912 var re *regexp.Regexp 913 if expectedRE != "" { 914 re = regexp.MustCompile(expectedRE) 915 } 916 for { 917 select { 918 case x, ok := <-s.outChan: 919 if !ok { 920 s.outChan = nil 921 } 922 s.last.o += x 923 case x, ok := <-s.errChan: 924 if !ok { 925 s.errChan = nil 926 } 927 s.last.e += x 928 } 929 if re != nil { 930 if re.MatchString(s.last.o) { 931 break 932 } 933 if re.MatchString(s.last.e) { 934 break 935 } 936 } 937 } 938 if *verbose { 939 fmt.Printf("<= %s%s", s.last.o, s.last.e) 940 } 941 return s.last 942 } 943 944 // replaceEnv returns a new environment derived from env 945 // by removing any existing definition of ev and adding ev=evv. 946 func replaceEnv(env []string, ev string, evv string) []string { 947 evplus := ev + "=" 948 var found bool 949 for i, v := range env { 950 if strings.HasPrefix(v, evplus) { 951 found = true 952 env[i] = evplus + evv 953 } 954 } 955 if !found { 956 env = append(env, evplus+evv) 957 } 958 return env 959 } 960 961 // asCommandLine renders cmd as something that could be copy-and-pasted into a command line 962 // If cwd is not empty and different from the command's directory, prepend an appropriate "cd" 963 func asCommandLine(cwd string, cmd *exec.Cmd) string { 964 s := "(" 965 if cmd.Dir != "" && cmd.Dir != cwd { 966 s += "cd" + escape(cmd.Dir) + ";" 967 } 968 for _, e := range cmd.Env { 969 if !strings.HasPrefix(e, "PATH=") && 970 !strings.HasPrefix(e, "HOME=") && 971 !strings.HasPrefix(e, "USER=") && 972 !strings.HasPrefix(e, "SHELL=") { 973 s += escape(e) 974 } 975 } 976 for _, a := range cmd.Args { 977 s += escape(a) 978 } 979 s += " )" 980 return s 981 } 982 983 // escape inserts escapes appropriate for use in a shell command line 984 func escape(s string) string { 985 s = strings.Replace(s, "\\", "\\\\", -1) 986 s = strings.Replace(s, "'", "\\'", -1) 987 // Conservative guess at characters that will force quoting 988 if strings.ContainsAny(s, "\\ ;#*&$~?!|[]()<>{}`") { 989 s = " '" + s + "'" 990 } else { 991 s = " " + s 992 } 993 return s 994 } 995 996 func expect(want string, got tstring) { 997 if want != "" { 998 match, err := regexp.MatchString(want, got.o) 999 if err != nil { 1000 panic(fmt.Sprintf("Error for regexp %s, %v\n", want, err)) 1001 } 1002 if match { 1003 return 1004 } 1005 // Ignore error as we have already checked for it before 1006 match, _ = regexp.MatchString(want, got.e) 1007 if match { 1008 return 1009 } 1010 fmt.Printf("EXPECTED '%s'\n GOT O='%s'\nAND E='%s'\n", want, got.o, got.e) 1011 } 1012 }