[DEV-81] 100% Database Coverage (#110)

* [DEV-81] Overwrite maxOpenFiles for testInterface to force tests to check the LRU-mechanism in openFile

* [DEV-81] Added database.UseLogger test

* [DEV-81] Completed coverage of reconcileDB()

* [DEV-81] Added some tests for dbcache

* [DEV-81] Moved init and UseLogger to separate file to make them more easily-testable + added tests

* [DEV-81] Added tests for deleteFile

* [DEV-81] Added tests to cursor.Delete + made sure it returns error when transaction is not writable

* [DEV-81] Moved database/error_test.go from database_test package to database package + added test for IsErrorCode

* [DEV-81] Added tests for handleRollback error-cases

* [DEV-81] Added tests for cursor.skipPendingUpdates

* [DEV-81] Added tests for various cursor edge-cases

* [DEV-81] tx.putKey no longer returns error, because there is no case when it does

* [DEV-81] Added tests to CreateBucket error cases

* [DEV-81] Added tests to bucket.Get and .Delete error cases + .Delete now returns error on empty key

* [DEV-81] Added test for ForEachBucket

* [DEV-81] Added tests to StoreBlock

* [DEV-81] Added test for deleting a double nested bucket

* [DEV-81] Removed log_test, as it is no longer necessary with the logging system re-design

* [DEV-81] Added test to some of writePendingAndCommit error-cases

* [DEV-81] Update references from btcutil to btcd/util

* [DEV-81] Add tests for dbCacheIterator{.Next(), .Prev(), .Key, .Value()} in cases when iterator is exhausted

* [DEV-81] Added tests for ldbIterator placeholder functions

* [DEV-81] Added test name to Error messsages in TestSkipPendingUpdates

* [DEV-81] Begin writing TestSkipPendingUpdatesCache

* [DEV-81] Added error-cases for DBCache.flush() and DBCache.commitTreaps()

* [DEV-81] Use monkey.patch from bou.ke and not from github

* [DEV-81] Rewrote IsErrorCode in both database and txscript packages to be more concise

* [DEV-81] Rename any database.Tx to dbTx instead of tx - to remove confusion with coin Tx

* [DEV-81] Fix typo

* [DEV-81] Use os.TempDir() instead of /tmp/ to be cross-platform

* [DEV-81] use SimNet for database tests + Error if testDB exists after deleting it

* [DEV-81] Removed useLogger - it's redundant

* [DEV-81] Added comment on how CRC32 checksums are calculated in reconcile_test.go

* [DEV-81] Added comment that explains what setWriteRow does

* [DEV-81] Use constant instead of hard-coded value

* [DEV-81] Fixed some typo's + better formatting
This commit is contained in:
Svarog 2018-11-06 18:18:42 +02:00 committed by stasatdaglabs
parent 60afa37acf
commit 225f349e6a
26 changed files with 1690 additions and 218 deletions

View File

@ -777,7 +777,7 @@ func (p *provisionalNode) pastUTXO(virtual *virtualBlock, db database.DB) (pastU
// Fetch from the database all the transactions for this block's blue set (besides the selected parent)
var blueBlockTransactions []*TxWithBlockHash
transactionCount := 0
err = db.View(func(tx database.Tx) error {
err = db.View(func(dbTx database.Tx) error {
// Precalculate the amount of transactions in this block's blue set, besides the selected parent.
// This is to avoid an attack in which an attacker fabricates a block that will deliberately cause
// a lot of copying, causing a high cost to the whole network.
@ -788,7 +788,7 @@ func (p *provisionalNode) pastUTXO(virtual *virtualBlock, db database.DB) (pastU
continue
}
blueBlock, err := dbFetchBlockByNode(tx, blueBlockNode)
blueBlock, err := dbFetchBlockByNode(dbTx, blueBlockNode)
if err != nil {
return err
}

View File

@ -43,10 +43,10 @@ func (cmd *fetchBlockCmd) Execute(args []string) error {
}
defer db.Close()
return db.View(func(tx database.Tx) error {
return db.View(func(dbTx database.Tx) error {
log.Infof("Fetching block %s", blockHash)
startTime := time.Now()
blockBytes, err := tx.FetchBlock(blockHash)
blockBytes, err := dbTx.FetchBlock(blockHash)
if err != nil {
return err
}

View File

@ -64,7 +64,7 @@ func (cmd *blockRegionCmd) Execute(args []string) error {
}
defer db.Close()
return db.View(func(tx database.Tx) error {
return db.View(func(dbTx database.Tx) error {
log.Infof("Fetching block region %s<%d:%d>", blockHash,
startOffset, startOffset+regionLen-1)
region := database.BlockRegion{
@ -73,7 +73,7 @@ func (cmd *blockRegionCmd) Execute(args []string) error {
Len: uint32(regionLen),
}
startTime := time.Now()
regionBytes, err := tx.FetchBlockRegion(&region)
regionBytes, err := dbTx.FetchBlockRegion(&region)
if err != nil {
return err
}

View File

@ -119,8 +119,8 @@ func (bi *blockImporter) processBlock(serializedBlock []byte) (bool, error) {
// Skip blocks that already exist.
var exists bool
err = bi.db.View(func(tx database.Tx) error {
exists, err = tx.HasBlock(block.Hash())
err = bi.db.View(func(dbTx database.Tx) error {
exists, err = dbTx.HasBlock(block.Hash())
return err
})
if err != nil {
@ -134,8 +134,8 @@ func (bi *blockImporter) processBlock(serializedBlock []byte) (bool, error) {
parentHashes := block.MsgBlock().Header.ParentHashes
for _, parentHash := range parentHashes {
var exists bool
err := bi.db.View(func(tx database.Tx) error {
exists, err = tx.HasBlock(&parentHash)
err := bi.db.View(func(dbTx database.Tx) error {
exists, err = dbTx.HasBlock(&parentHash)
return err
})
if err != nil {
@ -149,8 +149,8 @@ func (bi *blockImporter) processBlock(serializedBlock []byte) (bool, error) {
}
// Put the blocks into the database with no checking of chain rules.
err = bi.db.Update(func(tx database.Tx) error {
return tx.StoreBlock(block)
err = bi.db.Update(func(dbTx database.Tx) error {
return dbTx.StoreBlock(block)
})
if err != nil {
return false, err

View File

@ -41,9 +41,9 @@ func (cmd *headersCmd) Execute(args []string) error {
// the database would keep a metadata index of its own.
blockIdxName := []byte("ffldb-blockidx")
if !headersCfg.Bulk {
err = db.View(func(tx database.Tx) error {
err = db.View(func(dbTx database.Tx) error {
totalHdrs := 0
blockIdxBucket := tx.Metadata().Bucket(blockIdxName)
blockIdxBucket := dbTx.Metadata().Bucket(blockIdxName)
blockIdxBucket.ForEach(func(k, v []byte) error {
totalHdrs++
return nil
@ -54,7 +54,7 @@ func (cmd *headersCmd) Execute(args []string) error {
blockIdxBucket.ForEach(func(k, v []byte) error {
var hash daghash.Hash
copy(hash[:], k)
_, err := tx.FetchBlockHeader(&hash)
_, err := dbTx.FetchBlockHeader(&hash)
if err != nil {
return err
}
@ -69,8 +69,8 @@ func (cmd *headersCmd) Execute(args []string) error {
}
// Bulk load headers.
err = db.View(func(tx database.Tx) error {
blockIdxBucket := tx.Metadata().Bucket(blockIdxName)
err = db.View(func(dbTx database.Tx) error {
blockIdxBucket := dbTx.Metadata().Bucket(blockIdxName)
hashes := make([]daghash.Hash, 0, 500000)
blockIdxBucket.ForEach(func(k, v []byte) error {
var hash daghash.Hash
@ -81,7 +81,7 @@ func (cmd *headersCmd) Execute(args []string) error {
log.Infof("Loading headers for %d blocks...", len(hashes))
startTime := time.Now()
hdrs, err := tx.FetchBlockHeaders(hashes)
hdrs, err := dbTx.FetchBlockHeaders(hashes)
if err != nil {
return err
}

View File

@ -195,3 +195,13 @@ func (e Error) Error() string {
func makeError(c ErrorCode, desc string, err error) Error {
return Error{ErrorCode: c, Description: desc, Err: err}
}
// IsErrorCode returns whether or not the provided error is a script error with
// the provided error code.
func IsErrorCode(err error, c ErrorCode) bool {
if err, ok := err.(Error); ok {
return err.ErrorCode == c
}
return false
}

View File

@ -2,48 +2,46 @@
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package database_test
package database
import (
"errors"
"testing"
"github.com/daglabs/btcd/database"
)
// TestErrorCodeStringer tests the stringized output for the ErrorCode type.
func TestErrorCodeStringer(t *testing.T) {
tests := []struct {
in database.ErrorCode
in ErrorCode
want string
}{
{database.ErrDbTypeRegistered, "ErrDbTypeRegistered"},
{database.ErrDbUnknownType, "ErrDbUnknownType"},
{database.ErrDbDoesNotExist, "ErrDbDoesNotExist"},
{database.ErrDbExists, "ErrDbExists"},
{database.ErrDbNotOpen, "ErrDbNotOpen"},
{database.ErrDbAlreadyOpen, "ErrDbAlreadyOpen"},
{database.ErrInvalid, "ErrInvalid"},
{database.ErrCorruption, "ErrCorruption"},
{database.ErrTxClosed, "ErrTxClosed"},
{database.ErrTxNotWritable, "ErrTxNotWritable"},
{database.ErrBucketNotFound, "ErrBucketNotFound"},
{database.ErrBucketExists, "ErrBucketExists"},
{database.ErrBucketNameRequired, "ErrBucketNameRequired"},
{database.ErrKeyRequired, "ErrKeyRequired"},
{database.ErrKeyTooLarge, "ErrKeyTooLarge"},
{database.ErrValueTooLarge, "ErrValueTooLarge"},
{database.ErrIncompatibleValue, "ErrIncompatibleValue"},
{database.ErrBlockNotFound, "ErrBlockNotFound"},
{database.ErrBlockExists, "ErrBlockExists"},
{database.ErrBlockRegionInvalid, "ErrBlockRegionInvalid"},
{database.ErrDriverSpecific, "ErrDriverSpecific"},
{ErrDbTypeRegistered, "ErrDbTypeRegistered"},
{ErrDbUnknownType, "ErrDbUnknownType"},
{ErrDbDoesNotExist, "ErrDbDoesNotExist"},
{ErrDbExists, "ErrDbExists"},
{ErrDbNotOpen, "ErrDbNotOpen"},
{ErrDbAlreadyOpen, "ErrDbAlreadyOpen"},
{ErrInvalid, "ErrInvalid"},
{ErrCorruption, "ErrCorruption"},
{ErrTxClosed, "ErrTxClosed"},
{ErrTxNotWritable, "ErrTxNotWritable"},
{ErrBucketNotFound, "ErrBucketNotFound"},
{ErrBucketExists, "ErrBucketExists"},
{ErrBucketNameRequired, "ErrBucketNameRequired"},
{ErrKeyRequired, "ErrKeyRequired"},
{ErrKeyTooLarge, "ErrKeyTooLarge"},
{ErrValueTooLarge, "ErrValueTooLarge"},
{ErrIncompatibleValue, "ErrIncompatibleValue"},
{ErrBlockNotFound, "ErrBlockNotFound"},
{ErrBlockExists, "ErrBlockExists"},
{ErrBlockRegionInvalid, "ErrBlockRegionInvalid"},
{ErrDriverSpecific, "ErrDriverSpecific"},
{0xffff, "Unknown ErrorCode (65535)"},
}
// Detect additional error codes that don't have the stringer added.
if len(tests)-1 != int(database.TstNumErrorCodes) {
if len(tests)-1 != int(TstNumErrorCodes) {
t.Errorf("It appears an error code was added without adding " +
"an associated stringer test")
}
@ -64,20 +62,20 @@ func TestError(t *testing.T) {
t.Parallel()
tests := []struct {
in database.Error
in Error
want string
}{
{
database.Error{Description: "some error"},
Error{Description: "some error"},
"some error",
},
{
database.Error{Description: "human-readable error"},
Error{Description: "human-readable error"},
"human-readable error",
},
{
database.Error{
ErrorCode: database.ErrDriverSpecific,
Error{
ErrorCode: ErrDriverSpecific,
Description: "some error",
Err: errors.New("driver-specific error"),
},
@ -95,3 +93,26 @@ func TestError(t *testing.T) {
}
}
}
func TestIsErrorCode(t *testing.T) {
dummyError := errors.New("")
tests := []struct {
err error
code ErrorCode
expectedResult bool
}{
{makeError(ErrBucketExists, "", dummyError), ErrBucketExists, true},
{makeError(ErrBucketExists, "", dummyError), ErrBlockExists, false},
{dummyError, ErrBlockExists, false},
{nil, ErrBlockExists, false},
}
for i, test := range tests {
actualResult := IsErrorCode(test.err, test.code)
if test.expectedResult != actualResult {
t.Errorf("TestIsErrorCode: %d: Expected: %t, but got: %t",
i, test.expectedResult, actualResult)
}
}
}

View File

@ -68,25 +68,25 @@ func Example_basicUsage() {
// Use the Update function of the database to perform a managed
// read-write transaction. The transaction will automatically be rolled
// back if the supplied inner function returns a non-nil error.
err = db.Update(func(tx database.Tx) error {
err = db.Update(func(dbTx database.Tx) error {
// Store a key/value pair directly in the metadata bucket.
// Typically a nested bucket would be used for a given feature,
// but this example is using the metadata bucket directly for
// simplicity.
key := []byte("mykey")
value := []byte("myvalue")
if err := tx.Metadata().Put(key, value); err != nil {
if err := dbTx.Metadata().Put(key, value); err != nil {
return err
}
// Read the key back and ensure it matches.
if !bytes.Equal(tx.Metadata().Get(key), value) {
if !bytes.Equal(dbTx.Metadata().Get(key), value) {
return fmt.Errorf("unexpected value for key '%s'", key)
}
// Create a new nested bucket under the metadata bucket.
nestedBucketKey := []byte("mybucket")
nestedBucket, err := tx.Metadata().CreateBucket(nestedBucketKey)
nestedBucket, err := dbTx.Metadata().CreateBucket(nestedBucketKey)
if err != nil {
return err
}
@ -134,9 +134,9 @@ func Example_blockStorageAndRetrieval() {
// Use the Update function of the database to perform a managed
// read-write transaction and store a genesis block in the database as
// and example.
err = db.Update(func(tx database.Tx) error {
err = db.Update(func(dbTx database.Tx) error {
genesisBlock := dagconfig.MainNetParams.GenesisBlock
return tx.StoreBlock(util.NewBlock(genesisBlock))
return dbTx.StoreBlock(util.NewBlock(genesisBlock))
})
if err != nil {
fmt.Println(err)
@ -146,9 +146,9 @@ func Example_blockStorageAndRetrieval() {
// Use the View function of the database to perform a managed read-only
// transaction and fetch the block stored above.
var loadedBlockBytes []byte
err = db.Update(func(tx database.Tx) error {
err = db.Update(func(dbTx database.Tx) error {
genesisHash := dagconfig.MainNetParams.GenesisHash
blockBytes, err := tx.FetchBlock(genesisHash)
blockBytes, err := dbTx.FetchBlock(genesisHash)
if err != nil {
return err
}

View File

@ -27,9 +27,9 @@ func BenchmarkBlockHeader(b *testing.B) {
}
defer os.RemoveAll(dbPath)
defer db.Close()
err = db.Update(func(tx database.Tx) error {
err = db.Update(func(dbTx database.Tx) error {
block := util.NewBlock(dagconfig.MainNetParams.GenesisBlock)
return tx.StoreBlock(block)
return dbTx.StoreBlock(block)
})
if err != nil {
b.Fatal(err)
@ -37,10 +37,10 @@ func BenchmarkBlockHeader(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
err = db.View(func(tx database.Tx) error {
err = db.View(func(dbTx database.Tx) error {
blockHash := dagconfig.MainNetParams.GenesisHash
for i := 0; i < b.N; i++ {
_, err := tx.FetchBlockHeader(blockHash)
_, err := dbTx.FetchBlockHeader(blockHash)
if err != nil {
return err
}
@ -68,9 +68,9 @@ func BenchmarkBlock(b *testing.B) {
}
defer os.RemoveAll(dbPath)
defer db.Close()
err = db.Update(func(tx database.Tx) error {
err = db.Update(func(dbTx database.Tx) error {
block := util.NewBlock(dagconfig.MainNetParams.GenesisBlock)
return tx.StoreBlock(block)
return dbTx.StoreBlock(block)
})
if err != nil {
b.Fatal(err)
@ -78,10 +78,10 @@ func BenchmarkBlock(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
err = db.View(func(tx database.Tx) error {
err = db.View(func(dbTx database.Tx) error {
blockHash := dagconfig.MainNetParams.GenesisHash
for i := 0; i < b.N; i++ {
_, err := tx.FetchBlock(blockHash)
_, err := dbTx.FetchBlock(blockHash)
if err != nil {
return err
}

View File

@ -116,6 +116,12 @@ type blockStore struct {
// override the value.
maxBlockFileSize uint32
// maxOpenFiles is the max number of open files to maintain in the
// open blocks cache. Note that this does not include the current
// write file, so there will typically be one more than this value open.
// It is defined on the store so the whitebox tests can override the value.
maxOpenFiles int
// The following fields are related to the flat files which hold the
// actual blocks. The number of open files is limited by maxOpenFiles.
//
@ -272,7 +278,7 @@ func (s *blockStore) openFile(fileNum uint32) (*lockableFile, error) {
// therefore should be closed last.
s.lruMutex.Lock()
lruList := s.openBlocksLRU
if lruList.Len() >= maxOpenFiles {
if lruList.Len() >= s.maxOpenFiles {
lruFileNum := lruList.Remove(lruList.Back()).(uint32)
oldBlockFile := s.openBlockFiles[lruFileNum]
@ -752,6 +758,7 @@ func newBlockStore(basePath string, network wire.BitcoinNet) *blockStore {
network: network,
basePath: basePath,
maxBlockFileSize: maxBlockFileSize,
maxOpenFiles: maxOpenFiles,
openBlockFiles: make(map[uint32]*lockableFile),
openBlocksLRU: list.New(),
fileNumToLRUElem: make(map[uint32]*list.Element),

View File

@ -0,0 +1,116 @@
package ffldb
import (
"errors"
"os"
"testing"
"bou.ke/monkey"
"github.com/daglabs/btcd/dagconfig/daghash"
"github.com/daglabs/btcd/database"
"github.com/daglabs/btcd/util"
"github.com/daglabs/btcd/wire"
)
func TestDeleteFile(t *testing.T) {
testBlock := util.NewBlock(wire.NewMsgBlock(
wire.NewBlockHeader(1, []daghash.Hash{}, &daghash.Hash{}, 0, 0)))
tests := []struct {
fileNum uint32
expectedErr bool
}{
{0, false},
{1, true},
}
for _, test := range tests {
func() {
pdb := newTestDb("TestDeleteFile", t)
defer pdb.Close()
err := pdb.Update(func(dbTx database.Tx) error {
dbTx.StoreBlock(testBlock)
return nil
})
if err != nil {
t.Fatalf("TestDeleteFile: Error storing block: %s", err)
}
err = pdb.store.deleteFile(test.fileNum)
if (err != nil) != test.expectedErr {
t.Errorf("TestDeleteFile: %d: Expected error status: %t, but got: %t",
test.fileNum, test.expectedErr, (err != nil))
}
if err == nil {
filePath := blockFilePath(pdb.store.basePath, test.fileNum)
if _, err := os.Stat(filePath); !os.IsNotExist(err) {
t.Errorf("TestDeleteFile: %d: File %s still exists", test.fileNum, filePath)
}
}
}()
}
}
// TestHandleRollbackErrors tests all error-cases in *blockStore.handleRollback().
// The non-error-cases are tested in the more general tests.
// Since handleRollback just logs errors, this test simply causes all error-cases to be hit,
// and makes sure no panic occurs, as well as ensures the writeCursor was updated correctly.
func TestHandleRollbackErrors(t *testing.T) {
testBlock := util.NewBlock(wire.NewMsgBlock(
wire.NewBlockHeader(1, []daghash.Hash{}, &daghash.Hash{}, 0, 0)))
testBlockSize := uint32(testBlock.MsgBlock().SerializeSize())
tests := []struct {
name string
fileNum uint32
offset uint32
target interface{}
replacement interface{}
}{
// offset should be size of block + 12 bytes for block network, size and checksum
{"Nothing to rollback", 1, testBlockSize + 12, nil, nil},
{"deleteFile fails", 0, 0, (*blockStore).deleteFile,
func(*blockStore, uint32) error { return errors.New("error in blockstore.deleteFile") }},
{"openWriteFile fails", 0, 0, (*blockStore).openWriteFile,
func(*blockStore, uint32) (filer, error) { return nil, errors.New("error in blockstore.openWriteFile") }},
{"file.Truncate fails", 0, 0, (*os.File).Truncate,
func(*os.File, int64) error { return errors.New("error in file.Truncate") }},
{"file.Sync fails", 0, 0, (*os.File).Sync,
func(*os.File) error { return errors.New("error in file.Sync") }},
}
for _, test := range tests {
func() {
pdb := newTestDb("TestHandleRollbackErrors", t)
defer pdb.Close()
// Set maxBlockFileSize to testBlockSize so that writeCursor.curFileNum increments
pdb.store.maxBlockFileSize = testBlockSize
err := pdb.Update(func(dbTx database.Tx) error {
return dbTx.StoreBlock(testBlock)
})
if err != nil {
t.Fatalf("TestHandleRollbackErrors: %s: Error adding test block to database: %s", test.name, err)
}
if test.target != nil && test.replacement != nil {
patch := monkey.Patch(test.target, test.replacement)
defer patch.Unpatch()
}
pdb.store.handleRollback(test.fileNum, test.offset)
if pdb.store.writeCursor.curFileNum != test.fileNum {
t.Errorf("TestHandleRollbackErrors: %s: Expected fileNum: %d, but got: %d",
test.name, test.fileNum, pdb.store.writeCursor.curFileNum)
}
if pdb.store.writeCursor.curOffset != test.offset {
t.Errorf("TestHandleRollbackErrors: %s: offset fileNum: %d, but got: %d",
test.name, test.offset, pdb.store.writeCursor.curOffset)
}
}()
}
}

View File

@ -0,0 +1,43 @@
package ffldb
import (
"os"
"path"
"path/filepath"
"testing"
"github.com/btcsuite/goleveldb/leveldb"
"github.com/btcsuite/goleveldb/leveldb/filter"
"github.com/btcsuite/goleveldb/leveldb/opt"
"github.com/daglabs/btcd/wire"
)
func newTestDb(testName string, t *testing.T) *db {
dbPath := path.Join(os.TempDir(), "db_test")
err := os.RemoveAll(dbPath)
if err != nil && !os.IsNotExist(err) {
t.Fatalf("%s: Error deleting database folder before starting: %s", testName, err)
}
network := wire.SimNet
opts := opt.Options{
ErrorIfExist: true,
Strict: opt.DefaultStrict,
Compression: opt.NoCompression,
Filter: filter.NewBloomFilter(10),
}
metadataDbPath := filepath.Join(dbPath, metadataDbName)
ldb, err := leveldb.OpenFile(metadataDbPath, &opts)
if err != nil {
t.Errorf("%s: Error opening metadataDbPath: %s", testName, err)
}
err = initDB(ldb)
if err != nil {
t.Errorf("%s: Error initializing metadata Db: %s", testName, err)
}
store := newBlockStore(dbPath, network)
cache := newDbCache(ldb, store, defaultCacheSize, defaultFlushSecs)
return &db{store: store, cache: cache}
}

View File

@ -203,6 +203,12 @@ func (c *cursor) Delete() error {
return err
}
// Ensure the transaction is writable.
if !c.bucket.tx.writable {
str := "delete requires a writable database transaction"
return makeDbErr(database.ErrTxNotWritable, str, nil)
}
// Error if the cursor is exhausted.
if c.currentIter == nil {
str := "cursor is exhausted"
@ -652,10 +658,7 @@ func (b *bucket) CreateBucket(key []byte) (database.Bucket, error) {
}
// Add the new bucket to the bucket index.
if err := b.tx.putKey(bidxKey, childID[:]); err != nil {
str := fmt.Sprintf("failed to create bucket with key %q", key)
return nil, convertErr(str, err)
}
b.tx.putKey(bidxKey, childID[:])
return &bucket{tx: b.tx, id: childID}, nil
}
@ -881,7 +884,9 @@ func (b *bucket) Put(key, value []byte) error {
return makeDbErr(database.ErrKeyRequired, str, nil)
}
return b.tx.putKey(bucketizedKey(b.id, key), value)
b.tx.putKey(bucketizedKey(b.id, key), value)
return nil
}
// Get returns the value for the given key. Returns nil if the key does not
@ -931,7 +936,8 @@ func (b *bucket) Delete(key []byte) error {
// Nothing to do if there is no key.
if len(key) == 0 {
return nil
str := "delete requires a key"
return makeDbErr(database.ErrKeyRequired, str, nil)
}
b.tx.deleteKey(bucketizedKey(b.id, key), true)
@ -1044,7 +1050,7 @@ func (tx *transaction) hasKey(key []byte) bool {
//
// NOTE: This function must only be called on a writable transaction. Since it
// is an internal helper function, it does not check.
func (tx *transaction) putKey(key, value []byte) error {
func (tx *transaction) putKey(key, value []byte) {
// Prevent the key from being deleted if it was previously scheduled
// to be deleted on transaction commit.
tx.pendingRemove.Delete(key)
@ -1053,7 +1059,6 @@ func (tx *transaction) putKey(key, value []byte) error {
// commit.
tx.pendingKeys.Put(key, value)
tx.notifyActiveIters()
return nil
}
// fetchKey attempts to fetch the provided key from the database cache (and
@ -1107,9 +1112,9 @@ func (tx *transaction) nextBucketID() ([4]byte, error) {
// Increment and update the current bucket ID and return it.
var nextBucketID [4]byte
binary.BigEndian.PutUint32(nextBucketID[:], curBucketNum+1)
if err := tx.putKey(curBucketIDKeyName, nextBucketID[:]); err != nil {
return [4]byte{}, err
}
tx.putKey(curBucketIDKeyName, nextBucketID[:])
return nextBucketID, nil
}

727
database/ffldb/db_test.go Normal file
View File

@ -0,0 +1,727 @@
package ffldb
import (
"bytes"
"errors"
"fmt"
"testing"
"bou.ke/monkey"
"github.com/daglabs/btcd/dagconfig/daghash"
"github.com/daglabs/btcd/database"
"github.com/daglabs/btcd/util"
"github.com/daglabs/btcd/wire"
)
// TestCursorDeleteErrors tests all error-cases in *cursor.Delete().
// The non-error-cases are tested in the more general tests.
func TestCursorDeleteErrors(t *testing.T) {
pdb := newTestDb("TestCursorDeleteErrors", t)
nestedBucket := []byte("nestedBucket")
key := []byte("key")
value := []byte("value")
err := pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
_, err := metadata.CreateBucket(nestedBucket)
if err != nil {
return err
}
metadata.Put(key, value)
return nil
})
if err != nil {
t.Fatalf("TestCursorDeleteErrors: Error setting up test-database: %s", err)
}
// Check for error when attempted to delete a bucket
err = pdb.Update(func(dbTx database.Tx) error {
cursor := dbTx.Metadata().Cursor()
found := false
for ok := cursor.First(); ok; ok = cursor.Next() {
if bytes.Equal(cursor.Key(), nestedBucket) {
found = true
break
}
}
if !found {
t.Errorf("TestCursorDeleteErrors: Key '%s' not found", string(nestedBucket))
}
err := cursor.Delete()
if !database.IsErrorCode(err, database.ErrIncompatibleValue) {
t.Errorf("TestCursorDeleteErrors: Expected error of type ErrIncompatibleValue, "+
"when deleting bucket, but got %v", err)
}
return nil
})
if err != nil {
t.Fatalf("TestCursorDeleteErrors: Unexpected error from pdb.Update "+
"when attempting to delete bucket: %s", err)
}
// Check for error when transaction is not writable
err = pdb.View(func(dbTx database.Tx) error {
cursor := dbTx.Metadata().Cursor()
if !cursor.First() {
t.Fatal("TestCursorDeleteErrors: Nothing in cursor when testing for delete in " +
"non-writable transaction")
}
err := cursor.Delete()
if !database.IsErrorCode(err, database.ErrTxNotWritable) {
t.Errorf("TestCursorDeleteErrors: Expected error of type ErrTxNotWritable "+
"when calling .Delete() on non-writable transaction, but got '%v' instead", err)
}
return nil
})
if err != nil {
t.Fatalf("TestCursorDeleteErrors: Unexpected error from pdb.Update "+
"when attempting to delete on non-writable transaction: %s", err)
}
// Check for error when cursor was exhausted
err = pdb.Update(func(dbTx database.Tx) error {
cursor := dbTx.Metadata().Cursor()
for ok := cursor.First(); ok; ok = cursor.Next() {
}
err := cursor.Delete()
if !database.IsErrorCode(err, database.ErrIncompatibleValue) {
t.Errorf("TestCursorDeleteErrors: Expected error of type ErrIncompatibleValue "+
"when calling .Delete() on exhausted cursor, but got '%v' instead", err)
}
return nil
})
if err != nil {
t.Fatalf("TestCursorDeleteErrors: Unexpected error from pdb.Update "+
"when attempting to delete on exhausted cursor: %s", err)
}
// Check for error when transaction is closed
tx, err := pdb.Begin(true)
if err != nil {
t.Fatalf("TestCursorDeleteErrors: Error in pdb.Begin(): %s", err)
}
cursor := tx.Metadata().Cursor()
err = tx.Commit()
if err != nil {
t.Fatalf("TestCursorDeleteErrors: Error in tx.Commit(): %s", err)
}
err = cursor.Delete()
if !database.IsErrorCode(err, database.ErrTxClosed) {
t.Errorf("TestCursorDeleteErrors: Expected error of type ErrTxClosed "+
"when calling .Delete() on with closed transaction, but got '%s' instead", err)
}
}
func TestSkipPendingUpdates(t *testing.T) {
pdb := newTestDb("TestSkipPendingUpdates", t)
defer pdb.Close()
value := []byte("value")
// Add numbered prefixes to keys so that they are in expected order, and before any other keys
firstKey := []byte("1 - first")
toDeleteKey := []byte("2 - toDelete")
toUpdateKey := []byte("3 - toUpdate")
secondKey := []byte("4 - second")
// create initial metadata for test
err := pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
if err := metadata.Put(firstKey, value); err != nil {
return err
}
if err := metadata.Put(toDeleteKey, value); err != nil {
return err
}
if err := metadata.Put(toUpdateKey, value); err != nil {
return err
}
if err := metadata.Put(secondKey, value); err != nil {
return err
}
return nil
})
if err != nil {
t.Fatalf("TestSkipPendingUpdates: Error adding to metadata: %s", err)
}
// test skips
err = pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
if err := metadata.Delete(toDeleteKey); err != nil {
return err
}
if err := metadata.Put(toUpdateKey, value); err != nil {
return err
}
cursor := metadata.Cursor().(*cursor)
dbIter := cursor.dbIter
// Check that first is ok
dbIter.First()
expectedKey := bucketizedKey(metadataBucketID, firstKey)
if !bytes.Equal(dbIter.Key(), expectedKey) {
t.Errorf("TestSkipPendingUpdates: 1: key expected to be %v but is %v", expectedKey, dbIter.Key())
}
// Go to the next key, which is toDelete
dbIter.Next()
expectedKey = bucketizedKey(metadataBucketID, toDeleteKey)
if !bytes.Equal(dbIter.Key(), expectedKey) {
t.Errorf("TestSkipPendingUpdates: 2: key expected to be %s but is %s", expectedKey, dbIter.Key())
}
// at this point toDeleteKey and toUpdateKey should be skipped
cursor.skipPendingUpdates(true)
expectedKey = bucketizedKey(metadataBucketID, secondKey)
if !bytes.Equal(dbIter.Key(), expectedKey) {
t.Errorf("TestSkipPendingUpdates: 3: key expected to be %s but is %s", expectedKey, dbIter.Key())
}
// now traverse backwards - should get toUpdate
dbIter.Prev()
expectedKey = bucketizedKey(metadataBucketID, toUpdateKey)
if !bytes.Equal(dbIter.Key(), expectedKey) {
t.Errorf("TestSkipPendingUpdates: 4: key expected to be %s but is %s", expectedKey, dbIter.Key())
}
// at this point toUpdateKey and toDeleteKey should be skipped
cursor.skipPendingUpdates(false)
expectedKey = bucketizedKey(metadataBucketID, firstKey)
if !bytes.Equal(dbIter.Key(), expectedKey) {
t.Errorf("TestSkipPendingUpdates: 5: key expected to be %s but is %s", expectedKey, dbIter.Key())
}
return nil
})
if err != nil {
t.Fatalf("TestSkipPendingUpdates: Error running main part of test: %s", err)
}
}
// TestCursor tests various edge-cases in cursor that were not hit by the more general tests
func TestCursor(t *testing.T) {
pdb := newTestDb("TestCursor", t)
defer pdb.Close()
value := []byte("value")
// Add numbered prefixes to keys so that they are in expected order, and before any other keys
firstKey := []byte("1 - first")
toDeleteKey := []byte("2 - toDelete")
toUpdateKey := []byte("3 - toUpdate")
secondKey := []byte("4 - second")
// create initial metadata for test
err := pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
if err := metadata.Put(firstKey, value); err != nil {
return err
}
if err := metadata.Put(toDeleteKey, value); err != nil {
return err
}
if err := metadata.Put(toUpdateKey, value); err != nil {
return err
}
if err := metadata.Put(secondKey, value); err != nil {
return err
}
return nil
})
if err != nil {
t.Fatalf("Error adding to metadata: %s", err)
}
// run the actual tests
err = pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
if err := metadata.Delete(toDeleteKey); err != nil {
return err
}
if err := metadata.Put(toUpdateKey, value); err != nil {
return err
}
cursor := metadata.Cursor().(*cursor)
// Check prev when currentIter == nil
if ok := cursor.Prev(); ok {
t.Error("1: .Prev() should have returned false, but have returned true")
}
// Same thing for .Next()
for ok := cursor.First(); ok; ok = cursor.Next() {
}
if ok := cursor.Next(); ok {
t.Error("2: .Next() should have returned false, but have returned true")
}
// Check that Key(), rawKey(), Value(), and rawValue() all return nil when currentIter == nil
if key := cursor.Key(); key != nil {
t.Errorf("3: .Key() should have returned nil, but have returned '%s' instead", key)
}
if key := cursor.rawKey(); key != nil {
t.Errorf("4: .rawKey() should have returned nil, but have returned '%s' instead", key)
}
if value := cursor.Value(); value != nil {
t.Errorf("5: .Value() should have returned nil, but have returned '%s' instead", value)
}
if value := cursor.rawValue(); value != nil {
t.Errorf("6: .rawValue() should have returned nil, but have returned '%s' instead", value)
}
// Check rawValue in normal operation
cursor.First()
if rawValue := cursor.rawValue(); !bytes.Equal(rawValue, value) {
t.Errorf("7: rawValue should have returned '%s' but have returned '%s' instead", value, rawValue)
}
return nil
})
if err != nil {
t.Fatalf("Error running the actual tests: %s", err)
}
}
// TestCreateBucketErrors tests all error-cases in *bucket.CreateBucket().
// The non-error-cases are tested in the more general tests.
func TestCreateBucketErrors(t *testing.T) {
testKey := []byte("key")
tests := []struct {
name string
key []byte
target interface{}
replacement interface{}
isWritable bool
isClosed bool
expectedErr database.ErrorCode
}{
{"empty key", []byte{}, nil, nil, true, false, database.ErrBucketNameRequired},
{"transaction is closed", testKey, nil, nil, true, true, database.ErrTxClosed},
{"transaction is not writable", testKey, nil, nil, false, false, database.ErrTxNotWritable},
{"key already exists", blockIdxBucketName, nil, nil, true, false, database.ErrBucketExists},
{"nextBucketID error", testKey, (*transaction).nextBucketID,
func(*transaction) ([4]byte, error) {
return [4]byte{}, makeDbErr(database.ErrTxClosed, "error in newBucketID", nil)
},
true, false, database.ErrTxClosed},
}
for _, test := range tests {
func() {
pdb := newTestDb("TestCreateBucketErrors", t)
defer pdb.Close()
if test.target != nil && test.replacement != nil {
patch := monkey.Patch(test.target, test.replacement)
defer patch.Unpatch()
}
tx, err := pdb.Begin(test.isWritable)
defer tx.Commit()
if err != nil {
t.Fatalf("TestCreateBucketErrors: %s: error from pdb.Begin: %s", test.name, err)
}
if test.isClosed {
err = tx.Commit()
if err != nil {
t.Fatalf("TestCreateBucketErrors: %s: error from tx.Commit: %s", test.name, err)
}
}
metadata := tx.Metadata()
_, err = metadata.CreateBucket(test.key)
if !database.IsErrorCode(err, test.expectedErr) {
t.Errorf("TestCreateBucketErrors: %s: Expected error of type %d "+
"but got '%v'", test.name, test.expectedErr, err)
}
}()
}
}
// TestPutErrors tests all error-cases in *bucket.Put().
// The non-error-cases are tested in the more general tests.
func TestPutErrors(t *testing.T) {
testKey := []byte("key")
testValue := []byte("value")
tests := []struct {
name string
key []byte
isWritable bool
isClosed bool
expectedErr database.ErrorCode
}{
{"empty key", []byte{}, true, false, database.ErrKeyRequired},
{"transaction is closed", testKey, true, true, database.ErrTxClosed},
{"transaction is not writable", testKey, false, false, database.ErrTxNotWritable},
}
for _, test := range tests {
func() {
pdb := newTestDb("TestPutErrors", t)
defer pdb.Close()
tx, err := pdb.Begin(test.isWritable)
defer tx.Commit()
if err != nil {
t.Fatalf("TestPutErrors: %s: error from pdb.Begin: %s", test.name, err)
}
if test.isClosed {
err = tx.Commit()
if err != nil {
t.Fatalf("TestPutErrors: %s: error from tx.Commit: %s", test.name, err)
}
}
metadata := tx.Metadata()
err = metadata.Put(test.key, testValue)
if !database.IsErrorCode(err, test.expectedErr) {
t.Errorf("TestPutErrors: %s: Expected error of type %d "+
"but got '%v'", test.name, test.expectedErr, err)
}
}()
}
}
// TestGetErrors tests all error-cases in *bucket.Get().
// The non-error-cases are tested in the more general tests.
func TestGetErrors(t *testing.T) {
testKey := []byte("key")
tests := []struct {
name string
key []byte
isClosed bool
}{
{"empty key", []byte{}, false},
{"transaction is closed", testKey, true},
}
for _, test := range tests {
func() {
pdb := newTestDb("TestGetErrors", t)
defer pdb.Close()
tx, err := pdb.Begin(false)
defer tx.Rollback()
if err != nil {
t.Fatalf("TestGetErrors: %s: error from pdb.Begin: %s", test.name, err)
}
if test.isClosed {
err = tx.Rollback()
if err != nil {
t.Fatalf("TestGetErrors: %s: error from tx.Commit: %s", test.name, err)
}
}
metadata := tx.Metadata()
if result := metadata.Get(test.key); result != nil {
t.Errorf("TestGetErrors: %s: Expected to return nil, but got %v", test.name, result)
}
}()
}
}
// TestDeleteErrors tests all error-cases in *bucket.Delete().
// The non-error-cases are tested in the more general tests.
func TestDeleteErrors(t *testing.T) {
testKey := []byte("key")
tests := []struct {
name string
key []byte
isWritable bool
isClosed bool
expectedErr database.ErrorCode
}{
{"empty key", []byte{}, true, false, database.ErrKeyRequired},
{"transaction is closed", testKey, true, true, database.ErrTxClosed},
{"transaction is not writable", testKey, false, false, database.ErrTxNotWritable},
}
for _, test := range tests {
func() {
pdb := newTestDb("TestDeleteErrors", t)
defer pdb.Close()
tx, err := pdb.Begin(test.isWritable)
defer tx.Commit()
if err != nil {
t.Fatalf("TestDeleteErrors: %s: error from pdb.Begin: %s", test.name, err)
}
if test.isClosed {
err = tx.Commit()
if err != nil {
t.Fatalf("TestDeleteErrors: %s: error from tx.Commit: %s", test.name, err)
}
}
metadata := tx.Metadata()
err = metadata.Delete(test.key)
if !database.IsErrorCode(err, test.expectedErr) {
t.Errorf("TestDeleteErrors: %s: Expected error of type %d "+
"but got '%v'", test.name, test.expectedErr, err)
}
}()
}
}
func TestForEachBucket(t *testing.T) {
pdb := newTestDb("TestForEachBucket", t)
// set-up test
testKey := []byte("key")
testValue := []byte("value")
bucketKeys := [][]byte{{1}, {2}, {3}}
err := pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
for _, bucketKey := range bucketKeys {
bucket, err := metadata.CreateBucket(bucketKey)
if err != nil {
return err
}
err = bucket.Put(testKey, testValue)
if err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatalf("TestForEachBucket: Error setting up test-database: %s", err)
}
// actual test
err = pdb.View(func(dbTx database.Tx) error {
i := 0
metadata := dbTx.Metadata()
err := metadata.ForEachBucket(func(bucketKey []byte) error {
if i >= len(bucketKeys) { // in case there are any other buckets in metadata
return nil
}
expectedBucketKey := bucketKeys[i]
if !bytes.Equal(expectedBucketKey, bucketKey) {
t.Errorf("TestForEachBucket: %d: Expected bucket key: %v, but got: %v",
i, expectedBucketKey, bucketKey)
return nil
}
bucket := metadata.Bucket(bucketKey)
if bucket == nil {
t.Errorf("TestForEachBucket: %d: Bucket is nil", i)
return nil
}
value := bucket.Get(testKey)
if !bytes.Equal(testValue, value) {
t.Errorf("TestForEachBucket: %d: Expected value: %s, but got: %s",
i, testValue, value)
return nil
}
i++
return nil
})
return err
})
if err != nil {
t.Fatalf("TestForEachBucket: Error running actual tests: %s", err)
}
}
// TestStoreBlockErrors tests all error-cases in *tx.StoreBlock().
// The non-error-cases are tested in the more general tests.
func TestStoreBlockErrors(t *testing.T) {
testBlock := util.NewBlock(wire.NewMsgBlock(wire.NewBlockHeader(1, []daghash.Hash{}, &daghash.Hash{}, 0, 0)))
tests := []struct {
name string
target interface{}
replacement interface{}
isWritable bool
isClosed bool
expectedErr database.ErrorCode
}{
{"transaction is closed", nil, nil, true, true, database.ErrTxClosed},
{"transaction is not writable", nil, nil, false, false, database.ErrTxNotWritable},
{"block exists", (*transaction).hasBlock,
func(*transaction, *daghash.Hash) bool { return true },
true, false, database.ErrBlockExists},
{"error in block.Bytes", (*util.Block).Bytes,
func(*util.Block) ([]byte, error) { return nil, errors.New("Error in block.Bytes()") },
true, false, database.ErrDriverSpecific},
}
for _, test := range tests {
func() {
pdb := newTestDb("TestStoreBlockErrors", t)
defer pdb.Close()
if test.target != nil && test.replacement != nil {
patch := monkey.Patch(test.target, test.replacement)
defer patch.Unpatch()
}
tx, err := pdb.Begin(test.isWritable)
defer tx.Commit()
if err != nil {
t.Fatalf("TestStoreBlockErrors: %s: error from pdb.Begin: %s", test.name, err)
}
if test.isClosed {
err = tx.Commit()
if err != nil {
t.Fatalf("TestStoreBlockErrors: %s: error from tx.Commit: %s", test.name, err)
}
}
err = tx.StoreBlock(testBlock)
if !database.IsErrorCode(err, test.expectedErr) {
t.Errorf("TestStoreBlockErrors: %s: Expected error of type %d "+
"but got '%v'", test.name, test.expectedErr, err)
}
}()
}
}
// TestDeleteDoubleNestedBucket tests what happens when bucket.DeleteBucket()
// is invoked on a bucket that contains a nested bucket.
func TestDeleteDoubleNestedBucket(t *testing.T) {
pdb := newTestDb("TestDeleteDoubleNestedBucket", t)
defer pdb.Close()
firstKey := []byte("first")
secondKey := []byte("second")
key := []byte("key")
value := []byte("value")
var rawKey, rawSecondKey []byte
// Test setup
err := pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
firstBucket, err := metadata.CreateBucket(firstKey)
if err != nil {
return fmt.Errorf("Error creating first bucket: %s", err)
}
secondBucket, err := firstBucket.CreateBucket(secondKey)
if err != nil {
return fmt.Errorf("Error creating second bucket: %s", err)
}
secondBucket.Put(key, value)
// extract rawKey from cursor and make sure it's in raw database
c := secondBucket.Cursor()
for ok := c.First(); ok && !bytes.Equal(c.Key(), key); ok = c.Next() {
}
if !bytes.Equal(c.Key(), key) {
return fmt.Errorf("Couldn't find key to extract rawKey")
}
rawKey = c.(*cursor).rawKey()
if dbTx.(*transaction).fetchKey(rawKey) == nil {
return fmt.Errorf("rawKey not found")
}
// extract rawSecondKey from cursor and make sure it's in raw database
c = firstBucket.Cursor()
for ok := c.First(); ok && !bytes.Equal(c.Key(), secondKey); ok = c.Next() {
}
if !bytes.Equal(c.Key(), secondKey) {
return fmt.Errorf("Couldn't find secondKey to extract rawSecondKey")
}
rawSecondKey = c.(*cursor).rawKey()
if dbTx.(*transaction).fetchKey(rawSecondKey) == nil {
return fmt.Errorf("rawSecondKey not found for some reason")
}
return nil
})
if err != nil {
t.Fatalf("TestDeleteDoubleNestedBucket: Error in test setup pdb.Update: %s", err)
}
// Actual test
err = pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
err := metadata.DeleteBucket(firstKey)
if err != nil {
return err
}
if dbTx.(*transaction).fetchKey(rawSecondKey) != nil {
t.Error("TestDeleteDoubleNestedBucket: secondBucket was not deleted")
}
if dbTx.(*transaction).fetchKey(rawKey) != nil {
t.Error("TestDeleteDoubleNestedBucket: value inside secondBucket was not deleted")
}
return nil
})
if err != nil {
t.Fatalf("TestDeleteDoubleNestedBucket: Error in actual test pdb.Update: %s", err)
}
}
// TestWritePendingAndCommitErrors tests some error-cases in *tx.writePendingAndCommit().
// The non-error-cases are tested in the more general tests.
func TestWritePendingAndCommitErrors(t *testing.T) {
putPatch := monkey.Patch((*bucket).Put,
func(_ *bucket, _, _ []byte) error { return errors.New("Error in bucket.Put") })
defer putPatch.Unpatch()
rollbackCalled := false
var rollbackPatch *monkey.PatchGuard
rollbackPatch = monkey.Patch((*blockStore).handleRollback,
func(s *blockStore, oldBlockFileNum, oldBlockOffset uint32) {
rollbackPatch.Unpatch()
defer rollbackPatch.Restore()
rollbackCalled = true
s.handleRollback(oldBlockFileNum, oldBlockOffset)
})
defer rollbackPatch.Unpatch()
pdb := newTestDb("TestWritePendingAndCommitErrors", t)
defer pdb.Close()
err := pdb.Update(func(dbTx database.Tx) error { return nil })
if err == nil {
t.Errorf("No error returned when metaBucket.Put() should have returned an error")
}
if !rollbackCalled {
t.Errorf("No rollback called when metaBucket.Put() have returned an error")
}
rollbackCalled = false
err = pdb.Update(func(dbTx database.Tx) error {
return dbTx.StoreBlock(util.NewBlock(wire.NewMsgBlock(
wire.NewBlockHeader(1, []daghash.Hash{}, &daghash.Hash{}, 0, 0))))
})
if err == nil {
t.Errorf("No error returned when blockIdx.Put() should have returned an error")
}
if !rollbackCalled {
t.Errorf("No rollback called when blockIdx.Put() have returned an error")
}
}

View File

@ -0,0 +1,319 @@
package ffldb
import (
"bytes"
"errors"
"testing"
"bou.ke/monkey"
"github.com/btcsuite/goleveldb/leveldb"
"github.com/btcsuite/goleveldb/leveldb/opt"
ldbutil "github.com/btcsuite/goleveldb/leveldb/util"
"github.com/daglabs/btcd/database"
)
// TestDBCacheCloseErrors tests all error-cases in *dbCache.Close().
// The non-error-cases are tested in the more general tests.
func TestDBCacheCloseErrors(t *testing.T) {
cache := newTestDb("TestDBCacheCloseErrors", t).cache
defer cache.Close()
closeCalled := false
closePatch := monkey.Patch((*leveldb.DB).Close, func(*leveldb.DB) error { closeCalled = true; return nil })
defer closePatch.Unpatch()
expectedErr := errors.New("error on flush")
flushPatch := monkey.Patch((*dbCache).flush, func(*dbCache) error { return expectedErr })
defer flushPatch.Unpatch()
err := cache.Close()
if err != expectedErr {
t.Errorf("TestDBCacheCloseErrors: Expected error on bad flush is %s but got %s", expectedErr, err)
}
if !closeCalled {
t.Errorf("TestDBCacheCloseErrors: ldb.Close was not called when error flushing")
}
}
// TestUpdateDBErrors tests all error-cases in *dbCache.UpdateDB().
// The non-error-cases are tested in the more general tests.
func TestUpdateDBErrors(t *testing.T) {
// Test when ldb.OpenTransaction returns error
func() {
cache := newTestDb("TestDBCacheCloseErrors", t).cache
defer cache.Close()
patch := monkey.Patch((*leveldb.DB).OpenTransaction,
func(*leveldb.DB) (*leveldb.Transaction, error) { return nil, errors.New("error in OpenTransaction") })
defer patch.Unpatch()
err := cache.updateDB(func(ldbTx *leveldb.Transaction) error { return nil })
if err == nil {
t.Errorf("No error in updateDB when ldb.OpenTransaction returns an error")
}
}()
// Test when ldbTx.Commit returns an error
func() {
cache := newTestDb("TestDBCacheCloseErrors", t).cache
defer cache.Close()
patch := monkey.Patch((*leveldb.Transaction).Commit,
func(*leveldb.Transaction) error { return errors.New("error in Commit") })
defer patch.Unpatch()
err := cache.updateDB(func(ldbTx *leveldb.Transaction) error { return nil })
if err == nil {
t.Errorf("No error in updateDB when ldbTx.Commit returns an error")
}
}()
cache := newTestDb("TestDBCacheCloseErrors", t).cache
defer cache.Close()
// Test when function passed to updateDB returns an error
err := cache.updateDB(func(ldbTx *leveldb.Transaction) error { return errors.New("Error in fn") })
if err == nil {
t.Errorf("No error in updateDB when passed function returns an error")
}
}
// TestCommitTxFlushNeeded test the *dbCache.commitTx function when flush is needed,
// including error-cases.
// When flush is not needed is tested in the more general tests.
func TestCommitTxFlushNeeded(t *testing.T) {
tests := []struct {
name string
target interface{}
replacement interface{}
expectedError bool
}{
{"No errors", nil, nil, false},
{"Error in flush", (*dbCache).flush, func(*dbCache) error { return errors.New("error") }, true},
{"Error in commitTreaps", (*dbCache).commitTreaps,
func(*dbCache, TreapForEacher, TreapForEacher) error { return errors.New("error") }, true},
}
for _, test := range tests {
func() {
db := newTestDb("TestDBCacheCloseErrors", t)
defer db.Close()
cache := db.cache
cache.flushInterval = 0 // set flushInterval to 0 so that flush is always required
if test.target != nil && test.replacement != nil {
patch := monkey.Patch(test.target, test.replacement)
defer patch.Unpatch()
}
tx, err := db.Begin(true)
if err != nil {
t.Fatalf("Error begining transaction: %s", err)
}
cache.commitTx(tx.(*transaction))
db.closeLock.RUnlock()
}()
}
}
func TestExhaustedDbCacheIterator(t *testing.T) {
db := newTestDb("TestExhaustedDbCacheIterator", t)
defer db.Close()
snapshot, err := db.cache.Snapshot()
if err != nil {
t.Fatalf("TestExhaustedDbCacheIterator: Error creating cache snapshot: %s", err)
}
iterator := snapshot.NewIterator(&ldbutil.Range{})
if next := iterator.Next(); next != false {
t.Errorf("TestExhaustedDbCacheIterator: Expected .Next() = false, but got %v", next)
}
if prev := iterator.Prev(); prev != false {
t.Errorf("TestExhaustedDbCacheIterator: Expected .Prev() = false, but got %v", prev)
}
if key := iterator.Key(); key != nil {
t.Errorf("TestExhaustedDbCacheIterator: Expected .Key() = nil, but got %v", key)
}
if value := iterator.Value(); value != nil {
t.Errorf("TestExhaustedDbCacheIterator: Expected .Value() = nil, but got %v", value)
}
}
// TestLDBIteratorImplPlaceholders hits functions that are there to implement leveldb iterator.Iterator interface,
// but surve no other purpose.
func TestLDBIteratorImplPlaceholders(t *testing.T) {
db := newTestDb("TestIteratorImplPlaceholders", t)
defer db.Close()
snapshot, err := db.cache.Snapshot()
if err != nil {
t.Fatalf("TestLDBIteratorImplPlaceholders: Error creating cache snapshot: %s", err)
}
iterator := newLdbCacheIter(snapshot, &ldbutil.Range{})
if err = iterator.Error(); err != nil {
t.Errorf("TestLDBIteratorImplPlaceholders: Expected .Error() = nil, but got %v", err)
}
// Call SetReleaser to achieve coverage of it. Actually does nothing
iterator.SetReleaser(nil)
}
func TestSkipPendingUpdatesCache(t *testing.T) {
pdb := newTestDb("TestSkipPendingUpdatesCache", t)
defer pdb.Close()
value := []byte("value")
// Add numbered prefixes to keys so that they are in expected order, and before any other keys
firstKey := []byte("1 - first")
toDeleteKey := []byte("2 - toDelete")
toUpdateKey := []byte("3 - toUpdate")
secondKey := []byte("4 - second")
// create initial metadata for test
err := pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
if err := metadata.Put(firstKey, value); err != nil {
return err
}
if err := metadata.Put(toDeleteKey, value); err != nil {
return err
}
if err := metadata.Put(toUpdateKey, value); err != nil {
return err
}
if err := metadata.Put(secondKey, value); err != nil {
return err
}
return nil
})
if err != nil {
t.Fatalf("Error adding to metadata: %s", err)
}
err = pdb.cache.flush()
if err != nil {
t.Fatalf("Error flushing cache: %s", err)
}
// test skips
err = pdb.Update(func(dbTx database.Tx) error {
snapshot, err := pdb.cache.Snapshot()
if err != nil {
t.Fatalf("TestSkipPendingUpdatesCache: Error getting snapshot: %s", err)
}
iterator := snapshot.NewIterator(&ldbutil.Range{})
snapshot.pendingRemove = snapshot.pendingRemove.Put(bucketizedKey(metadataBucketID, toDeleteKey), value)
snapshot.pendingKeys = snapshot.pendingKeys.Put(bucketizedKey(metadataBucketID, toUpdateKey), value)
// Check that first is ok
iterator.First()
expectedKey := bucketizedKey(metadataBucketID, firstKey)
actualKey := iterator.Key()
if !bytes.Equal(actualKey, expectedKey) {
t.Errorf("TestSkipPendingUpdatesCache: 1: key expected to be %v but is %v", expectedKey, actualKey)
}
// Go to the next key, which is second, toDelete and toUpdate will be skipped
iterator.Next()
expectedKey = bucketizedKey(metadataBucketID, secondKey)
actualKey = iterator.Key()
if !bytes.Equal(actualKey, expectedKey) {
t.Errorf("TestSkipPendingUpdatesCache: 2: key expected to be %s but is %s", expectedKey, actualKey)
}
// now traverse backwards - should get first, toUpdate and toDelete will be skipped
iterator.Prev()
expectedKey = bucketizedKey(metadataBucketID, firstKey)
actualKey = iterator.Key()
if !bytes.Equal(actualKey, expectedKey) {
t.Errorf("TestSkipPendingUpdatesCache: 4: key expected to be %s but is %s", expectedKey, actualKey)
}
return nil
})
if err != nil {
t.Fatalf("TestSkipPendingUpdatesCache: Error running main part of test: %s", err)
}
}
// TestFlushCommitTreapsErrors tests error-cases in *dbCache.flush() when commitTreaps returns error.
// The non-error-cases are tested in the more general tests.
func TestFlushCommitTreapsErrors(t *testing.T) {
pdb := newTestDb("TestFlushCommitTreapsErrors", t)
defer pdb.Close()
key := []byte("key")
value := []byte("value")
// Before setting flush interval to zero - put some data so that there's something to flush
err := pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
metadata.Put(key, value)
return nil
})
if err != nil {
t.Fatalf("TestFlushCommitTreapsErrors: Error putting some data to flush: %s", err)
}
cache := pdb.cache
cache.flushInterval = 0 // set flushInterval to 0 so that flush is always required
// Test for correctness when encountered error on Put
func() {
patch := monkey.Patch((*leveldb.Transaction).Put,
func(*leveldb.Transaction, []byte, []byte, *opt.WriteOptions) error { return errors.New("error") })
defer patch.Unpatch()
err := pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
metadata.Put(key, value)
return nil
})
if err == nil {
t.Errorf("TestFlushCommitTreapsErrors: No error from pdb.Update when ldbTx.Put returned error")
}
}()
// Test for correctness when encountered error on Delete
// First put some data we can later "fail" to delete
err = pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
metadata.Put(key, value)
return nil
})
if err != nil {
t.Fatalf("TestFlushCommitTreapsErrors: Error putting some data to delete: %s", err)
}
// Now "fail" to delete it
func() {
patch := monkey.Patch((*leveldb.Transaction).Delete,
func(*leveldb.Transaction, []byte, *opt.WriteOptions) error { return errors.New("error") })
defer patch.Unpatch()
err := pdb.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
metadata.Delete(key)
return nil
})
if err == nil {
t.Errorf("TestFlushCommitTreapsErrors: No error from pdb.Update when ldbTx.Delete returned error")
}
}()
}

View File

@ -59,16 +59,3 @@ func createDBDriver(args ...interface{}) (database.DB, error) {
return openDB(dbPath, network, true)
}
func init() {
// Register the driver.
driver := database.Driver{
DbType: dbType,
Create: createDBDriver,
Open: openDBDriver,
}
if err := database.RegisterDriver(driver); err != nil {
panic(fmt.Sprintf("Failed to regiser database driver '%s': %v",
dbType, err))
}
}

View File

@ -113,7 +113,7 @@ func TestCreateOpenFail(t *testing.T) {
db.Close()
wantErrCode = database.ErrDbNotOpen
err = db.View(func(tx database.Tx) error {
err = db.View(func(dbTx database.Tx) error {
return nil
})
if !checkDbError(t, "View", err, wantErrCode) {
@ -121,7 +121,7 @@ func TestCreateOpenFail(t *testing.T) {
}
wantErrCode = database.ErrDbNotOpen
err = db.Update(func(tx database.Tx) error {
err = db.Update(func(dbTx database.Tx) error {
return nil
})
if !checkDbError(t, "Update", err, wantErrCode) {
@ -173,8 +173,8 @@ func TestPersistence(t *testing.T) {
}
genesisBlock := util.NewBlock(dagconfig.MainNetParams.GenesisBlock)
genesisHash := dagconfig.MainNetParams.GenesisHash
err = db.Update(func(tx database.Tx) error {
metadataBucket := tx.Metadata()
err = db.Update(func(dbTx database.Tx) error {
metadataBucket := dbTx.Metadata()
if metadataBucket == nil {
return fmt.Errorf("Metadata: unexpected nil bucket")
}
@ -193,7 +193,7 @@ func TestPersistence(t *testing.T) {
}
}
if err := tx.StoreBlock(genesisBlock); err != nil {
if err := dbTx.StoreBlock(genesisBlock); err != nil {
return fmt.Errorf("StoreBlock: unexpected error: %v",
err)
}
@ -216,8 +216,8 @@ func TestPersistence(t *testing.T) {
// Ensure the values previously stored in the 3rd namespace still exist
// and are correct.
err = db.View(func(tx database.Tx) error {
metadataBucket := tx.Metadata()
err = db.View(func(dbTx database.Tx) error {
metadataBucket := dbTx.Metadata()
if metadataBucket == nil {
return fmt.Errorf("Metadata: unexpected nil bucket")
}
@ -237,7 +237,7 @@ func TestPersistence(t *testing.T) {
}
genesisBlockBytes, _ := genesisBlock.Bytes()
gotBytes, err := tx.FetchBlock(genesisHash)
gotBytes, err := dbTx.FetchBlock(genesisHash)
if err != nil {
return fmt.Errorf("FetchBlock: unexpected error: %v",
err)
@ -282,7 +282,9 @@ func TestInterface(t *testing.T) {
// Change the maximum file size to a small value to force multiple flat
// files with the test data set.
ffldb.TstRunWithMaxBlockFileSize(db, 2048, func() {
// Change maximum open files to small value to force shifts in the LRU
// mechanism
ffldb.TstRunWithMaxBlockFileSizeAndMaxOpenFiles(db, 2048, 10, func() {
testInterface(t, db)
})
}

View File

@ -16,11 +16,14 @@ import "github.com/daglabs/btcd/database"
// TstRunWithMaxBlockFileSize runs the passed function with the maximum allowed
// file size for the database set to the provided value. The value will be set
// back to the original value upon completion.
func TstRunWithMaxBlockFileSize(idb database.DB, size uint32, fn func()) {
func TstRunWithMaxBlockFileSizeAndMaxOpenFiles(idb database.DB, size uint32, maxOpenFiles int, fn func()) {
ffldb := idb.(*db)
origSize := ffldb.store.maxBlockFileSize
origMaxOpenFiles := ffldb.store.maxOpenFiles
ffldb.store.maxBlockFileSize = size
ffldb.store.maxOpenFiles = maxOpenFiles
fn()
ffldb.store.maxBlockFileSize = origSize
ffldb.store.maxOpenFiles = origMaxOpenFiles
}

23
database/ffldb/init.go Normal file
View File

@ -0,0 +1,23 @@
package ffldb
import (
"fmt"
"github.com/daglabs/btcd/database"
)
func registerDriver() {
driver := database.Driver{
DbType: dbType,
Create: createDBDriver,
Open: openDBDriver,
}
if err := database.RegisterDriver(driver); err != nil {
panic(fmt.Sprintf("Failed to regiser database driver '%s': %v",
dbType, err))
}
}
func init() {
registerDriver()
}

View File

@ -0,0 +1,26 @@
package ffldb
import (
"errors"
"testing"
"bou.ke/monkey"
"github.com/daglabs/btcd/database"
)
// TestRegisterDriverErrors tests all error-cases in registerDriver().
// The non-error-cases are tested in the more general tests.
func TestInitErrors(t *testing.T) {
patch := monkey.Patch(database.RegisterDriver,
func(driver database.Driver) error { return errors.New("Error in database.RegisterDriver") })
defer patch.Unpatch()
defer func() {
err := recover()
if err == nil {
t.Errorf("TestRegisterDriverErrors: No panic on init when database.RegisterDriver returned an error")
}
}()
registerDriver()
}

View File

@ -626,10 +626,10 @@ func testBucketInterface(tc *testContext, bucket database.Bucket) bool {
// 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) {
func rollbackOnPanic(t *testing.T, dbTx database.Tx) {
if err := recover(); err != nil {
t.Errorf("Unexpected panic: %v", err)
_ = tx.Rollback()
_ = dbTx.Rollback()
panic(err)
}
}
@ -851,8 +851,8 @@ func testManagedTxPanics(tc *testContext) bool {
// Ensure calling Commit on a managed read-only transaction panics.
paniced := testPanic(func() {
tc.db.View(func(tx database.Tx) error {
tx.Commit()
tc.db.View(func(dbTx database.Tx) error {
dbTx.Commit()
return nil
})
})
@ -863,8 +863,8 @@ func testManagedTxPanics(tc *testContext) bool {
// Ensure calling Rollback on a managed read-only transaction panics.
paniced = testPanic(func() {
tc.db.View(func(tx database.Tx) error {
tx.Rollback()
tc.db.View(func(dbTx database.Tx) error {
dbTx.Rollback()
return nil
})
})
@ -875,8 +875,8 @@ func testManagedTxPanics(tc *testContext) bool {
// Ensure calling Commit on a managed read-write transaction panics.
paniced = testPanic(func() {
tc.db.Update(func(tx database.Tx) error {
tx.Commit()
tc.db.Update(func(dbTx database.Tx) error {
dbTx.Commit()
return nil
})
})
@ -887,8 +887,8 @@ func testManagedTxPanics(tc *testContext) bool {
// Ensure calling Rollback on a managed read-write transaction panics.
paniced = testPanic(func() {
tc.db.Update(func(tx database.Tx) error {
tx.Rollback()
tc.db.Update(func(dbTx database.Tx) error {
dbTx.Rollback()
return nil
})
})
@ -909,8 +909,8 @@ func testMetadataTxInterface(tc *testContext) bool {
}
bucket1Name := []byte("bucket1")
err := tc.db.Update(func(tx database.Tx) error {
_, err := tx.Metadata().CreateBucket(bucket1Name)
err := tc.db.Update(func(dbTx database.Tx) error {
_, err := dbTx.Metadata().CreateBucket(bucket1Name)
return err
})
if err != nil {
@ -932,8 +932,8 @@ func testMetadataTxInterface(tc *testContext) bool {
}
// Test the bucket interface via a managed read-only transaction.
err = tc.db.View(func(tx database.Tx) error {
metadataBucket := tx.Metadata()
err = tc.db.View(func(dbTx database.Tx) error {
metadataBucket := dbTx.Metadata()
if metadataBucket == nil {
return fmt.Errorf("Metadata: unexpected nil bucket")
}
@ -960,7 +960,7 @@ func testMetadataTxInterface(tc *testContext) bool {
// 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 {
err = tc.db.View(func(dbTx database.Tx) error {
return viewError
})
if err != viewError {
@ -973,8 +973,8 @@ func testMetadataTxInterface(tc *testContext) bool {
// 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()
err = tc.db.Update(func(dbTx database.Tx) error {
metadataBucket := dbTx.Metadata()
if metadataBucket == nil {
return fmt.Errorf("Metadata: unexpected nil bucket")
}
@ -1008,8 +1008,8 @@ func testMetadataTxInterface(tc *testContext) bool {
// 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()
err = tc.db.View(func(dbTx database.Tx) error {
metadataBucket := dbTx.Metadata()
if metadataBucket == nil {
return fmt.Errorf("Metadata: unexpected nil bucket")
}
@ -1028,8 +1028,8 @@ func testMetadataTxInterface(tc *testContext) bool {
}
// Store a series of values via a managed read-write transaction.
err = tc.db.Update(func(tx database.Tx) error {
metadataBucket := tx.Metadata()
err = tc.db.Update(func(dbTx database.Tx) error {
metadataBucket := dbTx.Metadata()
if metadataBucket == nil {
return fmt.Errorf("Metadata: unexpected nil bucket")
}
@ -1053,8 +1053,8 @@ func testMetadataTxInterface(tc *testContext) bool {
}
// Ensure the values stored above were committed as expected.
err = tc.db.View(func(tx database.Tx) error {
metadataBucket := tx.Metadata()
err = tc.db.View(func(dbTx database.Tx) error {
metadataBucket := dbTx.Metadata()
if metadataBucket == nil {
return fmt.Errorf("Metadata: unexpected nil bucket")
}
@ -1078,8 +1078,8 @@ func testMetadataTxInterface(tc *testContext) bool {
}
// Clean up the values stored above in a managed read-write transaction.
err = tc.db.Update(func(tx database.Tx) error {
metadataBucket := tx.Metadata()
err = tc.db.Update(func(dbTx database.Tx) error {
metadataBucket := dbTx.Metadata()
if metadataBucket == nil {
return fmt.Errorf("Metadata: unexpected nil bucket")
}
@ -1107,7 +1107,7 @@ func testMetadataTxInterface(tc *testContext) bool {
// 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 {
func testFetchBlockIOMissing(tc *testContext, dbTx database.Tx) bool {
wantErrCode := database.ErrBlockNotFound
// ---------------------
@ -1132,7 +1132,7 @@ func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool {
// Ensure FetchBlock returns expected error.
testName := fmt.Sprintf("FetchBlock #%d on missing block", i)
_, err = tx.FetchBlock(blockHash)
_, err = dbTx.FetchBlock(blockHash)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1140,7 +1140,7 @@ func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool {
// Ensure FetchBlockHeader returns expected error.
testName = fmt.Sprintf("FetchBlockHeader #%d on missing block",
i)
_, err = tx.FetchBlockHeader(blockHash)
_, err = dbTx.FetchBlockHeader(blockHash)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1153,13 +1153,13 @@ func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool {
Len: uint32(txLocs[0].TxLen),
}
allBlockRegions[i] = region
_, err = tx.FetchBlockRegion(&region)
_, err = dbTx.FetchBlockRegion(&region)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
// Ensure HasBlock returns false.
hasBlock, err := tx.HasBlock(blockHash)
hasBlock, err := dbTx.HasBlock(blockHash)
if err != nil {
tc.t.Errorf("HasBlock #%d: unexpected err: %v", i, err)
return false
@ -1176,27 +1176,27 @@ func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool {
// Ensure FetchBlocks returns expected error.
testName := "FetchBlocks on missing blocks"
_, err := tx.FetchBlocks(allBlockHashes)
_, err := dbTx.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)
_, err = dbTx.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)
_, err = dbTx.FetchBlockRegions(allBlockRegions)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
// Ensure HasBlocks returns false for all blocks.
hasBlocks, err := tx.HasBlocks(allBlockHashes)
hasBlocks, err := dbTx.HasBlocks(allBlockHashes)
if err != nil {
tc.t.Errorf("HasBlocks: unexpected err: %v", err)
}
@ -1215,7 +1215,7 @@ func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool {
// 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 {
func testFetchBlockIO(tc *testContext, dbTx database.Tx) bool {
// ---------------------
// Non-bulk Block IO API
// ---------------------
@ -1251,7 +1251,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
// Ensure the block data fetched from the database matches the
// expected bytes.
gotBlockBytes, err := tx.FetchBlock(blockHash)
gotBlockBytes, err := dbTx.FetchBlock(blockHash)
if err != nil {
tc.t.Errorf("FetchBlock(%s): unexpected error: %v",
blockHash, err)
@ -1264,7 +1264,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
}
wantHeaderBytes := blockBytes[0:allBlockHeaderSizes[i]]
gotHeaderBytes, err := tx.FetchBlockHeader(blockHash)
gotHeaderBytes, err := dbTx.FetchBlockHeader(blockHash)
if err != nil {
tc.t.Errorf("FetchBlockHeader(%s): unexpected error: %v",
blockHash, err)
@ -1287,7 +1287,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
allBlockRegions[i] = region
endRegionOffset := region.Offset + region.Len
wantRegionBytes := blockBytes[region.Offset:endRegionOffset]
gotRegionBytes, err := tx.FetchBlockRegion(&region)
gotRegionBytes, err := dbTx.FetchBlockRegion(&region)
if err != nil {
tc.t.Errorf("FetchBlockRegion(%s): unexpected error: %v",
blockHash, err)
@ -1302,7 +1302,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
// Ensure the block header fetched from the database matches the
// expected bytes.
hasBlock, err := tx.HasBlock(blockHash)
hasBlock, err := dbTx.HasBlock(blockHash)
if err != nil {
tc.t.Errorf("HasBlock(%s): unexpected error: %v",
blockHash, err)
@ -1324,7 +1324,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
testName := fmt.Sprintf("FetchBlock(%s) invalid block",
badBlockHash)
wantErrCode := database.ErrBlockNotFound
_, err = tx.FetchBlock(badBlockHash)
_, err = dbTx.FetchBlock(badBlockHash)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1333,7 +1333,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
// the expected error.
testName = fmt.Sprintf("FetchBlockHeader(%s) invalid block",
badBlockHash)
_, err = tx.FetchBlockHeader(badBlockHash)
_, err = dbTx.FetchBlockHeader(badBlockHash)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1345,7 +1345,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
wantErrCode = database.ErrBlockNotFound
region.Hash = badBlockHash
region.Offset = ^uint32(0)
_, err = tx.FetchBlockRegion(&region)
_, err = dbTx.FetchBlockRegion(&region)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1357,7 +1357,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
wantErrCode = database.ErrBlockRegionInvalid
region.Hash = blockHash
region.Offset = ^uint32(0)
_, err = tx.FetchBlockRegion(&region)
_, err = dbTx.FetchBlockRegion(&region)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1369,7 +1369,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
// Ensure the bulk block data fetched from the database matches the
// expected bytes.
blockData, err := tx.FetchBlocks(allBlockHashes)
blockData, err := dbTx.FetchBlocks(allBlockHashes)
if err != nil {
tc.t.Errorf("FetchBlocks: unexpected error: %v", err)
return false
@ -1393,7 +1393,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
// Ensure the bulk block headers fetched from the database match the
// expected bytes.
blockHeaderData, err := tx.FetchBlockHeaders(allBlockHashes)
blockHeaderData, err := dbTx.FetchBlockHeaders(allBlockHashes)
if err != nil {
tc.t.Errorf("FetchBlockHeaders: unexpected error: %v", err)
return false
@ -1418,7 +1418,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
// Ensure the first transaction of every block fetched in bulk block
// regions from the database matches the expected bytes.
allRegionBytes, err := tx.FetchBlockRegions(allBlockRegions)
allRegionBytes, err := dbTx.FetchBlockRegions(allBlockRegions)
if err != nil {
tc.t.Errorf("FetchBlockRegions: unexpected error: %v", err)
return false
@ -1444,7 +1444,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
// 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)
hasBlocks, err := dbTx.HasBlocks(allBlockHashes)
if err != nil {
tc.t.Errorf("HasBlocks: unexpected error: %v", err)
return false
@ -1467,7 +1467,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
copy(badBlockHashes, allBlockHashes)
badBlockHashes[len(badBlockHashes)-1] = daghash.Hash{}
wantErrCode := database.ErrBlockNotFound
_, err = tx.FetchBlocks(badBlockHashes)
_, err = dbTx.FetchBlocks(badBlockHashes)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1475,7 +1475,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
// Ensure fetching block headers for which one doesn't exist returns the
// expected error.
testName = "FetchBlockHeaders invalid hash"
_, err = tx.FetchBlockHeaders(badBlockHashes)
_, err = dbTx.FetchBlockHeaders(badBlockHashes)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1487,7 +1487,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
copy(badBlockRegions, allBlockRegions)
badBlockRegions[len(badBlockRegions)-1].Hash = &daghash.Hash{}
wantErrCode = database.ErrBlockNotFound
_, err = tx.FetchBlockRegions(badBlockRegions)
_, err = dbTx.FetchBlockRegions(badBlockRegions)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1500,7 +1500,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
badBlockRegions[i].Offset = ^uint32(0)
}
wantErrCode = database.ErrBlockRegionInvalid
_, err = tx.FetchBlockRegions(badBlockRegions)
_, err = dbTx.FetchBlockRegions(badBlockRegions)
return checkDbError(tc.t, testName, err, wantErrCode)
}
@ -1510,11 +1510,11 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
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 {
err := tc.db.View(func(dbTx 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)
err := dbTx.StoreBlock(block)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return errSubTestFail
}
@ -1534,10 +1534,10 @@ func testBlockIOTxInterface(tc *testContext) bool {
// 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 {
err = tc.db.Update(func(dbTx database.Tx) error {
// Store all blocks in the same transaction.
for i, block := range tc.blocks {
err := tx.StoreBlock(block)
err := dbTx.StoreBlock(block)
if err != nil {
tc.t.Errorf("StoreBlock #%d: unexpected error: "+
"%v", i, err)
@ -1551,7 +1551,7 @@ func testBlockIOTxInterface(tc *testContext) bool {
for i, block := range tc.blocks {
testName := fmt.Sprintf("duplicate block entry #%d "+
"(before commit)", i)
err := tx.StoreBlock(block)
err := dbTx.StoreBlock(block)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return errSubTestFail
}
@ -1559,7 +1559,7 @@ func testBlockIOTxInterface(tc *testContext) bool {
// Ensure that all data fetches from the stored blocks before
// the transaction has been committed work as expected.
if !testFetchBlockIO(tc, tx) {
if !testFetchBlockIO(tc, dbTx) {
return errSubTestFail
}
@ -1576,8 +1576,8 @@ func testBlockIOTxInterface(tc *testContext) bool {
}
// Ensure rollback was successful
err = tc.db.View(func(tx database.Tx) error {
if !testFetchBlockIOMissing(tc, tx) {
err = tc.db.View(func(dbTx database.Tx) error {
if !testFetchBlockIOMissing(tc, dbTx) {
return errSubTestFail
}
return nil
@ -1591,10 +1591,10 @@ func testBlockIOTxInterface(tc *testContext) bool {
// 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 {
err = tc.db.Update(func(dbTx database.Tx) error {
// Store a bunch of blocks in the same transaction.
for i, block := range tc.blocks {
err := tx.StoreBlock(block)
err := dbTx.StoreBlock(block)
if err != nil {
tc.t.Errorf("StoreBlock #%d: unexpected error: "+
"%v", i, err)
@ -1609,7 +1609,7 @@ func testBlockIOTxInterface(tc *testContext) bool {
testName := fmt.Sprintf("duplicate block entry #%d "+
"(before commit)", i)
wantErrCode := database.ErrBlockExists
err := tx.StoreBlock(block)
err := dbTx.StoreBlock(block)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return errSubTestFail
}
@ -1617,7 +1617,7 @@ func testBlockIOTxInterface(tc *testContext) bool {
// Ensure that all data fetches from the stored blocks before
// the transaction has been committed work as expected.
if !testFetchBlockIO(tc, tx) {
if !testFetchBlockIO(tc, dbTx) {
return errSubTestFail
}
@ -1633,8 +1633,8 @@ func testBlockIOTxInterface(tc *testContext) bool {
// 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) {
err = tc.db.View(func(dbTx database.Tx) error {
if !testFetchBlockIO(tc, dbTx) {
return errSubTestFail
}
@ -1650,8 +1650,8 @@ func testBlockIOTxInterface(tc *testContext) bool {
// 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) {
err = tc.db.Update(func(dbTx database.Tx) error {
if !testFetchBlockIO(tc, dbTx) {
return errSubTestFail
}
@ -1663,7 +1663,7 @@ func testBlockIOTxInterface(tc *testContext) bool {
for i, block := range tc.blocks {
testName := fmt.Sprintf("duplicate block entry #%d "+
"(before commit)", i)
err := tx.StoreBlock(block)
err := dbTx.StoreBlock(block)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return errSubTestFail
}
@ -1683,10 +1683,10 @@ func testBlockIOTxInterface(tc *testContext) bool {
// 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 {
func testClosedTxInterface(tc *testContext, dbTx database.Tx) bool {
wantErrCode := database.ErrTxClosed
bucket := tx.Metadata()
cursor := tx.Metadata().Cursor()
bucket := dbTx.Metadata()
cursor := dbTx.Metadata().Cursor()
bucketName := []byte("closedtxbucket")
keyName := []byte("closedtxkey")
@ -1852,21 +1852,21 @@ func testClosedTxInterface(tc *testContext, tx database.Tx) bool {
// Ensure StoreBlock returns expected error.
testName = "StoreBlock on closed tx"
err = tx.StoreBlock(block)
err = dbTx.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)
_, err = dbTx.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)
_, err = dbTx.FetchBlockHeader(blockHash)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1879,14 +1879,14 @@ func testClosedTxInterface(tc *testContext, tx database.Tx) bool {
Len: uint32(txLocs[0].TxLen),
}
allBlockRegions[i] = region
_, err = tx.FetchBlockRegion(&region)
_, err = dbTx.FetchBlockRegion(&region)
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)
_, err = dbTx.HasBlock(blockHash)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1898,28 +1898,28 @@ func testClosedTxInterface(tc *testContext, tx database.Tx) bool {
// Ensure FetchBlocks returns expected error.
testName = "FetchBlocks on closed tx"
_, err = tx.FetchBlocks(allBlockHashes)
_, err = dbTx.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)
_, err = dbTx.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)
_, err = dbTx.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)
_, err = dbTx.HasBlocks(allBlockHashes)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return false
}
@ -1930,11 +1930,11 @@ func testClosedTxInterface(tc *testContext, tx database.Tx) bool {
// Ensure that attempting to rollback or commit a transaction that is
// already closed returns the expected error.
err = tx.Rollback()
err = dbTx.Rollback()
if !checkDbError(tc.t, "closed tx rollback", err, wantErrCode) {
return false
}
err = tx.Commit()
err = dbTx.Commit()
return checkDbError(tc.t, "closed tx commit", err, wantErrCode)
}
@ -2003,8 +2003,8 @@ func testConcurrecy(tc *testContext) bool {
// 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())
err := tc.db.View(func(dbTx database.Tx) error {
_, err := dbTx.FetchBlock(tc.blocks[0].Hash())
return err
})
if err != nil {
@ -2024,9 +2024,9 @@ func testConcurrecy(tc *testContext) bool {
numReaders := len(tc.blocks)
resultChan := make(chan bool, numReaders)
reader := func(blockNum int) {
err := tc.db.View(func(tx database.Tx) error {
err := tc.db.View(func(dbTx database.Tx) error {
time.Sleep(sleepTime)
_, err := tx.FetchBlock(tc.blocks[blockNum].Hash())
_, err := dbTx.FetchBlock(tc.blocks[blockNum].Hash())
return err
})
if err != nil {
@ -2092,7 +2092,7 @@ func testConcurrecy(tc *testContext) bool {
started := make(chan struct{})
writeComplete := make(chan struct{})
reader = func(blockNum int) {
err := tc.db.View(func(tx database.Tx) error {
err := tc.db.View(func(dbTx database.Tx) error {
started <- struct{}{}
// Wait for the writer to complete.
@ -2100,7 +2100,7 @@ func testConcurrecy(tc *testContext) bool {
// Since this reader was created before the write took
// place, the data it added should not be visible.
val := tx.Metadata().Get(concurrentKey)
val := dbTx.Metadata().Get(concurrentKey)
if val != nil {
return fmt.Errorf("%s should not be visible",
concurrentKey)
@ -2124,8 +2124,8 @@ func testConcurrecy(tc *testContext) bool {
// 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)
err = tc.db.Update(func(dbTx database.Tx) error {
return dbTx.Metadata().Put(concurrentKey, concurrentVal)
})
if err != nil {
tc.t.Errorf("Unexpected error in update: %v", err)
@ -2145,7 +2145,7 @@ func testConcurrecy(tc *testContext) bool {
// can be active at a time.
writeSleepTime := time.Millisecond * 250
writer := func() {
err := tc.db.Update(func(tx database.Tx) error {
err := tc.db.Update(func(dbTx database.Tx) error {
time.Sleep(writeSleepTime)
return nil
})
@ -2195,7 +2195,7 @@ func testConcurrentClose(tc *testContext) bool {
finishReaders := make(chan struct{})
resultChan := make(chan bool, numReaders+1)
reader := func() {
err := tc.db.View(func(tx database.Tx) error {
err := tc.db.View(func(dbTx database.Tx) error {
atomic.AddInt32(&activeReaders, 1)
started <- struct{}{}
<-finishReaders

View File

@ -60,8 +60,8 @@ func reconcileDB(pdb *db, create bool) (database.DB, error) {
// Load the current write cursor position from the metadata.
var curFileNum, curOffset uint32
err := pdb.View(func(tx database.Tx) error {
writeRow := tx.Metadata().Get(writeLocKeyName)
err := pdb.View(func(dbTx database.Tx) error {
writeRow := dbTx.Metadata().Get(writeLocKeyName)
if writeRow == nil {
str := "write cursor does not exist"
return makeDbErr(database.ErrCorruption, str, nil)

View File

@ -0,0 +1,180 @@
package ffldb
import (
"reflect"
"testing"
"bou.ke/monkey"
)
func TestSerializeWriteRow(t *testing.T) {
tests := []struct {
curBlockFileNum uint32
curFileOffset uint32
expectedWriteRow []byte
}{
// WriteRow format:
// First 4 bytes: curBlockFileNum
// Next 4 bytes: curFileOffset
// Next 4 bytes: Castagnoli CRC-32 checksum
// One can easily calculate checksums using the following code:
// https://play.golang.org/p/zoMKT-ORyF9
{0, 0, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0xB2, 0x28, 0x8C}},
{10, 11, []byte{0x0A, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0xC1, 0xA6, 0x0D, 0xC8}},
}
for i, test := range tests {
actualWriteRow := serializeWriteRow(test.curBlockFileNum, test.curFileOffset)
if !reflect.DeepEqual(test.expectedWriteRow, actualWriteRow) {
t.Errorf("TestSerializeWriteRow: %d: Expected: %v, but got: %v",
i, test.expectedWriteRow, actualWriteRow)
}
}
}
func TestDeserializeWriteRow(t *testing.T) {
tests := []struct {
writeRow []byte
expectedCurBlockFileNum uint32
expectedCurFileOffset uint32
expectedError bool
}{
// WriteRow format:
// First 4 bytes: curBlockFileNum
// Next 4 bytes: curFileOffset
// Next 4 bytes: Castagnoli CRC-32 checksum
// One can easily calculate checksums using the following code:
// https://play.golang.org/p/zoMKT-ORyF9
{[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0xB2, 0x28, 0x8C}, 0, 0, false},
{[]byte{0x0A, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0xC1, 0xA6, 0x0D, 0xC8}, 10, 11, false},
{[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0xB2, 0x28, 0x8D}, 0, 0, true},
{[]byte{0x0A, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0, 0, true},
}
for i, test := range tests {
actualCurBlockFileNum, actualCurFileOffset, err := deserializeWriteRow(test.writeRow)
if (err != nil) != test.expectedError {
t.Errorf("TestDeserializeWriteRow: %d: Expected error status: %t, but got: %t",
i, test.expectedError, err != nil)
}
if test.expectedCurBlockFileNum != actualCurBlockFileNum {
t.Errorf("TestDeserializeWriteRow: %d: Expected curBlockFileNum: %d, but got: %d",
i, test.expectedCurBlockFileNum, actualCurBlockFileNum)
}
if test.expectedCurFileOffset != actualCurFileOffset {
t.Errorf("TestDeserializeWriteRow: %d: Expected curFileOffset: %d, but got: %d",
i, test.expectedCurFileOffset, actualCurFileOffset)
}
}
}
// setWriteRow is a low-level helper method to update the write row in the
// metadata bucket to enable certain test-cases in TestReconcileErrors
// if writeRow = nil deletes the write row altogether
func setWriteRow(pdb *db, writeRow []byte, t *testing.T) {
tx, err := pdb.begin(true)
if err != nil {
t.Fatalf("TestReconcileErrors: Error getting tx for setting "+
"writeLoc in metadata: %s", err)
}
if writeRow == nil {
tx.metaBucket.Delete(writeLocKeyName)
if err != nil {
t.Fatalf("TestReconcileErrors: Error deleting writeLoc from metadata: %s",
err)
}
} else {
tx.metaBucket.Put(writeLocKeyName, writeRow)
if err != nil {
t.Fatalf("TestReconcileErrors: Error updating writeLoc in metadata: %s",
err)
}
}
err = pdb.cache.commitTx(tx)
if err != nil {
t.Fatalf("TestReconcileErrors: Error commiting the update of "+
"writeLoc in metadata: %s", err)
}
pdb.writeLock.Unlock()
pdb.closeLock.RUnlock()
}
// TestReconcileErrors tests all error-cases in reconclieDB.
// The non-error-cases are tested in the more general tests.
func TestReconcileErrors(t *testing.T) {
// Set-up tests
pdb := newTestDb("TestReconcileErrors", t)
// Test without writeLoc
setWriteRow(pdb, nil, t)
_, err := reconcileDB(pdb, false)
if err == nil {
t.Errorf("TestReconcileErrors: ReconcileDB() didn't error out when " +
"running without a writeRowLoc")
}
// Test with writeLoc in metadata after the actual cursor position
setWriteRow(pdb, serializeWriteRow(1, 0), t)
_, err = reconcileDB(pdb, false)
if err == nil {
t.Errorf("TestReconcileErrors: ReconcileDB() didn't error out when " +
"curBlockFileNum after the actual cursor position")
}
setWriteRow(pdb, serializeWriteRow(0, 1), t)
_, err = reconcileDB(pdb, false)
if err == nil {
t.Errorf("TestReconcileErrors: ReconcileDB() didn't error out when " +
"curFileOffset after the actual cursor position")
}
// Restore previous writeRow
setWriteRow(pdb, serializeWriteRow(0, 0), t)
// Test with writeLoc in metadata before the actual cursor position
handleRollbackCalled := false
patch := monkey.Patch((*blockStore).handleRollback,
func(s *blockStore, oldBlockFileNum, oldBlockOffset uint32) {
handleRollbackCalled = true
})
defer patch.Unpatch()
pdb.store.writeCursor.curFileNum = 1
_, err = reconcileDB(pdb, false)
if err != nil {
t.Errorf("TestReconcileErrors: Error in ReconcileDB() when curFileNum before " +
"the actual cursor position ")
}
if !handleRollbackCalled {
t.Errorf("TestReconcileErrors: handleRollback was not called when curFileNum before " +
"the actual cursor position ")
}
pdb.store.writeCursor.curFileNum = 0
pdb.store.writeCursor.curOffset = 1
_, err = reconcileDB(pdb, false)
if err != nil {
t.Errorf("TestReconcileErrors: Error in ReconcileDB() when curOffset before " +
"the actual cursor position ")
}
if !handleRollbackCalled {
t.Errorf("TestReconcileErrors: handleRollback was not called when curOffset before " +
"the actual cursor position ")
}
// Restore previous writeCursor location
pdb.store.writeCursor.curFileNum = 0
pdb.store.writeCursor.curOffset = 0
// Test create with closed DB to force initDB to fail
pdb.Close()
_, err = reconcileDB(pdb, true)
if err == nil {
t.Errorf("ReconcileDB didn't error out when running with closed db and create = true")
}
}

View File

@ -231,7 +231,7 @@ func TestCornerCases(t *testing.T) {
// properly.
testName = "View: underlying leveldb error"
wantErrCode = database.ErrDbNotOpen
err = idb.View(func(tx database.Tx) error {
err = idb.View(func(dbTx database.Tx) error {
return nil
})
if !checkDbError(t, testName, err, wantErrCode) {
@ -241,7 +241,7 @@ func TestCornerCases(t *testing.T) {
// Ensure the Update handles errors in the underlying leveldb database
// properly.
testName = "Update: underlying leveldb error"
err = idb.Update(func(tx database.Tx) error {
err = idb.Update(func(dbTx database.Tx) error {
return nil
})
if !checkDbError(t, testName, err, wantErrCode) {
@ -253,13 +253,13 @@ func TestCornerCases(t *testing.T) {
// test context including all metadata and the mock files.
func resetDatabase(tc *testContext) bool {
// Reset the metadata.
err := tc.db.Update(func(tx database.Tx) error {
err := tc.db.Update(func(dbTx database.Tx) error {
// Remove all the keys using a cursor while also generating a
// list of buckets. It's not safe to remove keys during ForEach
// iteration nor is it safe to remove buckets during cursor
// iteration, so this dual approach is needed.
var bucketNames [][]byte
cursor := tx.Metadata().Cursor()
cursor := dbTx.Metadata().Cursor()
for ok := cursor.First(); ok; ok = cursor.Next() {
if cursor.Value() != nil {
if err := cursor.Delete(); err != nil {
@ -272,12 +272,12 @@ func resetDatabase(tc *testContext) bool {
// Remove the buckets.
for _, k := range bucketNames {
if err := tx.Metadata().DeleteBucket(k); err != nil {
if err := dbTx.Metadata().DeleteBucket(k); err != nil {
return err
}
}
_, err := tx.Metadata().CreateBucket(blockIdxBucketName)
_, err := dbTx.Metadata().CreateBucket(blockIdxBucketName)
return err
})
if err != nil {
@ -360,9 +360,9 @@ func testWriteFailures(tc *testContext) bool {
// file that fails the write fails when the transaction is
// committed, not when the block is stored.
tc.maxFileSizes = map[uint32]int64{test.fileNum: test.maxSize}
err := tc.db.Update(func(tx database.Tx) error {
err := tc.db.Update(func(dbTx database.Tx) error {
for i, block := range tc.blocks {
err := tx.StoreBlock(block)
err := dbTx.StoreBlock(block)
if err != nil {
tc.t.Errorf("StoreBlock (%d): unexpected "+
"error: %v", i, err)
@ -423,8 +423,8 @@ func testBlockFileErrors(tc *testContext) bool {
}
// Insert the first block into the mock file.
err = tc.db.Update(func(tx database.Tx) error {
err := tx.StoreBlock(tc.blocks[0])
err = tc.db.Update(func(dbTx database.Tx) error {
err := dbTx.StoreBlock(tc.blocks[0])
if err != nil {
tc.t.Errorf("StoreBlock: unexpected error: %v", err)
return errSubTestFail
@ -464,10 +464,10 @@ func testBlockFileErrors(tc *testContext) bool {
// Ensure failures in FetchBlock and FetchBlockRegion(s) since the
// underlying file they need to read from has been closed.
err = tc.db.View(func(tx database.Tx) error {
err = tc.db.View(func(dbTx database.Tx) error {
testName = "FetchBlock closed file"
wantErrCode := database.ErrDriverSpecific
_, err := tx.FetchBlock(block0Hash)
_, err := dbTx.FetchBlock(block0Hash)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return errSubTestFail
}
@ -480,13 +480,13 @@ func testBlockFileErrors(tc *testContext) bool {
Offset: 0,
},
}
_, err = tx.FetchBlockRegion(&regions[0])
_, err = dbTx.FetchBlockRegion(&regions[0])
if !checkDbError(tc.t, testName, err, wantErrCode) {
return errSubTestFail
}
testName = "FetchBlockRegions closed file"
_, err = tx.FetchBlockRegions(regions)
_, err = dbTx.FetchBlockRegions(regions)
if !checkDbError(tc.t, testName, err, wantErrCode) {
return errSubTestFail
}
@ -511,8 +511,8 @@ func testCorruption(tc *testContext) bool {
}
// Insert the first block into the mock file.
err := tc.db.Update(func(tx database.Tx) error {
err := tx.StoreBlock(tc.blocks[0])
err := tc.db.Update(func(dbTx database.Tx) error {
err := dbTx.StoreBlock(tc.blocks[0])
if err != nil {
tc.t.Errorf("StoreBlock: unexpected error: %v", err)
return errSubTestFail
@ -556,7 +556,7 @@ func testCorruption(tc *testContext) bool {
// Random checksum byte.
{uint32(len(block0Bytes)) + 10, false, database.ErrCorruption},
}
err = tc.db.View(func(tx database.Tx) error {
err = tc.db.View(func(dbTx database.Tx) error {
data := tc.files[0].file.(*mockFile).data
for i, test := range tests {
// Corrupt the byte at the offset by a single bit.
@ -574,7 +574,7 @@ func testCorruption(tc *testContext) bool {
testName := fmt.Sprintf("FetchBlock (test #%d): "+
"corruption", i)
_, err := tx.FetchBlock(block0Hash)
_, err := dbTx.FetchBlock(block0Hash)
if !checkDbError(tc.t, testName, err, test.wantErrCode) {
return errSubTestFail
}

View File

@ -2132,8 +2132,8 @@ func (s *Server) Start() {
func (s *Server) Stop() error {
// Save fee estimator state in the database.
s.db.Update(func(tx database.Tx) error {
metadata := tx.Metadata()
s.db.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
metadata.Put(mempool.EstimateFeeDatabaseKey, s.FeeEstimator.Save())
return nil
@ -2396,8 +2396,8 @@ func NewServer(listenAddrs []string, db database.DB, dagParams *dagconfig.Params
// Search for a FeeEstimator state in the database. If none can be found
// or if it cannot be loaded, create a new one.
db.Update(func(tx database.Tx) error {
metadata := tx.Metadata()
db.Update(func(dbTx database.Tx) error {
metadata := dbTx.Metadata()
feeEstimationData := metadata.Get(mempool.EstimateFeeDatabaseKey)
if feeEstimationData != nil {
// delete it from the database so that we don't try to restore the

View File

@ -314,6 +314,9 @@ func scriptError(c ErrorCode, desc string) Error {
// IsErrorCode returns whether or not the provided error is a script error with
// the provided error code.
func IsErrorCode(err error, c ErrorCode) bool {
serr, ok := err.(Error)
return ok && serr.ErrorCode == c
if err, ok := err.(Error); ok {
return err.ErrorCode == c
}
return false
}