Skip to content

Commit

Permalink
feat(kv): update tests to capture forward cursor
Browse files Browse the repository at this point in the history
  • Loading branch information
GeorgeMac committed Dec 12, 2019
1 parent d0ac4c8 commit 5dd5ab3
Show file tree
Hide file tree
Showing 2 changed files with 263 additions and 6 deletions.
32 changes: 26 additions & 6 deletions bolt/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,15 @@ func (b *Bucket) Delete(key []byte) error {
// ForwardCursor retrieves a cursor for iterating through the entries
// in the key value store in a given direction (ascending / descending).
func (b *Bucket) ForwardCursor(seek []byte, opts ...kv.CursorOption) (kv.ForwardCursor, error) {
cursor := b.bucket.Cursor()
cursor.Seek(seek)
var (
cursor = b.bucket.Cursor()
key, value = cursor.Seek(seek)
)

return &Cursor{
cursor: cursor,
key: key,
value: value,
config: kv.NewCursorConfig(opts...),
}, nil
}
Expand All @@ -215,6 +220,9 @@ func (b *Bucket) Cursor(opts ...kv.CursorHint) (kv.Cursor, error) {
type Cursor struct {
cursor *bolt.Cursor

// previously seeked key/value
key, value []byte

config kv.CursorConfig
}

Expand Down Expand Up @@ -246,27 +254,39 @@ func (c *Cursor) Last() ([]byte, []byte) {
}

// Next retrieves the next key in the bucket.
func (c *Cursor) Next() ([]byte, []byte) {
func (c *Cursor) Next() (k []byte, v []byte) {
// get and unset previously seeked values if they exist
k, v, c.key, c.value = c.key, c.value, nil, nil
if len(k) > 0 && len(v) > 0 {
return
}

next := c.cursor.Next
if c.config.Direction == kv.CursorDescending {
next = c.cursor.Prev
}

k, v := next()
k, v = next()
if len(k) == 0 && len(v) == 0 {
return nil, nil
}
return k, v
}

// Prev retrieves the previous key in the bucket.
func (c *Cursor) Prev() ([]byte, []byte) {
func (c *Cursor) Prev() (k []byte, v []byte) {
// get and unset previously seeked values if they exist
k, v, c.key, c.value = c.key, c.value, nil, nil
if len(k) > 0 && len(v) > 0 {
return
}

prev := c.cursor.Prev
if c.config.Direction == kv.CursorDescending {
prev = c.cursor.Next
}

k, v := prev()
k, v = prev()
if len(k) == 0 && len(v) == 0 {
return nil, nil
}
Expand Down
237 changes: 237 additions & 0 deletions testing/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ func KVStore(
name: "CursorWithHints",
fn: KVCursorWithHints,
},
{
name: "ForwardCursor",
fn: KVForwardCursor,
},
{
name: "View",
fn: KVView,
Expand Down Expand Up @@ -672,6 +676,239 @@ func KVCursorWithHints(
}
}

// KVForwardCursor tests the forward cursor contract for the key value store.
func KVForwardCursor(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
t *testing.T,
) {
type args struct {
seek string
direction kv.CursorDirection
until string
hints []kv.CursorHint
}

pairs := func(keys ...string) []kv.Pair {
p := make([]kv.Pair, len(keys))
for i, k := range keys {
p[i].Key = []byte(k)
p[i].Value = []byte("val:" + k)
}
return p
}

tests := []struct {
name string
fields KVStoreFields
args args
exp []string
}{
{
name: "no hints",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: pairs(
"aa/00", "aa/01",
"aaa/00", "aaa/01", "aaa/02", "aaa/03",
"bbb/00", "bbb/01", "bbb/02"),
},
args: args{
seek: "aaa",
until: "bbb/00",
},
exp: []string{"aaa/00", "aaa/01", "aaa/02", "aaa/03", "bbb/00"},
},
{
name: "prefix hint",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: pairs(
"aa/00", "aa/01",
"aaa/00", "aaa/01", "aaa/02", "aaa/03",
"bbb/00", "bbb/01", "bbb/02"),
},
args: args{
seek: "aaa",
until: "aaa/03",
hints: []kv.CursorHint{kv.WithCursorHintPrefix("aaa/")},
},
exp: []string{"aaa/00", "aaa/01", "aaa/02", "aaa/03"},
},
{
name: "start hint",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: pairs(
"aa/00", "aa/01",
"aaa/00", "aaa/01", "aaa/02", "aaa/03",
"bbb/00", "bbb/01", "bbb/02"),
},
args: args{
seek: "aaa",
until: "bbb/00",
hints: []kv.CursorHint{kv.WithCursorHintKeyStart("aaa/")},
},
exp: []string{"aaa/00", "aaa/01", "aaa/02", "aaa/03", "bbb/00"},
},
{
name: "predicate for key",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: pairs(
"aa/00", "aa/01",
"aaa/00", "aaa/01", "aaa/02", "aaa/03",
"bbb/00", "bbb/01", "bbb/02"),
},
args: args{
seek: "aaa",
until: "aaa/03",
hints: []kv.CursorHint{
kv.WithCursorHintPredicate(func(key, _ []byte) bool {
return len(key) < 3 || string(key[:3]) == "aaa"
})},
},
exp: []string{"aaa/00", "aaa/01", "aaa/02", "aaa/03"},
},
{
name: "predicate for value",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: pairs(
"aa/00", "aa/01",
"aaa/00", "aaa/01", "aaa/02", "aaa/03",
"bbb/00", "bbb/01", "bbb/02"),
},
args: args{
seek: "",
until: "aa/01",
hints: []kv.CursorHint{
kv.WithCursorHintPredicate(func(_, val []byte) bool {
return len(val) < 7 || string(val[:7]) == "val:aa/"
})},
},
exp: []string{"aa/00", "aa/01"},
},
{
name: "no hints - descending",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: pairs(
"aa/00", "aa/01",
"aaa/00", "aaa/01", "aaa/02", "aaa/03",
"bbb/00", "bbb/01", "bbb/02"),
},
args: args{
seek: "bbb/00",
until: "aaa/00",
direction: kv.CursorDescending,
},
exp: []string{"bbb/00", "aaa/03", "aaa/02", "aaa/01", "aaa/00"},
},
{
name: "start hint - descending",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: pairs(
"aa/00", "aa/01",
"aaa/00", "aaa/01", "aaa/02", "aaa/03",
"bbb/00", "bbb/01", "bbb/02"),
},
args: args{
seek: "bbb/00",
until: "aaa/00",
direction: kv.CursorDescending,
hints: []kv.CursorHint{kv.WithCursorHintKeyStart("aaa/")},
},
exp: []string{"bbb/00", "aaa/03", "aaa/02", "aaa/01", "aaa/00"},
},
{
name: "predicate for key - descending",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: pairs(
"aa/00", "aa/01",
"aaa/00", "aaa/01", "aaa/02", "aaa/03",
"bbb/00", "bbb/01", "bbb/02"),
},
args: args{
seek: "aaa/03",
until: "aaa",
direction: kv.CursorDescending,
hints: []kv.CursorHint{
kv.WithCursorHintPredicate(func(key, _ []byte) bool {
return len(key) < 3 || string(key[:3]) == "aaa"
})},
},
exp: []string{"aaa/03", "aaa/02", "aaa/01", "aaa/00"},
},
{
name: "predicate for value - descending",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: pairs(
"aa/00", "aa/01",
"aaa/00", "aaa/01", "aaa/02", "aaa/03",
"bbb/00", "bbb/01", "bbb/02"),
},
args: args{
seek: "aa/01",
until: "aa/00",
direction: kv.CursorDescending,
hints: []kv.CursorHint{
kv.WithCursorHintPredicate(func(_, val []byte) bool {
return len(val) >= 7 && string(val[:7]) == "val:aa/"
})},
},
exp: []string{"aa/01", "aa/00"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, fin := init(tt.fields, t)
defer fin()

err := s.View(context.Background(), func(tx kv.Tx) error {
b, err := tx.Bucket([]byte("bucket"))
if err != nil {
t.Errorf("unexpected error retrieving bucket: %v", err)
return err
}

cur, err := b.ForwardCursor([]byte(tt.args.seek),
kv.WithCursorDirection(tt.args.direction),
kv.WithCursorHints(tt.args.hints...))
if err != nil {
t.Errorf("unexpected error: %v", err)
return err
}

var got []string

k, _ := cur.Next()
for len(k) > 0 {
got = append(got, string(k))
if string(k) == tt.args.until {
break
}

k, _ = cur.Next()
}

if exp := tt.exp; !cmp.Equal(got, exp) {
t.Errorf("unexpected cursor values: -got/+exp\n%v", cmp.Diff(got, exp))
}

return nil
})

if err != nil {
t.Fatalf("error during view transaction: %v", err)
}
})
}
}

// KVView tests the view method contract for the key value store.
func KVView(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
Expand Down

0 comments on commit 5dd5ab3

Please sign in to comment.