Config: Refactor version packages (#1887)

* Config: Move config versions to separate pacakges

* Config: Move version tests to blackbox texts

* Config: Protect registerVersion from overflow

* Config: Protect against version already registered
This commit is contained in:
Gareth Kirwan
2025-04-22 04:13:01 +02:00
committed by GitHub
parent 545fa9d01a
commit 1bf3433d61
19 changed files with 254 additions and 153 deletions

View File

@@ -0,0 +1,21 @@
package versions
import (
v0 "github.com/thrasher-corp/gocryptotrader/config/versions/v0"
v1 "github.com/thrasher-corp/gocryptotrader/config/versions/v1"
v2 "github.com/thrasher-corp/gocryptotrader/config/versions/v2"
v3 "github.com/thrasher-corp/gocryptotrader/config/versions/v3"
v4 "github.com/thrasher-corp/gocryptotrader/config/versions/v4"
v5 "github.com/thrasher-corp/gocryptotrader/config/versions/v5"
v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6"
)
func init() {
Manager.registerVersion(0, &v0.Version{})
Manager.registerVersion(1, &v1.Version{})
Manager.registerVersion(2, &v2.Version{})
Manager.registerVersion(3, &v3.Version{})
Manager.registerVersion(4, &v4.Version{})
Manager.registerVersion(5, &v5.Version{})
Manager.registerVersion(6, &v6.Version{})
}

View File

@@ -0,0 +1,49 @@
package versions_test
import (
"io/fs"
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"testing"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/config/versions"
testutils "github.com/thrasher-corp/gocryptotrader/internal/testing/utils"
)
func TestVersionsRegistered(t *testing.T) {
t.Parallel()
r, err := testutils.RootPathFromCWD()
require.NoError(t, err)
versionsDir := filepath.Join(r, "config", "versions")
_, err = os.Stat(versionsDir)
require.NoError(t, err, "config/versions must exist")
err = filepath.WalkDir(versionsDir, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() || p == versionsDir {
return nil
}
verStr := filepath.Base(p)
verMatch := regexp.MustCompile(`v(\d+)`).FindStringSubmatch(verStr)
if len(verMatch) != 2 {
return filepath.SkipDir
}
t.Run(verStr, func(t *testing.T) {
version, err := strconv.ParseUint(verMatch[1], 10, 16)
require.NoError(t, err, "verMatch must ParseUint without error")
v := versions.Manager.Version(uint16(version))
require.NotNil(t, v, "version.Manager init must register this version")
require.Contains(t, reflect.TypeOf(v).String(), "*"+verStr+".Version", "version registered must be the correct type")
})
return filepath.SkipDir
})
require.NoError(t, err, "WalkDir must not error")
}

View File

@@ -1,23 +0,0 @@
package versions
import (
"context"
)
// Version0 is a baseline version with no changes, so we can downgrade back to nothing
// It does not implement any upgrade interfaces
type Version0 struct{}
func init() {
Manager.registerVersion(0, &Version0{})
}
// UpgradeConfig is an empty stub
func (v *Version0) UpgradeConfig(_ context.Context, j []byte) ([]byte, error) {
return j, nil
}
// DowngradeConfig is an empty stub
func (v *Version0) DowngradeConfig(_ context.Context, j []byte) ([]byte, error) {
return j, nil
}

19
config/versions/v0/v0.go Normal file
View File

@@ -0,0 +1,19 @@
package v0
import (
"context"
)
// Version is a baseline version with no changes, so we can downgrade back to nothing
// It does not implement any upgrade interfaces
type Version struct{}
// UpgradeConfig is an empty stub
func (*Version) UpgradeConfig(_ context.Context, j []byte) ([]byte, error) {
return j, nil
}
// DowngradeConfig is an empty stub
func (*Version) DowngradeConfig(_ context.Context, j []byte) ([]byte, error) {
return j, nil
}

View File

@@ -0,0 +1,26 @@
package v0_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v0 "github.com/thrasher-corp/gocryptotrader/config/versions/v0"
)
func TestUpgradeConfig(t *testing.T) {
t.Parallel()
in := []byte(`{"untouched":true}`)
out, err := new(v0.Version).UpgradeConfig(context.Background(), in)
require.NoError(t, err)
assert.Equal(t, in, out)
}
func TestDowngradeConfig(t *testing.T) {
t.Parallel()
in := []byte(`{"untouched":true}`)
out, err := new(v0.Version).DowngradeConfig(context.Background(), in)
require.NoError(t, err)
assert.Equal(t, in, out)
}

View File

