refactor!: abstractions for snapshot and pruning; snapshot intervals eventually pruned; unit tests (#11496)

This commit is contained in:
Roman
2022-04-21 15:30:36 -04:00
committed by GitHub
parent d4dd44469f
commit 42f8d45b68
57 changed files with 2643 additions and 638 deletions
+29
View File
@@ -0,0 +1,29 @@
# Pruning
## Overview
Pruning is the mechanism for deleting old application heights from the disk. Depending on the use case,
nodes may require different pruning strategies. For example, archive nodes must keep all
the states and prune nothing. On the other hand, a regular validator node may want to only keep 100 latest heights for performance reasons.
## Strategies
The strategies are configured in `app.toml`, with the format `pruning = "<strategy>"` where the options are:
- `default`: only the last 362,880 states(approximately 3.5 weeks worth of state) are kept; pruning at 10 block intervals
- `nothing`: all historic states will be saved, nothing will be deleted (i.e. archiving node)
- `everything`: 2 latest states will be kept; pruning at 10 block intervals.
- `custom`: allow pruning options to be manually specified through 'pruning-keep-recent', and 'pruning-interval'
If no strategy is given to the BaseApp, `nothing` is selected. However, we perform validation on the CLI layer to require these to be always set in the config file.
## Custom Pruning
These are applied if and only if the pruning strategy is custom:
- `pruning-keep-recent`: N means to keep all of the last N states
- `pruning-interval`: N means to delete old states from disk every Nth block.
## Relationship to State Sync Snapshots
Snapshot settings are optional. However, if set, they have an effect on how pruning is done by
persisting the heights that are multiples of `state-sync.snapshot-interval` until after the snapshot is complete. See the "Relationship to Pruning" section in `snapshots/README.md` for more details.
+11
View File
@@ -0,0 +1,11 @@
package pruning
var (
PruneHeightsKey = pruneHeightsKey
PruneSnapshotHeightsKey = pruneSnapshotHeightsKey
Int64SliceToBytes = int64SliceToBytes
ListToBytes = listToBytes
LoadPruningHeights = loadPruningHeights
LoadPruningSnapshotHeights = loadPruningSnapshotHeights
)
+282
View File
@@ -0,0 +1,282 @@
package pruning
import (
"container/list"
"encoding/binary"
"fmt"
"sync"
"github.com/tendermint/tendermint/libs/log"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/pruning/types"
)
// Manager is an abstraction to handle the logic needed for
// determinging when to prune old heights of the store
// based on the strategy described by the pruning options.
type Manager struct {
db dbm.DB
logger log.Logger
opts types.PruningOptions
snapshotInterval uint64
// Although pruneHeights happen in the same goroutine with the normal execution,
// we sync access to them to avoid soundness issues in the future if concurrency pattern changes.
pruneHeightsMx sync.Mutex
pruneHeights []int64
// Snapshots are taken in a separate goroutine from the regular execution
// and can be delivered asynchrounously via HandleHeightSnapshot.
// Therefore, we sync access to pruneSnapshotHeights with this mutex.
pruneSnapshotHeightsMx sync.Mutex
// These are the heights that are multiples of snapshotInterval and kept for state sync snapshots.
// The heights are added to this list to be pruned when a snapshot is complete.
pruneSnapshotHeights *list.List
}
// NegativeHeightsError is returned when a negative height is provided to the manager.
type NegativeHeightsError struct {
Height int64
}
var _ error = &NegativeHeightsError{}
func (e *NegativeHeightsError) Error() string {
return fmt.Sprintf("failed to get pruned heights: %d", e.Height)
}
var (
pruneHeightsKey = []byte("s/pruneheights")
pruneSnapshotHeightsKey = []byte("s/prunesnapshotheights")
)
// NewManager returns a new Manager with the given db and logger.
// The retuned manager uses a pruning strategy of "nothing" which
// keeps all heights. Users of the Manager may change the strategy
// by calling SetOptions.
func NewManager(db dbm.DB, logger log.Logger) *Manager {
return &Manager{
db: db,
logger: logger,
opts: types.NewPruningOptions(types.PruningNothing),
pruneHeights: []int64{},
pruneSnapshotHeights: list.New(),
}
}
// SetOptions sets the pruning strategy on the manager.
func (m *Manager) SetOptions(opts types.PruningOptions) {
m.opts = opts
}
// GetOptions fetches the pruning strategy from the manager.
func (m *Manager) GetOptions() types.PruningOptions {
return m.opts
}
// GetFlushAndResetPruningHeights returns all heights to be pruned during the next call to Prune().
// It also flushes and resets the pruning heights.
func (m *Manager) GetFlushAndResetPruningHeights() ([]int64, error) {
if m.opts.GetPruningStrategy() == types.PruningNothing {
return []int64{}, nil
}
m.pruneHeightsMx.Lock()
defer m.pruneHeightsMx.Unlock()
// flush the updates to disk so that it is not lost if crash happens.
if err := m.db.SetSync(pruneHeightsKey, int64SliceToBytes(m.pruneHeights)); err != nil {
return nil, err
}
// Return a copy to prevent data races.
pruningHeights := make([]int64, len(m.pruneHeights))
copy(pruningHeights, m.pruneHeights)
m.pruneHeights = m.pruneHeights[:0]
return pruningHeights, nil
}
// HandleHeight determines if previousHeight height needs to be kept for pruning at the right interval prescribed by
// the pruning strategy. Returns previousHeight, if it was kept to be pruned at the next call to Prune(), 0 otherwise.
// previousHeight must be greater than 0 for the handling to take effect since valid heights start at 1 and 0 represents
// the latest height. The latest height cannot be pruned. As a result, if previousHeight is less than or equal to 0, 0 is returned.
func (m *Manager) HandleHeight(previousHeight int64) int64 {
if m.opts.GetPruningStrategy() == types.PruningNothing || previousHeight <= 0 {
return 0
}
defer func() {
m.pruneHeightsMx.Lock()
defer m.pruneHeightsMx.Unlock()
m.pruneSnapshotHeightsMx.Lock()
defer m.pruneSnapshotHeightsMx.Unlock()
// move persisted snapshot heights to pruneHeights which
// represent the heights to be pruned at the next pruning interval.
var next *list.Element
for e := m.pruneSnapshotHeights.Front(); e != nil; e = next {
snHeight := e.Value.(int64)
if snHeight < previousHeight-int64(m.opts.KeepRecent) {
m.pruneHeights = append(m.pruneHeights, snHeight)
// We must get next before removing to be able to continue iterating.
next = e.Next()
m.pruneSnapshotHeights.Remove(e)
} else {
next = e.Next()
}
}
// flush the updates to disk so that they are not lost if crash happens.
if err := m.db.SetSync(pruneHeightsKey, int64SliceToBytes(m.pruneHeights)); err != nil {
panic(err)
}
}()
if int64(m.opts.KeepRecent) < previousHeight {
pruneHeight := previousHeight - int64(m.opts.KeepRecent)
// We consider this height to be pruned iff:
//
// - snapshotInterval is zero as that means that all heights should be pruned.
// - snapshotInterval % (height - KeepRecent) != 0 as that means the height is not
// a 'snapshot' height.
if m.snapshotInterval == 0 || pruneHeight%int64(m.snapshotInterval) != 0 {
m.pruneHeightsMx.Lock()
defer m.pruneHeightsMx.Unlock()
m.pruneHeights = append(m.pruneHeights, pruneHeight)
return pruneHeight
}
}
return 0
}
// HandleHeightSnapshot persists the snapshot height to be pruned at the next appropriate
// height defined by the pruning strategy. Flushes the update to disk and panics if the flush fails
// The input height must be greater than 0 and pruning strategy any but pruning nothing.
// If one of these conditions is not met, this function does nothing.
func (m *Manager) HandleHeightSnapshot(height int64) {
if m.opts.GetPruningStrategy() == types.PruningNothing || height <= 0 {
return
}
m.pruneSnapshotHeightsMx.Lock()
defer m.pruneSnapshotHeightsMx.Unlock()
m.logger.Debug("HandleHeightSnapshot", "height", height)
m.pruneSnapshotHeights.PushBack(height)
// flush the updates to disk so that they are not lost if crash happens.
if err := m.db.SetSync(pruneSnapshotHeightsKey, listToBytes(m.pruneSnapshotHeights)); err != nil {
panic(err)
}
}
// SetSnapshotInterval sets the interval at which the snapshots are taken.
func (m *Manager) SetSnapshotInterval(snapshotInterval uint64) {
m.snapshotInterval = snapshotInterval
}
// ShouldPruneAtHeight return true if the given height should be pruned, false otherwise
func (m *Manager) ShouldPruneAtHeight(height int64) bool {
return m.opts.Interval > 0 && m.opts.GetPruningStrategy() != types.PruningNothing && height%int64(m.opts.Interval) == 0
}
// LoadPruningHeights loads the pruning heights from the database as a crash recovery.
func (m *Manager) LoadPruningHeights(db dbm.DB) error {
if m.opts.GetPruningStrategy() == types.PruningNothing {
return nil
}
loadedPruneHeights, err := loadPruningHeights(db)
if err != nil {
return err
}
if len(loadedPruneHeights) > 0 {
m.pruneHeightsMx.Lock()
defer m.pruneHeightsMx.Unlock()
m.pruneHeights = loadedPruneHeights
}
loadedPruneSnapshotHeights, err := loadPruningSnapshotHeights(db)
if err != nil {
return err
}
if loadedPruneSnapshotHeights.Len() > 0 {
m.pruneSnapshotHeightsMx.Lock()
defer m.pruneSnapshotHeightsMx.Unlock()
m.pruneSnapshotHeights = loadedPruneSnapshotHeights
}
return nil
}
func loadPruningHeights(db dbm.DB) ([]int64, error) {
bz, err := db.Get(pruneHeightsKey)
if err != nil {
return nil, fmt.Errorf("failed to get pruned heights: %w", err)
}
if len(bz) == 0 {
return []int64{}, nil
}
prunedHeights := make([]int64, len(bz)/8)
i, offset := 0, 0
for offset < len(bz) {
h := int64(binary.BigEndian.Uint64(bz[offset : offset+8]))
if h < 0 {
return []int64{}, &NegativeHeightsError{Height: h}
}
prunedHeights[i] = h
i++
offset += 8
}
return prunedHeights, nil
}
func loadPruningSnapshotHeights(db dbm.DB) (*list.List, error) {
bz, err := db.Get(pruneSnapshotHeightsKey)
if err != nil {
return nil, fmt.Errorf("failed to get post-snapshot pruned heights: %w", err)
}
pruneSnapshotHeights := list.New()
if len(bz) == 0 {
return pruneSnapshotHeights, nil
}
i, offset := 0, 0
for offset < len(bz) {
h := int64(binary.BigEndian.Uint64(bz[offset : offset+8]))
if h < 0 {
return nil, &NegativeHeightsError{Height: h}
}
pruneSnapshotHeights.PushBack(h)
i++
offset += 8
}
return pruneSnapshotHeights, nil
}
func int64SliceToBytes(slice []int64) []byte {
bz := make([]byte, 0, len(slice)*8)
for _, ph := range slice {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(ph))
bz = append(bz, buf...)
}
return bz
}
func listToBytes(list *list.List) []byte {
bz := make([]byte, 0, list.Len()*8)
for e := list.Front(); e != nil; e = e.Next() {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(e.Value.(int64)))
bz = append(bz, buf...)
}
return bz
}
+536
View File
@@ -0,0 +1,536 @@
package pruning_test
import (
"container/list"
"errors"
"fmt"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/libs/log"
db "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/pruning"
"github.com/cosmos/cosmos-sdk/pruning/mock"
"github.com/cosmos/cosmos-sdk/pruning/types"
)
const dbErr = "db error"
func TestNewManager(t *testing.T) {
manager := pruning.NewManager(db.NewMemDB(), log.NewNopLogger())
require.NotNil(t, manager)
heights, err := manager.GetFlushAndResetPruningHeights()
require.NoError(t, err)
require.NotNil(t, heights)
require.Equal(t, types.PruningNothing, manager.GetOptions().GetPruningStrategy())
}
func TestStrategies(t *testing.T) {
testcases := map[string]struct {
strategy types.PruningOptions
snapshotInterval uint64
strategyToAssert types.PruningStrategy
isValid bool
}{
"prune nothing - no snapshot": {
strategy: types.NewPruningOptions(types.PruningNothing),
strategyToAssert: types.PruningNothing,
},
"prune nothing - snapshot": {
strategy: types.NewPruningOptions(types.PruningNothing),
strategyToAssert: types.PruningNothing,
snapshotInterval: 100,
},
"prune default - no snapshot": {
strategy: types.NewPruningOptions(types.PruningDefault),
strategyToAssert: types.PruningDefault,
},
"prune default - snapshot": {
strategy: types.NewPruningOptions(types.PruningDefault),
strategyToAssert: types.PruningDefault,
snapshotInterval: 100,
},
"prune everything - no snapshot": {
strategy: types.NewPruningOptions(types.PruningEverything),
strategyToAssert: types.PruningEverything,
},
"prune everything - snapshot": {
strategy: types.NewPruningOptions(types.PruningEverything),
strategyToAssert: types.PruningEverything,
snapshotInterval: 100,
},
"custom 100-10-15": {
strategy: types.NewCustomPruningOptions(100, 15),
snapshotInterval: 10,
strategyToAssert: types.PruningCustom,
},
"custom 10-10-15": {
strategy: types.NewCustomPruningOptions(10, 15),
snapshotInterval: 10,
strategyToAssert: types.PruningCustom,
},
"custom 100-0-15": {
strategy: types.NewCustomPruningOptions(100, 15),
snapshotInterval: 0,
strategyToAssert: types.PruningCustom,
},
}
manager := pruning.NewManager(db.NewMemDB(), log.NewNopLogger())
require.NotNil(t, manager)
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
curStrategy := tc.strategy
manager.SetSnapshotInterval(tc.snapshotInterval)
pruneStrategy := curStrategy.GetPruningStrategy()
require.Equal(t, tc.strategyToAssert, pruneStrategy)
// Validate strategy parameters
switch pruneStrategy {
case types.PruningDefault:
require.Equal(t, uint64(362880), curStrategy.KeepRecent)
require.Equal(t, uint64(10), curStrategy.Interval)
case types.PruningNothing:
require.Equal(t, uint64(0), curStrategy.KeepRecent)
require.Equal(t, uint64(0), curStrategy.Interval)
case types.PruningEverything:
require.Equal(t, uint64(2), curStrategy.KeepRecent)
require.Equal(t, uint64(10), curStrategy.Interval)
default:
//
}
manager.SetOptions(curStrategy)
require.Equal(t, tc.strategy, manager.GetOptions())
curKeepRecent := curStrategy.KeepRecent
curInterval := curStrategy.Interval
for curHeight := int64(0); curHeight < 110000; curHeight++ {
handleHeightActual := manager.HandleHeight(curHeight)
shouldPruneAtHeightActual := manager.ShouldPruneAtHeight(curHeight)
curPruningHeihts, err := manager.GetFlushAndResetPruningHeights()
require.Nil(t, err)
curHeightStr := fmt.Sprintf("height: %d", curHeight)
switch curStrategy.GetPruningStrategy() {
case types.PruningNothing:
require.Equal(t, int64(0), handleHeightActual, curHeightStr)
require.False(t, shouldPruneAtHeightActual, curHeightStr)
heights, err := manager.GetFlushAndResetPruningHeights()
require.NoError(t, err)
require.Equal(t, 0, len(heights))
default:
if curHeight > int64(curKeepRecent) && (tc.snapshotInterval != 0 && (curHeight-int64(curKeepRecent))%int64(tc.snapshotInterval) != 0 || tc.snapshotInterval == 0) {
expectedHeight := curHeight - int64(curKeepRecent)
require.Equal(t, curHeight-int64(curKeepRecent), handleHeightActual, curHeightStr)
require.Contains(t, curPruningHeihts, expectedHeight, curHeightStr)
} else {
require.Equal(t, int64(0), handleHeightActual, curHeightStr)
heights, err := manager.GetFlushAndResetPruningHeights()
require.NoError(t, err)
require.Equal(t, 0, len(heights))
}
require.Equal(t, curHeight%int64(curInterval) == 0, shouldPruneAtHeightActual, curHeightStr)
}
heights, err := manager.GetFlushAndResetPruningHeights()
require.NoError(t, err)
require.Equal(t, 0, len(heights))
}
})
}
}
func TestHandleHeight_Inputs(t *testing.T) {
var keepRecent int64 = int64(types.NewPruningOptions(types.PruningEverything).KeepRecent)
testcases := map[string]struct {
height int64
expectedResult int64
strategy types.PruningStrategy
expectedHeights []int64
}{
"previousHeight is negative - prune everything - invalid previousHeight": {
-1,
0,
types.PruningEverything,
[]int64{},
},
"previousHeight is zero - prune everything - invalid previousHeight": {
0,
0,
types.PruningEverything,
[]int64{},
},
"previousHeight is positive but within keep recent- prune everything - not kept": {
keepRecent,
0,
types.PruningEverything,
[]int64{},
},
"previousHeight is positive and greater than keep recent - kept": {
keepRecent + 1,
keepRecent + 1 - keepRecent,
types.PruningEverything,
[]int64{keepRecent + 1 - keepRecent},
},
"pruning nothing, previousHeight is positive and greater than keep recent - not kept": {
keepRecent + 1,
0,
types.PruningNothing,
[]int64{},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
manager := pruning.NewManager(db.NewMemDB(), log.NewNopLogger())
require.NotNil(t, manager)
manager.SetOptions(types.NewPruningOptions(tc.strategy))
handleHeightActual := manager.HandleHeight(tc.height)
require.Equal(t, tc.expectedResult, handleHeightActual)
actualHeights, err := manager.GetFlushAndResetPruningHeights()
require.NoError(t, err)
require.Equal(t, len(tc.expectedHeights), len(actualHeights))
require.Equal(t, tc.expectedHeights, actualHeights)
})
}
}
func TestHandleHeight_FlushLoadFromDisk(t *testing.T) {
testcases := map[string]struct {
previousHeight int64
keepRecent uint64
snapshotInterval uint64
movedSnapshotHeights []int64
expectedHandleHeightResult int64
expectedLoadPruningHeightsResult error
expectedLoadedHeights []int64
}{
"simple flush occurs": {
previousHeight: 11,
keepRecent: 10,
snapshotInterval: 0,
movedSnapshotHeights: []int64{},
expectedHandleHeightResult: 11 - 10,
expectedLoadPruningHeightsResult: nil,
expectedLoadedHeights: []int64{11 - 10},
},
"previous height <= keep recent - no update and no flush": {
previousHeight: 9,
keepRecent: 10,
snapshotInterval: 0,
movedSnapshotHeights: []int64{},
expectedHandleHeightResult: 0,
expectedLoadPruningHeightsResult: nil,
expectedLoadedHeights: []int64{},
},
"previous height alligns with snapshot interval - no update and no flush": {
previousHeight: 12,
keepRecent: 10,
snapshotInterval: 2,
movedSnapshotHeights: []int64{},
expectedHandleHeightResult: 0,
expectedLoadPruningHeightsResult: nil,
expectedLoadedHeights: []int64{},
},
"previous height does not align with snapshot interval - flush": {
previousHeight: 12,
keepRecent: 10,
snapshotInterval: 3,
movedSnapshotHeights: []int64{},
expectedHandleHeightResult: 2,
expectedLoadPruningHeightsResult: nil,
expectedLoadedHeights: []int64{2},
},
"moved snapshot heights - flushed": {
previousHeight: 32,
keepRecent: 10,
snapshotInterval: 5,
movedSnapshotHeights: []int64{15, 20, 25},
expectedHandleHeightResult: 22,
expectedLoadPruningHeightsResult: nil,
expectedLoadedHeights: []int64{15, 20, 22},
},
"previous height alligns with snapshot interval - no update but flush snapshot heights": {
previousHeight: 30,
keepRecent: 10,
snapshotInterval: 5,
movedSnapshotHeights: []int64{15, 20, 25},
expectedHandleHeightResult: 0,
expectedLoadPruningHeightsResult: nil,
expectedLoadedHeights: []int64{15},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
// Setup
db := db.NewMemDB()
manager := pruning.NewManager(db, log.NewNopLogger())
require.NotNil(t, manager)
manager.SetSnapshotInterval(tc.snapshotInterval)
manager.SetOptions(types.NewCustomPruningOptions(uint64(tc.keepRecent), uint64(10)))
for _, snapshotHeight := range tc.movedSnapshotHeights {
manager.HandleHeightSnapshot(snapshotHeight)
}
// Test HandleHeight and flush
handleHeightActual := manager.HandleHeight(tc.previousHeight)
require.Equal(t, tc.expectedHandleHeightResult, handleHeightActual)
loadedPruneHeights, err := pruning.LoadPruningHeights(db)
require.NoError(t, err)
require.Equal(t, len(loadedPruneHeights), len(loadedPruneHeights))
// Test load back
err = manager.LoadPruningHeights(db)
require.NoError(t, err)
heights, err := manager.GetFlushAndResetPruningHeights()
require.NoError(t, err)
require.Equal(t, len(tc.expectedLoadedHeights), len(heights))
require.ElementsMatch(t, tc.expectedLoadedHeights, heights)
})
}
}
func TestHandleHeight_DbErr_Panic(t *testing.T) {
ctrl := gomock.NewController(t)
// Setup
dbMock := mock.NewMockDB(ctrl)
dbMock.EXPECT().SetSync(gomock.Any(), gomock.Any()).Return(errors.New(dbErr)).Times(1)
manager := pruning.NewManager(dbMock, log.NewNopLogger())
manager.SetOptions(types.NewPruningOptions(types.PruningEverything))
require.NotNil(t, manager)
defer func() {
if r := recover(); r == nil {
t.Fail()
}
}()
manager.HandleHeight(10)
}
func TestHandleHeightSnapshot_FlushLoadFromDisk(t *testing.T) {
loadedHeightsMirror := []int64{}
// Setup
db := db.NewMemDB()
manager := pruning.NewManager(db, log.NewNopLogger())
require.NotNil(t, manager)
manager.SetOptions(types.NewPruningOptions(types.PruningEverything))
for snapshotHeight := int64(-1); snapshotHeight < 100; snapshotHeight++ {
// Test flush
manager.HandleHeightSnapshot(snapshotHeight)
// Post test
if snapshotHeight > 0 {
loadedHeightsMirror = append(loadedHeightsMirror, snapshotHeight)
}
loadedSnapshotHeights, err := pruning.LoadPruningSnapshotHeights(db)
require.NoError(t, err)
require.Equal(t, len(loadedHeightsMirror), loadedSnapshotHeights.Len())
// Test load back
err = manager.LoadPruningHeights(db)
require.NoError(t, err)
loadedSnapshotHeights, err = pruning.LoadPruningSnapshotHeights(db)
require.NoError(t, err)
require.Equal(t, len(loadedHeightsMirror), loadedSnapshotHeights.Len())
}
}
func TestHandleHeightSnapshot_DbErr_Panic(t *testing.T) {
ctrl := gomock.NewController(t)
// Setup
dbMock := mock.NewMockDB(ctrl)
dbMock.EXPECT().SetSync(gomock.Any(), gomock.Any()).Return(errors.New(dbErr)).Times(1)
manager := pruning.NewManager(dbMock, log.NewNopLogger())
manager.SetOptions(types.NewPruningOptions(types.PruningEverything))
require.NotNil(t, manager)
defer func() {
if r := recover(); r == nil {
t.Fail()
}
}()
manager.HandleHeightSnapshot(10)
}
func TestFlushLoad(t *testing.T) {
db := db.NewMemDB()
manager := pruning.NewManager(db, log.NewNopLogger())
require.NotNil(t, manager)
curStrategy := types.NewCustomPruningOptions(100, 15)
snapshotInterval := uint64(10)
manager.SetSnapshotInterval(snapshotInterval)
manager.SetOptions(curStrategy)
require.Equal(t, curStrategy, manager.GetOptions())
keepRecent := curStrategy.KeepRecent
heightsToPruneMirror := make([]int64, 0)
for curHeight := int64(0); curHeight < 1000; curHeight++ {
handleHeightActual := manager.HandleHeight(curHeight)
curHeightStr := fmt.Sprintf("height: %d", curHeight)
if curHeight > int64(keepRecent) && (snapshotInterval != 0 && (curHeight-int64(keepRecent))%int64(snapshotInterval) != 0 || snapshotInterval == 0) {
expectedHandleHeight := curHeight - int64(keepRecent)
require.Equal(t, expectedHandleHeight, handleHeightActual, curHeightStr)
heightsToPruneMirror = append(heightsToPruneMirror, expectedHandleHeight)
} else {
require.Equal(t, int64(0), handleHeightActual, curHeightStr)
}
if manager.ShouldPruneAtHeight(curHeight) && curHeight > int64(keepRecent) {
actualHeights, err := manager.GetFlushAndResetPruningHeights()
require.NoError(t, err)
require.Equal(t, len(heightsToPruneMirror), len(actualHeights))
require.Equal(t, heightsToPruneMirror, actualHeights)
err = manager.LoadPruningHeights(db)
require.NoError(t, err)
actualHeights, err = manager.GetFlushAndResetPruningHeights()
require.NoError(t, err)
require.Equal(t, len(heightsToPruneMirror), len(actualHeights))
require.Equal(t, heightsToPruneMirror, actualHeights)
heightsToPruneMirror = make([]int64, 0)
}
}
}
func TestLoadPruningHeights(t *testing.T) {
var (
manager = pruning.NewManager(db.NewMemDB(), log.NewNopLogger())
err error
)
require.NotNil(t, manager)
// must not be PruningNothing
manager.SetOptions(types.NewPruningOptions(types.PruningDefault))
testcases := map[string]struct {
flushedPruningHeights []int64
getFlushedPruningSnapshotHeights func() *list.List
expectedResult error
}{
"negative pruningHeight - error": {
flushedPruningHeights: []int64{10, 0, -1},
expectedResult: &pruning.NegativeHeightsError{Height: -1},
},
"negative snapshotPruningHeight - error": {
getFlushedPruningSnapshotHeights: func() *list.List {
l := list.New()
l.PushBack(int64(5))
l.PushBack(int64(-2))
l.PushBack(int64(3))
return l
},
expectedResult: &pruning.NegativeHeightsError{Height: -2},
},
"both have negative - pruningHeight error": {
flushedPruningHeights: []int64{10, 0, -1},
getFlushedPruningSnapshotHeights: func() *list.List {
l := list.New()
l.PushBack(int64(5))
l.PushBack(int64(-2))
l.PushBack(int64(3))
return l
},
expectedResult: &pruning.NegativeHeightsError{Height: -1},
},
"both non-negative - success": {
flushedPruningHeights: []int64{10, 0, 3},
getFlushedPruningSnapshotHeights: func() *list.List {
l := list.New()
l.PushBack(int64(5))
l.PushBack(int64(0))
l.PushBack(int64(3))
return l
},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
db := db.NewMemDB()
if tc.flushedPruningHeights != nil {
err = db.Set(pruning.PruneHeightsKey, pruning.Int64SliceToBytes(tc.flushedPruningHeights))
require.NoError(t, err)
}
if tc.getFlushedPruningSnapshotHeights != nil {
err = db.Set(pruning.PruneSnapshotHeightsKey, pruning.ListToBytes(tc.getFlushedPruningSnapshotHeights()))
require.NoError(t, err)
}
err = manager.LoadPruningHeights(db)
require.Equal(t, tc.expectedResult, err)
})
}
}
func TestLoadPruningHeights_PruneNothing(t *testing.T) {
var manager = pruning.NewManager(db.NewMemDB(), log.NewNopLogger())
require.NotNil(t, manager)
manager.SetOptions(types.NewPruningOptions(types.PruningNothing))
require.Nil(t, manager.LoadPruningHeights(db.NewMemDB()))
}
func TestGetFlushAndResetPruningHeights_DbErr_Panic(t *testing.T) {
ctrl := gomock.NewController(t)
// Setup
dbMock := mock.NewMockDB(ctrl)
dbMock.EXPECT().SetSync(gomock.Any(), gomock.Any()).Return(errors.New(dbErr)).Times(1)
manager := pruning.NewManager(dbMock, log.NewNopLogger())
manager.SetOptions(types.NewPruningOptions(types.PruningEverything))
require.NotNil(t, manager)
heights, err := manager.GetFlushAndResetPruningHeights()
require.Error(t, err)
require.Nil(t, heights)
}
+420
View File
@@ -0,0 +1,420 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: /home/roman/projects/cosmos-sdk/vendor/github.com/tendermint/tm-db/types.go
// Package mock_db is a generated GoMock package.
package mock
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
db "github.com/tendermint/tm-db"
)
// MockDB is a mock of DB interface.
type MockDB struct {
ctrl *gomock.Controller
recorder *MockDBMockRecorder
}
// MockDBMockRecorder is the mock recorder for MockDB.
type MockDBMockRecorder struct {
mock *MockDB
}
// NewMockDB creates a new mock instance.
func NewMockDB(ctrl *gomock.Controller) *MockDB {
mock := &MockDB{ctrl: ctrl}
mock.recorder = &MockDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDB) EXPECT() *MockDBMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockDB) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockDBMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDB)(nil).Close))
}
// Delete mocks base method.
func (m *MockDB) Delete(arg0 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockDBMockRecorder) Delete(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockDB)(nil).Delete), arg0)
}
// DeleteSync mocks base method.
func (m *MockDB) DeleteSync(arg0 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteSync", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteSync indicates an expected call of DeleteSync.
func (mr *MockDBMockRecorder) DeleteSync(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSync", reflect.TypeOf((*MockDB)(nil).DeleteSync), arg0)
}
// Get mocks base method.
func (m *MockDB) Get(arg0 []byte) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockDBMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDB)(nil).Get), arg0)
}
// Has mocks base method.
func (m *MockDB) Has(key []byte) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Has", key)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Has indicates an expected call of Has.
func (mr *MockDBMockRecorder) Has(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Has", reflect.TypeOf((*MockDB)(nil).Has), key)
}
// Iterator mocks base method.
func (m *MockDB) Iterator(start, end []byte) (db.Iterator, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Iterator", start, end)
ret0, _ := ret[0].(db.Iterator)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Iterator indicates an expected call of Iterator.
func (mr *MockDBMockRecorder) Iterator(start, end interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Iterator", reflect.TypeOf((*MockDB)(nil).Iterator), start, end)
}
// NewBatch mocks base method.
func (m *MockDB) NewBatch() db.Batch {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatch")
ret0, _ := ret[0].(db.Batch)
return ret0
}
// NewBatch indicates an expected call of NewBatch.
func (mr *MockDBMockRecorder) NewBatch() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatch", reflect.TypeOf((*MockDB)(nil).NewBatch))
}
// Print mocks base method.
func (m *MockDB) Print() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Print")
ret0, _ := ret[0].(error)
return ret0
}
// Print indicates an expected call of Print.
func (mr *MockDBMockRecorder) Print() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Print", reflect.TypeOf((*MockDB)(nil).Print))
}
// ReverseIterator mocks base method.
func (m *MockDB) ReverseIterator(start, end []byte) (db.Iterator, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReverseIterator", start, end)
ret0, _ := ret[0].(db.Iterator)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReverseIterator indicates an expected call of ReverseIterator.
func (mr *MockDBMockRecorder) ReverseIterator(start, end interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReverseIterator", reflect.TypeOf((*MockDB)(nil).ReverseIterator), start, end)
}
// Set mocks base method.
func (m *MockDB) Set(arg0, arg1 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Set", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Set indicates an expected call of Set.
func (mr *MockDBMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockDB)(nil).Set), arg0, arg1)
}
// SetSync mocks base method.
func (m *MockDB) SetSync(arg0, arg1 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetSync", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// SetSync indicates an expected call of SetSync.
func (mr *MockDBMockRecorder) SetSync(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSync", reflect.TypeOf((*MockDB)(nil).SetSync), arg0, arg1)
}
// Stats mocks base method.
func (m *MockDB) Stats() map[string]string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stats")
ret0, _ := ret[0].(map[string]string)
return ret0
}
// Stats indicates an expected call of Stats.
func (mr *MockDBMockRecorder) Stats() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stats", reflect.TypeOf((*MockDB)(nil).Stats))
}
// MockBatch is a mock of Batch interface.
type MockBatch struct {
ctrl *gomock.Controller
recorder *MockBatchMockRecorder
}
// MockBatchMockRecorder is the mock recorder for MockBatch.
type MockBatchMockRecorder struct {
mock *MockBatch
}
// NewMockBatch creates a new mock instance.
func NewMockBatch(ctrl *gomock.Controller) *MockBatch {
mock := &MockBatch{ctrl: ctrl}
mock.recorder = &MockBatchMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockBatch) EXPECT() *MockBatchMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockBatch) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockBatchMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockBatch)(nil).Close))
}
// Delete mocks base method.
func (m *MockBatch) Delete(key []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", key)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockBatchMockRecorder) Delete(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockBatch)(nil).Delete), key)
}
// Set mocks base method.
func (m *MockBatch) Set(key, value []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Set", key, value)
ret0, _ := ret[0].(error)
return ret0
}
// Set indicates an expected call of Set.
func (mr *MockBatchMockRecorder) Set(key, value interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockBatch)(nil).Set), key, value)
}
// Write mocks base method.
func (m *MockBatch) Write() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Write")
ret0, _ := ret[0].(error)
return ret0
}
// Write indicates an expected call of Write.
func (mr *MockBatchMockRecorder) Write() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockBatch)(nil).Write))
}
// WriteSync mocks base method.
func (m *MockBatch) WriteSync() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WriteSync")
ret0, _ := ret[0].(error)
return ret0
}
// WriteSync indicates an expected call of WriteSync.
func (mr *MockBatchMockRecorder) WriteSync() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteSync", reflect.TypeOf((*MockBatch)(nil).WriteSync))
}
// MockIterator is a mock of Iterator interface.
type MockIterator struct {
ctrl *gomock.Controller
recorder *MockIteratorMockRecorder
}
// MockIteratorMockRecorder is the mock recorder for MockIterator.
type MockIteratorMockRecorder struct {
mock *MockIterator
}
// NewMockIterator creates a new mock instance.
func NewMockIterator(ctrl *gomock.Controller) *MockIterator {
mock := &MockIterator{ctrl: ctrl}
mock.recorder = &MockIteratorMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIterator) EXPECT() *MockIteratorMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockIterator) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockIteratorMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockIterator)(nil).Close))
}
// Domain mocks base method.
func (m *MockIterator) Domain() ([]byte, []byte) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Domain")
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].([]byte)
return ret0, ret1
}
// Domain indicates an expected call of Domain.
func (mr *MockIteratorMockRecorder) Domain() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Domain", reflect.TypeOf((*MockIterator)(nil).Domain))
}
// Error mocks base method.
func (m *MockIterator) Error() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Error")
ret0, _ := ret[0].(error)
return ret0
}
// Error indicates an expected call of Error.
func (mr *MockIteratorMockRecorder) Error() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockIterator)(nil).Error))
}
// Key mocks base method.
func (m *MockIterator) Key() []byte {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Key")
ret0, _ := ret[0].([]byte)
return ret0
}
// Key indicates an expected call of Key.
func (mr *MockIteratorMockRecorder) Key() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockIterator)(nil).Key))
}
// Next mocks base method.
func (m *MockIterator) Next() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Next")
}
// Next indicates an expected call of Next.
func (mr *MockIteratorMockRecorder) Next() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockIterator)(nil).Next))
}
// Valid mocks base method.
func (m *MockIterator) Valid() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Valid")
ret0, _ := ret[0].(bool)
return ret0
}
// Valid indicates an expected call of Valid.
func (mr *MockIteratorMockRecorder) Valid() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Valid", reflect.TypeOf((*MockIterator)(nil).Valid))
}
// Value mocks base method.
func (m *MockIterator) Value() []byte {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Value")
ret0, _ := ret[0].([]byte)
return ret0
}
// Value indicates an expected call of Value.
func (mr *MockIteratorMockRecorder) Value() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*MockIterator)(nil).Value))
}
+130
View File
@@ -0,0 +1,130 @@
package types
import (
"errors"
"fmt"
)
// PruningOptions defines the pruning strategy used when determining which
// heights are removed from disk when committing state.
type PruningOptions struct {
// KeepRecent defines how many recent heights to keep on disk.
KeepRecent uint64
// Interval defines when the pruned heights are removed from disk.
Interval uint64
// Strategy defines the kind of pruning strategy. See below for more information on each.
Strategy PruningStrategy
}
type PruningStrategy int
// Pruning option string constants
const (
PruningOptionDefault = "default"
PruningOptionEverything = "everything"
PruningOptionNothing = "nothing"
PruningOptionCustom = "custom"
)
const (
// PruningDefault defines a pruning strategy where the last 362880 heights are
// kept where to-be pruned heights are pruned at every 10th height.
// The last 362880 heights are kept(approximately 3.5 weeks worth of state) assuming the typical
// block time is 6s. If these values do not match the applications' requirements, use the "custom" option.
PruningDefault PruningStrategy = iota
// PruningEverything defines a pruning strategy where all committed heights are
// deleted, storing only the current height and last 2 states. To-be pruned heights are
// pruned at every 10th height.
PruningEverything
// PruningNothing defines a pruning strategy where all heights are kept on disk.
// This is the only stretegy where KeepEvery=1 is allowed with state-sync snapshots disabled.
PruningNothing
// PruningCustom defines a pruning strategy where the user specifies the pruning.
PruningCustom
// PruningUndefined defines an undefined pruning strategy. It is to be returned by stores that do not support pruning.
PruningUndefined
)
const (
pruneEverythingKeepRecent = 2
pruneEverythingInterval = 10
)
var (
ErrPruningIntervalZero = errors.New("'pruning-interval' must not be 0. If you want to disable pruning, select pruning = \"nothing\"")
ErrPruningIntervalTooSmall = fmt.Errorf("'pruning-interval' must not be less than %d. For the most aggressive pruning, select pruning = \"everything\"", pruneEverythingInterval)
ErrPruningKeepRecentTooSmall = fmt.Errorf("'pruning-keep-recent' must not be less than %d. For the most aggressive pruning, select pruning = \"everything\"", pruneEverythingKeepRecent)
)
func NewPruningOptions(pruningStrategy PruningStrategy) PruningOptions {
switch pruningStrategy {
case PruningDefault:
return PruningOptions{
KeepRecent: 362880,
Interval: 10,
Strategy: PruningDefault,
}
case PruningEverything:
return PruningOptions{
KeepRecent: pruneEverythingKeepRecent,
Interval: pruneEverythingInterval,
Strategy: PruningEverything,
}
case PruningNothing:
return PruningOptions{
KeepRecent: 0,
Interval: 0,
Strategy: PruningNothing,
}
default:
return PruningOptions{
Strategy: PruningCustom,
}
}
}
func NewCustomPruningOptions(keepRecent, interval uint64) PruningOptions {
return PruningOptions{
KeepRecent: keepRecent,
Interval: interval,
Strategy: PruningCustom,
}
}
func (po PruningOptions) GetPruningStrategy() PruningStrategy {
return po.Strategy
}
func (po PruningOptions) Validate() error {
if po.Strategy == PruningNothing {
return nil
}
if po.Interval == 0 {
return ErrPruningIntervalZero
}
if po.Interval < pruneEverythingInterval {
return ErrPruningIntervalTooSmall
}
if po.KeepRecent < pruneEverythingKeepRecent {
return ErrPruningKeepRecentTooSmall
}
return nil
}
func NewPruningOptionsFromString(strategy string) PruningOptions {
switch strategy {
case PruningOptionEverything:
return NewPruningOptions(PruningEverything)
case PruningOptionNothing:
return NewPruningOptions(PruningNothing)
case PruningOptionDefault:
return NewPruningOptions(PruningDefault)
default:
return NewPruningOptions(PruningDefault)
}
}
+65
View File
@@ -0,0 +1,65 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestPruningOptions_Validate(t *testing.T) {
testCases := []struct {
opts PruningOptions
expectErr error
}{
{NewPruningOptions(PruningDefault), nil},
{NewPruningOptions(PruningEverything), nil},
{NewPruningOptions(PruningNothing), nil},
{NewPruningOptions(PruningCustom), ErrPruningIntervalZero},
{NewCustomPruningOptions(2, 10), nil},
{NewCustomPruningOptions(100, 15), nil},
{NewCustomPruningOptions(1, 10), ErrPruningKeepRecentTooSmall},
{NewCustomPruningOptions(2, 9), ErrPruningIntervalTooSmall},
{NewCustomPruningOptions(2, 0), ErrPruningIntervalZero},
{NewCustomPruningOptions(2, 0), ErrPruningIntervalZero},
}
for _, tc := range testCases {
err := tc.opts.Validate()
require.Equal(t, tc.expectErr, err, "options: %v, err: %s", tc.opts, err)
}
}
func TestPruningOptions_GetStrategy(t *testing.T) {
testCases := []struct {
opts PruningOptions
expectedStrategy PruningStrategy
}{
{NewPruningOptions(PruningDefault), PruningDefault},
{NewPruningOptions(PruningEverything), PruningEverything},
{NewPruningOptions(PruningNothing), PruningNothing},
{NewPruningOptions(PruningCustom), PruningCustom},
{NewCustomPruningOptions(2, 10), PruningCustom},
}
for _, tc := range testCases {
actualStrategy := tc.opts.GetPruningStrategy()
require.Equal(t, tc.expectedStrategy, actualStrategy)
}
}
func TestNewPruningOptionsFromString(t *testing.T) {
testCases := []struct {
optString string
expect PruningOptions
}{
{PruningOptionDefault, NewPruningOptions(PruningDefault)},
{PruningOptionEverything, NewPruningOptions(PruningEverything)},
{PruningOptionNothing, NewPruningOptions(PruningNothing)},
{"invalid", NewPruningOptions(PruningDefault)},
}
for _, tc := range testCases {
actual := NewPruningOptionsFromString(tc.optString)
require.Equal(t, tc.expect, actual)
}
}