lbcd/interface_test.go
Dave Collins 9ec86518ac Improve interface test error messages.
This commit changes the interface test errors to all report the block
height and hash prior to the failure.  Test failures that transactions
also now report the transaction number and hash.  The makes it much
easier to track down where a test failure occurs.
2013-10-14 18:27:12 -05:00

508 lines
16 KiB
Go

// Copyright (c) 2013 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcdb_test
import (
"github.com/conformal/btcdb"
"github.com/conformal/btcutil"
"github.com/conformal/btcwire"
"github.com/davecgh/go-spew/spew"
"reflect"
"testing"
)
// testContext is used to store context information about a running test which
// is passed into helper functions.
type testContext struct {
t *testing.T
dbType string
db btcdb.Db
blockHeight int64
blockHash *btcwire.ShaHash
block *btcutil.Block
}
// testInsertBlock ensures InsertBlock conforms to the interface contract.
func testInsertBlock(tc *testContext) bool {
// The block must insert without any errors.
newHeight, err := tc.db.InsertBlock(tc.block)
if err != nil {
tc.t.Errorf("InsertBlock (%s): failed to insert block #%d (%s) "+
"err %v", tc.dbType, tc.blockHeight, tc.blockHash, err)
return false
}
// The returned height must be the expected value.
if newHeight != tc.blockHeight {
tc.t.Errorf("InsertBlock (%s): height mismatch got: %v, "+
"want: %v", tc.dbType, newHeight, tc.blockHeight)
return false
}
return true
}
// testExistsSha ensures ExistsSha conforms to the interface contract.
func testExistsSha(tc *testContext) bool {
// The block must exist in the database.
if exists := tc.db.ExistsSha(tc.blockHash); !exists {
tc.t.Errorf("ExistsSha (%s): block #%d (%s) does not exist",
tc.dbType, tc.blockHeight, tc.blockHash)
return false
}
return true
}
// testFetchBlockBySha ensures FetchBlockBySha conforms to the interface
// contract.
func testFetchBlockBySha(tc *testContext) bool {
// The block must be fetchable by its hash without any errors.
blockFromDb, err := tc.db.FetchBlockBySha(tc.blockHash)
if err != nil {
tc.t.Errorf("FetchBlockBySha (%s): block #%d (%s) err: %v",
tc.dbType, tc.blockHeight, tc.blockHash, err)
return false
}
// The block fetched from the database must give back the same MsgBlock
// and raw bytes that were stored.
if !reflect.DeepEqual(tc.block.MsgBlock(), blockFromDb.MsgBlock()) {
tc.t.Errorf("FetchBlockBySha (%s): block #%d (%s) does not "+
"match stored block\ngot: %v\nwant: %v", tc.dbType,
tc.blockHeight, tc.blockHash,
spew.Sdump(blockFromDb.MsgBlock()),
spew.Sdump(tc.block.MsgBlock()))
return false
}
blockBytes, err := tc.block.Bytes()
if err != nil {
tc.t.Errorf("block.Bytes: %v", err)
return false
}
blockFromDbBytes, err := blockFromDb.Bytes()
if err != nil {
tc.t.Errorf("blockFromDb.Bytes: %v", err)
return false
}
if !reflect.DeepEqual(blockBytes, blockFromDbBytes) {
tc.t.Errorf("FetchBlockBySha (%s): block #%d (%s) bytes do "+
"not match stored bytes\ngot: %v\nwant: %v", tc.dbType,
tc.blockHeight, tc.blockHash,
spew.Sdump(blockFromDbBytes), spew.Sdump(blockBytes))
return false
}
return true
}
// testFetchBlockShaByHeight ensures FetchBlockShaByHeight conforms to the
// interface contract.
func testFetchBlockShaByHeight(tc *testContext) bool {
// The hash returned for the block by its height must be the expected
// value.
hashFromDb, err := tc.db.FetchBlockShaByHeight(tc.blockHeight)
if err != nil {
tc.t.Errorf("FetchBlockShaByHeight (%s): block #%d (%s) err: %v",
tc.dbType, tc.blockHeight, tc.blockHash, err)
return false
}
if !hashFromDb.IsEqual(tc.blockHash) {
tc.t.Errorf("FetchBlockShaByHeight (%s): block #%d (%s) hash "+
"does not match expected value - got: %v", tc.dbType,
tc.blockHeight, tc.blockHash, hashFromDb)
return false
}
// Invalid heights must error and return a nil hash.
tests := []int64{-1, tc.blockHeight + 1, tc.blockHeight + 2}
for i, wantHeight := range tests {
hashFromDb, err := tc.db.FetchBlockShaByHeight(wantHeight)
if err == nil {
tc.t.Errorf("FetchBlockShaByHeight #%d (%s): did not "+
"return error on invalid index: %d - got: %v, "+
"want: non-nil", i, tc.dbType, wantHeight, err)
return false
}
if hashFromDb != nil {
tc.t.Errorf("FetchBlockShaByHeight #%d (%s): returned "+
"hash is not nil on invalid index: %d - got: "+
"%v, want: nil", i, tc.dbType, wantHeight, err)
return false
}
}
return true
}
// testExistsTxSha ensures ExistsTxSha conforms to the interface contract.
func testExistsTxSha(tc *testContext) bool {
txHashes, err := tc.block.TxShas()
if err != nil {
tc.t.Errorf("block.TxShas: %v", err)
return false
}
for i := range txHashes {
// The transaction must exist in the database.
txHash := txHashes[i]
if exists := tc.db.ExistsTxSha(txHash); !exists {
tc.t.Errorf("ExistsTxSha (%s): block #%d (%s) "+
"tx #%d (%s) does not exist", tc.dbType,
tc.blockHeight, tc.blockHash, i, txHash)
return false
}
}
return true
}
// testFetchTxBySha ensures FetchTxBySha conforms to the interface contract.
func testFetchTxBySha(tc *testContext) bool {
txHashes, err := tc.block.TxShas()
if err != nil {
tc.t.Errorf("block.TxShas: %v", err)
return false
}
for i, tx := range tc.block.MsgBlock().Transactions {
txHash := txHashes[i]
txReplyList, err := tc.db.FetchTxBySha(txHash)
if err != nil {
tc.t.Errorf("FetchTxBySha (%s): block #%d (%s) "+
"tx #%d (%s) err: %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, err)
return false
}
if len(txReplyList) == 0 {
tc.t.Errorf("FetchTxBySha (%s): block #%d (%s) "+
"tx #%d (%s) did not return reply data",
tc.dbType, tc.blockHeight, tc.blockHash, i,
txHash)
return false
}
txFromDb := txReplyList[len(txReplyList)-1].Tx
if !reflect.DeepEqual(tx, txFromDb) {
tc.t.Errorf("FetchTxBySha (%s): block #%d (%s) "+
"tx #%d (%s) does not match stored tx\n"+
"got: %v\nwant: %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, spew.Sdump(txFromDb),
spew.Sdump(tx))
return false
}
}
return true
}
// testFetchTxByShaList ensures FetchTxByShaList conforms to the interface
// contract.
func testFetchTxByShaList(tc *testContext) bool {
txHashes, err := tc.block.TxShas()
if err != nil {
tc.t.Errorf("block.TxShas: %v", err)
return false
}
txReplyList := tc.db.FetchTxByShaList(txHashes)
if len(txReplyList) != len(txHashes) {
tc.t.Errorf("FetchTxByShaList (%s): block #%d (%s) "+
"tx reply list does not match expected length "+
"- got: %v, want: %v", tc.dbType, tc.blockHeight,
tc.blockHash, len(txReplyList), len(txHashes))
return false
}
for i, tx := range tc.block.MsgBlock().Transactions {
txHash := txHashes[i]
txD := txReplyList[i]
// The transaction hash in the reply must be the expected value.
if !txD.Sha.IsEqual(txHash) {
tc.t.Errorf("FetchTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) hash does not match expected "+
"value - got %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, txD.Sha)
return false
}
// The reply must not indicate any errors.
if txD.Err != nil {
tc.t.Errorf("FetchTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned unexpected error - "+
"got %v, want nil", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, txD.Err)
return false
}
// The transaction in the reply fetched from the database must
// be the same MsgTx that was stored.
if !reflect.DeepEqual(tx, txD.Tx) {
tc.t.Errorf("FetchTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) does not match stored tx\n"+
"got: %v\nwant: %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, spew.Sdump(txD.Tx),
spew.Sdump(tx))
return false
}
// The block hash in the reply from the database must be the
// expected value.
if txD.BlkSha == nil {
tc.t.Errorf("FetchTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned nil block hash", tc.dbType,
tc.blockHeight, tc.blockHash, i, txHash)
return false
}
if !txD.BlkSha.IsEqual(tc.blockHash) {
tc.t.Errorf("FetchTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned unexpected block hash - "+
"got %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, txD.BlkSha)
return false
}
// The block height in the reply from the database must be the
// expected value.
if txD.Height != tc.blockHeight {
tc.t.Errorf("FetchTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned unexpected block height "+
"- got %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, txD.Height)
return false
}
// The spend data in the reply from the database must not
// indicate any of the transactions that were just inserted are
// spent.
if txD.TxSpent == nil {
tc.t.Errorf("FetchTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned nil spend data", tc.dbType,
tc.blockHeight, tc.blockHash, i, txHash)
return false
}
noSpends := make([]bool, len(tx.TxOut))
if !reflect.DeepEqual(txD.TxSpent, noSpends) {
tc.t.Errorf("FetchTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned unexpected spend data - "+
"got %v, want %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, txD.TxSpent, noSpends)
return false
}
}
return true
}
// testFetchUnSpentTxByShaList ensures FetchUnSpentTxByShaList conforms to the
// interface contract.
func testFetchUnSpentTxByShaList(tc *testContext) bool {
txHashes, err := tc.block.TxShas()
if err != nil {
tc.t.Errorf("block.TxShas: %v", err)
return false
}
txReplyList := tc.db.FetchUnSpentTxByShaList(txHashes)
if len(txReplyList) != len(txHashes) {
tc.t.Errorf("FetchUnSpentTxByShaList (%s): block #%d (%s) "+
"tx reply list does not match expected length "+
"- got: %v, want: %v", tc.dbType, tc.blockHeight,
tc.blockHash, len(txReplyList), len(txHashes))
return false
}
for i, tx := range tc.block.MsgBlock().Transactions {
txHash := txHashes[i]
txD := txReplyList[i]
// The transaction hash in the reply must be the expected value.
if !txD.Sha.IsEqual(txHash) {
tc.t.Errorf("FetchUnSpentTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) hash does not match expected "+
"value - got %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, txD.Sha)
return false
}
// The reply must not indicate any errors.
if txD.Err != nil {
tc.t.Errorf("FetchUnSpentTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned unexpected error - "+
"got %v, want nil", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, txD.Err)
return false
}
// The transaction in the reply fetched from the database must
// be the same MsgTx that was stored.
if !reflect.DeepEqual(tx, txD.Tx) {
tc.t.Errorf("FetchUnSpentTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) does not match stored tx\n"+
"got: %v\nwant: %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, spew.Sdump(txD.Tx),
spew.Sdump(tx))
return false
}
// The block hash in the reply from the database must be the
// expected value.
if txD.BlkSha == nil {
tc.t.Errorf("FetchUnSpentTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned nil block hash", tc.dbType,
tc.blockHeight, tc.blockHash, i, txHash)
return false
}
if !txD.BlkSha.IsEqual(tc.blockHash) {
tc.t.Errorf("FetchUnSpentTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned unexpected block hash - "+
"got %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, txD.BlkSha)
return false
}
// The block height in the reply from the database must be the
// expected value.
if txD.Height != tc.blockHeight {
tc.t.Errorf("FetchUnSpentTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned unexpected block height "+
"- got %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, txD.Height)
return false
}
// The spend data in the reply from the database must not
// indicate any of the transactions that were just inserted are
// spent.
if txD.TxSpent == nil {
tc.t.Errorf("FetchUnSpentTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned nil spend data", tc.dbType,
tc.blockHeight, tc.blockHash, i, txHash)
return false
}
noSpends := make([]bool, len(tx.TxOut))
if !reflect.DeepEqual(txD.TxSpent, noSpends) {
tc.t.Errorf("FetchUnSpentTxByShaList (%s): block #%d (%s) "+
"tx #%d (%s) returned unexpected spend data - "+
"got %v, want %v", tc.dbType, tc.blockHeight,
tc.blockHash, i, txHash, txD.TxSpent, noSpends)
return false
}
}
return true
}
// testInterface tests performs tests for the various interfaces of btcdb which
// require state in the database for the given database type.
func testInterface(t *testing.T, dbType string) {
db, teardown, err := setupDB(dbType, "interface")
if err != nil {
t.Errorf("Failed to create test database (%s) %v", dbType, err)
return
}
defer teardown()
// Load up a bunch of test blocks.
blocks, err := loadBlocks(t)
if err != nil {
t.Errorf("Unable to load blocks from test data %v: %v",
blockDataFile, err)
return
}
// Create a test context to pass around.
context := testContext{t: t, dbType: dbType, db: db}
t.Logf("Loaded %d blocks for testing %s", len(blocks), dbType)
for height := int64(1); height < int64(len(blocks)); height++ {
// Get the appropriate block and hash and update the test
// context accordingly.
block := blocks[height]
blockHash, err := block.Sha()
if err != nil {
t.Errorf("block.Sha: %v", err)
return
}
context.blockHeight = height
context.blockHash = blockHash
context.block = block
// The block must insert without any errors and return the
// expected height.
if !testInsertBlock(&context) {
return
}
// The block must now exist in the database.
if !testExistsSha(&context) {
return
}
// Loading the block back from the database must give back
// the same MsgBlock and raw bytes that were stored.
if !testFetchBlockBySha(&context) {
return
}
// The hash returned for the block by its height must be the
// expected value.
if !testFetchBlockShaByHeight(&context) {
return
}
// All of the transactions in the block must now exist in the
// database.
if !testExistsTxSha(&context) {
return
}
// Loading all of the transactions in the block back from the
// database must give back the same MsgTx that was stored.
if !testFetchTxBySha(&context) {
return
}
// All of the transactions in the block must be fetchable via
// FetchTxByShaList and all of the list replies must have the
// expected values.
if !testFetchTxByShaList(&context) {
return
}
// All of the transactions in the block must be fetchable via
// FetchUnSpentTxByShaList and all of the list replies must have
// the expected values.
if !testFetchUnSpentTxByShaList(&context) {
return
}
}
// TODO(davec): Need to figure out how to handle the special checks
// required for the duplicate transactions allowed by blocks 91842 and
// 91880 on the main network due to the old miner + Satoshi client bug.
// TODO(davec): Add tests for the following functions:
/*
Close()
DropAfterBlockBySha(*btcwire.ShaHash) (err error)
- ExistsSha(sha *btcwire.ShaHash) (exists bool)
- FetchBlockBySha(sha *btcwire.ShaHash) (blk *btcutil.Block, err error)
- FetchBlockShaByHeight(height int64) (sha *btcwire.ShaHash, err error)
FetchHeightRange(startHeight, endHeight int64) (rshalist []btcwire.ShaHash, err error)
- ExistsTxSha(sha *btcwire.ShaHash) (exists bool)
- FetchTxBySha(txsha *btcwire.ShaHash) ([]*TxListReply, error)
- FetchTxByShaList(txShaList []*btcwire.ShaHash) []*TxListReply
- FetchUnSpentTxByShaList(txShaList []*btcwire.ShaHash) []*TxListReply
- InsertBlock(block *btcutil.Block) (height int64, err error)
InvalidateBlockCache()
InvalidateCache()
InvalidateTxCache()
NewIterateBlocks() (pbi BlockIterator, err error)
NewestSha() (sha *btcwire.ShaHash, height int64, err error)
RollbackClose()
SetDBInsertMode(InsertMode)
Sync()
*/
}