Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests for different error types' stacks #214

Merged
merged 2 commits into from
Feb 23, 2024
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
4 changes: 4 additions & 0 deletions v2/errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ func New(e interface{}, skip int) *Error {
trace := e.StackTrace()
stack := make([]uintptr, len(trace))
for i, ptr := range trace {
// We do not modify the uintptr representation of the stack frame
// stack is processed by runtime.CallersFrames and then by Next() on Frames slice
// it's already doing uintptr-1
// refer to: https://github.com/golang/go/blob/897b3da2e079b9b940b309747305a5379fffa6ec/src/runtime/symtab.go#L108
stack[i] = uintptr(ptr)
}
return &Error{
Expand Down
22 changes: 14 additions & 8 deletions v2/errors/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,23 +291,29 @@ func TestUnwrapCustomCause(t *testing.T) {
}
}

func ExampleErrorf() {
func TestExampleErrorf(t *testing.T) {
errorStr := ""
for i := 1; i <= 2; i++ {
if i%2 == 1 {
e := Errorf("can only halve even numbers, got %d", i)
fmt.Printf("Error: %+v", e)
errorStr += fmt.Sprintf("Error: %+v", e)
}
}
// Output:
// Error: can only halve even numbers, got 1

expected := "Error: can only halve even numbers, got 1"
if expected != errorStr {
t.Errorf("Actual error does not match expected")
}
}

func ExampleNew() {
func TestExampleNew(t *testing.T) {
// Wrap io.EOF with the current stack-trace and return it
e := New(io.EOF, 0)
fmt.Printf("%+v", e)
// Output:
// EOF
errorStr := fmt.Sprintf("%+v", e)
expected := "EOF"
if expected != errorStr {
t.Errorf("Actual error does not match expected")
}
}

func ExampleNew_skip() {
Expand Down
149 changes: 149 additions & 0 deletions v2/errors/error_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package errors

import (
"fmt"
"runtime"
"testing"

pkgerror "github.com/pkg/errors"
)

const (
bugsnagType = "bugsnagError"
callersType = "callersType"
stackFramesType = "stackFramesType"
stackType = "stackType"
internalType = "internalType"
stringType = "stringType"
)

// Prepare to test inlining
func AInternal() interface{} { return fmt.Errorf("pure golang error") }
func BInternal() interface{} { return AInternal() }
func CInternal() interface{} { return BInternal() }

func AString() interface{} { defer func() interface{} { return recover() }(); panic("panic") }
func BString() interface{} { return AString() }
func CString() interface{} { return BString() }

func AStack() interface{} { return pkgerror.Errorf("from package") }
func BStack() interface{} { return AStack() }
func CStack() interface{} { return BStack() }

func ACallers() interface{} { return newCustomErr("oh no an error", fmt.Errorf("parent error")) }
func BCallers() interface{} { return ACallers() }
func CCallers() interface{} { return BCallers() }

func AFrames() interface{} { return &testErrorWithStackFrames{Err: New("foo", 0)} }
func BFrames() interface{} { return AFrames() }
func CFrames() interface{} { return BFrames() }

// Golang internal errors don't have stacktrace
// StackFrames are going to report only the line where internal golang error was wrapped in Bugsnag error
func TestInternalError(t *testing.T) {
err := CInternal()
typeAssert(t, err, internalType)

_, _, line, _ := runtime.Caller(0) // grab line immediately before error generator
bgError := New(err, 0)
DariaKunoichi marked this conversation as resolved.
Show resolved Hide resolved
actualStack := bgError.StackFrames()
expected := []StackFrame{
{Name: "TestInternalError", File: "errors/error_types_test.go", LineNumber: line + 1},
}
assertStacksMatch(t, expected, actualStack)
}

// Errors from panic contain only the message about panic
// Same as above - StackFrames are going to contain only line numer of wrapping
func TestStringError(t *testing.T) {
err := CString()
typeAssert(t, err, stringType)

_, _, line, _ := runtime.Caller(0) // grab line immediately before error generator
bgError := New(err, 0)
actualStack := bgError.StackFrames()
expected := []StackFrame{
{Name: "TestStringError", File: "errors/error_types_test.go", LineNumber: line + 1},
}
assertStacksMatch(t, expected, actualStack)
}

// Errors from pkg/errors have their own stack
// Inlined functions should be visible in StackFrames
func TestStackError(t *testing.T) {
_, _, line, _ := runtime.Caller(0) // grab line immediately before error generator
err := CStack()
typeAssert(t, err, stackType)

bgError := New(err, 0)
actualStack := bgError.StackFrames()
expected := []StackFrame{
{Name: "AStack", File: "errors/error_types_test.go", LineNumber: 29},
{Name: "BStack", File: "errors/error_types_test.go", LineNumber: 30},
{Name: "CStack", File: "errors/error_types_test.go", LineNumber: 31},
{Name: "TestStackError", File: "errors/error_types_test.go", LineNumber: line + 1},
}

assertStacksMatch(t, expected, actualStack)
}

// Errors implementing Callers() interface should have their own stack
// Inlined functions should be visible in StackFrames
func TestCallersError(t *testing.T) {
_, _, line, _ := runtime.Caller(0) // grab line immediately before error generator
err := CCallers()
typeAssert(t, err, callersType)

bgError := New(err, 0)
actualStack := bgError.StackFrames()
expected := []StackFrame{
{Name: "ACallers", File: "errors/error_types_test.go", LineNumber: 33},
{Name: "BCallers", File: "errors/error_types_test.go", LineNumber: 34},
{Name: "CCallers", File: "errors/error_types_test.go", LineNumber: 35},
{Name: "TestCallersError", File: "errors/error_types_test.go", LineNumber: line + 1},
}
assertStacksMatch(t, expected, actualStack)
}

// Errors with StackFrames are explicitly adding stacktrace to error
// Inlined functions should be visible in StackFrames
func TestFramesError(t *testing.T) {
_, _, line, _ := runtime.Caller(0) // grab line immediately before error generator
err := CFrames()
typeAssert(t, err, stackFramesType)

bgError := New(err, 0)
actualStack := bgError.StackFrames()
expected := []StackFrame{
{Name: "AFrames", File: "errors/error_types_test.go", LineNumber: 37},
{Name: "BFrames", File: "errors/error_types_test.go", LineNumber: 38},
{Name: "CFrames", File: "errors/error_types_test.go", LineNumber: 39},
{Name: "TestFramesError", File: "errors/error_types_test.go", LineNumber: line + 1},
}

assertStacksMatch(t, expected, actualStack)
}

func typeAssert(t *testing.T, err interface{}, expectedType string) {
actualType := checkType(err)
if actualType != expectedType {
t.Errorf("Types don't match. Actual: %+v and expected: %+v\n", actualType, expectedType)
}
}

func checkType(err interface{}) string {
switch err.(type) {
case *Error:
return bugsnagType
case ErrorWithCallers:
return callersType
case errorWithStack:
return stackType
case ErrorWithStackFrames:
return stackFramesType
case error:
return internalType
default:
return stringType
}
}
Loading