diff --git a/bench_test.go b/bench_test.go index 8a6ff9c..9a8d2d3 100644 --- a/bench_test.go +++ b/bench_test.go @@ -1,7 +1,6 @@ package honu_test import ( - "fmt" "io/ioutil" "math/rand" "os" @@ -22,26 +21,26 @@ var ( func setupLevelDB(t testing.TB) (*leveldb.DB, string) { // Create a new leveldb database in a temporary directory - tmpDir, err := ioutil.TempDir("", "honuldb-*") + tmpDir, err := ioutil.TempDir("", "leveldb-*") require.NoError(t, err) // Open a leveldb database directly without honu wrapper db, err := leveldb.OpenFile(tmpDir, nil) - require.NoError(t, err) if err != nil && tmpDir != "" { - fmt.Println(tmpDir) os.RemoveAll(tmpDir) } + require.NoError(t, err) + + t.Cleanup(func() { + db.Close() + os.RemoveAll(tmpDir) + }) + return db, tmpDir } func BenchmarkHonuGet(b *testing.B) { - db, tmpDir := setupHonuDB(b) - - // Cleanup when we're done with the test - // NOTE: defers are evaluated in FIFO order, so this ensures the db is closed first then the directory deleted - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupHonuDB(b) // Create a key and value key := []byte("foo") @@ -63,11 +62,7 @@ func BenchmarkHonuGet(b *testing.B) { } func BenchmarkLevelDBGet(b *testing.B) { - db, tmpDir := setupLevelDB(b) - - // Cleanup when we're done with the test - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupLevelDB(b) // Create a key and value key := []byte("foo") @@ -88,11 +83,7 @@ func BenchmarkLevelDBGet(b *testing.B) { } func BenchmarkHonuPut(b *testing.B) { - db, tmpDir := setupHonuDB(b) - - // Cleanup when we're done with the test - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupHonuDB(b) // Create a key and value key := []byte("foo") @@ -110,11 +101,7 @@ func BenchmarkHonuPut(b *testing.B) { } func BenchmarkLevelDBPut(b *testing.B) { - db, tmpDir := setupLevelDB(b) - - // Cleanup when we're done with the test - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupLevelDB(b) // Create a key and value key := []byte("foo") @@ -132,11 +119,7 @@ func BenchmarkLevelDBPut(b *testing.B) { } func BenchmarkHonuDelete(b *testing.B) { - db, tmpDir := setupHonuDB(b) - - // Cleanup when we're done with the test - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupHonuDB(b) // Create a key and value key := []byte("foo") @@ -158,11 +141,7 @@ func BenchmarkHonuDelete(b *testing.B) { } func BenchmarkLevelDBDelete(b *testing.B) { - db, tmpDir := setupLevelDB(b) - - // Cleanup when we're done with the test - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupLevelDB(b) // Create a key and value key := []byte("foo") @@ -183,11 +162,7 @@ func BenchmarkLevelDBDelete(b *testing.B) { } func BenchmarkHonuIter(b *testing.B) { - db, tmpDir := setupHonuDB(b) - - // Cleanup when we're done with the test - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupHonuDB(b) // Create a key and value for _, key := range []string{"aa", "bb", "cc", "dd", "ee", "ff", "gg", "hh", "ii", "jj"} { @@ -219,11 +194,7 @@ func BenchmarkHonuIter(b *testing.B) { } func BenchmarkLevelDBIter(b *testing.B) { - db, tmpDir := setupLevelDB(b) - - // Cleanup when we're done with the test - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupLevelDB(b) // Create a key and value for _, key := range []string{"aa", "bb", "cc", "dd", "ee", "ff", "gg", "hh", "ii", "jj"} { @@ -253,11 +224,7 @@ func BenchmarkLevelDBIter(b *testing.B) { } func BenchmarkHonuObject(b *testing.B) { - db, tmpDir := setupHonuDB(b) - - // Cleanup when we're done with the test - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupHonuDB(b) // Create a key and value key := []byte("foo") diff --git a/engines/leveldb/iter.go b/engines/leveldb/iter.go index ac8fb22..73599d2 100644 --- a/engines/leveldb/iter.go +++ b/engines/leveldb/iter.go @@ -2,46 +2,79 @@ package leveldb import ( "bytes" - "fmt" honuiter "github.com/rotationalio/honu/iterator" pb "github.com/rotationalio/honu/object" + opts "github.com/rotationalio/honu/options" "github.com/syndtr/goleveldb/leveldb/iterator" "google.golang.org/protobuf/proto" ) // NewLevelDBIterator creates a new iterator that wraps a leveldb Iterator with object // management access and Honu-specific serialization. -func NewLevelDBIterator(iter iterator.Iterator, namespace string) honuiter.Iterator { - return &ldbIterator{ldb: iter, namespace: namespace} +func NewLevelDBIterator(iter iterator.Iterator, options *opts.Options) honuiter.Iterator { + return &ldbIterator{ldb: iter, options: options} } // Wraps the underlying leveldb iterator to provide object management access. type ldbIterator struct { - ldb iterator.Iterator - namespace string + ldb iterator.Iterator + options *opts.Options } // Type check for the ldbIterator var _ honuiter.Iterator = &ldbIterator{} -func (i *ldbIterator) Next() bool { return i.ldb.Next() } -func (i *ldbIterator) Prev() bool { return i.ldb.Prev() } func (i *ldbIterator) Error() error { return i.ldb.Error() } func (i *ldbIterator) Release() { i.ldb.Release() } +func (i *ldbIterator) Next() bool { + if ok := i.ldb.Next(); !ok { + return false + } + + // If we aren't including Tombstones, we need to check if the next version is a + // tombstone before we know if we have a next value or not. + if !i.options.Tombstones { + if obj, err := i.Object(); err != nil || obj.Tombstone() { + return i.Next() + } + } + return true +} + +func (i *ldbIterator) Prev() bool { + if ok := i.ldb.Prev(); !ok { + return false + } + + // If we aren't including Tombstones, we need to check if the next version is a + // tombstone before we know if we have a next value or not. + if !i.options.Tombstones { + if obj, err := i.Object(); err != nil || obj.Tombstone() { + return i.Prev() + } + } + return true +} + func (i *ldbIterator) Seek(key []byte) bool { + // NOTE: no need to do tombstone checking in Seek because Next will be called. // We need to prefix the seek with the correct namespace - key = prepend(i.namespace, key) + if i.options.Namespace != "" { + key = prepend(i.options.Namespace, key) + } return i.ldb.Seek(key) } func (i *ldbIterator) Key() []byte { // Fetch the key then split the namespace from the key + // Note that because the namespace itself might have colons in it, we + // strip off the namespace prefix then remove any preceding colons. key := i.ldb.Key() - parts := bytes.SplitN(key, nssep, 2) - if len(parts) == 2 { - return parts[1] + if i.options.Namespace != "" { + prefix := prepend(i.options.Namespace, nil) + return bytes.TrimPrefix(key, prefix) } return key } @@ -49,11 +82,10 @@ func (i *ldbIterator) Key() []byte { func (i *ldbIterator) Value() []byte { obj, err := i.Object() if err != nil { - fmt.Println(err) + // NOTE: if err is not nil, it's up to the caller to get the error from Object return nil - } else { - return obj.Data } + return obj.Data } func (i *ldbIterator) Object() (obj *pb.Object, err error) { @@ -65,5 +97,5 @@ func (i *ldbIterator) Object() (obj *pb.Object, err error) { } func (i *ldbIterator) Namespace() string { - return i.namespace + return i.options.Namespace } diff --git a/engines/leveldb/leveldb.go b/engines/leveldb/leveldb.go index 0b4a678..3a38af6 100644 --- a/engines/leveldb/leveldb.go +++ b/engines/leveldb/leveldb.go @@ -192,7 +192,7 @@ func (db *LevelDBEngine) Iter(prefix []byte, options *opts.Options) (i iterator. if len(prefix) > 0 { slice = util.BytesPrefix(prefix) } - return NewLevelDBIterator(db.ldb.NewIterator(slice, options.LevelDBRead), options.Namespace), nil + return NewLevelDBIterator(db.ldb.NewIterator(slice, options.LevelDBRead), options), nil } var nssep = []byte("::") diff --git a/engines/leveldb/leveldb_test.go b/engines/leveldb/leveldb_test.go index f03549f..0686375 100644 --- a/engines/leveldb/leveldb_test.go +++ b/engines/leveldb/leveldb_test.go @@ -15,12 +15,15 @@ import ( "google.golang.org/protobuf/proto" ) +// a test set of key/value pairs used to evaluate iteration +// note because :: is the namespace separator in leveldb, we want to ensure that keys +// with colons are correctly iterated on. var pairs = [][]string{ {"aa", "first"}, {"ab", "second"}, - {"ba", "third"}, - {"bb", "fourth"}, - {"bc", "fifth"}, + {"b::a", "third"}, + {"b::b", "fourth"}, + {"b::c", "fifth"}, {"ca", "sixth"}, {"cb", "seventh"}, } @@ -44,11 +47,18 @@ func setupLevelDBEngine(t testing.TB) (_ *leveldb.LevelDBEngine, path string) { os.RemoveAll(tempDir) } require.NoError(t, err) + + // Add a cleanup function to ensure the fixture is deleted after tests + t.Cleanup(func() { + // Teardown after finishing the test + engine.Close() + os.RemoveAll(tempDir) + }) + return engine, tempDir } -// Creates an options.Options struct with namespace set and returns -// a pointer to it. +// Creates an options.Options struct with namespace set and returns a pointer to it. func namespaceOpts(namespace string, t *testing.T) *options.Options { opts, err := options.New(options.WithNamespace(namespace)) require.NoError(t, err) @@ -78,7 +88,7 @@ func checkDelete(ldbStore engine.Store, opts *options.Options, key []byte, t *te require.Empty(t, value) } -func TestLeveldbEngine(t *testing.T) { +func TestLevelDBEngine(t *testing.T) { // Setup a levelDB Engine. ldbEngine, ldbPath := setupLevelDBEngine(t) require.Equal(t, "leveldb", ldbEngine.Engine()) @@ -86,10 +96,6 @@ func TestLeveldbEngine(t *testing.T) { // Ensure the db was created. require.DirExists(t, ldbPath) - // Teardown after finishing the test. - defer os.RemoveAll(ldbPath) - defer ldbEngine.Close() - // Use a constant key to ensure namespaces // are working correctly. key := []byte("foo") @@ -111,12 +117,8 @@ func TestLeveldbEngine(t *testing.T) { } } -func TestLeveldbTransactions(t *testing.T) { - ldbEngine, ldbPath := setupLevelDBEngine(t) - - // Teardown after finishing the test - defer os.RemoveAll(ldbPath) - defer ldbEngine.Close() +func TestLevelDBTransactions(t *testing.T) { + ldbEngine, _ := setupLevelDBEngine(t) // Use a constant key to ensure namespaces // are working correctly. @@ -155,20 +157,9 @@ func TestLeveldbTransactions(t *testing.T) { } func TestLevelDBIter(t *testing.T) { - ldbEngine, ldbPath := setupLevelDBEngine(t) - - // Teardown after finishing the test - defer os.RemoveAll(ldbPath) - defer ldbEngine.Close() + ldbEngine, _ := setupLevelDBEngine(t) for _, namespace := range testNamespaces { - // TODO: figure out what to do with this testcase. - // Iter currently grabs the namespace by splitting - // on :: and grabbing the first string, so it only - // grabs "namespace". - if namespace == "namespace::with::colons" { - continue - } // Add data to the database to iterate over. opts := namespaceOpts(namespace, t) @@ -223,7 +214,16 @@ func addIterPairsToDB(ldbStore engine.Store, opts *options.Options, pairs [][]st obj := &pb.Object{ Key: key, Namespace: opts.Namespace, - Data: value, + Version: &pb.Version{ + Pid: 1, + Version: 1, + Region: "testing", + Parent: nil, + Tombstone: false, + }, + Region: "testing", + Owner: "testing", + Data: value, } data, err := proto.Marshal(obj) diff --git a/honu_test.go b/honu_test.go index 56a2908..3ea8b4a 100644 --- a/honu_test.go +++ b/honu_test.go @@ -16,12 +16,15 @@ import ( "github.com/stretchr/testify/require" ) +// a test set of key/value pairs used to evaluate iteration +// note because :: is the namespace separator in leveldb, we want to ensure that keys +// with colons are correctly iterated on. var pairs = [][]string{ {"aa", "first"}, {"ab", "second"}, - {"ba", "third"}, - {"bb", "fourth"}, - {"bc", "fifth"}, + {"b::a", "third"}, + {"b::b", "fourth"}, + {"b::c", "fifth"}, {"ca", "sixth"}, {"cb", "seventh"}, } @@ -37,26 +40,27 @@ var testNamespaces = []string{ func setupHonuDB(t testing.TB) (db *honu.DB, tmpDir string) { // Create a new leveldb database in a temporary directory - tmpDir, err := ioutil.TempDir("", "honuldb-*") + tmpDir, err := ioutil.TempDir("", "honudb-*") require.NoError(t, err) // Open a Honu leveldb database with default configuration uri := fmt.Sprintf("leveldb:///%s", tmpDir) db, err = honu.Open(uri, config.WithReplica(config.ReplicaConfig{PID: 8, Region: "us-southwest-16", Name: "testing"})) - require.NoError(t, err) if err != nil && tmpDir != "" { - fmt.Println(tmpDir) os.RemoveAll(tmpDir) } + require.NoError(t, err) + + t.Cleanup(func() { + db.Close() + os.RemoveAll(tmpDir) + }) + return db, tmpDir } func TestLevelDBInteractions(t *testing.T) { - db, tmpDir := setupHonuDB(t) - - // Cleanup when we're done with the test - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupHonuDB(t) totalKeys := 0 for _, namespace := range testNamespaces { @@ -125,14 +129,6 @@ func TestLevelDBInteractions(t *testing.T) { _, err = db.Update(obj) require.NoError(t, err) - // TODO: figure out what to do with this testcase. - // Iter currently grabs the namespace by splitting - // on :: and grabbing the first string, so it only - // grabs "namespace". - if namespace == "namespace::with::colons" { - continue - } - // Put a range of data into the database for _, pair := range pairs { key := []byte(pair[0]) @@ -166,27 +162,14 @@ func TestLevelDBInteractions(t *testing.T) { } // Test iteration over all the namespaces - // FIXME: This is skipping the undeleted values - engine, ok := db.Engine().(*leveldb.LevelDBEngine) - require.True(t, ok) - ldb := engine.DB() - iter := ldb.NewIterator(nil, nil) - collected := 0 - for iter.Next() { - collected++ - } - require.Equal(t, totalKeys, collected) - require.NoError(t, iter.Error()) - iter.Release() + _, ok := db.Engine().(*leveldb.LevelDBEngine) + require.True(t, ok, "the engine type returned should be a leveldb.DB") + requireDatabaseLen(t, db, totalKeys) } func TestUpdate(t *testing.T) { // Create a test database to attempt to update - db, tmpDir := setupHonuDB(t) - - // Cleanup when we're done with the test - defer os.RemoveAll(tmpDir) - defer db.Close() + db, _ := setupHonuDB(t) // Create a random object in the database to start update tests on key := randomData(32) @@ -310,6 +293,179 @@ func TestUpdate(t *testing.T) { require.Equal(t, honu.UpdateLinear, update) } +func TestTombstones(t *testing.T) { + // Create a test database + db, _ := setupHonuDB(t) + + // Assert that there is nothing in the namespace as an initial check + requireNamespaceLen(t, db, "graveyard", 0) + requireDatabaseLen(t, db, 0) + + // Create a list of keys with integer values + keys := make([][]byte, 0, 20) + for i := 0; i < 20; i++ { + key := []byte(fmt.Sprintf("%00X", i+1)) + keys = append(keys, key) + } + + // Add data to the database + for _, key := range keys { + db.Put(key, randomData(256), options.WithNamespace("graveyard")) + } + requireNamespaceLen(t, db, "graveyard", 20) + requireDatabaseLen(t, db, 20) + + // Delete all even keys + for i, key := range keys { + if i%2 == 0 { + db.Delete(key, options.WithNamespace("graveyard")) + } + } + + // Ensure that the iterator returns 10 items but that there are still 20 objects + // including tombstones still stored in the database. + requireNamespaceLen(t, db, "graveyard", 10) + requireGraveyardLen(t, db, "graveyard", 20) + requireDatabaseLen(t, db, 20) + + // Sanity check, attempt to get Get all keys and verify tombstones + for i, key := range keys { + if i%2 == 0 { + // This is a tombstone + val, err := db.Get(key, options.WithNamespace("graveyard")) + require.EqualError(t, err, "not found", "tombstone did not return a not found error") + require.Nil(t, val, "tombstone returned a non nil value") + + obj, err := db.Object(key, options.WithNamespace("graveyard")) + require.NoError(t, err, "tombstone did not return an object") + require.True(t, obj.Tombstone()) + } else { + // Not a tombstone + val, err := db.Get(key, options.WithNamespace("graveyard")) + require.NoError(t, err, "a live object returned error on get") + require.Len(t, val, 256) + + obj, err := db.Object(key, options.WithNamespace("graveyard")) + require.NoError(t, err, "live object did not return an object") + require.False(t, obj.Tombstone()) + } + } + + // "Resurrect" every 4th tombstone and give it a new value + for i, key := range keys { + if i%4 == 0 { + db.Put(key, randomData(192), options.WithNamespace("graveyard")) + } + } + + // Ensure that the iterator returns 15 items but that there are still 20 objects + // including tombstones still stored in the database. + requireNamespaceLen(t, db, "graveyard", 15) + requireGraveyardLen(t, db, "graveyard", 20) + requireDatabaseLen(t, db, 20) + + // Sanity check, attempt to get Get all keys and verify tombstones and undead keys + for i, key := range keys { + if i%2 == 0 { + if i%4 == 0 { + // This is an undead version + val, err := db.Get(key, options.WithNamespace("graveyard")) + require.NoError(t, err, "undead object returned error on get") + require.Len(t, val, 192) + + obj, err := db.Object(key, options.WithNamespace("graveyard")) + require.NoError(t, err, "undead object did not return an object") + require.False(t, obj.Tombstone()) + } else { + // This is a tombstone + val, err := db.Get(key, options.WithNamespace("graveyard")) + require.EqualError(t, err, "not found", "tombstone did not return a not found error") + require.Nil(t, val, "tombstone returned a non nil value") + + obj, err := db.Object(key, options.WithNamespace("graveyard")) + require.NoError(t, err, "tombstone did not return an object") + require.True(t, obj.Tombstone()) + } + } else { + // Not a tombstone + val, err := db.Get(key, options.WithNamespace("graveyard")) + require.NoError(t, err, "a live object returned error on get") + require.Len(t, val, 256) + + obj, err := db.Object(key, options.WithNamespace("graveyard")) + require.NoError(t, err, "live object did not return an object") + require.False(t, obj.Tombstone()) + } + } +} + +func TestTombstonesMultipleNamespaces(t *testing.T) { + // Create a test database + db, _ := setupHonuDB(t) + namespaces := []string{"graveyard", "cemetery", "catacombs"} + + // Assert that there is nothing in the namespaces as an initial check + for _, ns := range namespaces { + requireNamespaceLen(t, db, ns, 0) + } + requireDatabaseLen(t, db, 0) + + // Create a list of keys with integer values + keys := make([][]byte, 0, 100) + for i := 0; i < 100; i++ { + key := []byte(fmt.Sprintf("%00X", i+1)) + keys = append(keys, key) + } + + // Add data to the database + for _, key := range keys { + for _, ns := range namespaces { + db.Put(key, randomData(256), options.WithNamespace(ns)) + } + } + + for _, ns := range namespaces { + requireNamespaceLen(t, db, ns, 100) + } + requireDatabaseLen(t, db, 300) + + // Delete all even keys + for i, key := range keys { + if i%2 == 0 { + for _, ns := range namespaces { + db.Delete(key, options.WithNamespace(ns)) + } + } + } + + // Ensure that the iterator returns 50 items but that there are still 100 objects + // including tombstones still stored in the database. Also ensure that the entire + // database still contains 300 objects. + for _, ns := range namespaces { + requireNamespaceLen(t, db, ns, 50) + requireGraveyardLen(t, db, ns, 100) + } + requireDatabaseLen(t, db, 300) + + // "Resurrect" every 4th tombstone and give it a new value + for i, key := range keys { + if i%4 == 0 { + for _, ns := range namespaces { + db.Put(key, randomData(192), options.WithNamespace(ns)) + } + } + } + + // Ensure that the iterator returns 75 items but that there are still 100 objects + // including tombstones still stored in the database. Also ensure that the entire + // database still contains 300 objects. + for _, ns := range namespaces { + requireNamespaceLen(t, db, ns, 75) + requireGraveyardLen(t, db, ns, 100) + } + requireDatabaseLen(t, db, 300) +} + // Helper assertion function to check to make sure an object matches what is in the database func requireObjectEqual(t *testing.T, db *honu.DB, expected *object.Object, key []byte, namespace string) { actual, err := db.Object(key, options.WithNamespace(namespace)) @@ -335,6 +491,51 @@ func requireObjectEqual(t *testing.T, db *honu.DB, expected *object.Object, key require.True(t, bytes.Equal(expected.Data, actual.Data), "value is not equal") } +func requireNamespaceLen(t *testing.T, db *honu.DB, namespace string, expected int) { + iter, err := db.Iter(nil, options.WithNamespace(namespace)) + require.NoError(t, err) + + actual := 0 + for iter.Next() { + actual++ + } + + require.NoError(t, iter.Error()) + iter.Release() + require.Equal(t, expected, actual) +} + +func requireGraveyardLen(t *testing.T, db *honu.DB, namespace string, expected int) { + iter, err := db.Iter(nil, options.WithNamespace(namespace), options.WithTombstones()) + require.NoError(t, err) + + actual := 0 + for iter.Next() { + actual++ + } + + require.NoError(t, iter.Error()) + iter.Release() + require.Equal(t, expected, actual) +} + +func requireDatabaseLen(t *testing.T, db *honu.DB, expected int) { + engine, ok := db.Engine().(*leveldb.LevelDBEngine) + require.True(t, ok, "database len requires a leveldb engine") + ldb := engine.DB() + + actual := 0 + iter := ldb.NewIterator(nil, nil) + for iter.Next() { + actual++ + } + + require.NoError(t, iter.Error(), "could not iterate using leveldb directly") + iter.Release() + + require.Equal(t, expected, actual, "database key count does not match") +} + // Helper function to generate random data func randomData(len int) []byte { data := make([]byte, len) diff --git a/iterator/iterator.go b/iterator/iterator.go index d9ca2d4..ae9612a 100644 --- a/iterator/iterator.go +++ b/iterator/iterator.go @@ -1,9 +1,6 @@ /* Package iterator provides an interface and implementations to traverse over the contents of an embedded database while maintaining and reading replicated object metadata. - -TODO: Implement IteratorSeeker interface from leveldb -TODO: Implement sqliteIterator and genericize rows with key/values */ package iterator @@ -23,6 +20,7 @@ var ( // a leveldb iterator or a sqlite rows context, fetching one row at a time in a Next // loop. The Iterator also provides access to the versioned metadata for low-level // interactions with the replicated data types. +// TODO: Implement IteratorSeeker interface from leveldb type Iterator interface { // Next moves the iterator to the next key/value pair or row. // It returns false if the iterator has been exhausted. diff --git a/options/options.go b/options/options.go index f4cfadd..89cacb7 100644 --- a/options/options.go +++ b/options/options.go @@ -29,6 +29,7 @@ type Options struct { PebbleWrite *pebble.WriteOptions Namespace string Force bool + Tombstones bool } // Defines the signature of functions accepted as parameters by Honu methods. @@ -53,6 +54,14 @@ func WithForce() Option { } } +// WithTombstones causes the iterator to include tombstones as its iterating. +func WithTombstones() Option { + return func(cfg *Options) error { + cfg.Tombstones = true + return nil + } +} + //Closure returning a function that adds the leveldbRead //parameter to an Options struct's LeveldbRead field. func WithLevelDBRead(opts *ldb.ReadOptions) Option { diff --git a/options/options_test.go b/options/options_test.go index 245dbdd..8196894 100644 --- a/options/options_test.go +++ b/options/options_test.go @@ -14,6 +14,8 @@ func TestHonuOptions(t *testing.T) { opts, err := options.New() require.NoError(t, err, "could not create options") require.Equal(t, options.NamespaceDefault, opts.Namespace) + require.False(t, opts.Force) + require.False(t, opts.Tombstones) // Test setting multiple options opts, err = options.New(options.WithLevelDBRead(&ldb.ReadOptions{Strict: ldb.StrictJournal}), options.WithNamespace("foo")) @@ -26,6 +28,12 @@ func TestHonuOptions(t *testing.T) { opts, err = options.New(options.WithNamespace("")) require.NoError(t, err, "could not create options with empty string namespace") require.Equal(t, options.NamespaceDefault, opts.Namespace) + + // Test boolean options + opts, err = options.New(options.WithForce(), options.WithTombstones()) + require.NoError(t, err, "boolean options returned an error") + require.True(t, opts.Force) + require.True(t, opts.Tombstones) } func TestLevelDBReadOptions(t *testing.T) {