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  }