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

panics: allow usage of RecoveredPanic as a normal error #75

Merged
merged 3 commits into from
Jan 23, 2023
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
25 changes: 21 additions & 4 deletions panics/panics.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,29 @@ type RecoveredPanic struct {
Stack []byte
}

func (c *RecoveredPanic) Error() string {
return fmt.Sprintf("panic: %v\nstacktrace:\n%s\n", c.Value, c.Stack)
// String renders a human-readable formatting of the panic.
func (p *RecoveredPanic) String() string {
return fmt.Sprintf("panic: %v\nstacktrace:\n%s\n", p.Value, p.Stack)
}

func (c *RecoveredPanic) Unwrap() error {
if err, ok := c.Value.(error); ok {
// AsError casts the panic into an error implementation. The implementation
// is unwrappable with the cause of the panic, if the panic was provided one.
func (p *RecoveredPanic) AsError() error {
if p == nil {
return nil
}
return &ErrRecoveredPanic{*p}
}

// ErrRecoveredPanic wraps a RecoveredPanic in an error implementation.
type ErrRecoveredPanic struct{ RecoveredPanic }

var _ error = (*ErrRecoveredPanic)(nil)

func (p *ErrRecoveredPanic) Error() string { return p.String() }

func (p *ErrRecoveredPanic) Unwrap() error {
if err, ok := p.Value.(error); ok {
return err
}
return nil
Expand Down
58 changes: 52 additions & 6 deletions panics/panics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"sync"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -60,6 +61,26 @@ func ExampleCatcher_callers() {
// runtime.goexit
}

func ExampleCatcher_error() {
helper := func() error {
var pc Catcher
pc.Try(func() { panic(errors.New("error")) })
return pc.Recovered().AsError()
}

if err := helper(); err != nil {
// In normal use cases, you can use err.Error() output directly to
// dump the panic's stack. This is not used in the example because
// its output is machine-specific - instead, we demonstrate getting
// the underlying error that was used for the panic.
if cause := errors.Unwrap(err); cause != nil {
fmt.Printf("helper panicked with an error: %s", cause)
}
}
// Output:
// helper panicked with an error: error
}

func TestCatcher(t *testing.T) {
t.Parallel()

Expand All @@ -70,21 +91,21 @@ func TestCatcher(t *testing.T) {
var pc Catcher
pc.Try(func() { panic(err1) })
recovered := pc.Recovered()
require.ErrorIs(t, recovered, err1)
require.ErrorAs(t, recovered, &err1)
require.ErrorIs(t, recovered.AsError(), err1)
require.ErrorAs(t, recovered.AsError(), &err1)
// The exact contents aren't tested because the stacktrace contains local file paths
// and even the structure of the stacktrace is bound to be unstable over time. Just
// test a couple of basics.
require.Contains(t, recovered.Error(), "SOS", "error should contain the panic message")
require.Contains(t, recovered.Error(), "panics.(*Catcher).Try", recovered.Error(), "error should contain the stack trace")
require.Contains(t, recovered.String(), "SOS", "formatted panic should contain the panic message")
require.Contains(t, recovered.String(), "panics.(*Catcher).Try", recovered.String(), "formatted panic should contain the stack trace")
})

t.Run("not error", func(t *testing.T) {
var pc Catcher
pc.Try(func() { panic("definitely not an error") })
recovered := pc.Recovered()
require.NotErrorIs(t, recovered, err1)
require.Nil(t, recovered.Unwrap())
require.NotErrorIs(t, recovered.AsError(), err1)
require.Nil(t, errors.Unwrap(recovered.AsError()))
})

t.Run("repanic panics", func(t *testing.T) {
Expand Down Expand Up @@ -121,3 +142,28 @@ func TestCatcher(t *testing.T) {
require.Equal(t, "50", pc.Recovered().Value)
})
}

func TestRecoveredPanicAsError(t *testing.T) {
t.Parallel()
t.Run("as error is nil", func(t *testing.T) {
t.Parallel()
fn := func() error {
var c Catcher
c.Try(func() {})
return c.Recovered().AsError()
}
err := fn()
assert.Nil(t, err)
})

t.Run("as error is not nil nil", func(t *testing.T) {
t.Parallel()
fn := func() error {
var c Catcher
c.Try(func() { panic("oh dear!") })
return c.Recovered().AsError()
}
err := fn()
assert.NotNil(t, err)
})
}