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