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()) }