Skip to content

Commit

Permalink
fix: return correct stack for local/global vars and add stack tests
Browse files Browse the repository at this point in the history
  • Loading branch information
morningvera committed Feb 11, 2020
1 parent fe18b71 commit 6dde6db
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 86 deletions.
5 changes: 4 additions & 1 deletion eris.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func wrap(err error, msg string) error {

// callers(4) skips this method, Wrap(f), stack.callers, and runtime.Callers
stack := callers(4)
frame := caller(4)
frame := caller(3)
switch e := err.(type) {
case *rootError:
if e.global {
Expand All @@ -60,6 +60,9 @@ func wrap(err error, msg string) error {
msg: e.msg,
stack: stack,
}
} else {
// insert the frame into the stack
e.stack.insertPC(*stack)
}
case *wrapError:
// insert the frame into the stack
Expand Down
46 changes: 21 additions & 25 deletions eris_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func TestErrorWrapping(t *testing.T) {
input: []string{"additional context", "even more context"},
output: "external error: additional context: even more context",
},
"no error wrapping with a local root cause (eris.Errorf)": { // todo: also test globals with Errorf (wrapping included)
"no error wrapping with a local root cause (eris.Errorf)": {
cause: eris.Errorf("%v root error", "formatted"),
output: "formatted root error",
},
Expand Down Expand Up @@ -260,40 +260,35 @@ func TestErrorFormatting(t *testing.T) {
t.Errorf("%v: expected { %v } got { %v }", desc, tc.output, err)
}

// todo: automate stack trace verification
_ = fmt.Sprintf("error formatting results (%v):\n", desc)
_ = fmt.Sprintf("%v\n", err)
_ = fmt.Sprintf("%+v", err)
})
}
}

