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
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased

* ...
* [#1126](https://github.com/NibiruChain/nibiru/pull/1126) - refactor(perp): remove unnecessary panics
* [#1126](https://github.com/NibiruChain/nibiru/pull/1126) - test(oracle): stop the tyrannical behavior of TestFuzz_PickReferencePair

## [v0.16.3](https://github.com/NibiruChain/nibiru/releases/tag/v0.16.3)

Expand Down
25 changes: 15 additions & 10 deletions x/common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ import (
"github.com/NibiruChain/nibiru/x/common"
)

type FunctionTestCase struct {
name string
test func()
}

func RunFunctionTests(t *testing.T, testCases []FunctionTestCase) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.test()
})
}
}

func TestNewAssetPair_Constructor(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -75,10 +88,7 @@ func TestAsset_GetQuoteBaseToken(t *testing.T) {
}

func TestAssetPair_Marshaling(t *testing.T) {
testCases := []struct {
name string
test func()
}{
testCases := []FunctionTestCase{
{
name: "verbose equal suite",
test: func() {
Expand Down Expand Up @@ -110,10 +120,5 @@ func TestAssetPair_Marshaling(t *testing.T) {
},
}

for _, testCase := range testCases {
tc := testCase
t.Run(tc.name, func(t *testing.T) {
tc.test()
})
}
RunFunctionTests(t, testCases)
}
182 changes: 182 additions & 0 deletions x/common/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package common

import (
"errors"
"fmt"
"runtime/debug"
)

// 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, true
}
switch v := v.(type) {
case string:
return errors.New(v), true
case error:
return v, true
case fmt.Stringer:
return errors.New(v.String()), true
case []string:
return toErrorFromSlice(v)
case []error:
return toErrorFromSlice(v)
case []fmt.Stringer:
return toErrorFromSlice(v)
default:
// 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) (outErr error) {
for _, e := range errs {
switch {
case e != nil && outErr == nil:
outErr = e
case e != nil && outErr != nil:
outErr = fmt.Errorf("%s: %s", outErr, e)
}
}
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) (err error) {
var errs []error
for _, s := range strs {
err, _ := ToError(s)
errs = append(errs, err)
}
return CombineErrors(errs...)
}
Loading