Files
gocryptotrader/engine/websocketroutine_manager_test.go
Gareth Kirwan 3218982b3a Engine: Integrate IsRunning into order manager interface; enhance websocket manager accordingly (#1337)
* Engine: Fix false test passes for nil OrderManager

TestWebsocketRoutineManagerSetup tests that passing a nil value returns errNilOrderManager;
However that's not actually what would happen when order manager is configured off.

The arguments to setupWebsocketRoutineManager are interface types.
When a nil pointer of interface type is passed, it does NOT equal nil.
nil is a primitive type.
A nil pointer of interface type has the value (nil;type).
See [the Go lang spec](https://golang.org/ref/spec#Comparison_operators):
```
Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.
```

So that means that whilst this test was passing, because it was sending
in a real nil value and comparing it to a real nil, that's not what
would happen at runtime. At runtime the bot.OrderManager would be a nil
pointer to a concrete type *OrderManager, and so not comparible to nil.

This commit just fixes that oversight, and explains the often
misunderstood mechanics of comparing interface types to nil.

In practical terms this means that the tests assert that the WSRM would
not run without a OM, but in fact it actually would. And panic later.

This commit SHOULD introduce a FAILing test. Sorry if you're bisecting.

* WSM: Fix error on OrderManager not enabled

It's okay for OrderManager to not be enabled; It's configurable.
Remove the WSM setup protection and move it to runtime using IsRunning.

Left the WSM deps so they can be fixed as apporpriate to each.
2023-09-11 17:14:59 +10:00

359 lines
9.7 KiB
Go

package engine
import (
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
)
func TestWebsocketRoutineManagerSetup(t *testing.T) {
_, err := setupWebsocketRoutineManager(nil, nil, nil, nil, false)
if !errors.Is(err, errNilExchangeManager) {
t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager)
}
_, err = setupWebsocketRoutineManager(NewExchangeManager(), (*OrderManager)(nil), nil, nil, false)
if !errors.Is(err, errNilCurrencyPairSyncer) {
t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairSyncer)
}
_, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, nil, false)
if !errors.Is(err, errNilCurrencyConfig) {
t.Errorf("error '%v', expected '%v'", err, errNilCurrencyConfig)
}
_, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, &currency.Config{}, true)
if !errors.Is(err, errNilCurrencyPairFormat) {
t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairFormat)
}
m, err := setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, &currency.Config{CurrencyPairFormat: &currency.PairFormat{}}, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m == nil {
t.Error("expecting manager")
}
}
func TestWebsocketRoutineManagerStart(t *testing.T) {
var m *WebsocketRoutineManager
err := m.Start()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
cfg := &currency.Config{CurrencyPairFormat: &currency.PairFormat{
Uppercase: false,
Delimiter: "-",
}}
m, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, cfg, true)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, ErrSubSystemAlreadyStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemAlreadyStarted)
}
}
func TestWebsocketRoutineManagerIsRunning(t *testing.T) {
var m *WebsocketRoutineManager
if m.IsRunning() {
t.Error("expected false")
}
m, err := setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, &currency.Config{CurrencyPairFormat: &currency.PairFormat{}}, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
if m.IsRunning() {
t.Error("expected false")
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
for atomic.LoadInt32(&m.state) == startingState {
<-time.After(time.Second / 100)
}
if !m.IsRunning() {
t.Error("expected true")
}
}
func TestWebsocketRoutineManagerStop(t *testing.T) {
var m *WebsocketRoutineManager
err := m.Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem)
}
m, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, &currency.Config{CurrencyPairFormat: &currency.PairFormat{}}, false)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("error '%v', expected '%v'", err, ErrSubSystemNotStarted)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Stop()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
}
func TestWebsocketRoutineManagerHandleData(t *testing.T) {
var exchName = "Bitstamp"
var wg sync.WaitGroup
em := NewExchangeManager()
exch, err := em.NewExchangeByName(exchName)
if !errors.Is(err, nil) {
t.Fatalf("error '%v', expected '%v'", err, nil)
}
exch.SetDefaults()
err = em.Add(exch)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false, 0)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = om.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
cfg := &currency.Config{CurrencyPairFormat: &currency.PairFormat{
Uppercase: false,
Delimiter: "-",
}}
m, err := setupWebsocketRoutineManager(em, om, &syncManager{}, cfg, true)
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.Start()
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
var orderID = "1337"
err = m.websocketDataHandler(exchName, errors.New("error"))
if err == nil {
t.Error("Error not handled correctly")
}
err = m.websocketDataHandler(exchName, stream.FundingData{})
if err != nil {
t.Error(err)
}
err = m.websocketDataHandler(exchName, &ticker.Price{
ExchangeName: exchName,
Pair: currency.NewPair(currency.BTC, currency.USDC),
AssetType: asset.Spot,
})
if !errors.Is(err, nil) {
t.Errorf("error '%v', expected '%v'", err, nil)
}
err = m.websocketDataHandler(exchName, stream.KlineData{})
if err != nil {
t.Error(err)
}
origOrder := &order.Detail{
Exchange: exchName,
OrderID: orderID,
Amount: 1337,
Price: 1337,
}
err = m.websocketDataHandler(exchName, origOrder)
if err != nil {
t.Error(err)
}
// Send it again since it exists now
err = m.websocketDataHandler(exchName, &order.Detail{
Exchange: exchName,
OrderID: orderID,
Amount: 1338,
})
if err != nil {
t.Error(err)
}
updated, err := m.orderManager.GetByExchangeAndID(origOrder.Exchange, origOrder.OrderID)
if err != nil {
t.Error(err)
}
if updated.Amount != 1338 {
t.Error("Bad pipeline")
}
err = m.websocketDataHandler(exchName, &order.Detail{
Exchange: "Bitstamp",
OrderID: orderID,
Status: order.Active,
})
if err != nil {
t.Error(err)
}
updated, err = m.orderManager.GetByExchangeAndID(origOrder.Exchange, origOrder.OrderID)
if err != nil {
t.Error(err)
}
if updated.Status != order.Active {
t.Error("Expected order to be modified to Active")
}
// Send some gibberish
err = m.websocketDataHandler(exchName, order.Stop)
if err != nil {
t.Error(err)
}
err = m.websocketDataHandler(exchName, stream.UnhandledMessageWarning{
Message: "there's an issue here's a tissue"},
)
if err != nil {
t.Error(err)
}
classificationError := order.ClassificationError{
Exchange: "test",
OrderID: "one",
Err: errors.New("lol"),
}
err = m.websocketDataHandler(exchName, classificationError)
if err == nil {
t.Error("Expected error")
}
if !errors.Is(err, classificationError.Err) {
t.Errorf("error '%v', expected '%v'", err, classificationError.Err)
}
err = m.websocketDataHandler(exchName, &orderbook.Base{
Exchange: "Bitstamp",
Pair: currency.NewPair(currency.BTC, currency.USD),
})
if err != nil {
t.Error(err)
}
err = m.websocketDataHandler(exchName, "this is a test string")
if err != nil {
t.Error(err)
}
}
func TestRegisterWebsocketDataHandlerWithFunctionality(t *testing.T) {
t.Parallel()
var m *WebsocketRoutineManager
err := m.registerWebsocketDataHandler(nil, false)
if !errors.Is(err, ErrNilSubsystem) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrNilSubsystem)
}
m = new(WebsocketRoutineManager)
m.shutdown = make(chan struct{})
err = m.registerWebsocketDataHandler(nil, false)
if !errors.Is(err, errNilWebsocketDataHandlerFunction) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilWebsocketDataHandlerFunction)
}
// externally defined capture device
dataChan := make(chan interface{})
fn := func(_ string, data interface{}) error {
switch data.(type) {
case string:
dataChan <- data
default:
}
return nil
}
err = m.registerWebsocketDataHandler(fn, true)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(m.dataHandlers) != 1 {
t.Fatal("unexpected data handlers registered")
}
mock := stream.New()
mock.ToRoutine = make(chan interface{})
m.state = readyState
err = m.websocketDataReceiver(mock)
if err != nil {
t.Fatal(err)
}
mock.ToRoutine <- nil
mock.ToRoutine <- 1336
mock.ToRoutine <- "intercepted"
if r := <-dataChan; r != "intercepted" {
t.Fatal("unexpected value received")
}
close(m.shutdown)
m.wg.Wait()
}
func TestSetWebsocketDataHandler(t *testing.T) {
t.Parallel()
var m *WebsocketRoutineManager
err := m.setWebsocketDataHandler(nil)
if !errors.Is(err, ErrNilSubsystem) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrNilSubsystem)
}
m = new(WebsocketRoutineManager)
m.shutdown = make(chan struct{})
err = m.setWebsocketDataHandler(nil)
if !errors.Is(err, errNilWebsocketDataHandlerFunction) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilWebsocketDataHandlerFunction)
}
err = m.registerWebsocketDataHandler(m.websocketDataHandler, false)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
err = m.registerWebsocketDataHandler(m.websocketDataHandler, false)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
err = m.registerWebsocketDataHandler(m.websocketDataHandler, false)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(m.dataHandlers) != 3 {
t.Fatal("unexpected data handler count")
}
err = m.setWebsocketDataHandler(m.websocketDataHandler)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if len(m.dataHandlers) != 1 {
t.Fatal("unexpected data handler count")
}
}