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)
+	}
+}