Skip to content

Commit

Permalink
feat: improve error wrapping, stack trace management, and formatting (#…
Browse files Browse the repository at this point in the history
…46)

This commit includes a change to error wrapping that enables wrapError frames to be inserted into the rootError stack trace. It also changes error formatting to display root errors first followed by wrapped errors in the same order as the stack trace (also same order as runtime.Callers).
  • Loading branch information
morningvera authored Jan 10, 2020
1 parent 7fe3c59 commit ebc3771
Show file tree
Hide file tree
Showing 6 changed files with 505 additions and 197 deletions.
28 changes: 21 additions & 7 deletions eris.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ import (
func New(msg string) error {
return &rootError{
msg: msg,
stack: callers(3),
stack: callers(3), // callers(3) skips this method, stack.callers, and runtime.Callers
}
}

// NewGlobal creates a new root error for use as a global sentinel type.
func NewGlobal(msg string) error {
return &rootError{
global: true,
msg: msg,
stack: callers(3),
}
}

Expand Down Expand Up @@ -44,21 +53,25 @@ func wrap(err error, msg string) error {
return nil
}

// callers(4) skips this method, Wrap(f), stack.callers, and runtime.Callers
stack := callers(4)
switch e := err.(type) {
case *rootError:
e.stack = callers(4)
if e.global {
e.stack = stack
}
case *wrapError:
default:
err = &rootError{
msg: e.Error(),
stack: callers(4),
stack: stack,
}
}

return &wrapError{
msg: msg,
err: err,
frame: caller(3),
stack: stack,
}
}

Expand Down Expand Up @@ -112,8 +125,9 @@ func Cause(err error) error {
}

type rootError struct {
msg string
stack *stack
global bool
msg string
stack *stack
}

