syncbase: Implementation and tests for Glob and Scan.
This CL includes a few smaller changes on the side:
- Implements row-level Get/Put/Delete, needed so we can
test the behavior of Scan. Includes tests for those
methods.
- Enables TestTableCreate and TestTableDelete (since Table
Create/Delete is a prereq for Scan). This required
extending the utilities in testutil/layer.go a bit, and
implementing Table.{Get,Set}Permissions for the "empty
prefix" case.
- Adds FullName() methods to all levels of hierarchy.
I ended up not needing these, but they seem worth keeping
nonetheless.
- Adds various util functions (along with tests for them)
and fixes a few small TODOs.
Change-Id: I55a4b0da9fb8f5551958d09c5ac8afd88dff2ebb
diff --git a/services/syncbase/server/app.go b/services/syncbase/server/app.go
index c1c6dcd..188ef16 100644
--- a/services/syncbase/server/app.go
+++ b/services/syncbase/server/app.go
@@ -12,6 +12,7 @@
"v.io/syncbase/x/ref/services/syncbase/server/util"
"v.io/syncbase/x/ref/services/syncbase/store"
"v.io/v23/context"
+ "v.io/v23/naming"
"v.io/v23/rpc"
"v.io/v23/security/access"
"v.io/v23/verror"
@@ -61,7 +62,15 @@
return data.Perms, util.FormatVersion(data.Version), nil
}
-// TODO(sadovsky): Implement Glob.
+func (a *app) Glob__(ctx *context.T, call rpc.ServerCall, pattern string) (<-chan naming.GlobReply, error) {
+ // Check perms.
+ sn := a.s.st.NewSnapshot()
+ if err := util.Get(ctx, call, sn, a, &appData{}); err != nil {
+ sn.Close()
+ return nil, err
+ }
+ return util.Glob(ctx, call, pattern, sn, util.JoinKeyParts(util.DbInfoPrefix, a.name))
+}
////////////////////////////////////////
// util.App methods
diff --git a/services/syncbase/server/db_info.go b/services/syncbase/server/db_info.go
index 6ccc664..e284fae 100644
--- a/services/syncbase/server/db_info.go
+++ b/services/syncbase/server/db_info.go
@@ -37,7 +37,7 @@
}
func (d *dbInfoLayer) StKey() string {
- return util.JoinKeyParts(util.DbInfoPrefix, d.stKeyPart())
+ return util.JoinKeyParts(util.DbInfoPrefix, d.a.name, d.stKeyPart())
}
////////////////////////////////////////
diff --git a/services/syncbase/server/dispatcher.go b/services/syncbase/server/dispatcher.go
index a6127cd..c676f97 100644
--- a/services/syncbase/server/dispatcher.go
+++ b/services/syncbase/server/dispatcher.go
@@ -8,8 +8,8 @@
"strings"
wire "v.io/syncbase/v23/services/syncbase"
+ pubutil "v.io/syncbase/v23/syncbase/util"
"v.io/syncbase/x/ref/services/syncbase/server/nosql"
- "v.io/syncbase/x/ref/services/syncbase/server/util"
"v.io/v23/rpc"
"v.io/v23/security"
"v.io/v23/verror"
@@ -37,7 +37,7 @@
// Validate all key atoms up front, so that we can avoid doing so in all our
// method implementations.
appName := parts[0]
- if !util.ValidKeyAtom(appName) {
+ if !pubutil.ValidName(appName) {
return nil, nil, wire.NewErrInvalidName(nil, suffix)
}
diff --git a/services/syncbase/server/nosql/database.go b/services/syncbase/server/nosql/database.go
index 03377fc..7ca9271 100644
--- a/services/syncbase/server/nosql/database.go
+++ b/services/syncbase/server/nosql/database.go
@@ -10,6 +10,7 @@
"v.io/syncbase/x/ref/services/syncbase/store"
"v.io/syncbase/x/ref/services/syncbase/store/memstore"
"v.io/v23/context"
+ "v.io/v23/naming"
"v.io/v23/rpc"
"v.io/v23/security/access"
"v.io/v23/verror"
@@ -34,7 +35,7 @@
// Designed for use from within App.CreateNoSQLDatabase.
func NewDatabase(ctx *context.T, call rpc.ServerCall, a util.App, name string, perms access.Permissions) (*database, error) {
if perms == nil {
- return nil, verror.New(verror.ErrInternal, nil, "perms must be specified")
+ return nil, verror.New(verror.ErrInternal, ctx, "perms must be specified")
}
// TODO(sadovsky): Make storage engine pluggable.
d := &database{
@@ -90,7 +91,15 @@
return data.Perms, util.FormatVersion(data.Version), nil
}
-// TODO(sadovsky): Implement Glob.
+func (d *database) Glob__(ctx *context.T, call rpc.ServerCall, pattern string) (<-chan naming.GlobReply, error) {
+ // Check perms.
+ sn := d.st.NewSnapshot()
+ if err := util.Get(ctx, call, sn, d, &databaseData{}); err != nil {
+ sn.Close()
+ return nil, err
+ }
+ return util.Glob(ctx, call, pattern, sn, util.TablePrefix)
+}
////////////////////////////////////////
// util.Database methods
diff --git a/services/syncbase/server/nosql/dispatcher.go b/services/syncbase/server/nosql/dispatcher.go
index 50737c5..db18382 100644
--- a/services/syncbase/server/nosql/dispatcher.go
+++ b/services/syncbase/server/nosql/dispatcher.go
@@ -9,6 +9,7 @@
wire "v.io/syncbase/v23/services/syncbase"
nosqlWire "v.io/syncbase/v23/services/syncbase/nosql"
+ pubutil "v.io/syncbase/v23/syncbase/util"
"v.io/syncbase/x/ref/services/syncbase/server/util"
"v.io/v23/rpc"
"v.io/v23/security"
@@ -37,7 +38,7 @@
// Validate all key atoms up front, so that we can avoid doing so in all our
// method implementations.
for _, s := range parts {
- if !util.ValidKeyAtom(s) {
+ if !pubutil.ValidName(s) {
return nil, nil, wire.NewErrInvalidName(nil, suffix)
}
}
diff --git a/services/syncbase/server/nosql/row.go b/services/syncbase/server/nosql/row.go
index c699e88..c26eb39 100644
--- a/services/syncbase/server/nosql/row.go
+++ b/services/syncbase/server/nosql/row.go
@@ -10,7 +10,6 @@
"v.io/syncbase/x/ref/services/syncbase/store"
"v.io/v23/context"
"v.io/v23/rpc"
- "v.io/v23/vdl"
"v.io/v23/verror"
)
@@ -32,11 +31,11 @@
////////////////////////////////////////
// RPC methods
-func (r *row) Get(ctx *context.T, call rpc.ServerCall) (*vdl.Value, error) {
+func (r *row) Get(ctx *context.T, call rpc.ServerCall) ([]byte, error) {
return r.get(ctx, call, r.t.d.st)
}
-func (r *row) Put(ctx *context.T, call rpc.ServerCall, value *vdl.Value) error {
+func (r *row) Put(ctx *context.T, call rpc.ServerCall, value []byte) error {
return r.put(ctx, call, r.t.d.st, value)
}
@@ -68,18 +67,18 @@
// an authorization check (currently against the table perms).
// Returns a VDL-compatible error.
func (r *row) checkAccess(ctx *context.T, call rpc.ServerCall, st store.StoreReader) error {
- return util.Get(ctx, call, st, r.t.d, &databaseData{})
+ return util.Get(ctx, call, st, r.t, &tableData{})
}
// get reads data from the storage engine.
// Performs authorization check.
// Returns a VDL-compatible error.
-func (r *row) get(ctx *context.T, call rpc.ServerCall, st store.StoreReader) (*vdl.Value, error) {
+func (r *row) get(ctx *context.T, call rpc.ServerCall, st store.StoreReader) ([]byte, error) {
if err := r.checkAccess(ctx, call, st); err != nil {
return nil, err
}
- value := &vdl.Value{}
- if err := util.GetObject(st, r.StKey(), value); err != nil {
+ value, err := st.Get([]byte(r.StKey()), nil)
+ if err != nil {
if _, ok := err.(*store.ErrUnknownKey); ok {
// We've already done an auth check, so here we can safely return NoExist
// rather than NoExistOrNoAccess.
@@ -93,11 +92,14 @@
// put writes data to the storage engine.
// Performs authorization check.
// Returns a VDL-compatible error.
-func (r *row) put(ctx *context.T, call rpc.ServerCall, st store.StoreReadWriter, value *vdl.Value) error {
+func (r *row) put(ctx *context.T, call rpc.ServerCall, st store.StoreReadWriter, value []byte) error {
if err := r.checkAccess(ctx, call, st); err != nil {
return err
}
- return util.Put(ctx, call, st, r, value)
+ if err := st.Put([]byte(r.StKey()), value); err != nil {
+ return verror.New(verror.ErrInternal, ctx, err)
+ }
+ return nil
}
// del deletes data from the storage engine.
@@ -107,5 +109,8 @@
if err := r.checkAccess(ctx, call, st); err != nil {
return err
}
- return util.Delete(ctx, call, st, r)
+ if err := st.Delete([]byte(r.StKey())); err != nil {
+ return verror.New(verror.ErrInternal, ctx, err)
+ }
+ return nil
}
diff --git a/services/syncbase/server/nosql/table.go b/services/syncbase/server/nosql/table.go
index 1983ba8..67c79a7 100644
--- a/services/syncbase/server/nosql/table.go
+++ b/services/syncbase/server/nosql/table.go
@@ -9,6 +9,7 @@
"v.io/syncbase/x/ref/services/syncbase/server/util"
"v.io/syncbase/x/ref/services/syncbase/store"
"v.io/v23/context"
+ "v.io/v23/naming"
"v.io/v23/rpc"
"v.io/v23/security/access"
"v.io/v23/verror"
@@ -67,23 +68,67 @@
})
}
-func (t *table) DeleteRowRange(ctx *context.T, call rpc.ServerCall, start, limit string) error {
+func (t *table) DeleteRowRange(ctx *context.T, call rpc.ServerCall, start, end string) error {
return verror.NewErrNotImplemented(ctx)
}
+func (t *table) Scan(ctx *context.T, call wire.TableScanServerCall, start, end string) error {
+ sn := t.d.st.NewSnapshot()
+ defer sn.Close()
+ it, err := sn.Scan(util.ScanRangeArgs(util.JoinKeyParts(util.RowPrefix, t.name), start, end))
+ if err == nil {
+ sender := call.SendStream()
+ key, value := []byte{}, []byte{}
+ for it.Advance() {
+ key = it.Key(key)
+ parts := util.SplitKeyParts(string(key))
+ sender.Send(wire.KeyValue{Key: parts[len(parts)-1], Value: it.Value(value)})
+ }
+ err = it.Err()
+ }
+ if err != nil {
+ return verror.New(verror.ErrInternal, ctx, err)
+ }
+ return nil
+}
+
func (t *table) SetPermissions(ctx *context.T, call rpc.ServerCall, prefix string, perms access.Permissions) error {
- return verror.NewErrNotImplemented(ctx)
+ if prefix != "" {
+ return verror.NewErrNotImplemented(ctx)
+ }
+ return store.RunInTransaction(t.d.st, func(st store.StoreReadWriter) error {
+ data := &tableData{}
+ return util.Update(ctx, call, st, t, data, func() error {
+ data.Perms = perms
+ return nil
+ })
+ })
}
func (t *table) GetPermissions(ctx *context.T, call rpc.ServerCall, key string) ([]wire.PrefixPermissions, error) {
- return nil, verror.NewErrNotImplemented(ctx)
+ if key != "" {
+ return nil, verror.NewErrNotImplemented(ctx)
+ }
+ data := &tableData{}
+ if err := util.Get(ctx, call, t.d.st, t, data); err != nil {
+ return nil, err
+ }
+ return []wire.PrefixPermissions{{Prefix: "", Perms: data.Perms}}, nil
}
func (t *table) DeletePermissions(ctx *context.T, call rpc.ServerCall, prefix string) error {
return verror.NewErrNotImplemented(ctx)
}
-// TODO(sadovsky): Implement Glob.
+func (t *table) Glob__(ctx *context.T, call rpc.ServerCall, pattern string) (<-chan naming.GlobReply, error) {
+ // Check perms.
+ sn := t.d.st.NewSnapshot()
+ if err := util.Get(ctx, call, sn, t, &tableData{}); err != nil {
+ sn.Close()
+ return nil, err
+ }
+ return util.Glob(ctx, call, pattern, sn, util.JoinKeyParts(util.RowPrefix, t.name))
+}
////////////////////////////////////////
// util.Layer methods
diff --git a/services/syncbase/server/service.go b/services/syncbase/server/service.go
index e268787..c8ea107 100644
--- a/services/syncbase/server/service.go
+++ b/services/syncbase/server/service.go
@@ -12,6 +12,7 @@
"v.io/syncbase/x/ref/services/syncbase/store"
"v.io/syncbase/x/ref/services/syncbase/store/memstore"
"v.io/v23/context"
+ "v.io/v23/naming"
"v.io/v23/rpc"
"v.io/v23/security/access"
"v.io/v23/verror"
@@ -34,7 +35,7 @@
// Returns a VDL-compatible error.
func NewService(ctx *context.T, call rpc.ServerCall, perms access.Permissions) (*service, error) {
if perms == nil {
- return nil, verror.New(verror.ErrInternal, nil, "perms must be specified")
+ return nil, verror.New(verror.ErrInternal, ctx, "perms must be specified")
}
// TODO(sadovsky): Make storage engine pluggable.
s := &service{
@@ -75,7 +76,15 @@
return data.Perms, util.FormatVersion(data.Version), nil
}
-// TODO(sadovsky): Implement Glob.
+func (s *service) Glob__(ctx *context.T, call rpc.ServerCall, pattern string) (<-chan naming.GlobReply, error) {
+ // Check perms.
+ sn := s.st.NewSnapshot()
+ if err := util.Get(ctx, call, sn, s, &serviceData{}); err != nil {
+ sn.Close()
+ return nil, err
+ }
+ return util.Glob(ctx, call, pattern, sn, util.AppPrefix)
+}
////////////////////////////////////////
// App management methods
diff --git a/services/syncbase/server/util/glob.go b/services/syncbase/server/util/glob.go
new file mode 100644
index 0000000..750f4db
--- /dev/null
+++ b/services/syncbase/server/util/glob.go
@@ -0,0 +1,82 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package util
+
+import (
+ "strings"
+
+ "v.io/syncbase/x/ref/services/syncbase/store"
+ "v.io/v23/context"
+ "v.io/v23/naming"
+ "v.io/v23/rpc"
+ "v.io/v23/verror"
+ "v.io/x/lib/vlog"
+ "v.io/x/ref/lib/glob"
+)
+
+// globPatternToPrefix takes "foo*" and returns "foo". It returns ErrBadArg for
+// inputs that are not valid glob patterns as well as for inputs that are valid
+// glob patterns but not valid prefixes.
+func globPatternToPrefix(pattern string) (string, error) {
+ if _, err := glob.Parse(pattern); err != nil {
+ return "", verror.NewErrBadArg(nil)
+ }
+ if pattern == "" {
+ return "", verror.NewErrBadArg(nil)
+ }
+ if pattern[len(pattern)-1] != '*' {
+ return "", verror.NewErrBadArg(nil)
+ }
+ res := pattern[:len(pattern)-1]
+ // Disallow chars and char sequences that have special meaning in glob, since
+ // our Glob() does not support these.
+ if strings.ContainsAny(res, "/*?[") {
+ return "", verror.NewErrBadArg(nil)
+ }
+ if strings.Contains(res, "...") {
+ return "", verror.NewErrBadArg(nil)
+ }
+ return res, nil
+}
+
+// TODO(sadovsky): It sucks that Glob must be implemented differently from other
+// streaming RPC handlers. I don't have much confidence that I've implemented
+// both types of streaming correctly.
+func Glob(ctx *context.T, call rpc.ServerCall, pattern string, sn store.Snapshot, stKeyPrefix string) (<-chan naming.GlobReply, error) {
+ // TODO(sadovsky): Support glob with non-prefix pattern.
+ prefix, err := globPatternToPrefix(pattern)
+ if err != nil {
+ sn.Close()
+ if verror.ErrorID(err) == verror.ErrBadArg.ID {
+ return nil, verror.NewErrNotImplemented(ctx)
+ } else {
+ return nil, verror.New(verror.ErrInternal, ctx, err)
+ }
+ }
+ // TODO(sadovsky): Does Scan really need to return an error?
+ it, err := sn.Scan(ScanPrefixArgs(stKeyPrefix, prefix))
+ if err != nil {
+ sn.Close()
+ return nil, verror.New(verror.ErrInternal, ctx, err)
+ }
+ ch := make(chan naming.GlobReply)
+ go func() {
+ defer sn.Close()
+ defer close(ch)
+ key := []byte{}
+ for it.Advance() {
+ key = it.Key(key)
+ parts := SplitKeyParts(string(key))
+ ch <- naming.GlobReplyEntry{naming.MountEntry{Name: parts[len(parts)-1]}}
+ }
+ if err := it.Err(); err != nil {
+ vlog.VI(1).Infof("Glob(%q) failed: %v", pattern, err)
+ ch <- naming.GlobReplyError{naming.GlobError{
+ Error: verror.New(verror.ErrInternal, ctx, err),
+ }}
+ }
+ }()
+ return ch, nil
+}
diff --git a/services/syncbase/server/util/glob_test.go b/services/syncbase/server/util/glob_test.go
new file mode 100644
index 0000000..d9c8b71
--- /dev/null
+++ b/services/syncbase/server/util/glob_test.go
@@ -0,0 +1,54 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Note, we use package util rather than util_test so that we can access the
+// unexported name util.globPatternToPrefix.
+
+package util
+
+import (
+ "testing"
+
+ "v.io/v23/verror"
+)
+
+var (
+ errBadArg = string(verror.ErrBadArg.ID)
+)
+
+func TestGlobPatternToPrefix(t *testing.T) {
+ tests := []struct {
+ pattern, prefix string
+ }{
+ {"", errBadArg},
+ {"*", ""},
+ {"foo", errBadArg},
+ {"foo*", "foo"},
+ {"foo/", errBadArg},
+ {"foo/*", errBadArg},
+ {"foo/bar", errBadArg},
+ {"foo/bar*", errBadArg},
+ {".*", "."},
+ {"..*", ".."},
+ {"...*", errBadArg},
+ {"...foo*", errBadArg},
+ {"foo...foo*", errBadArg},
+ {"..foo*", "..foo"},
+ {"foo..foo*", "foo..foo"},
+ {"...", errBadArg},
+ {"*/...", errBadArg},
+ {"foo/...", errBadArg},
+ {"/", errBadArg},
+ {"/*", errBadArg},
+ }
+ for _, test := range tests {
+ prefix, err := globPatternToPrefix(test.pattern)
+ if err != nil {
+ prefix = string(verror.ErrorID(err))
+ }
+ if prefix != test.prefix {
+ t.Errorf("%q: got %q, want %q", test.pattern, prefix, test.prefix)
+ }
+ }
+}
diff --git a/services/syncbase/server/util/key_util.go b/services/syncbase/server/util/key_util.go
index 542a0ce..d5b6e36 100644
--- a/services/syncbase/server/util/key_util.go
+++ b/services/syncbase/server/util/key_util.go
@@ -4,23 +4,34 @@
package util
-// Note, some of the code below is copied from
-// v.io/syncbase/v23/syncbase/util/util.go.
-
import (
- "regexp"
"strings"
+
+ "v.io/syncbase/v23/syncbase/util"
)
-// TODO(sadovsky): Consider loosening. Perhaps model after MySQL:
-// http://dev.mysql.com/doc/refman/5.7/en/identifiers.html
-var keyAtomRegexp *regexp.Regexp = regexp.MustCompile("^[a-zA-Z0-9_.-]+$")
-
-func ValidKeyAtom(s string) bool {
- return keyAtomRegexp.MatchString(s)
-}
-
+// JoinKeyParts builds keys for accessing data in the storage engine.
func JoinKeyParts(parts ...string) string {
// TODO(sadovsky): Figure out which delimeter makes the most sense.
return strings.Join(parts, ":")
}
+
+// SplitKeyParts is the inverse of JoinKeyParts.
+func SplitKeyParts(key string) []string {
+ return strings.Split(key, ":")
+}
+
+// ScanPrefixArgs returns args for sn.Scan() for the specified prefix.
+func ScanPrefixArgs(stKeyPrefix, prefix string) ([]byte, []byte) {
+ return ScanRangeArgs(stKeyPrefix, util.PrefixRangeStart(prefix), util.PrefixRangeEnd(prefix))
+}
+
+// ScanRangeArgs returns args for sn.Scan() for the specified range.
+// If end is "", all rows with keys >= start are included.
+func ScanRangeArgs(stKeyPrefix, start, end string) ([]byte, []byte) {
+ fullStart, fullEnd := JoinKeyParts(stKeyPrefix, start), JoinKeyParts(stKeyPrefix, end)
+ if end == "" {
+ fullEnd = util.PrefixRangeEnd(fullEnd)
+ }
+ return []byte(fullStart), []byte(fullEnd)
+}
diff --git a/services/syncbase/server/util/key_util_test.go b/services/syncbase/server/util/key_util_test.go
new file mode 100644
index 0000000..1bc1148
--- /dev/null
+++ b/services/syncbase/server/util/key_util_test.go
@@ -0,0 +1,83 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package util_test
+
+import (
+ "reflect"
+ "testing"
+
+ "v.io/syncbase/x/ref/services/syncbase/server/util"
+)
+
+type kpt struct {
+ parts []string
+ key string
+}
+
+var keyPartTests []kpt = []kpt{
+ {[]string{"a", "b"}, "a:b"},
+ {[]string{"aa", "bb"}, "aa:bb"},
+ {[]string{"a", "b", "c"}, "a:b:c"},
+}
+
+func TestJoinKeyParts(t *testing.T) {
+ for _, test := range keyPartTests {
+ got, want := util.JoinKeyParts(test.parts...), test.key
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("%v: got %q, want %q", test.parts, got, want)
+ }
+ }
+}
+
+func TestSplitKeyParts(t *testing.T) {
+ for _, test := range keyPartTests {
+ got, want := util.SplitKeyParts(test.key), test.parts
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("%q: got %v, want %v", test.key, got, want)
+ }
+ }
+}
+
+func TestScanPrefixArgs(t *testing.T) {
+ tests := []struct {
+ stKeyPrefix, prefix, wantStart, wantEnd string
+ }{
+ {"x", "", "x:", "x;"},
+ {"x", "a", "x:a", "x:b"},
+ {"x", "a\xff", "x:a\xff", "x:b"},
+ }
+ for _, test := range tests {
+ start, end := util.ScanPrefixArgs(test.stKeyPrefix, test.prefix)
+ gotStart, gotEnd := string(start), string(end)
+ if gotStart != test.wantStart {
+ t.Errorf("{%q, %q} start: got %q, want %q", test.stKeyPrefix, test.prefix, gotStart, test.wantStart)
+ }
+ if gotEnd != test.wantEnd {
+ t.Errorf("{%q, %q} end: got %q, want %q", test.stKeyPrefix, test.prefix, gotEnd, test.wantEnd)
+ }
+ }
+}
+
+func TestScanRangeArgs(t *testing.T) {
+ tests := []struct {
+ stKeyPrefix, start, end, wantStart, wantEnd string
+ }{
+ {"x", "", "", "x:", "x;"}, // end "" means "no limit"
+ {"x", "a", "", "x:a", "x;"}, // end "" means "no limit"
+ {"x", "a", "b", "x:a", "x:b"},
+ {"x", "a", "a", "x:a", "x:a"}, // empty range
+ {"x", "b", "a", "x:b", "x:a"}, // empty range
+ }
+ for _, test := range tests {
+ start, end := util.ScanRangeArgs(test.stKeyPrefix, test.start, test.end)
+ gotStart, gotEnd := string(start), string(end)
+ if gotStart != test.wantStart {
+ t.Errorf("{%q, %q, %q} start: got %q, want %q", test.stKeyPrefix, test.start, test.end, gotStart, test.wantStart)
+ }
+ if gotEnd != test.wantEnd {
+ t.Errorf("{%q, %q, %q} end: got %q, want %q", test.stKeyPrefix, test.start, test.end, gotEnd, test.wantEnd)
+ }
+ }
+}