mirror of
https://github.com/status-im/status-go.git
synced 2025-01-21 12:11:44 +00:00
2df9df10ab
interface for initializing db, which is implemented for appdatabase and walletdatabase. TBD for multiaccounts DB. Unified DB initializion for all tests using helpers and new interface. Reduced sqlcipher kdf iterations for all tests to 1.
725 lines
26 KiB
Go
725 lines
26 KiB
Go
package history
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math"
|
|
"math/big"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/status-im/status-go/t/helpers"
|
|
"github.com/status-im/status-go/walletdatabase"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupBalanceTest(t *testing.T) (*Balance, func()) {
|
|
db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{})
|
|
require.NoError(t, err)
|
|
return NewBalance(NewBalanceDB(db)), func() {
|
|
require.NoError(t, db.Close())
|
|
}
|
|
}
|
|
|
|
type requestedBlock struct {
|
|
time uint64
|
|
headerInfoRequests int
|
|
balanceRequests int
|
|
}
|
|
|
|
// chainClientTestSource is a test implementation of the DataSource interface
|
|
// It generates dummy consecutive blocks of data and stores them for validation
|
|
type chainClientTestSource struct {
|
|
t *testing.T
|
|
firstTimeRequest int64
|
|
requestedBlocks map[int64]*requestedBlock // map of block number to block data
|
|
lastBlockTimestamp int64
|
|
firstBlockTimestamp int64
|
|
headerByNumberFn func(ctx context.Context, number *big.Int) (*types.Header, error)
|
|
balanceAtFn func(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error)
|
|
mockTime int64
|
|
timeAtMock int64
|
|
}
|
|
|
|
const (
|
|
testTimeLayout = "2006-01-02 15:04:05 Z07:00"
|
|
testTime = "2022-12-15 12:01:10 +02:00"
|
|
oneYear = 365 * 24 * time.Hour
|
|
)
|
|
|
|
func getTestTime(t *testing.T) time.Time {
|
|
testTime, err := time.Parse(testTimeLayout, testTime)
|
|
require.NoError(t, err)
|
|
return testTime.UTC()
|
|
}
|
|
|
|
func newTestSource(t *testing.T, availableYears float64) *chainClientTestSource {
|
|
return newTestSourceWithCurrentTime(t, availableYears, getTestTime(t).Unix())
|
|
}
|
|
|
|
func newTestSourceWithCurrentTime(t *testing.T, availableYears float64, currentTime int64) *chainClientTestSource {
|
|
newInst := &chainClientTestSource{
|
|
t: t,
|
|
requestedBlocks: make(map[int64]*requestedBlock),
|
|
lastBlockTimestamp: currentTime,
|
|
firstBlockTimestamp: currentTime - int64(availableYears*oneYear.Seconds()),
|
|
mockTime: currentTime,
|
|
timeAtMock: time.Now().UTC().Unix(),
|
|
}
|
|
newInst.headerByNumberFn = newInst.HeaderByNumberMock
|
|
newInst.balanceAtFn = newInst.BalanceAtMock
|
|
return newInst
|
|
}
|
|
|
|
const (
|
|
averageBlockTimeSeconds = 12.1
|
|
)
|
|
|
|
func (src *chainClientTestSource) setCurrentTime(newTime int64) {
|
|
src.mockTime = newTime
|
|
src.lastBlockTimestamp = newTime
|
|
}
|
|
|
|
func (src *chainClientTestSource) resetStats() {
|
|
src.requestedBlocks = make(map[int64]*requestedBlock)
|
|
}
|
|
|
|
func (src *chainClientTestSource) availableYears() float64 {
|
|
return float64(src.TimeNow()-src.firstBlockTimestamp) / oneYear.Seconds()
|
|
}
|
|
|
|
func (src *chainClientTestSource) blocksCount() int64 {
|
|
return int64(math.Round(float64(src.TimeNow()-src.firstBlockTimestamp) / averageBlockTimeSeconds))
|
|
}
|
|
|
|
func (src *chainClientTestSource) blockNumberToTimestamp(number int64) int64 {
|
|
return src.firstBlockTimestamp + int64(float64(number)*averageBlockTimeSeconds)
|
|
}
|
|
|
|
func (src *chainClientTestSource) generateBlockInfo(blockNumber int64, time uint64) *types.Header {
|
|
return &types.Header{
|
|
Number: big.NewInt(blockNumber),
|
|
Time: time,
|
|
}
|
|
}
|
|
|
|
func (src *chainClientTestSource) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
|
|
return src.headerByNumberFn(ctx, number)
|
|
}
|
|
|
|
func (src *chainClientTestSource) HeaderByNumberMock(ctx context.Context, number *big.Int) (*types.Header, error) {
|
|
var blockNo int64
|
|
if number == nil {
|
|
// Last block was requested
|
|
blockNo = src.blocksCount()
|
|
} else if number.Cmp(big.NewInt(src.blocksCount())) > 0 {
|
|
return nil, ethereum.NotFound
|
|
} else {
|
|
require.Greater(src.t, number.Int64(), int64(0))
|
|
blockNo = number.Int64()
|
|
}
|
|
timestamp := src.blockNumberToTimestamp(blockNo)
|
|
|
|
if _, contains := src.requestedBlocks[blockNo]; contains {
|
|
src.requestedBlocks[blockNo].headerInfoRequests++
|
|
} else {
|
|
src.requestedBlocks[blockNo] = &requestedBlock{
|
|
time: uint64(timestamp),
|
|
headerInfoRequests: 1,
|
|
}
|
|
}
|
|
|
|
return src.generateBlockInfo(blockNo, uint64(timestamp)), nil
|
|
}
|
|
|
|
func (src *chainClientTestSource) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
|
return src.balanceAtFn(ctx, account, blockNumber)
|
|
}
|
|
|
|
func weiInEth() *big.Int {
|
|
res, _ := new(big.Int).SetString("1000000000000000000", 0)
|
|
return res
|
|
}
|
|
|
|
func (src *chainClientTestSource) BalanceAtMock(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
|
var blockNo int64
|
|
if blockNumber == nil {
|
|
// Last block was requested
|
|
blockNo = src.blocksCount()
|
|
} else if blockNumber.Cmp(big.NewInt(src.blocksCount())) > 0 {
|
|
return nil, ethereum.NotFound
|
|
} else {
|
|
require.Greater(src.t, blockNumber.Int64(), int64(0))
|
|
blockNo = blockNumber.Int64()
|
|
}
|
|
|
|
if _, contains := src.requestedBlocks[blockNo]; contains {
|
|
src.requestedBlocks[blockNo].balanceRequests++
|
|
} else {
|
|
src.requestedBlocks[blockNo] = &requestedBlock{
|
|
time: uint64(src.blockNumberToTimestamp(blockNo)),
|
|
balanceRequests: 1,
|
|
}
|
|
}
|
|
|
|
return new(big.Int).Mul(big.NewInt(blockNo), weiInEth()), nil
|
|
}
|
|
|
|
func (src *chainClientTestSource) ChainID() uint64 {
|
|
return 777
|
|
}
|
|
|
|
func (src *chainClientTestSource) Currency() string {
|
|
return "eth"
|
|
}
|
|
|
|
func (src *chainClientTestSource) TimeNow() int64 {
|
|
if src.firstTimeRequest == 0 {
|
|
src.firstTimeRequest = time.Now().UTC().Unix()
|
|
}
|
|
return src.mockTime + (time.Now().UTC().Unix() - src.firstTimeRequest)
|
|
}
|
|
|
|
// extractTestData returns reqBlkNos sorted in ascending order
|
|
func extractTestData(dataSource *chainClientTestSource) (reqBlkNos []int64, infoRequests map[int64]int, balanceRequests map[int64]int) {
|
|
reqBlkNos = make([]int64, 0, len(dataSource.requestedBlocks))
|
|
for blockNo := range dataSource.requestedBlocks {
|
|
reqBlkNos = append(reqBlkNos, blockNo)
|
|
}
|
|
sort.Slice(reqBlkNos, func(i, j int) bool {
|
|
return reqBlkNos[i] < reqBlkNos[j]
|
|
})
|
|
|
|
infoRequests = make(map[int64]int)
|
|
balanceRequests = make(map[int64]int, len(reqBlkNos))
|
|
for i := 0; i < len(reqBlkNos); i++ {
|
|
n := reqBlkNos[i]
|
|
rB := dataSource.requestedBlocks[n]
|
|
|
|
if rB.headerInfoRequests > 0 {
|
|
infoRequests[n] = rB.headerInfoRequests
|
|
}
|
|
if rB.balanceRequests > 0 {
|
|
balanceRequests[n] = rB.balanceRequests
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func minimumExpectedDataPoints(interval TimeInterval) int {
|
|
return int(math.Ceil(float64(timeIntervalDuration[interval]) / float64(timeIntervalToStrideDuration[interval])))
|
|
}
|
|
|
|
func getTimeError(dataSource *chainClientTestSource, data []*DataPoint, interval TimeInterval) int64 {
|
|
timeRange := int64(data[len(data)-1].Timestamp - data[0].Timestamp)
|
|
var expectedDuration int64
|
|
if interval != BalanceHistoryAllTime {
|
|
expectedDuration = int64(timeIntervalDuration[interval].Seconds())
|
|
} else {
|
|
expectedDuration = int64((time.Duration(dataSource.availableYears()) * oneYear).Seconds())
|
|
}
|
|
return timeRange - expectedDuration
|
|
}
|
|
|
|
func TestBalanceHistoryGetWithoutFetch(t *testing.T) {
|
|
bh, cleanDB := setupBalanceTest(t)
|
|
defer cleanDB()
|
|
|
|
dataSource := newTestSource(t, 20 /*years*/)
|
|
currentTimestamp := dataSource.TimeNow()
|
|
|
|
testData := []struct {
|
|
name string
|
|
interval TimeInterval
|
|
}{
|
|
{"Week", BalanceHistory7Days},
|
|
{"Month", BalanceHistory1Month},
|
|
{"HalfYear", BalanceHistory6Months},
|
|
{"Year", BalanceHistory1Year},
|
|
{"AllTime", BalanceHistoryAllTime},
|
|
}
|
|
for _, testInput := range testData {
|
|
t.Run(testInput.name, func(t *testing.T) {
|
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, testInput.interval)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, len(balanceData))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBalanceHistoryGetWithoutOverlappingFetch(t *testing.T) {
|
|
testData := []struct {
|
|
name string
|
|
interval TimeInterval
|
|
}{
|
|
{"Week", BalanceHistory7Days},
|
|
{"Month", BalanceHistory1Month},
|
|
{"HalfYear", BalanceHistory6Months},
|
|
{"Year", BalanceHistory1Year},
|
|
{"AllTime", BalanceHistoryAllTime},
|
|
}
|
|
for _, testInput := range testData {
|
|
t.Run(testInput.name, func(t *testing.T) {
|
|
bh, cleanDB := setupBalanceTest(t)
|
|
defer cleanDB()
|
|
|
|
dataSource := newTestSource(t, 20 /*years*/)
|
|
currentTimestamp := dataSource.TimeNow()
|
|
getUntilTimestamp := currentTimestamp - int64((400 /*days*/ * 24 * time.Hour).Seconds())
|
|
|
|
getInterval := testInput.interval + 3
|
|
if getInterval > BalanceHistoryAllTime {
|
|
getInterval = BalanceHistory7Days + BalanceHistoryAllTime - testInput.interval
|
|
}
|
|
err := bh.update(context.Background(), dataSource, common.Address{7}, testInput.interval)
|
|
require.NoError(t, err)
|
|
|
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, getUntilTimestamp, getInterval)
|
|
require.NoError(t, err)
|
|
if testInput.interval != BalanceHistoryAllTime {
|
|
require.Equal(t, 0, len(balanceData))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBalanceHistoryGetWithOverlappingFetch(t *testing.T) {
|
|
testData := []struct {
|
|
name string
|
|
interval TimeInterval
|
|
lessDaysToGet int
|
|
}{
|
|
{"Week", BalanceHistory7Days, 6},
|
|
{"Month", BalanceHistory1Month, 1},
|
|
{"HalfYear", BalanceHistory6Months, 8},
|
|
{"Year", BalanceHistory1Year, 16},
|
|
{"AllTime", BalanceHistoryAllTime, 130},
|
|
}
|
|
for _, testInput := range testData {
|
|
t.Run(testInput.name, func(t *testing.T) {
|
|
bh, cleanDB := setupBalanceTest(t)
|
|
defer cleanDB()
|
|
|
|
dataSource := newTestSource(t, 20 /*years*/)
|
|
currentTimestamp := dataSource.TimeNow()
|
|
olderUntilTimestamp := currentTimestamp - int64((time.Duration(testInput.lessDaysToGet) * 24 * time.Hour).Seconds())
|
|
|
|
err := bh.update(context.Background(), dataSource, common.Address{7}, testInput.interval)
|
|
require.NoError(t, err)
|
|
|
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, testInput.interval)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(balanceData), minimumExpectedDataPoints(testInput.interval))
|
|
|
|
olderBalanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, olderUntilTimestamp, testInput.interval)
|
|
require.NoError(t, err)
|
|
require.Less(t, len(olderBalanceData), len(balanceData))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBalanceHistoryFetchFirstTime(t *testing.T) {
|
|
testData := []struct {
|
|
name string
|
|
interval TimeInterval
|
|
}{
|
|
{"Week", BalanceHistory7Days},
|
|
{"Month", BalanceHistory1Month},
|
|
{"HalfYear", BalanceHistory6Months},
|
|
{"Year", BalanceHistory1Year},
|
|
{"AllTime", BalanceHistoryAllTime},
|
|
}
|
|
for _, testInput := range testData {
|
|
t.Run(testInput.name, func(t *testing.T) {
|
|
bh, cleanDB := setupBalanceTest(t)
|
|
defer cleanDB()
|
|
|
|
dataSource := newTestSource(t, 20 /*years*/)
|
|
currentTimestamp := dataSource.TimeNow()
|
|
|
|
err := bh.update(context.Background(), dataSource, common.Address{7}, testInput.interval)
|
|
require.NoError(t, err)
|
|
|
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, testInput.interval)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(balanceData), minimumExpectedDataPoints(testInput.interval))
|
|
|
|
reqBlkNos, headerInfos, balances := extractTestData(dataSource)
|
|
require.Equal(t, len(balanceData), len(balances))
|
|
|
|
// Ensure we don't request the same info twice
|
|
for block, count := range headerInfos {
|
|
require.Equal(t, 1, count, "block %d has one info request", block)
|
|
if balanceCount, contains := balances[block]; contains {
|
|
require.Equal(t, 1, balanceCount, "block %d has one balance request", block)
|
|
}
|
|
}
|
|
for block, count := range balances {
|
|
require.Equal(t, 1, count, "block %d has one request", block)
|
|
}
|
|
|
|
resIdx := 0
|
|
for i := 0; i < len(reqBlkNos); i++ {
|
|
n := reqBlkNos[i]
|
|
rB := dataSource.requestedBlocks[n]
|
|
|
|
if _, contains := balances[n]; contains {
|
|
require.Equal(t, rB.time, balanceData[resIdx].Timestamp)
|
|
if resIdx > 0 {
|
|
require.Greater(t, balanceData[resIdx].Timestamp, balanceData[resIdx-1].Timestamp, "result timestamps are in order")
|
|
}
|
|
resIdx++
|
|
}
|
|
}
|
|
|
|
errorFromIdeal := getTimeError(dataSource, balanceData, testInput.interval)
|
|
require.Less(t, math.Abs(float64(errorFromIdeal)), timeIntervalToStrideDuration[testInput.interval].Seconds(), "Duration error [%d s] is within 1 stride [%.f s] for interval [%#v]", errorFromIdeal, timeIntervalToStrideDuration[testInput.interval].Seconds(), testInput.interval)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBalanceHistoryFetchError(t *testing.T) {
|
|
bh, cleanDB := setupBalanceTest(t)
|
|
defer cleanDB()
|
|
|
|
dataSource := newTestSource(t, 20 /*years*/)
|
|
bkFn := dataSource.balanceAtFn
|
|
// Fail first request
|
|
dataSource.balanceAtFn = func(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
|
return nil, errors.New("test error")
|
|
}
|
|
currentTimestamp := dataSource.TimeNow()
|
|
err := bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Year)
|
|
require.Error(t, err, "Expect \"test error\"")
|
|
|
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, BalanceHistory1Year)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, len(balanceData))
|
|
|
|
_, headerInfos, balances := extractTestData(dataSource)
|
|
require.Equal(t, 0, len(balances))
|
|
require.Equal(t, 1, len(headerInfos))
|
|
|
|
dataSource.resetStats()
|
|
// Fail later
|
|
dataSource.balanceAtFn = func(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
|
if len(dataSource.requestedBlocks) == 15 {
|
|
return nil, errors.New("test error")
|
|
}
|
|
return dataSource.BalanceAtMock(ctx, account, blockNumber)
|
|
}
|
|
err = bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Year)
|
|
require.Error(t, err, "Expect \"test error\"")
|
|
|
|
balanceData, err = bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, BalanceHistory1Year)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 14, len(balanceData))
|
|
|
|
reqBlkNos, headerInfos, balances := extractTestData(dataSource)
|
|
// The request for block info is made before the balance request
|
|
require.Equal(t, 1, dataSource.requestedBlocks[reqBlkNos[0]].headerInfoRequests)
|
|
require.Equal(t, 0, dataSource.requestedBlocks[reqBlkNos[0]].balanceRequests)
|
|
require.Equal(t, 14, len(balances))
|
|
require.Equal(t, len(balances), len(headerInfos)-1)
|
|
|
|
dataSource.resetStats()
|
|
dataSource.balanceAtFn = bkFn
|
|
err = bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Year)
|
|
require.NoError(t, err)
|
|
|
|
balanceData, err = bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, BalanceHistory1Year)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(balanceData), minimumExpectedDataPoints(BalanceHistory1Year))
|
|
|
|
_, headerInfos, balances = extractTestData(dataSource)
|
|
// Account for cache hits
|
|
require.Equal(t, len(balanceData)-14, len(balances))
|
|
require.Equal(t, len(balances), len(headerInfos))
|
|
|
|
for i := 1; i < len(balanceData); i++ {
|
|
require.Greater(t, balanceData[i].Timestamp, balanceData[i-1].Timestamp, "result timestamps are in order")
|
|
}
|
|
|
|
errorFromIdeal := getTimeError(dataSource, balanceData, BalanceHistory1Year)
|
|
require.Less(t, math.Abs(float64(errorFromIdeal)), timeIntervalToStrideDuration[BalanceHistory1Year].Seconds(), "Duration error [%d s] is within 1 stride [%.f s] for interval [%#v]", errorFromIdeal, timeIntervalToStrideDuration[BalanceHistory1Year].Seconds(), BalanceHistory1Year)
|
|
}
|
|
|
|
func TestBalanceHistoryValidateBalanceValuesAndCacheHit(t *testing.T) {
|
|
bh, cleanDB := setupBalanceTest(t)
|
|
defer cleanDB()
|
|
|
|
dataSource := newTestSource(t, 20 /*years*/)
|
|
currentTimestamp := dataSource.TimeNow()
|
|
requestedBalance := make(map[int64]*big.Int)
|
|
dataSource.balanceAtFn = func(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
|
balance, err := dataSource.BalanceAtMock(ctx, account, blockNumber)
|
|
requestedBalance[blockNumber.Int64()] = new(big.Int).Set(balance)
|
|
return balance, err
|
|
}
|
|
|
|
testData := []struct {
|
|
name string
|
|
interval TimeInterval
|
|
}{
|
|
{"Week", BalanceHistory7Days},
|
|
{"Month", BalanceHistory1Month},
|
|
{"HalfYear", BalanceHistory6Months},
|
|
{"Year", BalanceHistory1Year},
|
|
{"AllTime", BalanceHistoryAllTime},
|
|
}
|
|
for _, testInput := range testData {
|
|
t.Run(testInput.name, func(t *testing.T) {
|
|
dataSource.resetStats()
|
|
err := bh.update(context.Background(), dataSource, common.Address{7}, testInput.interval)
|
|
require.NoError(t, err)
|
|
|
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, currentTimestamp, testInput.interval)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(balanceData), minimumExpectedDataPoints(testInput.interval))
|
|
|
|
reqBlkNos, headerInfos, _ := extractTestData(dataSource)
|
|
// Only first run is not affected by cache
|
|
if testInput.interval == BalanceHistory7Days {
|
|
require.Equal(t, len(balanceData), len(requestedBalance))
|
|
require.Equal(t, len(balanceData), len(headerInfos))
|
|
} else {
|
|
require.Greater(t, len(balanceData), len(requestedBalance))
|
|
require.Greater(t, len(balanceData), len(headerInfos))
|
|
}
|
|
|
|
resIdx := 0
|
|
// Check that balance values are the one requested
|
|
for i := 0; i < len(reqBlkNos); i++ {
|
|
n := reqBlkNos[i]
|
|
|
|
if value, contains := requestedBalance[n]; contains {
|
|
require.Equal(t, value.Cmp(balanceData[resIdx].Balance.ToInt()), 0)
|
|
resIdx++
|
|
}
|
|
blockHeaderRequestCount := dataSource.requestedBlocks[n].headerInfoRequests
|
|
require.Less(t, blockHeaderRequestCount, 2)
|
|
blockBalanceRequestCount := dataSource.requestedBlocks[n].balanceRequests
|
|
require.Less(t, blockBalanceRequestCount, 2)
|
|
}
|
|
|
|
// Check that balance values are in order
|
|
for i := 1; i < len(balanceData); i++ {
|
|
require.Greater(t, balanceData[i].Balance.ToInt().Cmp(balanceData[i-1].Balance.ToInt()), 0, "expected balanceData[%d] > balanceData[%d] for interval %d", i, i-1, testInput.interval)
|
|
}
|
|
requestedBalance = make(map[int64]*big.Int)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetBalanceHistoryUpdateLater(t *testing.T) {
|
|
bh, cleanDB := setupBalanceTest(t)
|
|
defer cleanDB()
|
|
|
|
currentTime := getTestTime(t)
|
|
initialTime := currentTime
|
|
moreThanADay := 24*time.Hour + 15*time.Minute
|
|
moreThanAMonth := 401 * moreThanADay
|
|
initialTime = initialTime.Add(-moreThanADay - moreThanAMonth)
|
|
dataSource := newTestSourceWithCurrentTime(t, 20 /*years*/, initialTime.Unix())
|
|
|
|
err := bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Month)
|
|
require.NoError(t, err)
|
|
|
|
prevBalanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, dataSource.TimeNow(), BalanceHistory1Month)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(prevBalanceData), minimumExpectedDataPoints(BalanceHistory1Month))
|
|
|
|
// Advance little bit more than a day
|
|
later := initialTime
|
|
later = later.Add(moreThanADay)
|
|
dataSource.setCurrentTime(later.Unix())
|
|
dataSource.resetStats()
|
|
|
|
err = bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Month)
|
|
require.NoError(t, err)
|
|
|
|
updatedBalanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, dataSource.TimeNow(), BalanceHistory1Month)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(updatedBalanceData), minimumExpectedDataPoints(BalanceHistory1Month))
|
|
|
|
reqBlkNos, blockInfos, balances := extractTestData(dataSource)
|
|
require.Equal(t, 2, len(reqBlkNos))
|
|
require.Equal(t, len(reqBlkNos), len(blockInfos))
|
|
require.Equal(t, len(blockInfos), len(balances))
|
|
|
|
for block, count := range balances {
|
|
require.Equal(t, 1, count, "block %d has one request", block)
|
|
}
|
|
|
|
resIdx := len(updatedBalanceData) - 2
|
|
for i := 0; i < len(reqBlkNos); i++ {
|
|
rB := dataSource.requestedBlocks[reqBlkNos[i]]
|
|
|
|
// Ensure block approximation error doesn't exceed 10 blocks
|
|
require.Equal(t, 0.0, math.Abs(float64(int64(rB.time)-int64(updatedBalanceData[resIdx].Timestamp))))
|
|
if resIdx > 0 {
|
|
// Ensure result timestamps are in order
|
|
require.Greater(t, updatedBalanceData[resIdx].Timestamp, updatedBalanceData[resIdx-1].Timestamp)
|
|
}
|
|
resIdx++
|
|
}
|
|
|
|
errorFromIdeal := getTimeError(dataSource, updatedBalanceData, BalanceHistory1Month)
|
|
require.Less(t, math.Abs(float64(errorFromIdeal)), timeIntervalToStrideDuration[BalanceHistory1Month].Seconds(), "Duration error [%d s] is within 1 stride [%.f s] for interval [%#v]", errorFromIdeal, timeIntervalToStrideDuration[BalanceHistory1Month].Seconds(), BalanceHistory1Month)
|
|
|
|
// Advance little bit more than a month
|
|
dataSource.setCurrentTime(currentTime.Unix())
|
|
dataSource.resetStats()
|
|
|
|
err = bh.update(context.Background(), dataSource, common.Address{7}, BalanceHistory1Month)
|
|
require.NoError(t, err)
|
|
|
|
newBalanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, dataSource.TimeNow(), BalanceHistory1Month)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(newBalanceData), minimumExpectedDataPoints(BalanceHistory1Month))
|
|
|
|
_, headerInfos, balances := extractTestData(dataSource)
|
|
require.Greater(t, len(balances), len(newBalanceData), "Expected more balance requests due to missing time catch up")
|
|
|
|
// Ensure we don't request the same info twice
|
|
for block, count := range headerInfos {
|
|
require.Equal(t, 1, count, "block %d has one info request", block)
|
|
if balanceCount, contains := balances[block]; contains {
|
|
require.Equal(t, 1, balanceCount, "block %d has one balance request", block)
|
|
}
|
|
}
|
|
for block, count := range balances {
|
|
require.Equal(t, 1, count, "block %d has one request", block)
|
|
}
|
|
|
|
for i := 1; i < len(newBalanceData); i++ {
|
|
require.Greater(t, newBalanceData[i].Timestamp, newBalanceData[i-1].Timestamp, "result timestamps are in order")
|
|
}
|
|
|
|
errorFromIdeal = getTimeError(dataSource, newBalanceData, BalanceHistory1Month)
|
|
require.Less(t, math.Abs(float64(errorFromIdeal)), timeIntervalToStrideDuration[BalanceHistory1Month].Seconds(), "Duration error [%d s] is within 1 stride [%.f s] for interval [%#v]", errorFromIdeal, timeIntervalToStrideDuration[BalanceHistory1Month].Seconds(), BalanceHistory1Month)
|
|
}
|
|
|
|
func TestGetBalanceHistoryFetchMultipleAccounts(t *testing.T) {
|
|
bh, cleanDB := setupBalanceTest(t)
|
|
defer cleanDB()
|
|
|
|
sevenDataSource := newTestSource(t, 5 /*years*/)
|
|
|
|
err := bh.update(context.Background(), sevenDataSource, common.Address{7}, BalanceHistory1Month)
|
|
require.NoError(t, err)
|
|
|
|
sevenBalanceData, err := bh.get(context.Background(), sevenDataSource.ChainID(), sevenDataSource.Currency(), common.Address{7}, sevenDataSource.TimeNow(), BalanceHistory1Month)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(sevenBalanceData), minimumExpectedDataPoints(BalanceHistory1Month))
|
|
|
|
_, sevenBlockInfos, _ := extractTestData(sevenDataSource)
|
|
require.Greater(t, len(sevenBlockInfos), 0)
|
|
|
|
nineDataSource := newTestSource(t, 5 /*years*/)
|
|
err = bh.update(context.Background(), nineDataSource, common.Address{9}, BalanceHistory1Month)
|
|
require.NoError(t, err)
|
|
|
|
nineBalanceData, err := bh.get(context.Background(), nineDataSource.ChainID(), nineDataSource.Currency(), common.Address{7}, nineDataSource.TimeNow(), BalanceHistory1Month)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(nineBalanceData), minimumExpectedDataPoints(BalanceHistory1Month))
|
|
|
|
_, nineBlockInfos, nineBalances := extractTestData(nineDataSource)
|
|
require.Equal(t, 0, len(nineBlockInfos))
|
|
require.Equal(t, len(nineBalanceData), len(nineBalances))
|
|
}
|
|
|
|
func TestGetBalanceHistoryUpdateCancellation(t *testing.T) {
|
|
bh, cleanDB := setupBalanceTest(t)
|
|
defer cleanDB()
|
|
|
|
dataSource := newTestSource(t, 5 /*years*/)
|
|
ctx, cancelFn := context.WithCancel(context.Background())
|
|
bkFn := dataSource.balanceAtFn
|
|
// Fail after 15 requests
|
|
dataSource.balanceAtFn = func(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
|
|
if len(dataSource.requestedBlocks) == 15 {
|
|
cancelFn()
|
|
}
|
|
return dataSource.BalanceAtMock(ctx, account, blockNumber)
|
|
}
|
|
err := bh.update(ctx, dataSource, common.Address{7}, BalanceHistory1Year)
|
|
require.Error(t, ctx.Err(), "Service canceled")
|
|
require.Error(t, err, "context cancelled")
|
|
|
|
balanceData, err := bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, dataSource.TimeNow(), BalanceHistory1Year)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 15, len(balanceData))
|
|
|
|
_, blockInfos, balances := extractTestData(dataSource)
|
|
// The request for block info is made before the balance fails
|
|
require.Equal(t, 15, len(balances))
|
|
require.Equal(t, 15, len(blockInfos))
|
|
|
|
dataSource.balanceAtFn = bkFn
|
|
ctx, cancelFn = context.WithCancel(context.Background())
|
|
|
|
err = bh.update(ctx, dataSource, common.Address{7}, BalanceHistory1Year)
|
|
require.NoError(t, ctx.Err())
|
|
require.NoError(t, err)
|
|
|
|
balanceData, err = bh.get(context.Background(), dataSource.ChainID(), dataSource.Currency(), common.Address{7}, dataSource.TimeNow(), BalanceHistory1Year)
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(balanceData), minimumExpectedDataPoints(BalanceHistory1Year))
|
|
cancelFn()
|
|
}
|
|
|
|
func TestBlockStrideHaveCommonDivisor(t *testing.T) {
|
|
values := make([]time.Duration, 0, len(timeIntervalToStrideDuration))
|
|
for _, blockDuration := range timeIntervalToStrideDuration {
|
|
values = append(values, blockDuration)
|
|
}
|
|
sort.Slice(values, func(i, j int) bool {
|
|
return values[i] < values[j]
|
|
})
|
|
for i := 1; i < len(values); i++ {
|
|
require.Equal(t, time.Duration(0), values[i]%values[i-1], " %d value from index %d is divisible with previous %d", values[i], i, values[i-1])
|
|
}
|
|
}
|
|
|
|
func TestBlockStrideMatchesBitsetFilter(t *testing.T) {
|
|
filterToStrideEquivalence := map[bitsetFilter]time.Duration{
|
|
filterAllTime: monthsStride,
|
|
filterWeekly: weekStride,
|
|
filterTwiceADay: twiceADayStride,
|
|
}
|
|
|
|
for interval, bitsetFiler := range timeIntervalToBitsetFilter {
|
|
stride, found := timeIntervalToStrideDuration[interval]
|
|
require.True(t, found)
|
|
require.Equal(t, stride, filterToStrideEquivalence[bitsetFiler])
|
|
}
|
|
}
|
|
|
|
func TestTimeIntervalToBitsetFilterAreConsecutiveFlags(t *testing.T) {
|
|
values := make([]int, 0, len(timeIntervalToBitsetFilter))
|
|
for i := BalanceHistoryAllTime; i >= BalanceHistory7Days; i-- {
|
|
values = append(values, int(timeIntervalToBitsetFilter[i]))
|
|
}
|
|
|
|
for i := 0; i < len(values); i++ {
|
|
// count number of bits set
|
|
count := 0
|
|
for j := 0; j <= 30; j++ {
|
|
if values[i]&(1<<j) != 0 {
|
|
count++
|
|
}
|
|
}
|
|
require.Equal(t, 1, count, "%b value from index %d has only one bit set", values[i], i)
|
|
|
|
if i > 0 {
|
|
require.GreaterOrEqual(t, values[i], values[i-1], "%b value from index %d is higher then previous %d", values[i], i, values[i-1])
|
|
}
|
|
}
|
|
}
|