@@ -1,26 +1,21 @@
package versions package v1
import ( import (
"context" "context"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
v0 "github.com/thrasher-corp/gocryptotrader/config/versions/v0" v0 "github.com/thrasher-corp/gocryptotrader/config/versions/v0"
v1 "github.com/thrasher-corp/gocryptotrader/config/versions/v1"
"github.com/thrasher-corp/gocryptotrader/encoding/json" "github.com/thrasher-corp/gocryptotrader/encoding/json"
) )
// Version1 is an ExchangeVersion to upgrade currency pair format for exchanges // Version is an ExchangeVersion to upgrade currency pair format for exchanges
type Version1 struct{} type Version struct{}
func init() {
Manager.registerVersion(1, &Version1{})
}
// Exchanges returns all exchanges: "*" // Exchanges returns all exchanges: "*"
func (v *Version1) Exchanges() []string { return []string{"*"} } func (*Version) Exchanges() []string { return []string{"*"} }
// UpgradeExchange will upgrade currency pair format // UpgradeExchange will upgrade currency pair format
func (v *Version1) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) { func (*Version) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) {
if _, d, _, err := jsonparser.Get(e, "currencyPairs"); err == nil && d == jsonparser.Object { if _, d, _, err := jsonparser.Get(e, "currencyPairs"); err == nil && d == jsonparser.Object {
return e, nil return e, nil
} }
@@ -30,12 +25,12 @@ func (v *Version1) UpgradeExchange(_ context.Context, e []byte) ([]byte, error)
return e, err return e, err
} }
p := &v1.PairsManager{ p := &PairsManager{
UseGlobalFormat: true, UseGlobalFormat: true,
LastUpdated: d.PairsLastUpdated, LastUpdated: d.PairsLastUpdated,
ConfigFormat: d.ConfigCurrencyPairFormat, ConfigFormat: d.ConfigCurrencyPairFormat,
RequestFormat: d.RequestCurrencyPairFormat, RequestFormat: d.RequestCurrencyPairFormat,
Pairs: v1.FullStore{ Pairs: FullStore{
"spot": { "spot": {
Available: d.AvailablePairs, Available: d.AvailablePairs,
Enabled: d.EnabledPairs, Enabled: d.EnabledPairs,
@@ -53,6 +48,6 @@ func (v *Version1) UpgradeExchange(_ context.Context, e []byte) ([]byte, error)
} }
// DowngradeExchange doesn't do anything for v1; There's no downgrade path since the original state is lossy and v1 was before versioning // DowngradeExchange doesn't do anything for v1; There's no downgrade path since the original state is lossy and v1 was before versioning
func (v *Version1) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) { func (*Version) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) {
return e, nil return e, nil
} }

View File

@@ -1,4 +1,4 @@
package versions package v1_test
import ( import (
"bytes" "bytes"
@@ -7,12 +7,18 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
v1 "github.com/thrasher-corp/gocryptotrader/config/versions/v1"
) )
func TestVersion1Upgrade(t *testing.T) { func TestExchanges(t *testing.T) {
t.Parallel()
assert.Equal(t, []string{"*"}, new(v1.Version).Exchanges())
}
func TestUpgradeExchange(t *testing.T) {
t.Parallel() t.Parallel()
v := &Version1{} v := &v1.Version{}
in := []byte(`{"name":"Wibble","pairsLastUpdated":1566798411,"assetTypes":"spot","configCurrencyPairFormat":{"uppercase":true,"delimiter":"_"},"requestCurrencyPairFormat":{"uppercase":false,"delimiter":"_","separator":"-"},"enabledPairs":"LTC_BTC","availablePairs":"LTC_BTC,ETH_BTC,BTC_USD"}`) in := []byte(`{"name":"Wibble","pairsLastUpdated":1566798411,"assetTypes":"spot","configCurrencyPairFormat":{"uppercase":true,"delimiter":"_"},"requestCurrencyPairFormat":{"uppercase":false,"delimiter":"_","separator":"-"},"enabledPairs":"LTC_BTC","availablePairs":"LTC_BTC,ETH_BTC,BTC_USD"}`)
exp := []byte(`{"name":"Wibble","currencyPairs":{"bypassConfigFormatUpgrades":false,"requestFormat":{"uppercase":false,"delimiter":"_","separator":"-"},"configFormat":{"uppercase":true,"delimiter":"_"},"useGlobalFormat":true,"lastUpdated":1566798411,"pairs":{"spot":{"enabled":"LTC_BTC","available":"LTC_BTC,ETH_BTC,BTC_USD"}}}}`) exp := []byte(`{"name":"Wibble","currencyPairs":{"bypassConfigFormatUpgrades":false,"requestFormat":{"uppercase":false,"delimiter":"_","separator":"-"},"configFormat":{"uppercase":true,"delimiter":"_"},"useGlobalFormat":true,"lastUpdated":1566798411,"pairs":{"spot":{"enabled":"LTC_BTC","available":"LTC_BTC,ETH_BTC,BTC_USD"}}}}`)
@@ -22,10 +28,10 @@ func TestVersion1Upgrade(t *testing.T) {
assert.Equal(t, string(exp), string(out)) assert.Equal(t, string(exp), string(out))
} }
func TestVersion1Downgrade(t *testing.T) { func TestDowngradeExchange(t *testing.T) {
t.Parallel() t.Parallel()
in := []byte("just leave me alone, mkay?") in := []byte("just leave me alone, mkay?")
out, err := new(Version1).DowngradeExchange(context.Background(), bytes.Clone(in)) out, err := new(v1.Version).DowngradeExchange(context.Background(), bytes.Clone(in))
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, out, in) assert.Equal(t, out, in)
} }

View File

@@ -1,4 +1,4 @@
package versions package v2
import ( import (
"context" "context"
@@ -6,18 +6,14 @@ import (
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
) )
// Version2 is an ExchangeVersion to change the name of GDAX to CoinbasePro // Version is an ExchangeVersion to change the name of GDAX to CoinbasePro
type Version2 struct{} type Version struct{}
func init() {
Manager.registerVersion(2, &Version2{})
}
// Exchanges returns just GDAX and CoinbasePro // Exchanges returns just GDAX and CoinbasePro
func (v *Version2) Exchanges() []string { return []string{"GDAX", "CoinbasePro"} } func (*Version) Exchanges() []string { return []string{"GDAX", "CoinbasePro"} }
// UpgradeExchange will change the exchange name from GDAX to CoinbasePro // UpgradeExchange will change the exchange name from GDAX to CoinbasePro
func (v *Version2) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) { func (*Version) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) {
if n, err := jsonparser.GetString(e, "name"); err == nil && n == "GDAX" { if n, err := jsonparser.GetString(e, "name"); err == nil && n == "GDAX" {
return jsonparser.Set(e, []byte(`"CoinbasePro"`), "name") return jsonparser.Set(e, []byte(`"CoinbasePro"`), "name")
} }
@@ -25,7 +21,7 @@ func (v *Version2) UpgradeExchange(_ context.Context, e []byte) ([]byte, error)
} }
// DowngradeExchange will change the exchange name from CoinbasePro to GDAX // DowngradeExchange will change the exchange name from CoinbasePro to GDAX
func (v *Version2) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) { func (*Version) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) {
if n, err := jsonparser.GetString(e, "name"); err == nil && n == "CoinbasePro" { if n, err := jsonparser.GetString(e, "name"); err == nil && n == "CoinbasePro" {
return jsonparser.Set(e, []byte(`"GDAX"`), "name") return jsonparser.Set(e, []byte(`"GDAX"`), "name")
} }

View File

@@ -1,4 +1,4 @@
package versions package v2_test
import ( import (
"context" "context"
@@ -6,30 +6,31 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
v2 "github.com/thrasher-corp/gocryptotrader/config/versions/v2"
) )
func TestVersion2Upgrade(t *testing.T) { func TestUpgradeExchange(t *testing.T) {
t.Parallel() t.Parallel()
for _, tt := range [][]string{ for _, tt := range [][]string{
{"GDAX", "CoinbasePro"}, {"GDAX", "CoinbasePro"},
{"Kraken", "Kraken"}, {"Kraken", "Kraken"},
{"CoinbasePro", "CoinbasePro"}, {"CoinbasePro", "CoinbasePro"},
} { } {
out, err := new(Version2).UpgradeExchange(context.Background(), []byte(`{"name":"`+tt[0]+`"}`)) out, err := new(v2.Version).UpgradeExchange(context.Background(), []byte(`{"name":"`+tt[0]+`"}`))
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, out) require.NotEmpty(t, out)
assert.Equalf(t, `{"name":"`+tt[1]+`"}`, string(out), "Test exchange name %s", tt[0]) assert.Equalf(t, `{"name":"`+tt[1]+`"}`, string(out), "Test exchange name %s", tt[0])
} }
} }
func TestVersion2Downgrade(t *testing.T) { func TestDowngradeExchange(t *testing.T) {
t.Parallel() t.Parallel()
for _, tt := range [][]string{ for _, tt := range [][]string{
{"GDAX", "GDAX"}, {"GDAX", "GDAX"},
{"Kraken", "Kraken"}, {"Kraken", "Kraken"},
{"CoinbasePro", "GDAX"}, {"CoinbasePro", "GDAX"},
} { } {
out, err := new(Version2).DowngradeExchange(context.Background(), []byte(`{"name":"`+tt[0]+`"}`)) out, err := new(v2.Version).DowngradeExchange(context.Background(), []byte(`{"name":"`+tt[0]+`"}`))
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, out) require.NotEmpty(t, out)
assert.Equalf(t, `{"name":"`+tt[1]+`"}`, string(out), "Test exchange name %s", tt[0]) assert.Equalf(t, `{"name":"`+tt[1]+`"}`, string(out), "Test exchange name %s", tt[0])

View File

@@ -1,4 +1,4 @@
package versions package v3
import ( import (
"context" "context"
@@ -8,18 +8,14 @@ import (
"github.com/thrasher-corp/gocryptotrader/encoding/json" "github.com/thrasher-corp/gocryptotrader/encoding/json"
) )
// Version3 is an ExchangeVersion to remove the publishPeriod from the exchange's orderbook config // Version is an ExchangeVersion to remove the publishPeriod from the exchange's orderbook config
type Version3 struct{} type Version struct{}
func init() {
Manager.registerVersion(3, &Version3{})
}
// Exchanges returns all exchanges: "*" // Exchanges returns all exchanges: "*"
func (v *Version3) Exchanges() []string { return []string{"*"} } func (*Version) Exchanges() []string { return []string{"*"} }
// UpgradeExchange will remove the publishPeriod from the exchange's orderbook config // UpgradeExchange will remove the publishPeriod from the exchange's orderbook config
func (v *Version3) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) { func (*Version) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) {
e = jsonparser.Delete(e, "orderbook", "publishPeriod") e = jsonparser.Delete(e, "orderbook", "publishPeriod")
return e, nil return e, nil
} }
@@ -27,7 +23,7 @@ func (v *Version3) UpgradeExchange(_ context.Context, e []byte) ([]byte, error)
const defaultOrderbookPublishPeriod = time.Second * 10 const defaultOrderbookPublishPeriod = time.Second * 10
// DowngradeExchange will downgrade the exchange's config by setting the default orderbook publish period // DowngradeExchange will downgrade the exchange's config by setting the default orderbook publish period
func (v *Version3) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) { func (*Version) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) {
if _, _, _, err := jsonparser.Get(e, "orderbook"); err != nil { if _, _, _, err := jsonparser.Get(e, "orderbook"); err != nil {
return e, nil //nolint:nilerr // No error, just return the original config return e, nil //nolint:nilerr // No error, just return the original config
} }

View File

@@ -1,42 +1,43 @@
package versions package v3_test
import ( import (
"context" "context"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
v3 "github.com/thrasher-corp/gocryptotrader/config/versions/v3"
) )
func TestVersion3UpgradeExchange(t *testing.T) { func TestUpgradeExchange(t *testing.T) {
t.Parallel() t.Parallel()
got, err := (&Version3{}).UpgradeExchange(context.Background(), nil) got, err := (&v3.Version{}).UpgradeExchange(context.Background(), nil)
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, got) require.Nil(t, got)
payload := []byte(`{"orderbook": {"verificationBypass": false,"websocketBufferLimit": 5,"websocketBufferEnabled": false,"publishPeriod": 10000000000}}`) payload := []byte(`{"orderbook": {"verificationBypass": false,"websocketBufferLimit": 5,"websocketBufferEnabled": false,"publishPeriod": 10000000000}}`)
expected := []byte(`{"orderbook": {"verificationBypass": false,"websocketBufferLimit": 5,"websocketBufferEnabled": false}}`) expected := []byte(`{"orderbook": {"verificationBypass": false,"websocketBufferLimit": 5,"websocketBufferEnabled": false}}`)
got, err = (&Version3{}).UpgradeExchange(context.Background(), payload) got, err = (&v3.Version{}).UpgradeExchange(context.Background(), payload)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, got) require.Equal(t, expected, got)
} }
func TestVersion3DowngradeExchange(t *testing.T) { func TestDowngradeExchange(t *testing.T) {
t.Parallel() t.Parallel()
got, err := (&Version3{}).DowngradeExchange(context.Background(), nil) got, err := (&v3.Version{}).DowngradeExchange(context.Background(), nil)
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, got) require.Nil(t, got)
payload := []byte(`{"orderbook": {"verificationBypass": false,"websocketBufferLimit": 5,"websocketBufferEnabled": false}}`) payload := []byte(`{"orderbook": {"verificationBypass": false,"websocketBufferLimit": 5,"websocketBufferEnabled": false}}`)
expected := []byte(`{"orderbook": {"verificationBypass": false,"websocketBufferLimit": 5,"websocketBufferEnabled": false,"publishPeriod":10000000000}}`) expected := []byte(`{"orderbook": {"verificationBypass": false,"websocketBufferLimit": 5,"websocketBufferEnabled": false,"publishPeriod":10000000000}}`)
got, err = (&Version3{}).DowngradeExchange(context.Background(), payload) got, err = (&v3.Version{}).DowngradeExchange(context.Background(), payload)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, got) require.Equal(t, expected, got)
} }
func TestVersion3Exchanges(t *testing.T) { func TestExchanges(t *testing.T) {
t.Parallel() t.Parallel()
assert := require.New(t) assert := require.New(t)
assert.Equal([]string{"*"}, (&Version3{}).Exchanges()) assert.Equal([]string{"*"}, (&v3.Version{}).Exchanges())
} }

