From 5dd5ab304e33f73b02b503f9ac5ec41073931eca Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Thu, 12 Dec 2019 16:26:38 +0100 Subject: [PATCH] feat(kv): update tests to capture forward cursor --- bolt/kv.go | 32 +++++-- testing/kv.go | 237 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+), 6 deletions(-) diff --git a/bolt/kv.go b/bolt/kv.go index 6d31d5833f7..e70c2b63b9c 100644 --- a/bolt/kv.go +++ b/bolt/kv.go @@ -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 } @@ -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 } @@ -246,13 +254,19 @@ 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 } @@ -260,13 +274,19 @@ func (c *Cursor) Next() ([]byte, []byte) { } // 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 } diff --git a/testing/kv.go b/testing/kv.go index 57b5468e683..9dedfc3381a 100644 --- a/testing/kv.go +++ b/testing/kv.go @@ -50,6 +50,10 @@ func KVStore( name: "CursorWithHints", fn: KVCursorWithHints, }, + { + name: "ForwardCursor", + fn: KVForwardCursor, + }, { name: "View", fn: KVView, @@ -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()),