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

View as plain text