bbolt/movebucket_test.go

399 lines
12 KiB
Go

package bbolt_test
import (
crand "crypto/rand"
"math/rand"
"os"
"path/filepath"
"testing"
"go.etcd.io/bbolt"
"go.etcd.io/bbolt/errors"
"go.etcd.io/bbolt/internal/btesting"
"github.com/stretchr/testify/require"
)
func TestTx_MoveBucket(t *testing.T) {
testCases := []struct {
name string
srcBucketPath []string
dstBucketPath []string
bucketToMove string
bucketExistInSrc bool
bucketExistInDst bool
hasIncompatibleKeyInSrc bool
hasIncompatibleKeyInDst bool
expectedErr error
}{
// normal cases
{
name: "normal case",
srcBucketPath: []string{"sb1", "sb2"},
dstBucketPath: []string{"db1", "db2"},
bucketToMove: "bucketToMove",
bucketExistInSrc: true,
bucketExistInDst: false,
hasIncompatibleKeyInSrc: false,
hasIncompatibleKeyInDst: false,
expectedErr: nil,
},
{
name: "the source and target bucket share the same grandparent",
srcBucketPath: []string{"grandparent", "sb2"},
dstBucketPath: []string{"grandparent", "db2"},
bucketToMove: "bucketToMove",
bucketExistInSrc: true,
bucketExistInDst: false,
hasIncompatibleKeyInSrc: false,
hasIncompatibleKeyInDst: false,
expectedErr: nil,
},
{
name: "bucketToMove is a top level bucket",
srcBucketPath: []string{},
dstBucketPath: []string{"db1", "db2"},
bucketToMove: "bucketToMove",
bucketExistInSrc: true,
bucketExistInDst: false,
hasIncompatibleKeyInSrc: false,
hasIncompatibleKeyInDst: false,
expectedErr: nil,
},
{
name: "convert bucketToMove to a top level bucket",
srcBucketPath: []string{"sb1", "sb2"},
dstBucketPath: []string{},
bucketToMove: "bucketToMove",
bucketExistInSrc: true,
bucketExistInDst: false,
hasIncompatibleKeyInSrc: false,
hasIncompatibleKeyInDst: false,
expectedErr: nil,
},
// negative cases
{
name: "bucketToMove not exist in source bucket",
srcBucketPath: []string{"sb1", "sb2"},
dstBucketPath: []string{"db1", "db2"},
bucketToMove: "bucketToMove",
bucketExistInSrc: false,
bucketExistInDst: false,
hasIncompatibleKeyInSrc: false,
hasIncompatibleKeyInDst: false,
expectedErr: errors.ErrBucketNotFound,
},
{
name: "bucketToMove exist in target bucket",
srcBucketPath: []string{"sb1", "sb2"},
dstBucketPath: []string{"db1", "db2"},
bucketToMove: "bucketToMove",
bucketExistInSrc: true,
bucketExistInDst: true,
hasIncompatibleKeyInSrc: false,
hasIncompatibleKeyInDst: false,
expectedErr: errors.ErrBucketExists,
},
{
name: "incompatible key exist in source bucket",
srcBucketPath: []string{"sb1", "sb2"},
dstBucketPath: []string{"db1", "db2"},
bucketToMove: "bucketToMove",
bucketExistInSrc: false,
bucketExistInDst: false,
hasIncompatibleKeyInSrc: true,
hasIncompatibleKeyInDst: false,
expectedErr: errors.ErrIncompatibleValue,
},
{
name: "incompatible key exist in target bucket",
srcBucketPath: []string{"sb1", "sb2"},
dstBucketPath: []string{"db1", "db2"},
bucketToMove: "bucketToMove",
bucketExistInSrc: true,
bucketExistInDst: false,
hasIncompatibleKeyInSrc: false,
hasIncompatibleKeyInDst: true,
expectedErr: errors.ErrIncompatibleValue,
},
{
name: "the source and target are the same bucket",
srcBucketPath: []string{"sb1", "sb2"},
dstBucketPath: []string{"sb1", "sb2"},
bucketToMove: "bucketToMove",
bucketExistInSrc: true,
bucketExistInDst: false,
hasIncompatibleKeyInSrc: false,
hasIncompatibleKeyInDst: false,
expectedErr: errors.ErrSameBuckets,
},
{
name: "both the source and target are the root bucket",
srcBucketPath: []string{},
dstBucketPath: []string{},
bucketToMove: "bucketToMove",
bucketExistInSrc: true,
bucketExistInDst: false,
hasIncompatibleKeyInSrc: false,
hasIncompatibleKeyInDst: false,
expectedErr: errors.ErrSameBuckets,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(*testing.T) {
db := btesting.MustCreateDBWithOption(t, &bbolt.Options{PageSize: 4096})
dumpBucketBeforeMoving := filepath.Join(t.TempDir(), "dbBeforeMove")
dumpBucketAfterMoving := filepath.Join(t.TempDir(), "dbAfterMove")
t.Log("Creating sample db and populate some data")
err := db.Update(func(tx *bbolt.Tx) error {
srcBucket := prepareBuckets(t, tx, tc.srcBucketPath...)
dstBucket := prepareBuckets(t, tx, tc.dstBucketPath...)
if tc.bucketExistInSrc {
_ = createBucketAndPopulateData(t, tx, srcBucket, tc.bucketToMove)
}
if tc.bucketExistInDst {
_ = createBucketAndPopulateData(t, tx, dstBucket, tc.bucketToMove)
}
if tc.hasIncompatibleKeyInSrc {
putErr := srcBucket.Put([]byte(tc.bucketToMove), []byte("bar"))
require.NoError(t, putErr)
}
if tc.hasIncompatibleKeyInDst {
putErr := dstBucket.Put([]byte(tc.bucketToMove), []byte("bar"))
require.NoError(t, putErr)
}
return nil
})
require.NoError(t, err)
t.Log("Moving bucket")
err = db.Update(func(tx *bbolt.Tx) error {
srcBucket := prepareBuckets(t, tx, tc.srcBucketPath...)
dstBucket := prepareBuckets(t, tx, tc.dstBucketPath...)
if tc.expectedErr == nil {
t.Logf("Dump the bucket to %s before moving it", dumpBucketBeforeMoving)
bk := openBucket(tx, srcBucket, tc.bucketToMove)
dumpErr := dumpBucket([]byte(tc.bucketToMove), bk, dumpBucketBeforeMoving)
require.NoError(t, dumpErr)
}
mErr := tx.MoveBucket([]byte(tc.bucketToMove), srcBucket, dstBucket)
require.Equal(t, tc.expectedErr, mErr)
if tc.expectedErr == nil {
t.Logf("Dump the bucket to %s after moving it", dumpBucketAfterMoving)
bk := openBucket(tx, dstBucket, tc.bucketToMove)
dumpErr := dumpBucket([]byte(tc.bucketToMove), bk, dumpBucketAfterMoving)
require.NoError(t, dumpErr)
}
return nil
})
require.NoError(t, err)
// skip assertion if failure expected
if tc.expectedErr != nil {
return
}
t.Log("Verifying the bucket should be identical before and after being moved")
dataBeforeMove, err := os.ReadFile(dumpBucketBeforeMoving)
require.NoError(t, err)
dataAfterMove, err := os.ReadFile(dumpBucketAfterMoving)
require.NoError(t, err)
require.Equal(t, dataBeforeMove, dataAfterMove)
})
}
}
func TestBucket_MoveBucket_DiffDB(t *testing.T) {
srcBucketPath := []string{"sb1", "sb2"}
dstBucketPath := []string{"db1", "db2"}
bucketToMove := "bucketToMove"
var srcBucket *bbolt.Bucket
t.Log("Creating source bucket and populate some data")
srcDB := btesting.MustCreateDBWithOption(t, &bbolt.Options{PageSize: 4096})
err := srcDB.Update(func(tx *bbolt.Tx) error {
srcBucket = prepareBuckets(t, tx, srcBucketPath...)
return nil
})
require.NoError(t, err)
defer func() {
require.NoError(t, srcDB.Close())
}()
t.Log("Creating target bucket and populate some data")
dstDB := btesting.MustCreateDBWithOption(t, &bbolt.Options{PageSize: 4096})
err = dstDB.Update(func(tx *bbolt.Tx) error {
prepareBuckets(t, tx, dstBucketPath...)
return nil
})
require.NoError(t, err)
defer func() {
require.NoError(t, dstDB.Close())
}()
t.Log("Reading source bucket in a separate RWTx")
sTx, sErr := srcDB.Begin(true)
require.NoError(t, sErr)
defer func() {
require.NoError(t, sTx.Rollback())
}()
srcBucket = prepareBuckets(t, sTx, srcBucketPath...)
t.Log("Moving the sub-bucket in a separate RWTx")
err = dstDB.Update(func(tx *bbolt.Tx) error {
dstBucket := prepareBuckets(t, tx, dstBucketPath...)
mErr := srcBucket.MoveBucket([]byte(bucketToMove), dstBucket)
require.Equal(t, errors.ErrDifferentDB, mErr)
return nil
})
require.NoError(t, err)
}
func TestBucket_MoveBucket_DiffTx(t *testing.T) {
testCases := []struct {
name string
srcBucketPath []string
dstBucketPath []string
isSrcReadonlyTx bool
isDstReadonlyTx bool
bucketToMove string
expectedErr error
}{
{
name: "src is RWTx and target is RTx",
srcBucketPath: []string{"sb1", "sb2"},
dstBucketPath: []string{"db1", "db2"},
isSrcReadonlyTx: true,
isDstReadonlyTx: false,
bucketToMove: "bucketToMove",
expectedErr: errors.ErrTxNotWritable,
},
{
name: "src is RTx and target is RWTx",
srcBucketPath: []string{"sb1", "sb2"},
dstBucketPath: []string{"db1", "db2"},
isSrcReadonlyTx: false,
isDstReadonlyTx: true,
bucketToMove: "bucketToMove",
expectedErr: errors.ErrTxNotWritable,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var srcBucket *bbolt.Bucket
var dstBucket *bbolt.Bucket
t.Log("Creating source and target buckets and populate some data")
db := btesting.MustCreateDBWithOption(t, &bbolt.Options{PageSize: 4096})
err := db.Update(func(tx *bbolt.Tx) error {
srcBucket = prepareBuckets(t, tx, tc.srcBucketPath...)
dstBucket = prepareBuckets(t, tx, tc.dstBucketPath...)
return nil
})
require.NoError(t, err)
defer func() {
require.NoError(t, db.Close())
}()
t.Log("Opening source bucket in a separate Tx")
sTx, sErr := db.Begin(tc.isSrcReadonlyTx)
require.NoError(t, sErr)
defer func() {
require.NoError(t, sTx.Rollback())
}()
srcBucket = prepareBuckets(t, sTx, tc.srcBucketPath...)
t.Log("Opening target bucket in a separate Tx")
dTx, dErr := db.Begin(tc.isDstReadonlyTx)
require.NoError(t, dErr)
defer func() {
require.NoError(t, dTx.Rollback())
}()
dstBucket = prepareBuckets(t, dTx, tc.dstBucketPath...)
t.Log("Moving the sub-bucket")
err = db.View(func(tx *bbolt.Tx) error {
mErr := srcBucket.MoveBucket([]byte(tc.bucketToMove), dstBucket)
require.Equal(t, tc.expectedErr, mErr)
return nil
})
require.NoError(t, err)
})
}
}
// prepareBuckets opens the bucket chain. For each bucket in the chain,
// open it if existed, otherwise create it and populate sample data.
func prepareBuckets(t testing.TB, tx *bbolt.Tx, buckets ...string) *bbolt.Bucket {
var bk *bbolt.Bucket
for _, key := range buckets {
if childBucket := openBucket(tx, bk, key); childBucket == nil {
bk = createBucketAndPopulateData(t, tx, bk, key)
} else {
bk = childBucket
}
}
return bk
}
func openBucket(tx *bbolt.Tx, bk *bbolt.Bucket, bucketToOpen string) *bbolt.Bucket {
if bk == nil {
return tx.Bucket([]byte(bucketToOpen))
}
return bk.Bucket([]byte(bucketToOpen))
}
func createBucketAndPopulateData(t testing.TB, tx *bbolt.Tx, bk *bbolt.Bucket, bucketName string) *bbolt.Bucket {
if bk == nil {
newBucket, err := tx.CreateBucket([]byte(bucketName))
require.NoError(t, err, "failed to create bucket %s", bucketName)
populateSampleDataInBucket(t, newBucket, rand.Intn(4096))
return newBucket
}
newBucket, err := bk.CreateBucket([]byte(bucketName))
require.NoError(t, err, "failed to create bucket %s", bucketName)
populateSampleDataInBucket(t, newBucket, rand.Intn(4096))
return newBucket
}
func populateSampleDataInBucket(t testing.TB, bk *bbolt.Bucket, n int) {
var min, max = 1, 1024
for i := 0; i < n; i++ {
// generate rand key/value length
keyLength := rand.Intn(max-min) + min
valLength := rand.Intn(max-min) + min
keyData := make([]byte, keyLength)
valData := make([]byte, valLength)
_, err := crand.Read(keyData)
require.NoError(t, err)
_, err = crand.Read(valData)
require.NoError(t, err)
err = bk.Put(keyData, valData)
require.NoError(t, err)
}
}