blob: ac52f7c172439f2521b283558fed221aa3f1f2c6 [file] [log] [blame]
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package datastore
import (
"testing"
"time"
"cloud.google.com/go/internal/testutil"
pb "google.golang.org/genproto/googleapis/datastore/v1"
)
func TestInterfaceToProtoNil(t *testing.T) {
// A nil *Key, or a nil value of any other pointer type, should convert to a NullValue.
for _, in := range []interface{}{
(*Key)(nil),
(*int)(nil),
(*string)(nil),
(*bool)(nil),
(*float64)(nil),
(*GeoPoint)(nil),
(*time.Time)(nil),
} {
got, err := interfaceToProto(in, false)
if err != nil {
t.Fatalf("%T: %v", in, err)
}
_, ok := got.ValueType.(*pb.Value_NullValue)
if !ok {
t.Errorf("%T: got: %T\nwant: %T", in, got.ValueType, &pb.Value_NullValue{})
}
}
}
func TestSaveEntityNested(t *testing.T) {
type WithKey struct {
X string
I int
K *Key `datastore:"__key__"`
}
type NestedWithKey struct {
Y string
N WithKey
}
type WithoutKey struct {
X string
I int
}
type NestedWithoutKey struct {
Y string
N WithoutKey
}
type a struct {
S string
}
type UnexpAnonym struct {
a
}
testCases := []struct {
desc string
src interface{}
key *Key
want *pb.Entity
}{
{
desc: "nested entity with key",
src: &NestedWithKey{
Y: "yyy",
N: WithKey{
X: "two",
I: 2,
K: testKey1a,
},
},
key: testKey0,
want: &pb.Entity{
Key: keyToProto(testKey0),
Properties: map[string]*pb.Value{
"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
"N": {ValueType: &pb.Value_EntityValue{
EntityValue: &pb.Entity{
Key: keyToProto(testKey1a),
Properties: map[string]*pb.Value{
"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
},
},
}},
},
},
},
{
desc: "nested entity with incomplete key",
src: &NestedWithKey{
Y: "yyy",
N: WithKey{
X: "two",
I: 2,
K: incompleteKey,
},
},
key: testKey0,
want: &pb.Entity{
Key: keyToProto(testKey0),
Properties: map[string]*pb.Value{
"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
"N": {ValueType: &pb.Value_EntityValue{
EntityValue: &pb.Entity{
Key: keyToProto(incompleteKey),
Properties: map[string]*pb.Value{
"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
},
},
}},
},
},
},
{
desc: "nested entity without key",
src: &NestedWithoutKey{
Y: "yyy",
N: WithoutKey{
X: "two",
I: 2,
},
},
key: testKey0,
want: &pb.Entity{
Key: keyToProto(testKey0),
Properties: map[string]*pb.Value{
"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
"N": {ValueType: &pb.Value_EntityValue{
EntityValue: &pb.Entity{
Properties: map[string]*pb.Value{
"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
},
},
}},
},
},
},
{
desc: "key at top level",
src: &WithKey{
X: "three",
I: 3,
K: testKey0,
},
key: testKey0,
want: &pb.Entity{
Key: keyToProto(testKey0),
Properties: map[string]*pb.Value{
"X": {ValueType: &pb.Value_StringValue{StringValue: "three"}},
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 3}},
},
},
},
{
desc: "nested unexported anonymous struct field",
src: &UnexpAnonym{
a{S: "hello"},
},
key: testKey0,
want: &pb.Entity{
Key: keyToProto(testKey0),
Properties: map[string]*pb.Value{
"S": {ValueType: &pb.Value_StringValue{StringValue: "hello"}},
},
},
},
}
for _, tc := range testCases {
got, err := saveEntity(tc.key, tc.src)
if err != nil {
t.Errorf("saveEntity: %s: %v", tc.desc, err)
continue
}
if !testutil.Equal(tc.want, got) {
t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, got, tc.want)
}
}
}
func TestSavePointers(t *testing.T) {
for _, test := range []struct {
desc string
in interface{}
want []Property
}{
{
desc: "nil pointers save as nil-valued properties",
in: &Pointers{},
want: []Property{
{Name: "Pi", Value: nil},
{Name: "Ps", Value: nil},
{Name: "Pb", Value: nil},
{Name: "Pf", Value: nil},
{Name: "Pg", Value: nil},
{Name: "Pt", Value: nil},
},
},
{
desc: "nil omitempty pointers not saved",
in: &PointersOmitEmpty{},
want: []Property(nil),
},
{
desc: "non-nil omitempty zero-valued pointers are saved",
in: func() *PointersOmitEmpty { pi := 0; return &PointersOmitEmpty{Pi: &pi} }(),
want: []Property{{Name: "Pi", Value: int64(0)}},
},
{
desc: "non-nil zero-valued pointers save as zero values",
in: populatedPointers(),
want: []Property{
{Name: "Pi", Value: int64(0)},
{Name: "Ps", Value: ""},
{Name: "Pb", Value: false},
{Name: "Pf", Value: 0.0},
{Name: "Pg", Value: GeoPoint{}},
{Name: "Pt", Value: time.Time{}},
},
},
{
desc: "non-nil non-zero-valued pointers save as the appropriate values",
in: func() *Pointers {
p := populatedPointers()
*p.Pi = 1
*p.Ps = "x"
*p.Pb = true
*p.Pf = 3.14
*p.Pg = GeoPoint{Lat: 1, Lng: 2}
*p.Pt = time.Unix(100, 0)
return p
}(),
want: []Property{
{Name: "Pi", Value: int64(1)},
{Name: "Ps", Value: "x"},
{Name: "Pb", Value: true},
{Name: "Pf", Value: 3.14},
{Name: "Pg", Value: GeoPoint{Lat: 1, Lng: 2}},
{Name: "Pt", Value: time.Unix(100, 0)},
},
},
} {
got, err := SaveStruct(test.in)
if err != nil {
t.Fatalf("%s: %v", test.desc, err)
}
if !testutil.Equal(got, test.want) {
t.Errorf("%s\ngot %#v\nwant %#v\n", test.desc, got, test.want)
}
}
}
func TestSaveEmptySlice(t *testing.T) {
// Zero-length slice fields are not saved.
for _, slice := range [][]string{nil, {}} {
got, err := SaveStruct(&struct{ S []string }{S: slice})
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Errorf("%#v: got %d properties, wanted zero", slice, len(got))
}
}
}
// Map is used by TestSaveFieldsWithInterface
// to test a custom type property save.
type Map map[int]int
func (*Map) Load(_ []Property) error {
return nil
}
func (*Map) Save() ([]Property, error) {
return []Property{}, nil
}
// Struct is used by TestSaveFieldsWithInterface
// to test a custom type property save.
type Struct struct {
Map Map
}
func TestSaveFieldsWithInterface(t *testing.T) {
// We should be able to extract the underlying value behind an interface.
// See issue https://github.com/googleapis/google-cloud-go/issues/1474.
type n1 struct {
Inner interface{}
}
type n2 struct {
Inner2 *n1
}
type n3 struct {
N2 interface{}
}
cases := []struct {
name string
in interface{}
want []Property
}{
{
name: "Non-Nil value",
in: &struct {
Value interface{}
ID int
key interface{}
}{
Value: "this is a string",
ID: 17,
key: "key1",
},
want: []Property{
{Name: "Value", Value: "this is a string"},
{Name: "ID", Value: int64(17)},
},
},
{
name: "Nil value",
in: &struct {
foo interface{}
}{
foo: (*string)(nil),
},
want: nil,
},
{
name: "Nil interface",
in: &struct {
Value interface{}
key interface{}
}{
Value: nil,
key: "key1",
},
want: []Property{
{Name: "Value", Value: nil},
},
},
{
name: "Nil map",
in: &Struct{},
want: []Property{
{Name: "Map", Value: []Property{}},
},
},
{
name: "Nested",
in: &n3{
N2: &n2{
Inner2: &n1{
Inner: "Innest",
},
},
},
want: []Property{
{
Name: "N2",
Value: &Entity{
Properties: []Property{{
Name: "Inner2",
Value: &Entity{
Properties: []Property{{
Name: "Inner", Value: "Innest",
}},
},
}},
},
},
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
got, err := SaveStruct(tt.in)
if err != nil {
t.Fatal(err)
}
if diff := testutil.Diff(got, tt.want); diff != "" {
t.Fatalf("got - want +\n%s", diff)
}
})
}
}