[NOD-849] Database tests (#695)

* [NOD-849] Cover ffldb/transaction with tests.

* [NOD-849] Cover cursor.go with tests.

* [NOD-849] Cover ldb/transaction with tests.

* [NOD-849] Cover location.go with tests.

* [NOD-849] Write TestFlatFileMultiFileRollback.

* [NOD-849] Fix merge errors.

* [NOD-849] Fix a comment.

* [NOD-849] Fix a comment.

* [NOD-849] Add a test that makes sure that files get deleted on rollback.

* [NOD-849] Add a test that makes sure that serializeLocation serialized to an expected value.

* [NOD-849] Improve TestFlatFileLocationDeserializationErrors.

* [NOD-849] Fix a copy+paste error.

* [NOD-849] Explain maxFileSize = 16.

* [NOD-849] Remove redundant RollbackUnlessClosed call.

* [NOD-849] Extract bucket to a variable in TestCursorSanity.

* [NOD-849] Rename TestKeyValueTransactionCommit to TestTransactionCommitForLevelDBMethods.

* [NOD-849] Extract prepareXXX into separate functions.

* [NOD-849] Simplify function calls in TestTransactionCloseErrors.

* [NOD-849] Extract validateCurrentCursorKeyAndValue to a separate function.

* [NOD-849] Add a comment over TestCursorSanity.

* [NOD-849] Add a comment over function in TestCursorCloseErrors.

* [NOD-849] Add a comment over function in TestTransactionCloseErrors.

* [NOD-849] Separate TestTransactionCloseErrors to TestTransactionCommitErrors and TestTransactionRollbackErrors.

* [NOD-849] Separate TestTransactionCloseErrors to TestTransactionCommitErrors and TestTransactionRollbackErrors.

* [NOD-849] Fix copy+paste error in comments.

* [NOD-849] Fix merge errors.

* [NOD-849] Merge TestTransactionCommitErrors and TestTransactionRollbackErrors into TestTransactionCloseErrors.

* [NOD-849] Move prepareDatabaseForTest into ffldb_test.go.

* [NOD-849] Add cursorKey to Value error messages in validateCurrentCursorKeyAndValue.
This commit is contained in:
stasatdaglabs 2020-05-03 12:19:09 +03:00 committed by GitHub
parent 2ef5c2cbac
commit 2e2492cc5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1161 additions and 46 deletions

View File

@ -16,12 +16,14 @@ const (
// cache. Note that this does not include the current/write file, so there
// will typically be one more than this value open.
maxOpenFiles = 25
)
var (
// maxFileSize is the maximum size for each file used to store data.
//
// NOTE: The current code uses uint32 for all offsets, so this value
// must be less than 2^32 (4 GiB). This is also why it's a typed
// constant.
// must be less than 2^32 (4 GiB).
// NOTE: This is a var rather than a const for testing purposes.
maxFileSize uint32 = 512 * 1024 * 1024 // 512 MiB
)

View File

@ -1,24 +1,40 @@
package ff
import (
"bytes"
"github.com/kaspanet/kaspad/database"
"io/ioutil"
"os"
"reflect"
"testing"
)
func TestFlatFileStoreSanity(t *testing.T) {
// Open a test store
path, err := ioutil.TempDir("", "TestFlatFileStoreSanity")
func prepareStoreForTest(t *testing.T, testName string) (store *flatFileStore, teardownFunc func()) {
// Create a temp db to run tests against
path, err := ioutil.TempDir("", testName)
if err != nil {
t.Fatalf("TestFlatFileStoreSanity: TempDir unexpectedly "+
"failed: %s", err)
t.Fatalf("%s: TempDir unexpectedly "+
"failed: %s", testName, err)
}
name := "test"
store, err := openFlatFileStore(path, name)
store, err = openFlatFileStore(path, name)
if err != nil {
t.Fatalf("TestFlatFileStoreSanity: openFlatFileStore "+
"unexpectedly failed: %s", err)
t.Fatalf("%s: openFlatFileStore "+
"unexpectedly failed: %s", testName, err)
}
teardownFunc = func() {
err = store.Close()
if err != nil {
t.Fatalf("%s: Close unexpectedly "+
"failed: %s", testName, err)
}
}
return store, teardownFunc
}
func TestFlatFileStoreSanity(t *testing.T) {
store, teardownFunc := prepareStoreForTest(t, "TestFlatFileStoreSanity")
defer teardownFunc()
// Write something to the store
writeData := []byte("Hello world!")
@ -72,3 +88,88 @@ func TestFlatFilePath(t *testing.T) {
}
}
}
func TestFlatFileMultiFileRollback(t *testing.T) {
store, teardownFunc := prepareStoreForTest(t, "TestFlatFileMultiFileRollback")
defer teardownFunc()
// Set the maxFileSize to 16 bytes so that we don't have to write
// an enormous amount of data to disk to get multiple files, all
// for the sake of this test.
currentMaxFileSize := maxFileSize
maxFileSize = 16
defer func() {
maxFileSize = currentMaxFileSize
}()
// Write five 8 byte chunks and keep the last location written to
var lastWriteLocation1 *flatFileLocation
for i := byte(0); i < 5; i++ {
writeData := []byte{i, i, i, i, i, i, i, i}
var err error
lastWriteLocation1, err = store.write(writeData)
if err != nil {
t.Fatalf("TestFlatFileMultiFileRollback: write returned "+
"unexpected error: %s", err)
}
}
// Grab the current location and the current file number
currentLocation := store.currentLocation()
fileNumberBeforeWriting := store.writeCursor.currentFileNumber
// Write (2 * maxOpenFiles) more 8 byte chunks and keep the last location written to
var lastWriteLocation2 *flatFileLocation
for i := byte(0); i < byte(2*maxFileSize); i++ {
writeData := []byte{0, 1, 2, 3, 4, 5, 6, 7}
var err error
lastWriteLocation2, err = store.write(writeData)
if err != nil {
t.Fatalf("TestFlatFileMultiFileRollback: write returned "+
"unexpected error: %s", err)
}
}
// Grab the file number again to later make sure its file no longer exists
fileNumberAfterWriting := store.writeCursor.currentFileNumber
// Rollback
err := store.rollback(currentLocation)
if err != nil {
t.Fatalf("TestFlatFileMultiFileRollback: rollback returned "+
"unexpected error: %s", err)
}
// Make sure that lastWriteLocation1 still exists
expectedData := []byte{4, 4, 4, 4, 4, 4, 4, 4}
data, err := store.read(lastWriteLocation1)
if err != nil {
t.Fatalf("TestFlatFileMultiFileRollback: read returned "+
"unexpected error: %s", err)
}
if !bytes.Equal(data, expectedData) {
t.Fatalf("TestFlatFileMultiFileRollback: read returned "+
"unexpected data. Want: %s, got: %s", string(expectedData),
string(data))
}
// Make sure that lastWriteLocation2 does NOT exist
_, err = store.read(lastWriteLocation2)
if err == nil {
t.Fatalf("TestFlatFileMultiFileRollback: read " +
"unexpectedly succeeded")
}
if !database.IsNotFoundError(err) {
t.Fatalf("TestFlatFileMultiFileRollback: read "+
"returned unexpected error: %s", err)
}
// Make sure that all the appropriate files have been deleted
for i := fileNumberAfterWriting; i > fileNumberBeforeWriting; i-- {
filePath := flatFilePath(store.basePath, store.storeName, i)
if _, err := os.Stat(filePath); err == nil || !os.IsNotExist(err) {
t.Fatalf("TestFlatFileMultiFileRollback: file "+
"unexpectedly still exists: %s", filePath)
}
}
}

View File

@ -0,0 +1,62 @@
package ff
import (
"bytes"
"encoding/hex"
"reflect"
"strings"
"testing"
)
func TestFlatFileLocationSerialization(t *testing.T) {
location := &flatFileLocation{
fileNumber: 1,
fileOffset: 2,
dataLength: 3,
}
serializedLocation := serializeLocation(location)
expectedSerializedLocation := []byte{1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0}
if !bytes.Equal(serializedLocation, expectedSerializedLocation) {
t.Fatalf("TestFlatFileLocationSerialization: serializeLocation "+
"returned unexpected bytes. Want: %s, got: %s",
hex.EncodeToString(expectedSerializedLocation), hex.EncodeToString(serializedLocation))
}
deserializedLocation, err := deserializeLocation(serializedLocation)
if err != nil {
t.Fatalf("TestFlatFileLocationSerialization: deserializeLocation "+
"unexpectedly failed: %s", err)
}
if !reflect.DeepEqual(deserializedLocation, location) {
t.Fatalf("TestFlatFileLocationSerialization: original "+
"location and deserialized location aren't the same. Want: %v, "+
"got: %v", location, deserializedLocation)
}
}
func TestFlatFileLocationDeserializationErrors(t *testing.T) {
expectedError := "unexpected serializedLocation length"
tooShortSerializedLocation := []byte{0, 1, 2, 3, 4, 5}
_, err := deserializeLocation(tooShortSerializedLocation)
if err == nil {
t.Fatalf("TestFlatFileLocationSerialization: deserializeLocation " +
"unexpectedly succeeded")
}
if !strings.Contains(err.Error(), expectedError) {
t.Fatalf("TestFlatFileLocationSerialization: deserializeLocation "+
"returned unexpected error. Want: %s, got: %s", expectedError, err)
}
tooLongSerializedLocation := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}
_, err = deserializeLocation(tooLongSerializedLocation)
if err == nil {
t.Fatalf("TestFlatFileLocationSerialization: deserializeLocation " +
"unexpectedly succeeded")
}
if !strings.Contains(err.Error(), expectedError) {
t.Fatalf("TestFlatFileLocationSerialization: deserializeLocation "+
"returned unexpected error. Want: %s, got: %s", expectedError, err)
}
}

