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