diff --git a/walletdb/bdb/driver_test.go b/walletdb/bdb/driver_test.go index 6ebcae6..bb96c06 100644 --- a/walletdb/bdb/driver_test.go +++ b/walletdb/bdb/driver_test.go @@ -81,7 +81,7 @@ func TestCreateOpenFail(t *testing.T) { db.Close() wantErr = walletdb.ErrDbNotOpen - if _, err := db.Namespace([]byte("ns1")); err != wantErr { + if _, err := db.BeginReadTx(); err != wantErr { t.Errorf("Namespace: did not receive expected error - got %v, "+ "want %v", err, wantErr) return @@ -109,19 +109,14 @@ func TestPersistence(t *testing.T) { "ns1key3": "foo3", } ns1Key := []byte("ns1") - ns1, err := db.Namespace(ns1Key) - if err != nil { - t.Errorf("Namespace: unexpected error: %v", err) - return - } - err = ns1.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns1, err := tx.CreateTopLevelBucket(ns1Key) + if err != nil { + return err } for k, v := range storeValues { - if err := rootBucket.Put([]byte(k), []byte(v)); err != nil { + if err := ns1.Put([]byte(k), []byte(v)); err != nil { return fmt.Errorf("Put: unexpected error: %v", err) } } @@ -144,19 +139,14 @@ func TestPersistence(t *testing.T) { // Ensure the values previously stored in the 3rd namespace still exist // and are correct. - ns1, err = db.Namespace(ns1Key) - if err != nil { - t.Errorf("Namespace: unexpected error: %v", err) - return - } - err = ns1.View(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") + err = walletdb.View(db, func(tx walletdb.ReadTx) error { + ns1 := tx.ReadBucket(ns1Key) + if ns1 == nil { + return fmt.Errorf("ReadTx.ReadBucket: unexpected nil root bucket") } for k, v := range storeValues { - gotVal := rootBucket.Get([]byte(k)) + gotVal := ns1.Get([]byte(k)) if !reflect.DeepEqual(gotVal, []byte(v)) { return fmt.Errorf("Get: key '%s' does not "+ "match expected value - got %s, want %s", @@ -171,19 +161,3 @@ func TestPersistence(t *testing.T) { return } } - -// TestInterface performs all interfaces tests for this database driver. -func TestInterface(t *testing.T) { - // Create a new database to run tests against. - dbPath := "interfacetest.db" - db, err := walletdb.Create(dbType, dbPath) - if err != nil { - t.Errorf("Failed to create test database (%s) %v", dbType, err) - return - } - defer os.Remove(dbPath) - defer db.Close() - - // Run all of the interface tests against the database. - testInterface(t, db) -} diff --git a/walletdb/bdb/interface_test.go b/walletdb/bdb/interface_test.go index d86dcdf..ec10fec 100644 --- a/walletdb/bdb/interface_test.go +++ b/walletdb/bdb/interface_test.go @@ -13,793 +13,15 @@ package bdb_test import ( - "fmt" - "reflect" + "os" "testing" - "github.com/roasbeef/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/walletdb/walletdbtest" ) -// subTestFailError is used to signal that a sub test returned false. -var subTestFailError = 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 *testing.T - 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.ReadWriteBucket, 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 -} - -// testNestedBucket reruns the testBucketInterface against a nested bucket along -// with a counter to only test a couple of level deep. -func testNestedBucket(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 !testBucketInterface(tc, testBucket) { - return false - } - - return true -} - -// testBucketInterface ensures the bucket interface is working properly by -// exercising all of its functions. -func testBucketInterface(tc *testContext, bucket walletdb.ReadWriteBucket) bool { - if bucket.Writable() != tc.isWritable { - tc.t.Errorf("Bucket writable state does not match.") - return false - } - - if tc.isWritable { - // 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 { - kString := string(k) - wantV, ok := keyValues[kString] - if !ok { - return fmt.Errorf("ForEach: key '%s' should "+ - "exist", kString) - } - - if !reflect.DeepEqual(v, []byte(wantV)) { - return fmt.Errorf("ForEach: value for key '%s' "+ - "does not match - got %s, want %s", - kString, v, wantV) - } - - keysFound[kString] = 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 - } - - // 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 !testNestedBucket(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 !testNestedBucket(tc, testBucket) { - return false - } - - // Ensure retrieving and existing bucket works as expected. - testBucket = bucket.ReadWriteBucket(testBucketName) - if !testNestedBucket(tc, testBucket) { - return false - } - - // Ensure deleting a bucket works as intended. - if err := bucket.DeleteBucket(testBucketName); err != nil { - tc.t.Errorf("DeleteBucket: unexpected error: %v", err) - return false - } - if b := bucket.ReadWriteBucket(testBucketName); b != nil { - tc.t.Errorf("DeleteBucket: 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.DeleteBucket(testBucketName); err != wantErr { - tc.t.Errorf("DeleteBucket: 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 !testNestedBucket(tc, testBucket) { - return false - } - - // Delete the test bucket to avoid leaving it around for future - // calls. - if err := bucket.DeleteBucket(testBucketName); err != nil { - tc.t.Errorf("DeleteBucket: unexpected error: %v", err) - return false - } - if b := bucket.ReadWriteBucket(testBucketName); b != nil { - tc.t.Errorf("DeleteBucket: bucket '%s' still exists", - testBucketName) - return false - } - } else { - // Put should fail with bucket that is not writable. - wantErr := walletdb.ErrTxNotWritable - failBytes := []byte("fail") - if err := bucket.Put(failBytes, failBytes); err != wantErr { - tc.t.Errorf("Put did not fail with unwritable bucket") - return false - } - - // Delete should fail with bucket that is not writable. - if err := bucket.Delete(failBytes); err != wantErr { - tc.t.Errorf("Put did not fail with unwritable bucket") - return false - } - - // CreateBucket should fail with bucket that is not writable. - if _, err := bucket.CreateBucket(failBytes); err != wantErr { - tc.t.Errorf("CreateBucket did not fail with unwritable " + - "bucket") - return false - } - - // CreateBucketIfNotExists should fail with bucket that is not - // writable. - if _, err := bucket.CreateBucketIfNotExists(failBytes); err != wantErr { - tc.t.Errorf("CreateBucketIfNotExists did not fail with " + - "unwritable bucket") - return false - } - - // DeleteBucket should fail with bucket that is not writable. - if err := bucket.DeleteBucket(failBytes); err != wantErr { - tc.t.Errorf("DeleteBucket did not fail with unwritable " + - "bucket") - return false - } - } - - return true -} - -// testManualTxInterface ensures that manual transactions work as expected. -func testManualTxInterface(tc *testContext, namespace walletdb.Namespace) bool { - // 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 { - tx, err := namespace.Begin(writable) - if err != nil { - tc.t.Errorf("Begin: unexpected error %v", err) - return false - } - - rootBucket := tx.RootBucket() - if rootBucket == nil { - tc.t.Errorf("RootBucket: unexpected nil root bucket") - _ = tx.Rollback() - return false - } - - tc.isWritable = writable - if !testBucketInterface(tc, rootBucket) { - _ = tx.Rollback() - return false - } - - if !writable { - // The transaction is not writable, so it should fail - // the commit. - if err := tx.Commit(); err != walletdb.ErrTxNotWritable { - tc.t.Errorf("Commit: unexpected error %v, "+ - "want %v", err, walletdb.ErrTxNotWritable) - _ = tx.Rollback() - return false - } - - // Rollback the transaction. - if err := tx.Rollback(); err != nil { - tc.t.Errorf("Commit: unexpected error %v", err) - return false - } - } else { - if !testPutValues(tc, rootBucket, putValues) { - return false - } - - if rollback { - // Rollback the transaction. - if err := tx.Rollback(); err != nil { - tc.t.Errorf("Rollback: unexpected "+ - "error %v", err) - return false - } - } else { - // The commit should succeed. - if err := tx.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... - tx, err := namespace.Begin(false) - if err != nil { - tc.t.Errorf("Begin: unexpected error %v", err) - return false - } - - rootBucket := tx.RootBucket() - if rootBucket == nil { - tc.t.Errorf("RootBucket: unexpected nil root bucket") - _ = tx.Rollback() - return false - } - - if !testGetValues(tc, rootBucket, expectedValues) { - _ = tx.Rollback() - return false - } - - // Rollback the read-only transaction. - if err := tx.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 { - tx, err := namespace.Begin(true) - if err != nil { - - } - - rootBucket := tx.RootBucket() - if rootBucket == nil { - tc.t.Errorf("RootBucket: unexpected nil root bucket") - _ = tx.Rollback() - return false - } - - // Delete the keys and ensure they were deleted. - if !testDeleteValues(tc, rootBucket, values) { - _ = tx.Rollback() - return false - } - if !testGetValues(tc, rootBucket, rollbackValues(values)) { - _ = tx.Rollback() - return false - } - - // Commit the changes and ensure it was successful. - if err := tx.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) - namespace, err := tc.db.Namespace(namespaceKeyBytes) - if err != nil { - tc.t.Errorf("Namespace: unexpected error: %v", err) - return false - } - defer func() { - // Remove the namespace now that the tests are done for it. - if err := tc.db.DeleteNamespace(namespaceKeyBytes); err != nil { - tc.t.Errorf("DeleteNamespace: unexpected error: %v", err) - return - } - }() - - if !testManualTxInterface(tc, namespace) { - 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 = namespace.View(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - tc.isWritable = false - if !testBucketInterface(tc, rootBucket) { - return subTestFailError - } - - return nil - }) - if err != nil { - if err != subTestFailError { - tc.t.Errorf("%v", err) - } - return false - } - - // Ensure errors returned from the user-supplied View function are - // returned. - viewError := fmt.Errorf("example view error") - err = namespace.View(func(tx walletdb.Tx) error { - return viewError - }) - if err != viewError { - tc.t.Errorf("View: inner function error not returned - got "+ - "%v, want %v", err, viewError) - 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 = namespace.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - tc.isWritable = true - if !testBucketInterface(tc, rootBucket) { - return subTestFailError - } - - if !testPutValues(tc, rootBucket, keyValues) { - return subTestFailError - } - - // Return an error to force a rollback. - return forceRollbackError - }) - if err != forceRollbackError { - if err == subTestFailError { - 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 = namespace.View(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - if !testGetValues(tc, rootBucket, rollbackValues(keyValues)) { - return subTestFailError - } - - return nil - }) - if err != nil { - if err != subTestFailError { - tc.t.Errorf("%v", err) - } - return false - } - - // Store a series of values via a managed read-write transaction. - err = namespace.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - if !testPutValues(tc, rootBucket, keyValues) { - return subTestFailError - } - - return nil - }) - if err != nil { - if err != subTestFailError { - tc.t.Errorf("%v", err) - } - return false - } - - // Ensure the values stored above were committed as expected. - err = namespace.View(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - if !testGetValues(tc, rootBucket, keyValues) { - return subTestFailError - } - - return nil - }) - if err != nil { - if err != subTestFailError { - tc.t.Errorf("%v", err) - } - return false - } - - // Clean up the values stored above in a managed read-write transaction. - err = namespace.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - if !testDeleteValues(tc, rootBucket, keyValues) { - return subTestFailError - } - - return nil - }) - if err != nil { - if err != subTestFailError { - 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 { - // Create a new namespace and then intentionally delete the namespace - // bucket out from under it to force errors. - ns3Key := []byte("ns3") - ns3, err := tc.db.Namespace(ns3Key) - if err != nil { - tc.t.Errorf("Namespace: unexpected error: %v", err) - return false - } - if err := tc.db.DeleteNamespace(ns3Key); err != nil { - tc.t.Errorf("DeleteNamespace: unexpected error: %v", err) - return false - } - - // Ensure Begin fails when the namespace bucket does not exist. - wantErr := walletdb.ErrBucketNotFound - if _, err := ns3.Begin(false); err != wantErr { - tc.t.Errorf("Begin: did not receive expected error - got %v, "+ - "want %v", err, wantErr) - return false - } - - // Ensure View fails when the namespace bucket does not exist. - err = ns3.View(func(tx walletdb.Tx) error { - return nil - }) - if err != wantErr { - tc.t.Errorf("View: did not receive expected error - got %v, "+ - "want %v", err, wantErr) - return false - } - - // Ensure Update fails when the namespace bucket does not exist. - err = ns3.Update(func(tx walletdb.Tx) error { - return nil - }) - if err != wantErr { - tc.t.Errorf("View: did not receive expected error - got %v, "+ - "want %v", err, wantErr) - return false - } - - // Recreate the namespace to bring the bucket back. - ns3, err = tc.db.Namespace(ns3Key) - if err != nil { - tc.t.Errorf("Namespace: unexpected error: %v", err) - return false - } - defer func() { - // Remove the namespace now that the tests are done for it. - if err := tc.db.DeleteNamespace(ns3Key); err != nil { - tc.t.Errorf("DeleteNamespace: unexpected error: %v", err) - return - } - }() - - err = ns3.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - // 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 DeleteBucket returns the expected error when no bucket - // key is specified. - wantErr = walletdb.ErrIncompatibleValue - if err := rootBucket.DeleteBucket(nil); err != wantErr { - return fmt.Errorf("DeleteBucket: 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 != subTestFailError { - 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 := ns3.Begin(false) - 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 -} - -// testInterface tests performs tests for the various interfaces of walletdb -// which require state in the database for the given database type. -func testInterface(t *testing.T, db walletdb.DB) { - // 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 - } +// TestInterface performs all interfaces tests for this database driver. +func TestInterface(t *testing.T) { + dbPath := "interfacetest.db" + defer os.RemoveAll(dbPath) + walletdbtest.TestInterface(t, dbType, dbPath) } diff --git a/walletdb/example_test.go b/walletdb/example_test.go index 2d7c0a5..9777925 100644 --- a/walletdb/example_test.go +++ b/walletdb/example_test.go @@ -60,8 +60,8 @@ func exampleLoadDB() (walletdb.DB, func(), error) { return db, teardownFunc, err } -// This example demonstrates creating a new namespace. -func ExampleDB_namespace() { +// This example demonstrates creating a new top level bucket. +func ExampleDB_createTopLevelBucket() { // Load a database for the purposes of this example and schedule it to // be closed and removed on exit. See the Create example for more // details on what this step is doing. @@ -72,19 +72,25 @@ func ExampleDB_namespace() { } defer teardownFunc() - // Get or create a namespace in the database as needed. This namespace + dbtx, err := db.BeginReadWriteTx() + if err != nil { + fmt.Println(err) + return + } + defer dbtx.Commit() + + // Get or create a bucket in the database as needed. This bucket // is what is typically passed to specific sub-packages so they have // their own area to work in without worrying about conflicting keys. - namespaceKey := []byte("walletsubpackage") - namespace, err := db.Namespace(namespaceKey) + bucketKey := []byte("walletsubpackage") + bucket, err := dbtx.CreateTopLevelBucket(bucketKey) if err != nil { fmt.Println(err) return } - // Prevent unused error. Ordinarily the namespace would be used at this - // point to start a managed or manual transaction. - _ = namespace + // Prevent unused error. + _ = bucket // Output: } @@ -113,11 +119,20 @@ func Example_basicUsage() { defer os.Remove(dbPath) defer db.Close() - // Get or create a namespace in the database as needed. This namespace + // Get or create a bucket in the database as needed. This bucket // is what is typically passed to specific sub-packages so they have // their own area to work in without worrying about conflicting keys. - namespaceKey := []byte("walletsubpackage") - namespace, err := db.Namespace(namespaceKey) + bucketKey := []byte("walletsubpackage") + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + bucket := tx.ReadWriteBucket(bucketKey) + if bucket == nil { + _, err = tx.CreateTopLevelBucket(bucketKey) + if err != nil { + return err + } + } + return nil + }) if err != nil { fmt.Println(err) return @@ -126,13 +141,13 @@ func Example_basicUsage() { // Use the Update function of the namespace to perform a managed // read-write transaction. The transaction will automatically be rolled // back if the supplied inner function returns a non-nil error. - err = namespace.Update(func(tx walletdb.Tx) error { + err = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { // All data is stored against the root bucket of the namespace, // or nested buckets of the root bucket. It's not really // necessary to store it in a separate variable like this, but // it has been done here for the purposes of the example to // illustrate. - rootBucket := tx.RootBucket() + rootBucket := tx.ReadWriteBucket(bucketKey) // Store a key/value pair directly in the root bucket. key := []byte("mykey") diff --git a/walletdb/interface_test.go b/walletdb/interface_test.go deleted file mode 100644 index 44418fc..0000000 --- a/walletdb/interface_test.go +++ /dev/null @@ -1,805 +0,0 @@ -// Copyright (c) 2014 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -// This file intended to be copied into each backend driver directory. Each -// driver should have their own driver_test.go file which creates a database and -// invokes the testInterface function in this file to ensure the driver properly -// implements the interface. See the bdb backend driver for a working example. -// -// NOTE: When copying this file into the backend driver folder, the package name -// will need to be changed accordingly. - -package walletdb_test - -import ( - "fmt" - "reflect" - "testing" - - "github.com/roasbeef/btcwallet/walletdb" -) - -// subTestFailError is used to signal that a sub test returned false. -var subTestFailError = 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 *testing.T - 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.Bucket, 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.Bucket, 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.Bucket, 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 -} - -// testNestedBucket reruns the testBucketInterface against a nested bucket along -// with a counter to only test a couple of level deep. -func testNestedBucket(tc *testContext, testBucket walletdb.Bucket) bool { - // Don't go more than 2 nested level deep. - if tc.bucketDepth > 1 { - return true - } - - tc.bucketDepth++ - defer func() { - tc.bucketDepth-- - }() - if !testBucketInterface(tc, testBucket) { - return false - } - - return true -} - -// testBucketInterface ensures the bucket interface is working properly by -// exercising all of its functions. -func testBucketInterface(tc *testContext, bucket walletdb.Bucket) bool { - if bucket.Writable() != tc.isWritable { - tc.t.Errorf("Bucket writable state does not match.") - return false - } - - if tc.isWritable { - // 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 { - kString := string(k) - wantV, ok := keyValues[kString] - if !ok { - return fmt.Errorf("ForEach: key '%s' should "+ - "exist", kString) - } - - if !reflect.DeepEqual(v, []byte(wantV)) { - return fmt.Errorf("ForEach: value for key '%s' "+ - "does not match - got %s, want %s", - kString, v, wantV) - } - - keysFound[kString] = 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 - } - - // 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 !testNestedBucket(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 !testNestedBucket(tc, testBucket) { - return false - } - - // Ensure retrieving and existing bucket works as expected. - testBucket = bucket.Bucket(testBucketName) - if !testNestedBucket(tc, testBucket) { - return false - } - - // Ensure deleting a bucket works as intended. - if err := bucket.DeleteBucket(testBucketName); err != nil { - tc.t.Errorf("DeleteBucket: unexpected error: %v", err) - return false - } - if b := bucket.Bucket(testBucketName); b != nil { - tc.t.Errorf("DeleteBucket: 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.DeleteBucket(testBucketName); err != wantErr { - tc.t.Errorf("DeleteBucket: 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 !testNestedBucket(tc, testBucket) { - return false - } - - // Delete the test bucket to avoid leaving it around for future - // calls. - if err := bucket.DeleteBucket(testBucketName); err != nil { - tc.t.Errorf("DeleteBucket: unexpected error: %v", err) - return false - } - if b := bucket.Bucket(testBucketName); b != nil { - tc.t.Errorf("DeleteBucket: bucket '%s' still exists", - testBucketName) - return false - } - } else { - // Put should fail with bucket that is not writable. - wantErr := walletdb.ErrTxNotWritable - failBytes := []byte("fail") - if err := bucket.Put(failBytes, failBytes); err != wantErr { - tc.t.Errorf("Put did not fail with unwritable bucket") - return false - } - - // Delete should fail with bucket that is not writable. - if err := bucket.Delete(failBytes); err != wantErr { - tc.t.Errorf("Put did not fail with unwritable bucket") - return false - } - - // CreateBucket should fail with bucket that is not writable. - if _, err := bucket.CreateBucket(failBytes); err != wantErr { - tc.t.Errorf("CreateBucket did not fail with unwritable " + - "bucket") - return false - } - - // CreateBucketIfNotExists should fail with bucket that is not - // writable. - if _, err := bucket.CreateBucketIfNotExists(failBytes); err != wantErr { - tc.t.Errorf("CreateBucketIfNotExists did not fail with " + - "unwritable bucket") - return false - } - - // DeleteBucket should fail with bucket that is not writable. - if err := bucket.DeleteBucket(failBytes); err != wantErr { - tc.t.Errorf("DeleteBucket did not fail with unwritable " + - "bucket") - return false - } - } - - return true -} - -// testManualTxInterface ensures that manual transactions work as expected. -func testManualTxInterface(tc *testContext, namespace walletdb.Namespace) bool { - // 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 { - tx, err := namespace.Begin(writable) - if err != nil { - tc.t.Errorf("Begin: unexpected error %v", err) - return false - } - - rootBucket := tx.RootBucket() - if rootBucket == nil { - tc.t.Errorf("RootBucket: unexpected nil root bucket") - _ = tx.Rollback() - return false - } - - tc.isWritable = writable - if !testBucketInterface(tc, rootBucket) { - _ = tx.Rollback() - return false - } - - if !writable { - // The transaction is not writable, so it should fail - // the commit. - if err := tx.Commit(); err != walletdb.ErrTxNotWritable { - tc.t.Errorf("Commit: unexpected error %v, "+ - "want %v", err, walletdb.ErrTxNotWritable) - _ = tx.Rollback() - return false - } - - // Rollback the transaction. - if err := tx.Rollback(); err != nil { - tc.t.Errorf("Commit: unexpected error %v", err) - return false - } - } else { - if !testPutValues(tc, rootBucket, putValues) { - return false - } - - if rollback { - // Rollback the transaction. - if err := tx.Rollback(); err != nil { - tc.t.Errorf("Rollback: unexpected "+ - "error %v", err) - return false - } - } else { - // The commit should succeed. - if err := tx.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... - tx, err := namespace.Begin(false) - if err != nil { - tc.t.Errorf("Begin: unexpected error %v", err) - return false - } - - rootBucket := tx.RootBucket() - if rootBucket == nil { - tc.t.Errorf("RootBucket: unexpected nil root bucket") - _ = tx.Rollback() - return false - } - - if !testGetValues(tc, rootBucket, expectedValues) { - _ = tx.Rollback() - return false - } - - // Rollback the read-only transaction. - if err := tx.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 { - tx, err := namespace.Begin(true) - if err != nil { - - } - - rootBucket := tx.RootBucket() - if rootBucket == nil { - tc.t.Errorf("RootBucket: unexpected nil root bucket") - _ = tx.Rollback() - return false - } - - // Delete the keys and ensure they were deleted. - if !testDeleteValues(tc, rootBucket, values) { - _ = tx.Rollback() - return false - } - if !testGetValues(tc, rootBucket, rollbackValues(values)) { - _ = tx.Rollback() - return false - } - - // Commit the changes and ensure it was successful. - if err := tx.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) - namespace, err := tc.db.Namespace(namespaceKeyBytes) - if err != nil { - tc.t.Errorf("Namespace: unexpected error: %v", err) - return false - } - defer func() { - // Remove the namespace now that the tests are done for it. - if err := tc.db.DeleteNamespace(namespaceKeyBytes); err != nil { - tc.t.Errorf("DeleteNamespace: unexpected error: %v", err) - return - } - }() - - if !testManualTxInterface(tc, namespace) { - 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 = namespace.View(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - tc.isWritable = false - if !testBucketInterface(tc, rootBucket) { - return subTestFailError - } - - return nil - }) - if err != nil { - if err != subTestFailError { - tc.t.Errorf("%v", err) - } - return false - } - - // Ensure errors returned from the user-supplied View function are - // returned. - viewError := fmt.Errorf("example view error") - err = namespace.View(func(tx walletdb.Tx) error { - return viewError - }) - if err != viewError { - tc.t.Errorf("View: inner function error not returned - got "+ - "%v, want %v", err, viewError) - 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 = namespace.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - tc.isWritable = true - if !testBucketInterface(tc, rootBucket) { - return subTestFailError - } - - if !testPutValues(tc, rootBucket, keyValues) { - return subTestFailError - } - - // Return an error to force a rollback. - return forceRollbackError - }) - if err != forceRollbackError { - if err == subTestFailError { - 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 = namespace.View(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - if !testGetValues(tc, rootBucket, rollbackValues(keyValues)) { - return subTestFailError - } - - return nil - }) - if err != nil { - if err != subTestFailError { - tc.t.Errorf("%v", err) - } - return false - } - - // Store a series of values via a managed read-write transaction. - err = namespace.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - if !testPutValues(tc, rootBucket, keyValues) { - return subTestFailError - } - - return nil - }) - if err != nil { - if err != subTestFailError { - tc.t.Errorf("%v", err) - } - return false - } - - // Ensure the values stored above were committed as expected. - err = namespace.View(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - if !testGetValues(tc, rootBucket, keyValues) { - return subTestFailError - } - - return nil - }) - if err != nil { - if err != subTestFailError { - tc.t.Errorf("%v", err) - } - return false - } - - // Clean up the values stored above in a managed read-write transaction. - err = namespace.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - if !testDeleteValues(tc, rootBucket, keyValues) { - return subTestFailError - } - - return nil - }) - if err != nil { - if err != subTestFailError { - 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 { - // Create a new namespace and then intentionally delete the namespace - // bucket out from under it to force errors. - ns3Key := []byte("ns3") - ns3, err := tc.db.Namespace(ns3Key) - if err != nil { - tc.t.Errorf("Namespace: unexpected error: %v", err) - return false - } - if err := tc.db.DeleteNamespace(ns3Key); err != nil { - tc.t.Errorf("DeleteNamespace: unexpected error: %v", err) - return false - } - - // Ensure Begin fails when the namespace bucket does not exist. - wantErr := walletdb.ErrBucketNotFound - if _, err := ns3.Begin(false); err != wantErr { - tc.t.Errorf("Begin: did not receive expected error - got %v, "+ - "want %v", err, wantErr) - return false - } - - // Ensure View fails when the namespace bucket does not exist. - err = ns3.View(func(tx walletdb.Tx) error { - return nil - }) - if err != wantErr { - tc.t.Errorf("View: did not receive expected error - got %v, "+ - "want %v", err, wantErr) - return false - } - - // Ensure Update fails when the namespace bucket does not exist. - err = ns3.Update(func(tx walletdb.Tx) error { - return nil - }) - if err != wantErr { - tc.t.Errorf("View: did not receive expected error - got %v, "+ - "want %v", err, wantErr) - return false - } - - // Recreate the namespace to bring the bucket back. - ns3, err = tc.db.Namespace(ns3Key) - if err != nil { - tc.t.Errorf("Namespace: unexpected error: %v", err) - return false - } - defer func() { - // Remove the namespace now that the tests are done for it. - if err := tc.db.DeleteNamespace(ns3Key); err != nil { - tc.t.Errorf("DeleteNamespace: unexpected error: %v", err) - return - } - }() - - err = ns3.Update(func(tx walletdb.Tx) error { - rootBucket := tx.RootBucket() - if rootBucket == nil { - return fmt.Errorf("RootBucket: unexpected nil root bucket") - } - - // 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 DeleteBucket returns the expected error when no bucket - // key is specified. - wantErr = walletdb.ErrIncompatibleValue - if err := rootBucket.DeleteBucket(nil); err != wantErr { - return fmt.Errorf("DeleteBucket: 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 != subTestFailError { - 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 := ns3.Begin(false) - 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 -} - -// testInterface tests performs tests for the various interfaces of walletdb -// which require state in the database for the given database type. -func testInterface(t *testing.T, db walletdb.DB) { - // 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 - } -} diff --git a/walletdb/walletdbtest/doc.go b/walletdb/walletdbtest/doc.go new file mode 100644 index 0000000..8e9c613 --- /dev/null +++ b/walletdb/walletdbtest/doc.go @@ -0,0 +1,8 @@ +// Copyright (c) 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 provides exported tests that can be imported and +// consumed by walletdb driver tests to help ensure that drivers confirm to the +// database driver interface correctly. +package walletdbtest diff --git a/walletdb/walletdbtest/interface.go b/walletdb/walletdbtest/interface.go new file mode 100644 index 0000000..dcbdaca --- /dev/null +++ b/walletdb/walletdbtest/interface.go @@ -0,0 +1,707 @@ +// 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 ( + "fmt" + "os" + "reflect" + + "github.com/btcsuite/btcwallet/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 +} + +// 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 { + kString := string(k) + wantV, ok := keyValues[kString] + if !ok { + return fmt.Errorf("ForEach: key '%s' should "+ + "exist", kString) + } + + if !reflect.DeepEqual(v, []byte(wantV)) { + return fmt.Errorf("ForEach: value for key '%s' "+ + "does not match - got %s, want %s", + kString, v, wantV) + } + + keysFound[kString] = 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 + } + + // 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 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 +} + +// TestInterface performs all interfaces tests for this database driver. +func TestInterface(t Tester, dbType, dbPath string) { + db, err := walletdb.Create(dbType, dbPath) + if err != nil { + t.Errorf("Failed to create test database (%s) %v", dbType, err) + return + } + defer os.Remove(dbPath) + 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 + } +} diff --git a/walletdb/walletdbtest/tester.go b/walletdb/walletdbtest/tester.go new file mode 100644 index 0000000..f556a15 --- /dev/null +++ b/walletdb/walletdbtest/tester.go @@ -0,0 +1,24 @@ +// Copyright (c) 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 + +// Tester is an interface type that can be implemented by *testing.T. This +// allows drivers to call into the non-test API using their own test contexts. +type Tester interface { + Error(...interface{}) + Errorf(string, ...interface{}) + Fail() + FailNow() + Failed() bool + Fatal(...interface{}) + Fatalf(string, ...interface{}) + Log(...interface{}) + Logf(string, ...interface{}) + Parallel() + Skip(...interface{}) + SkipNow() + Skipf(string, ...interface{}) + Skipped() bool +}