View File

@ -170,8 +170,9 @@ func (db *ffldb) Begin() (database.Transaction, error) {
}
transaction := &transaction{
ldbTx: ldbTx,
ffdb: db.flatFileDB,
ldbTx: ldbTx,
ffdb: db.flatFileDB,
isClosed: false,
}
return transaction, nil
}

View File

@ -7,6 +7,28 @@ import (
"testing"
)
func prepareDatabaseForTest(t *testing.T, testName string) (db database.Database, teardownFunc func()) {
// Create a temp db to run tests against
path, err := ioutil.TempDir("", testName)
if err != nil {
t.Fatalf("%s: TempDir unexpectedly "+
"failed: %s", testName, err)
}
db, err = Open(path)
if err != nil {
t.Fatalf("%s: Open unexpectedly "+
"failed: %s", testName, err)
}
teardownFunc = func() {
err = db.Close()
if err != nil {
t.Fatalf("%s: Close unexpectedly "+
"failed: %s", testName, err)
}
}
return db, teardownFunc
}
func TestRepairFlatFiles(t *testing.T) {
// Create a temp db to run tests against
path, err := ioutil.TempDir("", "TestRepairFlatFiles")

View File

@ -0,0 +1,246 @@
package ldb
import (
"bytes"
"fmt"
"github.com/kaspanet/kaspad/database"
"reflect"
"strings"
"testing"
)
func validateCurrentCursorKeyAndValue(t *testing.T, testName string, cursor *LevelDBCursor,
expectedKey *database.Key, expectedValue []byte) {
cursorKey, err := cursor.Key()
if err != nil {
t.Fatalf("%s: Key "+
"unexpectedly failed: %s", testName, err)
}
if !reflect.DeepEqual(cursorKey, expectedKey) {
t.Fatalf("%s: Key "+
"returned wrong key. Want: %s, got: %s",
testName, string(expectedKey.Bytes()), string(cursorKey.Bytes()))
}
cursorValue, err := cursor.Value()
if err != nil {
t.Fatalf("%s: Value "+
"unexpectedly failed for key %s: %s",
testName, cursorKey, err)
}
if !bytes.Equal(cursorValue, expectedValue) {
t.Fatalf("%s: Value "+
"returned wrong value for key %s. Want: %s, got: %s",
testName, cursorKey, string(expectedValue), string(cursorValue))
}
}
func recoverFromClosedCursorPanic(t *testing.T, testName string) {
panicErr := recover()
if panicErr == nil {
t.Fatalf("%s: cursor unexpectedly "+
"didn't panic after being closed", testName)
}
expectedPanicErr := "closed cursor"
if !strings.Contains(fmt.Sprintf("%v", panicErr), expectedPanicErr) {
t.Fatalf("%s: cursor panicked "+
"with wrong message. Want: %v, got: %s",
testName, expectedPanicErr, panicErr)
}
}
// TestCursorSanity validates typical cursor usage, including
// opening a cursor over some existing data, seeking back
// and forth over that data, and getting some keys/values out
// of the cursor.
func TestCursorSanity(t *testing.T) {
ldb, teardownFunc := prepareDatabaseForTest(t, "TestCursorSanity")
defer teardownFunc()
// Write some data to the database
bucket := database.MakeBucket([]byte("bucket"))
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key%d", i)
value := fmt.Sprintf("value%d", i)
err := ldb.Put(bucket.Key([]byte(key)), []byte(value))
if err != nil {
t.Fatalf("TestCursorSanity: Put "+
"unexpectedly failed: %s", err)
}
}
// Open a new cursor
cursor := ldb.Cursor(bucket)
defer func() {
err := cursor.Close()
if err != nil {
t.Fatalf("TestCursorSanity: Close "+
"unexpectedly failed: %s", err)
}
}()
// Seek to first key and make sure its key and value are correct
hasNext := cursor.First()
if !hasNext {
t.Fatalf("TestCursorSanity: First " +
"unexpectedly returned non-existance")
}
expectedKey := bucket.Key([]byte("key0"))
expectedValue := []byte("value0")
validateCurrentCursorKeyAndValue(t, "TestCursorSanity", cursor, expectedKey, expectedValue)
// Seek to a non-existant key
err := cursor.Seek(database.MakeBucket().Key([]byte("doesn't exist")))
if err == nil {
t.Fatalf("TestCursorSanity: Seek " +
"unexpectedly succeeded")
}
if !database.IsNotFoundError(err) {
t.Fatalf("TestCursorSanity: Seek "+
"returned wrong error: %s", err)
}
// Seek to the last key
err = cursor.Seek(bucket.Key([]byte("key9")))
if err != nil {
t.Fatalf("TestCursorSanity: Seek "+
"unexpectedly failed: %s", err)
}
expectedKey = bucket.Key([]byte("key9"))
expectedValue = []byte("value9")
validateCurrentCursorKeyAndValue(t, "TestCursorSanity", cursor, expectedKey, expectedValue)
// Call Next to get to the end of the cursor. This should
// return false to signify that there are no items after that.
// Key and Value calls should return ErrNotFound.
hasNext = cursor.Next()
if hasNext {
t.Fatalf("TestCursorSanity: Next " +
"after last value is unexpectedly not done")
}
_, err = cursor.Key()
if err == nil {
t.Fatalf("TestCursorSanity: Key " +
"unexpectedly succeeded")
}
if !database.IsNotFoundError(err) {
t.Fatalf("TestCursorSanity: Key "+
"returned wrong error: %s", err)
}
_, err = cursor.Value()
if err == nil {
t.Fatalf("TestCursorSanity: Value " +
"unexpectedly succeeded")
}
if !database.IsNotFoundError(err) {
t.Fatalf("TestCursorSanity: Value "+
"returned wrong error: %s", err)
}
}
func TestCursorCloseErrors(t *testing.T) {
tests := []struct {
name string
// function is the LevelDBCursor function that we're
// verifying returns an error after the cursor had
// been closed.
function func(dbTx *LevelDBCursor) error
}{
{
name: "Seek",
function: func(cursor *LevelDBCursor) error {
return cursor.Seek(database.MakeBucket().Key([]byte{}))
},
},
{
name: "Key",
function: func(cursor *LevelDBCursor) error {
_, err := cursor.Key()
return err
},
},
{
name: "Value",
function: func(cursor *LevelDBCursor) error {
_, err := cursor.Value()
return err
},
},
{
name: "Close",
function: func(cursor *LevelDBCursor) error {
return cursor.Close()
},
},
}
for _, test := range tests {
func() {
ldb, teardownFunc := prepareDatabaseForTest(t, "TestCursorCloseErrors")
defer teardownFunc()
// Open a new cursor
cursor := ldb.Cursor(database.MakeBucket())
// Close the cursor
err := cursor.Close()
if err != nil {
t.Fatalf("TestCursorCloseErrors: Close "+
"unexpectedly failed: %s", err)
}
expectedErrContainsString := "closed cursor"
// Make sure that the test function returns a "closed transaction" error
err = test.function(cursor)
if err == nil {
t.Fatalf("TestCursorCloseErrors: %s "+
"unexpectedly succeeded", test.name)
}
if !strings.Contains(err.Error(), expectedErrContainsString) {
t.Fatalf("TestCursorCloseErrors: %s "+
"returned wrong error. Want: %s, got: %s",
test.name, expectedErrContainsString, err)
}
}()
}
}
func TestCursorCloseFirstAndNext(t *testing.T) {
ldb, teardownFunc := prepareDatabaseForTest(t, "TestCursorCloseFirstAndNext")
defer teardownFunc()
// Write some data to the database
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key%d", i)
value := fmt.Sprintf("value%d", i)
err := ldb.Put(database.MakeBucket([]byte("bucket")).Key([]byte(key)), []byte(value))
if err != nil {
t.Fatalf("TestCursorCloseFirstAndNext: Put "+
"unexpectedly failed: %s", err)
}
}
// Open a new cursor
cursor := ldb.Cursor(database.MakeBucket([]byte("bucket")))
// Close the cursor
err := cursor.Close()
if err != nil {
t.Fatalf("TestCursorCloseFirstAndNext: Close "+
"unexpectedly failed: %s", err)
}
// We expect First to panic
func() {
defer recoverFromClosedCursorPanic(t, "TestCursorCloseFirstAndNext")
cursor.First()
}()
// We expect Next to panic
func() {
defer recoverFromClosedCursorPanic(t, "TestCursorCloseFirstAndNext")
cursor.Next()
}()
}

