cosmos-sdk/db/dbtest/testcases.go

590 lines
15 KiB
Go

package dbtest
import (
"fmt"
"sort"
"sync"
"testing"
"github.com/stretchr/testify/require"
dbm "github.com/cosmos/cosmos-sdk/db"
)
type Loader func(*testing.T, string) dbm.DBConnection
func ikey(i int) []byte { return []byte(fmt.Sprintf("key-%03d", i)) }
func ival(i int) []byte { return []byte(fmt.Sprintf("val-%03d", i)) }
func DoTestGetSetHasDelete(t *testing.T, load Loader) {
t.Helper()
db := load(t, t.TempDir())
var txn dbm.DBReadWriter
var view dbm.DBReader
view = db.Reader()
require.NotNil(t, view)
// A nonexistent key should return nil.
value, err := view.Get([]byte("a"))
require.NoError(t, err)
require.Nil(t, value)
ok, err := view.Has([]byte("a"))
require.NoError(t, err)
require.False(t, ok)
txn = db.ReadWriter()
// Set and get a value.
err = txn.Set([]byte("a"), []byte{0x01})
require.NoError(t, err)
ok, err = txn.Has([]byte("a"))
require.NoError(t, err)
require.True(t, ok)
value, err = txn.Get([]byte("a"))
require.NoError(t, err)
require.Equal(t, []byte{0x01}, value)
// New value is not visible from another txn.
ok, err = view.Has([]byte("a"))
require.NoError(t, err)
require.False(t, ok)
// Deleting a non-existent value is fine.
err = txn.Delete([]byte("x"))
require.NoError(t, err)
// Delete a value.
err = txn.Delete([]byte("a"))
require.NoError(t, err)
value, err = txn.Get([]byte("a"))
require.NoError(t, err)
require.Nil(t, value)
err = txn.Set([]byte("b"), []byte{0x02})
require.NoError(t, err)
require.NoError(t, view.Discard())
require.NoError(t, txn.Commit())
txn = db.ReadWriter()
// Verify committed values.
value, err = txn.Get([]byte("b"))
require.NoError(t, err)
require.Equal(t, []byte{0x02}, value)
ok, err = txn.Has([]byte("a"))
require.NoError(t, err)
require.False(t, ok)
// Setting, getting, and deleting an empty key should error.
_, err = txn.Get([]byte{})
require.Equal(t, dbm.ErrKeyEmpty, err)
_, err = txn.Get(nil)
require.Equal(t, dbm.ErrKeyEmpty, err)
_, err = txn.Has([]byte{})
require.Equal(t, dbm.ErrKeyEmpty, err)
_, err = txn.Has(nil)
require.Equal(t, dbm.ErrKeyEmpty, err)
err = txn.Set([]byte{}, []byte{0x01})
require.Equal(t, dbm.ErrKeyEmpty, err)
err = txn.Set(nil, []byte{0x01})
require.Equal(t, dbm.ErrKeyEmpty, err)
err = txn.Delete([]byte{})
require.Equal(t, dbm.ErrKeyEmpty, err)
err = txn.Delete(nil)
require.Equal(t, dbm.ErrKeyEmpty, err)
// Setting a nil value should error, but an empty value is fine.
err = txn.Set([]byte("x"), nil)
require.Equal(t, dbm.ErrValueNil, err)
err = txn.Set([]byte("x"), []byte{})
require.NoError(t, err)
value, err = txn.Get([]byte("x"))
require.NoError(t, err)
require.Equal(t, []byte{}, value)
require.NoError(t, txn.Commit())
require.NoError(t, db.Close())
}
func DoTestIterators(t *testing.T, load Loader) {
t.Helper()
db := load(t, t.TempDir())
type entry struct {
key []byte
val string
}
entries := []entry{
{[]byte{0}, "0"},
{[]byte{0, 0}, "0 0"},
{[]byte{0, 1}, "0 1"},
{[]byte{0, 2}, "0 2"},
{[]byte{1}, "1"},
}
txn := db.ReadWriter()
for _, e := range entries {
require.NoError(t, txn.Set(e.key, []byte(e.val)))
}
require.NoError(t, txn.Commit())
testRange := func(t *testing.T, iter dbm.Iterator, expected []string) {
i := 0
for ; iter.Next(); i++ {
expectedValue := expected[i]
value := iter.Value()
require.Equal(t, expectedValue, string(value), "i=%v", i)
}
require.Equal(t, len(expected), i)
}
type testCase struct {
start, end []byte
expected []string
}
view := db.Reader()
iterCases := []testCase{
{nil, nil, []string{"0", "0 0", "0 1", "0 2", "1"}},
{[]byte{0x00}, nil, []string{"0", "0 0", "0 1", "0 2", "1"}},
{[]byte{0x00}, []byte{0x00, 0x01}, []string{"0", "0 0"}},
{[]byte{0x00}, []byte{0x01}, []string{"0", "0 0", "0 1", "0 2"}},
{[]byte{0x00, 0x01}, []byte{0x01}, []string{"0 1", "0 2"}},
{nil, []byte{0x01}, []string{"0", "0 0", "0 1", "0 2"}},
}
for i, tc := range iterCases {
t.Logf("Iterator case %d: [%v, %v)", i, tc.start, tc.end)
it, err := view.Iterator(tc.start, tc.end)
require.NoError(t, err)
testRange(t, it, tc.expected)
it.Close()
}
reverseCases := []testCase{
{nil, nil, []string{"1", "0 2", "0 1", "0 0", "0"}},
{[]byte{0x00}, nil, []string{"1", "0 2", "0 1", "0 0", "0"}},
{[]byte{0x00}, []byte{0x00, 0x01}, []string{"0 0", "0"}},
{[]byte{0x00}, []byte{0x01}, []string{"0 2", "0 1", "0 0", "0"}},
{[]byte{0x00, 0x01}, []byte{0x01}, []string{"0 2", "0 1"}},
{nil, []byte{0x01}, []string{"0 2", "0 1", "0 0", "0"}},
}
for i, tc := range reverseCases {
t.Logf("ReverseIterator case %d: [%v, %v)", i, tc.start, tc.end)
it, err := view.ReverseIterator(tc.start, tc.end)
require.NoError(t, err)
testRange(t, it, tc.expected)
it.Close()
}
require.NoError(t, view.Discard())
require.NoError(t, db.Close())
}
func DoTestVersioning(t *testing.T, load Loader) {
t.Helper()
db := load(t, t.TempDir())
view := db.Reader()
require.NotNil(t, view)
// Write, then read different versions
txn := db.ReadWriter()
require.NoError(t, txn.Set([]byte("0"), []byte("a")))
require.NoError(t, txn.Set([]byte("1"), []byte("b")))
require.NoError(t, txn.Commit())
v1, err := db.SaveNextVersion()
require.NoError(t, err)
txn = db.ReadWriter()
require.NoError(t, txn.Set([]byte("0"), []byte("c")))
require.NoError(t, txn.Delete([]byte("1")))
require.NoError(t, txn.Set([]byte("2"), []byte("c")))
require.NoError(t, txn.Commit())
v2, err := db.SaveNextVersion()
require.NoError(t, err)
// Skip to a future version
v3 := (v2 + 2)
require.NoError(t, db.SaveVersion(v3))
// Try to save to a past version
err = db.SaveVersion(v2)
require.Error(t, err)
// Verify existing versions
versions, err := db.Versions()
require.NoError(t, err)
require.Equal(t, 3, versions.Count())
var all []uint64
for it := versions.Iterator(); it.Next(); {
all = append(all, it.Value())
}
sort.Slice(all, func(i, j int) bool { return all[i] < all[j] })
require.Equal(t, []uint64{v1, v2, v3}, all)
require.Equal(t, v3, versions.Last())
view, err = db.ReaderAt(v1)
require.NoError(t, err)
require.NotNil(t, view)
val, err := view.Get([]byte("0"))
require.Equal(t, []byte("a"), val)
require.NoError(t, err)
val, err = view.Get([]byte("1"))
require.Equal(t, []byte("b"), val)
require.NoError(t, err)
has, err := view.Has([]byte("2"))
require.False(t, has)
require.NoError(t, view.Discard())
view, err = db.ReaderAt(v2)
require.NoError(t, err)
require.NotNil(t, view)
val, err = view.Get([]byte("0"))
require.Equal(t, []byte("c"), val)
require.NoError(t, err)
val, err = view.Get([]byte("2"))
require.Equal(t, []byte("c"), val)
require.NoError(t, err)
has, err = view.Has([]byte("1"))
require.False(t, has)
require.NoError(t, view.Discard())
view, err = db.ReaderAt(versions.Last() + 1)
require.Equal(t, dbm.ErrVersionDoesNotExist, err, "should fail to read a nonexistent version")
require.NoError(t, db.DeleteVersion(v2), "should delete version v2")
view, err = db.ReaderAt(v2)
require.Equal(t, dbm.ErrVersionDoesNotExist, err)
// Ensure latest version is accurate
prev := v3
for i := 0; i < 10; i++ {
w := db.Writer()
require.NoError(t, w.Set(ikey(i), ival(i)))
require.NoError(t, w.Commit())
ver, err := db.SaveNextVersion()
require.NoError(t, err)
require.Equal(t, prev+1, ver)
versions, err := db.Versions()
require.NoError(t, err)
require.Equal(t, ver, versions.Last())
prev = ver
}
// Open multiple readers for the same past version
view, err = db.ReaderAt(v3)
require.NoError(t, err)
view2, err := db.ReaderAt(v3)
require.NoError(t, err)
require.NoError(t, view.Discard())
require.NoError(t, view2.Discard())
require.NoError(t, db.Close())
}
func DoTestTransactions(t *testing.T, load Loader, multipleWriters bool) {
t.Helper()
db := load(t, t.TempDir())
// Both methods should work in a DBWriter context
writerFuncs := []func() dbm.DBWriter{
db.Writer,
func() dbm.DBWriter { return db.ReadWriter() },
}
for _, getWriter := range writerFuncs {
// Uncommitted records are not saved
t.Run("no commit", func(t *testing.T) {
t.Helper()
view := db.Reader()
tx := getWriter()
require.NoError(t, tx.Set([]byte("0"), []byte("a")))
v, err := view.Get([]byte("0"))
require.NoError(t, err)
require.Nil(t, v)
require.NoError(t, view.Discard())
require.NoError(t, tx.Discard())
})
// Try to commit version with open txns
t.Run("cannot save with open transactions", func(t *testing.T) {
t.Helper()
tx := getWriter()
require.NoError(t, tx.Set([]byte("0"), []byte("a")))
_, err := db.SaveNextVersion()
require.Equal(t, dbm.ErrOpenTransactions, err)
require.NoError(t, tx.Discard())
})
// Try to use a transaction after closing
t.Run("cannot reuse transaction", func(t *testing.T) {
t.Helper()
tx := getWriter()
require.NoError(t, tx.Commit())
require.Error(t, tx.Set([]byte("0"), []byte("a")))
require.NoError(t, tx.Discard()) // redundant discard is fine
tx = getWriter()
require.NoError(t, tx.Discard())
require.Error(t, tx.Set([]byte("0"), []byte("a")))
require.NoError(t, tx.Discard())
})
// Continue only if the backend supports multiple concurrent writers
if !multipleWriters {
continue
}
// Writing separately to same key causes a conflict
t.Run("write conflict", func(t *testing.T) {
t.Helper()
tx1 := getWriter()
tx2 := db.ReadWriter()
tx2.Get([]byte("1"))
require.NoError(t, tx1.Set([]byte("1"), []byte("b")))
require.NoError(t, tx2.Set([]byte("1"), []byte("c")))
require.NoError(t, tx1.Commit())
require.Error(t, tx2.Commit())
})
// Writing from concurrent txns
t.Run("concurrent transactions", func(t *testing.T) {
t.Helper()
var wg sync.WaitGroup
setkv := func(k, v []byte) {
defer wg.Done()
tx := getWriter()
require.NoError(t, tx.Set(k, v))
require.NoError(t, tx.Commit())
}
n := 10
wg.Add(n)
for i := 0; i < n; i++ {
go setkv(ikey(i), ival(i))
}
wg.Wait()
view := db.Reader()
v, err := view.Get(ikey(0))
require.NoError(t, err)
require.Equal(t, ival(0), v)
require.NoError(t, view.Discard())
})
}
// Try to reuse a reader txn
view := db.Reader()
require.NoError(t, view.Discard())
_, err := view.Get([]byte("0"))
require.Error(t, err)
require.NoError(t, view.Discard()) // redundant discard is fine
require.NoError(t, db.Close())
}
// Test that Revert works as intended, optionally closing and
// reloading the DB both before and after reverting
func DoTestRevert(t *testing.T, load Loader, reload bool) {
t.Helper()
dirname := t.TempDir()
db := load(t, dirname)
var txn dbm.DBWriter
initContents := func() {
txn = db.Writer()
require.NoError(t, txn.Set([]byte{2}, []byte{2}))
require.NoError(t, txn.Commit())
txn = db.Writer()
for i := byte(6); i < 10; i++ {
require.NoError(t, txn.Set([]byte{i}, []byte{i}))
}
require.NoError(t, txn.Delete([]byte{2}))
require.NoError(t, txn.Delete([]byte{3}))
require.NoError(t, txn.Commit())
}
initContents()
require.NoError(t, db.Revert())
view := db.Reader()
it, err := view.Iterator(nil, nil)
require.NoError(t, err)
require.False(t, it.Next()) // db is empty
require.NoError(t, it.Close())
require.NoError(t, view.Discard())
initContents()
_, err = db.SaveNextVersion()
require.NoError(t, err)
// get snapshot of db state
state := map[string][]byte{}
view = db.Reader()
it, err = view.Iterator(nil, nil)
require.NoError(t, err)
for it.Next() {
state[string(it.Key())] = it.Value()
}
require.NoError(t, it.Close())
view.Discard()
checkContents := func() {
view = db.Reader()
count := 0
it, err = view.Iterator(nil, nil)
require.NoError(t, err)
for it.Next() {
val, has := state[string(it.Key())]
require.True(t, has, "key should not be present: %v => %v", it.Key(), it.Value())
require.Equal(t, val, it.Value())
count++
}
require.NoError(t, it.Close())
require.Equal(t, len(state), count)
view.Discard()
}
changeContents := func() {
txn = db.Writer()
require.NoError(t, txn.Set([]byte{3}, []byte{15}))
require.NoError(t, txn.Set([]byte{7}, []byte{70}))
require.NoError(t, txn.Delete([]byte{8}))
require.NoError(t, txn.Delete([]byte{9}))
require.NoError(t, txn.Set([]byte{10}, []byte{0}))
require.NoError(t, txn.Commit())
txn = db.Writer()
require.NoError(t, txn.Set([]byte{3}, []byte{30}))
require.NoError(t, txn.Set([]byte{8}, []byte{8}))
require.NoError(t, txn.Delete([]byte{9}))
require.NoError(t, txn.Commit())
}
changeContents()
if reload {
db.Close()
db = load(t, dirname)
}
txn = db.Writer()
require.Error(t, db.Revert()) // can't revert with open writers
txn.Discard()
require.NoError(t, db.Revert())
if reload {
db.Close()
db = load(t, dirname)
}
checkContents()
// With intermediate versions added & deleted, revert again to v1
changeContents()
v2, _ := db.SaveNextVersion()
txn = db.Writer()
require.NoError(t, txn.Delete([]byte{6}))
require.NoError(t, txn.Set([]byte{8}, []byte{9}))
require.NoError(t, txn.Set([]byte{11}, []byte{11}))
txn.Commit()
v3, _ := db.SaveNextVersion()
txn = db.Writer()
require.NoError(t, txn.Set([]byte{12}, []byte{12}))
txn.Commit()
db.DeleteVersion(v2)
db.DeleteVersion(v3)
db.Revert()
checkContents()
require.NoError(t, db.Close())
}
// Tests reloading a saved DB from disk.
func DoTestReloadDB(t *testing.T, load Loader) {
t.Helper()
dirname := t.TempDir()
db := load(t, dirname)
var firstVersions []uint64
for i := 0; i < 10; i++ {
txn := db.Writer()
require.NoError(t, txn.Set(ikey(i), ival(i)))
require.NoError(t, txn.Commit())
ver, err := db.SaveNextVersion()
require.NoError(t, err)
firstVersions = append(firstVersions, ver)
}
txn := db.Writer()
for i := 0; i < 5; i++ { // overwrite some values
require.NoError(t, txn.Set(ikey(i), ival(i*10)))
}
require.NoError(t, txn.Commit())
last, err := db.SaveNextVersion()
require.NoError(t, err)
txn = db.Writer()
require.NoError(t, txn.Set([]byte("working-version"), ival(100)))
require.NoError(t, txn.Commit())
txn = db.Writer()
require.NoError(t, txn.Set([]byte("uncommitted"), ival(200)))
// Reload and check each saved version
db.Close()
db = load(t, dirname)
// require.True(t, db.Versions().Equal(versions))
vset, err := db.Versions()
require.NoError(t, err)
require.Equal(t, last, vset.Last())
for i := 0; i < 10; i++ {
view, err := db.ReaderAt(firstVersions[i])
require.NoError(t, err)
val, err := view.Get(ikey(i))
require.NoError(t, err)
require.Equal(t, ival(i), val)
require.NoError(t, view.Discard())
}
view, err := db.ReaderAt(last)
require.NoError(t, err)
for i := 0; i < 10; i++ {
v, err := view.Get(ikey(i))
require.NoError(t, err)
if i < 5 {
require.Equal(t, ival(i*10), v)
} else {
require.Equal(t, ival(i), v)
}
}
require.NoError(t, view.Discard())
// Load working version
view = db.Reader()
val, err := view.Get([]byte("working-version"))
require.NoError(t, err)
require.Equal(t, ival(100), val)
val, err = view.Get([]byte("uncommitted"))
require.NoError(t, err)
require.Nil(t, val)
require.NoError(t, view.Discard())
require.NoError(t, db.Close())
}