Skip to content

Commit

Permalink
Subscription: Test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
gbjk committed Feb 24, 2024
1 parent 2ffdc57 commit 65ac511
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 83 deletions.
13 changes: 13 additions & 0 deletions currency/pairs.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ func (p Pairs) Strings() []string {
return list
}

// String is a convenience method returning a comma-separated string of uppercase currencies using / as delimiter
func (p Pairs) String() string {
f := PairFormat{
Delimiter: "/",
Uppercase: true,
}
l := make([]string, len(p))
for i, pair := range p {
l[i] = f.Format(pair)
}
return strings.Join(l, ",")
}

// Join returns a comma separated list of currency pairs
func (p Pairs) Join() string {
return strings.Join(p.Strings(), ",")
Expand Down
51 changes: 11 additions & 40 deletions exchanges/stream/websocket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"net"
"net/http"
"os"
"sort"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -913,48 +912,20 @@ func TestCheckWebsocketURL(t *testing.T) {
assert.NoError(t, err, "checkWebsocketURL should not error")
}

// TestGetChannelDifference exercises GetChannelDifference
// See subscription.TestStoreDiff for further testing
func TestGetChannelDifference(t *testing.T) {
t.Parallel()
web := Websocket{}

newChans := subscription.List{
{Channel: "Test1"},
{Channel: "Test2"},
{Channel: "Test3"},
}
subs, unsubs := web.GetChannelDifference(newChans)
assert.Implements(t, (*subscription.MatchableKey)(nil), subs[0].Key, "Sub key must be matchable")
assert.Equal(t, 3, len(subs), "Should get the correct number of subs")
assert.Empty(t, unsubs, "Should get no unsubs")

for _, s := range subs {
s.SetState(subscription.SubscribedState)
}

web.AddSubscriptions(subs)

flushedSubs := subscription.List{
{Channel: "Test2"},
}

subs, unsubs = web.GetChannelDifference(flushedSubs)
assert.Empty(t, subs, "Should get no subs")
assert.Equal(t, 2, len(unsubs), "Should get the correct number of unsubs")

flushedSubs = subscription.List{
{Channel: "Test2"},
{Channel: "Test4"},
}

subs, unsubs = web.GetChannelDifference(flushedSubs)
if assert.Equal(t, 1, len(subs), "Should get the correct number of subs") {
assert.Equal(t, "Test4", subs[0].Channel, "Should subscribe to the right channel")
}
if assert.Equal(t, 2, len(unsubs), "Should get the correct number of unsubs") {
sort.Slice(unsubs, func(i, j int) bool { return unsubs[i].Channel <= unsubs[j].Channel })
assert.Equal(t, "Test1", unsubs[0].Channel, "Should unsubscribe from the right channels")
assert.Equal(t, "Test3", unsubs[1].Channel, "Should unsubscribe from the right channels")
}
w := &Websocket{}
assert.NotPanics(t, func() { w.GetChannelDifference(subscription.List{}) }, "Should not panic when called without a store")
subs, unsubs := w.GetChannelDifference(subscription.List{{Channel: subscription.CandlesChannel}})
require.Equal(t, 1, len(subs), "Should get the correct number of subs")
require.Empty(t, unsubs, "Should get no unsubs")
w.AddSubscriptions(subs)
subs, unsubs = w.GetChannelDifference(subscription.List{{Channel: subscription.TickerChannel}})
require.Equal(t, 1, len(subs), "Should get the correct number of subs")
assert.Equal(t, 1, len(unsubs), "Should get the correct number of unsubs")
}

// GenSubs defines a theoretical exchange with pair management
Expand Down
25 changes: 25 additions & 0 deletions exchanges/subscription/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package subscription

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)

