forked from TrueCloudLab/restic
3493 lines
70 KiB
Go
3493 lines
70 KiB
Go
// Copyright 2014 Google Inc. All Rights Reserved.
|
|
//
|
|
// 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 (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"cloud.google.com/go/internal/testutil"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/google/go-cmp/cmp"
|
|
"golang.org/x/net/context"
|
|
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
type (
|
|
myBlob []byte
|
|
myByte byte
|
|
myString string
|
|
)
|
|
|
|
func makeMyByteSlice(n int) []myByte {
|
|
b := make([]myByte, n)
|
|
for i := range b {
|
|
b[i] = myByte(i)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func makeInt8Slice(n int) []int8 {
|
|
b := make([]int8, n)
|
|
for i := range b {
|
|
b[i] = int8(i)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func makeUint8Slice(n int) []uint8 {
|
|
b := make([]uint8, n)
|
|
for i := range b {
|
|
b[i] = uint8(i)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func newKey(stringID string, parent *Key) *Key {
|
|
return NameKey("kind", stringID, parent)
|
|
}
|
|
|
|
var (
|
|
testKey0 = newKey("name0", nil)
|
|
testKey1a = newKey("name1", nil)
|
|
testKey1b = newKey("name1", nil)
|
|
testKey2a = newKey("name2", testKey0)
|
|
testKey2b = newKey("name2", testKey0)
|
|
testGeoPt0 = GeoPoint{Lat: 1.2, Lng: 3.4}
|
|
testGeoPt1 = GeoPoint{Lat: 5, Lng: 10}
|
|
testBadGeoPt = GeoPoint{Lat: 1000, Lng: 34}
|
|
|
|
ts = time.Unix(1e9, 0).UTC()
|
|
)
|
|
|
|
type B0 struct {
|
|
B []byte `datastore:",noindex"`
|
|
}
|
|
|
|
type B1 struct {
|
|
B []int8
|
|
}
|
|
|
|
type B2 struct {
|
|
B myBlob `datastore:",noindex"`
|
|
}
|
|
|
|
type B3 struct {
|
|
B []myByte `datastore:",noindex"`
|
|
}
|
|
|
|
type B4 struct {
|
|
B [][]byte
|
|
}
|
|
|
|
type C0 struct {
|
|
I int
|
|
C chan int
|
|
}
|
|
|
|
type C1 struct {
|
|
I int
|
|
C *chan int
|
|
}
|
|
|
|
type C2 struct {
|
|
I int
|
|
C []chan int
|
|
}
|
|
|
|
type C3 struct {
|
|
C string
|
|
}
|
|
|
|
type c4 struct {
|
|
C string
|
|
}
|
|
|
|
type E struct{}
|
|
|
|
type G0 struct {
|
|
G GeoPoint
|
|
}
|
|
|
|
type G1 struct {
|
|
G []GeoPoint
|
|
}
|
|
|
|
type K0 struct {
|
|
K *Key
|
|
}
|
|
|
|
type K1 struct {
|
|
K []*Key
|
|
}
|
|
|
|
type S struct {
|
|
St string
|
|
}
|
|
|
|
type NoOmit struct {
|
|
A string
|
|
B int `datastore:"Bb"`
|
|
C bool `datastore:",noindex"`
|
|
}
|
|
|
|
type OmitAll struct {
|
|
A string `datastore:",omitempty"`
|
|
B int `datastore:"Bb,omitempty"`
|
|
C bool `datastore:",omitempty,noindex"`
|
|
F []int `datastore:",omitempty"`
|
|
}
|
|
|
|
type Omit struct {
|
|
A string `datastore:",omitempty"`
|
|
B int `datastore:"Bb,omitempty"`
|
|
C bool `datastore:",omitempty,noindex"`
|
|
F []int `datastore:",omitempty"`
|
|
S `datastore:",omitempty"`
|
|
}
|
|
|
|
type NoOmits struct {
|
|
No []NoOmit `datastore:",omitempty"`
|
|
S `datastore:",omitempty"`
|
|
Ss S `datastore:",omitempty"`
|
|
}
|
|
|
|
type N0 struct {
|
|
X0
|
|
Nonymous X0
|
|
Ignore string `datastore:"-"`
|
|
Other string
|
|
}
|
|
|
|
type N1 struct {
|
|
X0
|
|
Nonymous []X0
|
|
Ignore string `datastore:"-"`
|
|
Other string
|
|
}
|
|
|
|
type N2 struct {
|
|
N1 `datastore:"red"`
|
|
Green N1 `datastore:"green"`
|
|
Blue N1
|
|
White N1 `datastore:"-"`
|
|
}
|
|
|
|
type N3 struct {
|
|
C3 `datastore:"red"`
|
|
}
|
|
|
|
type N4 struct {
|
|
c4
|
|
}
|
|
|
|
type N5 struct {
|
|
c4 `datastore:"red"`
|
|
}
|
|
|
|
type O0 struct {
|
|
I int64
|
|
}
|
|
|
|
type O1 struct {
|
|
I int32
|
|
}
|
|
|
|
type U0 struct {
|
|
U uint
|
|
}
|
|
|
|
type U1 struct {
|
|
U string
|
|
}
|
|
|
|
type T struct {
|
|
T time.Time
|
|
}
|
|
|
|
type X0 struct {
|
|
S string
|
|
I int
|
|
i int
|
|
}
|
|
|
|
type X1 struct {
|
|
S myString
|
|
I int32
|
|
J int64
|
|
}
|
|
|
|
type X2 struct {
|
|
Z string
|
|
i int
|
|
}
|
|
|
|
type X3 struct {
|
|
S bool
|
|
I int
|
|
}
|
|
|
|
type Y0 struct {
|
|
B bool
|
|
F []float64
|
|
G []float64
|
|
}
|
|
|
|
type Y1 struct {
|
|
B bool
|
|
F float64
|
|
}
|
|
|
|
type Y2 struct {
|
|
B bool
|
|
F []int64
|
|
}
|
|
|
|
type Pointers struct {
|
|
Pi *int
|
|
Ps *string
|
|
Pb *bool
|
|
Pf *float64
|
|
Pg *GeoPoint
|
|
Pt *time.Time
|
|
}
|
|
|
|
type PointersOmitEmpty struct {
|
|
Pi *int `datastore:",omitempty"`
|
|
Ps *string `datastore:",omitempty"`
|
|
Pb *bool `datastore:",omitempty"`
|
|
Pf *float64 `datastore:",omitempty"`
|
|
Pg *GeoPoint `datastore:",omitempty"`
|
|
Pt *time.Time `datastore:",omitempty"`
|
|
}
|
|
|
|
func populatedPointers() *Pointers {
|
|
var (
|
|
i int
|
|
s string
|
|
b bool
|
|
f float64
|
|
g GeoPoint
|
|
t time.Time
|
|
)
|
|
return &Pointers{
|
|
Pi: &i,
|
|
Ps: &s,
|
|
Pb: &b,
|
|
Pf: &f,
|
|
Pg: &g,
|
|
Pt: &t,
|
|
}
|
|
}
|
|
|
|
type Tagged struct {
|
|
A int `datastore:"a,noindex"`
|
|
B []int `datastore:"b"`
|
|
C int `datastore:",noindex"`
|
|
D int `datastore:""`
|
|
E int
|
|
I int `datastore:"-"`
|
|
J int `datastore:",noindex" json:"j"`
|
|
|
|
Y0 `datastore:"-"`
|
|
Z chan int `datastore:"-"`
|
|
}
|
|
|
|
type InvalidTagged1 struct {
|
|
I int `datastore:"\t"`
|
|
}
|
|
|
|
type InvalidTagged2 struct {
|
|
I int
|
|
J int `datastore:"I"`
|
|
}
|
|
|
|
type InvalidTagged3 struct {
|
|
X string `datastore:"-,noindex"`
|
|
}
|
|
|
|
type InvalidTagged4 struct {
|
|
X string `datastore:",garbage"`
|
|
}
|
|
|
|
type Inner1 struct {
|
|
W int32
|
|
X string
|
|
}
|
|
|
|
type Inner2 struct {
|
|
Y float64
|
|
}
|
|
|
|
type Inner3 struct {
|
|
Z bool
|
|
}
|
|
|
|
type Inner5 struct {
|
|
WW int
|
|
}
|
|
|
|
type Inner4 struct {
|
|
X Inner5
|
|
}
|
|
|
|
type Outer struct {
|
|
A int16
|
|
I []Inner1
|
|
J Inner2
|
|
Inner3
|
|
}
|
|
|
|
type OuterFlatten struct {
|
|
A int16
|
|
I []Inner1 `datastore:",flatten"`
|
|
J Inner2 `datastore:",flatten,noindex"`
|
|
Inner3 `datastore:",flatten"`
|
|
K Inner4 `datastore:",flatten"`
|
|
}
|
|
|
|
type OuterEquivalent struct {
|
|
A int16
|
|
IDotW []int32 `datastore:"I.W"`
|
|
IDotX []string `datastore:"I.X"`
|
|
JDotY float64 `datastore:"J.Y"`
|
|
Z bool
|
|
}
|
|
|
|
type Dotted struct {
|
|
A DottedA `datastore:"A0.A1.A2"`
|
|
}
|
|
|
|
type DottedA struct {
|
|
B DottedB `datastore:"B3"`
|
|
}
|
|
|
|
type DottedB struct {
|
|
C int `datastore:"C4.C5"`
|
|
}
|
|
|
|
type SliceOfSlices struct {
|
|
I int
|
|
S []struct {
|
|
J int
|
|
F []float64
|
|
} `datastore:",flatten"`
|
|
}
|
|
|
|
type Recursive struct {
|
|
I int
|
|
R []Recursive
|
|
}
|
|
|
|
type MutuallyRecursive0 struct {
|
|
I int
|
|
R []MutuallyRecursive1
|
|
}
|
|
|
|
type MutuallyRecursive1 struct {
|
|
I int
|
|
R []MutuallyRecursive0
|
|
}
|
|
|
|
type EntityWithKey struct {
|
|
I int
|
|
S string
|
|
K *Key `datastore:"__key__"`
|
|
}
|
|
|
|
type EntityWithKey2 EntityWithKey
|
|
|
|
type WithNestedEntityWithKey struct {
|
|
N EntityWithKey
|
|
}
|
|
|
|
type WithNonKeyField struct {
|
|
I int
|
|
K string `datastore:"__key__"`
|
|
}
|
|
|
|
type NestedWithNonKeyField struct {
|
|
N WithNonKeyField
|
|
}
|
|
|
|
type Basic struct {
|
|
A string
|
|
}
|
|
|
|
type PtrToStructField struct {
|
|
B *Basic
|
|
C *Basic `datastore:"c,noindex"`
|
|
*Basic
|
|
D []*Basic
|
|
}
|
|
|
|
var two int = 2
|
|
|
|
type EmbeddedTime struct {
|
|
time.Time
|
|
}
|
|
|
|
type SpecialTime struct {
|
|
MyTime EmbeddedTime
|
|
}
|
|
|
|
type Doubler struct {
|
|
S string
|
|
I int64
|
|
B bool
|
|
}
|
|
|
|
type Repeat struct {
|
|
Key string
|
|
Value []byte
|
|
}
|
|
|
|
type Repeated struct {
|
|
Repeats []Repeat
|
|
}
|
|
|
|
func (d *Doubler) Load(props []Property) error {
|
|
return LoadStruct(d, props)
|
|
}
|
|
|
|
func (d *Doubler) Save() ([]Property, error) {
|
|
// Save the default Property slice to an in-memory buffer (a PropertyList).
|
|
props, err := SaveStruct(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var list PropertyList
|
|
if err := list.Load(props); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Edit that PropertyList, and send it on.
|
|
for i := range list {
|
|
switch v := list[i].Value.(type) {
|
|
case string:
|
|
// + means string concatenation.
|
|
list[i].Value = v + v
|
|
case int64:
|
|
// + means integer addition.
|
|
list[i].Value = v + v
|
|
}
|
|
}
|
|
return list.Save()
|
|
}
|
|
|
|
var _ PropertyLoadSaver = (*Doubler)(nil)
|
|
|
|
type Deriver struct {
|
|
S, Derived, Ignored string
|
|
}
|
|
|
|
func (e *Deriver) Load(props []Property) error {
|
|
for _, p := range props {
|
|
if p.Name != "S" {
|
|
continue
|
|
}
|
|
e.S = p.Value.(string)
|
|
e.Derived = "derived+" + e.S
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Deriver) Save() ([]Property, error) {
|
|
return []Property{
|
|
{
|
|
Name: "S",
|
|
Value: e.S,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
var _ PropertyLoadSaver = (*Deriver)(nil)
|
|
|
|
type BadMultiPropEntity struct{}
|
|
|
|
func (e *BadMultiPropEntity) Load(props []Property) error {
|
|
return errors.New("unimplemented")
|
|
}
|
|
|
|
func (e *BadMultiPropEntity) Save() ([]Property, error) {
|
|
// Write multiple properties with the same name "I".
|
|
var props []Property
|
|
for i := 0; i < 3; i++ {
|
|
props = append(props, Property{
|
|
Name: "I",
|
|
Value: int64(i),
|
|
})
|
|
}
|
|
return props, nil
|
|
}
|
|
|
|
var _ PropertyLoadSaver = (*BadMultiPropEntity)(nil)
|
|
|
|
type testCase struct {
|
|
desc string
|
|
src interface{}
|
|
want interface{}
|
|
putErr string
|
|
getErr string
|
|
}
|
|
|
|
var testCases = []testCase{
|
|
{
|
|
"chan save fails",
|
|
&C0{I: -1},
|
|
&E{},
|
|
"unsupported struct field",
|
|
"",
|
|
},
|
|
{
|
|
"*chan save fails",
|
|
&C1{I: -1},
|
|
&E{},
|
|
"unsupported struct field",
|
|
"",
|
|
},
|
|
{
|
|
"[]chan save fails",
|
|
&C2{I: -1, C: make([]chan int, 8)},
|
|
&E{},
|
|
"unsupported struct field",
|
|
"",
|
|
},
|
|
{
|
|
"chan load fails",
|
|
&C3{C: "not a chan"},
|
|
&C0{},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"*chan load fails",
|
|
&C3{C: "not a *chan"},
|
|
&C1{},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"[]chan load fails",
|
|
&C3{C: "not a []chan"},
|
|
&C2{},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"empty struct",
|
|
&E{},
|
|
&E{},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"geopoint",
|
|
&G0{G: testGeoPt0},
|
|
&G0{G: testGeoPt0},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"geopoint invalid",
|
|
&G0{G: testBadGeoPt},
|
|
&G0{},
|
|
"invalid GeoPoint value",
|
|
"",
|
|
},
|
|
{
|
|
"geopoint as props",
|
|
&G0{G: testGeoPt0},
|
|
&PropertyList{
|
|
Property{Name: "G", Value: testGeoPt0, NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"geopoint slice",
|
|
&G1{G: []GeoPoint{testGeoPt0, testGeoPt1}},
|
|
&G1{G: []GeoPoint{testGeoPt0, testGeoPt1}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"omit empty, all",
|
|
&OmitAll{},
|
|
new(PropertyList),
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"omit empty",
|
|
&Omit{},
|
|
&PropertyList{
|
|
Property{Name: "St", Value: "", NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"omit empty, fields populated",
|
|
&Omit{
|
|
A: "a",
|
|
B: 10,
|
|
C: true,
|
|
F: []int{11},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "A", Value: "a", NoIndex: false},
|
|
Property{Name: "Bb", Value: int64(10), NoIndex: false},
|
|
Property{Name: "C", Value: true, NoIndex: true},
|
|
Property{Name: "F", Value: []interface{}{int64(11)}, NoIndex: false},
|
|
Property{Name: "St", Value: "", NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"omit empty, fields populated",
|
|
&Omit{
|
|
A: "a",
|
|
B: 10,
|
|
C: true,
|
|
F: []int{11},
|
|
S: S{St: "string"},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "A", Value: "a", NoIndex: false},
|
|
Property{Name: "Bb", Value: int64(10), NoIndex: false},
|
|
Property{Name: "C", Value: true, NoIndex: true},
|
|
Property{Name: "F", Value: []interface{}{int64(11)}, NoIndex: false},
|
|
Property{Name: "St", Value: "string", NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"omit empty does not propagate",
|
|
&NoOmits{
|
|
No: []NoOmit{
|
|
NoOmit{},
|
|
},
|
|
S: S{},
|
|
Ss: S{},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "No", Value: []interface{}{
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "A", Value: "", NoIndex: false},
|
|
Property{Name: "Bb", Value: int64(0), NoIndex: false},
|
|
Property{Name: "C", Value: false, NoIndex: true},
|
|
},
|
|
},
|
|
}, NoIndex: false},
|
|
Property{Name: "Ss", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "St", Value: "", NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
Property{Name: "St", Value: "", NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"key",
|
|
&K0{K: testKey1a},
|
|
&K0{K: testKey1b},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"key with parent",
|
|
&K0{K: testKey2a},
|
|
&K0{K: testKey2b},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"nil key",
|
|
&K0{},
|
|
&K0{},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"all nil keys in slice",
|
|
&K1{[]*Key{nil, nil}},
|
|
&K1{[]*Key{nil, nil}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"some nil keys in slice",
|
|
&K1{[]*Key{testKey1a, nil, testKey2a}},
|
|
&K1{[]*Key{testKey1b, nil, testKey2b}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"overflow",
|
|
&O0{I: 1 << 48},
|
|
&O1{},
|
|
"",
|
|
"overflow",
|
|
},
|
|
{
|
|
"time",
|
|
&T{T: time.Unix(1e9, 0)},
|
|
&T{T: time.Unix(1e9, 0)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"time as props",
|
|
&T{T: time.Unix(1e9, 0)},
|
|
&PropertyList{
|
|
Property{Name: "T", Value: time.Unix(1e9, 0), NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"uint save",
|
|
&U0{U: 1},
|
|
&U0{},
|
|
"unsupported struct field",
|
|
"",
|
|
},
|
|
{
|
|
"uint load",
|
|
&U1{U: "not a uint"},
|
|
&U0{},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"zero",
|
|
&X0{},
|
|
&X0{},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"basic",
|
|
&X0{S: "one", I: 2, i: 3},
|
|
&X0{S: "one", I: 2},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save string/int load myString/int32",
|
|
&X0{S: "one", I: 2, i: 3},
|
|
&X1{S: "one", I: 2},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"missing fields",
|
|
&X0{S: "one", I: 2, i: 3},
|
|
&X2{},
|
|
"",
|
|
"no such struct field",
|
|
},
|
|
{
|
|
"save string load bool",
|
|
&X0{S: "one", I: 2, i: 3},
|
|
&X3{I: 2},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"basic slice",
|
|
&Y0{B: true, F: []float64{7, 8, 9}},
|
|
&Y0{B: true, F: []float64{7, 8, 9}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save []float64 load float64",
|
|
&Y0{B: true, F: []float64{7, 8, 9}},
|
|
&Y1{B: true},
|
|
"",
|
|
"requires a slice",
|
|
},
|
|
{
|
|
"save []float64 load []int64",
|
|
&Y0{B: true, F: []float64{7, 8, 9}},
|
|
&Y2{B: true},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"single slice is too long",
|
|
&Y0{F: make([]float64, maxIndexedProperties+1)},
|
|
&Y0{},
|
|
"too many indexed properties",
|
|
"",
|
|
},
|
|
{
|
|
"two slices are too long",
|
|
&Y0{F: make([]float64, maxIndexedProperties), G: make([]float64, maxIndexedProperties)},
|
|
&Y0{},
|
|
"too many indexed properties",
|
|
"",
|
|
},
|
|
{
|
|
"one slice and one scalar are too long",
|
|
&Y0{F: make([]float64, maxIndexedProperties), B: true},
|
|
&Y0{},
|
|
"too many indexed properties",
|
|
"",
|
|
},
|
|
{
|
|
"slice of slices of bytes",
|
|
&Repeated{
|
|
Repeats: []Repeat{
|
|
{
|
|
Key: "key 1",
|
|
Value: []byte("value 1"),
|
|
},
|
|
{
|
|
Key: "key 2",
|
|
Value: []byte("value 2"),
|
|
},
|
|
},
|
|
},
|
|
&Repeated{
|
|
Repeats: []Repeat{
|
|
{
|
|
Key: "key 1",
|
|
Value: []byte("value 1"),
|
|
},
|
|
{
|
|
Key: "key 2",
|
|
Value: []byte("value 2"),
|
|
},
|
|
},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"long blob",
|
|
&B0{B: makeUint8Slice(maxIndexedProperties + 1)},
|
|
&B0{B: makeUint8Slice(maxIndexedProperties + 1)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"long []int8 is too long",
|
|
&B1{B: makeInt8Slice(maxIndexedProperties + 1)},
|
|
&B1{},
|
|
"too many indexed properties",
|
|
"",
|
|
},
|
|
{
|
|
"short []int8",
|
|
&B1{B: makeInt8Slice(3)},
|
|
&B1{B: makeInt8Slice(3)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"long myBlob",
|
|
&B2{B: makeUint8Slice(maxIndexedProperties + 1)},
|
|
&B2{B: makeUint8Slice(maxIndexedProperties + 1)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"short myBlob",
|
|
&B2{B: makeUint8Slice(3)},
|
|
&B2{B: makeUint8Slice(3)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"long []myByte",
|
|
&B3{B: makeMyByteSlice(maxIndexedProperties + 1)},
|
|
&B3{B: makeMyByteSlice(maxIndexedProperties + 1)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"short []myByte",
|
|
&B3{B: makeMyByteSlice(3)},
|
|
&B3{B: makeMyByteSlice(3)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"slice of blobs",
|
|
&B4{B: [][]byte{
|
|
makeUint8Slice(3),
|
|
makeUint8Slice(4),
|
|
makeUint8Slice(5),
|
|
}},
|
|
&B4{B: [][]byte{
|
|
makeUint8Slice(3),
|
|
makeUint8Slice(4),
|
|
makeUint8Slice(5),
|
|
}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"[]byte must be noindex",
|
|
&PropertyList{
|
|
Property{Name: "B", Value: makeUint8Slice(1501), NoIndex: false},
|
|
},
|
|
nil,
|
|
"[]byte property too long to index",
|
|
"",
|
|
},
|
|
{
|
|
"string must be noindex",
|
|
&PropertyList{
|
|
Property{Name: "B", Value: strings.Repeat("x", 1501), NoIndex: false},
|
|
},
|
|
nil,
|
|
"string property too long to index",
|
|
"",
|
|
},
|
|
{
|
|
"slice of []byte must be noindex",
|
|
&PropertyList{
|
|
Property{Name: "B", Value: []interface{}{
|
|
[]byte("short"),
|
|
makeUint8Slice(1501),
|
|
}, NoIndex: false},
|
|
},
|
|
nil,
|
|
"[]byte property too long to index",
|
|
"",
|
|
},
|
|
{
|
|
"slice of string must be noindex",
|
|
&PropertyList{
|
|
Property{Name: "B", Value: []interface{}{
|
|
"short",
|
|
strings.Repeat("x", 1501),
|
|
}, NoIndex: false},
|
|
},
|
|
nil,
|
|
"string property too long to index",
|
|
"",
|
|
},
|
|
{
|
|
"save tagged load props",
|
|
&Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, I: 6, J: 7},
|
|
&PropertyList{
|
|
// A and B are renamed to a and b; A and C are noindex, I is ignored.
|
|
// Order is sorted as per byName.
|
|
Property{Name: "C", Value: int64(3), NoIndex: true},
|
|
Property{Name: "D", Value: int64(4), NoIndex: false},
|
|
Property{Name: "E", Value: int64(5), NoIndex: false},
|
|
Property{Name: "J", Value: int64(7), NoIndex: true},
|
|
Property{Name: "a", Value: int64(1), NoIndex: true},
|
|
Property{Name: "b", Value: []interface{}{int64(21), int64(22), int64(23)}, NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save tagged load tagged",
|
|
&Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, I: 6, J: 7},
|
|
&Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, J: 7},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"invalid tagged1",
|
|
&InvalidTagged1{I: 1},
|
|
&InvalidTagged1{},
|
|
"struct tag has invalid property name",
|
|
"",
|
|
},
|
|
{
|
|
"invalid tagged2",
|
|
&InvalidTagged2{I: 1, J: 2},
|
|
&InvalidTagged2{J: 2},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"invalid tagged3",
|
|
&InvalidTagged3{X: "hello"},
|
|
&InvalidTagged3{},
|
|
"struct tag has invalid property name: \"-\"",
|
|
"",
|
|
},
|
|
{
|
|
"invalid tagged4",
|
|
&InvalidTagged4{X: "hello"},
|
|
&InvalidTagged4{},
|
|
"struct tag has invalid option: \"garbage\"",
|
|
"",
|
|
},
|
|
{
|
|
"doubler",
|
|
&Doubler{S: "s", I: 1, B: true},
|
|
&Doubler{S: "ss", I: 2, B: true},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save struct load props",
|
|
&X0{S: "s", I: 1},
|
|
&PropertyList{
|
|
Property{Name: "I", Value: int64(1), NoIndex: false},
|
|
Property{Name: "S", Value: "s", NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save props load struct",
|
|
&PropertyList{
|
|
Property{Name: "I", Value: int64(1), NoIndex: false},
|
|
Property{Name: "S", Value: "s", NoIndex: false},
|
|
},
|
|
&X0{S: "s", I: 1},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"nil-value props",
|
|
&PropertyList{
|
|
Property{Name: "I", Value: nil, NoIndex: false},
|
|
Property{Name: "B", Value: nil, NoIndex: false},
|
|
Property{Name: "S", Value: nil, NoIndex: false},
|
|
Property{Name: "F", Value: nil, NoIndex: false},
|
|
Property{Name: "K", Value: nil, NoIndex: false},
|
|
Property{Name: "T", Value: nil, NoIndex: false},
|
|
Property{Name: "J", Value: []interface{}{nil, int64(7), nil}, NoIndex: false},
|
|
},
|
|
&struct {
|
|
I int64
|
|
B bool
|
|
S string
|
|
F float64
|
|
K *Key
|
|
T time.Time
|
|
J []int64
|
|
}{
|
|
J: []int64{0, 7, 0},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save outer load props flatten",
|
|
&OuterFlatten{
|
|
A: 1,
|
|
I: []Inner1{
|
|
{10, "ten"},
|
|
{20, "twenty"},
|
|
{30, "thirty"},
|
|
},
|
|
J: Inner2{
|
|
Y: 3.14,
|
|
},
|
|
Inner3: Inner3{
|
|
Z: true,
|
|
},
|
|
K: Inner4{
|
|
X: Inner5{
|
|
WW: 12,
|
|
},
|
|
},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "A", Value: int64(1), NoIndex: false},
|
|
Property{Name: "I.W", Value: []interface{}{int64(10), int64(20), int64(30)}, NoIndex: false},
|
|
Property{Name: "I.X", Value: []interface{}{"ten", "twenty", "thirty"}, NoIndex: false},
|
|
Property{Name: "J.Y", Value: float64(3.14), NoIndex: true},
|
|
Property{Name: "K.X.WW", Value: int64(12), NoIndex: false},
|
|
Property{Name: "Z", Value: true, NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"load outer props flatten",
|
|
&PropertyList{
|
|
Property{Name: "A", Value: int64(1), NoIndex: false},
|
|
Property{Name: "I.W", Value: []interface{}{int64(10), int64(20), int64(30)}, NoIndex: false},
|
|
Property{Name: "I.X", Value: []interface{}{"ten", "twenty", "thirty"}, NoIndex: false},
|
|
Property{Name: "J.Y", Value: float64(3.14), NoIndex: true},
|
|
Property{Name: "Z", Value: true, NoIndex: false},
|
|
},
|
|
&OuterFlatten{
|
|
A: 1,
|
|
I: []Inner1{
|
|
{10, "ten"},
|
|
{20, "twenty"},
|
|
{30, "thirty"},
|
|
},
|
|
J: Inner2{
|
|
Y: 3.14,
|
|
},
|
|
Inner3: Inner3{
|
|
Z: true,
|
|
},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save outer load props",
|
|
&Outer{
|
|
A: 1,
|
|
I: []Inner1{
|
|
{10, "ten"},
|
|
{20, "twenty"},
|
|
{30, "thirty"},
|
|
},
|
|
J: Inner2{
|
|
Y: 3.14,
|
|
},
|
|
Inner3: Inner3{
|
|
Z: true,
|
|
},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "A", Value: int64(1), NoIndex: false},
|
|
Property{Name: "I", Value: []interface{}{
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "W", Value: int64(10), NoIndex: false},
|
|
Property{Name: "X", Value: "ten", NoIndex: false},
|
|
},
|
|
},
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "W", Value: int64(20), NoIndex: false},
|
|
Property{Name: "X", Value: "twenty", NoIndex: false},
|
|
},
|
|
},
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "W", Value: int64(30), NoIndex: false},
|
|
Property{Name: "X", Value: "thirty", NoIndex: false},
|
|
},
|
|
},
|
|
}, NoIndex: false},
|
|
Property{Name: "J", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "Y", Value: float64(3.14), NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
Property{Name: "Z", Value: true, NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save props load outer-equivalent",
|
|
&PropertyList{
|
|
Property{Name: "A", Value: int64(1), NoIndex: false},
|
|
Property{Name: "I.W", Value: []interface{}{int64(10), int64(20), int64(30)}, NoIndex: false},
|
|
Property{Name: "I.X", Value: []interface{}{"ten", "twenty", "thirty"}, NoIndex: false},
|
|
Property{Name: "J.Y", Value: float64(3.14), NoIndex: false},
|
|
Property{Name: "Z", Value: true, NoIndex: false},
|
|
},
|
|
&OuterEquivalent{
|
|
A: 1,
|
|
IDotW: []int32{10, 20, 30},
|
|
IDotX: []string{"ten", "twenty", "thirty"},
|
|
JDotY: 3.14,
|
|
Z: true,
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"dotted names save",
|
|
&Dotted{A: DottedA{B: DottedB{C: 88}}},
|
|
&PropertyList{
|
|
Property{Name: "A0.A1.A2", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "B3", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "C4.C5", Value: int64(88), NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"dotted names load",
|
|
&PropertyList{
|
|
Property{Name: "A0.A1.A2", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "B3", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "C4.C5", Value: 99, NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
},
|
|
&Dotted{A: DottedA{B: DottedB{C: 99}}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save struct load deriver",
|
|
&X0{S: "s", I: 1},
|
|
&Deriver{S: "s", Derived: "derived+s"},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save deriver load struct",
|
|
&Deriver{S: "s", Derived: "derived+s", Ignored: "ignored"},
|
|
&X0{S: "s"},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"zero time.Time",
|
|
&T{T: time.Time{}},
|
|
&T{T: time.Time{}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"time.Time near Unix zero time",
|
|
&T{T: time.Unix(0, 4e3)},
|
|
&T{T: time.Unix(0, 4e3)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"time.Time, far in the future",
|
|
&T{T: time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
&T{T: time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"time.Time, very far in the past",
|
|
&T{T: time.Date(-300000, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
&T{},
|
|
"time value out of range",
|
|
"",
|
|
},
|
|
{
|
|
"time.Time, very far in the future",
|
|
&T{T: time.Date(294248, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
&T{},
|
|
"time value out of range",
|
|
"",
|
|
},
|
|
{
|
|
"structs",
|
|
&N0{
|
|
X0: X0{S: "one", I: 2, i: 3},
|
|
Nonymous: X0{S: "four", I: 5, i: 6},
|
|
Ignore: "ignore",
|
|
Other: "other",
|
|
},
|
|
&N0{
|
|
X0: X0{S: "one", I: 2},
|
|
Nonymous: X0{S: "four", I: 5},
|
|
Other: "other",
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"slice of structs",
|
|
&N1{
|
|
X0: X0{S: "one", I: 2, i: 3},
|
|
Nonymous: []X0{
|
|
{S: "four", I: 5, i: 6},
|
|
{S: "seven", I: 8, i: 9},
|
|
{S: "ten", I: 11, i: 12},
|
|
{S: "thirteen", I: 14, i: 15},
|
|
},
|
|
Ignore: "ignore",
|
|
Other: "other",
|
|
},
|
|
&N1{
|
|
X0: X0{S: "one", I: 2},
|
|
Nonymous: []X0{
|
|
{S: "four", I: 5},
|
|
{S: "seven", I: 8},
|
|
{S: "ten", I: 11},
|
|
{S: "thirteen", I: 14},
|
|
},
|
|
Other: "other",
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"structs with slices of structs",
|
|
&N2{
|
|
N1: N1{
|
|
X0: X0{S: "rouge"},
|
|
Nonymous: []X0{
|
|
{S: "rosso0"},
|
|
{S: "rosso1"},
|
|
},
|
|
},
|
|
Green: N1{
|
|
X0: X0{S: "vert"},
|
|
Nonymous: []X0{
|
|
{S: "verde0"},
|
|
{S: "verde1"},
|
|
{S: "verde2"},
|
|
},
|
|
},
|
|
Blue: N1{
|
|
X0: X0{S: "bleu"},
|
|
Nonymous: []X0{
|
|
{S: "blu0"},
|
|
{S: "blu1"},
|
|
{S: "blu2"},
|
|
{S: "blu3"},
|
|
},
|
|
},
|
|
},
|
|
&N2{
|
|
N1: N1{
|
|
X0: X0{S: "rouge"},
|
|
Nonymous: []X0{
|
|
{S: "rosso0"},
|
|
{S: "rosso1"},
|
|
},
|
|
},
|
|
Green: N1{
|
|
X0: X0{S: "vert"},
|
|
Nonymous: []X0{
|
|
{S: "verde0"},
|
|
{S: "verde1"},
|
|
{S: "verde2"},
|
|
},
|
|
},
|
|
Blue: N1{
|
|
X0: X0{S: "bleu"},
|
|
Nonymous: []X0{
|
|
{S: "blu0"},
|
|
{S: "blu1"},
|
|
{S: "blu2"},
|
|
{S: "blu3"},
|
|
},
|
|
},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save structs load props",
|
|
&N2{
|
|
N1: N1{
|
|
X0: X0{S: "rouge"},
|
|
Nonymous: []X0{
|
|
{S: "rosso0"},
|
|
{S: "rosso1"},
|
|
},
|
|
},
|
|
Green: N1{
|
|
X0: X0{S: "vert"},
|
|
Nonymous: []X0{
|
|
{S: "verde0"},
|
|
{S: "verde1"},
|
|
{S: "verde2"},
|
|
},
|
|
},
|
|
Blue: N1{
|
|
X0: X0{S: "bleu"},
|
|
Nonymous: []X0{
|
|
{S: "blu0"},
|
|
{S: "blu1"},
|
|
{S: "blu2"},
|
|
{S: "blu3"},
|
|
},
|
|
},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "Blue", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "Nonymous", Value: []interface{}{
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "S", Value: "blu0", NoIndex: false},
|
|
},
|
|
},
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "S", Value: "blu1", NoIndex: false},
|
|
},
|
|
},
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "S", Value: "blu2", NoIndex: false},
|
|
},
|
|
},
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "S", Value: "blu3", NoIndex: false},
|
|
},
|
|
},
|
|
}, NoIndex: false},
|
|
Property{Name: "Other", Value: "", NoIndex: false},
|
|
Property{Name: "S", Value: "bleu", NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
Property{Name: "green", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "Nonymous", Value: []interface{}{
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "S", Value: "verde0", NoIndex: false},
|
|
},
|
|
},
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "S", Value: "verde1", NoIndex: false},
|
|
},
|
|
},
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "S", Value: "verde2", NoIndex: false},
|
|
},
|
|
},
|
|
}, NoIndex: false},
|
|
Property{Name: "Other", Value: "", NoIndex: false},
|
|
Property{Name: "S", Value: "vert", NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
Property{Name: "red", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "Nonymous", Value: []interface{}{
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "S", Value: "rosso0", NoIndex: false},
|
|
},
|
|
},
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(0), NoIndex: false},
|
|
Property{Name: "S", Value: "rosso1", NoIndex: false},
|
|
},
|
|
},
|
|
}, NoIndex: false},
|
|
Property{Name: "Other", Value: "", NoIndex: false},
|
|
Property{Name: "S", Value: "rouge", NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"nested entity with key",
|
|
&WithNestedEntityWithKey{
|
|
N: EntityWithKey{
|
|
I: 12,
|
|
S: "abcd",
|
|
K: testKey0,
|
|
},
|
|
},
|
|
&WithNestedEntityWithKey{
|
|
N: EntityWithKey{
|
|
I: 12,
|
|
S: "abcd",
|
|
K: testKey0,
|
|
},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"entity with key at top level",
|
|
&EntityWithKey{
|
|
I: 12,
|
|
S: "abc",
|
|
K: testKey0,
|
|
},
|
|
&EntityWithKey{
|
|
I: 12,
|
|
S: "abc",
|
|
K: testKey0,
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"entity with key at top level (key is populated on load)",
|
|
&EntityWithKey{
|
|
I: 12,
|
|
S: "abc",
|
|
},
|
|
&EntityWithKey{
|
|
I: 12,
|
|
S: "abc",
|
|
K: testKey0,
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"__key__ field not a *Key",
|
|
&NestedWithNonKeyField{
|
|
N: WithNonKeyField{
|
|
I: 12,
|
|
K: "abcd",
|
|
},
|
|
},
|
|
&NestedWithNonKeyField{
|
|
N: WithNonKeyField{
|
|
I: 12,
|
|
K: "abcd",
|
|
},
|
|
},
|
|
"datastore: __key__ field on struct datastore.WithNonKeyField is not a *datastore.Key",
|
|
"",
|
|
},
|
|
{
|
|
"save struct with ptr to struct fields",
|
|
&PtrToStructField{
|
|
&Basic{
|
|
A: "b",
|
|
},
|
|
&Basic{
|
|
A: "c",
|
|
},
|
|
&Basic{
|
|
A: "anon",
|
|
},
|
|
[]*Basic{
|
|
&Basic{
|
|
A: "slice0",
|
|
},
|
|
&Basic{
|
|
A: "slice1",
|
|
},
|
|
},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "A", Value: "anon", NoIndex: false},
|
|
Property{Name: "B", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "A", Value: "b", NoIndex: false},
|
|
},
|
|
}},
|
|
Property{Name: "D", Value: []interface{}{
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "A", Value: "slice0", NoIndex: false},
|
|
},
|
|
},
|
|
&Entity{
|
|
Properties: []Property{
|
|
Property{Name: "A", Value: "slice1", NoIndex: false},
|
|
},
|
|
},
|
|
}, NoIndex: false},
|
|
Property{Name: "c", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "A", Value: "c", NoIndex: true},
|
|
},
|
|
}, NoIndex: true},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save and load struct with ptr to struct fields",
|
|
&PtrToStructField{
|
|
&Basic{
|
|
A: "b",
|
|
},
|
|
&Basic{
|
|
A: "c",
|
|
},
|
|
&Basic{
|
|
A: "anon",
|
|
},
|
|
[]*Basic{
|
|
&Basic{
|
|
A: "slice0",
|
|
},
|
|
&Basic{
|
|
A: "slice1",
|
|
},
|
|
},
|
|
},
|
|
&PtrToStructField{
|
|
&Basic{
|
|
A: "b",
|
|
},
|
|
&Basic{
|
|
A: "c",
|
|
},
|
|
&Basic{
|
|
A: "anon",
|
|
},
|
|
[]*Basic{
|
|
&Basic{
|
|
A: "slice0",
|
|
},
|
|
&Basic{
|
|
A: "slice1",
|
|
},
|
|
},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"struct with nil ptr to struct fields",
|
|
&PtrToStructField{
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
},
|
|
new(PropertyList),
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"nested load entity with key",
|
|
&WithNestedEntityWithKey{
|
|
N: EntityWithKey{
|
|
I: 12,
|
|
S: "abcd",
|
|
K: testKey0,
|
|
},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "N", Value: &Entity{
|
|
Key: testKey0,
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(12), NoIndex: false},
|
|
Property{Name: "S", Value: "abcd", NoIndex: false},
|
|
},
|
|
},
|
|
NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"nested save entity with key",
|
|
&PropertyList{
|
|
Property{Name: "N", Value: &Entity{
|
|
Key: testKey0,
|
|
Properties: []Property{
|
|
Property{Name: "I", Value: int64(12), NoIndex: false},
|
|
Property{Name: "S", Value: "abcd", NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
},
|
|
|
|
&WithNestedEntityWithKey{
|
|
N: EntityWithKey{
|
|
I: 12,
|
|
S: "abcd",
|
|
K: testKey0,
|
|
},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"anonymous field with tag",
|
|
&N3{
|
|
C3: C3{C: "s"},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "red", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "C", Value: "s", NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"unexported anonymous field",
|
|
&N4{
|
|
c4: c4{C: "s"},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "C", Value: "s", NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"unexported anonymous field with tag",
|
|
&N5{
|
|
c4: c4{C: "s"},
|
|
},
|
|
new(PropertyList),
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save props load structs with ragged fields",
|
|
&PropertyList{
|
|
Property{Name: "red.S", Value: "rot", NoIndex: false},
|
|
Property{Name: "green.Nonymous.I", Value: []interface{}{int64(10), int64(11), int64(12), int64(13)}, NoIndex: false},
|
|
Property{Name: "Blue.Nonymous.I", Value: []interface{}{int64(20), int64(21)}, NoIndex: false},
|
|
Property{Name: "Blue.Nonymous.S", Value: []interface{}{"blau0", "blau1", "blau2"}, NoIndex: false},
|
|
},
|
|
&N2{
|
|
N1: N1{
|
|
X0: X0{S: "rot"},
|
|
},
|
|
Green: N1{
|
|
Nonymous: []X0{
|
|
{I: 10},
|
|
{I: 11},
|
|
{I: 12},
|
|
{I: 13},
|
|
},
|
|
},
|
|
Blue: N1{
|
|
Nonymous: []X0{
|
|
{S: "blau0", I: 20},
|
|
{S: "blau1", I: 21},
|
|
{S: "blau2"},
|
|
},
|
|
},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save structs with noindex tags",
|
|
&struct {
|
|
A struct {
|
|
X string `datastore:",noindex"`
|
|
Y string
|
|
} `datastore:",noindex"`
|
|
B struct {
|
|
X string `datastore:",noindex"`
|
|
Y string
|
|
}
|
|
}{},
|
|
&PropertyList{
|
|
Property{Name: "A", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "X", Value: "", NoIndex: true},
|
|
Property{Name: "Y", Value: "", NoIndex: true},
|
|
},
|
|
}, NoIndex: true},
|
|
Property{Name: "B", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "X", Value: "", NoIndex: true},
|
|
Property{Name: "Y", Value: "", NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"embedded struct with name override",
|
|
&struct {
|
|
Inner1 `datastore:"foo"`
|
|
}{},
|
|
&PropertyList{
|
|
Property{Name: "foo", Value: &Entity{
|
|
Properties: []Property{
|
|
Property{Name: "W", Value: int64(0), NoIndex: false},
|
|
Property{Name: "X", Value: "", NoIndex: false},
|
|
},
|
|
}, NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"slice of slices",
|
|
&SliceOfSlices{},
|
|
nil,
|
|
"flattening nested structs leads to a slice of slices",
|
|
"",
|
|
},
|
|
{
|
|
"recursive struct",
|
|
&Recursive{},
|
|
&Recursive{},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"mutually recursive struct",
|
|
&MutuallyRecursive0{},
|
|
&MutuallyRecursive0{},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"non-exported struct fields",
|
|
&struct {
|
|
i, J int64
|
|
}{i: 1, J: 2},
|
|
&PropertyList{
|
|
Property{Name: "J", Value: int64(2), NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"json.RawMessage",
|
|
&struct {
|
|
J json.RawMessage
|
|
}{
|
|
J: json.RawMessage("rawr"),
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "J", Value: []byte("rawr"), NoIndex: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"json.RawMessage to myBlob",
|
|
&struct {
|
|
B json.RawMessage
|
|
}{
|
|
B: json.RawMessage("rawr"),
|
|
},
|
|
&B2{B: myBlob("rawr")},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"repeated property names",
|
|
&PropertyList{
|
|
Property{Name: "A", Value: ""},
|
|
Property{Name: "A", Value: ""},
|
|
},
|
|
nil,
|
|
"duplicate Property",
|
|
"",
|
|
},
|
|
{
|
|
"embedded time field",
|
|
&SpecialTime{MyTime: EmbeddedTime{ts}},
|
|
&SpecialTime{MyTime: EmbeddedTime{ts}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"embedded time load",
|
|
&PropertyList{
|
|
Property{Name: "MyTime.Time", Value: ts},
|
|
},
|
|
&SpecialTime{MyTime: EmbeddedTime{ts}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"pointer fields: nil",
|
|
&Pointers{},
|
|
&Pointers{},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"pointer fields: populated with zeroes",
|
|
populatedPointers(),
|
|
populatedPointers(),
|
|
"",
|
|
"",
|
|
},
|
|
}
|
|
|
|
// checkErr returns the empty string if either both want and err are zero,
|
|
// or if want is a non-empty substring of err's string representation.
|
|
func checkErr(want string, err error) string {
|
|
if err != nil {
|
|
got := err.Error()
|
|
if want == "" || strings.Index(got, want) == -1 {
|
|
return got
|
|
}
|
|
} else if want != "" {
|
|
return fmt.Sprintf("want error %q", want)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func TestRoundTrip(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
p, err := saveEntity(testKey0, tc.src)
|
|
if s := checkErr(tc.putErr, err); s != "" {
|
|
t.Errorf("%s: save: %s", tc.desc, s)
|
|
continue
|
|
}
|
|
if p == nil {
|
|
continue
|
|
}
|
|
var got interface{}
|
|
if _, ok := tc.want.(*PropertyList); ok {
|
|
got = new(PropertyList)
|
|
} else {
|
|
got = reflect.New(reflect.TypeOf(tc.want).Elem()).Interface()
|
|
}
|
|
err = loadEntityProto(got, p)
|
|
if s := checkErr(tc.getErr, err); s != "" {
|
|
t.Errorf("%s: load: %s", tc.desc, s)
|
|
continue
|
|
}
|
|
if pl, ok := got.(*PropertyList); ok {
|
|
// Sort by name to make sure we have a deterministic order.
|
|
sortPL(*pl)
|
|
}
|
|
|
|
if !testutil.Equal(got, tc.want, cmp.AllowUnexported(X0{}, X2{})) {
|
|
t.Errorf("%s: compare:\ngot: %+#v\nwant: %+#v", tc.desc, got, tc.want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
type aPtrPLS struct {
|
|
Count int
|
|
}
|
|
|
|
func (pls *aPtrPLS) Load([]Property) error {
|
|
pls.Count += 1
|
|
return nil
|
|
}
|
|
|
|
func (pls *aPtrPLS) Save() ([]Property, error) {
|
|
return []Property{{Name: "Count", Value: 4}}, nil
|
|
}
|
|
|
|
type aValuePLS struct {
|
|
Count int
|
|
}
|
|
|
|
func (pls aValuePLS) Load([]Property) error {
|
|
pls.Count += 2
|
|
return nil
|
|
}
|
|
|
|
func (pls aValuePLS) Save() ([]Property, error) {
|
|
return []Property{{Name: "Count", Value: 8}}, nil
|
|
}
|
|
|
|
type aValuePtrPLS struct {
|
|
Count int
|
|
}
|
|
|
|
func (pls *aValuePtrPLS) Load([]Property) error {
|
|
pls.Count = 11
|
|
return nil
|
|
}
|
|
|
|
func (pls *aValuePtrPLS) Save() ([]Property, error) {
|
|
return []Property{{Name: "Count", Value: 12}}, nil
|
|
}
|
|
|
|
type aNotPLS struct {
|
|
Count int
|
|
}
|
|
|
|
type plsString string
|
|
|
|
func (s *plsString) Load([]Property) error {
|
|
*s = "LOADED"
|
|
return nil
|
|
}
|
|
|
|
func (s *plsString) Save() ([]Property, error) {
|
|
return []Property{{Name: "SS", Value: "SAVED"}}, nil
|
|
}
|
|
|
|
func ptrToplsString(s string) *plsString {
|
|
plsStr := plsString(s)
|
|
return &plsStr
|
|
}
|
|
|
|
type aSubPLS struct {
|
|
Foo string
|
|
Bar *aPtrPLS
|
|
Baz aValuePtrPLS
|
|
S plsString
|
|
}
|
|
|
|
type aSubNotPLS struct {
|
|
Foo string
|
|
Bar *aNotPLS
|
|
}
|
|
|
|
type aSubPLSErr struct {
|
|
Foo string
|
|
Bar aValuePLS
|
|
}
|
|
|
|
type aSubPLSNoErr struct {
|
|
Foo string
|
|
Bar aPtrPLS
|
|
}
|
|
|
|
type GrandparentFlatten struct {
|
|
Parent Parent `datastore:",flatten"`
|
|
}
|
|
|
|
type GrandparentOfPtrFlatten struct {
|
|
Parent ParentOfPtr `datastore:",flatten"`
|
|
}
|
|
|
|
type GrandparentOfSlice struct {
|
|
Parent ParentOfSlice
|
|
}
|
|
|
|
type GrandparentOfSlicePtrs struct {
|
|
Parent ParentOfSlicePtrs
|
|
}
|
|
|
|
type GrandparentOfSliceFlatten struct {
|
|
Parent ParentOfSlice `datastore:",flatten"`
|
|
}
|
|
|
|
type GrandparentOfSlicePtrsFlatten struct {
|
|
Parent ParentOfSlicePtrs `datastore:",flatten"`
|
|
}
|
|
|
|
type Grandparent struct {
|
|
Parent Parent
|
|
}
|
|
|
|
type Parent struct {
|
|
Child Child
|
|
String plsString
|
|
}
|
|
|
|
type ParentOfPtr struct {
|
|
Child *Child
|
|
String *plsString
|
|
}
|
|
|
|
type ParentOfSlice struct {
|
|
Children []Child
|
|
Strings []plsString
|
|
}
|
|
|
|
type ParentOfSlicePtrs struct {
|
|
Children []*Child
|
|
Strings []*plsString
|
|
}
|
|
|
|
type Child struct {
|
|
I int
|
|
Grandchild Grandchild
|
|
}
|
|
|
|
type Grandchild struct {
|
|
S string
|
|
}
|
|
|
|
func (c *Child) Load(props []Property) error {
|
|
for _, p := range props {
|
|
if p.Name == "I" {
|
|
c.I += 1
|
|
} else if p.Name == "Grandchild.S" {
|
|
c.Grandchild.S = "grandchild loaded"
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Child) Save() ([]Property, error) {
|
|
v := c.I + 1
|
|
return []Property{
|
|
{Name: "I", Value: v},
|
|
{Name: "Grandchild.S", Value: fmt.Sprintf("grandchild saved %d", v)},
|
|
}, nil
|
|
}
|
|
|
|
func TestLoadSavePLS(t *testing.T) {
|
|
type testCase struct {
|
|
desc string
|
|
src interface{}
|
|
wantSave *pb.Entity
|
|
wantLoad interface{}
|
|
saveErr string
|
|
loadErr string
|
|
}
|
|
|
|
testCases := []testCase{
|
|
{
|
|
desc: "non-struct implements PLS (top-level)",
|
|
src: ptrToplsString("hello"),
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
wantLoad: ptrToplsString("LOADED"),
|
|
},
|
|
{
|
|
desc: "substructs do implement PLS",
|
|
src: &aSubPLS{Foo: "foo", Bar: &aPtrPLS{Count: 2}, Baz: aValuePtrPLS{Count: 15}, S: "something"},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Foo": {ValueType: &pb.Value_StringValue{StringValue: "foo"}},
|
|
"Bar": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"Count": {ValueType: &pb.Value_IntegerValue{IntegerValue: 4}},
|
|
},
|
|
},
|
|
}},
|
|
"Baz": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"Count": {ValueType: &pb.Value_IntegerValue{IntegerValue: 12}},
|
|
},
|
|
},
|
|
}},
|
|
"S": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
wantLoad: &aSubPLS{Foo: "foo", Bar: &aPtrPLS{Count: 1}, Baz: aValuePtrPLS{Count: 11}, S: "LOADED"},
|
|
},
|
|
{
|
|
desc: "substruct (ptr) does implement PLS, nil valued substruct",
|
|
src: &aSubPLS{Foo: "foo", S: "something"},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Foo": {ValueType: &pb.Value_StringValue{StringValue: "foo"}},
|
|
"Baz": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"Count": {ValueType: &pb.Value_IntegerValue{IntegerValue: 12}},
|
|
},
|
|
},
|
|
}},
|
|
"S": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
wantLoad: &aSubPLS{Foo: "foo", Baz: aValuePtrPLS{Count: 11}, S: "LOADED"},
|
|
},
|
|
{
|
|
desc: "substruct (ptr) does not implement PLS",
|
|
src: &aSubNotPLS{Foo: "foo", Bar: &aNotPLS{Count: 2}},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Foo": {ValueType: &pb.Value_StringValue{StringValue: "foo"}},
|
|
"Bar": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"Count": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
wantLoad: &aSubNotPLS{Foo: "foo", Bar: &aNotPLS{Count: 2}},
|
|
},
|
|
{
|
|
desc: "substruct (value) does implement PLS, error on save",
|
|
src: &aSubPLSErr{Foo: "foo", Bar: aValuePLS{Count: 2}},
|
|
wantSave: (*pb.Entity)(nil),
|
|
wantLoad: &aSubPLSErr{},
|
|
saveErr: "PropertyLoadSaver methods must be implemented on a pointer",
|
|
},
|
|
{
|
|
desc: "substruct (value) does implement PLS, error on load",
|
|
src: &aSubPLSNoErr{Foo: "foo", Bar: aPtrPLS{Count: 2}},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Foo": {ValueType: &pb.Value_StringValue{StringValue: "foo"}},
|
|
"Bar": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"Count": {ValueType: &pb.Value_IntegerValue{IntegerValue: 4}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
wantLoad: &aSubPLSErr{},
|
|
loadErr: "PropertyLoadSaver methods must be implemented on a pointer",
|
|
},
|
|
|
|
{
|
|
desc: "parent does not have flatten option, child impl PLS",
|
|
src: &Grandparent{
|
|
Parent: Parent{
|
|
Child: Child{
|
|
I: 9,
|
|
Grandchild: Grandchild{
|
|
S: "BAD",
|
|
},
|
|
},
|
|
String: plsString("something"),
|
|
},
|
|
},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Parent": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"Child": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 10}},
|
|
"Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 10"}},
|
|
},
|
|
},
|
|
}},
|
|
"String": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
wantLoad: &Grandparent{
|
|
Parent: Parent{
|
|
Child: Child{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
String: "LOADED",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "parent has flatten option enabled, child impl PLS",
|
|
src: &GrandparentFlatten{
|
|
Parent: Parent{
|
|
Child: Child{
|
|
I: 7,
|
|
Grandchild: Grandchild{
|
|
S: "BAD",
|
|
},
|
|
},
|
|
String: plsString("something"),
|
|
},
|
|
},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Parent.Child.I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 8}},
|
|
"Parent.Child.Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}},
|
|
"Parent.String.SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
wantLoad: &GrandparentFlatten{
|
|
Parent: Parent{
|
|
Child: Child{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
String: "LOADED",
|
|
},
|
|
},
|
|
},
|
|
|
|
{
|
|
desc: "parent has flatten option enabled, child (ptr to) impl PLS",
|
|
src: &GrandparentOfPtrFlatten{
|
|
Parent: ParentOfPtr{
|
|
Child: &Child{
|
|
I: 7,
|
|
Grandchild: Grandchild{
|
|
S: "BAD",
|
|
},
|
|
},
|
|
String: ptrToplsString("something"),
|
|
},
|
|
},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Parent.Child.I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 8}},
|
|
"Parent.Child.Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}},
|
|
"Parent.String.SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
wantLoad: &GrandparentOfPtrFlatten{
|
|
Parent: ParentOfPtr{
|
|
Child: &Child{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
String: ptrToplsString("LOADED"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "children (slice of) impl PLS",
|
|
src: &GrandparentOfSlice{
|
|
Parent: ParentOfSlice{
|
|
Children: []Child{
|
|
{
|
|
I: 7,
|
|
Grandchild: Grandchild{
|
|
S: "BAD",
|
|
},
|
|
},
|
|
{
|
|
I: 9,
|
|
Grandchild: Grandchild{
|
|
S: "BAD2",
|
|
},
|
|
},
|
|
},
|
|
Strings: []plsString{
|
|
"something1",
|
|
"something2",
|
|
},
|
|
},
|
|
},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Parent": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"Children": {ValueType: &pb.Value_ArrayValue{
|
|
ArrayValue: &pb.ArrayValue{Values: []*pb.Value{
|
|
{ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 8}},
|
|
"Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}},
|
|
},
|
|
},
|
|
}},
|
|
{ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 10}},
|
|
"Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 10"}},
|
|
},
|
|
},
|
|
}},
|
|
}},
|
|
}},
|
|
"Strings": {ValueType: &pb.Value_ArrayValue{
|
|
ArrayValue: &pb.ArrayValue{Values: []*pb.Value{
|
|
{ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
}},
|
|
{ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
}},
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
wantLoad: &GrandparentOfSlice{
|
|
Parent: ParentOfSlice{
|
|
Children: []Child{
|
|
{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
},
|
|
Strings: []plsString{
|
|
"LOADED",
|
|
"LOADED",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "children (slice of ptrs) impl PLS",
|
|
src: &GrandparentOfSlicePtrs{
|
|
Parent: ParentOfSlicePtrs{
|
|
Children: []*Child{
|
|
{
|
|
I: 7,
|
|
Grandchild: Grandchild{
|
|
S: "BAD",
|
|
},
|
|
},
|
|
{
|
|
I: 9,
|
|
Grandchild: Grandchild{
|
|
S: "BAD2",
|
|
},
|
|
},
|
|
},
|
|
Strings: []*plsString{
|
|
ptrToplsString("something1"),
|
|
ptrToplsString("something2"),
|
|
},
|
|
},
|
|
},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Parent": {ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"Children": {ValueType: &pb.Value_ArrayValue{
|
|
ArrayValue: &pb.ArrayValue{Values: []*pb.Value{
|
|
{ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 8}},
|
|
"Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}},
|
|
},
|
|
},
|
|
}},
|
|
{ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 10}},
|
|
"Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 10"}},
|
|
},
|
|
},
|
|
}},
|
|
}},
|
|
}},
|
|
"Strings": {ValueType: &pb.Value_ArrayValue{
|
|
ArrayValue: &pb.ArrayValue{Values: []*pb.Value{
|
|
{ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
}},
|
|
{ValueType: &pb.Value_EntityValue{
|
|
EntityValue: &pb.Entity{
|
|
Properties: map[string]*pb.Value{
|
|
"SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
}},
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
wantLoad: &GrandparentOfSlicePtrs{
|
|
Parent: ParentOfSlicePtrs{
|
|
Children: []*Child{
|
|
{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
},
|
|
Strings: []*plsString{
|
|
ptrToplsString("LOADED"),
|
|
ptrToplsString("LOADED"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "parent has flatten option, children (slice of) impl PLS",
|
|
src: &GrandparentOfSliceFlatten{
|
|
Parent: ParentOfSlice{
|
|
Children: []Child{
|
|
{
|
|
I: 7,
|
|
Grandchild: Grandchild{
|
|
S: "BAD",
|
|
},
|
|
},
|
|
{
|
|
I: 9,
|
|
Grandchild: Grandchild{
|
|
S: "BAD2",
|
|
},
|
|
},
|
|
},
|
|
Strings: []plsString{
|
|
"something1",
|
|
"something2",
|
|
},
|
|
},
|
|
},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Parent.Children.I": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{
|
|
Values: []*pb.Value{
|
|
{ValueType: &pb.Value_IntegerValue{IntegerValue: 8}},
|
|
{ValueType: &pb.Value_IntegerValue{IntegerValue: 10}},
|
|
},
|
|
},
|
|
}},
|
|
"Parent.Children.Grandchild.S": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{
|
|
Values: []*pb.Value{
|
|
{ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}},
|
|
{ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 10"}},
|
|
},
|
|
},
|
|
}},
|
|
"Parent.Strings.SS": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{
|
|
Values: []*pb.Value{
|
|
{ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
{ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
wantLoad: &GrandparentOfSliceFlatten{
|
|
Parent: ParentOfSlice{
|
|
Children: []Child{
|
|
{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
},
|
|
Strings: []plsString{
|
|
"LOADED",
|
|
"LOADED",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "parent has flatten option, children (slice of ptrs) impl PLS",
|
|
src: &GrandparentOfSlicePtrsFlatten{
|
|
Parent: ParentOfSlicePtrs{
|
|
Children: []*Child{
|
|
{
|
|
I: 7,
|
|
Grandchild: Grandchild{
|
|
S: "BAD",
|
|
},
|
|
},
|
|
{
|
|
I: 9,
|
|
Grandchild: Grandchild{
|
|
S: "BAD2",
|
|
},
|
|
},
|
|
},
|
|
Strings: []*plsString{
|
|
ptrToplsString("something1"),
|
|
ptrToplsString("something1"),
|
|
},
|
|
},
|
|
},
|
|
wantSave: &pb.Entity{
|
|
Key: keyToProto(testKey0),
|
|
Properties: map[string]*pb.Value{
|
|
"Parent.Children.I": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{
|
|
Values: []*pb.Value{
|
|
{ValueType: &pb.Value_IntegerValue{IntegerValue: 8}},
|
|
{ValueType: &pb.Value_IntegerValue{IntegerValue: 10}},
|
|
},
|
|
},
|
|
}},
|
|
"Parent.Children.Grandchild.S": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{
|
|
Values: []*pb.Value{
|
|
{ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}},
|
|
{ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 10"}},
|
|
},
|
|
},
|
|
}},
|
|
"Parent.Strings.SS": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{
|
|
Values: []*pb.Value{
|
|
{ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
{ValueType: &pb.Value_StringValue{StringValue: "SAVED"}},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
wantLoad: &GrandparentOfSlicePtrsFlatten{
|
|
Parent: ParentOfSlicePtrs{
|
|
Children: []*Child{
|
|
{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
{
|
|
I: 1,
|
|
Grandchild: Grandchild{
|
|
S: "grandchild loaded",
|
|
},
|
|
},
|
|
},
|
|
Strings: []*plsString{
|
|
ptrToplsString("LOADED"),
|
|
ptrToplsString("LOADED"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
e, err := saveEntity(testKey0, tc.src)
|
|
if tc.saveErr == "" { // Want no error.
|
|
if err != nil {
|
|
t.Errorf("%s: save: %v", tc.desc, err)
|
|
continue
|
|
}
|
|
if !testutil.Equal(e, tc.wantSave) {
|
|
t.Errorf("%s: save: \ngot: %+v\nwant: %+v", tc.desc, e, tc.wantSave)
|
|
continue
|
|
}
|
|
} else { // Want error.
|
|
if err == nil {
|
|
t.Errorf("%s: save: want err", tc.desc)
|
|
continue
|
|
}
|
|
if !strings.Contains(err.Error(), tc.saveErr) {
|
|
t.Errorf("%s: save: \ngot err '%s'\nwant err '%s'", tc.desc, err.Error(), tc.saveErr)
|
|
}
|
|
continue
|
|
}
|
|
|
|
gota := reflect.New(reflect.TypeOf(tc.wantLoad).Elem()).Interface()
|
|
err = loadEntityProto(gota, e)
|
|
if tc.loadErr == "" { // Want no error.
|
|
if err != nil {
|
|
t.Errorf("%s: load: %v", tc.desc, err)
|
|
continue
|
|
}
|
|
if !testutil.Equal(gota, tc.wantLoad) {
|
|
t.Errorf("%s: load: \ngot: %+v\nwant: %+v", tc.desc, gota, tc.wantLoad)
|
|
continue
|
|
}
|
|
} else { // Want error.
|
|
if err == nil {
|
|
t.Errorf("%s: load: want err", tc.desc)
|
|
continue
|
|
}
|
|
if !strings.Contains(err.Error(), tc.loadErr) {
|
|
t.Errorf("%s: load: \ngot err '%s'\nwant err '%s'", tc.desc, err.Error(), tc.loadErr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQueryConstruction(t *testing.T) {
|
|
tests := []struct {
|
|
q, exp *Query
|
|
err string
|
|
}{
|
|
{
|
|
q: NewQuery("Foo"),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Regular filtered query with standard spacing.
|
|
q: NewQuery("Foo").Filter("foo >", 7),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
filter: []filter{
|
|
{
|
|
FieldName: "foo",
|
|
Op: greaterThan,
|
|
Value: 7,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Filtered query with no spacing.
|
|
q: NewQuery("Foo").Filter("foo=", 6),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
filter: []filter{
|
|
{
|
|
FieldName: "foo",
|
|
Op: equal,
|
|
Value: 6,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Filtered query with funky spacing.
|
|
q: NewQuery("Foo").Filter(" foo< ", 8),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
filter: []filter{
|
|
{
|
|
FieldName: "foo",
|
|
Op: lessThan,
|
|
Value: 8,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Filtered query with multicharacter op.
|
|
q: NewQuery("Foo").Filter("foo >=", 9),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
filter: []filter{
|
|
{
|
|
FieldName: "foo",
|
|
Op: greaterEq,
|
|
Value: 9,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Query with ordering.
|
|
q: NewQuery("Foo").Order("bar"),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
order: []order{
|
|
{
|
|
FieldName: "bar",
|
|
Direction: ascending,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Query with reverse ordering, and funky spacing.
|
|
q: NewQuery("Foo").Order(" - bar"),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
order: []order{
|
|
{
|
|
FieldName: "bar",
|
|
Direction: descending,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Query with an empty ordering.
|
|
q: NewQuery("Foo").Order(""),
|
|
err: "empty order",
|
|
},
|
|
{
|
|
// Query with a + ordering.
|
|
q: NewQuery("Foo").Order("+bar"),
|
|
err: "invalid order",
|
|
},
|
|
}
|
|
for i, test := range tests {
|
|
if test.q.err != nil {
|
|
got := test.q.err.Error()
|
|
if !strings.Contains(got, test.err) {
|
|
t.Errorf("%d: error mismatch: got %q want something containing %q", i, got, test.err)
|
|
}
|
|
continue
|
|
}
|
|
if !testutil.Equal(test.q, test.exp, cmp.AllowUnexported(Query{})) {
|
|
t.Errorf("%d: mismatch: got %v want %v", i, test.q, test.exp)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPutMultiTypes(t *testing.T) {
|
|
ctx := context.Background()
|
|
type S struct {
|
|
A int
|
|
B string
|
|
}
|
|
|
|
testCases := []struct {
|
|
desc string
|
|
src interface{}
|
|
wantErr bool
|
|
}{
|
|
// Test cases to check each of the valid input types for src.
|
|
// Each case has the same elements.
|
|
{
|
|
desc: "type []struct",
|
|
src: []S{
|
|
{1, "one"}, {2, "two"},
|
|
},
|
|
},
|
|
{
|
|
desc: "type []*struct",
|
|
src: []*S{
|
|
{1, "one"}, {2, "two"},
|
|
},
|
|
},
|
|
{
|
|
desc: "type []interface{} with PLS elems",
|
|
src: []interface{}{
|
|
&PropertyList{Property{Name: "A", Value: 1}, Property{Name: "B", Value: "one"}},
|
|
&PropertyList{Property{Name: "A", Value: 2}, Property{Name: "B", Value: "two"}},
|
|
},
|
|
},
|
|
{
|
|
desc: "type []interface{} with struct ptr elems",
|
|
src: []interface{}{
|
|
&S{1, "one"}, &S{2, "two"},
|
|
},
|
|
},
|
|
{
|
|
desc: "type []PropertyLoadSaver{}",
|
|
src: []PropertyLoadSaver{
|
|
&PropertyList{Property{Name: "A", Value: 1}, Property{Name: "B", Value: "one"}},
|
|
&PropertyList{Property{Name: "A", Value: 2}, Property{Name: "B", Value: "two"}},
|
|
},
|
|
},
|
|
{
|
|
desc: "type []P (non-pointer, *P implements PropertyLoadSaver)",
|
|
src: []PropertyList{
|
|
{Property{Name: "A", Value: 1}, Property{Name: "B", Value: "one"}},
|
|
{Property{Name: "A", Value: 2}, Property{Name: "B", Value: "two"}},
|
|
},
|
|
},
|
|
// Test some invalid cases.
|
|
{
|
|
desc: "type []interface{} with struct elems",
|
|
src: []interface{}{
|
|
S{1, "one"}, S{2, "two"},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "PropertyList",
|
|
src: PropertyList{
|
|
Property{Name: "A", Value: 1},
|
|
Property{Name: "B", Value: "one"},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "type []int",
|
|
src: []int{1, 2},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "not a slice",
|
|
src: S{1, "one"},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
// Use the same keys and expected entities for all tests.
|
|
keys := []*Key{
|
|
NameKey("testKind", "first", nil),
|
|
NameKey("testKind", "second", nil),
|
|
}
|
|
want := []*pb.Mutation{
|
|
{Operation: &pb.Mutation_Upsert{
|
|
Upsert: &pb.Entity{
|
|
Key: keyToProto(keys[0]),
|
|
Properties: map[string]*pb.Value{
|
|
"A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 1}},
|
|
"B": {ValueType: &pb.Value_StringValue{StringValue: "one"}},
|
|
},
|
|
}}},
|
|
{Operation: &pb.Mutation_Upsert{
|
|
Upsert: &pb.Entity{
|
|
Key: keyToProto(keys[1]),
|
|
Properties: map[string]*pb.Value{
|
|
"A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
|
"B": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
|
|
},
|
|
}}},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
// Set up a fake client which captures upserts.
|
|
var got []*pb.Mutation
|
|
client := &Client{
|
|
client: &fakeClient{
|
|
commitFn: func(req *pb.CommitRequest) (*pb.CommitResponse, error) {
|
|
got = req.Mutations
|
|
return &pb.CommitResponse{}, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
_, err := client.PutMulti(ctx, keys, tt.src)
|
|
if err != nil {
|
|
if !tt.wantErr {
|
|
t.Errorf("%s: error %v", tt.desc, err)
|
|
}
|
|
continue
|
|
}
|
|
if tt.wantErr {
|
|
t.Errorf("%s: wanted error, but none returned", tt.desc)
|
|
continue
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Errorf("%s: got %d entities, want %d", tt.desc, len(got), len(want))
|
|
continue
|
|
}
|
|
for i, e := range got {
|
|
if !proto.Equal(e, want[i]) {
|
|
t.Logf("%s: entity %d doesn't match\ngot: %v\nwant: %v", tt.desc, i, e, want[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNoIndexOnSliceProperties(t *testing.T) {
|
|
// Check that ExcludeFromIndexes is set on the inner elements,
|
|
// rather than the top-level ArrayValue value.
|
|
pl := PropertyList{
|
|
Property{
|
|
Name: "repeated",
|
|
Value: []interface{}{
|
|
123,
|
|
false,
|
|
"short",
|
|
strings.Repeat("a", 1503),
|
|
},
|
|
NoIndex: true,
|
|
},
|
|
}
|
|
key := NameKey("dummy", "dummy", nil)
|
|
|
|
entity, err := saveEntity(key, &pl)
|
|
if err != nil {
|
|
t.Fatalf("saveEntity: %v", err)
|
|
}
|
|
|
|
want := &pb.Value{
|
|
ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{Values: []*pb.Value{
|
|
{ValueType: &pb.Value_IntegerValue{IntegerValue: 123}, ExcludeFromIndexes: true},
|
|
{ValueType: &pb.Value_BooleanValue{BooleanValue: false}, ExcludeFromIndexes: true},
|
|
{ValueType: &pb.Value_StringValue{StringValue: "short"}, ExcludeFromIndexes: true},
|
|
{ValueType: &pb.Value_StringValue{StringValue: strings.Repeat("a", 1503)}, ExcludeFromIndexes: true},
|
|
}}},
|
|
}
|
|
if got := entity.Properties["repeated"]; !proto.Equal(got, want) {
|
|
t.Errorf("Entity proto differs\ngot: %v\nwant: %v", got, want)
|
|
}
|
|
}
|
|
|
|
type byName PropertyList
|
|
|
|
func (s byName) Len() int { return len(s) }
|
|
func (s byName) Less(i, j int) bool { return s[i].Name < s[j].Name }
|
|
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
|
|
// sortPL sorts the property list by property name, and
|
|
// recursively sorts any nested property lists, or nested slices of
|
|
// property lists.
|
|
func sortPL(pl PropertyList) {
|
|
sort.Stable(byName(pl))
|
|
for _, p := range pl {
|
|
switch p.Value.(type) {
|
|
case *Entity:
|
|
sortPL(p.Value.(*Entity).Properties)
|
|
case []interface{}:
|
|
for _, p2 := range p.Value.([]interface{}) {
|
|
if nent, ok := p2.(*Entity); ok {
|
|
sortPL(nent.Properties)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidGeoPoint(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
pt GeoPoint
|
|
want bool
|
|
}{
|
|
{
|
|
"valid",
|
|
GeoPoint{67.21, 13.37},
|
|
true,
|
|
},
|
|
{
|
|
"high lat",
|
|
GeoPoint{-90.01, 13.37},
|
|
false,
|
|
},
|
|
{
|
|
"low lat",
|
|
GeoPoint{90.01, 13.37},
|
|
false,
|
|
},
|
|
{
|
|
"high lng",
|
|
GeoPoint{67.21, 182},
|
|
false,
|
|
},
|
|
{
|
|
"low lng",
|
|
GeoPoint{67.21, -181},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
if got := tc.pt.Valid(); got != tc.want {
|
|
t.Errorf("%s: got %v, want %v", tc.desc, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPutInvalidEntity(t *testing.T) {
|
|
// Test that trying to put an invalid entity always returns the correct error
|
|
// type.
|
|
|
|
// Fake client that can pretend to start a transaction.
|
|
fakeClient := &fakeDatastoreClient{
|
|
beginTransaction: func(*pb.BeginTransactionRequest) (*pb.BeginTransactionResponse, error) {
|
|
return &pb.BeginTransactionResponse{
|
|
Transaction: []byte("deadbeef"),
|
|
}, nil
|
|
},
|
|
}
|
|
client := &Client{
|
|
client: fakeClient,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
key := IncompleteKey("kind", nil)
|
|
|
|
_, err := client.Put(ctx, key, "invalid entity")
|
|
if err != ErrInvalidEntityType {
|
|
t.Errorf("client.Put returned err %v, want %v", err, ErrInvalidEntityType)
|
|
}
|
|
|
|
_, err = client.PutMulti(ctx, []*Key{key}, []interface{}{"invalid entity"})
|
|
if me, ok := err.(MultiError); !ok {
|
|
t.Errorf("client.PutMulti returned err %v, want MultiError type", err)
|
|
} else if len(me) != 1 || me[0] != ErrInvalidEntityType {
|
|
t.Errorf("client.PutMulti returned err %v, want MulitError{ErrInvalidEntityType}", err)
|
|
}
|
|
|
|
client.RunInTransaction(ctx, func(tx *Transaction) error {
|
|
_, err := tx.Put(key, "invalid entity")
|
|
if err != ErrInvalidEntityType {
|
|
t.Errorf("tx.Put returned err %v, want %v", err, ErrInvalidEntityType)
|
|
}
|
|
|
|
_, err = tx.PutMulti([]*Key{key}, []interface{}{"invalid entity"})
|
|
if me, ok := err.(MultiError); !ok {
|
|
t.Errorf("tx.PutMulti returned err %v, want MultiError type", err)
|
|
} else if len(me) != 1 || me[0] != ErrInvalidEntityType {
|
|
t.Errorf("tx.PutMulti returned err %v, want MulitError{ErrInvalidEntityType}", err)
|
|
}
|
|
|
|
return errors.New("bang!") // Return error: we don't actually want to commit.
|
|
})
|
|
}
|
|
|
|
func TestDeferred(t *testing.T) {
|
|
type Ent struct {
|
|
A int
|
|
B string
|
|
}
|
|
|
|
keys := []*Key{
|
|
NameKey("testKind", "first", nil),
|
|
NameKey("testKind", "second", nil),
|
|
}
|
|
|
|
entity1 := &pb.Entity{
|
|
Key: keyToProto(keys[0]),
|
|
Properties: map[string]*pb.Value{
|
|
"A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 1}},
|
|
"B": {ValueType: &pb.Value_StringValue{StringValue: "one"}},
|
|
},
|
|
}
|
|
entity2 := &pb.Entity{
|
|
Key: keyToProto(keys[1]),
|
|
Properties: map[string]*pb.Value{
|
|
"A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
|
"B": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
|
|
},
|
|
}
|
|
|
|
// count keeps track of the number of times fakeClient.lookup has been
|
|
// called.
|
|
var count int
|
|
// Fake client that will return Deferred keys in resp on the first call.
|
|
fakeClient := &fakeDatastoreClient{
|
|
lookup: func(*pb.LookupRequest) (*pb.LookupResponse, error) {
|
|
count++
|
|
// On the first call, we return deferred keys.
|
|
if count == 1 {
|
|
return &pb.LookupResponse{
|
|
Found: []*pb.EntityResult{
|
|
{
|
|
Entity: entity1,
|
|
Version: 1,
|
|
},
|
|
},
|
|
Deferred: []*pb.Key{
|
|
keyToProto(keys[1]),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// On the second call, we do not return any more deferred keys.
|
|
return &pb.LookupResponse{
|
|
Found: []*pb.EntityResult{
|
|
{
|
|
Entity: entity2,
|
|
Version: 1,
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
client := &Client{
|
|
client: fakeClient,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
dst := make([]Ent, len(keys))
|
|
err := client.GetMulti(ctx, keys, dst)
|
|
if err != nil {
|
|
t.Fatalf("client.Get: %v", err)
|
|
}
|
|
|
|
if count != 2 {
|
|
t.Fatalf("expected client.lookup to be called 2 times. Got %d", count)
|
|
}
|
|
|
|
if len(dst) != 2 {
|
|
t.Fatalf("expected 2 entities returned, got %d", len(dst))
|
|
}
|
|
|
|
for _, e := range dst {
|
|
if e.A == 1 {
|
|
if e.B != "one" {
|
|
t.Fatalf("unexpected entity %+v", e)
|
|
}
|
|
} else if e.A == 2 {
|
|
if e.B != "two" {
|
|
t.Fatalf("unexpected entity %+v", e)
|
|
}
|
|
} else {
|
|
t.Fatalf("unexpected entity %+v", e)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
type KeyLoaderEnt struct {
|
|
A int
|
|
K *Key
|
|
}
|
|
|
|
func (e *KeyLoaderEnt) Load(p []Property) error {
|
|
e.A = 2
|
|
return nil
|
|
}
|
|
|
|
func (e *KeyLoaderEnt) LoadKey(k *Key) error {
|
|
e.K = k
|
|
return nil
|
|
}
|
|
|
|
func (e *KeyLoaderEnt) Save() ([]Property, error) {
|
|
return []Property{{Name: "A", Value: int64(3)}}, nil
|
|
}
|
|
|
|
func TestKeyLoaderEndToEnd(t *testing.T) {
|
|
keys := []*Key{
|
|
NameKey("testKind", "first", nil),
|
|
NameKey("testKind", "second", nil),
|
|
}
|
|
|
|
entity1 := &pb.Entity{
|
|
Key: keyToProto(keys[0]),
|
|
Properties: map[string]*pb.Value{
|
|
"A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 1}},
|
|
"B": {ValueType: &pb.Value_StringValue{StringValue: "one"}},
|
|
},
|
|
}
|
|
entity2 := &pb.Entity{
|
|
Key: keyToProto(keys[1]),
|
|
Properties: map[string]*pb.Value{
|
|
"A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
|
"B": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
|
|
},
|
|
}
|
|
|
|
fakeClient := &fakeDatastoreClient{
|
|
lookup: func(*pb.LookupRequest) (*pb.LookupResponse, error) {
|
|
return &pb.LookupResponse{
|
|
Found: []*pb.EntityResult{
|
|
{
|
|
Entity: entity1,
|
|
Version: 1,
|
|
},
|
|
{
|
|
Entity: entity2,
|
|
Version: 1,
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
client := &Client{
|
|
client: fakeClient,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
dst := make([]*KeyLoaderEnt, len(keys))
|
|
err := client.GetMulti(ctx, keys, dst)
|
|
if err != nil {
|
|
t.Fatalf("client.Get: %v", err)
|
|
}
|
|
|
|
for i := range dst {
|
|
if !testutil.Equal(dst[i].K, keys[i]) {
|
|
t.Fatalf("unexpected entity %d to have key %+v, got %+v", i, keys[i], dst[i].K)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeferredMissing(t *testing.T) {
|
|
type Ent struct {
|
|
A int
|
|
B string
|
|
}
|
|
|
|
keys := []*Key{
|
|
NameKey("testKind", "first", nil),
|
|
NameKey("testKind", "second", nil),
|
|
}
|
|
|
|
entity1 := &pb.Entity{
|
|
Key: keyToProto(keys[0]),
|
|
}
|
|
entity2 := &pb.Entity{
|
|
Key: keyToProto(keys[1]),
|
|
}
|
|
|
|
var count int
|
|
fakeClient := &fakeDatastoreClient{
|
|
lookup: func(*pb.LookupRequest) (*pb.LookupResponse, error) {
|
|
count++
|
|
|
|
if count == 1 {
|
|
return &pb.LookupResponse{
|
|
Missing: []*pb.EntityResult{
|
|
{
|
|
Entity: entity1,
|
|
Version: 1,
|
|
},
|
|
},
|
|
Deferred: []*pb.Key{
|
|
keyToProto(keys[1]),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return &pb.LookupResponse{
|
|
Missing: []*pb.EntityResult{
|
|
{
|
|
Entity: entity2,
|
|
Version: 1,
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
client := &Client{
|
|
client: fakeClient,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
dst := make([]Ent, len(keys))
|
|
err := client.GetMulti(ctx, keys, dst)
|
|
errs, ok := err.(MultiError)
|
|
if !ok {
|
|
t.Fatalf("expected error returns to be MultiError; got %v", err)
|
|
}
|
|
if len(errs) != 2 {
|
|
t.Fatalf("expected 2 errors returns, got %d", len(errs))
|
|
}
|
|
if errs[0] != ErrNoSuchEntity {
|
|
t.Fatalf("expected error to be ErrNoSuchEntity; got %v", errs[0])
|
|
}
|
|
if errs[1] != ErrNoSuchEntity {
|
|
t.Fatalf("expected error to be ErrNoSuchEntity; got %v", errs[1])
|
|
}
|
|
|
|
if count != 2 {
|
|
t.Fatalf("expected client.lookup to be called 2 times. Got %d", count)
|
|
}
|
|
|
|
if len(dst) != 2 {
|
|
t.Fatalf("expected 2 entities returned, got %d", len(dst))
|
|
}
|
|
|
|
for _, e := range dst {
|
|
if e.A != 0 || e.B != "" {
|
|
t.Fatalf("unexpected entity %+v", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
type fakeDatastoreClient struct {
|
|
pb.DatastoreClient
|
|
|
|
// Optional handlers for the datastore methods.
|
|
// Any handlers left undefined will return an error.
|
|
lookup func(*pb.LookupRequest) (*pb.LookupResponse, error)
|
|
runQuery func(*pb.RunQueryRequest) (*pb.RunQueryResponse, error)
|
|
beginTransaction func(*pb.BeginTransactionRequest) (*pb.BeginTransactionResponse, error)
|
|
commit func(*pb.CommitRequest) (*pb.CommitResponse, error)
|
|
rollback func(*pb.RollbackRequest) (*pb.RollbackResponse, error)
|
|
allocateIds func(*pb.AllocateIdsRequest) (*pb.AllocateIdsResponse, error)
|
|
}
|
|
|
|
func (c *fakeDatastoreClient) Lookup(ctx context.Context, in *pb.LookupRequest, opts ...grpc.CallOption) (*pb.LookupResponse, error) {
|
|
if c.lookup == nil {
|
|
return nil, errors.New("no lookup handler defined")
|
|
}
|
|
return c.lookup(in)
|
|
}
|
|
func (c *fakeDatastoreClient) RunQuery(ctx context.Context, in *pb.RunQueryRequest, opts ...grpc.CallOption) (*pb.RunQueryResponse, error) {
|
|
if c.runQuery == nil {
|
|
return nil, errors.New("no runQuery handler defined")
|
|
}
|
|
return c.runQuery(in)
|
|
}
|
|
func (c *fakeDatastoreClient) BeginTransaction(ctx context.Context, in *pb.BeginTransactionRequest, opts ...grpc.CallOption) (*pb.BeginTransactionResponse, error) {
|
|
if c.beginTransaction == nil {
|
|
return nil, errors.New("no beginTransaction handler defined")
|
|
}
|
|
return c.beginTransaction(in)
|
|
}
|
|
func (c *fakeDatastoreClient) Commit(ctx context.Context, in *pb.CommitRequest, opts ...grpc.CallOption) (*pb.CommitResponse, error) {
|
|
if c.commit == nil {
|
|
return nil, errors.New("no commit handler defined")
|
|
}
|
|
return c.commit(in)
|
|
}
|
|
func (c *fakeDatastoreClient) Rollback(ctx context.Context, in *pb.RollbackRequest, opts ...grpc.CallOption) (*pb.RollbackResponse, error) {
|
|
if c.rollback == nil {
|
|
return nil, errors.New("no rollback handler defined")
|
|
}
|
|
return c.rollback(in)
|
|
}
|
|
func (c *fakeDatastoreClient) AllocateIds(ctx context.Context, in *pb.AllocateIdsRequest, opts ...grpc.CallOption) (*pb.AllocateIdsResponse, error) {
|
|
if c.allocateIds == nil {
|
|
return nil, errors.New("no allocateIds handler defined")
|
|
}
|
|
return c.allocateIds(in)
|
|
}
|