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

refactor(perp): Remove unnecessary panics | #1 #1126

Merged
merged 12 commits into from
Jan 4, 2023
100 changes: 1 addition & 99 deletions x/common/common_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package common_test

import (
"errors"
"fmt"
"strings"
"testing"

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

"github.com/NibiruChain/nibiru/x/common"
Expand All @@ -18,8 +15,7 @@ type FunctionTestCase struct {
}

func RunFunctionTests(t *testing.T, testCases []FunctionTestCase) {
for _, testCase := range testCases {
tc := testCase
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.test()
})
Expand Down Expand Up @@ -126,97 +122,3 @@ func TestAssetPair_Marshaling(t *testing.T) {

RunFunctionTests(t, testCases)
}

func TestCombineErrors(t *testing.T) {
newErrors := func(strs ...string) []error {
var errs []error
for _, s := range strs {
errs = append(errs, errors.New(s))
}
return errs
}

testCases := []struct {
name string
errs []error
errOut error
}{
{name: "single nil remains nil", errs: []error{nil}, errOut: nil},
{name: "multiple nil becomes nil", errs: []error{nil, nil, nil}, errOut: nil},
{name: "single err unaffected", errs: newErrors("err0"), errOut: errors.New("err0")},
{
name: "multiple err coalesces - A",
errs: newErrors("err0", "err1"),
errOut: errors.New("err0: err1"),
},
{
name: "multiple err coalesces - B",
errs: newErrors("err0", "err1", "err2", "foobar"),
errOut: errors.New(strings.Join([]string{"err0", "err1", "err2", "foobar"}, ": ")),
},
}

for _, testCase := range testCases {
tc := testCase
t.Run(tc.name, func(t *testing.T) {
errOut := common.CombineErrors(tc.errs...)
assert.EqualValuesf(t, tc.errOut, errOut,
"tc.errOut: %s\nerrOut: %s", tc.errOut, errOut)
})
}
}

func TestCombineErrorsFromStrings(t *testing.T) {
// REALUTODO
testCases := []FunctionTestCase{
{name: "", test: func() {}},
}

RunFunctionTests(t, testCases)
}

func TestToError(t *testing.T) {
testCases := []FunctionTestCase{
{
name: "string nonempty",
test: func() {
description := "an error description"
out := common.ToError(description)
assert.EqualValues(t, out.Error(), description)
},
},
{
name: "error nonempty",
test: func() {
description := "an error description"
out := common.ToError(errors.New(description))
assert.EqualValues(t, out.Error(), description)
},
},
{
name: "empty string creates blank error",
test: func() {
description := ""
out := common.ToError("")
assert.EqualValues(t, out.Error(), description)
},
},
{
name: "fail - bad type",
test: func() {
descriptionOfBadType := int64(2200)
assert.Panics(t, func() {
_ = common.ToError(descriptionOfBadType)
})
},
},
{
name: "nil input returns nil",
test: func() {
assert.Equal(t, nil, common.ToError(nil))
},
},
}

RunFunctionTests(t, testCases)
}
169 changes: 152 additions & 17 deletions x/common/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,180 @@ package common
import (
"errors"
"fmt"
"runtime/debug"
)

// ToError converts a value to error if if (1) is a string, (2) has a String() function
// or (3) is already an error.
func ToError(v any) error {
// TryCatch is an implementation of the try-catch block from languages like C++ and JS.
// Given a 'callback' function, TryCatch defers and recovers from any panics or
// errors, allowing one to safely handle multiple panics in succession.
//
// Typically, you'll write something like: `err := TryCatch(aRiskyFunction)()`
//
// Usage example:
//
// var calmPanic error = TryCatch(func() {
// panic("something crazy")
// })()
// fmt.Println(calmPanic.Error()) // prints "something crazy"
//
// Note that TryCatch doesn't return an error. It returns a function that returns
// an error. Only when you call the output of TryCatch will it "feel" like a
// try-catch block from other languages.
//
// This means that TryCatch can be used to restart go routines that panic as well.
func TryCatch(callback func()) func() error {
return func() (err error) {
defer func() {
if panicInfo := recover(); panicInfo != nil {
err = fmt.Errorf("%v, %s", panicInfo, string(debug.Stack()))
return
}
}()
callback() // calling the decorated function
return err
}
}

// ToError converts a value to an error type if it:
// (1) is a string,
// (2) has a String() function
// (3) is already an error.
// (4) or is a slice of these cases
// I.e., the types supported are:
// string, []string, error, []error, fmt.Stringer, []fmt.Stringer
//
// The type is inferred from try catch blocks at runtime.
func ToError(v any) (out error, ok bool) {
if v == nil {
return nil
return nil, true
}
switch v := v.(type) {
case string:
return errors.New(v)
return errors.New(v), true
case error:
return v
return v, true
case fmt.Stringer:
return errors.New(v.String())
return errors.New(v.String()), true
case []string:
return toErrorFromSlice(v)
case []error:
return toErrorFromSlice(v)
case []fmt.Stringer:
return toErrorFromSlice(v)
default:
panic(fmt.Errorf("invalid type: %T", v))
// Cases for duck typing at runtime

// case: error
if tcErr := TryCatch(func() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't the type switch statement catch all these cases already?

Copy link
Member Author

@Unique-Divine Unique-Divine Jan 4, 2023

Choose a reason for hiding this comment

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

That's how I thought it would work at first too. The reason it skips those blocks is that any and interface{} are actual types. They don't actual mean "any type" but rather "an object of type any".

Let's say we pass in an argument, arg, that fits the fmt.Stringer interface like sdk.Coin or sdk.AccAddress. When arg enters this function, it won't actually have the type of fmt.Stringer, so it will pass all of the case statements and end up in default.

Then, when it reaches the arg := arg.(fmt.Stringer) block, it will enter the function with fmt.Stringer as its value for arg.(type). Since arg actually is an instance of that fits this interface, the TryCatch will run smoothly.

However, it has to guess by manually attempting to cast as each type. Go assumes the type casting will work and forces the object to have a certain type, then it panics if something doesn't make sense, then defers, then recovers, and finally moves to the next TryCatch.

This basically shows why Python is so slow.

v := v.(error)
out = errors.New(v.Error())
})(); tcErr == nil {
return out, true
}

// case: string
if tcErr := TryCatch(func() {
v := v.(string)
out = errors.New(v)
})(); tcErr == nil {
return out, true
}

// case: fmt.Stringer (object with String method)
if tcErr := TryCatch(func() {
v := v.(fmt.Stringer)
out = errors.New(v.String())
})(); tcErr == nil {
return out, true
}

// case: []string
if tcErr := TryCatch(func() {
if maybeOut, okLocal := ToError(v.([]string)); okLocal {
out = maybeOut
}
})(); tcErr == nil {
return out, true
}

// case: []error
if tcErr := TryCatch(func() {
if maybeOut, okLocal := ToError(v.([]error)); okLocal {
out = maybeOut
}
})(); tcErr == nil {
return out, true
}

// case: []fmt.Stringer
if tcErr := TryCatch(func() {
if maybeOut, okLocal := ToError(v.([]fmt.Stringer)); okLocal {
out = maybeOut
}
})(); tcErr == nil {
return out, true
}

return fmt.Errorf("invalid type: %T", v), false
}
}

func toErrorFromSlice(slice any) (out error, ok bool) {
switch slice := slice.(type) {
case []string:
var errs []error
for _, str := range slice {
if err, okLocal := ToError(str); okLocal {
errs = append(errs, err)
} else {
return err, false
}
}
return CombineErrors(errs...), true
case []error:
return CombineErrors(slice...), true
case []fmt.Stringer:
var errs []error
for _, stringer := range slice {
if err, okLocal := ToError(stringer.String()); okLocal {
errs = append(errs, err)
} else {
return err, false
}
}
return CombineErrors(errs...), true
}
return nil, false
}

// Combines errors into single error. Error descriptions are ordered the same way
// they're passed to the function.
func CombineErrors(errs ...error) error {
var err error
func CombineErrors(errs ...error) (outErr error) {
for _, e := range errs {
switch {
case e != nil && err == nil:
err = e
case e != nil && err != nil:
err = fmt.Errorf("%s: %s", err, e)
case e != nil && outErr == nil:
outErr = e
case e != nil && outErr != nil:
outErr = fmt.Errorf("%s: %s", outErr, e)
}
}
return err
return outErr
}

func CombineErrorsGeneric(errAnySlice any) (out error, ok bool) {
err, ok := ToError(errAnySlice)
if ok {
return err, true
} else {
return err, false
}
}

func CombineErrorsFromStrings(strs ...string) error {
func CombineErrorsFromStrings(strs ...string) (err error) {
var errs []error
for _, s := range strs {
errs = append(errs, ToError(s))
err, _ := ToError(s)
errs = append(errs, err)
}
return CombineErrors(errs...)
}
Loading