BTSE: Fix duplicate pair errors on Million pairs (M_*) (#1401)

* BTSE: Fix duplicate error on Million pairs (M_*)

BTSE has listed Pitbull token with two symbols:
PIT-USD and M_PIT-USD for millons of PIT / USD.
The native token is not tradable, so we ignore them and
get a base of M_PIT because that's what later APIs will accept

* BTSE: Fix test errors on locked market

* Common: Improve AppendError and ExcludeError

This change switches from a stateful multiError to caring more about the
Unwrap() []error interface, the same as [go standard
lib](https://github.com/golang/go/blob/go1.21.4/src/errors/wrap.go#L54-L68)

Notably, if we implement Unwrap() []error and do NOT implement Is() then
we get free compatibility with the core functions.

The only distateful thing here is needing to deeply unwrap fmt.Errorf
errors, since they don't flatten. I can't see any way around that

* Pairs: Fix exchange config Pairs loading

When a pair string contained two punctuation runes, the first one is used,
and the configFormat is ignored.

This fix checks the list and corrects any with the wrong delimiter, or
errors if the format is inconsistent.

* BTSE: Fix all tickers retrieved by GetTicker

PR #764 introduced GetTickers, but it wasn't rolled out to BTSE.
This fix ensures that when one ticker is a locked market, the rest continue to
function. Particularly important if the locked market wasn't even
enabled anyway.

* Kucoin: Fix test config future pairs

* BTSE: Remove PIT tests; Token removed

BTSE have removed the PIT token pairs

All these changes stand, and this just removes the test

* ITBit: Fix fatal error on second run

This fix removes incorrect config pair delimiter, because it would be
re-inserted into config the first run, and then error the second time.

This delimiter doesn't match the config we have.
There's no implementation of fetching pairs, so what's in config files
now is all that matters

* Engine: Fix TestConfigAllJsonResponse

* Clarity of non-matching json improved
* Handling for fixing pair delimiters
This commit is contained in:
Gareth Kirwan
2023-12-19 04:40:13 +01:00
committed by GitHub
parent dc6873c66f
commit 37b1121bbd
21 changed files with 537 additions and 778 deletions

View File

@@ -463,78 +463,88 @@ func InArray(val, array interface{}) (exists bool, index int) {
return
}
// multiError holds all the errors as a slice, this is unexported, so it forces
// inbuilt error handling.
// multiError holds errors as a slice
type multiError struct {
loadedErrors []error
offset *int
errs []error
}
// AppendError appends error in a more idiomatic way. This can start out as a
// standard error e.g. err := errors.New("random error")
// err = AppendError(err, errors.New("another random error"))
// AppendError appends an error to a list of exesting errors
// Either argument may be:
// * A vanilla error
// * An error implementing Unwrap() []error e.g. fmt.Errorf("%w: %w")
// * nil
// The result will be an error which may be a multiError if multipleErrors were found
func AppendError(original, incoming error) error {
errSliceP, ok := original.(*multiError)
if ok {
errSliceP.offset = nil
}
if incoming == nil {
return original // Skip append - continue as normal.
return original
}
if !ok {
// This assumes that a standard error is passed in and we can want to
// track it and add additional errors.
errSliceP = &multiError{}
if original != nil {
errSliceP.loadedErrors = append(errSliceP.loadedErrors, original)
if original == nil {
return incoming
}
newErrs := []error{incoming}
if u, ok := incoming.(interface{ Unwrap() []error }); ok {
newErrs = u.Unwrap()
}
if u, ok := original.(interface{ Unwrap() []error }); ok {
return &multiError{
errs: append(u.Unwrap(), newErrs...),
}
}
if incomingSlice, ok := incoming.(*multiError); ok {
// Join slices if needed.
errSliceP.loadedErrors = append(errSliceP.loadedErrors, incomingSlice.loadedErrors...)
} else {
errSliceP.loadedErrors = append(errSliceP.loadedErrors, incoming)
return &multiError{
errs: append([]error{original}, newErrs...),
}
return errSliceP
}
// Error displays all errors comma separated, if unwrapped has been called and
// has not been reset will display the individual error
// Error displays all errors comma separated
func (e *multiError) Error() string {
if e.offset != nil {
return e.loadedErrors[*e.offset].Error()
}
allErrors := make([]string, len(e.loadedErrors))
for x := range e.loadedErrors {
allErrors[x] = e.loadedErrors[x].Error()
allErrors := make([]string, len(e.errs))
for x := range e.errs {
allErrors[x] = e.errs[x].Error()
}
return strings.Join(allErrors, ", ")
}
// Unwrap increments the offset so errors.Is() can be called to its individual
// error for correct matching.
func (e *multiError) Unwrap() error {
if e.offset == nil {
e.offset = new(int)
} else {
*e.offset++
}
if *e.offset == len(e.loadedErrors) {
e.offset = nil
return nil // Force errors.Is package to return false.
}
return e
// Unwrap returns all of the errors in the multiError
func (e *multiError) Unwrap() []error {
return e.errs
}
// Is checks to see if the errors match. It calls package errors.Is() so that
// we can keep fmt.Errorf() trimmings. This is called in errors package at
// interface assertion err.(interface{ Is(error) bool }).
func (e *multiError) Is(incoming error) bool {
if e.offset != nil && errors.Is(e.loadedErrors[*e.offset], incoming) {
e.offset = nil
return true
type unwrappable interface {
Unwrap() []error
}
// unwrapDeep walks down a stack of nested fmt.Errorf("%w: %w") errors
// This is necessary since fmt.wrapErrors doesn't flatten the error slices
func unwrapDeep(err unwrappable) []error {
var n []error
for _, e := range err.Unwrap() {
if u, ok := e.(unwrappable); ok {
n = append(n, u.Unwrap()...)
} else {
n = append(n, e)
}
}
return false
return n
}
// ExcludeError returns a new error excluding any errors matching excl
// For a standard error it will either return the error unchanged or nil
// For an error which implements Unwrap() []error it will remove any errors matching excl and return the remaining errors or nil
// Any non-error messages and formatting from fmt.Errorf will be lost; This function is written for conditions
func ExcludeError(err, excl error) error {
if u, ok := err.(unwrappable); ok {
var n error
for _, e := range unwrapDeep(u) {
if !errors.Is(e, excl) {
n = AppendError(n, e)
}
}
return n
}
if errors.Is(err, excl) {
return nil
}
return err
}
// StartEndTimeCheck provides some basic checks which occur

View File

@@ -16,6 +16,7 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/thrasher-corp/gocryptotrader/common/file"
)
@@ -656,105 +657,52 @@ func TestInArray(t *testing.T) {
func TestErrors(t *testing.T) {
t.Parallel()
var errTestOne = errors.New("test1")
var test error
test = AppendError(test, errTestOne)
if !errors.Is(test, errTestOne) {
t.Fatal("does not match error")
e1 := errors.New("inconsistent gravity")
e2 := errors.New("barely marginal interest in your story")
e3 := errors.New("error making dinner")
e4 := errors.New("inconsistent gravy")
e5 := errors.New("add vodka")
// Nil tests
assert.Nil(t, AppendError(nil, nil), "Append nil to nil should nil")
assert.Same(t, AppendError(e1, nil), e1, "Append nil to e1 should e1")
assert.Same(t, AppendError(nil, e2), e2, "Append e2 to nil should e2")
// Vanila error tests
err := AppendError(AppendError(AppendError(nil, e1), e2), e1)
assert.ErrorContains(t, err, "inconsistent gravity, barely marginal interest in your story, inconsistent gravity", "Should format consistently")
assert.ErrorIs(t, err, e1, "Should have inconsistent gravity")
assert.ErrorIs(t, err, e2, "Should be bored by your witty tales")
err = ExcludeError(err, e2)
assert.ErrorIs(t, err, e1, "Should still be bored")
assert.False(t, errors.Is(err, e2), "Should not be an e2")
me, ok := err.(*multiError)
if assert.True(t, ok, "Should be a multiError") {
assert.Len(t, me.errs, 2, "Should only have 2 errors")
}
err = ExcludeError(err, e1)
assert.NoError(t, err, "Error should be empty")
err = ExcludeError(err, e1)
assert.NoError(t, err, "Excluding a nil error should be okay")
var errTestTwo = errors.New("test2")
test = AppendError(test, errTestTwo)
if !errors.Is(test, errTestTwo) {
t.Fatal("does not match error")
}
// Wrapped error tests
err = fmt.Errorf("%w: %w", e3, fmt.Errorf("%w: %w", e4, e5))
assert.ErrorIs(t, ExcludeError(err, e4), e3, "Excluding e4 should retain e3")
assert.ErrorIs(t, ExcludeError(err, e4), e5, "Excluding e4 should retain the vanilla co-wrapped e5")
assert.False(t, errors.Is(ExcludeError(err, e4), e4), "e4 should be excluded")
assert.ErrorIs(t, ExcludeError(err, e5), e3, "Excluding e5 should retain e3")
assert.ErrorIs(t, ExcludeError(err, e5), e4, "Excluding e5 should retain the vanilla co-wrapped e4")
assert.False(t, errors.Is(ExcludeError(err, e5), e5), "e5 should be excluded")
if !errors.Is(test, errTestTwo) {
t.Fatal("does not match error")
}
// Append nil should log
test = AppendError(test, nil)
if test.Error() != "test1, test2" {
t.Fatal("does not match error")
}
// Join slices for whatever reason
test = AppendError(test, test)
if test.Error() != "test1, test2, test1, test2" {
t.Fatal("does not match error")
}
var errTestThree = errors.New("test3")
if errors.Is(test, errTestThree) {
t.Fatal("expected errors.Is() should not match")
}
if errors.Is(test, errTestThree) {
t.Fatal("expected errors.Is() should not match")
}
strangeError := errors.New("this is a strange error")
strangeError = AppendError(strangeError, errTestOne)
if strangeError.Error() != "this is a strange error, test1" {
t.Fatal("does not match error")
}
// Add trimmings
strangeError = AppendError(strangeError, fmt.Errorf("TRIMMINGS: %w", errTestTwo))
if strangeError.Error() != "this is a strange error, test1, TRIMMINGS: test2" {
t.Fatal("does not match error")
}
if !errors.Is(strangeError, errTestTwo) {
t.Fatal("does not match error")
}
if errors.Is(strangeError, errTestThree) {
t.Fatal("should not match")
}
// Test again because unwrap was called multiple times.
if strangeError.Error() != "this is a strange error, test1, TRIMMINGS: test2" {
t.Fatalf("received: '%v' but expected: '%v'", strangeError.Error(), "this is a strange error, test1, TRIMMINGS: test2")
}
strangeError = AppendError(strangeError, errors.New("even more error"))
strangeError = AppendError(strangeError, nil) // Skip this nasty thing.
// Test for individual display of errors
target := 0
for indv := errors.Unwrap(strangeError); indv != nil; indv = errors.Unwrap(indv) {
switch target {
case 0:
if indv.Error() != "this is a strange error" {
t.Fatalf("received: '%v' but expected: '%v'", indv.Error(), "this is a strange error")
}
case 1:
if indv.Error() != "test1" {
t.Fatalf("received: '%v' but expected: '%v'", indv.Error(), "test1")
}
case 2:
if indv.Error() != "TRIMMINGS: test2" {
t.Fatalf("received: '%v' but expected: '%v'", indv.Error(), "TRIMMINGS: test2")
}
case 3:
if indv.Error() != "even more error" {
t.Fatalf("received: '%v' but expected: '%v'", indv.Error(), "even more error")
}
default:
t.Fatal("unhandled case")
}
target++
}
if target != 4 {
t.Fatal("targets not achieved")
}
// Hybrid tests
err = AppendError(fmt.Errorf("%w: %w", e4, e5), e3)
assert.ErrorIs(t, ExcludeError(err, e4), e3, "Excluding e4 should retain e3")
assert.ErrorIs(t, ExcludeError(err, e4), e5, "Excluding e4 should retain the vanilla co-wrapped e5")
assert.False(t, errors.Is(ExcludeError(err, e4), e4), "e4 should be excluded")
assert.ErrorIs(t, ExcludeError(err, e5), e3, "Excluding e5 should retain e3")
assert.ErrorIs(t, ExcludeError(err, e5), e4, "Excluding e5 should retain the vanilla co-wrapped e4")
assert.False(t, errors.Is(ExcludeError(err, e5), e5), "e4 should be excluded")
}
func TestParseStartEndDate(t *testing.T) {