lbcwallet/walletdb/walletdbtest/interface.go

852 lines
22 KiB
Go
Raw Normal View History

// Copyright (c) 2014-2017 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package walletdbtest
import (
"bytes"
"fmt"
"reflect"
"sync"
"github.com/lbryio/lbcwallet/walletdb"
)
// errSubTestFail is used to signal that a sub test returned false.
var errSubTestFail = fmt.Errorf("sub test failure")
// testContext is used to store context information about a running test which
// is passed into helper functions.
type testContext struct {
t Tester
db walletdb.DB
bucketDepth int
isWritable bool
}
// rollbackValues returns a copy of the provided map with all values set to an
// empty string. This is used to test that values are properly rolled back.
func rollbackValues(values map[string]string) map[string]string {
retMap := make(map[string]string, len(values))
for k := range values {
retMap[k] = ""
}
return retMap
}
// testGetValues checks that all of the provided key/value pairs can be
// retrieved from the database and the retrieved values match the provided
// values.
func testGetValues(tc *testContext, bucket walletdb.ReadBucket, values map[string]string) bool {
for k, v := range values {
var vBytes []byte
if v != "" {
vBytes = []byte(v)
}
gotValue := bucket.Get([]byte(k))
if !reflect.DeepEqual(gotValue, vBytes) {
tc.t.Errorf("Get: unexpected value - got %s, want %s",
gotValue, vBytes)
return false
}
}
return true
}
// testPutValues stores all of the provided key/value pairs in the provided
// bucket while checking for errors.
func testPutValues(tc *testContext, bucket walletdb.ReadWriteBucket, values map[string]string) bool {
for k, v := range values {
var vBytes []byte
if v != "" {
vBytes = []byte(v)
}
if err := bucket.Put([]byte(k), vBytes); err != nil {
tc.t.Errorf("Put: unexpected error: %v", err)
return false
}
}
return true
}
// testDeleteValues removes all of the provided key/value pairs from the
// provided bucket.
func testDeleteValues(tc *testContext, bucket walletdb.ReadWriteBucket, values map[string]string) bool {
for k := range values {
if err := bucket.Delete([]byte(k)); err != nil {
tc.t.Errorf("Delete: unexpected error: %v", err)
return false
}
}
return true
}
// testNestedReadWriteBucket reruns the testBucketInterface against a nested bucket along
// with a counter to only test a couple of level deep.
func testNestedReadWriteBucket(tc *testContext, testBucket walletdb.ReadWriteBucket) bool {
// Don't go more than 2 nested level deep.
if tc.bucketDepth > 1 {
return true
}
tc.bucketDepth++
defer func() {
tc.bucketDepth--
}()
if !testReadWriteBucketInterface(tc, testBucket) {
return false
}
return true
}
// testSequence tests that the sequence related methods work as expected.
func testSequence(tc *testContext, testBucket walletdb.ReadWriteBucket) bool {
// Obtaining the current sequence twice should give us the same value.
seqNo1 := testBucket.Sequence()
seqNo2 := testBucket.Sequence()
if seqNo1 != seqNo2 {
tc.t.Errorf("Sequence: seq has incremented")
return false
}
// Incrementing to the next sequence should give us a value one larger
// than the prior number.
seqNo3, err := testBucket.NextSequence()
if err != nil {
tc.t.Errorf("Sequence: unexpected error: %v", err)
return false
}
if seqNo3 != seqNo2+1 {
tc.t.Errorf("Sequence: expected seq no of %v, instead got %v",
seqNo2+1, seqNo3)
return false
}
// We should be able to modify the sequence base number.
newBase := uint64(100)
if err := testBucket.SetSequence(newBase); err != nil {
tc.t.Errorf("Sequence: unexpected error: %v", err)
return false
}
// Any offset from this new sequence should now be properly reflected.
seqNo4, err := testBucket.NextSequence()
if err != nil {
tc.t.Errorf("Sequence: unexpected error: %v", err)
return false
}
if seqNo4 != newBase+1 {
tc.t.Errorf("Sequence: expected seq no of %v, instead got %v",
newBase+1, seqNo4)
return false
}
return true
}
// testReadWriteBucketInterface ensures the bucket interface is working properly by
// exercising all of its functions.
func testReadWriteBucketInterface(tc *testContext, bucket walletdb.ReadWriteBucket) bool {
// keyValues holds the keys and values to use when putting
// values into the bucket.
var keyValues = map[string]string{
"bucketkey1": "foo1",
"bucketkey2": "foo2",
"bucketkey3": "foo3",
}
if !testPutValues(tc, bucket, keyValues) {
return false
}
if !testGetValues(tc, bucket, keyValues) {
return false
}
// Iterate all of the keys using ForEach while making sure the
// stored values are the expected values.
keysFound := make(map[string]struct{}, len(keyValues))
err := bucket.ForEach(func(k, v []byte) error {
ks := string(k)
wantV, ok := keyValues[ks]
if !ok {
return fmt.Errorf("ForEach: key '%s' should "+
"exist", ks)
}
if !reflect.DeepEqual(v, []byte(wantV)) {
return fmt.Errorf("ForEach: value for key '%s' "+
"does not match - got %s, want %s",
ks, v, wantV)
}
keysFound[ks] = struct{}{}
return nil
})
if err != nil {
tc.t.Errorf("%v", err)
return false
}
// Ensure all keys were iterated.
for k := range keyValues {
if _, ok := keysFound[k]; !ok {
tc.t.Errorf("ForEach: key '%s' was not iterated "+
"when it should have been", k)
return false
}
}
// Delete the keys and ensure they were deleted.
if !testDeleteValues(tc, bucket, keyValues) {
return false
}
if !testGetValues(tc, bucket, rollbackValues(keyValues)) {
return false
}
// Test that the sequence methods work as expected.
if !testSequence(tc, bucket) {
return false
}
// Ensure creating a new bucket works as expected.
testBucketName := []byte("testbucket")
testBucket, err := bucket.CreateBucket(testBucketName)
if err != nil {
tc.t.Errorf("CreateBucket: unexpected error: %v", err)
return false
}
if !testNestedReadWriteBucket(tc, testBucket) {
return false
}
// Ensure creating a bucket that already exists fails with the
// expected error.
wantErr := walletdb.ErrBucketExists
if _, err := bucket.CreateBucket(testBucketName); err != wantErr {
tc.t.Errorf("CreateBucket: unexpected error - got %v, "+
"want %v", err, wantErr)
return false
}
// Ensure CreateBucketIfNotExists returns an existing bucket.
testBucket, err = bucket.CreateBucketIfNotExists(testBucketName)
if err != nil {
tc.t.Errorf("CreateBucketIfNotExists: unexpected "+
"error: %v", err)
return false
}
if !testNestedReadWriteBucket(tc, testBucket) {
return false
}
// Ensure retrieving and existing bucket works as expected.
testBucket = bucket.NestedReadWriteBucket(testBucketName)
if !testNestedReadWriteBucket(tc, testBucket) {
return false
}
// Ensure deleting a bucket works as intended.
if err := bucket.DeleteNestedBucket(testBucketName); err != nil {
tc.t.Errorf("DeleteNestedBucket: unexpected error: %v", err)
return false
}
if b := bucket.NestedReadWriteBucket(testBucketName); b != nil {
tc.t.Errorf("DeleteNestedBucket: bucket '%s' still exists",
testBucketName)
return false
}
// Ensure deleting a bucket that doesn't exist returns the
// expected error.
wantErr = walletdb.ErrBucketNotFound
if err := bucket.DeleteNestedBucket(testBucketName); err != wantErr {
tc.t.Errorf("DeleteNestedBucket: unexpected error - got %v, "+
"want %v", err, wantErr)
return false
}
// Ensure CreateBucketIfNotExists creates a new bucket when
// it doesn't already exist.
testBucket, err = bucket.CreateBucketIfNotExists(testBucketName)
if err != nil {
tc.t.Errorf("CreateBucketIfNotExists: unexpected "+
"error: %v", err)
return false
}
if !testNestedReadWriteBucket(tc, testBucket) {
return false
}
// Delete the test bucket to avoid leaving it around for future
// calls.
if err := bucket.DeleteNestedBucket(testBucketName); err != nil {
tc.t.Errorf("DeleteNestedBucket: unexpected error: %v", err)
return false
}
if b := bucket.NestedReadWriteBucket(testBucketName); b != nil {
tc.t.Errorf("DeleteNestedBucket: bucket '%s' still exists",
testBucketName)
return false
}
return true
}
// testManualTxInterface ensures that manual transactions work as expected.
func testManualTxInterface(tc *testContext, bucketKey []byte) bool {
db := tc.db
// populateValues tests that populating values works as expected.
//
// When the writable flag is false, a read-only tranasction is created,
// standard bucket tests for read-only transactions are performed, and
// the Commit function is checked to ensure it fails as expected.
//
// Otherwise, a read-write transaction is created, the values are
// written, standard bucket tests for read-write transactions are
// performed, and then the transaction is either commited or rolled
// back depending on the flag.
populateValues := func(writable, rollback bool, putValues map[string]string) bool {
var dbtx walletdb.ReadTx
var rootBucket walletdb.ReadBucket
var err error
if writable {
dbtx, err = db.BeginReadWriteTx()
if err != nil {
tc.t.Errorf("BeginReadWriteTx: unexpected error %v", err)
return false
}
rootBucket = dbtx.(walletdb.ReadWriteTx).ReadWriteBucket(bucketKey)
} else {
dbtx, err = db.BeginReadTx()
if err != nil {
tc.t.Errorf("BeginReadTx: unexpected error %v", err)
return false
}
rootBucket = dbtx.ReadBucket(bucketKey)
}
if rootBucket == nil {
tc.t.Errorf("ReadWriteBucket/ReadBucket: unexpected nil root bucket")
_ = dbtx.Rollback()
return false
}
if writable {
tc.isWritable = writable
if !testReadWriteBucketInterface(tc, rootBucket.(walletdb.ReadWriteBucket)) {
_ = dbtx.Rollback()
return false
}
}
if !writable {
// Rollback the transaction.
if err := dbtx.Rollback(); err != nil {
tc.t.Errorf("Commit: unexpected error %v", err)
return false
}
} else {
rootBucket := rootBucket.(walletdb.ReadWriteBucket)
if !testPutValues(tc, rootBucket, putValues) {
return false
}
if rollback {
// Rollback the transaction.
if err := dbtx.Rollback(); err != nil {
tc.t.Errorf("Rollback: unexpected "+
"error %v", err)
return false
}
} else {
// The commit should succeed.
if err := dbtx.(walletdb.ReadWriteTx).Commit(); err != nil {
tc.t.Errorf("Commit: unexpected error "+
"%v", err)
return false
}
}
}
return true
}
// checkValues starts a read-only transaction and checks that all of
// the key/value pairs specified in the expectedValues parameter match
// what's in the database.
checkValues := func(expectedValues map[string]string) bool {
// Begin another read-only transaction to ensure...
dbtx, err := db.BeginReadTx()
if err != nil {
tc.t.Errorf("BeginReadTx: unexpected error %v", err)
return false
}
rootBucket := dbtx.ReadBucket(bucketKey)
if rootBucket == nil {
tc.t.Errorf("ReadBucket: unexpected nil root bucket")
_ = dbtx.Rollback()
return false
}
if !testGetValues(tc, rootBucket, expectedValues) {
_ = dbtx.Rollback()
return false
}
// Rollback the read-only transaction.
if err := dbtx.Rollback(); err != nil {
tc.t.Errorf("Commit: unexpected error %v", err)
return false
}
return true
}
// deleteValues starts a read-write transaction and deletes the keys
// in the passed key/value pairs.
deleteValues := func(values map[string]string) bool {
dbtx, err := db.BeginReadWriteTx()
if err != nil {
tc.t.Errorf("BeginReadWriteTx: unexpected error %v", err)
_ = dbtx.Rollback()
return false
}
rootBucket := dbtx.ReadWriteBucket(bucketKey)
if rootBucket == nil {
tc.t.Errorf("RootBucket: unexpected nil root bucket")
_ = dbtx.Rollback()
return false
}
// Delete the keys and ensure they were deleted.
if !testDeleteValues(tc, rootBucket, values) {
_ = dbtx.Rollback()
return false
}
if !testGetValues(tc, rootBucket, rollbackValues(values)) {
_ = dbtx.Rollback()
return false
}
// Commit the changes and ensure it was successful.
if err := dbtx.Commit(); err != nil {
tc.t.Errorf("Commit: unexpected error %v", err)
return false
}
return true
}
// keyValues holds the keys and values to use when putting values
// into a bucket.
var keyValues = map[string]string{
"umtxkey1": "foo1",
"umtxkey2": "foo2",
"umtxkey3": "foo3",
}
// Ensure that attempting populating the values using a read-only
// transaction fails as expected.
if !populateValues(false, true, keyValues) {
return false
}
if !checkValues(rollbackValues(keyValues)) {
return false
}
// Ensure that attempting populating the values using a read-write
// transaction and then rolling it back yields the expected values.
if !populateValues(true, true, keyValues) {
return false
}
if !checkValues(rollbackValues(keyValues)) {
return false
}
// Ensure that attempting populating the values using a read-write
// transaction and then committing it stores the expected values.
if !populateValues(true, false, keyValues) {
return false
}
if !checkValues(keyValues) {
return false
}
// Clean up the keys.
if !deleteValues(keyValues) {
return false
}
return true
}
// testNamespaceAndTxInterfaces creates a namespace using the provided key and
// tests all facets of it interface as well as transaction and bucket
// interfaces under it.
func testNamespaceAndTxInterfaces(tc *testContext, namespaceKey string) bool {
namespaceKeyBytes := []byte(namespaceKey)
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
_, err := tx.CreateTopLevelBucket(namespaceKeyBytes)
return err
})
if err != nil {
tc.t.Errorf("CreateTopLevelBucket: unexpected error: %v", err)
return false
}
defer func() {
// Remove the namespace now that the tests are done for it.
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
return tx.DeleteTopLevelBucket(namespaceKeyBytes)
})
if err != nil {
tc.t.Errorf("DeleteTopLevelBucket: unexpected error: %v", err)
return
}
}()
if !testManualTxInterface(tc, namespaceKeyBytes) {
return false
}
// keyValues holds the keys and values to use when putting values
// into a bucket.
var keyValues = map[string]string{
"mtxkey1": "foo1",
"mtxkey2": "foo2",
"mtxkey3": "foo3",
}
// Test the bucket interface via a managed read-only transaction.
err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
rootBucket := tx.ReadBucket(namespaceKeyBytes)
if rootBucket == nil {
return fmt.Errorf("ReadBucket: unexpected nil root bucket")
}
return nil
})
if err != nil {
if err != errSubTestFail {
tc.t.Errorf("%v", err)
}
return false
}
// Test that we can read the top level buckets.
var topLevelBuckets []string
walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
return tx.ForEachBucket(func(key []byte) error {
topLevelBuckets = append(topLevelBuckets, string(key))
return nil
})
})
if err != nil {
if err != errSubTestFail {
tc.t.Errorf("%v", err)
}
return false
}
if len(topLevelBuckets) != 1 {
tc.t.Errorf("ForEachBucket: expected only one top level bucket")
return false
}
if topLevelBuckets[0] != namespaceKey {
tc.t.Errorf("ForEachBucket: expected %v, got %v", namespaceKey,
topLevelBuckets[0])
return false
}
// Test the bucket interface via a managed read-write transaction.
// Also, put a series of values and force a rollback so the following
// code can ensure the values were not stored.
forceRollbackError := fmt.Errorf("force rollback")
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
rootBucket := tx.ReadWriteBucket(namespaceKeyBytes)
if rootBucket == nil {
return fmt.Errorf("ReadWriteBucket: unexpected nil root bucket")
}
tc.isWritable = true
if !testReadWriteBucketInterface(tc, rootBucket) {
return errSubTestFail
}
if !testPutValues(tc, rootBucket, keyValues) {
return errSubTestFail
}
// Return an error to force a rollback.
return forceRollbackError
})
if err != forceRollbackError {
if err == errSubTestFail {
return false
}
tc.t.Errorf("Update: inner function error not returned - got "+
"%v, want %v", err, forceRollbackError)
return false
}
// Ensure the values that should have not been stored due to the forced
// rollback above were not actually stored.
err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
rootBucket := tx.ReadBucket(namespaceKeyBytes)
if rootBucket == nil {
return fmt.Errorf("ReadBucket: unexpected nil root bucket")
}
if !testGetValues(tc, rootBucket, rollbackValues(keyValues)) {
return errSubTestFail
}
return nil
})
if err != nil {
if err != errSubTestFail {
tc.t.Errorf("%v", err)
}
return false
}
// Store a series of values via a managed read-write transaction.
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
rootBucket := tx.ReadWriteBucket(namespaceKeyBytes)
if rootBucket == nil {
return fmt.Errorf("ReadWriteBucket: unexpected nil root bucket")
}
if !testPutValues(tc, rootBucket, keyValues) {
return errSubTestFail
}
return nil
})
if err != nil {
if err != errSubTestFail {
tc.t.Errorf("%v", err)
}
return false
}
// Ensure the values stored above were committed as expected.
err = walletdb.View(tc.db, func(tx walletdb.ReadTx) error {
rootBucket := tx.ReadBucket(namespaceKeyBytes)
if rootBucket == nil {
return fmt.Errorf("ReadBucket: unexpected nil root bucket")
}
if !testGetValues(tc, rootBucket, keyValues) {
return errSubTestFail
}
return nil
})
if err != nil {
if err != errSubTestFail {
tc.t.Errorf("%v", err)
}
return false
}
// Clean up the values stored above in a managed read-write transaction.
err = walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
rootBucket := tx.ReadWriteBucket(namespaceKeyBytes)
if rootBucket == nil {
return fmt.Errorf("ReadWriteBucket: unexpected nil root bucket")
}
if !testDeleteValues(tc, rootBucket, keyValues) {
return errSubTestFail
}
return nil
})
if err != nil {
if err != errSubTestFail {
tc.t.Errorf("%v", err)
}
return false
}
return true
}
// testAdditionalErrors performs some tests for error cases not covered
// elsewhere in the tests and therefore improves negative test coverage.
func testAdditionalErrors(tc *testContext) bool {
ns3Key := []byte("ns3")
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
// Create a new namespace
rootBucket, err := tx.CreateTopLevelBucket(ns3Key)
if err != nil {
return fmt.Errorf("CreateTopLevelBucket: unexpected error: %v", err)
}
// Ensure CreateBucket returns the expected error when no bucket
// key is specified.
wantErr := walletdb.ErrBucketNameRequired
if _, err := rootBucket.CreateBucket(nil); err != wantErr {
return fmt.Errorf("CreateBucket: unexpected error - "+
"got %v, want %v", err, wantErr)
}
// Ensure DeleteNestedBucket returns the expected error when no bucket
// key is specified.
wantErr = walletdb.ErrIncompatibleValue
if err := rootBucket.DeleteNestedBucket(nil); err != wantErr {
return fmt.Errorf("DeleteNestedBucket: unexpected error - "+
"got %v, want %v", err, wantErr)
}
// Ensure Put returns the expected error when no key is
// specified.
wantErr = walletdb.ErrKeyRequired
if err := rootBucket.Put(nil, nil); err != wantErr {
return fmt.Errorf("Put: unexpected error - got %v, "+
"want %v", err, wantErr)
}
return nil
})
if err != nil {
if err != errSubTestFail {
tc.t.Errorf("%v", err)
}
return false
}
// Ensure that attempting to rollback or commit a transaction that is
// already closed returns the expected error.
tx, err := tc.db.BeginReadWriteTx()
if err != nil {
tc.t.Errorf("Begin: unexpected error: %v", err)
return false
}
if err := tx.Rollback(); err != nil {
tc.t.Errorf("Rollback: unexpected error: %v", err)
return false
}
wantErr := walletdb.ErrTxClosed
if err := tx.Rollback(); err != wantErr {
tc.t.Errorf("Rollback: unexpected error - got %v, want %v", err,
wantErr)
return false
}
if err := tx.Commit(); err != wantErr {
tc.t.Errorf("Commit: unexpected error - got %v, want %v", err,
wantErr)
return false
}
return true
}
// testBatchInterface tests that if the target database implements the batch
// method, then the method functions as expected.
func testBatchInterface(tc *testContext) bool {
// If the database doesn't support the batch super-set of the
// interface, then we're done here.
batchDB, ok := tc.db.(walletdb.BatchDB)
if !ok {
return true
}
const numGoroutines = 5
errChan := make(chan error, numGoroutines)
var wg sync.WaitGroup
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
err := walletdb.Batch(batchDB, func(tx walletdb.ReadWriteTx) error {
b, err := tx.CreateTopLevelBucket([]byte("test"))
if err != nil {
return err
}
byteI := []byte{byte(i)}
return b.Put(byteI, byteI)
})
errChan <- err
}(i)
}
wg.Wait()
close(errChan)
for err := range errChan {
if err != nil {
tc.t.Errorf("Batch: unexpected error: %v", err)
return false
}
}
err := walletdb.View(batchDB, func(tx walletdb.ReadTx) error {
b := tx.ReadBucket([]byte("test"))
for i := 0; i < numGoroutines; i++ {
byteI := []byte{byte(i)}
if v := b.Get(byteI); v == nil {
return fmt.Errorf("key %v not present", byteI)
} else if !bytes.Equal(v, byteI) {
return fmt.Errorf("key %v not equal to value: "+
"%v", byteI, v)
}
}
return nil
})
if err != nil {
tc.t.Errorf("Batch: unexpected error: %v", err)
return false
}
return true
}
// TestInterface performs all interfaces tests for this database driver.
func TestInterface(t Tester, dbType string, args ...interface{}) {
db, err := walletdb.Create(dbType, args...)
if err != nil {
t.Errorf("Failed to create test database (%s) %v", dbType, err)
return
}
defer db.Close()
// Run all of the interface tests against the database.
// Create a test context to pass around.
context := testContext{t: t, db: db}
// Create a namespace and test the interface for it.
if !testNamespaceAndTxInterfaces(&context, "ns1") {
return
}
// Create a second namespace and test the interface for it.
if !testNamespaceAndTxInterfaces(&context, "ns2") {
return
}
// Check a few more error conditions not covered elsewhere.
if !testAdditionalErrors(&context) {
return
}
// If applicable, also test the behavior of the Batch call.
if !testBatchInterface(&context) {
return
}
}