internal/datastore/fake: use gob encoding
Use gob encoding for the datastore fake, avoiding issues with using
shared memory. Also, add a lock around access to in-memory store.
Moves datastore fake package to internal, as it is not cmd/relui
specific and will be useful elsewhere.
For golang/go#40279
Change-Id: I5ed3211a0899133d7d534cae8d4643ab8d40f75e
Reviewed-on: https://go-review.googlesource.com/c/build/+/291193
Trust: Alexander Rakoczy <alex@golang.org>
Run-TryBot: Alexander Rakoczy <alex@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
diff --git a/cmd/relui/web_test.go b/cmd/relui/web_test.go
index 8ab6428..145ad5b 100644
--- a/cmd/relui/web_test.go
+++ b/cmd/relui/web_test.go
@@ -15,8 +15,8 @@
"cloud.google.com/go/pubsub"
"cloud.google.com/go/pubsub/pstest"
- "golang.org/x/build/cmd/relui/internal/datastore/fake"
reluipb "golang.org/x/build/cmd/relui/protos"
+ "golang.org/x/build/internal/datastore/fake"
"google.golang.org/api/option"
"google.golang.org/grpc"
)
diff --git a/cmd/relui/internal/datastore/fake/client.go b/internal/datastore/fake/client.go
similarity index 78%
rename from cmd/relui/internal/datastore/fake/client.go
rename to internal/datastore/fake/client.go
index c697c33..73278e9 100644
--- a/cmd/relui/internal/datastore/fake/client.go
+++ b/internal/datastore/fake/client.go
@@ -2,23 +2,29 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// fake provides a fake implementation of a Datastore client to use in testing.
+// fake provides a fake implementation of a Datastore client to use in
+// testing.
package fake
import (
+ "bytes"
"context"
+ "encoding/gob"
"log"
"reflect"
+ "sync"
"cloud.google.com/go/datastore"
"github.com/googleapis/google-cloud-go-testing/datastore/dsiface"
)
-// Client is a fake implementation of dsiface.Client to use in testing.
+// Client is a fake implementation of dsiface.Client to use in
+// testing.
type Client struct {
dsiface.Client
- db map[string]map[string]interface{}
+ m sync.Mutex
+ db map[string]map[string][]byte
}
var _ dsiface.Client = &Client{}
@@ -48,12 +54,16 @@
panic("unimplemented")
}
-// Get loads the entity stored for key into dst, which must be a struct pointer.
+// Get loads the entity stored for key into dst, which must be a
+// struct pointer.
func (f *Client) Get(_ context.Context, key *datastore.Key, dst interface{}) (err error) {
+ f.m.Lock()
+ defer f.m.Unlock()
if f == nil {
return datastore.ErrNoSuchEntity
}
- if dst == nil { // get catches nil interfaces; we need to catch nil ptr here
+ // get catches nil interfaces; we need to catch nil ptr here
+ if dst == nil {
return datastore.ErrInvalidEntityType
}
kdb := f.db[key.Kind]
@@ -64,20 +74,22 @@
if rv.Kind() != reflect.Ptr {
return datastore.ErrInvalidEntityType
}
- rd := rv.Elem()
v := kdb[key.Encode()]
if v == nil {
return datastore.ErrNoSuchEntity
}
- rd.Set(reflect.ValueOf(v).Elem())
- return nil
+ d := gob.NewDecoder(bytes.NewReader(v))
+ return d.Decode(dst)
}
-// GetAll runs the provided query in the given context and returns all keys that match that query,
-// as well as appending the values to dst.
+// GetAll runs the provided query in the given context and returns all
+// keys that match that query, as well as appending the values to dst.
//
-// GetAll currently only supports a query of all entities of a given Kind, and a dst of a slice of pointers to structs.
+// GetAll currently only supports a query of all entities of a given
+// Kind, and a dst of a slice of pointers to structs.
func (f *Client) GetAll(_ context.Context, q *datastore.Query, dst interface{}) (keys []*datastore.Key, err error) {
+ f.m.Lock()
+ defer f.m.Unlock()
fv := reflect.ValueOf(q).Elem().FieldByName("kind")
kdb := f.db[fv.String()]
if kdb == nil {
@@ -92,9 +104,12 @@
}
keys = append(keys, dk)
// This value is expected to represent a slice of pointers to structs.
- // ev := reflect.New(s.Type().Elem().Elem())
- // json.Unmarshal(v, ev.Interface())
- s.Set(reflect.Append(s, reflect.ValueOf(v)))
+ ev := reflect.New(s.Type().Elem().Elem())
+ d := gob.NewDecoder(bytes.NewReader(v))
+ if err := d.DecodeValue(ev); err != nil {
+ return nil, err
+ }
+ s.Set(reflect.Append(s, ev))
}
return
}
@@ -114,17 +129,25 @@
panic("unimplemented")
}
-// Put saves the entity src into the datastore with the given key. src must be a struct pointer.
+// Put saves the entity src into the datastore with the given key. src
+// must be a struct pointer.
func (f *Client) Put(_ context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) {
+ f.m.Lock()
+ defer f.m.Unlock()
if f.db == nil {
- f.db = make(map[string]map[string]interface{})
+ f.db = make(map[string]map[string][]byte)
}
kdb := f.db[key.Kind]
if kdb == nil {
- f.db[key.Kind] = make(map[string]interface{})
+ f.db[key.Kind] = make(map[string][]byte)
kdb = f.db[key.Kind]
}
- kdb[key.Encode()] = src
+ dst := bytes.Buffer{}
+ e := gob.NewEncoder(&dst)
+ if err := e.Encode(src); err != nil {
+ return nil, err
+ }
+ kdb[key.Encode()] = dst.Bytes()
return key, nil
}
diff --git a/cmd/relui/internal/datastore/fake/client_test.go b/internal/datastore/fake/client_test.go
similarity index 72%
rename from cmd/relui/internal/datastore/fake/client_test.go
rename to internal/datastore/fake/client_test.go
index bb34a3d..dc3a2ca 100644
--- a/cmd/relui/internal/datastore/fake/client_test.go
+++ b/internal/datastore/fake/client_test.go
@@ -5,7 +5,9 @@
package fake
import (
+ "bytes"
"context"
+ "encoding/gob"
"testing"
"time"
@@ -20,7 +22,7 @@
func TestClientGet(t *testing.T) {
cases := []struct {
desc string
- db map[string]map[string]interface{}
+ db map[string]map[string][]byte
key *datastore.Key
dst interface{}
want *author
@@ -28,8 +30,8 @@
}{
{
desc: "correct key",
- db: map[string]map[string]interface{}{
- "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): &author{Name: "Kafka"}},
+ db: map[string]map[string][]byte{
+ "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): gobEncode(t, &author{Name: "Kafka"})},
},
key: datastore.NameKey("Author", "The Trial", nil),
dst: new(author),
@@ -37,8 +39,8 @@
},
{
desc: "incorrect key errors",
- db: map[string]map[string]interface{}{
- "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): &author{Name: "Kafka"}},
+ db: map[string]map[string][]byte{
+ "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): gobEncode(t, &author{Name: "Kafka"})},
},
key: datastore.NameKey("Author", "The Go Programming Language", nil),
dst: new(author),
@@ -46,16 +48,16 @@
},
{
desc: "nil dst errors",
- db: map[string]map[string]interface{}{
- "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): &author{Name: "Kafka"}},
+ db: map[string]map[string][]byte{
+ "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): gobEncode(t, &author{Name: "Kafka"})},
},
key: datastore.NameKey("Author", "The Go Programming Language", nil),
wantErr: true,
},
{
desc: "incorrect dst type errors",
- db: map[string]map[string]interface{}{
- "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): &author{Name: "Kafka"}},
+ db: map[string]map[string][]byte{
+ "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): gobEncode(t, &author{Name: "Kafka"})},
},
key: datastore.NameKey("Author", "The Go Programming Language", nil),
dst: &time.Time{},
@@ -63,8 +65,8 @@
},
{
desc: "non-pointer dst errors",
- db: map[string]map[string]interface{}{
- "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): &author{Name: "Kafka"}},
+ db: map[string]map[string][]byte{
+ "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): gobEncode(t, &author{Name: "Kafka"})},
},
key: datastore.NameKey("Author", "The Go Programming Language", nil),
dst: author{},
@@ -72,8 +74,8 @@
},
{
desc: "nil dst errors",
- db: map[string]map[string]interface{}{
- "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): &author{Name: "Kafka"}},
+ db: map[string]map[string][]byte{
+ "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): gobEncode(t, &author{Name: "Kafka"})},
},
key: datastore.NameKey("Author", "The Go Programming Language", nil),
dst: nil,
@@ -106,7 +108,7 @@
func TestClientGetAll(t *testing.T) {
cases := []struct {
desc string
- db map[string]map[string]interface{}
+ db map[string]map[string][]byte
query *datastore.Query
want []*author
wantKeys []*datastore.Key
@@ -114,8 +116,8 @@
}{
{
desc: "all of a Kind",
- db: map[string]map[string]interface{}{
- "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): &author{Name: "Kafka"}},
+ db: map[string]map[string][]byte{
+ "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): gobEncode(t, &author{Name: "Kafka"})},
},
query: datastore.NewQuery("Author"),
wantKeys: []*datastore.Key{datastore.NameKey("Author", "The Trial", nil)},
@@ -123,8 +125,8 @@
},
{
desc: "all of a non-existent kind",
- db: map[string]map[string]interface{}{
- "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): &author{Name: "Kafka"}},
+ db: map[string]map[string][]byte{
+ "Author": {datastore.NameKey("Author", "The Trial", nil).Encode(): gobEncode(t, &author{Name: "Kafka"})},
},
query: datastore.NewQuery("Book"),
wantErr: false,
@@ -158,7 +160,8 @@
if err != nil {
t.Fatalf("cl.Put(_, %v, %v) = %v, %q, wanted no error", gotKey, key, src, err)
}
- got := cl.db["Author"][key.Encode()]
+ got := new(author)
+ gobDecode(t, cl.db["Author"][key.Encode()], got)
if diff := cmp.Diff(src, got); diff != "" {
t.Errorf("author mismatch (-want +got):\n%s", diff)
@@ -167,3 +170,26 @@
t.Errorf("keys mismatch (-want +got):\n%s", diff)
}
}
+
+// gobEncode encodes src with gob, returning the encoded byte slice.
+// It will report errors on the provided testing.T.
+func gobEncode(t *testing.T, src interface{}) []byte {
+ t.Helper()
+ dst := bytes.Buffer{}
+ e := gob.NewEncoder(&dst)
+ if err := e.Encode(src); err != nil {
+ t.Errorf("e.Encode(%v) = %q, wanted no error", src, err)
+ return nil
+ }
+ return dst.Bytes()
+}
+
+// gobDecode decodes v into dst with gob. It will report errors on the
+// provided testing.T.
+func gobDecode(t *testing.T, v []byte, dst interface{}) {
+ t.Helper()
+ d := gob.NewDecoder(bytes.NewReader(v))
+ if err := d.Decode(dst); err != nil {
+ t.Errorf("d.Decode(%v) = %q, wanted no error", dst, err)
+ }
+}