+++ /dev/null
-// Copyright (c) 2015-2016 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.
-//
-// NOTE: When copying this file into the backend driver folder, the package name
-// will need to be changed accordingly.
-
-package ffldb_test
-
-import (
- "bytes"
- "compress/bzip2"
- "encoding/binary"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "reflect"
- "sync/atomic"
- "testing"
- "time"
-
- "github.com/btcsuite/btcd/chaincfg"
- "github.com/btcsuite/btcd/chaincfg/chainhash"
- "github.com/btcsuite/btcd/database"
- "github.com/btcsuite/btcd/wire"
- "github.com/btcsuite/btcutil"
-)
-
-var (
- // blockDataNet is the expected network in the test block data.
- blockDataNet = wire.MainNet
-
- // blockDataFile is the path to a file containing the first 256 blocks
- // of the block chain.
- blockDataFile = filepath.Join("..", "testdata", "blocks1-256.bz2")
-
- // errSubTestFail is used to signal that a sub test returned false.
- errSubTestFail = fmt.Errorf("sub test failure")
-)
-
-// loadBlocks loads the blocks contained in the testdata directory and returns
-// a slice of them.
-func loadBlocks(t *testing.T, dataFile string, network wire.BitcoinNet) ([]*btcutil.Block, error) {
- // Open the file that contains the blocks for reading.
- fi, err := os.Open(dataFile)
- if err != nil {
- t.Errorf("failed to open file %v, err %v", dataFile, err)
- return nil, err
- }
- defer func() {
- if err := fi.Close(); err != nil {
- t.Errorf("failed to close file %v %v", dataFile,
- err)
- }
- }()
- dr := bzip2.NewReader(fi)
-
- // Set the first block as the genesis block.
- blocks := make([]*btcutil.Block, 0, 256)
- genesis := btcutil.NewBlock(chaincfg.MainNetParams.GenesisBlock)
- blocks = append(blocks, genesis)
-
- // Load the remaining blocks.
- for height := 1; ; height++ {
- var net uint32
- err := binary.Read(dr, binary.LittleEndian, &net)
- if err == io.EOF {
- // Hit end of file at the expected offset. No error.
- break
- }
- if err != nil {
- t.Errorf("Failed to load network type for block %d: %v",
- height, err)
- return nil, err
- }
- if net != uint32(network) {
- t.Errorf("Block doesn't match network: %v expects %v",
- net, network)
- return nil, err
- }
-
- var blockLen uint32
- err = binary.Read(dr, binary.LittleEndian, &blockLen)
- if err != nil {
- t.Errorf("Failed to load block size for block %d: %v",
- height, err)
- return nil, err
- }
-
- // Read the block.
- blockBytes := make([]byte, blockLen)
- _, err = io.ReadFull(dr, blockBytes)
- if err != nil {
- t.Errorf("Failed to load block %d: %v", height, err)
- return nil, err
- }
-
- // Deserialize and store the block.
- block, err := btcutil.NewBlockFromBytes(blockBytes)
- if err != nil {
- t.Errorf("Failed to parse block %v: %v", height, err)
- return nil, err
- }
- blocks = append(blocks, block)
- }
-
- return blocks, nil
-}
-
-// checkDbError ensures the passed error is a database.Error with an error code
-// that matches the passed error code.
-func checkDbError(t *testing.T, testName string, gotErr error, wantErrCode database.ErrorCode) bool {
- dbErr, ok := gotErr.(database.Error)
- if !ok {
- t.Errorf("%s: unexpected error type - got %T, want %T",
- testName, gotErr, database.Error{})
- return false
- }
- if dbErr.ErrorCode != wantErrCode {
- t.Errorf("%s: unexpected error code - got %s (%s), want %s",
- testName, dbErr.ErrorCode, dbErr.Description,
- wantErrCode)
- return false
- }
-
- return true
-}
-
-// testContext is used to store context information about a running test which
-// is passed into helper functions.
-type testContext struct {
- t *testing.T
- db database.DB
- bucketDepth int
- isWritable bool
- blocks []*btcutil.Block
-}
-
-// keyPair houses a key/value pair. It is used over maps so ordering can be
-// maintained.
-type keyPair struct {
- key []byte
- value []byte
-}
-
-// lookupKey is a convenience method to lookup the requested key from the
-// provided keypair slice along with whether or not the key was found.
-func lookupKey(key []byte, values []keyPair) ([]byte, bool) {
- for _, item := range values {
- if bytes.Equal(item.key, key) {
- return item.value, true
- }
- }
-
- return nil, false
-}
-
-// toGetValues returns a copy of the provided keypairs with all of the nil
-// values set to an empty byte slice. This is used to ensure that keys set to
-// nil values result in empty byte slices when retrieved instead of nil.
-func toGetValues(values []keyPair) []keyPair {
- ret := make([]keyPair, len(values))
- copy(ret, values)
- for i := range ret {
- if ret[i].value == nil {
- ret[i].value = make([]byte, 0)
- }
- }
- return ret
-}
-
-// rollbackValues returns a copy of the provided keypairs with all values set to
-// nil. This is used to test that values are properly rolled back.
-func rollbackValues(values []keyPair) []keyPair {
- ret := make([]keyPair, len(values))
- copy(ret, values)
- for i := range ret {
- ret[i].value = nil
- }
- return ret
-}
-
-// testCursorKeyPair checks that the provide key and value match the expected
-// keypair at the provided index. It also ensures the index is in range for the
-// provided slice of expected keypairs.
-func testCursorKeyPair(tc *testContext, k, v []byte, index int, values []keyPair) bool {
- if index >= len(values) || index < 0 {
- tc.t.Errorf("Cursor: exceeded the expected range of values - "+
- "index %d, num values %d", index, len(values))
- return false
- }
-
- pair := &values[index]
- if !bytes.Equal(k, pair.key) {
- tc.t.Errorf("Mismatched cursor key: index %d does not match "+
- "the expected key - got %q, want %q", index, k,
- pair.key)
- return false
- }
- if !bytes.Equal(v, pair.value) {
- tc.t.Errorf("Mismatched cursor value: index %d does not match "+
- "the expected value - got %q, want %q", index, v,
- pair.value)
- return false
- }
-
- return true
-}
-
-// 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 database.Bucket, values []keyPair) bool {
- for _, item := range values {
- gotValue := bucket.Get(item.key)
- if !reflect.DeepEqual(gotValue, item.value) {
- tc.t.Errorf("Get: unexpected value for %q - got %q, "+
- "want %q", item.key, gotValue, item.value)
- 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 database.Bucket, values []keyPair) bool {
- for _, item := range values {
- if err := bucket.Put(item.key, item.value); 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 database.Bucket, values []keyPair) bool {
- for _, item := range values {
- if err := bucket.Delete(item.key); err != nil {
- tc.t.Errorf("Delete: unexpected error: %v", err)
- return false
- }
- }
-
- return true
-}
-
-// testCursorInterface ensures the cursor itnerface is working properly by
-// exercising all of its functions on the passed bucket.
-func testCursorInterface(tc *testContext, bucket database.Bucket) bool {
- // Ensure a cursor can be obtained for the bucket.
- cursor := bucket.Cursor()
- if cursor == nil {
- tc.t.Error("Bucket.Cursor: unexpected nil cursor returned")
- return false
- }
-
- // Ensure the cursor returns the same bucket it was created for.
- if cursor.Bucket() != bucket {
- tc.t.Error("Cursor.Bucket: does not match the bucket it was " +
- "created for")
- return false
- }
-
- if tc.isWritable {
- unsortedValues := []keyPair{
- {[]byte("cursor"), []byte("val1")},
- {[]byte("abcd"), []byte("val2")},
- {[]byte("bcd"), []byte("val3")},
- {[]byte("defg"), nil},
- }
- sortedValues := []keyPair{
- {[]byte("abcd"), []byte("val2")},
- {[]byte("bcd"), []byte("val3")},
- {[]byte("cursor"), []byte("val1")},
- {[]byte("defg"), nil},
- }
-
- // Store the values to be used in the cursor tests in unsorted
- // order and ensure they were actually stored.
- if !testPutValues(tc, bucket, unsortedValues) {
- return false
- }
- if !testGetValues(tc, bucket, toGetValues(unsortedValues)) {
- return false
- }
-
- // Ensure the cursor returns all items in byte-sorted order when
- // iterating forward.
- curIdx := 0
- for ok := cursor.First(); ok; ok = cursor.Next() {
- k, v := cursor.Key(), cursor.Value()
- if !testCursorKeyPair(tc, k, v, curIdx, sortedValues) {
- return false
- }
- curIdx++
- }
- if curIdx != len(unsortedValues) {
- tc.t.Errorf("Cursor: expected to iterate %d values, "+
- "but only iterated %d", len(unsortedValues),
- curIdx)
- return false
- }
-
- // Ensure the cursor returns all items in reverse byte-sorted
- // order when iterating in reverse.
- curIdx = len(sortedValues) - 1
- for ok := cursor.Last(); ok; ok = cursor.Prev() {
- k, v := cursor.Key(), cursor.Value()
- if !testCursorKeyPair(tc, k, v, curIdx, sortedValues) {
- return false
- }
- curIdx--
- }
- if curIdx > -1 {
- tc.t.Errorf("Reverse cursor: expected to iterate %d "+
- "values, but only iterated %d",
- len(sortedValues), len(sortedValues)-(curIdx+1))
- return false
- }
-
- // Ensure forward iteration works as expected after seeking.
- middleIdx := (len(sortedValues) - 1) / 2
- seekKey := sortedValues[middleIdx].key
- curIdx = middleIdx
- for ok := cursor.Seek(seekKey); ok; ok = cursor.Next() {
- k, v := cursor.Key(), cursor.Value()
- if !testCursorKeyPair(tc, k, v, curIdx, sortedValues) {
- return false
- }
- curIdx++
- }
- if curIdx != len(sortedValues) {
- tc.t.Errorf("Cursor after seek: expected to iterate "+
- "%d values, but only iterated %d",
- len(sortedValues)-middleIdx, curIdx-middleIdx)
- return false
- }
-
- // Ensure reverse iteration works as expected after seeking.
- curIdx = middleIdx
- for ok := cursor.Seek(seekKey); ok; ok = cursor.Prev() {
- k, v := cursor.Key(), cursor.Value()
- if !testCursorKeyPair(tc, k, v, curIdx, sortedValues) {
- return false
- }
- curIdx--
- }
- if curIdx > -1 {
- tc.t.Errorf("Reverse cursor after seek: expected to "+
- "iterate %d values, but only iterated %d",
- len(sortedValues)-middleIdx, middleIdx-curIdx)
- return false
- }
-
- // Ensure the cursor deletes items properly.
- if !cursor.First() {
- tc.t.Errorf("Cursor.First: no value")
- return false
- }
- k := cursor.Key()
- if err := cursor.Delete(); err != nil {
- tc.t.Errorf("Cursor.Delete: unexpected error: %v", err)
- return false
- }
- if val := bucket.Get(k); val != nil {
- tc.t.Errorf("Cursor.Delete: value for key %q was not "+
- "deleted", k)
- 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 database.Bucket) bool {
- // Don't go more than 2 nested levels deep.
- if tc.bucketDepth > 1 {
- return true
- }
-
- tc.bucketDepth++
- defer func() {
- tc.bucketDepth--
- }()
- return testBucketInterface(tc, testBucket)
-}
-
-// testBucketInterface ensures the bucket interface is working properly by
-// exercising all of its functions. This includes the cursor interface for the
-// cursor returned from the bucket.
-func testBucketInterface(tc *testContext, bucket database.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.
- keyValues := []keyPair{
- {[]byte("bucketkey1"), []byte("foo1")},
- {[]byte("bucketkey2"), []byte("foo2")},
- {[]byte("bucketkey3"), []byte("foo3")},
- {[]byte("bucketkey4"), nil},
- }
- expectedKeyValues := toGetValues(keyValues)
- if !testPutValues(tc, bucket, keyValues) {
- return false
- }
-
- if !testGetValues(tc, bucket, expectedKeyValues) {
- return false
- }
-
- // Ensure errors returned from the user-supplied ForEach
- // function are returned.
- forEachError := fmt.Errorf("example foreach error")
- err := bucket.ForEach(func(k, v []byte) error {
- return forEachError
- })
- if err != forEachError {
- tc.t.Errorf("ForEach: inner function error not "+
- "returned - got %v, want %v", err, forEachError)
- 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 {
- wantV, found := lookupKey(k, expectedKeyValues)
- if !found {
- return fmt.Errorf("ForEach: key '%s' should "+
- "exist", k)
- }
-
- if !reflect.DeepEqual(v, wantV) {
- return fmt.Errorf("ForEach: value for key '%s' "+
- "does not match - got %s, want %s", k,
- v, wantV)
- }
-
- keysFound[string(k)] = struct{}{}
- return nil
- })
- if err != nil {
- tc.t.Errorf("%v", err)
- return false
- }
-
- // Ensure all keys were iterated.
- for _, item := range keyValues {
- if _, ok := keysFound[string(item.key)]; !ok {
- tc.t.Errorf("ForEach: key '%s' was not iterated "+
- "when it should have been", item.key)
- 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 errors returned from the user-supplied ForEachBucket
- // function are returned.
- err = bucket.ForEachBucket(func(k []byte) error {
- return forEachError
- })
- if err != forEachError {
- tc.t.Errorf("ForEachBucket: inner function error not "+
- "returned - got %v, want %v", err, forEachError)
- return false
- }
-
- // Ensure creating a bucket that already exists fails with the
- // expected error.
- wantErrCode := database.ErrBucketExists
- _, err = bucket.CreateBucket(testBucketName)
- if !checkDbError(tc.t, "CreateBucket", err, wantErrCode) {
- 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 an 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.
- wantErrCode = database.ErrBucketNotFound
- err = bucket.DeleteBucket(testBucketName)
- if !checkDbError(tc.t, "DeleteBucket", err, wantErrCode) {
- 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
- }
-
- // Ensure the cursor interface works as expected.
- if !testCursorInterface(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.
- testName := "unwritable tx put"
- wantErrCode := database.ErrTxNotWritable
- failBytes := []byte("fail")
- err := bucket.Put(failBytes, failBytes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Delete should fail with bucket that is not writable.
- testName = "unwritable tx delete"
- err = bucket.Delete(failBytes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // CreateBucket should fail with bucket that is not writable.
- testName = "unwritable tx create bucket"
- _, err = bucket.CreateBucket(failBytes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // CreateBucketIfNotExists should fail with bucket that is not
- // writable.
- testName = "unwritable tx create bucket if not exists"
- _, err = bucket.CreateBucketIfNotExists(failBytes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // DeleteBucket should fail with bucket that is not writable.
- testName = "unwritable tx delete bucket"
- err = bucket.DeleteBucket(failBytes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure the cursor interface works as expected with read-only
- // buckets.
- if !testCursorInterface(tc, bucket) {
- return false
- }
- }
-
- return true
-}
-
-// rollbackOnPanic rolls the passed transaction back if the code in the calling
-// function panics. This is useful in case the tests unexpectedly panic which
-// would leave any manually created transactions with the database mutex locked
-// thereby leading to a deadlock and masking the real reason for the panic. It
-// also logs a test error and repanics so the original panic can be traced.
-func rollbackOnPanic(t *testing.T, tx database.Tx) {
- if err := recover(); err != nil {
- t.Errorf("Unexpected panic: %v", err)
- _ = tx.Rollback()
- panic(err)
- }
-}
-
-// testMetadataManualTxInterface ensures that the manual transactions metadata
-// interface works as expected.
-func testMetadataManualTxInterface(tc *testContext) 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 committed or rolled
- // back depending on the flag.
- bucket1Name := []byte("bucket1")
- populateValues := func(writable, rollback bool, putValues []keyPair) bool {
- tx, err := tc.db.Begin(writable)
- if err != nil {
- tc.t.Errorf("Begin: unexpected error %v", err)
- return false
- }
- defer rollbackOnPanic(tc.t, tx)
-
- metadataBucket := tx.Metadata()
- if metadataBucket == nil {
- tc.t.Errorf("Metadata: unexpected nil bucket")
- _ = tx.Rollback()
- return false
- }
-
- bucket1 := metadataBucket.Bucket(bucket1Name)
- if bucket1 == nil {
- tc.t.Errorf("Bucket1: unexpected nil bucket")
- return false
- }
-
- tc.isWritable = writable
- if !testBucketInterface(tc, bucket1) {
- _ = tx.Rollback()
- return false
- }
-
- if !writable {
- // The transaction is not writable, so it should fail
- // the commit.
- testName := "unwritable tx commit"
- wantErrCode := database.ErrTxNotWritable
- err := tx.Commit()
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- _ = tx.Rollback()
- return false
- }
- } else {
- if !testPutValues(tc, bucket1, 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 []keyPair) bool {
- tx, err := tc.db.Begin(false)
- if err != nil {
- tc.t.Errorf("Begin: unexpected error %v", err)
- return false
- }
- defer rollbackOnPanic(tc.t, tx)
-
- metadataBucket := tx.Metadata()
- if metadataBucket == nil {
- tc.t.Errorf("Metadata: unexpected nil bucket")
- _ = tx.Rollback()
- return false
- }
-
- bucket1 := metadataBucket.Bucket(bucket1Name)
- if bucket1 == nil {
- tc.t.Errorf("Bucket1: unexpected nil bucket")
- return false
- }
-
- if !testGetValues(tc, bucket1, 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 []keyPair) bool {
- tx, err := tc.db.Begin(true)
- if err != nil {
-
- }
- defer rollbackOnPanic(tc.t, tx)
-
- metadataBucket := tx.Metadata()
- if metadataBucket == nil {
- tc.t.Errorf("Metadata: unexpected nil bucket")
- _ = tx.Rollback()
- return false
- }
-
- bucket1 := metadataBucket.Bucket(bucket1Name)
- if bucket1 == nil {
- tc.t.Errorf("Bucket1: unexpected nil bucket")
- return false
- }
-
- // Delete the keys and ensure they were deleted.
- if !testDeleteValues(tc, bucket1, values) {
- _ = tx.Rollback()
- return false
- }
- if !testGetValues(tc, bucket1, 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 = []keyPair{
- {[]byte("umtxkey1"), []byte("foo1")},
- {[]byte("umtxkey2"), []byte("foo2")},
- {[]byte("umtxkey3"), []byte("foo3")},
- {[]byte("umtxkey4"), nil},
- }
-
- // 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(toGetValues(keyValues)) {
- return false
- }
-
- // Clean up the keys.
- if !deleteValues(keyValues) {
- return false
- }
-
- return true
-}
-
-// testManagedTxPanics ensures calling Rollback of Commit inside a managed
-// transaction panics.
-func testManagedTxPanics(tc *testContext) bool {
- testPanic := func(fn func()) (paniced bool) {
- // Setup a defer to catch the expected panic and update the
- // return variable.
- defer func() {
- if err := recover(); err != nil {
- paniced = true
- }
- }()
-
- fn()
- return false
- }
-
- // Ensure calling Commit on a managed read-only transaction panics.
- paniced := testPanic(func() {
- tc.db.View(func(tx database.Tx) error {
- tx.Commit()
- return nil
- })
- })
- if !paniced {
- tc.t.Error("Commit called inside View did not panic")
- return false
- }
-
- // Ensure calling Rollback on a managed read-only transaction panics.
- paniced = testPanic(func() {
- tc.db.View(func(tx database.Tx) error {
- tx.Rollback()
- return nil
- })
- })
- if !paniced {
- tc.t.Error("Rollback called inside View did not panic")
- return false
- }
-
- // Ensure calling Commit on a managed read-write transaction panics.
- paniced = testPanic(func() {
- tc.db.Update(func(tx database.Tx) error {
- tx.Commit()
- return nil
- })
- })
- if !paniced {
- tc.t.Error("Commit called inside Update did not panic")
- return false
- }
-
- // Ensure calling Rollback on a managed read-write transaction panics.
- paniced = testPanic(func() {
- tc.db.Update(func(tx database.Tx) error {
- tx.Rollback()
- return nil
- })
- })
- if !paniced {
- tc.t.Error("Rollback called inside Update did not panic")
- return false
- }
-
- return true
-}
-
-// testMetadataTxInterface tests all facets of the managed read/write and
-// manual transaction metadata interfaces as well as the bucket interfaces under
-// them.
-func testMetadataTxInterface(tc *testContext) bool {
- if !testManagedTxPanics(tc) {
- return false
- }
-
- bucket1Name := []byte("bucket1")
- err := tc.db.Update(func(tx database.Tx) error {
- _, err := tx.Metadata().CreateBucket(bucket1Name)
- return err
- })
- if err != nil {
- tc.t.Errorf("Update: unexpected error creating bucket: %v", err)
- return false
- }
-
- if !testMetadataManualTxInterface(tc) {
- return false
- }
-
- // keyValues holds the keys and values to use when putting values
- // into a bucket.
- keyValues := []keyPair{
- {[]byte("mtxkey1"), []byte("foo1")},
- {[]byte("mtxkey2"), []byte("foo2")},
- {[]byte("mtxkey3"), []byte("foo3")},
- {[]byte("mtxkey4"), nil},
- }
-
- // Test the bucket interface via a managed read-only transaction.
- err = tc.db.View(func(tx database.Tx) error {
- metadataBucket := tx.Metadata()
- if metadataBucket == nil {
- return fmt.Errorf("Metadata: unexpected nil bucket")
- }
-
- bucket1 := metadataBucket.Bucket(bucket1Name)
- if bucket1 == nil {
- return fmt.Errorf("Bucket1: unexpected nil bucket")
- }
-
- tc.isWritable = false
- if !testBucketInterface(tc, bucket1) {
- return errSubTestFail
- }
-
- return nil
- })
- if err != nil {
- if err != errSubTestFail {
- 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 = tc.db.View(func(tx database.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 = tc.db.Update(func(tx database.Tx) error {
- metadataBucket := tx.Metadata()
- if metadataBucket == nil {
- return fmt.Errorf("Metadata: unexpected nil bucket")
- }
-
- bucket1 := metadataBucket.Bucket(bucket1Name)
- if bucket1 == nil {
- return fmt.Errorf("Bucket1: unexpected nil bucket")
- }
-
- tc.isWritable = true
- if !testBucketInterface(tc, bucket1) {
- return errSubTestFail
- }
-
- if !testPutValues(tc, bucket1, 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 not have been stored due to the forced
- // rollback above were not actually stored.
- err = tc.db.View(func(tx database.Tx) error {
- metadataBucket := tx.Metadata()
- if metadataBucket == nil {
- return fmt.Errorf("Metadata: unexpected nil bucket")
- }
-
- if !testGetValues(tc, metadataBucket, 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 = tc.db.Update(func(tx database.Tx) error {
- metadataBucket := tx.Metadata()
- if metadataBucket == nil {
- return fmt.Errorf("Metadata: unexpected nil bucket")
- }
-
- bucket1 := metadataBucket.Bucket(bucket1Name)
- if bucket1 == nil {
- return fmt.Errorf("Bucket1: unexpected nil bucket")
- }
-
- if !testPutValues(tc, bucket1, 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 = tc.db.View(func(tx database.Tx) error {
- metadataBucket := tx.Metadata()
- if metadataBucket == nil {
- return fmt.Errorf("Metadata: unexpected nil bucket")
- }
-
- bucket1 := metadataBucket.Bucket(bucket1Name)
- if bucket1 == nil {
- return fmt.Errorf("Bucket1: unexpected nil bucket")
- }
-
- if !testGetValues(tc, bucket1, toGetValues(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 = tc.db.Update(func(tx database.Tx) error {
- metadataBucket := tx.Metadata()
- if metadataBucket == nil {
- return fmt.Errorf("Metadata: unexpected nil bucket")
- }
-
- bucket1 := metadataBucket.Bucket(bucket1Name)
- if bucket1 == nil {
- return fmt.Errorf("Bucket1: unexpected nil bucket")
- }
-
- if !testDeleteValues(tc, bucket1, keyValues) {
- return errSubTestFail
- }
-
- return nil
- })
- if err != nil {
- if err != errSubTestFail {
- tc.t.Errorf("%v", err)
- }
- return false
- }
-
- return true
-}
-
-// testFetchBlockIOMissing ensures that all of the block retrieval API functions
-// work as expected when requesting blocks that don't exist.
-func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool {
- wantErrCode := database.ErrBlockNotFound
-
- // ---------------------
- // Non-bulk Block IO API
- // ---------------------
-
- // Test the individual block APIs one block at a time to ensure they
- // return the expected error. Also, build the data needed to test the
- // bulk APIs below while looping.
- allBlockHashes := make([]chainhash.Hash, len(tc.blocks))
- allBlockRegions := make([]database.BlockRegion, len(tc.blocks))
- for i, block := range tc.blocks {
- blockHash := block.Hash()
- allBlockHashes[i] = *blockHash
-
- txLocs, err := block.TxLoc()
- if err != nil {
- tc.t.Errorf("block.TxLoc(%d): unexpected error: %v", i,
- err)
- return false
- }
-
- // Ensure FetchBlock returns expected error.
- testName := fmt.Sprintf("FetchBlock #%d on missing block", i)
- _, err = tx.FetchBlock(blockHash)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure FetchBlockHeader returns expected error.
- testName = fmt.Sprintf("FetchBlockHeader #%d on missing block",
- i)
- _, err = tx.FetchBlockHeader(blockHash)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure the first transaction fetched as a block region from
- // the database returns the expected error.
- region := database.BlockRegion{
- Hash: blockHash,
- Offset: uint32(txLocs[0].TxStart),
- Len: uint32(txLocs[0].TxLen),
- }
- allBlockRegions[i] = region
- _, err = tx.FetchBlockRegion(®ion)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure HasBlock returns false.
- hasBlock, err := tx.HasBlock(blockHash)
- if err != nil {
- tc.t.Errorf("HasBlock #%d: unexpected err: %v", i, err)
- return false
- }
- if hasBlock {
- tc.t.Errorf("HasBlock #%d: should not have block", i)
- return false
- }
- }
-
- // -----------------
- // Bulk Block IO API
- // -----------------
-
- // Ensure FetchBlocks returns expected error.
- testName := "FetchBlocks on missing blocks"
- _, err := tx.FetchBlocks(allBlockHashes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure FetchBlockHeaders returns expected error.
- testName = "FetchBlockHeaders on missing blocks"
- _, err = tx.FetchBlockHeaders(allBlockHashes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure FetchBlockRegions returns expected error.
- testName = "FetchBlockRegions on missing blocks"
- _, err = tx.FetchBlockRegions(allBlockRegions)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure HasBlocks returns false for all blocks.
- hasBlocks, err := tx.HasBlocks(allBlockHashes)
- if err != nil {
- tc.t.Errorf("HasBlocks: unexpected err: %v", err)
- }
- for i, hasBlock := range hasBlocks {
- if hasBlock {
- tc.t.Errorf("HasBlocks #%d: should not have block", i)
- return false
- }
- }
-
- return true
-}
-
-// testFetchBlockIO ensures all of the block retrieval API functions work as
-// expected for the provide set of blocks. The blocks must already be stored in
-// the database, or at least stored into the the passed transaction. It also
-// tests several error conditions such as ensuring the expected errors are
-// returned when fetching blocks, headers, and regions that don't exist.
-func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
- // ---------------------
- // Non-bulk Block IO API
- // ---------------------
-
- // Test the individual block APIs one block at a time. Also, build the
- // data needed to test the bulk APIs below while looping.
- allBlockHashes := make([]chainhash.Hash, len(tc.blocks))
- allBlockBytes := make([][]byte, len(tc.blocks))
- allBlockTxLocs := make([][]wire.TxLoc, len(tc.blocks))
- allBlockRegions := make([]database.BlockRegion, len(tc.blocks))
- for i, block := range tc.blocks {
- blockHash := block.Hash()
- allBlockHashes[i] = *blockHash
-
- blockBytes, err := block.Bytes()
- if err != nil {
- tc.t.Errorf("block.Bytes(%d): unexpected error: %v", i,
- err)
- return false
- }
- allBlockBytes[i] = blockBytes
-
- txLocs, err := block.TxLoc()
- if err != nil {
- tc.t.Errorf("block.TxLoc(%d): unexpected error: %v", i,
- err)
- return false
- }
- allBlockTxLocs[i] = txLocs
-
- // Ensure the block data fetched from the database matches the
- // expected bytes.
- gotBlockBytes, err := tx.FetchBlock(blockHash)
- if err != nil {
- tc.t.Errorf("FetchBlock(%s): unexpected error: %v",
- blockHash, err)
- return false
- }
- if !bytes.Equal(gotBlockBytes, blockBytes) {
- tc.t.Errorf("FetchBlock(%s): bytes mismatch: got %x, "+
- "want %x", blockHash, gotBlockBytes, blockBytes)
- return false
- }
-
- // Ensure the block header fetched from the database matches the
- // expected bytes.
- wantHeaderBytes := blockBytes[0:wire.MaxBlockHeaderPayload]
- gotHeaderBytes, err := tx.FetchBlockHeader(blockHash)
- if err != nil {
- tc.t.Errorf("FetchBlockHeader(%s): unexpected error: %v",
- blockHash, err)
- return false
- }
- if !bytes.Equal(gotHeaderBytes, wantHeaderBytes) {
- tc.t.Errorf("FetchBlockHeader(%s): bytes mismatch: "+
- "got %x, want %x", blockHash, gotHeaderBytes,
- wantHeaderBytes)
- return false
- }
-
- // Ensure the first transaction fetched as a block region from
- // the database matches the expected bytes.
- region := database.BlockRegion{
- Hash: blockHash,
- Offset: uint32(txLocs[0].TxStart),
- Len: uint32(txLocs[0].TxLen),
- }
- allBlockRegions[i] = region
- endRegionOffset := region.Offset + region.Len
- wantRegionBytes := blockBytes[region.Offset:endRegionOffset]
- gotRegionBytes, err := tx.FetchBlockRegion(®ion)
- if err != nil {
- tc.t.Errorf("FetchBlockRegion(%s): unexpected error: %v",
- blockHash, err)
- return false
- }
- if !bytes.Equal(gotRegionBytes, wantRegionBytes) {
- tc.t.Errorf("FetchBlockRegion(%s): bytes mismatch: "+
- "got %x, want %x", blockHash, gotRegionBytes,
- wantRegionBytes)
- return false
- }
-
- // Ensure the block header fetched from the database matches the
- // expected bytes.
- hasBlock, err := tx.HasBlock(blockHash)
- if err != nil {
- tc.t.Errorf("HasBlock(%s): unexpected error: %v",
- blockHash, err)
- return false
- }
- if !hasBlock {
- tc.t.Errorf("HasBlock(%s): database claims it doesn't "+
- "have the block when it should", blockHash)
- return false
- }
-
- // -----------------------
- // Invalid blocks/regions.
- // -----------------------
-
- // Ensure fetching a block that doesn't exist returns the
- // expected error.
- badBlockHash := &chainhash.Hash{}
- testName := fmt.Sprintf("FetchBlock(%s) invalid block",
- badBlockHash)
- wantErrCode := database.ErrBlockNotFound
- _, err = tx.FetchBlock(badBlockHash)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure fetching a block header that doesn't exist returns
- // the expected error.
- testName = fmt.Sprintf("FetchBlockHeader(%s) invalid block",
- badBlockHash)
- _, err = tx.FetchBlockHeader(badBlockHash)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure fetching a block region in a block that doesn't exist
- // return the expected error.
- testName = fmt.Sprintf("FetchBlockRegion(%s) invalid hash",
- badBlockHash)
- wantErrCode = database.ErrBlockNotFound
- region.Hash = badBlockHash
- region.Offset = ^uint32(0)
- _, err = tx.FetchBlockRegion(®ion)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure fetching a block region that is out of bounds returns
- // the expected error.
- testName = fmt.Sprintf("FetchBlockRegion(%s) invalid region",
- blockHash)
- wantErrCode = database.ErrBlockRegionInvalid
- region.Hash = blockHash
- region.Offset = ^uint32(0)
- _, err = tx.FetchBlockRegion(®ion)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
- }
-
- // -----------------
- // Bulk Block IO API
- // -----------------
-
- // Ensure the bulk block data fetched from the database matches the
- // expected bytes.
- blockData, err := tx.FetchBlocks(allBlockHashes)
- if err != nil {
- tc.t.Errorf("FetchBlocks: unexpected error: %v", err)
- return false
- }
- if len(blockData) != len(allBlockBytes) {
- tc.t.Errorf("FetchBlocks: unexpected number of results - got "+
- "%d, want %d", len(blockData), len(allBlockBytes))
- return false
- }
- for i := 0; i < len(blockData); i++ {
- blockHash := allBlockHashes[i]
- wantBlockBytes := allBlockBytes[i]
- gotBlockBytes := blockData[i]
- if !bytes.Equal(gotBlockBytes, wantBlockBytes) {
- tc.t.Errorf("FetchBlocks(%s): bytes mismatch: got %x, "+
- "want %x", blockHash, gotBlockBytes,
- wantBlockBytes)
- return false
- }
- }
-
- // Ensure the bulk block headers fetched from the database match the
- // expected bytes.
- blockHeaderData, err := tx.FetchBlockHeaders(allBlockHashes)
- if err != nil {
- tc.t.Errorf("FetchBlockHeaders: unexpected error: %v", err)
- return false
- }
- if len(blockHeaderData) != len(allBlockBytes) {
- tc.t.Errorf("FetchBlockHeaders: unexpected number of results "+
- "- got %d, want %d", len(blockHeaderData),
- len(allBlockBytes))
- return false
- }
- for i := 0; i < len(blockHeaderData); i++ {
- blockHash := allBlockHashes[i]
- wantHeaderBytes := allBlockBytes[i][0:wire.MaxBlockHeaderPayload]
- gotHeaderBytes := blockHeaderData[i]
- if !bytes.Equal(gotHeaderBytes, wantHeaderBytes) {
- tc.t.Errorf("FetchBlockHeaders(%s): bytes mismatch: "+
- "got %x, want %x", blockHash, gotHeaderBytes,
- wantHeaderBytes)
- return false
- }
- }
-
- // Ensure the first transaction of every block fetched in bulk block
- // regions from the database matches the expected bytes.
- allRegionBytes, err := tx.FetchBlockRegions(allBlockRegions)
- if err != nil {
- tc.t.Errorf("FetchBlockRegions: unexpected error: %v", err)
- return false
-
- }
- if len(allRegionBytes) != len(allBlockRegions) {
- tc.t.Errorf("FetchBlockRegions: unexpected number of results "+
- "- got %d, want %d", len(allRegionBytes),
- len(allBlockRegions))
- return false
- }
- for i, gotRegionBytes := range allRegionBytes {
- region := &allBlockRegions[i]
- endRegionOffset := region.Offset + region.Len
- wantRegionBytes := blockData[i][region.Offset:endRegionOffset]
- if !bytes.Equal(gotRegionBytes, wantRegionBytes) {
- tc.t.Errorf("FetchBlockRegions(%d): bytes mismatch: "+
- "got %x, want %x", i, gotRegionBytes,
- wantRegionBytes)
- return false
- }
- }
-
- // Ensure the bulk determination of whether a set of block hashes are in
- // the database returns true for all loaded blocks.
- hasBlocks, err := tx.HasBlocks(allBlockHashes)
- if err != nil {
- tc.t.Errorf("HasBlocks: unexpected error: %v", err)
- return false
- }
- for i, hasBlock := range hasBlocks {
- if !hasBlock {
- tc.t.Errorf("HasBlocks(%d): should have block", i)
- return false
- }
- }
-
- // -----------------------
- // Invalid blocks/regions.
- // -----------------------
-
- // Ensure fetching blocks for which one doesn't exist returns the
- // expected error.
- testName := "FetchBlocks invalid hash"
- badBlockHashes := make([]chainhash.Hash, len(allBlockHashes)+1)
- copy(badBlockHashes, allBlockHashes)
- badBlockHashes[len(badBlockHashes)-1] = chainhash.Hash{}
- wantErrCode := database.ErrBlockNotFound
- _, err = tx.FetchBlocks(badBlockHashes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure fetching block headers for which one doesn't exist returns the
- // expected error.
- testName = "FetchBlockHeaders invalid hash"
- _, err = tx.FetchBlockHeaders(badBlockHashes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure fetching block regions for which one of blocks doesn't exist
- // returns expected error.
- testName = "FetchBlockRegions invalid hash"
- badBlockRegions := make([]database.BlockRegion, len(allBlockRegions)+1)
- copy(badBlockRegions, allBlockRegions)
- badBlockRegions[len(badBlockRegions)-1].Hash = &chainhash.Hash{}
- wantErrCode = database.ErrBlockNotFound
- _, err = tx.FetchBlockRegions(badBlockRegions)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure fetching block regions that are out of bounds returns the
- // expected error.
- testName = "FetchBlockRegions invalid regions"
- badBlockRegions = badBlockRegions[:len(badBlockRegions)-1]
- for i := range badBlockRegions {
- badBlockRegions[i].Offset = ^uint32(0)
- }
- wantErrCode = database.ErrBlockRegionInvalid
- _, err = tx.FetchBlockRegions(badBlockRegions)
- return checkDbError(tc.t, testName, err, wantErrCode)
-}
-
-// testBlockIOTxInterface ensures that the block IO interface works as expected
-// for both managed read/write and manual transactions. This function leaves
-// all of the stored blocks in the database.
-func testBlockIOTxInterface(tc *testContext) bool {
- // Ensure attempting to store a block with a read-only transaction fails
- // with the expected error.
- err := tc.db.View(func(tx database.Tx) error {
- wantErrCode := database.ErrTxNotWritable
- for i, block := range tc.blocks {
- testName := fmt.Sprintf("StoreBlock(%d) on ro tx", i)
- err := tx.StoreBlock(block)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return errSubTestFail
- }
- }
-
- return nil
- })
- if err != nil {
- if err != errSubTestFail {
- tc.t.Errorf("%v", err)
- }
- return false
- }
-
- // Populate the database with loaded blocks and ensure all of the data
- // fetching APIs work properly on them within the transaction before a
- // commit or rollback. Then, force a rollback so the code below can
- // ensure none of the data actually gets stored.
- forceRollbackError := fmt.Errorf("force rollback")
- err = tc.db.Update(func(tx database.Tx) error {
- // Store all blocks in the same transaction.
- for i, block := range tc.blocks {
- err := tx.StoreBlock(block)
- if err != nil {
- tc.t.Errorf("StoreBlock #%d: unexpected error: "+
- "%v", i, err)
- return errSubTestFail
- }
- }
-
- // Ensure attempting to store the same block again, before the
- // transaction has been committed, returns the expected error.
- wantErrCode := database.ErrBlockExists
- for i, block := range tc.blocks {
- testName := fmt.Sprintf("duplicate block entry #%d "+
- "(before commit)", i)
- err := tx.StoreBlock(block)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return errSubTestFail
- }
- }
-
- // Ensure that all data fetches from the stored blocks before
- // the transaction has been committed work as expected.
- if !testFetchBlockIO(tc, tx) {
- return errSubTestFail
- }
-
- 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 rollback was successful
- err = tc.db.View(func(tx database.Tx) error {
- if !testFetchBlockIOMissing(tc, tx) {
- return errSubTestFail
- }
- return nil
- })
- if err != nil {
- if err != errSubTestFail {
- tc.t.Errorf("%v", err)
- }
- return false
- }
-
- // Populate the database with loaded blocks and ensure all of the data
- // fetching APIs work properly.
- err = tc.db.Update(func(tx database.Tx) error {
- // Store a bunch of blocks in the same transaction.
- for i, block := range tc.blocks {
- err := tx.StoreBlock(block)
- if err != nil {
- tc.t.Errorf("StoreBlock #%d: unexpected error: "+
- "%v", i, err)
- return errSubTestFail
- }
- }
-
- // Ensure attempting to store the same block again while in the
- // same transaction, but before it has been committed, returns
- // the expected error.
- for i, block := range tc.blocks {
- testName := fmt.Sprintf("duplicate block entry #%d "+
- "(before commit)", i)
- wantErrCode := database.ErrBlockExists
- err := tx.StoreBlock(block)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return errSubTestFail
- }
- }
-
- // Ensure that all data fetches from the stored blocks before
- // the transaction has been committed work as expected.
- if !testFetchBlockIO(tc, tx) {
- return errSubTestFail
- }
-
- return nil
- })
- if err != nil {
- if err != errSubTestFail {
- tc.t.Errorf("%v", err)
- }
- return false
- }
-
- // Ensure all data fetch tests work as expected using a managed
- // read-only transaction after the data was successfully committed
- // above.
- err = tc.db.View(func(tx database.Tx) error {
- if !testFetchBlockIO(tc, tx) {
- return errSubTestFail
- }
-
- return nil
- })
- if err != nil {
- if err != errSubTestFail {
- tc.t.Errorf("%v", err)
- }
- return false
- }
-
- // Ensure all data fetch tests work as expected using a managed
- // read-write transaction after the data was successfully committed
- // above.
- err = tc.db.Update(func(tx database.Tx) error {
- if !testFetchBlockIO(tc, tx) {
- return errSubTestFail
- }
-
- // Ensure attempting to store existing blocks again returns the
- // expected error. Note that this is different from the
- // previous version since this is a new transaction after the
- // blocks have been committed.
- wantErrCode := database.ErrBlockExists
- for i, block := range tc.blocks {
- testName := fmt.Sprintf("duplicate block entry #%d "+
- "(before commit)", i)
- err := tx.StoreBlock(block)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return errSubTestFail
- }
- }
-
- return nil
- })
- if err != nil {
- if err != errSubTestFail {
- tc.t.Errorf("%v", err)
- }
- return false
- }
-
- return true
-}
-
-// testClosedTxInterface ensures that both the metadata and block IO API
-// functions behave as expected when attempted against a closed transaction.
-func testClosedTxInterface(tc *testContext, tx database.Tx) bool {
- wantErrCode := database.ErrTxClosed
- bucket := tx.Metadata()
- cursor := tx.Metadata().Cursor()
- bucketName := []byte("closedtxbucket")
- keyName := []byte("closedtxkey")
-
- // ------------
- // Metadata API
- // ------------
-
- // Ensure that attempting to get an existing bucket returns nil when the
- // transaction is closed.
- if b := bucket.Bucket(bucketName); b != nil {
- tc.t.Errorf("Bucket: did not return nil on closed tx")
- return false
- }
-
- // Ensure CreateBucket returns expected error.
- testName := "CreateBucket on closed tx"
- _, err := bucket.CreateBucket(bucketName)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure CreateBucketIfNotExists returns expected error.
- testName = "CreateBucketIfNotExists on closed tx"
- _, err = bucket.CreateBucketIfNotExists(bucketName)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure Delete returns expected error.
- testName = "Delete on closed tx"
- err = bucket.Delete(keyName)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure DeleteBucket returns expected error.
- testName = "DeleteBucket on closed tx"
- err = bucket.DeleteBucket(bucketName)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure ForEach returns expected error.
- testName = "ForEach on closed tx"
- err = bucket.ForEach(nil)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure ForEachBucket returns expected error.
- testName = "ForEachBucket on closed tx"
- err = bucket.ForEachBucket(nil)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure Get returns expected error.
- testName = "Get on closed tx"
- if k := bucket.Get(keyName); k != nil {
- tc.t.Errorf("Get: did not return nil on closed tx")
- return false
- }
-
- // Ensure Put returns expected error.
- testName = "Put on closed tx"
- err = bucket.Put(keyName, []byte("test"))
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // -------------------
- // Metadata Cursor API
- // -------------------
-
- // Ensure attempting to get a bucket from a cursor on a closed tx gives
- // back nil.
- if b := cursor.Bucket(); b != nil {
- tc.t.Error("Cursor.Bucket: returned non-nil on closed tx")
- return false
- }
-
- // Ensure Cursor.Delete returns expected error.
- testName = "Cursor.Delete on closed tx"
- err = cursor.Delete()
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure Cursor.First on a closed tx returns false and nil key/value.
- if cursor.First() {
- tc.t.Error("Cursor.First: claims ok on closed tx")
- return false
- }
- if cursor.Key() != nil || cursor.Value() != nil {
- tc.t.Error("Cursor.First: key and/or value are not nil on " +
- "closed tx")
- return false
- }
-
- // Ensure Cursor.Last on a closed tx returns false and nil key/value.
- if cursor.Last() {
- tc.t.Error("Cursor.Last: claims ok on closed tx")
- return false
- }
- if cursor.Key() != nil || cursor.Value() != nil {
- tc.t.Error("Cursor.Last: key and/or value are not nil on " +
- "closed tx")
- return false
- }
-
- // Ensure Cursor.Next on a closed tx returns false and nil key/value.
- if cursor.Next() {
- tc.t.Error("Cursor.Next: claims ok on closed tx")
- return false
- }
- if cursor.Key() != nil || cursor.Value() != nil {
- tc.t.Error("Cursor.Next: key and/or value are not nil on " +
- "closed tx")
- return false
- }
-
- // Ensure Cursor.Prev on a closed tx returns false and nil key/value.
- if cursor.Prev() {
- tc.t.Error("Cursor.Prev: claims ok on closed tx")
- return false
- }
- if cursor.Key() != nil || cursor.Value() != nil {
- tc.t.Error("Cursor.Prev: key and/or value are not nil on " +
- "closed tx")
- return false
- }
-
- // Ensure Cursor.Seek on a closed tx returns false and nil key/value.
- if cursor.Seek([]byte{}) {
- tc.t.Error("Cursor.Seek: claims ok on closed tx")
- return false
- }
- if cursor.Key() != nil || cursor.Value() != nil {
- tc.t.Error("Cursor.Seek: key and/or value are not nil on " +
- "closed tx")
- return false
- }
-
- // ---------------------
- // Non-bulk Block IO API
- // ---------------------
-
- // Test the individual block APIs one block at a time to ensure they
- // return the expected error. Also, build the data needed to test the
- // bulk APIs below while looping.
- allBlockHashes := make([]chainhash.Hash, len(tc.blocks))
- allBlockRegions := make([]database.BlockRegion, len(tc.blocks))
- for i, block := range tc.blocks {
- blockHash := block.Hash()
- allBlockHashes[i] = *blockHash
-
- txLocs, err := block.TxLoc()
- if err != nil {
- tc.t.Errorf("block.TxLoc(%d): unexpected error: %v", i,
- err)
- return false
- }
-
- // Ensure StoreBlock returns expected error.
- testName = "StoreBlock on closed tx"
- err = tx.StoreBlock(block)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure FetchBlock returns expected error.
- testName = fmt.Sprintf("FetchBlock #%d on closed tx", i)
- _, err = tx.FetchBlock(blockHash)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure FetchBlockHeader returns expected error.
- testName = fmt.Sprintf("FetchBlockHeader #%d on closed tx", i)
- _, err = tx.FetchBlockHeader(blockHash)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure the first transaction fetched as a block region from
- // the database returns the expected error.
- region := database.BlockRegion{
- Hash: blockHash,
- Offset: uint32(txLocs[0].TxStart),
- Len: uint32(txLocs[0].TxLen),
- }
- allBlockRegions[i] = region
- _, err = tx.FetchBlockRegion(®ion)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure HasBlock returns expected error.
- testName = fmt.Sprintf("HasBlock #%d on closed tx", i)
- _, err = tx.HasBlock(blockHash)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
- }
-
- // -----------------
- // Bulk Block IO API
- // -----------------
-
- // Ensure FetchBlocks returns expected error.
- testName = "FetchBlocks on closed tx"
- _, err = tx.FetchBlocks(allBlockHashes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure FetchBlockHeaders returns expected error.
- testName = "FetchBlockHeaders on closed tx"
- _, err = tx.FetchBlockHeaders(allBlockHashes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure FetchBlockRegions returns expected error.
- testName = "FetchBlockRegions on closed tx"
- _, err = tx.FetchBlockRegions(allBlockRegions)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // Ensure HasBlocks returns expected error.
- testName = "HasBlocks on closed tx"
- _, err = tx.HasBlocks(allBlockHashes)
- if !checkDbError(tc.t, testName, err, wantErrCode) {
- return false
- }
-
- // ---------------
- // Commit/Rollback
- // ---------------
-
- // Ensure that attempting to rollback or commit a transaction that is
- // already closed returns the expected error.
- err = tx.Rollback()
- if !checkDbError(tc.t, "closed tx rollback", err, wantErrCode) {
- return false
- }
- err = tx.Commit()
- return checkDbError(tc.t, "closed tx commit", err, wantErrCode)
-}
-
-// testTxClosed ensures that both the metadata and block IO API functions behave
-// as expected when attempted against both read-only and read-write
-// transactions.
-func testTxClosed(tc *testContext) bool {
- bucketName := []byte("closedtxbucket")
- keyName := []byte("closedtxkey")
-
- // Start a transaction, create a bucket and key used for testing, and
- // immediately perform a commit on it so it is closed.
- tx, err := tc.db.Begin(true)
- if err != nil {
- tc.t.Errorf("Begin(true): unexpected error: %v", err)
- return false
- }
- defer rollbackOnPanic(tc.t, tx)
- if _, err := tx.Metadata().CreateBucket(bucketName); err != nil {
- tc.t.Errorf("CreateBucket: unexpected error: %v", err)
- return false
- }
- if err := tx.Metadata().Put(keyName, []byte("test")); err != nil {
- tc.t.Errorf("Put: unexpected error: %v", err)
- return false
- }
- if err := tx.Commit(); err != nil {
- tc.t.Errorf("Commit: unexpected error: %v", err)
- return false
- }
-
- // Ensure invoking all of the functions on the closed read-write
- // transaction behave as expected.
- if !testClosedTxInterface(tc, tx) {
- return false
- }
-
- // Repeat the tests with a rolled-back read-only transaction.
- tx, err = tc.db.Begin(false)
- if err != nil {
- tc.t.Errorf("Begin(false): unexpected error: %v", err)
- return false
- }
- defer rollbackOnPanic(tc.t, tx)
- if err := tx.Rollback(); err != nil {
- tc.t.Errorf("Rollback: unexpected error: %v", err)
- return false
- }
-
- // Ensure invoking all of the functions on the closed read-only
- // transaction behave as expected.
- return testClosedTxInterface(tc, tx)
-}
-
-// testConcurrecy ensure the database properly supports concurrent readers and
-// only a single writer. It also ensures views act as snapshots at the time
-// they are acquired.
-func testConcurrecy(tc *testContext) bool {
- // sleepTime is how long each of the concurrent readers should sleep to
- // aid in detection of whether or not the data is actually being read
- // concurrently. It starts with a sane lower bound.
- var sleepTime = time.Millisecond * 250
-
- // Determine about how long it takes for a single block read. When it's
- // longer than the default minimum sleep time, adjust the sleep time to
- // help prevent durations that are too short which would cause erroneous
- // test failures on slower systems.
- startTime := time.Now()
- err := tc.db.View(func(tx database.Tx) error {
- _, err := tx.FetchBlock(tc.blocks[0].Hash())
- return err
- })
- if err != nil {
- tc.t.Errorf("Unexpected error in view: %v", err)
- return false
- }
- elapsed := time.Since(startTime)
- if sleepTime < elapsed {
- sleepTime = elapsed
- }
- tc.t.Logf("Time to load block 0: %v, using sleep time: %v", elapsed,
- sleepTime)
-
- // reader takes a block number to load and channel to return the result
- // of the operation on. It is used below to launch multiple concurrent
- // readers.
- numReaders := len(tc.blocks)
- resultChan := make(chan bool, numReaders)
- reader := func(blockNum int) {
- err := tc.db.View(func(tx database.Tx) error {
- time.Sleep(sleepTime)
- _, err := tx.FetchBlock(tc.blocks[blockNum].Hash())
- return err
- })
- if err != nil {
- tc.t.Errorf("Unexpected error in concurrent view: %v",
- err)
- resultChan <- false
- }
- resultChan <- true
- }
-
- // Start up several concurrent readers for the same block and wait for
- // the results.
- startTime = time.Now()
- for i := 0; i < numReaders; i++ {
- go reader(0)
- }
- for i := 0; i < numReaders; i++ {
- if result := <-resultChan; !result {
- return false
- }
- }
- elapsed = time.Since(startTime)
- tc.t.Logf("%d concurrent reads of same block elapsed: %v", numReaders,
- elapsed)
-
- // Consider it a failure if it took longer than half the time it would
- // take with no concurrency.
- if elapsed > sleepTime*time.Duration(numReaders/2) {
- tc.t.Errorf("Concurrent views for same block did not appear to "+
- "run simultaneously: elapsed %v", elapsed)
- return false
- }
-
- // Start up several concurrent readers for different blocks and wait for
- // the results.
- startTime = time.Now()
- for i := 0; i < numReaders; i++ {
- go reader(i)
- }
- for i := 0; i < numReaders; i++ {
- if result := <-resultChan; !result {
- return false
- }
- }
- elapsed = time.Since(startTime)
- tc.t.Logf("%d concurrent reads of different blocks elapsed: %v",
- numReaders, elapsed)
-
- // Consider it a failure if it took longer than half the time it would
- // take with no concurrency.
- if elapsed > sleepTime*time.Duration(numReaders/2) {
- tc.t.Errorf("Concurrent views for different blocks did not "+
- "appear to run simultaneously: elapsed %v", elapsed)
- return false
- }
-
- // Start up a few readers and wait for them to acquire views. Each
- // reader waits for a signal from the writer to be finished to ensure
- // that the data written by the writer is not seen by the view since it
- // was started before the data was set.
- concurrentKey := []byte("notthere")
- concurrentVal := []byte("someval")
- started := make(chan struct{})
- writeComplete := make(chan struct{})
- reader = func(blockNum int) {
- err := tc.db.View(func(tx database.Tx) error {
- started <- struct{}{}
-
- // Wait for the writer to complete.
- <-writeComplete
-
- // Since this reader was created before the write took
- // place, the data it added should not be visible.
- val := tx.Metadata().Get(concurrentKey)
- if val != nil {
- return fmt.Errorf("%s should not be visible",
- concurrentKey)
- }
- return nil
- })
- if err != nil {
- tc.t.Errorf("Unexpected error in concurrent view: %v",
- err)
- resultChan <- false
- }
- resultChan <- true
- }
- for i := 0; i < numReaders; i++ {
- go reader(0)
- }
- for i := 0; i < numReaders; i++ {
- <-started
- }
-
- // All readers are started and waiting for completion of the writer.
- // Set some data the readers are expecting to not find and signal the
- // readers the write is done by closing the writeComplete channel.
- err = tc.db.Update(func(tx database.Tx) error {
- return tx.Metadata().Put(concurrentKey, concurrentVal)
- })
- if err != nil {
- tc.t.Errorf("Unexpected error in update: %v", err)
- return false
- }
- close(writeComplete)
-
- // Wait for reader results.
- for i := 0; i < numReaders; i++ {
- if result := <-resultChan; !result {
- return false
- }
- }
-
- // Start a few writers and ensure the total time is at least the
- // writeSleepTime * numWriters. This ensures only one write transaction
- // can be active at a time.
- writeSleepTime := time.Millisecond * 250
- writer := func() {
- err := tc.db.Update(func(tx database.Tx) error {
- time.Sleep(writeSleepTime)
- return nil
- })
- if err != nil {
- tc.t.Errorf("Unexpected error in concurrent view: %v",
- err)
- resultChan <- false
- }
- resultChan <- true
- }
- numWriters := 3
- startTime = time.Now()
- for i := 0; i < numWriters; i++ {
- go writer()
- }
- for i := 0; i < numWriters; i++ {
- if result := <-resultChan; !result {
- return false
- }
- }
- elapsed = time.Since(startTime)
- tc.t.Logf("%d concurrent writers elapsed using sleep time %v: %v",
- numWriters, writeSleepTime, elapsed)
-
- // The total time must have been at least the sum of all sleeps if the
- // writes blocked properly.
- if elapsed < writeSleepTime*time.Duration(numWriters) {
- tc.t.Errorf("Concurrent writes appeared to run simultaneously: "+
- "elapsed %v", elapsed)
- return false
- }
-
- return true
-}
-
-// testConcurrentClose ensures that closing the database with open transactions
-// blocks until the transactions are finished.
-//
-// The database will be closed upon returning from this function.
-func testConcurrentClose(tc *testContext) bool {
- // Start up a few readers and wait for them to acquire views. Each
- // reader waits for a signal to complete to ensure the transactions stay
- // open until they are explicitly signalled to be closed.
- var activeReaders int32
- numReaders := 3
- started := make(chan struct{})
- finishReaders := make(chan struct{})
- resultChan := make(chan bool, numReaders+1)
- reader := func() {
- err := tc.db.View(func(tx database.Tx) error {
- atomic.AddInt32(&activeReaders, 1)
- started <- struct{}{}
- <-finishReaders
- atomic.AddInt32(&activeReaders, -1)
- return nil
- })
- if err != nil {
- tc.t.Errorf("Unexpected error in concurrent view: %v",
- err)
- resultChan <- false
- }
- resultChan <- true
- }
- for i := 0; i < numReaders; i++ {
- go reader()
- }
- for i := 0; i < numReaders; i++ {
- <-started
- }
-
- // Close the database in a separate goroutine. This should block until
- // the transactions are finished. Once the close has taken place, the
- // dbClosed channel is closed to signal the main goroutine below.
- dbClosed := make(chan struct{})
- go func() {
- started <- struct{}{}
- err := tc.db.Close()
- if err != nil {
- tc.t.Errorf("Unexpected error in concurrent view: %v",
- err)
- resultChan <- false
- }
- close(dbClosed)
- resultChan <- true
- }()
- <-started
-
- // Wait a short period and then signal the reader transactions to
- // finish. When the db closed channel is received, ensure there are no
- // active readers open.
- time.AfterFunc(time.Millisecond*250, func() { close(finishReaders) })
- <-dbClosed
- if nr := atomic.LoadInt32(&activeReaders); nr != 0 {
- tc.t.Errorf("Close did not appear to block with active "+
- "readers: %d active", nr)
- return false
- }
-
- // Wait for all results.
- for i := 0; i < numReaders+1; i++ {
- if result := <-resultChan; !result {
- return false
- }
- }
-
- return true
-}
-
-// testInterface tests performs tests for the various interfaces of the database
-// package which require state in the database for the given database type.
-func testInterface(t *testing.T, db database.DB) {
- // Create a test context to pass around.
- context := testContext{t: t, db: db}
-
- // Load the test blocks and store in the test context for use throughout
- // the tests.
- blocks, err := loadBlocks(t, blockDataFile, blockDataNet)
- if err != nil {
- t.Errorf("loadBlocks: Unexpected error: %v", err)
- return
- }
- context.blocks = blocks
-
- // Test the transaction metadata interface including managed and manual
- // transactions as well as buckets.
- if !testMetadataTxInterface(&context) {
- return
- }
-
- // Test the transaction block IO interface using managed and manual
- // transactions. This function leaves all of the stored blocks in the
- // database since they're used later.
- if !testBlockIOTxInterface(&context) {
- return
- }
-
- // Test all of the transaction interface functions against a closed
- // transaction work as expected.
- if !testTxClosed(&context) {
- return
- }
-
- // Test the database properly supports concurrency.
- if !testConcurrecy(&context) {
- return
- }
-
- // Test that closing the database with open transactions blocks until
- // the transactions are finished.
- //
- // The database will be closed upon returning from this function, so it
- // must be the last thing called.
- testConcurrentClose(&context)
-}