Skip to content
This repository has been archived by the owner on Dec 1, 2021. It is now read-only.

Introduce Stacktrace and Frame #37

Merged
merged 2 commits into from
Jun 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,19 @@ if err != nil {
`New`, `Errorf`, `Wrap`, and `Wrapf` record a stack trace at the point they are invoked.
This information can be retrieved with the following interface.
```go
type Stack interface {
Stack() []uintptr
type Stacktrace interface {
Stacktrace() []Frame
}
```
The `Frame` type represents a call site in the stacktrace.
`Frame` supports the `fmt.Formatter` interface that can be used for printing information about the stacktrace of this error. For example
```
if err, ok := err.(Stacktrace); ok {
fmt.Printf("%+s:%d", err.Stacktrace())
}
```
See [the documentation for `Frame.Format`](https://godoc.org/github.com/pkg/errors#Frame_Format) for more details.

## Retrieving the cause of an error

Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to recurse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
Expand Down
103 changes: 26 additions & 77 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,6 @@
// return errors.Wrap(err, "read failed")
// }
//
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface.
//
// type Stack interface {
// Stack() []uintptr
// }
//
// Retrieving the cause of an error
//
// Using errors.Wrap constructs a stack of errors, adding context to the
Expand All @@ -51,25 +42,23 @@
// default:
// // unknown error
// }
//
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface.
//
// type Stacktrace interface {
// Stacktrace() []Frame
// }
package errors

import (
"errors"
"fmt"
"io"
"runtime"
"strings"
)

// stack represents a stack of program counters.
type stack []uintptr

func (s *stack) Stack() []uintptr { return *s }

func (s *stack) Location() (string, int) {
return location((*s)[0] - 1)
}

// New returns an error that formats as the given text.
func New(text string) error {
return struct {
Expand Down Expand Up @@ -167,26 +156,41 @@ func Cause(err error) error {
// Fprint prints the error to the supplied writer.
// If the error implements the Causer interface described in Cause
// Print will recurse into the error's cause.
// If the error implements the inteface:
// If the error implements one of the following interfaces:
//
// type Stacktrace interface {
// Stacktrace() []Frame
// }
//
// type Location interface {
// Location() (file string, line int)
// }
//
// Print will also print the file and line of the error.
// If err is nil, nothing is printed.
//
// Deprecated: Fprint will be removed in version 0.7.
func Fprint(w io.Writer, err error) {
type location interface {
Location() (string, int)
}
type stacktrace interface {
Stacktrace() []Frame
}
type message interface {
Message() string
}

for err != nil {
if err, ok := err.(location); ok {
switch err := err.(type) {
case stacktrace:
frame := err.Stacktrace()[0]
fmt.Fprintf(w, "%+s:%d: ", frame, frame)
case location:
file, line := err.Location()
fmt.Fprintf(w, "%s:%d: ", file, line)
default:
// de nada
}
switch err := err.(type) {
case message:
Expand All @@ -202,58 +206,3 @@ func Fprint(w io.Writer, err error) {
err = cause.Cause()
}
}

func callers() *stack {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
var st stack = pcs[0:n]
return &st
}

// location returns the source file and line matching pc.
func location(pc uintptr) (string, int) {
fn := runtime.FuncForPC(pc)
if fn == nil {
return "unknown", 0
}

// Here we want to get the source file path relative to the compile time
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
// GOPATH at runtime, but we can infer the number of path segments in the
// GOPATH. We note that fn.Name() returns the function name qualified by
// the import path, which does not include the GOPATH. Thus we can trim
// segments from the beginning of the file path until the number of path
// separators remaining is one more than the number of path separators in
// the function name. For example, given:
//
// GOPATH /home/user
// file /home/user/src/pkg/sub/file.go
// fn.Name() pkg/sub.Type.Method
//
// We want to produce:
//
// pkg/sub/file.go
//
// From this we can easily see that fn.Name() has one less path separator
// than our desired output. We count separators from the end of the file
// path until it finds two more than in the function name and then move
// one character forward to preserve the initial path segment without a
// leading separator.
const sep = "/"
goal := strings.Count(fn.Name(), sep) + 2
file, line := fn.FileLine(pc)
i := len(file)
for n := 0; n < goal; n++ {
i = strings.LastIndex(file[:i], sep)
if i == -1 {
// not enough separators found, set i so that the slice expression
// below leaves file unmodified
i = -len(sep)
break
}
}
// get back to 0 or trim the leading separator
file = file[i+len(sep):]
return file, line
}
5 changes: 3 additions & 2 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestCause(t *testing.T) {
}
}

func TestFprint(t *testing.T) {
func TestFprintError(t *testing.T) {
x := New("error")
tests := []struct {
err error
Expand Down Expand Up @@ -234,7 +234,8 @@ func TestStack(t *testing.T) {
}
st := x.Stack()
for i, want := range tt.want {
file, line := location(st[i] - 1)
frame := Frame(st[i])
file, line := fmt.Sprintf("%+s", frame), frame.line()
if file != want.file || line != want.line {
t.Errorf("frame %d: expected %s:%d, got %s:%d", i, want.file, want.line, file, line)
}
Expand Down
16 changes: 16 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,19 @@ func ExampleErrorf() {

// Output: github.com/pkg/errors/example_test.go:67: whoops: foo
}

func ExampleError_Stacktrace() {
type Stacktrace interface {
Stacktrace() []errors.Frame
}

err, ok := errors.Cause(fn()).(Stacktrace)
if !ok {
panic("oops, err does not implement Stacktrace")
}

st := err.Stacktrace()
fmt.Printf("%+v", st[0:2]) // top two framces

// Output: [github.com/pkg/errors/example_test.go:33 github.com/pkg/errors/example_test.go:78]
}
148 changes: 148 additions & 0 deletions stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package errors

import (
"fmt"
"io"
"path"
"runtime"
"strings"
)

// Frame repesents an activation record.
type Frame uintptr

// pc returns the program counter for this frame;
// multiple frames may have the same PC value.
func (f Frame) pc() uintptr { return uintptr(f) - 1 }

// file returns the full path to the file that contains the
// function for this Frame's pc.
func (f Frame) file() string {
fn := runtime.FuncForPC(f.pc())
if fn == nil {
return "unknown"
}
file, _ := fn.FileLine(f.pc())
return file
}

// line returns the line number of source code of the
// function for this Frame's pc.
func (f Frame) line() int {
fn := runtime.FuncForPC(f.pc())
if fn == nil {
return 0
}
_, line := fn.FileLine(f.pc())
return line
}

// Format formats the frame according to the fmt.Formatter interface.
//
// %s source file
// %d source line
// %n function name
// %v equivalent to %s:%d
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+s path of source file relative to the compile time GOPATH
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%+v is also supported because case 'v' recursively calls f.Format(s, 's').

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to add it til I had written a test case for it (I try to TDD where I can). As you say, it'll probably work because of the way I wrote %v

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test and docs.

// %+v equivalent to %+s:%d
func (f Frame) Format(s fmt.State, verb rune) {
switch verb {
case 's':
switch {
case s.Flag('+'):
pc := f.pc()
fn := runtime.FuncForPC(pc)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runtime.FuncFoPC docs say it can return nil.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that is true, I'm leaning on the recover inside the fmt package to catch me when I fall.

Can you think of a way we could write a test case for this ? We might have to extract the body of Format into a function so we can pass in an arbitrary pc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this test to TestFrameFormat:

    }, {
        Frame(0),
        "%+s",
        "unknown",
    }, {

It will fail. Fix it for TDD points.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if fn == nil {
io.WriteString(s, "unknown")
} else {
file, _ := fn.FileLine(pc)
io.WriteString(s, trimGOPATH(fn.Name(), file))
}
default:
io.WriteString(s, path.Base(f.file()))
}
case 'd':
fmt.Fprintf(s, "%d", f.line())
case 'n':
name := runtime.FuncForPC(f.pc()).Name()
i := strings.LastIndex(name, "/")
name = name[i+1:]
i = strings.Index(name, ".")
io.WriteString(s, name[i+1:])
case 'v':
f.Format(s, 's')
io.WriteString(s, ":")
f.Format(s, 'd')
}
}

// stack represents a stack of program counters.
type stack []uintptr

// Deprecated: use Stacktrace()
func (s *stack) Stack() []uintptr { return *s }

// Deprecated: use Stacktrace()[0]
func (s *stack) Location() (string, int) {
frame := s.Stacktrace()[0]
return fmt.Sprintf("%+s", frame), frame.line()
}

func (s *stack) Stacktrace() []Frame {
f := make([]Frame, len(*s))
for i := 0; i < len(f); i++ {
f[i] = Frame((*s)[i])
}
return f
}

func callers() *stack {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
var st stack = pcs[0:n]
return &st
}

func trimGOPATH(name, file string) string {
// Here we want to get the source file path relative to the compile time
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
// GOPATH at runtime, but we can infer the number of path segments in the
// GOPATH. We note that fn.Name() returns the function name qualified by
// the import path, which does not include the GOPATH. Thus we can trim
// segments from the beginning of the file path until the number of path
// separators remaining is one more than the number of path separators in
// the function name. For example, given:
//
// GOPATH /home/user
// file /home/user/src/pkg/sub/file.go
// fn.Name() pkg/sub.Type.Method
//
// We want to produce:
//
// pkg/sub/file.go
//
// From this we can easily see that fn.Name() has one less path separator
// than our desired output. We count separators from the end of the file
// path until it finds two more than in the function name and then move
// one character forward to preserve the initial path segment without a
// leading separator.
const sep = "/"
goal := strings.Count(name, sep) + 2
i := len(file)
for n := 0; n < goal; n++ {
i = strings.LastIndex(file[:i], sep)
if i == -1 {
// not enough separators found, set i so that the slice expression
// below leaves file unmodified
i = -len(sep)
break
}
}
// get back to 0 or trim the leading separator
file = file[i+len(sep):]
return file
}
Loading