package snapshots_test import ( "bytes" "errors" "io" "io/ioutil" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" db "github.com/tendermint/tm-db" "github.com/cosmos/cosmos-sdk/snapshots" "github.com/cosmos/cosmos-sdk/snapshots/types" "github.com/cosmos/cosmos-sdk/testutil" ) func setupStore(t *testing.T) *snapshots.Store { // ioutil.TempDir() is used instead of testing.T.TempDir() // see https://github.com/cosmos/cosmos-sdk/pull/8475 for // this change's rationale. tempdir, err := ioutil.TempDir("", "") require.NoError(t, err) t.Cleanup(func() { _ = os.RemoveAll(tempdir) }) store, err := snapshots.NewStore(db.NewMemDB(), tempdir) require.NoError(t, err) _, err = store.Save(1, 1, makeChunks([][]byte{ {1, 1, 0}, {1, 1, 1}, })) require.NoError(t, err) _, err = store.Save(2, 1, makeChunks([][]byte{ {2, 1, 0}, {2, 1, 1}, })) require.NoError(t, err) _, err = store.Save(2, 2, makeChunks([][]byte{ {2, 2, 0}, {2, 2, 1}, {2, 2, 2}, })) require.NoError(t, err) _, err = store.Save(3, 2, makeChunks([][]byte{ {3, 2, 0}, {3, 2, 1}, {3, 2, 2}, })) require.NoError(t, err) return store } func TestNewStore(t *testing.T) { tempdir := t.TempDir() _, err := snapshots.NewStore(db.NewMemDB(), tempdir) require.NoError(t, err) } func TestNewStore_ErrNoDir(t *testing.T) { _, err := snapshots.NewStore(db.NewMemDB(), "") require.Error(t, err) } func TestNewStore_ErrDirFailure(t *testing.T) { notADir := filepath.Join(testutil.TempFile(t).Name(), "subdir") _, err := snapshots.NewStore(db.NewMemDB(), notADir) require.Error(t, err) } func TestStore_Delete(t *testing.T) { store := setupStore(t) // Deleting a snapshot should remove it err := store.Delete(2, 2) require.NoError(t, err) snapshot, err := store.Get(2, 2) require.NoError(t, err) assert.Nil(t, snapshot) snapshots, err := store.List() require.NoError(t, err) assert.Len(t, snapshots, 3) // Deleting it again should not error err = store.Delete(2, 2) require.NoError(t, err) // Deleting a snapshot being saved should error ch := make(chan io.ReadCloser) go store.Save(9, 1, ch) time.Sleep(10 * time.Millisecond) err = store.Delete(9, 1) require.Error(t, err) // But after it's saved it should work close(ch) time.Sleep(10 * time.Millisecond) err = store.Delete(9, 1) require.NoError(t, err) } func TestStore_Get(t *testing.T) { store := setupStore(t) // Loading a missing snapshot should return nil snapshot, err := store.Get(9, 9) require.NoError(t, err) assert.Nil(t, snapshot) // Loading a snapshot should returns its metadata snapshot, err = store.Get(2, 1) require.NoError(t, err) assert.Equal(t, &types.Snapshot{ Height: 2, Format: 1, Chunks: 2, Hash: hash([][]byte{{2, 1, 0}, {2, 1, 1}}), Metadata: types.Metadata{ ChunkHashes: checksums([][]byte{ {2, 1, 0}, {2, 1, 1}}), }, }, snapshot) } func TestStore_GetLatest(t *testing.T) { store := setupStore(t) // Loading a missing snapshot should return nil snapshot, err := store.GetLatest() require.NoError(t, err) assert.Equal(t, &types.Snapshot{ Height: 3, Format: 2, Chunks: 3, Hash: hash([][]byte{ {3, 2, 0}, {3, 2, 1}, {3, 2, 2}, }), Metadata: types.Metadata{ ChunkHashes: checksums([][]byte{ {3, 2, 0}, {3, 2, 1}, {3, 2, 2}, }), }, }, snapshot) } func TestStore_List(t *testing.T) { store := setupStore(t) snapshots, err := store.List() require.NoError(t, err) require.Equal(t, []*types.Snapshot{ {Height: 3, Format: 2, Chunks: 3, Hash: hash([][]byte{{3, 2, 0}, {3, 2, 1}, {3, 2, 2}}), Metadata: types.Metadata{ChunkHashes: checksums([][]byte{{3, 2, 0}, {3, 2, 1}, {3, 2, 2}})}, }, {Height: 2, Format: 2, Chunks: 3, Hash: hash([][]byte{{2, 2, 0}, {2, 2, 1}, {2, 2, 2}}), Metadata: types.Metadata{ChunkHashes: checksums([][]byte{{2, 2, 0}, {2, 2, 1}, {2, 2, 2}})}, }, {Height: 2, Format: 1, Chunks: 2, Hash: hash([][]byte{{2, 1, 0}, {2, 1, 1}}), Metadata: types.Metadata{ChunkHashes: checksums([][]byte{{2, 1, 0}, {2, 1, 1}})}, }, {Height: 1, Format: 1, Chunks: 2, Hash: hash([][]byte{{1, 1, 0}, {1, 1, 1}}), Metadata: types.Metadata{ChunkHashes: checksums([][]byte{{1, 1, 0}, {1, 1, 1}})}, }, }, snapshots) } func TestStore_Load(t *testing.T) { store := setupStore(t) // Loading a missing snapshot should return nil snapshot, chunks, err := store.Load(9, 9) require.NoError(t, err) assert.Nil(t, snapshot) assert.Nil(t, chunks) // Loading a snapshot should returns its metadata and chunks snapshot, chunks, err = store.Load(2, 1) require.NoError(t, err) assert.Equal(t, &types.Snapshot{ Height: 2, Format: 1, Chunks: 2, Hash: hash([][]byte{{2, 1, 0}, {2, 1, 1}}), Metadata: types.Metadata{ ChunkHashes: checksums([][]byte{ {2, 1, 0}, {2, 1, 1}}), }, }, snapshot) for i := uint32(0); i < snapshot.Chunks; i++ { reader, ok := <-chunks require.True(t, ok) chunk, err := ioutil.ReadAll(reader) require.NoError(t, err) err = reader.Close() require.NoError(t, err) assert.Equal(t, []byte{2, 1, byte(i)}, chunk) } assert.Empty(t, chunks) } func TestStore_LoadChunk(t *testing.T) { store := setupStore(t) // Loading a missing snapshot should return nil chunk, err := store.LoadChunk(9, 9, 0) require.NoError(t, err) assert.Nil(t, chunk) // Loading a missing chunk index should return nil chunk, err = store.LoadChunk(2, 1, 2) require.NoError(t, err) require.Nil(t, chunk) // Loading a chunk should returns a content reader chunk, err = store.LoadChunk(2, 1, 0) require.NoError(t, err) require.NotNil(t, chunk) body, err := ioutil.ReadAll(chunk) require.NoError(t, err) assert.Equal(t, []byte{2, 1, 0}, body) err = chunk.Close() require.NoError(t, err) } func TestStore_Prune(t *testing.T) { store := setupStore(t) // Pruning too many snapshots should be fine pruned, err := store.Prune(4) require.NoError(t, err) assert.EqualValues(t, 0, pruned) snapshots, err := store.List() require.NoError(t, err) assert.Len(t, snapshots, 4) // Pruning until the last two heights should leave three snapshots (for two heights) pruned, err = store.Prune(2) require.NoError(t, err) assert.EqualValues(t, 1, pruned) snapshots, err = store.List() require.NoError(t, err) require.Equal(t, []*types.Snapshot{ {Height: 3, Format: 2, Chunks: 3, Hash: hash([][]byte{{3, 2, 0}, {3, 2, 1}, {3, 2, 2}}), Metadata: types.Metadata{ChunkHashes: checksums([][]byte{{3, 2, 0}, {3, 2, 1}, {3, 2, 2}})}, }, {Height: 2, Format: 2, Chunks: 3, Hash: hash([][]byte{{2, 2, 0}, {2, 2, 1}, {2, 2, 2}}), Metadata: types.Metadata{ChunkHashes: checksums([][]byte{{2, 2, 0}, {2, 2, 1}, {2, 2, 2}})}, }, {Height: 2, Format: 1, Chunks: 2, Hash: hash([][]byte{{2, 1, 0}, {2, 1, 1}}), Metadata: types.Metadata{ChunkHashes: checksums([][]byte{{2, 1, 0}, {2, 1, 1}})}, }, }, snapshots) // Pruning all heights should also be fine pruned, err = store.Prune(0) require.NoError(t, err) assert.EqualValues(t, 3, pruned) snapshots, err = store.List() require.NoError(t, err) assert.Empty(t, snapshots) } func TestStore_Save(t *testing.T) { store := setupStore(t) // Saving a snapshot should work snapshot, err := store.Save(4, 1, makeChunks([][]byte{{1}, {2}})) require.NoError(t, err) assert.Equal(t, &types.Snapshot{ Height: 4, Format: 1, Chunks: 2, Hash: hash([][]byte{{1}, {2}}), Metadata: types.Metadata{ ChunkHashes: checksums([][]byte{{1}, {2}}), }, }, snapshot) loaded, err := store.Get(snapshot.Height, snapshot.Format) require.NoError(t, err) assert.Equal(t, snapshot, loaded) // Saving an existing snapshot should error _, err = store.Save(4, 1, makeChunks([][]byte{{1}, {2}})) require.Error(t, err) // Saving at height 0 should error _, err = store.Save(0, 1, makeChunks([][]byte{{1}, {2}})) require.Error(t, err) // Saving at format 0 should be fine _, err = store.Save(1, 0, makeChunks([][]byte{{1}, {2}})) require.NoError(t, err) // Saving a snapshot with no chunks should be fine, as should loading it _, err = store.Save(5, 1, makeChunks([][]byte{})) require.NoError(t, err) snapshot, chunks, err := store.Load(5, 1) require.NoError(t, err) assert.Equal(t, &types.Snapshot{Height: 5, Format: 1, Hash: hash([][]byte{}), Metadata: types.Metadata{ChunkHashes: [][]byte{}}}, snapshot) assert.Empty(t, chunks) // Saving a snapshot should error if a chunk reader returns an error, and it should empty out // the channel someErr := errors.New("boom") pr, pw := io.Pipe() err = pw.CloseWithError(someErr) require.NoError(t, err) ch := make(chan io.ReadCloser, 2) ch <- pr ch <- ioutil.NopCloser(bytes.NewBuffer([]byte{0xff})) close(ch) _, err = store.Save(6, 1, ch) require.Error(t, err) require.True(t, errors.Is(err, someErr)) assert.Empty(t, ch) // Saving a snapshot should error if a snapshot is already in progress for the same height, // regardless of format. However, a different height should succeed. ch = make(chan io.ReadCloser) go store.Save(7, 1, ch) time.Sleep(10 * time.Millisecond) _, err = store.Save(7, 2, makeChunks(nil)) require.Error(t, err) _, err = store.Save(8, 1, makeChunks(nil)) require.NoError(t, err) close(ch) }