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