Source file src/cmd/compile/internal/ssa/debug_test.go

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

View as plain text