func (e *rootError) Error() string {
Expand All @@ -134,7 +148,7 @@ func (e *rootError) Is(target error) bool {
type wrapError struct {
msg string
err error
frame *frame
stack *stack
}

func (e *wrapError) Error() string {
Expand Down
105 changes: 61 additions & 44 deletions eris_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ func setupTestCase(wrapf bool, cause error, input []string) error {
}

func TestErrorWrapping(t *testing.T) {
globalErr := eris.NewGlobal("global error")

tests := map[string]struct {
cause error // root error
input []string // input for error wrapping
Expand All @@ -30,15 +32,20 @@ func TestErrorWrapping(t *testing.T) {
cause: nil,
input: []string{"additional context"},
},
"standard error wrapping with global root cause (eris.NewGlobal)": {
cause: globalErr,
input: []string{"additional context", "even more context"},
output: "global error: additional context: even more context",
},
"standard error wrapping with internal root cause (eris.New)": {
cause: eris.New("root error"),
input: []string{"additional context", "even more context"},
output: "even more context: additional context: root error",
output: "root error: additional context: even more context",
},
"standard error wrapping with external root cause (errors.New)": {
cause: errors.New("external error"),
input: []string{"additional context", "even more context"},
output: "even more context: additional context: external error",
output: "external error: additional context: even more context",
},
"no error wrapping with internal root cause (eris.Errorf)": {
cause: eris.Errorf("%v", "root error"),
Expand All @@ -51,12 +58,14 @@ func TestErrorWrapping(t *testing.T) {
}

for desc, tc := range tests {
err := setupTestCase(false, tc.cause, tc.input)
if err != nil && tc.cause == nil {
t.Errorf("%v: wrapping nil errors should return nil but got { %v }", desc, err)
} else if err != nil && tc.output != err.Error() {
t.Errorf("%v: expected { %v } got { %v }", desc, tc.output, err)
}
t.Run(desc, func(t *testing.T) {
err := setupTestCase(false, tc.cause, tc.input)
if err != nil && tc.cause == nil {
t.Errorf("%v: wrapping nil errors should return nil but got { %v }", desc, err)
} else if err != nil && tc.output != err.Error() {
t.Errorf("%v: expected { %v } got { %v }", desc, tc.output, err)
}
})
}
}

Expand All @@ -70,37 +79,39 @@ func TestErrorUnwrap(t *testing.T) {
cause: eris.New("root error"),
input: []string{"additional context", "even more context"},
output: []string{
"even more context: additional context: root error",
"additional context: root error",
"root error: additional context: even more context",
"root error: additional context",
"root error",
},
},
"unwrapping error with external root cause (errors.New)": {
cause: errors.New("external error"),
input: []string{"additional context", "even more context"},
output: []string{
"even more context: additional context: external error",
"additional context: external error",
"external error: additional context: even more context",
"external error: additional context",
"external error",
},
},
}

for desc, tc := range tests {
err := setupTestCase(true, tc.cause, tc.input)
for _, out := range tc.output {
if err == nil {
t.Errorf("%v: unwrapping error returned nil but expected { %v }", desc, out)
} else if out != err.Error() {
t.Errorf("%v: expected { %v } got { %v }", desc, out, err)
t.Run(desc, func(t *testing.T) {
err := setupTestCase(true, tc.cause, tc.input)
for _, out := range tc.output {
if err == nil {
t.Errorf("%v: unwrapping error returned nil but expected { %v }", desc, out)
} else if out != err.Error() {
t.Errorf("%v: expected { %v } got { %v }", desc, out, err)
}
err = eris.Unwrap(err)
}
err = eris.Unwrap(err)
}
})
}
}

func TestErrorIs(t *testing.T) {
globalErr := eris.New("global error")
globalErr := eris.NewGlobal("global error")

tests := map[string]struct {
cause error // root error
Expand Down Expand Up @@ -167,12 +178,14 @@ func TestErrorIs(t *testing.T) {
}

for desc, tc := range tests {
err := setupTestCase(false, tc.cause, tc.input)
if tc.output && !eris.Is(err, tc.compare) {
t.Errorf("%v: expected eris.Is('%v', '%v') to return true but got false", desc, err, tc.compare)
} else if !tc.output && eris.Is(err, tc.compare) {
t.Errorf("%v: expected eris.Is('%v', '%v') to return false but got true", desc, err, tc.compare)
}
t.Run(desc, func(t *testing.T) {
err := setupTestCase(false, tc.cause, tc.input)
if tc.output && !eris.Is(err, tc.compare) {
t.Errorf("%v: expected eris.Is('%v', '%v') to return true but got false", desc, err, tc.compare)
} else if !tc.output && eris.Is(err, tc.compare) {
t.Errorf("%v: expected eris.Is('%v', '%v') to return false but got true", desc, err, tc.compare)
}
})
}
}

Expand All @@ -196,11 +209,13 @@ func TestErrorCause(t *testing.T) {
}

for desc, tc := range tests {
err := setupTestCase(false, tc.cause, tc.input)
cause := eris.Cause(err)
if tc.output != eris.Cause(err) {
t.Errorf("%v: expected { %v } got { %v }", desc, tc.output, cause)
}
t.Run(desc, func(t *testing.T) {
err := setupTestCase(false, tc.cause, tc.input)
cause := eris.Cause(err)
if tc.output != eris.Cause(err) {
t.Errorf("%v: expected { %v } got { %v }", desc, tc.output, cause)
}
})
}
}

Expand All @@ -213,26 +228,28 @@ func TestErrorFormatting(t *testing.T) {
"standard error wrapping with internal root cause (eris.New)": {
cause: eris.New("root error"),
input: []string{"additional context", "even more context"},
output: "even more context: additional context: root error",
output: "root error: additional context: even more context",
},
"standard error wrapping with external root cause (errors.New)": {
cause: errors.New("external error"),
input: []string{"additional context", "even more context"},
output: "even more context: additional context: external error",
output: "external error: additional context: even more context",
},
}

for desc, tc := range tests {
err := setupTestCase(false, tc.cause, tc.input)
if err != nil && tc.cause == nil {
t.Errorf("%v: wrapping nil errors should return nil but got { %v }", desc, err)
} else if err != nil && tc.output != err.Error() {
t.Errorf("%v: expected { %v } got { %v }", desc, tc.output, err)
}
t.Run(desc, func(t *testing.T) {
err := setupTestCase(false, tc.cause, tc.input)
if err != nil && tc.cause == nil {
t.Errorf("%v: wrapping nil errors should return nil but got { %v }", desc, err)
} else if err != nil && tc.output != err.Error() {
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)
// todo: automate stack trace verification
_ = fmt.Sprintf("error formatting results (%v):\n", desc)
_ = fmt.Sprintf("%v\n", err)
_ = fmt.Sprintf("%+v", err)
})
}
}
Loading

0 comments on commit ebc3771

Please sign in to comment.