View File

@@ -1,4 +1,4 @@
package versions package v4
import ( import (
"bytes" "bytes"
@@ -10,18 +10,14 @@ import (
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
) )
// Version4 is an Exchange upgrade to move currencyPairs.assetTypes to currencyPairs.pairs.*.assetEnabled // Version is an Exchange upgrade to move currencyPairs.assetTypes to currencyPairs.pairs.*.assetEnabled
type Version4 struct{} type Version struct{}
func init() {
Manager.registerVersion(4, &Version4{})
}
// Exchanges returns all exchanges: "*" // Exchanges returns all exchanges: "*"
func (v *Version4) Exchanges() []string { return []string{"*"} } func (*Version) Exchanges() []string { return []string{"*"} }
// UpgradeExchange sets AssetEnabled: true for all assets listed in assetTypes, and false for any with no field // UpgradeExchange sets AssetEnabled: true for all assets listed in assetTypes, and false for any with no field
func (v *Version4) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) { func (*Version) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) {
toEnable := map[string]bool{} toEnable := map[string]bool{}
assetTypesFn := func(asset []byte, valueType jsonparser.ValueType, _ int, _ error) { assetTypesFn := func(asset []byte, valueType jsonparser.ValueType, _ int, _ error) {
@@ -31,7 +27,7 @@ func (v *Version4) UpgradeExchange(_ context.Context, e []byte) ([]byte, error)
} }
_, err := jsonparser.ArrayEach(e, assetTypesFn, "currencyPairs", "assetTypes") _, err := jsonparser.ArrayEach(e, assetTypesFn, "currencyPairs", "assetTypes")
if err != nil && !errors.Is(err, jsonparser.KeyPathNotFoundError) { if err != nil && !errors.Is(err, jsonparser.KeyPathNotFoundError) {
return e, fmt.Errorf("%w assetTypes: %w", errUpgrading, err) return e, fmt.Errorf("error upgrading assetTypes: %w", err)
} }
assetEnabledFn := func(assetBytes, v []byte, _ jsonparser.ValueType, _ int) (err error) { assetEnabledFn := func(assetBytes, v []byte, _ jsonparser.ValueType, _ int) (err error) {
@@ -54,14 +50,14 @@ func (v *Version4) UpgradeExchange(_ context.Context, e []byte) ([]byte, error)
return err return err
} }
if err = jsonparser.ObjectEach(bytes.Clone(e), assetEnabledFn, "currencyPairs", "pairs"); err != nil { if err = jsonparser.ObjectEach(bytes.Clone(e), assetEnabledFn, "currencyPairs", "pairs"); err != nil {
return e, fmt.Errorf("%w currencyPairs.pairs: %w", errUpgrading, err) return e, fmt.Errorf("error upgrading currencyPairs.pairs: %w", err)
} }
e = jsonparser.Delete(e, "currencyPairs", "assetTypes") e = jsonparser.Delete(e, "currencyPairs", "assetTypes")
return e, err return e, err
} }
// DowngradeExchange moves AssetEnabled assets into AssetType field // DowngradeExchange moves AssetEnabled assets into AssetType field
func (v *Version4) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) { func (*Version) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) {
assetTypes := []string{} assetTypes := []string{}
assetEnabledFn := func(asset, v []byte, _ jsonparser.ValueType, _ int) error { assetEnabledFn := func(asset, v []byte, _ jsonparser.ValueType, _ int) error {

View File

@@ -1,4 +1,4 @@
package versions package v4_test
import ( import (
"bytes" "bytes"
@@ -8,31 +8,25 @@ import (
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
v4 "github.com/thrasher-corp/gocryptotrader/config/versions/v4"
) )
func TestVersion4ExchangeType(t *testing.T) { func TestExchanges(t *testing.T) {
t.Parallel() t.Parallel()
assert.Implements(t, (*ExchangeVersion)(nil), new(Version4)) assert.Equal(t, []string{"*"}, new(v4.Version).Exchanges())
} }
func TestVersion4Exchanges(t *testing.T) { func TestUpgradeExchange(t *testing.T) {
t.Parallel()
assert.Equal(t, []string{"*"}, new(Version4).Exchanges())
}
func TestVersion4Upgrade(t *testing.T) {
t.Parallel() t.Parallel()
_, err := new(Version4).UpgradeExchange(context.Background(), []byte{}) _, err := new(v4.Version).UpgradeExchange(context.Background(), []byte{})
require.ErrorIs(t, err, errUpgrading) require.ErrorContains(t, err, `error upgrading assetTypes`)
require.ErrorContains(t, err, `assetTypes`)
_, err = new(Version4).UpgradeExchange(context.Background(), []byte(`{}`)) _, err = new(v4.Version).UpgradeExchange(context.Background(), []byte(`{}`))
require.ErrorIs(t, err, errUpgrading) require.ErrorContains(t, err, `error upgrading currencyPairs.pairs`)
require.ErrorContains(t, err, `currencyPairs.pairs`)
in := []byte(`{"name":"Cracken","currencyPairs":{"assetTypes":["spot"],"pairs":{"spot":{"enabled":"BTC-AUD","available":"BTC-AUD"},"futures":{"assetEnabled":true},"options":{},"margin":{"assetEnabled":null}}}}`) in := []byte(`{"name":"Cracken","currencyPairs":{"assetTypes":["spot"],"pairs":{"spot":{"enabled":"BTC-AUD","available":"BTC-AUD"},"futures":{"assetEnabled":true},"options":{},"margin":{"assetEnabled":null}}}}`)
out, err := new(Version4).UpgradeExchange(context.Background(), in) out, err := new(v4.Version).UpgradeExchange(context.Background(), in)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, out) require.NotEmpty(t, out)
@@ -55,26 +49,26 @@ func TestVersion4Upgrade(t *testing.T) {
require.NoError(t, err, "Must find assetEnabled for margin") require.NoError(t, err, "Must find assetEnabled for margin")
assert.False(t, e, "assetEnabled should be set to false") assert.False(t, e, "assetEnabled should be set to false")
out2, err := new(Version4).UpgradeExchange(context.Background(), out) out2, err := new(v4.Version).UpgradeExchange(context.Background(), out)
require.NoError(t, err, "Must not error on re-upgrading") require.NoError(t, err, "Must not error on re-upgrading")
assert.Equal(t, out, out2, "Should not affect an already upgraded config") assert.Equal(t, out, out2, "Should not affect an already upgraded config")
in = []byte(`{"name":"Cracken","currencyPairs":{"assetTypes":["spot"],"pairs":{"spot":{"assetEnabled":{}}}}}`) in = []byte(`{"name":"Cracken","currencyPairs":{"assetTypes":["spot"],"pairs":{"spot":{"assetEnabled":{}}}}}`)
_, err = new(Version4).UpgradeExchange(context.Background(), in) _, err = new(v4.Version).UpgradeExchange(context.Background(), in)
require.NoError(t, err) require.NoError(t, err)
in = []byte(`{"name":"Cracken","currencyPairs":{"assetTypes":["spot"],"pairs":{"margin":{"assetEnabled":{}}}}}`) in = []byte(`{"name":"Cracken","currencyPairs":{"assetTypes":["spot"],"pairs":{"margin":{"assetEnabled":{}}}}}`)
_, err = new(Version4).UpgradeExchange(context.Background(), in) _, err = new(v4.Version).UpgradeExchange(context.Background(), in)
require.ErrorIs(t, err, jsonparser.UnknownValueTypeError) require.ErrorIs(t, err, jsonparser.UnknownValueTypeError)
require.ErrorContains(t, err, "`margin`") require.ErrorContains(t, err, "`margin`")
require.ErrorContains(t, err, "`object`") require.ErrorContains(t, err, "`object`")
} }
func TestVersion4Downgrade(t *testing.T) { func TestDowngradeExchange(t *testing.T) {
t.Parallel() t.Parallel()
in := []byte(`{"name":"Cracken","currencyPairs":{"pairs":{"spot":{"enabled":"BTC-AUD","available":"BTC-AUD","assetEnabled":true},"futures":{"assetEnabled":false},"options":{},"options_combo":{"assetEnabled":true}}}}`) in := []byte(`{"name":"Cracken","currencyPairs":{"pairs":{"spot":{"enabled":"BTC-AUD","available":"BTC-AUD","assetEnabled":true},"futures":{"assetEnabled":false},"options":{},"options_combo":{"assetEnabled":true}}}}`)
out, err := new(Version4).DowngradeExchange(context.Background(), in) out, err := new(v4.Version).DowngradeExchange(context.Background(), in)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, out) require.NotEmpty(t, out)

View File

@@ -1,4 +1,4 @@
package versions package v5
import ( import (
"context" "context"
@@ -6,25 +6,20 @@ import (
"strconv" "strconv"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
v5 "github.com/thrasher-corp/gocryptotrader/config/versions/v5"
) )
// Version5 implements ConfigVersion // Version implements ConfigVersion
type Version5 struct{} type Version struct{}
func init() {
Manager.registerVersion(5, &Version5{})
}
// UpgradeConfig handles upgrading config for OrderManager: // UpgradeConfig handles upgrading config for OrderManager:
// * Sets OrderManager config to defaults if it doesn't exist or enabled is null // * Sets OrderManager config to defaults if it doesn't exist or enabled is null
// * Sets respectOrderHistoryLimits to true if it doesn't exist or is null // * Sets respectOrderHistoryLimits to true if it doesn't exist or is null
// * Sets futuresTrackingSeekDuration to positive if it's negative // * Sets futuresTrackingSeekDuration to positive if it's negative
func (v *Version5) UpgradeConfig(_ context.Context, e []byte) ([]byte, error) { func (*Version) UpgradeConfig(_ context.Context, e []byte) ([]byte, error) {
_, valueType, _, err := jsonparser.Get(e, "orderManager", "enabled") _, valueType, _, err := jsonparser.Get(e, "orderManager", "enabled")
switch { switch {
case errors.Is(err, jsonparser.KeyPathNotFoundError), valueType == jsonparser.Null: case errors.Is(err, jsonparser.KeyPathNotFoundError), valueType == jsonparser.Null:
return jsonparser.Set(e, v5.DefaultOrderbookConfig, "orderManager") return jsonparser.Set(e, DefaultOrderbookConfig, "orderManager")
case err != nil: case err != nil:
return e, err return e, err
} }
@@ -37,7 +32,7 @@ func (v *Version5) UpgradeConfig(_ context.Context, e []byte) ([]byte, error) {
} }
if i, err := jsonparser.GetInt(e, "orderManager", "futuresTrackingSeekDuration"); err != nil { if i, err := jsonparser.GetInt(e, "orderManager", "futuresTrackingSeekDuration"); err != nil {
if e, err = jsonparser.Set(e, []byte(v5.DefaultFuturesTrackingSeekDuration), "orderManager", "futuresTrackingSeekDuration"); err != nil { if e, err = jsonparser.Set(e, []byte(DefaultFuturesTrackingSeekDuration), "orderManager", "futuresTrackingSeekDuration"); err != nil {
return e, err return e, err
} }
} else if i < 0 { } else if i < 0 {
@@ -49,7 +44,7 @@ func (v *Version5) UpgradeConfig(_ context.Context, e []byte) ([]byte, error) {
} }
// DowngradeConfig just reverses the futuresTrackingSeekDuration to negative, and leaves everything else alone // DowngradeConfig just reverses the futuresTrackingSeekDuration to negative, and leaves everything else alone
func (v *Version5) DowngradeConfig(_ context.Context, e []byte) ([]byte, error) { func (*Version) DowngradeConfig(_ context.Context, e []byte) ([]byte, error) {
if i, err := jsonparser.GetInt(e, "orderManager", "futuresTrackingSeekDuration"); err == nil && i > 0 { if i, err := jsonparser.GetInt(e, "orderManager", "futuresTrackingSeekDuration"); err == nil && i > 0 {
if e, err = jsonparser.Set(e, []byte(strconv.FormatInt(-i, 10)), "orderManager", "futuresTrackingSeekDuration"); err != nil { if e, err = jsonparser.Set(e, []byte(strconv.FormatInt(-i, 10)), "orderManager", "futuresTrackingSeekDuration"); err != nil {
return e, err return e, err

View File

@@ -1,4 +1,4 @@
package versions package v5_test
import ( import (
"bytes" "bytes"
@@ -10,9 +10,10 @@ import (
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
v5 "github.com/thrasher-corp/gocryptotrader/config/versions/v5"
) )
func TestVersion5Upgrade(t *testing.T) { func TestUpgradeConfig(t *testing.T) {
t.Parallel() t.Parallel()
expDef := `{"orderManager":{"enabled":true,"verbose":false,"activelyTrackFuturesPositions":true,"futuresTrackingSeekDuration":31536000000000000,"cancelOrdersOnShutdown":false,"respectOrderHistoryLimits":true}}` expDef := `{"orderManager":{"enabled":true,"verbose":false,"activelyTrackFuturesPositions":true,"futuresTrackingSeekDuration":31536000000000000,"cancelOrdersOnShutdown":false,"respectOrderHistoryLimits":true}}`
@@ -37,7 +38,7 @@ func TestVersion5Upgrade(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
_ = t.Run(tt.name, func(t *testing.T) { _ = t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
out, err := new(Version5).UpgradeConfig(context.Background(), []byte(tt.in)) out, err := new(v5.Version).UpgradeConfig(context.Background(), []byte(tt.in))
if tt.err != nil { if tt.err != nil {
require.ErrorIs(t, err, tt.err) require.ErrorIs(t, err, tt.err)
return return
@@ -50,16 +51,16 @@ func TestVersion5Upgrade(t *testing.T) {
} }
} }
func TestVersion5Downgrade(t *testing.T) { func TestDowngradeConfig(t *testing.T) {
t.Parallel() t.Parallel()
in := `{"orderManager":{"enabled":false,"verbose":true,"activelyTrackFuturesPositions":false,"futuresTrackingSeekDuration":-47000,"cancelOrdersOnShutdown":true,"respectOrderHistoryLimits":true}}` in := `{"orderManager":{"enabled":false,"verbose":true,"activelyTrackFuturesPositions":false,"futuresTrackingSeekDuration":-47000,"cancelOrdersOnShutdown":true,"respectOrderHistoryLimits":true}}`
exp := `{"orderManager":{"enabled":false,"verbose":true,"activelyTrackFuturesPositions":false,"futuresTrackingSeekDuration":-47000,"cancelOrdersOnShutdown":true,"respectOrderHistoryLimits":true}}` exp := `{"orderManager":{"enabled":false,"verbose":true,"activelyTrackFuturesPositions":false,"futuresTrackingSeekDuration":-47000,"cancelOrdersOnShutdown":true,"respectOrderHistoryLimits":true}}`
out, err := new(Version5).DowngradeConfig(context.Background(), []byte(in)) out, err := new(v5.Version).DowngradeConfig(context.Background(), []byte(in))
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, exp, string(out), "DowngradeConfig should just reverse the futuresTrackingSeekDuration") assert.Equal(t, exp, string(out), "DowngradeConfig should just reverse the futuresTrackingSeekDuration")
out, err = new(Version5).DowngradeConfig(context.Background(), []byte(exp)) out, err = new(v5.Version).DowngradeConfig(context.Background(), []byte(exp))
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, exp, string(out), "DowngradeConfig should leave an already negative futuresTrackingSeekDuration alone") assert.Equal(t, exp, string(out), "DowngradeConfig should leave an already negative futuresTrackingSeekDuration alone")
} }

View File

@@ -1,26 +1,21 @@
package versions package v6
import ( import (
"context" "context"
"errors" "errors"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6"
) )
// Version6 implements ConfigVersion // Version implements ConfigVersion
type Version6 struct{} type Version struct{}
func init() {
Manager.registerVersion(6, &Version6{})
}
// UpgradeConfig checks and upgrades the portfolioAddresses.providers field // UpgradeConfig checks and upgrades the portfolioAddresses.providers field
func (v *Version6) UpgradeConfig(_ context.Context, e []byte) ([]byte, error) { func (*Version) UpgradeConfig(_ context.Context, e []byte) ([]byte, error) {
_, valueType, _, err := jsonparser.Get(e, "portfolioAddresses", "providers") _, valueType, _, err := jsonparser.Get(e, "portfolioAddresses", "providers")
switch { switch {
case errors.Is(err, jsonparser.KeyPathNotFoundError), valueType == jsonparser.Null: case errors.Is(err, jsonparser.KeyPathNotFoundError), valueType == jsonparser.Null:
return jsonparser.Set(e, v6.DefaultConfig, "portfolioAddresses", "providers") return jsonparser.Set(e, DefaultConfig, "portfolioAddresses", "providers")
case err != nil: case err != nil:
return e, err return e, err
} }
@@ -28,7 +23,7 @@ func (v *Version6) UpgradeConfig(_ context.Context, e []byte) ([]byte, error) {
} }
// DowngradeConfig removes the portfolioAddresses.providers field // DowngradeConfig removes the portfolioAddresses.providers field
func (v *Version6) DowngradeConfig(_ context.Context, e []byte) ([]byte, error) { func (*Version) DowngradeConfig(_ context.Context, e []byte) ([]byte, error) {
e = jsonparser.Delete(e, "portfolioAddresses", "providers") e = jsonparser.Delete(e, "portfolioAddresses", "providers")
return e, nil return e, nil
} }

View File

@@ -1,4 +1,4 @@
package versions package v6_test
import ( import (
"bytes" "bytes"
@@ -11,30 +11,30 @@ import (
v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6" v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6"
) )
func TestVersion6Upgrade(t *testing.T) { func TestUpgradeConfig(t *testing.T) {
t.Parallel() t.Parallel()
in := []byte(` in := []byte(`
{"portfolioAddresses":{"addresses":[{"Address":"1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy","CoinType":"BTC","Balance":0.00108832,"Description":"","WhiteListed":false,"ColdStorage":false,"SupportedExchanges":""}]}} {"portfolioAddresses":{"addresses":[{"Address":"1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy","CoinType":"BTC","Balance":0.00108832,"Description":"","WhiteListed":false,"ColdStorage":false,"SupportedExchanges":""}]}}
`) `)
r, err := new(Version6).UpgradeConfig(context.Background(), in) r, err := new(v6.Version).UpgradeConfig(context.Background(), in)
require.NoError(t, err, "UpgradeConfig must not error") require.NoError(t, err, "UpgradeConfig must not error")
require.True(t, bytes.Contains(r, v6.DefaultConfig)) require.True(t, bytes.Contains(r, v6.DefaultConfig))
r2, err := new(Version6).UpgradeConfig(context.Background(), r) r2, err := new(v6.Version).UpgradeConfig(context.Background(), r)
require.NoError(t, err, "UpgradeConfig must not error") require.NoError(t, err, "UpgradeConfig must not error")
assert.Equal(t, r, r2, "UpgradeConfig should not affect an already upgraded config") assert.Equal(t, r, r2, "UpgradeConfig should not affect an already upgraded config")
} }
func TestVersion6Downgrade(t *testing.T) { func TestDowngradeConfig(t *testing.T) {
t.Parallel() t.Parallel()
in := []byte(` in := []byte(`
{"portfolioAddresses":{"addresses":[{"Address":"1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy","CoinType":"BTC","Balance":0.00108832,"Description":"","WhiteListed":false,"ColdStorage":false,"SupportedExchanges":""}],"providers":[{"name":"Ethplorer","enabled":true},{"name":"XRPScan","enabled":true},{"name":"CryptoID","enabled":false,"apiKey":"Key"}]}} {"portfolioAddresses":{"addresses":[{"Address":"1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy","CoinType":"BTC","Balance":0.00108832,"Description":"","WhiteListed":false,"ColdStorage":false,"SupportedExchanges":""}],"providers":[{"name":"Ethplorer","enabled":true},{"name":"XRPScan","enabled":true},{"name":"CryptoID","enabled":false,"apiKey":"Key"}]}}
`) `)
r, err := new(Version6).DowngradeConfig(context.Background(), in) r, err := new(v6.Version).DowngradeConfig(context.Background(), in)
require.NoError(t, err, "DowngradeConfig must not error") require.NoError(t, err, "DowngradeConfig must not error")
_, _, _, err = jsonparser.Get(r, "portfolioAddresses", "providers") //nolint:dogsled // Return values not needed _, _, _, err = jsonparser.Get(r, "portfolioAddresses", "providers") //nolint:dogsled // Return values not needed
assert.ErrorIs(t, err, jsonparser.KeyPathNotFoundError, "providers should be removed") assert.ErrorIs(t, err, jsonparser.KeyPathNotFoundError, "providers should be removed")

View File

@@ -32,6 +32,7 @@ const UseLatestVersion = math.MaxUint16
var ( var (
errVersionIncompatible = errors.New("version does not implement ConfigVersion or ExchangeVersion") errVersionIncompatible = errors.New("version does not implement ConfigVersion or ExchangeVersion")
errAlreadyRegistered = errors.New("version is already registered")
errModifyingExchange = errors.New("error modifying exchange config") errModifyingExchange = errors.New("error modifying exchange config")
errNoVersions = errors.New("error retrieving latest config version: No config versions are registered") errNoVersions = errors.New("error retrieving latest config version: No config versions are registered")
errApplyingVersion = errors.New("error applying version") errApplyingVersion = errors.New("error applying version")
@@ -40,7 +41,6 @@ var (
errConfigVersionUnavail = errors.New("version is higher than the latest available version") errConfigVersionUnavail = errors.New("version is higher than the latest available version")
errConfigVersionNegative = errors.New("version is negative") errConfigVersionNegative = errors.New("version is negative")
errConfigVersionMax = errors.New("version is above max versions") errConfigVersionMax = errors.New("version is above max versions")
errUpgrading = errors.New("error upgrading")
) )
// ConfigVersion is a version that affects the general configuration // ConfigVersion is a version that affects the general configuration
@@ -211,15 +211,28 @@ func exchangeDeploy(ctx context.Context, patch ExchangeVersion, method func(Exch
} }
// registerVersion takes instances of config versions and adds them to the registry // registerVersion takes instances of config versions and adds them to the registry
func (m *manager) registerVersion(ver int, v any) { func (m *manager) registerVersion(ver uint16, v any) {
m.m.Lock() m.m.Lock()
defer m.m.Unlock() defer m.m.Unlock()
if ver >= len(m.versions) { if int(ver) >= len(m.versions) {
m.versions = slices.Grow(m.versions, ver+1)[:ver+1] m.versions = slices.Grow(m.versions, int(ver+1))[:ver+1]
}
if m.versions[ver] != nil {
panic(fmt.Errorf("%w: %d", errAlreadyRegistered, ver))
} }
m.versions[ver] = v m.versions[ver] = v
} }
// Version returns a version registered by init or nil if nothing has been registered with that version number
func (m *manager) Version(version uint16) any {
m.m.RLock()
defer m.m.RUnlock()
if int(version) < len(m.versions) {
return m.versions[version]
}
return nil
}
// latest returns the highest version number // latest returns the highest version number
func (m *manager) latest() (uint16, error) { func (m *manager) latest() (uint16, error) {
m.m.RLock() m.m.RLock()

View File

@@ -2,12 +2,17 @@ package versions
import ( import (
"context" "context"
"fmt"
"math"
"testing" "testing"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common"
v0 "github.com/thrasher-corp/gocryptotrader/config/versions/v0"
v1 "github.com/thrasher-corp/gocryptotrader/config/versions/v1"
v2 "github.com/thrasher-corp/gocryptotrader/config/versions/v2"
) )
func TestDeploy(t *testing.T) { func TestDeploy(t *testing.T) {
@@ -22,8 +27,8 @@ func TestDeploy(t *testing.T) {
m = manager{} m = manager{}
m.registerVersion(0, &Version0{}) m.registerVersion(0, &v0.Version{})
m.registerVersion(1, &Version1{}) m.registerVersion(1, &v1.Version{})
_, err = m.Deploy(context.Background(), []byte(`not an object`), UseLatestVersion) _, err = m.Deploy(context.Background(), []byte(`not an object`), UseLatestVersion)
require.ErrorIs(t, err, jsonparser.KeyPathNotFoundError, "Must throw the correct error trying to add version to bad json") require.ErrorIs(t, err, jsonparser.KeyPathNotFoundError, "Must throw the correct error trying to add version to bad json")
require.ErrorIs(t, err, common.ErrSettingField, "Must throw the correct error trying to add version to bad json") require.ErrorIs(t, err, common.ErrSettingField, "Must throw the correct error trying to add version to bad json")
@@ -95,7 +100,7 @@ func TestRegisterVersion(t *testing.T) {
t.Parallel() t.Parallel()
m := manager{} m := manager{}
m.registerVersion(0, &Version0{}) m.registerVersion(0, &v0.Version{})
assert.NotEmpty(t, m.versions) assert.NotEmpty(t, m.versions)
m.registerVersion(2, &TestVersion2{}) m.registerVersion(2, &TestVersion2{})
@@ -106,6 +111,10 @@ func TestRegisterVersion(t *testing.T) {
m.registerVersion(1, &TestVersion1{}) m.registerVersion(1, &TestVersion1{})
require.Equal(t, 3, len(m.versions), "Must leave len alone when registering out-of-sequence") require.Equal(t, 3, len(m.versions), "Must leave len alone when registering out-of-sequence")
require.NotNil(t, m.versions[1], "Must put Version 1 in the correct slot") require.NotNil(t, m.versions[1], "Must put Version 1 in the correct slot")
assert.PanicsWithError(t, fmt.Sprintf("%s: %d", errAlreadyRegistered, 2), func() {
m.registerVersion(2, &TestVersion2{})
}, "registeringVersion must panic registering an existing version")
} }
func TestLatest(t *testing.T) { func TestLatest(t *testing.T) {
@@ -114,14 +123,25 @@ func TestLatest(t *testing.T) {
_, err := m.latest() _, err := m.latest()
require.ErrorIs(t, err, errNoVersions) require.ErrorIs(t, err, errNoVersions)
m.registerVersion(0, &Version0{}) m.registerVersion(0, &v0.Version{})
m.registerVersion(1, &Version1{}) m.registerVersion(1, &v1.Version{})
v, err := m.latest() v, err := m.latest()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint16(1), v) assert.Equal(t, uint16(1), v)
m.registerVersion(2, &Version2{}) m.registerVersion(2, &v2.Version{})
v, err = m.latest() v, err = m.latest()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint16(2), v) assert.Equal(t, uint16(2), v)
} }
func TestVersion(t *testing.T) {
t.Parallel()
m := manager{}
m.registerVersion(0, &v0.Version{})
l, err := m.latest()
require.NoError(t, err, "latest must not error")
assert.Nil(t, m.Version(l-1))
assert.NotNil(t, m.Version(l))
assert.Nil(t, m.Version(math.MaxUint16))
}