diff --git a/.travis.yml b/.travis.yml index d5e5dd5..e91cc9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,13 @@ language: go sudo: false go: - - 1.2 - - 1.3 - - 1.4 - - 1.5 - - 1.6 + - 1.7 + - 1.8 + - 1.9 - tip before_install: - go get github.com/mattn/goveralls - - go get golang.org/x/tools/cmd/cover script: - goveralls -service=travis-ci diff --git a/stack-go19_test.go b/stack-go19_test.go new file mode 100644 index 0000000..d7aeea2 --- /dev/null +++ b/stack-go19_test.go @@ -0,0 +1,67 @@ +// +build go1.9 + +package stack_test + +import ( + "runtime" + "testing" + + "github.com/go-stack/stack" +) + +func TestCallerInlinedPanic(t *testing.T) { + t.Parallel() + + var line int + + defer func() { + if recover() != nil { + var pcs [32]uintptr + n := runtime.Callers(1, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + // count frames to runtime.sigpanic + panicIdx := 0 + for { + f, more := frames.Next() + if f.Function == "runtime.sigpanic" { + break + } + panicIdx++ + if !more { + t.Fatal("no runtime.sigpanic entry on the stack") + } + } + + c := stack.Caller(panicIdx) + if got, want := c.Frame().Function, "runtime.sigpanic"; got != want { + t.Errorf("sigpanic frame: got name == %v, want name == %v", got, want) + } + + c1 := stack.Caller(panicIdx + 1) + if got, want := c1.Frame().Function, "github.com/go-stack/stack_test.inlinablePanic"; got != want { + t.Errorf("TestCallerInlinedPanic frame: got name == %v, want name == %v", got, want) + } + if got, want := c1.Frame().Line, line; got != want { + t.Errorf("TestCallerInlinedPanic frame: got line == %v, want line == %v", got, want) + } + } + }() + + doPanic(t, &line) + t.Fatal("failed to panic") +} + +func doPanic(t *testing.T, panicLine *int) { + _, _, line, ok := runtime.Caller(0) + *panicLine = line + 11 // adjust to match line of panic below + if !ok { + t.Fatal("runtime.Caller(0) failed") + } + inlinablePanic() +} + +func inlinablePanic() { + // Initiate a sigpanic. + var x *uintptr + _ = *x +} diff --git a/stack.go b/stack.go index 79840ed..1bc6971 100644 --- a/stack.go +++ b/stack.go @@ -1,3 +1,5 @@ +// +build go1.7 + // Package stack implements utilities to capture, manipulate, and format call // stacks. It provides a simpler API than package runtime. // @@ -21,29 +23,31 @@ import ( // Call records a single function invocation from a goroutine stack. type Call struct { - fn *runtime.Func - pc uintptr + frame runtime.Frame } // Caller returns a Call from the stack of the current goroutine. The argument // skip is the number of stack frames to ascend, with 0 identifying the // calling function. func Caller(skip int) Call { - var pcs [2]uintptr + // As of Go 1.9 we need room for up to three PC entries. + // + // 0. An entry for the stack frame prior to the target to check for + // special handling needed if that prior entry is runtime.sigpanic. + // 1. A possible second entry to hold metadata about skipped inlined + // functions. If inline functions were not skipped the target frame + // PC will be here. + // 2. A third entry for the target frame PC when the second entry + // is used for skipped inline functions. + var pcs [3]uintptr n := runtime.Callers(skip+1, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + frame, _ := frames.Next() + frame, _ = frames.Next() - var c Call - - if n < 2 { - return c + return Call{ + frame: frame, } - - c.pc = pcs[1] - if runtime.FuncForPC(pcs[0]).Name() != "runtime.sigpanic" { - c.pc-- - } - c.fn = runtime.FuncForPC(c.pc) - return c } // String implements fmt.Stinger. It is equivalent to fmt.Sprintf("%v", c). @@ -54,9 +58,10 @@ func (c Call) String() string { // MarshalText implements encoding.TextMarshaler. It formats the Call the same // as fmt.Sprintf("%v", c). func (c Call) MarshalText() ([]byte, error) { - if c.fn == nil { + if c.frame == (runtime.Frame{}) { return nil, ErrNoFunc } + buf := bytes.Buffer{} fmt.Fprint(&buf, c) return buf.Bytes(), nil @@ -83,19 +88,19 @@ var ErrNoFunc = errors.New("no call stack information") // %+v equivalent to %+s:%d // %#v equivalent to %#s:%d func (c Call) Format(s fmt.State, verb rune) { - if c.fn == nil { + if c.frame == (runtime.Frame{}) { fmt.Fprintf(s, "%%!%c(NOFUNC)", verb) return } switch verb { case 's', 'v': - file, line := c.fn.FileLine(c.pc) + file := c.frame.File switch { case s.Flag('#'): // done case s.Flag('+'): - file = file[pkgIndex(file, c.fn.Name()):] + file = file[pkgIndex(file, c.frame.Function):] default: const sep = "/" if i := strings.LastIndex(file, sep); i != -1 { @@ -105,16 +110,15 @@ func (c Call) Format(s fmt.State, verb rune) { io.WriteString(s, file) if verb == 'v' { buf := [7]byte{':'} - s.Write(strconv.AppendInt(buf[:1], int64(line), 10)) + s.Write(strconv.AppendInt(buf[:1], int64(c.frame.Line), 10)) } case 'd': - _, line := c.fn.FileLine(c.pc) buf := [6]byte{} - s.Write(strconv.AppendInt(buf[:0], int64(line), 10)) + s.Write(strconv.AppendInt(buf[:0], int64(c.frame.Line), 10)) case 'k': - name := c.fn.Name() + name := c.frame.Function const pathSep = "/" start, end := 0, len(name) if i := strings.LastIndex(name, pathSep); i != -1 { @@ -130,7 +134,7 @@ func (c Call) Format(s fmt.State, verb rune) { io.WriteString(s, name[start:end]) case 'n': - name := c.fn.Name() + name := c.frame.Function if !s.Flag('+') { const pathSep = "/" if i := strings.LastIndex(name, pathSep); i != -1 { @@ -145,35 +149,17 @@ func (c Call) Format(s fmt.State, verb rune) { } } +// Frame returns the call frame infomation for the Call. +func (c Call) Frame() runtime.Frame { + return c.frame +} + // PC returns the program counter for this call frame; multiple frames may // have the same PC value. +// +// Deprecated: Use Call.Frame instead. func (c Call) PC() uintptr { - return c.pc -} - -// name returns the import path qualified name of the function containing the -// call. -func (c Call) name() string { - if c.fn == nil { - return "???" - } - return c.fn.Name() -} - -func (c Call) file() string { - if c.fn == nil { - return "???" - } - file, _ := c.fn.FileLine(c.pc) - return file -} - -func (c Call) line() int { - if c.fn == nil { - return 0 - } - _, line := c.fn.FileLine(c.pc) - return line + return c.frame.PC } // CallStack records a sequence of function invocations from a goroutine @@ -197,9 +183,6 @@ func (cs CallStack) MarshalText() ([]byte, error) { buf := bytes.Buffer{} buf.Write(openBracketBytes) for i, pc := range cs { - if pc.fn == nil { - return nil, ErrNoFunc - } if i > 0 { buf.Write(spaceBytes) } @@ -227,18 +210,18 @@ func (cs CallStack) Format(s fmt.State, verb rune) { // identifying the calling function. func Trace() CallStack { var pcs [512]uintptr - n := runtime.Callers(2, pcs[:]) - cs := make([]Call, n) + n := runtime.Callers(1, pcs[:]) - for i, pc := range pcs[:n] { - pcFix := pc - if i > 0 && cs[i-1].fn.Name() != "runtime.sigpanic" { - pcFix-- - } - cs[i] = Call{ - fn: runtime.FuncForPC(pcFix), - pc: pcFix, - } + frames := runtime.CallersFrames(pcs[:n]) + cs := make(CallStack, 0, n) + + // Skip extra frame retrieved just to make sure the runtime.sigpanic + // special case is handled. + frame, more := frames.Next() + + for more { + frame, more = frames.Next() + cs = append(cs, Call{frame: frame}) } return cs @@ -247,7 +230,7 @@ func Trace() CallStack { // TrimBelow returns a slice of the CallStack with all entries below c // removed. func (cs CallStack) TrimBelow(c Call) CallStack { - for len(cs) > 0 && cs[0].pc != c.pc { + for len(cs) > 0 && cs[0] != c { cs = cs[1:] } return cs @@ -256,7 +239,7 @@ func (cs CallStack) TrimBelow(c Call) CallStack { // TrimAbove returns a slice of the CallStack with all entries above c // removed. func (cs CallStack) TrimAbove(c Call) CallStack { - for len(cs) > 0 && cs[len(cs)-1].pc != c.pc { + for len(cs) > 0 && cs[len(cs)-1] != c { cs = cs[:len(cs)-1] } return cs @@ -305,12 +288,13 @@ func pkgIndex(file, funcName string) int { var runtimePath string func init() { - var pcs [1]uintptr + var pcs [3]uintptr runtime.Callers(0, pcs[:]) - fn := runtime.FuncForPC(pcs[0]) - file, _ := fn.FileLine(pcs[0]) + frames := runtime.CallersFrames(pcs[:]) + frame, _ := frames.Next() + file := frame.File - idx := pkgIndex(file, fn.Name()) + idx := pkgIndex(frame.File, frame.Function) runtimePath = file[:idx] if runtime.GOOS == "windows" { @@ -319,7 +303,7 @@ func init() { } func inGoroot(c Call) bool { - file := c.file() + file := c.frame.File if len(file) == 0 || file[0] == '?' { return true } diff --git a/stack_test.go b/stack_test.go index 6bccd56..44f3a7d 100644 --- a/stack_test.go +++ b/stack_test.go @@ -13,13 +13,192 @@ import ( "github.com/go-stack/stack" ) +func TestCaller(t *testing.T) { + t.Parallel() + + c := stack.Caller(0) + _, file, line, ok := runtime.Caller(0) + line-- + if !ok { + t.Fatal("runtime.Caller(0) failed") + } + + if got, want := c.Frame().File, file; got != want { + t.Errorf("got file == %v, want file == %v", got, want) + } + + if got, want := c.Frame().Line, line; got != want { + t.Errorf("got line == %v, want line == %v", got, want) + } +} + +func f3(f1 func() stack.Call) stack.Call { + return f2(f1) +} + +func f2(f1 func() stack.Call) stack.Call { + return f1() +} + +func TestCallerMidstackInlined(t *testing.T) { + t.Parallel() + + _, _, line, ok := runtime.Caller(0) + line -= 10 // adjust to return f1() line inside f2() + if !ok { + t.Fatal("runtime.Caller(0) failed") + } + + c := f3(func() stack.Call { + return stack.Caller(2) + }) + + if got, want := c.Frame().Line, line; got != want { + t.Errorf("got line == %v, want line == %v", got, want) + } + if got, want := c.Frame().Function, "github.com/go-stack/stack_test.f3"; got != want { + t.Errorf("got func name == %v, want func name == %v", got, want) + } +} + +func TestCallerPanic(t *testing.T) { + t.Parallel() + + var ( + line int + ok bool + ) + + defer func() { + if recover() != nil { + var pcs [32]uintptr + n := runtime.Callers(1, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + // count frames to runtime.sigpanic + panicIdx := 0 + for { + f, more := frames.Next() + if f.Function == "runtime.sigpanic" { + break + } + panicIdx++ + if !more { + t.Fatal("no runtime.sigpanic entry on the stack") + } + } + c := stack.Caller(panicIdx) + if got, want := c.Frame().Function, "runtime.sigpanic"; got != want { + t.Errorf("sigpanic frame: got name == %v, want name == %v", got, want) + } + c1 := stack.Caller(panicIdx + 1) + if got, want := c1.Frame().Function, "github.com/go-stack/stack_test.TestCallerPanic"; got != want { + t.Errorf("TestCallerPanic frame: got name == %v, want name == %v", got, want) + } + if got, want := c1.Frame().Line, line; got != want { + t.Errorf("TestCallerPanic frame: got line == %v, want line == %v", got, want) + } + } + }() + + _, _, line, ok = runtime.Caller(0) + line += 7 // adjust to match line of panic below + if !ok { + t.Fatal("runtime.Caller(0) failed") + } + // Initiate a sigpanic. + var x *uintptr + _ = *x +} + +type tholder struct { + trace func() stack.CallStack +} + +func (th *tholder) traceLabyrinth() stack.CallStack { + for { + return th.trace() + } +} + +func TestTrace(t *testing.T) { + t.Parallel() + + _, _, line, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller(0) failed") + } + + fh := tholder{ + trace: func() stack.CallStack { + cs := stack.Trace() + return cs + }, + } + + cs := fh.traceLabyrinth() + + lines := []int{line + 7, line - 7, line + 12} + + for i, line := range lines { + if got, want := cs[i].Frame().Line, line; got != want { + t.Errorf("got line[%d] == %v, want line[%d] == %v", i, got, i, want) + } + } +} + +// Test stack handling originating from a sigpanic. +func TestTracePanic(t *testing.T) { + t.Parallel() + + var ( + line int + ok bool + ) + + defer func() { + if recover() != nil { + trace := stack.Trace() + + // find runtime.sigpanic + panicIdx := -1 + for i, c := range trace { + if c.Frame().Function == "runtime.sigpanic" { + panicIdx = i + break + } + } + if panicIdx == -1 { + t.Fatal("no runtime.sigpanic entry on the stack") + } + if got, want := trace[panicIdx].Frame().Function, "runtime.sigpanic"; got != want { + t.Errorf("sigpanic frame: got name == %v, want name == %v", got, want) + } + if got, want := trace[panicIdx+1].Frame().Function, "github.com/go-stack/stack_test.TestTracePanic"; got != want { + t.Errorf("TestTracePanic frame: got name == %v, want name == %v", got, want) + } + if got, want := trace[panicIdx+1].Frame().Line, line; got != want { + t.Errorf("TestTracePanic frame: got line == %v, want line == %v", got, want) + } + } + }() + + _, _, line, ok = runtime.Caller(0) + line += 7 // adjust to match line of panic below + if !ok { + t.Fatal("runtime.Caller(0) failed") + } + // Initiate a sigpanic. + var x *uintptr + _ = *x +} + const importPath = "github.com/go-stack/stack" type testType struct{} -func (tt testType) testMethod() (c stack.Call, pc uintptr, file string, line int, ok bool) { +func (tt testType) testMethod() (c stack.Call, file string, line int, ok bool) { c = stack.Caller(0) - pc, file, line, ok = runtime.Caller(0) + _, file, line, ok = runtime.Caller(0) line-- return } @@ -28,14 +207,14 @@ func TestCallFormat(t *testing.T) { t.Parallel() c := stack.Caller(0) - pc, file, line, ok := runtime.Caller(0) + _, file, line, ok := runtime.Caller(0) line-- if !ok { t.Fatal("runtime.Caller(0) failed") } relFile := path.Join(importPath, filepath.Base(file)) - c2, pc2, file2, line2, ok2 := testType{}.testMethod() + c2, file2, line2, ok2 := testType{}.testMethod() if !ok2 { t.Fatal("runtime.Caller(0) failed") } @@ -54,7 +233,7 @@ func TestCallFormat(t *testing.T) { {c, "func", "%#s", file}, {c, "func", "%d", fmt.Sprint(line)}, {c, "func", "%n", "TestCallFormat"}, - {c, "func", "%+n", runtime.FuncForPC(pc - 1).Name()}, + {c, "func", "%+n", "github.com/go-stack/stack_test.TestCallFormat"}, {c, "func", "%k", "stack_test"}, {c, "func", "%+k", "github.com/go-stack/stack_test"}, {c, "func", "%v", fmt.Sprint(path.Base(file), ":", line)}, @@ -66,7 +245,7 @@ func TestCallFormat(t *testing.T) { {c2, "meth", "%#s", file2}, {c2, "meth", "%d", fmt.Sprint(line2)}, {c2, "meth", "%n", "testType.testMethod"}, - {c2, "meth", "%+n", runtime.FuncForPC(pc2).Name()}, + {c2, "meth", "%+n", "github.com/go-stack/stack_test.testType.testMethod"}, {c2, "meth", "%k", "stack_test"}, {c2, "meth", "%+k", "github.com/go-stack/stack_test"}, {c2, "meth", "%v", fmt.Sprint(path.Base(file2), ":", line2)}, @@ -92,7 +271,7 @@ func TestCallString(t *testing.T) { t.Fatal("runtime.Caller(0) failed") } - c2, _, file2, line2, ok2 := testType{}.testMethod() + c2, file2, line2, ok2 := testType{}.testMethod() if !ok2 { t.Fatal("runtime.Caller(0) failed") } @@ -125,7 +304,7 @@ func TestCallMarshalText(t *testing.T) { t.Fatal("runtime.Caller(0) failed") } - c2, _, file2, line2, ok2 := testType{}.testMethod() + c2, file2, line2, ok2 := testType{}.testMethod() if !ok2 { t.Fatal("runtime.Caller(0) failed") } @@ -178,6 +357,7 @@ func TestCallStackMarshalText(t *testing.T) { t.Errorf("\n got %v\nwant %v", got, want) } } + func getTrace(t *testing.T) (stack.CallStack, int) { cs := stack.Trace().TrimRuntime() _, _, line, ok := runtime.Caller(0) @@ -191,7 +371,7 @@ func getTrace(t *testing.T) (stack.CallStack, int) { func TestTrimAbove(t *testing.T) { trace := trimAbove() if got, want := len(trace), 2; got != want { - t.Errorf("got len(trace) == %v, want %v, trace: %n", got, want, trace) + t.Fatalf("got len(trace) == %v, want %v, trace: %n", got, want, trace) } if got, want := fmt.Sprintf("%n", trace[1]), "TestTrimAbove"; got != want { t.Errorf("got %q, want %q", got, want) diff --git a/stackinternal_test.go b/stackinternal_test.go deleted file mode 100644 index 774b7e0..0000000 --- a/stackinternal_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package stack - -import ( - "runtime" - "testing" -) - -func TestCaller(t *testing.T) { - t.Parallel() - - c := Caller(0) - _, file, line, ok := runtime.Caller(0) - line-- - if !ok { - t.Fatal("runtime.Caller(0) failed") - } - - if got, want := c.file(), file; got != want { - t.Errorf("got file == %v, want file == %v", got, want) - } - - if got, want := c.line(), line; got != want { - t.Errorf("got line == %v, want line == %v", got, want) - } -} - -type fholder struct { - f func() CallStack -} - -func (fh *fholder) labyrinth() CallStack { - for { - return fh.f() - } - panic("this line only needed for go 1.0") -} - -func TestTrace(t *testing.T) { - t.Parallel() - - fh := fholder{ - f: func() CallStack { - cs := Trace() - return cs - }, - } - - cs := fh.labyrinth() - - lines := []int{43, 33, 48} - - for i, line := range lines { - if got, want := cs[i].line(), line; got != want { - t.Errorf("got line[%d] == %v, want line[%d] == %v", i, got, i, want) - } - } -}