func TestListStrings(t *testing.T) {
l := List{
&Subscription{
Channel: TickerChannel,
Asset: asset.Spot,
Pairs: currency.Pairs{ethusdcPair, btcusdtPair},
},
&Subscription{
Channel: OrderbookChannel,
Pairs: currency.Pairs{ethusdcPair},
},
}
exp := []string{"orderbook ETH/USDC", "ticker spot ETH/USDC,BTC/USDT"}
assert.ElementsMatch(t, exp, l.Strings(), "String must return correct sorted list")
}
27 changes: 19 additions & 8 deletions exchanges/subscription/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ func (s *Store) Add(sub *Subscription) error {
// Add adds a subscription to the store
// This method provides no locking protection
func (s *Store) add(sub *Subscription) error {
if s.m == nil {
s.m = map[any]*Subscription{}
}
key := sub.EnsureKeyed()
if found := s.get(key); found != nil {
return ErrDuplicate
Expand All @@ -55,9 +58,10 @@ func (s *Store) add(sub *Subscription) error {
}

// Get returns a pointer to a subscription or nil if not found
// If key implements MatchableKey then key.Match will be used
// If the key passed in is a Subscription then its Key will be used; which may be a pointer to itself.
// If key implements MatchableKey then key.Match will be used; Note that *Subscription implements MatchableKey
func (s *Store) Get(key any) *Subscription {
if s == nil {
if s == nil || s.m == nil {
return nil
}
s.mu.RLock()
Expand All @@ -69,8 +73,10 @@ func (s *Store) Get(key any) *Subscription {
// If the key passed in is a Subscription then its Key will be used; which may be a pointer to itself.
// If key implements MatchableKey then key.Match will be used; Note that *Subscription implements MatchableKey
// This method provides no locking protection
// returned subscriptions are implicitly guaranteed to have a Key
func (s *Store) get(key any) *Subscription {
if s.m == nil {
return nil
}
switch v := key.(type) {
case Subscription:
key = v.EnsureKeyed()
Expand All @@ -87,14 +93,16 @@ func (s *Store) get(key any) *Subscription {
}

// Remove removes a subscription from the store
func (s *Store) Remove(sub *Subscription) error {
if s == nil || sub == nil {
// If the key passed in is a Subscription then its Key will be used; which may be a pointer to itself.
// If key implements MatchableKey then key.Match will be used; Note that *Subscription implements MatchableKey
func (s *Store) Remove(key any) error {
if s == nil || key == nil {
return common.ErrNilPointer
}
s.mu.Lock()
defer s.mu.Unlock()

if found := s.get(sub); found != nil {
if found := s.get(key); found != nil {
delete(s.m, found.Key)
return nil
}
Expand All @@ -104,7 +112,7 @@ func (s *Store) Remove(sub *Subscription) error {

// List returns a slice of Subscriptions pointers
func (s *Store) List() List {
if s == nil {
if s == nil || s.m == nil {
return List{}
}
s.mu.RLock()
Expand All @@ -123,6 +131,9 @@ func (s *Store) Clear() {
}
s.mu.Lock()
defer s.mu.Unlock()
if s.m == nil {
s.m = map[any]*Subscription{}
}
clear(s.m)
}

Expand Down Expand Up @@ -167,7 +178,7 @@ func (s *Store) Diff(compare List) (added, removed List) {

// Len returns the number of subscriptions
func (s *Store) Len() int {
if s == nil {
if s == nil || s.m == nil {
return 0
}
s.mu.RLock()
Expand Down
184 changes: 184 additions & 0 deletions exchanges/subscription/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package subscription

import (
"maps"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)

// TestNewStore exercises NewStore
func TestNewStore(t *testing.T) {
s := NewStore()
require.IsType(t, &Store{}, s, "Must return a store ref")
require.NotNil(t, s.m, "storage map must be initialised")
}

// TestNewStoreFromList exercises NewStoreFromList
func TestNewStoreFromList(t *testing.T) {
s, err := NewStoreFromList(List{})
assert.NoError(t, err, "Should not error on empty list")
require.IsType(t, &Store{}, s, "Must return a store ref")
l := List{
{Channel: OrderbookChannel},
{Channel: TickerChannel},
}
s, err = NewStoreFromList(l)
assert.NoError(t, err, "Should not error on empty list")
assert.Len(t, s.m, 2, "Map should have 2 values")
assert.NotNil(t, s.get(l[0]), "Should be able to get a list element")

l = append(l, &Subscription{Channel: OrderbookChannel})
s, err = NewStoreFromList(l)
assert.ErrorIs(t, err, ErrDuplicate, "Should error correctly on duplicates")
}

// TestAdd exercises Add and add methods
func TestAdd(t *testing.T) {
assert.ErrorIs(t, (*Store)(nil).Add(&Subscription{}), common.ErrNilPointer, "Should error nil pointer correctly")
assert.ErrorIs(t, (&Store{}).Add(nil), common.ErrNilPointer, "Should error nil pointer correctly")
assert.NoError(t, (&Store{}).Add(&Subscription{}), "Should with no map should not error or panic")
s := NewStore()
sub := &Subscription{Channel: TickerChannel}
require.NoError(t, s.Add(sub), "Should not error on a standard add")
assert.NotNil(t, s.get(sub), "Should have stored the sub")
assert.ErrorIs(t, s.Add(sub), ErrDuplicate, "Should error on duplicates")
assert.Same(t, sub.Key, sub, "Add should call EnsureKeyed")
}

// HobbitKey is just a fixture for testing MatchableKey
type HobbitKey int

// Match implements MatchableKey
// Returns true if the key provided is twice as big as the actual sub key
func (f HobbitKey) Match(key any) bool {
i, ok := key.(HobbitKey)
return ok && int(i)*2 == int(f)
}

// TestGet exercises Get and get methods
func TestGet(t *testing.T) {
assert.Nil(t, (*Store)(nil).Get(&Subscription{}), "Should return nil when called on nil")
assert.Nil(t, (&Store{}).Get(&Subscription{}), "Should return nil when called with no subscription map")
s := NewStore()
exp := List{
{Channel: OrderbookChannel},
{Channel: TickerChannel},
{Key: 42, Channel: CandlesChannel},
{Key: HobbitKey(24), Channel: CandlesChannel},
}
for _, sub := range exp {
require.NoError(t, s.Add(sub), "Adding subscription must not error)")
}

assert.Nil(t, s.Get(Subscription{Channel: OrderbookChannel, Asset: asset.Spot}), "Should return nil for an unknown sub")
assert.Same(t, exp[0], s.Get(exp[0]), "Should return same pointer for known sub")
assert.Same(t, exp[1], s.Get(Subscription{Channel: TickerChannel}), "Should return pointer for known sub passed-by-value")
assert.Same(t, exp[2], s.Get(42), "Should return pointer for simple key lookup")
assert.Same(t, exp[3], s.Get(HobbitKey(48)), "Should use MatchableKey interface to find subs")
assert.Nil(t, s.Get(HobbitKey(24)), "Should use MatchableKey interface to find subs, therefore not find a HobbitKey 24")
}

// TestRemove exercises the Remove method
func TestRemove(t *testing.T) {
assert.ErrorIs(t, (*Store)(nil).Remove(&Subscription{}), common.ErrNilPointer, "Should error correctly when called on nil")
assert.ErrorIs(t, (&Store{}).Remove(nil), common.ErrNilPointer, "Should error correctly when called passing nil")
assert.ErrorIs(t, (&Store{}).Remove(&Subscription{}), ErrNotFound, "Should error correctly when called with no subscription map")
s := NewStore()
require.NoError(t, s.Add(&Subscription{Key: HobbitKey(24), Channel: CandlesChannel}), "Adding subscription must not error")
assert.ErrorIs(t, s.Remove(HobbitKey(24)), ErrNotFound, "Should error correctly when called with a non-matching hobbitkey")
assert.NoError(t, s.Remove(HobbitKey(48)), ErrNotFound, "Should Remove correctly when called matching hobbitkey")
assert.ErrorIs(t, s.Remove(HobbitKey(48)), ErrNotFound, "Should error correctly when called twice on same key")
}

// TestList exercises the List and Len methods
func TestList(t *testing.T) {
assert.Empty(t, (*Store)(nil).List(), "Should return an empty List when called on nil")
assert.Empty(t, (&Store{}).List(), "Should return an empty List when called on Store without map")
s := NewStore()
exp := List{
{Channel: OrderbookChannel},
{Channel: TickerChannel},
{Key: 42, Channel: CandlesChannel},
}
for _, sub := range exp {
require.NoError(t, s.Add(sub), "Adding subscription must not error)")
}
l := s.List()
require.Len(t, l, 3, "Must have 3 elements in the list")
assert.ElementsMatch(t, exp, l, "List Should have the same subscriptions")

require.Equal(t, 3, s.Len(), "Len must return 3")
require.Equal(t, 0, (*Store)(nil).Len(), "Len must return 0 on a nil store")
require.Equal(t, 0, (&Store{}).Len(), "Len must return 0 on an uninitialized store")

}

// TestStoreClear exercises the Clear method
func TestStoreClear(t *testing.T) {
assert.NotPanics(t, func() { (*Store)(nil).Clear() }, "Should not panic when called on nil")
s := &Store{}
assert.NotPanics(t, func() { s.Clear() }, "Should not panic when called with no subscription map")
assert.NotNil(t, s.m, "Should create a map when called on an empty Store")
require.NoError(t, s.Add(&Subscription{Key: HobbitKey(24), Channel: CandlesChannel}), "Adding subscription must not error")
require.Len(t, s.m, 1, "Must have a subscription")
s.Clear()
require.Empty(t, s.m, "Map must be empty after clearing")
assert.NotPanics(t, func() { s.Clear() }, "Should not panic when called on an empty map")
}

// TestStoreDiff exercises the Diff method
func TestStoreDiff(t *testing.T) {
s := NewStore()
assert.NotPanics(t, func() { (*Store)(nil).Diff(List{}) }, "Should not panic when called on nil")
assert.NotPanics(t, func() { (&Store{}).Diff(List{}) }, "Should not panic when called with no subscription map")
subs, unsubs := s.Diff(List{{Channel: TickerChannel}, {Channel: CandlesChannel}, {Channel: OrderbookChannel}})
assert.Equal(t, 3, len(subs), "Should get the correct number of subs")
assert.Empty(t, unsubs, "Should get no unsubs")
for _, sub := range subs {
s.add(sub)
}
assert.NotPanics(t, func() { s.Diff(nil) }, "Should not panic when called with nil list")

subs, unsubs = s.Diff(List{{Channel: CandlesChannel}})
assert.Empty(t, subs, "Should get no subs")
assert.Equal(t, 2, len(unsubs), "Should get the correct number of unsubs")
subs, unsubs = s.Diff(List{{Channel: TickerChannel}, {Channel: MyTradesChannel}})
require.Equal(t, 1, len(subs), "Should get the correct number of subs")
assert.Equal(t, MyTradesChannel, subs[0].Channel, "Should get correct channels in sub")
require.Equal(t, 2, len(unsubs), "Should get the correct number of unsubs")
EqualLists(t, unsubs, List{{Channel: OrderbookChannel}, {Channel: CandlesChannel}})
}

func EqualLists(tb testing.TB, a, b List) {
tb.Helper()
// Must not use store.Diff directly
s, err := NewStoreFromList(a)
require.NoError(tb, err, "NewStoreFromList must not error")
missingMap := maps.Clone(s.m)
var added, missing List
for _, sub := range b {
if found := s.get(sub); found != nil {
delete(missingMap, found.Key)
} else {
added = append(added, sub)
}
}
for _, c := range missingMap {
missing = append(missing, c)
}
if len(added) > 0 || len(missing) > 0 {
fail := "Differences:"
if len(added) > 0 {
fail = fail + "\n + " + strings.Join(added.Strings(), "\n + ")
}
if len(missing) > 0 {
fail = fail + "\n - " + strings.Join(missing.Strings(), "\n - ")
}
assert.Fail(tb, fail, "Subscriptions should be equal")
}
}
Loading

0 comments on commit 65ac511

Please sign in to comment.