func getFrames(frames []uintptr) []eris.StackFrame {
var sFrames []eris.StackFrame
for _, u := range frames {
pc := u - 1
fn := runtime.FuncForPC(pc)
if fn == nil {
frame := eris.StackFrame{
Name: "unknown",
File: "unknown",
}
sFrames = append(sFrames, frame)
}

name := fn.Name()
i := strings.LastIndex(name, "/")
name = name[i+1:]
file, line := fn.FileLine(pc)
func getFrames(pc []uintptr) []eris.StackFrame {
var stackFrames []eris.StackFrame
if len(pc) == 0 {
return stackFrames
}

frame := eris.StackFrame{
frames := runtime.CallersFrames(pc)
for {
frame, more := frames.Next()
i := strings.LastIndex(frame.Function, "/")
name := frame.Function[i+1:]
stackFrames = append(stackFrames, eris.StackFrame{
Name: name,
File: file,
Line: line,
File: frame.File,
Line: frame.Line,
})
if !more {
break
}
sFrames = append(sFrames, frame)
}
return sFrames

return stackFrames
}

func TestStackFrames(t *testing.T) {
Expand Down Expand Up @@ -326,13 +321,14 @@ func TestStackFrames(t *testing.T) {
cause: nil,
},
}

for desc, tc := range tests {
t.Run(desc, func(t *testing.T) {
err := setupTestCase(false, tc.cause, tc.input)
uErr := eris.Unpack(err)
sFrames := eris.Stack(getFrames(eris.StackFrames(err)))
if !reflect.DeepEqual(uErr.ErrRoot.Stack, sFrames) {
t.Errorf("Stackframes() returned { %v }, was expecting { %v }", sFrames, uErr.ErrRoot.Stack)
t.Errorf("%v: expected { %v } got { %v }", desc, sFrames, uErr.ErrRoot.Stack)
}
})
}
Expand Down
48 changes: 24 additions & 24 deletions examples/logging/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import (

var (
// global error values can be useful when wrapping errors or inspecting error types
ErrInternalServer = eris.New("error internal server")
errInternalServer = eris.New("error internal server")

// declaring an error with pkg/errors for comparison
ErrNotFound = errors.New("error not found")
errNotFound = errors.New("error not found")
)

type Request struct {
type request struct {
ID string
}

func (req *Request) Validate() error {
func (req *request) validate() error {
if req.ID == "" {
// create a new local error and wrap it with some context
err := eris.New("error bad request")
Expand All @@ -31,73 +31,73 @@ func (req *Request) Validate() error {
return nil
}

type Resource struct {
type resource struct {
ID string
AbsPath string
}

func GetResource(req Request) (*Resource, error) {
func getResource(req request) (*resource, error) {
if req.ID == "res2" {
return &Resource{
return &resource{
ID: req.ID,
AbsPath: "./some/malformed/absolute/path/data.json", // malformed absolute filepath to simulate a "bug"
}, nil
} else if req.ID == "res3" {
return &Resource{
return &resource{
ID: req.ID,
AbsPath: "/some/correct/path/data.json",
}, nil
}

return nil, errors.Wrapf(ErrNotFound, "failed to get resource '%v'", req.ID)
return nil, errors.Wrapf(errNotFound, "failed to get resource '%v'", req.ID)
}

func GetRelPath(base string, path string) (string, error) {
func getRelPath(base string, path string) (string, error) {
relPath, err := filepath.Rel(base, path)
if err != nil {
// it's generally useful to wrap external errors with a type that you know how to handle
// first (e.g. ErrInternalServer). this will help if/when you want to do error inspection
// via eris.Is(err, ErrInternalServer) or eris.Cause(err).
return "", eris.Wrap(ErrInternalServer, err.Error())
return "", eris.Wrap(errInternalServer, err.Error())
}
return relPath, nil
}

type Response struct {
type response struct {
RelPath string
}

func ProcessResource(req Request) (*Response, error) {
if err := req.Validate(); err != nil {
func processResource(req request) (*response, error) {
if err := req.validate(); err != nil {
// simply return the error if there's no additional context
return nil, err
}

resource, err := GetResource(req)
resource, err := getResource(req)
if err != nil {
return nil, err
}

// do some processing on the data
relPath, err := GetRelPath("/Users/roti/", resource.AbsPath)
relPath, err := getRelPath("/Users/roti/", resource.AbsPath)
if err != nil {
// wrap the error if you want to add more context
return nil, eris.Wrapf(err, "failed to get relative path for resource '%v'", resource.ID)
}

return &Response{
return &response{
RelPath: relPath,
}, nil
}

type LogReq struct {
type logReq struct {
Method string
Req Request
Res *Response
Req request
Res *response
Err error
}

func LogRequest(logger *logrus.Logger, logReq LogReq) {
func logRequest(logger *logrus.Logger, logReq logReq) {
fields := logrus.Fields{
"method": logReq.Method,
}
Expand All @@ -123,7 +123,7 @@ func main() {
logger.SetFormatter(&logrus.JSONFormatter{})

// example requests
reqs := []Request{
reqs := []request{
{
ID: "", // bad request
},
Expand All @@ -140,10 +140,10 @@ func main() {

// process the example requests and log the results
for _, req := range reqs {
res, err := ProcessResource(req)
res, err := processResource(req)
if req.ID != "res1" {
// log the eris error
LogRequest(logger, LogReq{
logRequest(logger, logReq{
Method: "ProcessResource",
Req: req,
Res: res,
Expand Down
17 changes: 10 additions & 7 deletions examples/sentry/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ func init() {
flag.StringVar(&dsn, "dsn", "", "Sentry DSN for logging stack traces")
}

func Example() error {
func example() error {
return eris.New("test")
}

func WrapExample() error {
err := Example()
func wrapExample() error {
err := example()
if err != nil {
return eris.Wrap(err, "wrap 1")
}
return nil
}

func WrapSecondExample() error {
err := WrapExample()
func wrapSecondExample() error {
err := wrapExample()
if err != nil {
return eris.Wrap(err, "wrap 2")
}
Expand All @@ -41,12 +41,15 @@ func main() {
log.Fatal("Sentry DSN is a required flag, please pass it with '-dsn'")
}

err := WrapSecondExample()
err := wrapSecondExample()
err = eris.Wrap(err, "wrap 3")

sentry.Init(sentry.ClientOptions{
initErr := sentry.Init(sentry.ClientOptions{
Dsn: dsn,
})
if initErr != nil {
log.Fatalf("failed to initialize Sentry: %v", initErr)
}

sentry.CaptureException(err)
sentry.Flush(time.Second * 5)
Expand Down
59 changes: 30 additions & 29 deletions stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,37 +65,27 @@ type frame uintptr

// get returns a human readable stack frame.
func (f frame) get() StackFrame {
frame := StackFrame{
Name: "unknown",
File: "unknown",
}

pc := uintptr(f) - 1
fn := runtime.FuncForPC(pc)
if fn != nil {
name := fn.Name()
i := strings.LastIndex(name, "/")
name = name[i+1:]
file, line := fn.FileLine(pc)

frame = StackFrame{
Name: name,
File: file,
Line: line,
}
}
frames := runtime.CallersFrames([]uintptr{pc})
frame, _ := frames.Next()

i := strings.LastIndex(frame.Function, "/")
name := frame.Function[i+1:]

return frame
return StackFrame{
Name: name,
File: frame.File,
Line: frame.Line,
}
}

// callers returns a stack trace. the argument skip is the number of stack frames to skip
// before recording in pc, with 0 identifying the frame for Callers itself and 1 identifying
// the caller of Callers.
// callers returns a stack trace. the argument skip is the number of stack frames to skip before recording
// in pc, with 0 identifying the frame for Callers itself and 1 identifying the caller of Callers.
func callers(skip int) *stack {
const depth = 64
var pcs [depth]uintptr
n := runtime.Callers(skip, pcs[:])
var st stack = pcs[0 : n-2]
var st stack = pcs[0 : n-2] // todo: change this to filtering out runtime instead of hardcoding n-2
return &st
}

Expand All @@ -104,13 +94,24 @@ type stack []uintptr

// get returns a human readable stack trace.
func (s *stack) get() []StackFrame {
var sFrames []StackFrame
for _, f := range *s {
frame := frame(f)
sFrame := frame.get()
sFrames = append(sFrames, sFrame)
var stackFrames []StackFrame

frames := runtime.CallersFrames(*s)
for {
frame, more := frames.Next()
i := strings.LastIndex(frame.Function, "/")
name := frame.Function[i+1:]
stackFrames = append(stackFrames, StackFrame{
Name: name,
File: frame.File,
Line: frame.Line,
})
if !more {
break
}
}
return sFrames

return stackFrames
}

// isGlobal determines if the stack trace represents a global error
Expand Down
Loading

0 comments on commit 6dde6db

Please sign in to comment.