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