View File

@ -7,30 +7,36 @@ import (
"testing"
)
func TestLevelDBSanity(t *testing.T) {
// Open a test db
path, err := ioutil.TempDir("", "TestLevelDBSanity")
func prepareDatabaseForTest(t *testing.T, testName string) (ldb *LevelDB, teardownFunc func()) {
// Create a temp db to run tests against
path, err := ioutil.TempDir("", testName)
if err != nil {
t.Fatalf("TestLevelDBSanity: TempDir unexpectedly "+
"failed: %s", err)
t.Fatalf("%s: TempDir unexpectedly "+
"failed: %s", testName, err)
}
ldb, err := NewLevelDB(path)
ldb, err = NewLevelDB(path)
if err != nil {
t.Fatalf("TestLevelDBSanity: NewLevelDB "+
"unexpectedly failed: %s", err)
t.Fatalf("%s: NewLevelDB unexpectedly "+
"failed: %s", testName, err)
}
defer func() {
err := ldb.Close()
teardownFunc = func() {
err = ldb.Close()
if err != nil {
t.Fatalf("TestLevelDBSanity: Close "+
"unexpectedly failed: %s", err)
t.Fatalf("%s: Close unexpectedly "+
"failed: %s", testName, err)
}
}()
}
return ldb, teardownFunc
}
func TestLevelDBSanity(t *testing.T) {
ldb, teardownFunc := prepareDatabaseForTest(t, "TestLevelDBSanity")
defer teardownFunc()
// Put something into the db
key := database.MakeBucket().Key([]byte("key"))
putData := []byte("Hello world!")
err = ldb.Put(key, putData)
err := ldb.Put(key, putData)
if err != nil {
t.Fatalf("TestLevelDBSanity: Put returned "+
"unexpected error: %s", err)
@ -52,24 +58,8 @@ func TestLevelDBSanity(t *testing.T) {
}
func TestLevelDBTransactionSanity(t *testing.T) {
// Open a test db
path, err := ioutil.TempDir("", "TestLevelDBTransactionSanity")
if err != nil {
t.Fatalf("TestLevelDBTransactionSanity: TempDir unexpectedly "+
"failed: %s", err)
}
ldb, err := NewLevelDB(path)
if err != nil {
t.Fatalf("TestLevelDBTransactionSanity: NewLevelDB "+
"unexpectedly failed: %s", err)
}
defer func() {
err := ldb.Close()
if err != nil {
t.Fatalf("TestLevelDBTransactionSanity: Close "+
"unexpectedly failed: %s", err)
}
}()
ldb, teardownFunc := prepareDatabaseForTest(t, "TestLevelDBTransactionSanity")
defer teardownFunc()
// Case 1. Write in tx and then read directly from the DB
// Begin a new transaction

View File

@ -0,0 +1,146 @@
package ldb
import (
"github.com/kaspanet/kaspad/database"
"strings"
"testing"
)
func TestTransactionCloseErrors(t *testing.T) {
tests := []struct {
name string
// function is the LevelDBTransaction function that
// we're verifying whether it returns an error after
// the transaction had been closed.
function func(dbTx *LevelDBTransaction) error
shouldReturnError bool
}{
{
name: "Put",
function: func(dbTx *LevelDBTransaction) error {
return dbTx.Put(database.MakeBucket().Key([]byte("key")), []byte("value"))
},
shouldReturnError: true,
},
{
name: "Get",
function: func(dbTx *LevelDBTransaction) error {
_, err := dbTx.Get(database.MakeBucket().Key([]byte("key")))
return err
},
shouldReturnError: true,
},
{
name: "Has",
function: func(dbTx *LevelDBTransaction) error {
_, err := dbTx.Has(database.MakeBucket().Key([]byte("key")))
return err
},
shouldReturnError: true,
},
{
name: "Delete",
function: func(dbTx *LevelDBTransaction) error {
return dbTx.Delete(database.MakeBucket().Key([]byte("key")))
},
shouldReturnError: true,
},
{
name: "Cursor",
function: func(dbTx *LevelDBTransaction) error {
_, err := dbTx.Cursor(database.MakeBucket([]byte("bucket")))
return err
},
shouldReturnError: true,
},
{
name: "Rollback",
function: (*LevelDBTransaction).Rollback,
shouldReturnError: true,
},
{
name: "Commit",
function: (*LevelDBTransaction).Commit,
shouldReturnError: true,
},
{
name: "RollbackUnlessClosed",
function: (*LevelDBTransaction).RollbackUnlessClosed,
shouldReturnError: false,
},
}
for _, test := range tests {
func() {
ldb, teardownFunc := prepareDatabaseForTest(t, "TestTransactionCloseErrors")
defer teardownFunc()
// Begin a new transaction to test Commit
commitTx, err := ldb.Begin()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: Begin "+
"unexpectedly failed: %s", err)
}
defer func() {
err := commitTx.RollbackUnlessClosed()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: RollbackUnlessClosed "+
"unexpectedly failed: %s", err)
}
}()
// Commit the Commit test transaction
err = commitTx.Commit()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: Commit "+
"unexpectedly failed: %s", err)
}
// Begin a new transaction to test Rollback
rollbackTx, err := ldb.Begin()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: Begin "+
"unexpectedly failed: %s", err)
}
defer func() {
err := rollbackTx.RollbackUnlessClosed()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: RollbackUnlessClosed "+
"unexpectedly failed: %s", err)
}
}()
// Rollback the Rollback test transaction
err = rollbackTx.Rollback()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: Rollback "+
"unexpectedly failed: %s", err)
}
expectedErrContainsString := "closed transaction"
// Make sure that the test function returns a "closed transaction" error
// for both the commitTx and the rollbackTx
for _, closedTx := range []*LevelDBTransaction{commitTx, rollbackTx} {
err = test.function(closedTx)
if test.shouldReturnError {
if err == nil {
t.Fatalf("TestTransactionCloseErrors: %s "+
"unexpectedly succeeded", test.name)
}
if !strings.Contains(err.Error(), expectedErrContainsString) {
t.Fatalf("TestTransactionCloseErrors: %s "+
"returned wrong error. Want: %s, got: %s",
test.name, expectedErrContainsString, err)
}
} else {
if err != nil {
t.Fatalf("TestTransactionCloseErrors: %s "+
"unexpectedly failed: %s", test.name, err)
}
}
}
}()
}
}

