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  }