2323 lines
64 KiB
Go
2323 lines
64 KiB
Go
|
// 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"
|
||
|
database "github.com/btcsuite/btcd/database2"
|
||
|
"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 foward 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--
|
||
|
}()
|
||
|
if !testBucketInterface(tc, testBucket) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// 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 commited 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([]wire.ShaHash, len(tc.blocks))
|
||
|
allBlockRegions := make([]database.BlockRegion, len(tc.blocks))
|
||
|
for i, block := range tc.blocks {
|
||
|
blockHash := block.Sha()
|
||
|
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([]wire.ShaHash, 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.Sha()
|
||
|
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 := &wire.ShaHash{}
|
||
|
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([]wire.ShaHash, len(allBlockHashes)+1)
|
||
|
copy(badBlockHashes, allBlockHashes)
|
||
|
badBlockHashes[len(badBlockHashes)-1] = wire.ShaHash{}
|
||
|
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 = &wire.ShaHash{}
|
||
|
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)
|
||
|
if !checkDbError(tc.t, testName, err, wantErrCode) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// 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([]wire.ShaHash, len(tc.blocks))
|
||
|
allBlockRegions := make([]database.BlockRegion, len(tc.blocks))
|
||
|
for i, block := range tc.blocks {
|
||
|
blockHash := block.Sha()
|
||
|
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()
|
||
|
if !checkDbError(tc.t, "closed tx commit", err, wantErrCode) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// 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].Sha())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
if err != nil {
|
||
|
tc.t.Errorf("Unexpected error in view: %v", err)
|
||
|
return false
|
||
|
}
|
||
|
elapsed := time.Now().Sub(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].Sha())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
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.Now().Sub(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.Now().Sub(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 {
|
||
|
err := tx.Metadata().Put(concurrentKey, concurrentVal)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
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.Now().Sub(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)
|
||
|
}
|