View File

@ -4,6 +4,7 @@ import (
"github.com/kaspanet/kaspad/database"
"github.com/kaspanet/kaspad/database/ffldb/ff"
"github.com/kaspanet/kaspad/database/ffldb/ldb"
"github.com/pkg/errors"
)
// transaction is an ffldb transaction.
@ -13,14 +14,19 @@ import (
// NO guarantee that if one puts data into the transaction then
// it will be available to get within the same transaction.
type transaction struct {
ldbTx *ldb.LevelDBTransaction
ffdb *ff.FlatFileDB
ldbTx *ldb.LevelDBTransaction
ffdb *ff.FlatFileDB
isClosed bool
}
// Put sets the value for the given key. It overwrites
// any previous value for that key.
// This method is part of the DataAccessor interface.
func (tx *transaction) Put(key *database.Key, value []byte) error {
if tx.isClosed {
return errors.New("cannot put into a closed transaction")
}
return tx.ldbTx.Put(key, value)
}
@ -28,6 +34,10 @@ func (tx *transaction) Put(key *database.Key, value []byte) error {
// ErrNotFound if the given key does not exist.
// This method is part of the DataAccessor interface.
func (tx *transaction) Get(key *database.Key) ([]byte, error) {
if tx.isClosed {
return nil, errors.New("cannot get from a closed transaction")
}
return tx.ldbTx.Get(key)
}
@ -35,6 +45,10 @@ func (tx *transaction) Get(key *database.Key) ([]byte, error) {
// given key.
// This method is part of the DataAccessor interface.
func (tx *transaction) Has(key *database.Key) (bool, error) {
if tx.isClosed {
return false, errors.New("cannot has from a closed transaction")
}
return tx.ldbTx.Has(key)
}
@ -42,6 +56,10 @@ func (tx *transaction) Has(key *database.Key) (bool, error) {
// return an error if the key doesn't exist.
// This method is part of the DataAccessor interface.
func (tx *transaction) Delete(key *database.Key) error {
if tx.isClosed {
return errors.New("cannot delete from a closed transaction")
}
return tx.ldbTx.Delete(key)
}
@ -52,6 +70,10 @@ func (tx *transaction) Delete(key *database.Key) error {
// that has just now been inserted.
// This method is part of the DataAccessor interface.
func (tx *transaction) AppendToStore(storeName string, data []byte) ([]byte, error) {
if tx.isClosed {
return nil, errors.New("cannot append to store on a closed transaction")
}
return appendToStore(tx, tx.ffdb, storeName, data)
}
@ -61,12 +83,20 @@ func (tx *transaction) AppendToStore(storeName string, data []byte) ([]byte, err
// AppendToStore for further details.
// This method is part of the DataAccessor interface.
func (tx *transaction) RetrieveFromStore(storeName string, location []byte) ([]byte, error) {
if tx.isClosed {
return nil, errors.New("cannot retrieve from store on a closed transaction")
}
return tx.ffdb.Read(storeName, location)
}
// Cursor begins a new cursor over the given bucket.
// This method is part of the DataAccessor interface.
func (tx *transaction) Cursor(bucket *database.Bucket) (database.Cursor, error) {
if tx.isClosed {
return nil, errors.New("cannot open a cursor from a closed transaction")
}
return tx.ldbTx.Cursor(bucket)
}
@ -74,6 +104,11 @@ func (tx *transaction) Cursor(bucket *database.Bucket) (database.Cursor, error)
// database within this transaction.
// This method is part of the Transaction interface.
func (tx *transaction) Rollback() error {
if tx.isClosed {
return errors.New("cannot rollback a closed transaction")
}
tx.isClosed = true
return tx.ldbTx.Rollback()
}
@ -81,6 +116,11 @@ func (tx *transaction) Rollback() error {
// within this transaction.
// This method is part of the Transaction interface.
func (tx *transaction) Commit() error {
if tx.isClosed {
return errors.New("cannot commit a closed transaction")
}
tx.isClosed = true
return tx.ldbTx.Commit()
}
@ -88,5 +128,10 @@ func (tx *transaction) Commit() error {
// the database within the transaction, unless the transaction
// had already been closed using either Rollback or Commit.
func (tx *transaction) RollbackUnlessClosed() error {
if tx.isClosed {
return nil
}
tx.isClosed = true
return tx.ldbTx.RollbackUnlessClosed()
}

View File

@ -0,0 +1,500 @@
package ffldb
import (
"bytes"
"github.com/kaspanet/kaspad/database"
"strings"
"testing"
)
func TestTransactionCommitForLevelDBMethods(t *testing.T) {
db, teardownFunc := prepareDatabaseForTest(t, "TestTransactionCommitForLevelDBMethods")
defer teardownFunc()
// Put a value into the database
key1 := database.MakeBucket().Key([]byte("key1"))
value1 := []byte("value1")
err := db.Put(key1, value1)
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Put "+
"unexpectedly failed: %s", err)
}
// Begin a new transaction
dbTx, err := db.Begin()
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Begin "+
"unexpectedly failed: %s", err)
}
defer func() {
err := dbTx.RollbackUnlessClosed()
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: RollbackUnlessClosed "+
"unexpectedly failed: %s", err)
}
}()
// Make sure that Has returns that the original value exists
exists, err := dbTx.Has(key1)
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Has "+
"unexpectedly failed: %s", err)
}
if !exists {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Has " +
"unexpectedly returned that the value does not exist")
}
// Get the existing value and make sure it's equal to the original
existingValue, err := dbTx.Get(key1)
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+
"unexpectedly failed: %s", err)
}
if !bytes.Equal(existingValue, value1) {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+
"returned unexpected value. Want: %s, got: %s",
string(value1), string(existingValue))
}
// Delete the existing value
err = dbTx.Delete(key1)
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Delete "+
"unexpectedly failed: %s", err)
}
// Try to get a value that does not exist and make sure it returns ErrNotFound
_, err = dbTx.Get(database.MakeBucket().Key([]byte("doesn't exist")))
if err == nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Get " +
"unexpectedly succeeded")
}
if !database.IsNotFoundError(err) {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+
"returned unexpected error: %s", err)
}
// Put a new value
key2 := database.MakeBucket().Key([]byte("key2"))
value2 := []byte("value2")
err = dbTx.Put(key2, value2)
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Put "+
"unexpectedly failed: %s", err)
}
// Commit the transaction
err = dbTx.Commit()
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Commit "+
"unexpectedly failed: %s", err)
}
// Make sure that Has returns that the original value does NOT exist
exists, err = db.Has(key1)
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Has "+
"unexpectedly failed: %s", err)
}
if exists {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Has " +
"unexpectedly returned that the value exists")
}
// Try to Get the existing value and make sure an ErrNotFound is returned
_, err = db.Get(key1)
if err == nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Get " +
"unexpectedly succeeded")
}
if !database.IsNotFoundError(err) {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+
"returned unexpected err: %s", err)
}
// Make sure that Has returns that the new value exists
exists, err = db.Has(key2)
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Has "+
"unexpectedly failed: %s", err)
}
if !exists {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Has " +
"unexpectedly returned that the value does not exist")
}
// Get the new value and make sure it's equal to the original
existingValue, err = db.Get(key2)
if err != nil {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+
"unexpectedly failed: %s", err)
}
if !bytes.Equal(existingValue, value2) {
t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+
"returned unexpected value. Want: %s, got: %s",
string(value2), string(existingValue))
}
}
func TestTransactionRollbackForLevelDBMethods(t *testing.T) {
db, teardownFunc := prepareDatabaseForTest(t, "TestTransactionRollbackForLevelDBMethods")
defer teardownFunc()
// Put a value into the database
key1 := database.MakeBucket().Key([]byte("key1"))
value1 := []byte("value1")
err := db.Put(key1, value1)
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Put "+
"unexpectedly failed: %s", err)
}
// Begin a new transaction
dbTx, err := db.Begin()
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Begin "+
"unexpectedly failed: %s", err)
}
defer func() {
err := dbTx.RollbackUnlessClosed()
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: RollbackUnlessClosed "+
"unexpectedly failed: %s", err)
}
}()
// Make sure that Has returns that the original value exists
exists, err := dbTx.Has(key1)
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has "+
"unexpectedly failed: %s", err)
}
if !exists {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has " +
"unexpectedly returned that the value does not exist")
}
// Get the existing value and make sure it's equal to the original
existingValue, err := dbTx.Get(key1)
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get "+
"unexpectedly failed: %s", err)
}
if !bytes.Equal(existingValue, value1) {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get "+
"returned unexpected value. Want: %s, got: %s",
string(value1), string(existingValue))
}
// Delete the existing value
err = dbTx.Delete(key1)
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Delete "+
"unexpectedly failed: %s", err)
}
// Put a new value
key2 := database.MakeBucket().Key([]byte("key2"))
value2 := []byte("value2")
err = dbTx.Put(key2, value2)
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Put "+
"unexpectedly failed: %s", err)
}
// Rollback the transaction
err = dbTx.Rollback()
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Rollback "+
"unexpectedly failed: %s", err)
}
// Make sure that Has returns that the original value still exists
exists, err = db.Has(key1)
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has "+
"unexpectedly failed: %s", err)
}
if !exists {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has " +
"unexpectedly returned that the value does not exist")
}
// Get the existing value and make sure it is still returned
existingValue, err = db.Get(key1)
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get "+
"unexpectedly failed: %s", err)
}
if !bytes.Equal(existingValue, value1) {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get "+
"returned unexpected value. Want: %s, got: %s",
string(value1), string(existingValue))
}
// Make sure that Has returns that the new value does NOT exist
exists, err = db.Has(key2)
if err != nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has "+
"unexpectedly failed: %s", err)
}
if exists {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has " +
"unexpectedly returned that the value exists")
}
// Try to Get the new value and make sure it returns an ErrNotFound
_, err = db.Get(key2)
if err == nil {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get " +
"unexpectedly succeeded")
}
if !database.IsNotFoundError(err) {
t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get "+
"returned unexpected error: %s", err)
}
}
func TestTransactionCloseErrors(t *testing.T) {
tests := []struct {
name string
function func(dbTx database.Transaction) error
shouldReturnError bool
}{
{
name: "Put",
function: func(dbTx database.Transaction) error {
return dbTx.Put(database.MakeBucket().Key([]byte("key")), []byte("value"))
},
shouldReturnError: true,
},
{
name: "Get",
function: func(dbTx database.Transaction) error {
_, err := dbTx.Get(database.MakeBucket().Key([]byte("key")))
return err
},
shouldReturnError: true,
},
{
name: "Has",
function: func(dbTx database.Transaction) error {
_, err := dbTx.Has(database.MakeBucket().Key([]byte("key")))
return err
},
shouldReturnError: true,
},
{
name: "Delete",
function: func(dbTx database.Transaction) error {
return dbTx.Delete(database.MakeBucket().Key([]byte("key")))
},
shouldReturnError: true,
},
{
name: "Cursor",
function: func(dbTx database.Transaction) error {
_, err := dbTx.Cursor(database.MakeBucket([]byte("bucket")))
return err
},
shouldReturnError: true,
},
{
name: "AppendToStore",
function: func(dbTx database.Transaction) error {
_, err := dbTx.AppendToStore("store", []byte("data"))
return err
},
shouldReturnError: true,
},
{
name: "RetrieveFromStore",
function: func(dbTx database.Transaction) error {
_, err := dbTx.RetrieveFromStore("store", []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
return err
},
shouldReturnError: true,
},
{
name: "Rollback",
function: func(dbTx database.Transaction) error {
return dbTx.Rollback()
},
shouldReturnError: true,
},
{
name: "Commit",
function: func(dbTx database.Transaction) error {
return dbTx.Commit()
},
shouldReturnError: true,
},
{
name: "RollbackUnlessClosed",
function: func(dbTx database.Transaction) error {
return dbTx.RollbackUnlessClosed()
},
shouldReturnError: false,
},
}
for _, test := range tests {
func() {
db, teardownFunc := prepareDatabaseForTest(t, "TestTransactionCloseErrors")
defer teardownFunc()
// Begin a new transaction to test Commit
commitTx, err := db.Begin()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: Begin "+
"unexpectedly failed: %s", err)
}
defer func() {
err := commitTx.RollbackUnlessClosed()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: RollbackUnlessClosed "+
"unexpectedly failed: %s", err)
}
}()
// Commit the Commit test transaction
err = commitTx.Commit()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: Commit "+
"unexpectedly failed: %s", err)
}
// Begin a new transaction to test Rollback
rollbackTx, err := db.Begin()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: Begin "+
"unexpectedly failed: %s", err)
}
defer func() {
err := rollbackTx.RollbackUnlessClosed()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: RollbackUnlessClosed "+
"unexpectedly failed: %s", err)
}
}()
// Rollback the Rollback test transaction
err = rollbackTx.Rollback()
if err != nil {
t.Fatalf("TestTransactionCloseErrors: Rollback "+
"unexpectedly failed: %s", err)
}
expectedErrContainsString := "closed transaction"
// Make sure that the test function returns a "closed transaction" error
// for both the commitTx and the rollbackTx
for _, closedTx := range []database.Transaction{commitTx, rollbackTx} {
err = test.function(closedTx)
if test.shouldReturnError {
if err == nil {
t.Fatalf("TestTransactionCloseErrors: %s "+
"unexpectedly succeeded", test.name)
}
if !strings.Contains(err.Error(), expectedErrContainsString) {
t.Fatalf("TestTransactionCloseErrors: %s "+
"returned wrong error. Want: %s, got: %s",
test.name, expectedErrContainsString, err)
}
} else {
if err != nil {
t.Fatalf("TestTransactionCloseErrors: %s "+
"unexpectedly failed: %s", test.name, err)
}
}
}
}()
}
}
func TestTransactionRollbackUnlessClosed(t *testing.T) {
db, teardownFunc := prepareDatabaseForTest(t, "TestTransactionRollbackUnlessClosed")
defer teardownFunc()
// Begin a new transaction
dbTx, err := db.Begin()
if err != nil {
t.Fatalf("TestTransactionRollbackUnlessClosed: Begin "+
"unexpectedly failed: %s", err)
}
// Roll it back
err = dbTx.RollbackUnlessClosed()
if err != nil {
t.Fatalf("TestTransactionRollbackUnlessClosed: RollbackUnlessClosed "+
"unexpectedly failed: %s", err)
}
}
func TestTransactionCommitForFlatFileMethods(t *testing.T) {
db, teardownFunc := prepareDatabaseForTest(t, "TestTransactionCommitForFlatFileMethods")
defer teardownFunc()
// Put a value into the database
store := "store"
value1 := []byte("value1")
location1, err := db.AppendToStore(store, value1)
if err != nil {
t.Fatalf("TestTransactionCommitForFlatFileMethods: AppendToStore "+
"unexpectedly failed: %s", err)
}
// Begin a new transaction
dbTx, err := db.Begin()
if err != nil {
t.Fatalf("TestTransactionCommitForFlatFileMethods: Begin "+
"unexpectedly failed: %s", err)
}
defer func() {
err := dbTx.RollbackUnlessClosed()
if err != nil {
t.Fatalf("TestTransactionCommitForFlatFileMethods: RollbackUnlessClosed "+
"unexpectedly failed: %s", err)
}
}()
// Retrieve the existing value and make sure it's equal to the original
existingValue, err := dbTx.RetrieveFromStore(store, location1)
if err != nil {
t.Fatalf("TestTransactionCommitForFlatFileMethods: RetrieveFromStore "+
"unexpectedly failed: %s", err)
}
if !bytes.Equal(existingValue, value1) {
t.Fatalf("TestTransactionCommitForFlatFileMethods: RetrieveFromStore "+
"returned unexpected value. Want: %s, got: %s",
string(value1), string(existingValue))
}
// Put a new value
value2 := []byte("value2")
location2, err := dbTx.AppendToStore(store, value2)
if err != nil {
t.Fatalf("TestTransactionCommitForFlatFileMethods: AppendToStore "+
"unexpectedly failed: %s", err)
}
// Commit the transaction
err = dbTx.Commit()
if err != nil {
t.Fatalf("TestTransactionCommitForFlatFileMethods: Commit "+
"unexpectedly failed: %s", err)
}
// Retrieve the new value and make sure it's equal to the original
newValue, err := db.RetrieveFromStore(store, location2)
if err != nil {
t.Fatalf("TestTransactionCommitForFlatFileMethods: RetrieveFromStore "+
"unexpectedly failed: %s", err)
}
if !bytes.Equal(newValue, value2) {
t.Fatalf("TestTransactionCommitForFlatFileMethods: RetrieveFromStore "+
"returned unexpected value. Want: %s, got: %s",
string(value2), string(newValue))
}
}