github.com/mattn/go@v0.0.0-20171011075504-07f7db3ea99f/src/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 debug_test reference files") 26 var verbose = flag.Bool("v", false, "print more information about what's happening") 27 var dryrun = flag.Bool("n", false, "just print the command line and first bits") 28 var delve = flag.Bool("d", false, "use delve instead of gdb") 29 var force = flag.Bool("f", false, "force run under not linux-amd64; also do not use tempdir") 30 31 var hexRe = regexp.MustCompile("0x[a-zA-Z0-9]+") 32 var numRe = regexp.MustCompile("-?[0-9]+") 33 var stringRe = regexp.MustCompile("\"([^\\\"]|(\\.))*\"") 34 35 var gdb = "gdb" // Might be "ggdb" on Darwin, because gdb no longer part of XCode 36 37 // TestNexting go-builds a file, then uses a debugger (default gdb, optionally delve) 38 // to next through the generated executable, recording each line landed at, and 39 // then compares those lines with reference file(s). 40 // Flag -u updates the reference file(s). 41 // Flag -d changes the debugger to delve (and uses delve-specific reference files) 42 // Flag -v is ever-so-slightly verbose. 43 // Flag -n is for dry-run, and prints the shell and first debug commands. 44 // 45 // The file being tested may contain comments of the form 46 // //DBG-TAG=(v1,v2,v3) 47 // where DBG = {gdb,dlv} and TAG={dbg,opt} 48 // each variable may optionally be followed by a / and one or more of S,A,N 49 // to indicate normalization of Strings, (hex) addresses, and numbers. 50 // For example: 51 /* 52 if len(os.Args) > 1 { //gdb-dbg=(hist/A,cannedInput/A) //dlv-dbg=(hist/A,cannedInput/A) 53 */ 54 // TODO: not implemented for Delve yet, but this is the plan 55 // 56 // After a compiler change that causes a difference in the debug behavior, check 57 // to see if it is sensible or not, and if it is, update the reference files with 58 // go test debug_test.go -args -u 59 // (for Delve) 60 // go test debug_test.go -args -u -d 61 62 func TestNexting(t *testing.T) { 63 // Skip this test in an ordinary run.bash. Too many things 64 // can cause it to break. 65 if testing.Short() { 66 t.Skip("skipping in short mode; see issue #22206") 67 } 68 69 testenv.MustHaveGoBuild(t) 70 71 if !*delve && !*force && !(runtime.GOOS == "linux" && runtime.GOARCH == "amd64") { 72 // Running gdb on OSX/darwin is very flaky. 73 // Sometimes it is called ggdb, depending on how it is installed. 74 // It also probably requires an admin password typed into a dialog box. 75 // Various architectures tend to differ slightly sometimes, and keeping them 76 // all in sync is a pain for people who don't have them all at hand, 77 // so limit testing to amd64 (for now) 78 79 t.Skip("Skipped unless -d (delve), -f (force), or linux-amd64") 80 } 81 82 if *delve { 83 _, err := exec.LookPath("dlv") 84 if err != nil { 85 t.Fatal("dlv specified on command line with -d but no dlv on path") 86 } 87 } else { 88 _, err := exec.LookPath(gdb) 89 if err != nil { 90 if runtime.GOOS != "darwin" { 91 t.Skip("Skipped because gdb not available") 92 } 93 _, err = exec.LookPath("ggdb") 94 if err != nil { 95 t.Skip("Skipped because gdb (and also ggdb) not available") 96 } 97 gdb = "ggdb" 98 } 99 } 100 101 testNexting(t, "hist", "dbg", "-N -l") 102 // If this is test is run with a runtime compiled with -N -l, it is very likely to fail. 103 // This occurs in the noopt builders (for example). 104 if gogcflags := os.Getenv("GO_GCFLAGS"); *force || !strings.Contains(gogcflags, "-N") && !strings.Contains(gogcflags, "-l") { 105 testNexting(t, "hist", "opt", "") 106 } 107 } 108 109 func testNexting(t *testing.T, base, tag, gcflags string) { 110 // (1) In testdata, build sample.go into sample 111 // (2) Run debugger gathering a history 112 // (3) Read expected history from testdata/sample.nexts 113 // optionally, write out testdata/sample.nexts 114 115 exe := filepath.Join("testdata", base) 116 logbase := exe + "-" + tag 117 tmpbase := logbase + "-test" 118 119 if !*force { 120 tmpdir, err := ioutil.TempDir("", "debug_test") 121 if err != nil { 122 panic(fmt.Sprintf("Problem creating TempDir, error %v\n", err)) 123 } 124 exe = filepath.Join(tmpdir, base) 125 tmpbase = exe + "-" + tag + "-test" 126 if *verbose { 127 fmt.Printf("Tempdir is %s\n", tmpdir) 128 } 129 defer os.RemoveAll(tmpdir) 130 } 131 132 if gcflags == "" { 133 runGo(t, "", "build", "-o", exe, filepath.Join("testdata", base+".go")) 134 } else { 135 runGo(t, "", "build", "-o", exe, "-gcflags", gcflags, filepath.Join("testdata", base+".go")) 136 } 137 var h1 *nextHist 138 var nextlog, tmplog string 139 if *delve { 140 h1 = dlvTest(tag, exe, 1000) 141 nextlog = logbase + ".delve-nexts" 142 tmplog = tmpbase + ".delve-nexts" 143 } else { 144 h1 = gdbTest(tag, exe, 1000) 145 nextlog = logbase + ".gdb-nexts" 146 tmplog = tmpbase + ".gdb-nexts" 147 } 148 if *dryrun { 149 fmt.Printf("# Tag for above is %s\n", tag) 150 return 151 } 152 if *update { 153 h1.write(nextlog) 154 } else { 155 h0 := &nextHist{} 156 h0.read(nextlog) 157 if !h0.equals(h1) { 158 // Be very noisy about exactly what's wrong to simplify debugging. 159 h1.write(tmplog) 160 cmd := exec.Command("diff", "-u", nextlog, tmplog) 161 line := asCommandLine("", cmd) 162 bytes, err := cmd.CombinedOutput() 163 if err != nil && len(bytes) == 0 { 164 t.Fatalf("step/next histories differ, diff command %s failed with error=%v", line, err) 165 } 166 t.Fatalf("step/next histories differ, diff=\n%s", string(bytes)) 167 } 168 } 169 } 170 171 type dbgr interface { 172 start() 173 do(s string) 174 stepnext(s string) bool // step or next, possible with parameter, gets line etc. returns true for success, false for unsure response 175 quit() 176 hist() *nextHist 177 } 178 179 func gdbTest(tag, executable string, maxNext int, args ...string) *nextHist { 180 dbg := newGdb(tag, executable, args...) 181 dbg.start() 182 if *dryrun { 183 return nil 184 } 185 for i := 0; i < maxNext; i++ { 186 if !dbg.stepnext("n") { 187 break 188 } 189 } 190 h := dbg.hist() 191 return h 192 } 193 194 func dlvTest(tag, executable string, maxNext int, args ...string) *nextHist { 195 dbg := newDelve(tag, executable, args...) 196 dbg.start() 197 if *dryrun { 198 return nil 199 } 200 for i := 0; i < maxNext; i++ { 201 if !dbg.stepnext("n") { 202 break 203 } 204 } 205 h := dbg.hist() 206 return h 207 } 208 209 func runGo(t *testing.T, dir string, args ...string) string { 210 var stdout, stderr bytes.Buffer 211 cmd := exec.Command(testenv.GoToolPath(t), args...) 212 cmd.Dir = dir 213 if *dryrun { 214 fmt.Printf("%s\n", asCommandLine("", cmd)) 215 return "" 216 } 217 cmd.Stdout = &stdout 218 cmd.Stderr = &stderr 219 220 if err := cmd.Run(); err != nil { 221 t.Fatalf("error running cmd (%s): %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String()) 222 } 223 224 if s := stderr.String(); s != "" { 225 t.Fatalf("Stderr = %s\nWant empty", s) 226 } 227 228 return stdout.String() 229 } 230 231 type tstring struct { 232 o string 233 e string 234 } 235 236 func (t tstring) String() string { 237 return t.o + t.e 238 } 239 240 type pos struct { 241 line uint16 242 file uint8 243 } 244 245 type nextHist struct { 246 f2i map[string]uint8 247 fs []string 248 ps []pos // TODO: plan to automatically do the minimum distance conversion between a reference and a run for nicer errors. 249 texts []string 250 vars [][]string 251 } 252 253 func (h *nextHist) write(filename string) { 254 file, err := os.Create(filename) 255 if err != nil { 256 panic(fmt.Sprintf("Problem opening %s, error %v\n", filename, err)) 257 } 258 defer file.Close() 259 var lastfile uint8 260 for i, x := range h.texts { 261 p := h.ps[i] 262 if lastfile != p.file { 263 fmt.Fprintf(file, " %s\n", h.fs[p.file-1]) 264 lastfile = p.file 265 } 266 fmt.Fprintf(file, "%d:%s\n", p.line, x) 267 // Vars must begin with a dollar-sign. 268 // TODO, normalize between gdb and dlv into a common, comparable format. 269 for _, y := range h.vars[i] { 270 y = strings.TrimSpace(y) 271 if y[0] != '$' { 272 panic(fmt.Sprintf("Var line '%s' must begin with $, but does not\n", y)) 273 } 274 fmt.Fprintf(file, "%s\n", y) 275 } 276 } 277 file.Close() 278 } 279 280 func (h *nextHist) read(filename string) { 281 h.f2i = make(map[string]uint8) 282 bytes, err := ioutil.ReadFile(filename) 283 if err != nil { 284 panic(fmt.Sprintf("Problem reading %s, error %v\n", filename, err)) 285 } 286 var lastfile string 287 lines := strings.Split(string(bytes), "\n") 288 for i, l := range lines { 289 if len(l) > 0 && l[0] != '#' { 290 if l[0] == ' ' { 291 // file -- first two characters expected to be " " 292 lastfile = strings.TrimSpace(l) 293 } else if l[0] == '$' { 294 h.addVar(l) 295 } else { 296 // line number -- <number>:<line> 297 colonPos := strings.Index(l, ":") 298 if colonPos == -1 { 299 panic(fmt.Sprintf("Line %d (%s) in file %s expected to contain '<number>:' but does not.\n", i+1, l, filename)) 300 } 301 h.add(lastfile, l[0:colonPos], l[colonPos+1:]) 302 } 303 } 304 } 305 } 306 307 func (h *nextHist) add(file, line, text string) { 308 fi := h.f2i[file] 309 if fi == 0 { 310 h.fs = append(h.fs, file) 311 fi = uint8(len(h.fs)) 312 h.f2i[file] = fi 313 } 314 315 line = strings.TrimSpace(line) 316 var li int 317 var err error 318 if line != "" { 319 li, err = strconv.Atoi(line) 320 if err != nil { 321 panic(fmt.Sprintf("Non-numeric line: %s, error %v\n", line, err)) 322 } 323 } 324 h.ps = append(h.ps, pos{line: uint16(li), file: fi}) 325 h.texts = append(h.texts, text) 326 h.vars = append(h.vars, []string{}) 327 } 328 329 func (h *nextHist) addVar(text string) { 330 l := len(h.texts) 331 h.vars[l-1] = append(h.vars[l-1], text) 332 } 333 334 func invertMapSU8(hf2i map[string]uint8) map[uint8]string { 335 hi2f := make(map[uint8]string) 336 for hs, i := range hf2i { 337 hi2f[i] = hs 338 } 339 return hi2f 340 } 341 342 func (h *nextHist) equals(k *nextHist) bool { 343 if len(h.f2i) != len(k.f2i) { 344 return false 345 } 346 if len(h.ps) != len(k.ps) { 347 return false 348 } 349 hi2f := invertMapSU8(h.f2i) 350 ki2f := invertMapSU8(k.f2i) 351 352 for i, hs := range hi2f { 353 if hs != ki2f[i] { 354 return false 355 } 356 } 357 358 for i, x := range h.ps { 359 if k.ps[i] != x { 360 return false 361 } 362 } 363 return true 364 } 365 366 func canonFileName(f string) string { 367 i := strings.Index(f, "/src/") 368 if i != -1 { 369 f = f[i+1:] 370 } 371 return f 372 } 373 374 /* Delve */ 375 376 type delveState struct { 377 cmd *exec.Cmd 378 tag string 379 *ioState 380 atLineRe *regexp.Regexp // "\n =>" 381 funcFileLinePCre *regexp.Regexp // "^> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)" 382 line string 383 file string 384 function string 385 } 386 387 func newDelve(tag, executable string, args ...string) dbgr { 388 cmd := exec.Command("dlv", "exec", executable) 389 cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb") 390 if len(args) > 0 { 391 cmd.Args = append(cmd.Args, "--") 392 cmd.Args = append(cmd.Args, args...) 393 } 394 s := &delveState{tag: tag, cmd: cmd} 395 // HAHA Delve has control characters embedded to change the color of the => and the line number 396 // that would be '(\\x1b\\[[0-9;]+m)?' OR TERM=dumb 397 s.atLineRe = regexp.MustCompile("\n=>[[:space:]]+[0-9]+:(.*)") 398 s.funcFileLinePCre = regexp.MustCompile("> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)[)]\n") 399 s.ioState = newIoState(s.cmd) 400 return s 401 } 402 403 func (s *delveState) stepnext(ss string) bool { 404 x := s.ioState.writeReadExpect(ss+"\n", "[(]dlv[)] ") 405 excerpts := s.atLineRe.FindStringSubmatch(x.o) 406 locations := s.funcFileLinePCre.FindStringSubmatch(x.o) 407 excerpt := "" 408 if len(excerpts) > 1 { 409 excerpt = excerpts[1] 410 } 411 if len(locations) > 0 { 412 fn := canonFileName(locations[2]) 413 if *verbose { 414 if s.file != fn { 415 fmt.Printf("%s\n", locations[2]) // don't canonocalize verbose logging 416 } 417 fmt.Printf(" %s\n", locations[3]) 418 } 419 s.line = locations[3] 420 s.file = fn 421 s.function = locations[1] 422 s.ioState.history.add(s.file, s.line, excerpt) 423 return true 424 } 425 fmt.Printf("DID NOT MATCH EXPECTED NEXT OUTPUT\nO='%s'\nE='%s'\n", x.o, x.e) 426 return false 427 } 428 429 func (s *delveState) start() { 430 if *dryrun { 431 fmt.Printf("%s\n", asCommandLine("", s.cmd)) 432 fmt.Printf("b main.main\n") 433 fmt.Printf("c\n") 434 return 435 } 436 err := s.cmd.Start() 437 if err != nil { 438 line := asCommandLine("", s.cmd) 439 panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err)) 440 } 441 s.ioState.readExpecting(-1, 5000, "Type 'help' for list of commands.") 442 expect("Breakpoint [0-9]+ set at ", s.ioState.writeRead("b main.main\n")) 443 s.stepnext("c") 444 } 445 446 func (s *delveState) quit() { 447 s.do("q") 448 } 449 450 func (s *delveState) do(ss string) { 451 expect("", s.ioState.writeRead(ss+"\n")) 452 } 453 454 /* Gdb */ 455 456 type gdbState struct { 457 cmd *exec.Cmd 458 tag string 459 args []string 460 *ioState 461 atLineRe *regexp.Regexp 462 funcFileLinePCre *regexp.Regexp 463 line string 464 file string 465 function string 466 } 467 468 func newGdb(tag, executable string, args ...string) dbgr { 469 // Turn off shell, necessary for Darwin apparently 470 cmd := exec.Command(gdb, "-ex", "set startup-with-shell off", executable) 471 cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb") 472 s := &gdbState{tag: tag, cmd: cmd, args: args} 473 s.atLineRe = regexp.MustCompile("(^|\n)([0-9]+)(.*)") 474 s.funcFileLinePCre = regexp.MustCompile( 475 "([^ ]+) [(][)][ \\t\\n]+at ([^:]+):([0-9]+)") 476 // runtime.main () at /Users/drchase/GoogleDrive/work/go/src/runtime/proc.go:201 477 // function file line 478 // Thread 2 hit Breakpoint 1, main.main () at /Users/drchase/GoogleDrive/work/debug/hist.go:18 479 s.ioState = newIoState(s.cmd) 480 return s 481 } 482 483 func (s *gdbState) start() { 484 run := "run" 485 for _, a := range s.args { 486 run += " " + a // Can't quote args for gdb, it will pass them through including the quotes 487 } 488 if *dryrun { 489 fmt.Printf("%s\n", asCommandLine("", s.cmd)) 490 fmt.Printf("b main.main\n") 491 fmt.Printf("%s\n", run) 492 return 493 } 494 err := s.cmd.Start() 495 if err != nil { 496 line := asCommandLine("", s.cmd) 497 panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err)) 498 } 499 s.ioState.readExpecting(-1, 5000, "[(]gdb[)] ") 500 x := s.ioState.writeReadExpect("b main.main\n", "[(]gdb[)] ") 501 expect("Breakpoint [0-9]+ at", x) 502 s.stepnext(run) 503 } 504 505 func (s *gdbState) stepnext(ss string) bool { 506 x := s.ioState.writeReadExpect(ss+"\n", "[(]gdb[)] ") 507 excerpts := s.atLineRe.FindStringSubmatch(x.o) 508 locations := s.funcFileLinePCre.FindStringSubmatch(x.o) 509 excerpt := "" 510 if len(excerpts) == 0 && len(locations) == 0 { 511 fmt.Printf("DID NOT MATCH %s", x.o) 512 return false 513 } 514 if len(excerpts) > 0 { 515 excerpt = excerpts[3] 516 } 517 if len(locations) > 0 { 518 fn := canonFileName(locations[2]) 519 if *verbose { 520 if s.file != fn { 521 fmt.Printf("%s\n", locations[2]) 522 } 523 fmt.Printf(" %s\n", locations[3]) 524 } 525 s.line = locations[3] 526 s.file = fn 527 s.function = locations[1] 528 s.ioState.history.add(s.file, s.line, excerpt) 529 } 530 if len(excerpts) > 0 { 531 if *verbose { 532 fmt.Printf(" %s\n", excerpts[2]) 533 } 534 s.line = excerpts[2] 535 s.ioState.history.add(s.file, s.line, excerpt) 536 } 537 538 // Look for //gdb-<tag>=(v1,v2,v3) and print v1, v2, v3 539 vars := varsToPrint(excerpt, "//gdb-"+s.tag+"=(") 540 for _, v := range vars { 541 slashIndex := strings.Index(v, "/") 542 substitutions := "" 543 if slashIndex != -1 { 544 substitutions = v[slashIndex:] 545 v = v[:slashIndex] 546 } 547 response := s.ioState.writeRead("p " + v + "\n").String() 548 // expect something like "$1 = ..." 549 dollar := strings.Index(response, "$") 550 cr := strings.Index(response, "\n") 551 if dollar == -1 { 552 if cr == -1 { 553 response = strings.TrimSpace(response) // discards trailing newline 554 response = strings.Replace(response, "\n", "<BR>", -1) 555 s.ioState.history.addVar("$ Malformed response " + response) 556 continue 557 } 558 response = strings.TrimSpace(response[:cr]) 559 s.ioState.history.addVar("$ " + response) 560 continue 561 } 562 if cr == -1 { 563 cr = len(response) 564 } 565 response = strings.TrimSpace(response[dollar:cr]) 566 if strings.Contains(substitutions, "A") { 567 response = hexRe.ReplaceAllString(response, "<A>") 568 } 569 if strings.Contains(substitutions, "N") { 570 response = numRe.ReplaceAllString(response, "<N>") 571 } 572 if strings.Contains(substitutions, "S") { 573 response = stringRe.ReplaceAllString(response, "<S>") 574 } 575 s.ioState.history.addVar(response) 576 } 577 return true 578 } 579 580 func varsToPrint(line, lookfor string) []string { 581 var vars []string 582 if strings.Contains(line, lookfor) { 583 x := line[strings.Index(line, lookfor)+len(lookfor):] 584 end := strings.Index(x, ")") 585 if end == -1 { 586 panic(fmt.Sprintf("Saw variable list begin %s in %s but no closing ')'", lookfor, line)) 587 } 588 vars = strings.Split(x[:end], ",") 589 for i, y := range vars { 590 vars[i] = strings.TrimSpace(y) 591 } 592 } 593 return vars 594 } 595 596 func (s *gdbState) quit() { 597 response := s.ioState.writeRead("q\n") 598 if strings.Contains(response.o, "Quit anyway? (y or n)") { 599 s.ioState.writeRead("Y\n") 600 } 601 } 602 603 func (s *gdbState) do(ss string) { 604 expect("", s.ioState.writeRead(ss+"\n")) 605 } 606 607 type ioState struct { 608 stdout io.ReadCloser 609 stderr io.ReadCloser 610 stdin io.WriteCloser 611 outChan chan string 612 errChan chan string 613 last tstring // Output of previous step 614 history *nextHist 615 } 616 617 func newIoState(cmd *exec.Cmd) *ioState { 618 var err error 619 s := &ioState{} 620 s.history = &nextHist{} 621 s.history.f2i = make(map[string]uint8) 622 s.stdout, err = cmd.StdoutPipe() 623 line := asCommandLine("", cmd) 624 if err != nil { 625 panic(fmt.Sprintf("There was an error [stdoutpipe] running '%s', %v\n", line, err)) 626 } 627 s.stderr, err = cmd.StderrPipe() 628 if err != nil { 629 panic(fmt.Sprintf("There was an error [stdouterr] running '%s', %v\n", line, err)) 630 } 631 s.stdin, err = cmd.StdinPipe() 632 if err != nil { 633 panic(fmt.Sprintf("There was an error [stdinpipe] running '%s', %v\n", line, err)) 634 } 635 636 s.outChan = make(chan string, 1) 637 s.errChan = make(chan string, 1) 638 go func() { 639 buffer := make([]byte, 4096) 640 for { 641 n, err := s.stdout.Read(buffer) 642 if n > 0 { 643 s.outChan <- string(buffer[0:n]) 644 } 645 if err == io.EOF || n == 0 { 646 break 647 } 648 if err != nil { 649 fmt.Printf("Saw an error forwarding stdout") 650 break 651 } 652 } 653 close(s.outChan) 654 s.stdout.Close() 655 }() 656 657 go func() { 658 buffer := make([]byte, 4096) 659 for { 660 n, err := s.stderr.Read(buffer) 661 if n > 0 { 662 s.errChan <- string(buffer[0:n]) 663 } 664 if err == io.EOF || n == 0 { 665 break 666 } 667 if err != nil { 668 fmt.Printf("Saw an error forwarding stderr") 669 break 670 } 671 } 672 close(s.errChan) 673 s.stderr.Close() 674 }() 675 return s 676 } 677 678 func (s *ioState) hist() *nextHist { 679 return s.history 680 } 681 682 const ( 683 interlineDelay = 300 684 ) 685 686 func (s *ioState) writeRead(ss string) tstring { 687 if *verbose { 688 fmt.Printf("=> %s", ss) 689 } 690 _, err := io.WriteString(s.stdin, ss) 691 if err != nil { 692 panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err)) 693 } 694 return s.readWithDelay(-1, interlineDelay) 695 } 696 697 func (s *ioState) writeReadExpect(ss, expect string) tstring { 698 if *verbose { 699 fmt.Printf("=> %s", ss) 700 } 701 _, err := io.WriteString(s.stdin, ss) 702 if err != nil { 703 panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err)) 704 } 705 return s.readExpecting(-1, interlineDelay, expect) 706 } 707 708 func (s *ioState) readWithDelay(millis, interlineTimeout int) tstring { 709 return s.readExpecting(millis, interlineTimeout, "") 710 } 711 712 func (s *ioState) readExpecting(millis, interlineTimeout int, expected string) tstring { 713 timeout := time.Millisecond * time.Duration(millis) 714 interline := time.Millisecond * time.Duration(interlineTimeout) 715 s.last = tstring{} 716 var re *regexp.Regexp 717 if expected != "" { 718 re = regexp.MustCompile(expected) 719 } 720 loop: 721 for { 722 var timer <-chan time.Time 723 if timeout > 0 { 724 timer = time.After(timeout) 725 } 726 select { 727 case x, ok := <-s.outChan: 728 if !ok { 729 s.outChan = nil 730 } 731 s.last.o += x 732 case x, ok := <-s.errChan: 733 if !ok { 734 s.errChan = nil 735 } 736 s.last.e += x 737 case <-timer: 738 break loop 739 } 740 if re != nil { 741 if re.MatchString(s.last.o) { 742 break 743 } 744 if re.MatchString(s.last.e) { 745 break 746 } 747 } 748 timeout = interline 749 } 750 if *verbose { 751 fmt.Printf("<= %s%s", s.last.o, s.last.e) 752 } 753 return s.last 754 } 755 756 // replaceEnv returns a new environment derived from env 757 // by removing any existing definition of ev and adding ev=evv. 758 func replaceEnv(env []string, ev string, evv string) []string { 759 evplus := ev + "=" 760 var found bool 761 for i, v := range env { 762 if strings.HasPrefix(v, evplus) { 763 found = true 764 env[i] = evplus + evv 765 } 766 } 767 if !found { 768 env = append(env, evplus+evv) 769 } 770 return env 771 } 772 773 // asCommandLine renders cmd as something that could be copy-and-pasted into a command line 774 // If cwd is not empty and different from the command's directory, prepend an approprirate "cd" 775 func asCommandLine(cwd string, cmd *exec.Cmd) string { 776 s := "(" 777 if cmd.Dir != "" && cmd.Dir != cwd { 778 s += "cd" + escape(cmd.Dir) + ";" 779 } 780 for _, e := range cmd.Env { 781 if !strings.HasPrefix(e, "PATH=") && 782 !strings.HasPrefix(e, "HOME=") && 783 !strings.HasPrefix(e, "USER=") && 784 !strings.HasPrefix(e, "SHELL=") { 785 s += escape(e) 786 } 787 } 788 for _, a := range cmd.Args { 789 s += escape(a) 790 } 791 s += " )" 792 return s 793 } 794 795 // escape inserts escapes appropriate for use in a shell command line 796 func escape(s string) string { 797 s = strings.Replace(s, "\\", "\\\\", -1) 798 s = strings.Replace(s, "'", "\\'", -1) 799 // Conservative guess at characters that will force quoting 800 if strings.ContainsAny(s, "\\ ;#*&$~?!|[]()<>{}`") { 801 s = " '" + s + "'" 802 } else { 803 s = " " + s 804 } 805 return s 806 } 807 808 func expect(want string, got tstring) { 809 if want != "" { 810 match, err := regexp.MatchString(want, got.o) 811 if err != nil { 812 panic(fmt.Sprintf("Error for regexp %s, %v\n", want, err)) 813 } 814 if match { 815 return 816 } 817 match, err = regexp.MatchString(want, got.e) 818 if match { 819 return 820 } 821 fmt.Printf("EXPECTED '%s'\n GOT O='%s'\nAND E='%s'\n", want, got.o, got.e) 822 } 823 }