blob: d41788103d254c0eb9740673aca54cff6803ceed [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssa_test
import (
"bytes"
"flag"
"fmt"
"internal/testenv"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"testing"
"time"
)
var update = flag.Bool("u", false, "update debug_test reference files")
var verbose = flag.Bool("v", false, "print more information about what's happening")
var dryrun = flag.Bool("n", false, "just print the command line and first bits")
var delve = flag.Bool("d", false, "use delve instead of gdb")
var force = flag.Bool("f", false, "force run under not linux-amd64; also do not use tempdir")
var hexRe = regexp.MustCompile("0x[a-zA-Z0-9]+")
var numRe = regexp.MustCompile("-?[0-9]+")
var stringRe = regexp.MustCompile("\"([^\\\"]|(\\.))*\"")
// TestNexting go-builds a file, then uses a debugger (default gdb, optionally delve)
// to next through the generated executable, recording each line landed at, and
// then compares those lines with reference file(s).
// Flag -u updates the reference file(s).
// Flag -d changes the debugger to delve (and uses delve-specific reference files)
// Flag -v is ever-so-slightly verbose.
// Flag -n is for dry-run, and prints the shell and first debug commands.
//
// The file being tested may contain comments of the form
// //DBG-TAG=(v1,v2,v3)
// where DBG = {gdb,dlv} and TAG={dbg,opt}
// each variable may optionally be followed by a / and one or more of S,A,N
// to indicate normalization of Strings, (hex) addresses, and numbers.
// For example:
/*
if len(os.Args) > 1 { //gdb-dbg=(hist/A,cannedInput/A) //dlv-dbg=(hist/A,cannedInput/A)
*/
// TODO: not implemented for Delve yet, but this is the plan
//
// After a compiler change that causes a difference in the debug behavior, check
// to see if it is sensible or not, and if it is, update the reference files with
// go test debug_test.go -args -u
// (for Delve)
// go test debug_test.go -args -u -d
func TestNexting(t *testing.T) {
testenv.MustHaveGoBuild(t)
testNexting(t, "hist", "dbg", "-N -l")
testNexting(t, "hist", "opt", "")
}
func testNexting(t *testing.T, base, tag, gcflags string) {
// (1) In testdata, build sample.go into sample
// (2) Run debugger gathering a history
// (3) Read expected history from testdata/sample.nexts
// optionally, write out testdata/sample.nexts
if !*delve && !*force && !(runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
// Running gdb on OSX/darwin is very flaky.
// It also probably requires an admin password typed into a dialog box.
// Various architectures tend to differ slightly sometimes, and keeping them
// all in sync is a pain for people who don't have them all at hand,
// so limit testing to amd64 (for now)
t.Skip()
}
exe := filepath.Join("testdata", base)
logbase := exe + "-" + tag
tmpbase := logbase + "-test"
if !*force {
tmpdir, err := ioutil.TempDir("", "debug_test")
if err != nil {
panic(fmt.Sprintf("Problem creating TempDir, error %v\n", err))
}
exe = filepath.Join(tmpdir, base)
tmpbase = exe + "-" + tag + "-test"
if *verbose {
fmt.Printf("Tempdir is %s\n", tmpdir)
}
defer os.RemoveAll(tmpdir)
}
if gcflags == "" {
runGo(t, "", "build", "-o", exe, filepath.Join("testdata", base+".go"))
} else {
runGo(t, "", "build", "-o", exe, "-gcflags", gcflags, filepath.Join("testdata", base+".go"))
}
var h1 *nextHist
var nextlog, tmplog string
if *delve {
h1 = dlvTest(tag, exe, 1000)
nextlog = logbase + ".delve-nexts"
tmplog = tmpbase + ".delve-nexts"
} else {
h1 = gdbTest(tag, exe, 1000)
nextlog = logbase + ".gdb-nexts"
tmplog = tmpbase + ".gdb-nexts"
}
if *dryrun {
fmt.Printf("# Tag for above is %s\n", tag)
return
}
if *update {
h1.write(nextlog)
} else {
h0 := &nextHist{}
h0.read(nextlog)
if !h0.equals(h1) {
// Be very noisy about exactly what's wrong to simplify debugging.
h1.write(tmplog)
cmd := exec.Command("diff", "-u", nextlog, tmplog)
line := asCommandLine("", cmd)
bytes, err := cmd.CombinedOutput()
if err != nil && len(bytes) == 0 {
t.Fatalf("step/next histories differ, diff command %s failed with error=%v", line, err)
}
t.Fatalf("step/next histories differ, diff=\n%s", string(bytes))
}
}
}
type dbgr interface {
start()
do(s string)
stepnext(s string) bool // step or next, possible with parameter, gets line etc. returns true for success, false for unsure response
quit()
hist() *nextHist
}
func gdbTest(tag, executable string, maxNext int, args ...string) *nextHist {
dbg := newGdb(tag, executable, args...)
dbg.start()
if *dryrun {
return nil
}
for i := 0; i < maxNext; i++ {
if !dbg.stepnext("n") {
break
}
}
h := dbg.hist()
return h
}
func dlvTest(tag, executable string, maxNext int, args ...string) *nextHist {
dbg := newDelve(tag, executable, args...)
dbg.start()
if *dryrun {
return nil
}
for i := 0; i < maxNext; i++ {
if !dbg.stepnext("n") {
break
}
}
h := dbg.hist()
return h
}
func runGo(t *testing.T, dir string, args ...string) string {
var stdout, stderr bytes.Buffer
cmd := exec.Command(testenv.GoToolPath(t), args...)
cmd.Dir = dir
if *dryrun {
fmt.Printf("%s\n", asCommandLine("", cmd))
return ""
}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
t.Fatalf("error running cmd (%s): %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String())
}
if s := stderr.String(); s != "" {
t.Fatalf("Stderr = %s\nWant empty", s)
}
return stdout.String()
}
type tstring struct {
o string
e string
}
func (t tstring) String() string {
return t.o + t.e
}
type pos struct {
line uint16
file uint8
}
type nextHist struct {
f2i map[string]uint8
fs []string
ps []pos // TODO: plan to automatically do the minimum distance conversion between a reference and a run for nicer errors.
texts []string
vars [][]string
}
func (h *nextHist) write(filename string) {
file, err := os.Create(filename)
if err != nil {
panic(fmt.Sprintf("Problem opening %s, error %v\n", filename, err))
}
defer file.Close()
var lastfile uint8
for i, x := range h.texts {
p := h.ps[i]
if lastfile != p.file {
fmt.Fprintf(file, " %s\n", h.fs[p.file-1])
lastfile = p.file
}
fmt.Fprintf(file, "%d:%s\n", p.line, x)
// Vars must begin with a dollar-sign.
// TODO, normalize between gdb and dlv into a common, comparable format.
for _, y := range h.vars[i] {
y = strings.TrimSpace(y)
if y[0] != '$' {
panic(fmt.Sprintf("Var line '%s' must begin with $, but does not\n", y))
}
fmt.Fprintf(file, "%s\n", y)
}
}
file.Close()
}
func (h *nextHist) read(filename string) {
h.f2i = make(map[string]uint8)
bytes, err := ioutil.ReadFile(filename)
if err != nil {
panic(fmt.Sprintf("Problem reading %s, error %v\n", filename, err))
}
var lastfile string
lines := strings.Split(string(bytes), "\n")
for i, l := range lines {
if len(l) > 0 && l[0] != '#' {
if l[0] == ' ' {
// file -- first two characters expected to be " "
lastfile = strings.TrimSpace(l)
} else if l[0] == '$' {
h.addVar(l)
} else {
// line number -- <number>:<line>
colonPos := strings.Index(l, ":")
if colonPos == -1 {
panic(fmt.Sprintf("Line %d (%s) in file %s expected to contain '<number>:' but does not.\n", i+1, l, filename))
}
h.add(lastfile, l[0:colonPos], l[colonPos+1:])
}
}
}
}
func (h *nextHist) add(file, line, text string) {
fi := h.f2i[file]
if fi == 0 {
h.fs = append(h.fs, file)
fi = uint8(len(h.fs))
h.f2i[file] = fi
}
line = strings.TrimSpace(line)
var li int
var err error
if line != "" {
li, err = strconv.Atoi(line)
if err != nil {
panic(fmt.Sprintf("Non-numeric line: %s, error %v\n", line, err))
}
}
h.ps = append(h.ps, pos{line: uint16(li), file: fi})
h.texts = append(h.texts, text)
h.vars = append(h.vars, []string{})
}
func (h *nextHist) addVar(text string) {
l := len(h.texts)
h.vars[l-1] = append(h.vars[l-1], text)
}
func invertMapSU8(hf2i map[string]uint8) map[uint8]string {
hi2f := make(map[uint8]string)
for hs, i := range hf2i {
hsi := strings.Index(hs, "/src/")
if hsi != -1 {
hs = hs[hsi+1:]
}
hi2f[i] = hs
}
return hi2f
}
func (h *nextHist) equals(k *nextHist) bool {
if len(h.f2i) != len(k.f2i) {
return false
}
if len(h.ps) != len(k.ps) {
return false
}
hi2f := invertMapSU8(h.f2i)
ki2f := invertMapSU8(k.f2i)
for i, hs := range hi2f {
if hs != ki2f[i] {
return false
}
}
for i, x := range h.ps {
if k.ps[i] != x {
return false
}
}
return true
}
/* Delve */
type delveState struct {
cmd *exec.Cmd
tag string
*ioState
atLineRe *regexp.Regexp // "\n =>"
funcFileLinePCre *regexp.Regexp // "^> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)"
line string
file string
function string
}
func newDelve(tag, executable string, args ...string) dbgr {
cmd := exec.Command("dlv", "exec", executable)
cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb")
if len(args) > 0 {
cmd.Args = append(cmd.Args, "--")
cmd.Args = append(cmd.Args, args...)
}
s := &delveState{tag: tag, cmd: cmd}
// HAHA Delve has control characters embedded to change the color of the => and the line number
// that would be '(\\x1b\\[[0-9;]+m)?' OR TERM=dumb
s.atLineRe = regexp.MustCompile("\n=>[[:space:]]+[0-9]+:(.*)")
s.funcFileLinePCre = regexp.MustCompile("> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)[)]\n")
s.ioState = newIoState(s.cmd)
return s
}
func (s *delveState) stepnext(ss string) bool {
x := s.ioState.writeReadExpect(ss+"\n", "[(]dlv[)] ")
excerpts := s.atLineRe.FindStringSubmatch(x.o)
locations := s.funcFileLinePCre.FindStringSubmatch(x.o)
excerpt := ""
if len(excerpts) > 1 {
excerpt = excerpts[1]
}
if len(locations) > 0 {
if *verbose {
if s.file != locations[2] {
fmt.Printf("%s\n", locations[2])
}
fmt.Printf(" %s\n", locations[3])
}
s.line = locations[3]
s.file = locations[2]
s.function = locations[1]
s.ioState.history.add(s.file, s.line, excerpt)
return true
}
fmt.Printf("DID NOT MATCH EXPECTED NEXT OUTPUT\nO='%s'\nE='%s'\n", x.o, x.e)
return false
}
func (s *delveState) start() {
if *dryrun {
fmt.Printf("%s\n", asCommandLine("", s.cmd))
fmt.Printf("b main.main\n")
fmt.Printf("c\n")
return
}
err := s.cmd.Start()
if err != nil {
line := asCommandLine("", s.cmd)
panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err))
}
s.ioState.readExpecting(-1, 5000, "Type 'help' for list of commands.")
expect("Breakpoint [0-9]+ set at ", s.ioState.writeRead("b main.main\n"))
s.stepnext("c")
}
func (s *delveState) quit() {
s.do("q")
}
func (s *delveState) do(ss string) {
expect("", s.ioState.writeRead(ss+"\n"))
}
/* Gdb */
type gdbState struct {
cmd *exec.Cmd
tag string
args []string
*ioState
atLineRe *regexp.Regexp
funcFileLinePCre *regexp.Regexp
line string
file string
function string
}
func newGdb(tag, executable string, args ...string) dbgr {
gdb := "gdb"
if runtime.GOOS == "darwin" {
gdb = "ggdb" // A possibility on a Mac
}
cmd := exec.Command(gdb, executable)
cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb")
s := &gdbState{tag: tag, cmd: cmd, args: args}
s.atLineRe = regexp.MustCompile("(^|\n)([0-9]+)(.*)")
s.funcFileLinePCre = regexp.MustCompile(
"([^ ]+) [(][)][ \\t\\n]+at ([^:]+):([0-9]+)")
// runtime.main () at /Users/drchase/GoogleDrive/work/go/src/runtime/proc.go:201
// function file line
// Thread 2 hit Breakpoint 1, main.main () at /Users/drchase/GoogleDrive/work/debug/hist.go:18
s.ioState = newIoState(s.cmd)
return s
}
func (s *gdbState) start() {
run := "run"
for _, a := range s.args {
run += " " + a // Can't quote args for gdb, it will pass them through including the quotes
}
if *dryrun {
fmt.Printf("%s\n", asCommandLine("", s.cmd))
fmt.Printf("b main.main\n")
fmt.Printf("%s\n", run)
return
}
err := s.cmd.Start()
if err != nil {
line := asCommandLine("", s.cmd)
panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err))
}
s.ioState.readExpecting(-1, 5000, "[(]gdb[)] ")
x := s.ioState.writeReadExpect("b main.main\n", "[(]gdb[)] ")
expect("Breakpoint [0-9]+ at", x)
s.stepnext(run)
}
func (s *gdbState) stepnext(ss string) bool {
x := s.ioState.writeReadExpect(ss+"\n", "[(]gdb[)] ")
excerpts := s.atLineRe.FindStringSubmatch(x.o)
locations := s.funcFileLinePCre.FindStringSubmatch(x.o)
excerpt := ""
if len(excerpts) == 0 && len(locations) == 0 {
fmt.Printf("DID NOT MATCH %s", x.o)
return false
}
if len(excerpts) > 0 {
excerpt = excerpts[3]
}
if len(locations) > 0 {
if *verbose {
if s.file != locations[2] {
fmt.Printf("%s\n", locations[2])
}
fmt.Printf(" %s\n", locations[3])
}
s.line = locations[3]
s.file = locations[2]
s.function = locations[1]
s.ioState.history.add(s.file, s.line, excerpt)
}
if len(excerpts) > 0 {
if *verbose {
fmt.Printf(" %s\n", excerpts[2])
}
s.line = excerpts[2]
s.ioState.history.add(s.file, s.line, excerpt)
}
// Look for //gdb-<tag>=(v1,v2,v3) and print v1, v2, v3
vars := varsToPrint(excerpt, "//gdb-"+s.tag+"=(")
for _, v := range vars {
slashIndex := strings.Index(v, "/")
substitutions := ""
if slashIndex != -1 {
substitutions = v[slashIndex:]
v = v[:slashIndex]
}
response := s.ioState.writeRead("p " + v + "\n").String()
// expect something like "$1 = ..."
dollar := strings.Index(response, "$")
cr := strings.Index(response, "\n")
if dollar == -1 {
if cr == -1 {
response = strings.TrimSpace(response) // discards trailing newline
response = strings.Replace(response, "\n", "<BR>", -1)
s.ioState.history.addVar("$ Malformed response " + response)
continue
}
response = strings.TrimSpace(response[:cr])
s.ioState.history.addVar("$ " + response)
continue
}
if cr == -1 {
cr = len(response)
}
response = strings.TrimSpace(response[dollar:cr])
if strings.Contains(substitutions, "A") {
response = hexRe.ReplaceAllString(response, "<A>")
}
if strings.Contains(substitutions, "N") {
response = numRe.ReplaceAllString(response, "<N>")
}
if strings.Contains(substitutions, "S") {
response = stringRe.ReplaceAllString(response, "<S>")
}
s.ioState.history.addVar(response)
}
return true
}
func varsToPrint(line, lookfor string) []string {
var vars []string
if strings.Contains(line, lookfor) {
x := line[strings.Index(line, lookfor)+len(lookfor):]
end := strings.Index(x, ")")
if end == -1 {
panic(fmt.Sprintf("Saw variable list begin %s in %s but no closing ')'", lookfor, line))
}
vars = strings.Split(x[:end], ",")
for i, y := range vars {
vars[i] = strings.TrimSpace(y)
}
}
return vars
}
func (s *gdbState) quit() {
response := s.ioState.writeRead("q\n")
if strings.Contains(response.o, "Quit anyway? (y or n)") {
s.ioState.writeRead("Y\n")
}
}
func (s *gdbState) do(ss string) {
expect("", s.ioState.writeRead(ss+"\n"))
}
type ioState struct {
stdout io.ReadCloser
stderr io.ReadCloser
stdin io.WriteCloser
outChan chan string
errChan chan string
last tstring // Output of previous step
history *nextHist
}
func newIoState(cmd *exec.Cmd) *ioState {
var err error
s := &ioState{}
s.history = &nextHist{}
s.history.f2i = make(map[string]uint8)
s.stdout, err = cmd.StdoutPipe()
line := asCommandLine("", cmd)
if err != nil {
panic(fmt.Sprintf("There was an error [stdoutpipe] running '%s', %v\n", line, err))
}
s.stderr, err = cmd.StderrPipe()
if err != nil {
panic(fmt.Sprintf("There was an error [stdouterr] running '%s', %v\n", line, err))
}
s.stdin, err = cmd.StdinPipe()
if err != nil {
panic(fmt.Sprintf("There was an error [stdinpipe] running '%s', %v\n", line, err))
}
s.outChan = make(chan string, 1)
s.errChan = make(chan string, 1)
go func() {
buffer := make([]byte, 4096)
for {
n, err := s.stdout.Read(buffer)
if n > 0 {
s.outChan <- string(buffer[0:n])
}
if err == io.EOF || n == 0 {
break
}
if err != nil {
fmt.Printf("Saw an error forwarding stdout")
break
}
}
close(s.outChan)
s.stdout.Close()
}()
go func() {
buffer := make([]byte, 4096)
for {
n, err := s.stderr.Read(buffer)
if n > 0 {
s.errChan <- string(buffer[0:n])
}
if err == io.EOF || n == 0 {
break
}
if err != nil {
fmt.Printf("Saw an error forwarding stderr")
break
}
}
close(s.errChan)
s.stderr.Close()
}()
return s
}
func (s *ioState) hist() *nextHist {
return s.history
}
const (
interlineDelay = 300
)
func (s *ioState) writeRead(ss string) tstring {
if *verbose {
fmt.Printf("=> %s", ss)
}
_, err := io.WriteString(s.stdin, ss)
if err != nil {
panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err))
}
return s.readWithDelay(-1, interlineDelay)
}
func (s *ioState) writeReadExpect(ss, expect string) tstring {
if *verbose {
fmt.Printf("=> %s", ss)
}
_, err := io.WriteString(s.stdin, ss)
if err != nil {
panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err))
}
return s.readExpecting(-1, interlineDelay, expect)
}
func (s *ioState) readWithDelay(millis, interlineTimeout int) tstring {
return s.readExpecting(millis, interlineTimeout, "")
}
func (s *ioState) readExpecting(millis, interlineTimeout int, expected string) tstring {
timeout := time.Millisecond * time.Duration(millis)
interline := time.Millisecond * time.Duration(interlineTimeout)
s.last = tstring{}
var re *regexp.Regexp
if expected != "" {
re = regexp.MustCompile(expected)
}
loop:
for {
var timer <-chan time.Time
if timeout > 0 {
timer = time.After(timeout)
}
select {
case x, ok := <-s.outChan:
if !ok {
s.outChan = nil
}
s.last.o += x
case x, ok := <-s.errChan:
if !ok {
s.errChan = nil
}
s.last.e += x
case <-timer:
break loop
}
if re != nil {
if re.MatchString(s.last.o) {
break
}
if re.MatchString(s.last.e) {
break
}
}
timeout = interline
}
if *verbose {
fmt.Printf("<= %s%s", s.last.o, s.last.e)
}
return s.last
}
// replaceEnv returns a new environment derived from env
// by removing any existing definition of ev and adding ev=evv.
func replaceEnv(env []string, ev string, evv string) []string {
evplus := ev + "="
var found bool
for i, v := range env {
if strings.HasPrefix(v, evplus) {
found = true
env[i] = evplus + evv
}
}
if !found {
env = append(env, evplus+evv)
}
return env
}
// asCommandLine renders cmd as something that could be copy-and-pasted into a command line
// If cwd is not empty and different from the command's directory, prepend an approprirate "cd"
func asCommandLine(cwd string, cmd *exec.Cmd) string {
s := "("
if cmd.Dir != "" && cmd.Dir != cwd {
s += "cd" + escape(cmd.Dir) + ";"
}
for _, e := range cmd.Env {
if !strings.HasPrefix(e, "PATH=") &&
!strings.HasPrefix(e, "HOME=") &&
!strings.HasPrefix(e, "USER=") &&
!strings.HasPrefix(e, "SHELL=") {
s += escape(e)
}
}
for _, a := range cmd.Args {
s += escape(a)
}
s += " )"
return s
}
// escape inserts escapes appropriate for use in a shell command line
func escape(s string) string {
s = strings.Replace(s, "\\", "\\\\", -1)
s = strings.Replace(s, "'", "\\'", -1)
// Conservative guess at characters that will force quoting
if strings.ContainsAny(s, "\\ ;#*&$~?!|[]()<>{}`") {
s = " '" + s + "'"
} else {
s = " " + s
}
return s
}
func expect(want string, got tstring) {
if want != "" {
match, err := regexp.MatchString(want, got.o)
if err != nil {
panic(fmt.Sprintf("Error for regexp %s, %v\n", want, err))
}
if match {
return
}
match, err = regexp.MatchString(want, got.e)
if match {
return
}
fmt.Printf("EXPECTED '%s'\n GOT O='%s'\nAND E='%s'\n", want, got.o, got.e)
}
}