432 lines
11 KiB
Go
432 lines
11 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)
|
|
|
|
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.EqualValues(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()
|
|
}
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
// Try to read an invalid version
|
|
view, err = db.ReaderAt(versions.Last() + 1)
|
|
require.Equal(t, dbm.ErrVersionDoesNotExist, err)
|
|
|
|
require.NoError(t, db.DeleteVersion(v2))
|
|
// Try to read a deleted version
|
|
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
|
|
}
|
|
|
|
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()
|
|
defer view.Discard()
|
|
tx := getWriter()
|
|
defer tx.Discard()
|
|
require.NoError(t, tx.Set([]byte("0"), []byte("a")))
|
|
v, err := view.Get([]byte("0"))
|
|
require.NoError(t, err)
|
|
require.Nil(t, v)
|
|
})
|
|
|
|
// Try to commit version with open txns
|
|
t.Run("open transactions", func(t *testing.T) {
|
|
t.Helper()
|
|
tx := getWriter()
|
|
tx.Set([]byte("2"), []byte("a"))
|
|
_, err := db.SaveNextVersion()
|
|
require.Equal(t, dbm.ErrOpenTransactions, err)
|
|
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()
|
|
defer view.Discard()
|
|
v, err := view.Get(ikey(0))
|
|
require.NoError(t, err)
|
|
require.Equal(t, ival(0), v)
|
|
})
|
|
|
|
}
|
|
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)
|
|
|
|
txn := db.Writer()
|
|
for i := 0; i < 100; i++ {
|
|
require.NoError(t, txn.Set(ikey(i), ival(i)))
|
|
}
|
|
require.NoError(t, txn.Commit())
|
|
first, err := db.SaveNextVersion()
|
|
require.NoError(t, err)
|
|
|
|
txn = db.Writer()
|
|
for i := 0; i < 50; 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()
|
|
for i := 100; i < 150; i++ {
|
|
require.NoError(t, txn.Set(ikey(i), ival(i)))
|
|
}
|
|
require.NoError(t, txn.Commit())
|
|
db.Close()
|
|
|
|
// Reload and check each saved version
|
|
db = load(t, dirname)
|
|
|
|
view, err := db.ReaderAt(first)
|
|
require.NoError(t, err)
|
|
for i := 0; i < 100; i++ {
|
|
v, err := view.Get(ikey(i))
|
|
require.NoError(t, err)
|
|
require.Equal(t, ival(i), v)
|
|
}
|
|
view.Discard()
|
|
|
|
view, err = db.ReaderAt(last)
|
|
require.NoError(t, err)
|
|
for i := 0; i < 50; i++ {
|
|
v, err := view.Get(ikey(i))
|
|
require.NoError(t, err)
|
|
require.Equal(t, ival(i*10), v)
|
|
}
|
|
for i := 50; i < 100; i++ {
|
|
v, err := view.Get(ikey(i))
|
|
require.NoError(t, err)
|
|
require.Equal(t, ival(i), v)
|
|
}
|
|
view.Discard()
|
|
|
|
// Load working version
|
|
view = db.Reader()
|
|
for i := 100; i < 150; i++ {
|
|
v, err := view.Get(ikey(i))
|
|
require.NoError(t, err)
|
|
require.Equal(t, ival(i), v)
|
|
}
|
|
view.Discard()
|
|
|
|
require.NoError(t, db.Close())
|
|
}
|