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