Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(472)

Unified Diff: go/src/infra/gae/libs/gae/helper/datastore_test.go

Issue 1222903002: Refactor current GAE abstraction library to be free of the SDK* (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: more fixes Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: go/src/infra/gae/libs/gae/helper/datastore_test.go
diff --git a/go/src/infra/gae/libs/gae/helper/datastore_test.go b/go/src/infra/gae/libs/gae/helper/datastore_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..9fd3b49de82e4d75d2ac26ccb1f6e65ef84ed58d
--- /dev/null
+++ b/go/src/infra/gae/libs/gae/helper/datastore_test.go
@@ -0,0 +1,1688 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// adapted from github.com/golang/appengine/datastore
+
+package helper
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "math"
+ "reflect"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "infra/gae/libs/gae"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func mp(value interface{}, noIndexes ...bool) (ret gae.DSProperty) {
+ ni := false
+ if len(noIndexes) > 1 {
+ panic("YOU FOOL! YOU CANNOT HOPE TO PASS THAT MANY VALUES!")
+ } else if len(noIndexes) == 1 {
+ ni = noIndexes[0]
+ }
+ if err := ret.SetValue(value, ni); err != nil {
+ panic(err)
+ }
+ return
+}
+
+const testAppID = "testApp"
+
+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
+}
+
+var (
+ testKey0 = mkKey("aid", "", "kind", "name0")
+ testKey1a = mkKey("aid", "", "kind", "name1")
+ testKey1b = mkKey("aid", "", "kind", "name1")
+ testKey2a = mkKey("aid", "", "kind", "name0", "kind", "name2")
+ testKey2b = mkKey("aid", "", "kind", "name0", "kind", "name2")
+ testGeoPt0 = gae.DSGeoPoint{Lat: 1.2, Lng: 3.4}
+ testGeoPt1 = gae.DSGeoPoint{Lat: 5, Lng: 10}
+ testBadGeoPt = gae.DSGeoPoint{Lat: 1000, Lng: 34}
+)
+
+type B0 struct {
+ B []byte
+}
+
+type B1 struct {
+ B []int8
+}
+
+type B2 struct {
+ B myBlob
+}
+
+type B3 struct {
+ B []myByte
+}
+
+type B4 struct {
+ B [][]byte
+}
+
+type B5 struct {
+ B gae.DSByteString
+}
+
+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 E struct{}
+
+type G0 struct {
+ G gae.DSGeoPoint
+}
+
+type G1 struct {
+ G []gae.DSGeoPoint
+}
+
+type K0 struct {
+ K gae.DSKey
+}
+
+type K1 struct {
+ K []gae.DSKey
+}
+
+type N0 struct {
+ X0
+ ID int64 `gae:"$id"`
+ _kind string `gae:"$kind,whatnow"`
+ Nonymous X0
+ Ignore string `gae:"-"`
+ Other string
+}
+
+type N1 struct {
+ X0
+ Nonymous []X0
+ Ignore string `gae:"-"`
+ Other string
+}
+
+type N2 struct {
+ N1 `gae:"red"`
+ Green N1 `gae:"green"`
+ Blue N1
+ White N1 `gae:"-"`
+}
+
+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 Y3 struct {
+ B bool
+ F int64
+}
+
+type Tagged struct {
+ A int `gae:"a,noindex"`
+ B []int `gae:"b1"`
+ C int `gae:",noindex"`
+ D int `gae:""`
+ E int
+ I int `gae:"-"`
+ J int `gae:",noindex" json:"j"`
+
+ Y0 `gae:"-"`
+ Z chan int `gae:"-,"`
+}
+
+type InvalidTagged1 struct {
+ I int `gae:"\t"`
+}
+
+type InvalidTagged2 struct {
+ I int
+ J int `gae:"I"`
+}
+
+type InvalidTagged3 struct {
+ I int `gae:"a\t"`
+}
+
+type InvalidTagged4 struct {
+ I int `gae:"a."`
+}
+
+type InvalidTaggedSub struct {
+ I int
+}
+
+type InvalidTagged5 struct {
+ I int `gae:"V.I"`
+ V []InvalidTaggedSub
+}
+
+type Inner1 struct {
+ W int32
+ X string
+}
+
+type Inner2 struct {
+ Y float64
+}
+
+type Inner3 struct {
+ Z bool
+}
+
+type Outer struct {
+ A int16
+ I []Inner1
+ J Inner2
+ Inner3
+}
+
+type OuterEquivalent struct {
+ A int16
+ IDotW []int32 `gae:"I.W"`
+ IDotX []string `gae:"I.X"`
+ JDotY float64 `gae:"J.Y"`
+ Z bool
+}
+
+type Dotted struct {
+ A DottedA `gae:"A0.A1.A2"`
+}
+
+type DottedA struct {
+ B DottedB `gae:"B3"`
+}
+
+type DottedB struct {
+ C int `gae:"C4.C5"`
+}
+
+type SliceOfSlices struct {
+ I int
+ S []struct {
+ J int
+ F []float64
+ }
+}
+
+type Recursive struct {
+ I int
+ R []Recursive
+}
+
+type MutuallyRecursive0 struct {
+ I int
+ R []MutuallyRecursive1
+}
+
+type MutuallyRecursive1 struct {
+ I int
+ R []MutuallyRecursive0
+}
+
+type ExoticTypes struct {
+ BS gae.BSKey
+ DSBS gae.DSByteString
+}
+
+type Underspecified struct {
+ Iface gae.DSPropertyConverter
+}
+
+type MismatchTypes struct {
+ S string
+ B bool
+ F float32
+ K gae.DSKey
+ T time.Time
+ G gae.DSGeoPoint
+ IS []int
+}
+
+type BadSpecial struct {
+ ID int `gae:"$id"`
+ id string `gae:"$id"`
+}
+
+type Doubler struct {
+ S string
+ I int64
+ B bool
+}
+
+func (d *Doubler) Load(props gae.DSPropertyMap) ([]string, error) {
+ return GetStructPLS(d).Load(props)
+}
+
+func (d *Doubler) Save() (gae.DSPropertyMap, error) {
+ // Save the default gae.DSProperty slice to an in-memory buffer (a gae.DSPropertyList).
+ pls := GetStructPLS(d)
+ props, err := pls.Save()
+ if err != nil {
+ return nil, err
+ }
+ var propMap gae.DSPropertyMap
+ propMap.Load(props) // we know this returns nil/nil
+
+ // Edit that map and send it on.
+ for _, props := range propMap {
+ for i := range props {
+ switch v := props[i].Value().(type) {
+ case string:
+ // + means string concatenation.
+ props[i].SetValue(v+v, props[i].NoIndex())
+ case int64:
+ // + means integer addition.
+ props[i].SetValue(v+v, props[i].NoIndex())
+ }
+ }
+ }
+ return propMap.Save()
+}
+
+var _ gae.DSPropertyLoadSaver = (*Doubler)(nil)
+
+type Deriver struct {
+ S, Derived, Ignored string
+}
+
+func (e *Deriver) Load(props gae.DSPropertyMap) ([]string, error) {
+ for name, p := range props {
+ if name != "S" {
+ continue
+ }
+ e.S = p[0].Value().(string)
+ e.Derived = "derived+" + e.S
+ }
+ return nil, nil
+}
+
+func (e *Deriver) Save() (gae.DSPropertyMap, error) {
+ return map[string][]gae.DSProperty{
+ "S": {mp(e.S)},
+ }, nil
+}
+
+var _ gae.DSPropertyLoadSaver = (*Deriver)(nil)
+
+type BK struct {
+ Key gae.BSKey
+}
+
+type Convertable []int64
+
+var _ gae.DSPropertyConverter = (*Convertable)(nil)
+
+func (c *Convertable) ToDSProperty() (ret gae.DSProperty, err error) {
+ buf := make([]string, len(*c))
+ for i, v := range *c {
+ buf[i] = strconv.FormatInt(v, 10)
+ }
+ err = ret.SetValue(strings.Join(buf, ","), true)
+ return
+}
+
+func (c *Convertable) FromDSProperty(pv gae.DSProperty) error {
+ if sval, ok := pv.Value().(string); ok {
+ for _, t := range strings.Split(sval, ",") {
+ ival, err := strconv.ParseInt(t, 10, 64)
+ if err != nil {
+ return err
+ }
+ *c = append(*c, ival)
+ }
+ return nil
+ }
+ return fmt.Errorf("nope")
+}
+
+type Impossible struct {
+ Nested []ImpossibleInner
+}
+
+type ImpossibleInner struct {
+ Ints Convertable `gae:"wot"`
+}
+
+type Convertable2 struct {
+ Data string
+ Exploded []string
+}
+
+func (c *Convertable2) ToDSProperty() (ret gae.DSProperty, err error) {
+ err = ret.SetValue(c.Data, false)
+ return
+}
+
+func (c *Convertable2) FromDSProperty(pv gae.DSProperty) error {
+ if sval, ok := pv.Value().(string); ok {
+ c.Data = sval
+ c.Exploded = []string{"turn", "down", "for", "what"}
+ return nil
+ }
+ return fmt.Errorf("nope")
+}
+
+type Impossible2 struct {
+ Nested []ImpossibleInner2
+}
+
+type ImpossibleInner2 struct {
+ Thingy Convertable2 `gae:"nerb"`
+}
+
+type JSONKVProp map[string]interface{}
+
+var _ gae.DSPropertyConverter = (*JSONKVProp)(nil)
+
+func (j *JSONKVProp) ToDSProperty() (ret gae.DSProperty, err error) {
+ data, err := json.Marshal(map[string]interface{}(*j))
+ if err != nil {
+ return
+ }
+ err = ret.SetValue(data, true)
+ return
+}
+
+func (j *JSONKVProp) FromDSProperty(pv gae.DSProperty) error {
+ if bval, ok := pv.Value().([]byte); ok {
+ dec := json.NewDecoder(bytes.NewBuffer(bval))
+ dec.UseNumber()
+ return dec.Decode((*map[string]interface{})(j))
+ }
+ return fmt.Errorf("nope")
+}
+
+type Impossible3 struct {
+ KMap JSONKVProp `gae:"kewelmap"`
+}
+
+type Complex complex128
+
+var _ gae.DSPropertyConverter = (*Complex)(nil)
+
+func (c *Complex) ToDSProperty() (ret gae.DSProperty, err error) {
+ // cheat hardkore and usurp GeoPoint so datastore will index these suckers
+ // (note that this won't REALLY work, since GeoPoints are limited to a very
+ // limited range of values, but it's nice to pretend ;)). You'd probably
+ // really end up with a packed binary representation.
+ err = ret.SetValue(gae.DSGeoPoint{Lat: real(*c), Lng: imag(*c)}, false)
+ return
+}
+
+func (c *Complex) FromDSProperty(p gae.DSProperty) error {
+ if gval, ok := p.Value().(gae.DSGeoPoint); ok {
+ *c = Complex(complex(gval.Lat, gval.Lng))
+ return nil
+ }
+ return fmt.Errorf("nope")
+}
+
+type Impossible4 struct {
+ Values []Complex
+}
+
+type DerivedKey struct {
+ K *GenericDSKey
+}
+
+type IfaceKey struct {
+ K gae.DSKey
+}
+
+type testCase struct {
+ desc string
+ src interface{}
+ want interface{}
+ plsErr string
+ saveErr string
+ actualNoIndex bool
+ plsLoadErr string
+ convErr string
+ loadErr string
+}
+
+var testCases = []testCase{
+ {
+ desc: "chan save fails",
+ src: &C0{I: -1},
+ plsErr: `field "C" has invalid type: chan int`,
+ },
+ {
+ desc: "*chan save fails",
+ src: &C1{I: -1},
+ plsErr: `field "C" has invalid type: *chan int`,
+ },
+ {
+ desc: "[]chan save fails",
+ src: &C2{I: -1, C: make([]chan int, 8)},
+ plsErr: `field "C" has invalid type: []chan int`,
+ },
+ {
+ desc: "chan load fails",
+ src: &C3{C: "not a chan"},
+ want: &C0{},
+ plsLoadErr: `field "C" has invalid type: chan int`,
+ },
+ {
+ desc: "*chan load fails",
+ src: &C3{C: "not a *chan"},
+ want: &C1{},
+ plsLoadErr: `field "C" has invalid type: *chan int`,
+ },
+ {
+ desc: "[]chan load fails",
+ src: &C3{C: "not a []chan"},
+ want: &C2{},
+ plsLoadErr: `field "C" has invalid type: []chan int`,
+ },
+ {
+ desc: "empty struct",
+ src: &E{},
+ want: &E{},
+ },
+ {
+ desc: "geopoint",
+ src: &G0{G: testGeoPt0},
+ want: &G0{G: testGeoPt0},
+ },
+ {
+ desc: "geopoint invalid",
+ src: &G0{G: testBadGeoPt},
+ saveErr: "invalid GeoPoint value",
+ },
+ {
+ desc: "geopoint as props",
+ src: &G0{G: testGeoPt0},
+ want: &gae.DSPropertyMap{
+ "G": {mp(testGeoPt0)},
+ },
+ },
+ {
+ desc: "geopoint slice",
+ src: &G1{G: []gae.DSGeoPoint{testGeoPt0, testGeoPt1}},
+ want: &G1{G: []gae.DSGeoPoint{testGeoPt0, testGeoPt1}},
+ },
+ {
+ desc: "key",
+ src: &K0{K: testKey1a},
+ want: &K0{K: testKey1b},
+ },
+ {
+ desc: "key with parent",
+ src: &K0{K: testKey2a},
+ want: &K0{K: testKey2b},
+ },
+ {
+ desc: "nil key",
+ src: &K0{},
+ want: &K0{},
+ },
+ {
+ desc: "all nil keys in slice",
+ src: &K1{[]gae.DSKey{nil, nil}},
+ want: &K1{[]gae.DSKey{nil, nil}},
+ },
+ {
+ desc: "some nil keys in slice",
+ src: &K1{[]gae.DSKey{testKey1a, nil, testKey2a}},
+ want: &K1{[]gae.DSKey{testKey1b, nil, testKey2b}},
+ },
+ {
+ desc: "overflow",
+ src: &O0{I: 1 << 48},
+ want: &O1{},
+ convErr: "overflow",
+ },
+ {
+ desc: "time",
+ src: &T{T: time.Unix(1e9, 0).UTC()},
+ want: &T{T: time.Unix(1e9, 0).UTC()},
+ },
+ {
+ desc: "time as props",
+ src: &T{T: time.Unix(1e9, 0).UTC()},
+ want: &gae.DSPropertyMap{
+ "T": {mp(time.Unix(1e9, 0).UTC(), false)},
+ },
+ },
+ {
+ desc: "uint save",
+ src: &U0{U: 1},
+ plsErr: `field "U" has invalid type: uint`,
+ },
+ {
+ desc: "uint load",
+ src: &U1{U: "not a uint"},
+ want: &U0{},
+ plsLoadErr: `field "U" has invalid type: uint`,
+ },
+ {
+ desc: "zero",
+ src: &X0{},
+ want: &X0{},
+ },
+ {
+ desc: "basic",
+ src: &X0{S: "one", I: 2, i: 3},
+ want: &X0{S: "one", I: 2},
+ },
+ {
+ desc: "save string/int load myString/int32",
+ src: &X0{S: "one", I: 2, i: 3},
+ want: &X1{S: "one", I: 2},
+ },
+ {
+ desc: "missing fields",
+ src: &X0{S: "one", I: 2, i: 3},
+ want: &X2{},
+ convErr: "no such struct field",
+ },
+ {
+ desc: "save string load bool",
+ src: &X0{S: "one", I: 2, i: 3},
+ want: &X3{I: 2},
+ convErr: "type mismatch",
+ },
+ {
+ desc: "basic slice",
+ src: &Y0{B: true, F: []float64{7, 8, 9}},
+ want: &Y0{B: true, F: []float64{7, 8, 9}},
+ },
+ {
+ desc: "save []float64 load float64",
+ src: &Y0{B: true, F: []float64{7, 8, 9}},
+ want: &Y1{B: true},
+ convErr: "requires a slice",
+ },
+ {
+ desc: "save single []int64 load int64",
+ src: &Y2{B: true, F: []int64{7}},
+ want: &Y3{B: true, F: 7},
+ },
+ {
+ desc: "save int64 load single []int64",
+ src: &Y3{B: true, F: 7},
+ want: &Y2{B: true, F: []int64{7}},
+ },
+ {
+ desc: "use convertable slice",
+ src: &Impossible{[]ImpossibleInner{{Convertable{1, 5, 9}}, {Convertable{2, 4, 6}}}},
+ want: &Impossible{[]ImpossibleInner{{Convertable{1, 5, 9}}, {Convertable{2, 4, 6}}}},
+ },
+ {
+ desc: "use convertable slice (to map)",
+ src: &Impossible{[]ImpossibleInner{{Convertable{1, 5, 9}}, {Convertable{2, 4, 6}}}},
+ want: &gae.DSPropertyMap{
+ "Nested.wot": {mp("1,5,9", true), mp("2,4,6", true)},
+ },
+ },
+ {
+ desc: "convertable slice (bad load)",
+ src: &gae.DSPropertyMap{"Nested.wot": {mp([]byte("ohai"), true)}},
+ want: &Impossible{[]ImpossibleInner{{}}},
+ convErr: "nope",
+ },
+ {
+ desc: "use convertable struct",
+ src: &Impossible2{
+ []ImpossibleInner2{
+ {Convertable2{"nerb", nil}},
+ },
+ },
+ want: &Impossible2{
+ []ImpossibleInner2{
+ {Convertable2{"nerb", []string{"turn", "down", "for", "what"}}},
+ },
+ },
+ },
+ {
+ desc: "convertable json KVMap",
+ src: &Impossible3{
+ JSONKVProp{
+ "epic": "success",
+ "no_way!": []interface{}{true, "story"},
+ "what": []interface{}{"is", "really", 100},
+ },
+ },
+ want: &Impossible3{
+ JSONKVProp{
+ "epic": "success",
+ "no_way!": []interface{}{true, "story"},
+ "what": []interface{}{"is", "really", json.Number("100")},
+ },
+ },
+ },
+ {
+ desc: "convertable json KVMap (to map)",
+ src: &Impossible3{
+ JSONKVProp{
+ "epic": "success",
+ "no_way!": []interface{}{true, "story"},
+ "what": []interface{}{"is", "really", 100},
+ },
+ },
+ want: &gae.DSPropertyMap{
+ "kewelmap": {
+ mp([]byte(
+ `{"epic":"success","no_way!":[true,"story"],"what":["is","really",100]}`),
+ true)},
+ },
+ },
+ {
+ desc: "convertable complex slice",
+ src: &Impossible4{
+ []Complex{complex(1, 2), complex(3, 4)},
+ },
+ want: &Impossible4{
+ []Complex{complex(1, 2), complex(3, 4)},
+ },
+ },
+ {
+ desc: "convertable complex slice (to map)",
+ src: &Impossible4{
+ []Complex{complex(1, 2), complex(3, 4)},
+ },
+ want: &gae.DSPropertyMap{
+ "Values": {
+ mp(gae.DSGeoPoint{Lat: 1, Lng: 2}), mp(gae.DSGeoPoint{Lat: 3, Lng: 4})},
+ },
+ },
+ {
+ desc: "convertable complex slice (bad load)",
+ src: &gae.DSPropertyMap{"Values": {mp("hello")}},
+ want: &Impossible4{[]Complex(nil)},
+ convErr: "nope",
+ },
+ {
+ desc: "allow concrete gae.DSKey implementors (save)",
+ src: &DerivedKey{testKey2a.(*GenericDSKey)},
+ want: &IfaceKey{testKey2b},
+ },
+ {
+ desc: "allow concrete gae.DSKey implementors (load)",
+ src: &IfaceKey{testKey2b},
+ want: &DerivedKey{testKey2a.(*GenericDSKey)},
+ },
+ {
+ desc: "save []float64 load []int64",
+ src: &Y0{B: true, F: []float64{7, 8, 9}},
+ want: &Y2{B: true},
+ convErr: "type mismatch",
+ },
+ {
+ desc: "single slice is too long",
+ src: &Y0{F: make([]float64, maxIndexedProperties+1)},
+ want: &Y0{},
+ saveErr: "gae: too many indexed properties",
+ },
+ {
+ desc: "two slices are too long",
+ src: &Y0{F: make([]float64, maxIndexedProperties), G: make([]float64, maxIndexedProperties)},
+ want: &Y0{},
+ saveErr: "gae: too many indexed properties",
+ },
+ {
+ desc: "one slice and one scalar are too long",
+ src: &Y0{F: make([]float64, maxIndexedProperties), B: true},
+ want: &Y0{},
+ saveErr: "gae: too many indexed properties",
+ },
+ {
+ desc: "long blob",
+ src: &B0{B: makeUint8Slice(maxIndexedProperties + 1)},
+ want: &B0{B: makeUint8Slice(maxIndexedProperties + 1)},
+ },
+ {
+ desc: "long []int8 is too long",
+ src: &B1{B: makeInt8Slice(maxIndexedProperties + 1)},
+ want: &B1{},
+ saveErr: "gae: too many indexed properties",
+ },
+ {
+ desc: "short []int8",
+ src: &B1{B: makeInt8Slice(3)},
+ want: &B1{B: makeInt8Slice(3)},
+ },
+ {
+ desc: "long myBlob",
+ src: &B2{B: makeUint8Slice(maxIndexedProperties + 1)},
+ want: &B2{B: makeUint8Slice(maxIndexedProperties + 1)},
+ },
+ {
+ desc: "short myBlob",
+ src: &B2{B: makeUint8Slice(3)},
+ want: &B2{B: makeUint8Slice(3)},
+ },
+ {
+ desc: "long []myByte",
+ src: &B3{B: makeMyByteSlice(maxIndexedProperties + 1)},
+ want: &B3{B: makeMyByteSlice(maxIndexedProperties + 1)},
+ },
+ {
+ desc: "short []myByte",
+ src: &B3{B: makeMyByteSlice(3)},
+ want: &B3{B: makeMyByteSlice(3)},
+ },
+ {
+ desc: "slice of blobs",
+ src: &B4{B: [][]byte{
+ makeUint8Slice(3),
+ makeUint8Slice(4),
+ makeUint8Slice(5),
+ }},
+ want: &B4{B: [][]byte{
+ makeUint8Slice(3),
+ makeUint8Slice(4),
+ makeUint8Slice(5),
+ }},
+ },
+ {
+ desc: "short gae.DSByteString",
+ src: &B5{B: gae.DSByteString(makeUint8Slice(3))},
+ want: &B5{B: gae.DSByteString(makeUint8Slice(3))},
+ },
+ {
+ desc: "short gae.DSByteString as props",
+ src: &B5{B: gae.DSByteString(makeUint8Slice(3))},
+ want: &gae.DSPropertyMap{
+ "B": {mp(gae.DSByteString(makeUint8Slice(3)))},
+ },
+ },
+ {
+ desc: "[]byte must be noindex",
+ src: &gae.DSPropertyMap{
+ "B": {mp(makeUint8Slice(3))},
+ },
+ actualNoIndex: true,
+ },
+ {
+ desc: "save tagged load props",
+ src: &Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, I: 6, J: 7},
+ want: &gae.DSPropertyMap{
+ // A and B are renamed to a and b; A and C are noindex, I is ignored.
+ // Indexed properties are loaded before raw properties. Thus, the
+ // result is: b, b, b, D, E, a, c.
+ "b1": {
+ mp(21),
+ mp(22),
+ mp(23),
+ },
+ "D": {mp(4)},
+ "E": {mp(5)},
+ "a": {mp(1, true)},
+ "C": {mp(3, true)},
+ "J": {mp(7, true)},
+ },
+ },
+ {
+ desc: "save tagged load tagged",
+ src: &Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, I: 6, J: 7},
+ want: &Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, J: 7},
+ },
+ {
+ desc: "save props load tagged",
+ src: &gae.DSPropertyMap{
+ "A": {mp(11, true)},
+ "a": {mp(12, true)},
+ },
+ want: &Tagged{A: 12},
+ convErr: `cannot load field "A"`,
+ },
+ {
+ desc: "invalid tagged1",
+ src: &InvalidTagged1{I: 1},
+ plsErr: `struct tag has invalid property name: "\t"`,
+ },
+ {
+ desc: "invalid tagged2",
+ src: &InvalidTagged2{I: 1, J: 2},
+ want: &InvalidTagged2{},
+ plsErr: `struct tag has repeated property name: "I"`,
+ },
+ {
+ desc: "invalid tagged3",
+ src: &InvalidTagged3{I: 1},
+ plsErr: `struct tag has invalid property name: "a\t"`,
+ },
+ {
+ desc: "invalid tagged4",
+ src: &InvalidTagged4{I: 1},
+ plsErr: `struct tag has invalid property name: "a."`,
+ },
+ {
+ desc: "invalid tagged5",
+ src: &InvalidTagged5{I: 19, V: []InvalidTaggedSub{{1}}},
+ plsErr: `struct tag has repeated property name: "V.I"`,
+ },
+ {
+ desc: "doubler",
+ src: &Doubler{S: "s", I: 1, B: true},
+ want: &Doubler{S: "ss", I: 2, B: true},
+ },
+ {
+ desc: "save struct load props",
+ src: &X0{S: "s", I: 1},
+ want: &gae.DSPropertyMap{
+ "S": {mp("s")},
+ "I": {mp(1)},
+ },
+ },
+ {
+ desc: "save props load struct",
+ src: &gae.DSPropertyMap{
+ "S": {mp("s")},
+ "I": {mp(1)},
+ },
+ want: &X0{S: "s", I: 1},
+ },
+ {
+ desc: "nil-value props",
+ src: &gae.DSPropertyMap{
+ "I": {mp(nil)},
+ "B": {mp(nil)},
+ "S": {mp(nil)},
+ "F": {mp(nil)},
+ "K": {mp(nil)},
+ "T": {mp(nil)},
+ "J": {
+ mp(nil),
+ mp(7),
+ mp(nil),
+ },
+ },
+ want: &struct {
+ I int64
+ B bool
+ S string
+ F float64
+ K gae.DSKey
+ T time.Time
+ J []int64
+ }{
+ J: []int64{0, 7, 0},
+ },
+ },
+ {
+ desc: "save outer load props",
+ src: &Outer{
+ A: 1,
+ I: []Inner1{
+ {10, "ten"},
+ {20, "twenty"},
+ {30, "thirty"},
+ },
+ J: Inner2{
+ Y: 3.14,
+ },
+ Inner3: Inner3{
+ Z: true,
+ },
+ },
+ want: &gae.DSPropertyMap{
+ "A": {mp(1)},
+ "I.W": {
+ mp(10),
+ mp(20),
+ mp(30),
+ },
+ "I.X": {
+ mp("ten"),
+ mp("twenty"),
+ mp("thirty"),
+ },
+ "J.Y": {mp(3.14)},
+ "Z": {mp(true)},
+ },
+ },
+ {
+ desc: "save props load outer-equivalent",
+ src: &gae.DSPropertyMap{
+ "A": {mp(1)},
+ "I.W": {
+ mp(10),
+ mp(20),
+ mp(30),
+ },
+ "I.X": {
+ mp("ten"),
+ mp("twenty"),
+ mp("thirty"),
+ },
+ "J.Y": {mp(3.14)},
+ "Z": {mp(true)},
+ },
+ want: &OuterEquivalent{
+ A: 1,
+ IDotW: []int32{10, 20, 30},
+ IDotX: []string{"ten", "twenty", "thirty"},
+ JDotY: 3.14,
+ Z: true,
+ },
+ },
+ {
+ desc: "save outer-equivalent load outer",
+ src: &OuterEquivalent{
+ A: 1,
+ IDotW: []int32{10, 20, 30},
+ IDotX: []string{"ten", "twenty", "thirty"},
+ JDotY: 3.14,
+ Z: true,
+ },
+ want: &Outer{
+ A: 1,
+ I: []Inner1{
+ {10, "ten"},
+ {20, "twenty"},
+ {30, "thirty"},
+ },
+ J: Inner2{
+ Y: 3.14,
+ },
+ Inner3: Inner3{
+ Z: true,
+ },
+ },
+ },
+ {
+ desc: "dotted names save",
+ src: &Dotted{A: DottedA{B: DottedB{C: 88}}},
+ want: &gae.DSPropertyMap{
+ "A0.A1.A2.B3.C4.C5": {mp(88)},
+ },
+ },
+ {
+ desc: "dotted names load",
+ src: &gae.DSPropertyMap{
+ "A0.A1.A2.B3.C4.C5": {mp(99)},
+ },
+ want: &Dotted{A: DottedA{B: DottedB{C: 99}}},
+ },
+ {
+ desc: "save struct load deriver",
+ src: &X0{S: "s", I: 1},
+ want: &Deriver{S: "s", Derived: "derived+s"},
+ },
+ {
+ desc: "save deriver load struct",
+ src: &Deriver{S: "s", Derived: "derived+s", Ignored: "ignored"},
+ want: &X0{S: "s"},
+ },
+ // Regression: CL 25062824 broke handling of appengine.BlobKey fields.
+ {
+ desc: "appengine.BlobKey",
+ src: &BK{Key: "blah"},
+ want: &BK{Key: "blah"},
+ },
+ {
+ desc: "zero time.Time",
+ src: &T{T: time.Time{}},
+ want: &T{T: time.Time{}},
+ },
+ {
+ desc: "time.Time near Unix zero time",
+ src: &T{T: time.Unix(0, 4e3).UTC()},
+ want: &T{T: time.Unix(0, 4e3).UTC()},
+ },
+ {
+ desc: "time.Time, far in the future",
+ src: &T{T: time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC)},
+ want: &T{T: time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC)},
+ },
+ {
+ desc: "time.Time, very far in the past",
+ src: &T{T: time.Date(-300000, 1, 1, 0, 0, 0, 0, time.UTC)},
+ want: &T{},
+ saveErr: "time value out of range",
+ },
+ {
+ desc: "time.Time, very far in the future",
+ src: &T{T: time.Date(294248, 1, 1, 0, 0, 0, 0, time.UTC)},
+ want: &T{},
+ saveErr: "time value out of range",
+ },
+ {
+ desc: "structs",
+ src: &N0{
+ X0: X0{S: "one", I: 2, i: 3},
+ Nonymous: X0{S: "four", I: 5, i: 6},
+ Ignore: "ignore",
+ Other: "other",
+ },
+ want: &N0{
+ X0: X0{S: "one", I: 2},
+ Nonymous: X0{S: "four", I: 5},
+ Other: "other",
+ },
+ },
+ {
+ desc: "exotic types",
+ src: &ExoticTypes{
+ BS: "sup",
+ DSBS: gae.DSByteString("nerds"),
+ },
+ want: &ExoticTypes{
+ BS: "sup",
+ DSBS: gae.DSByteString("nerds"),
+ },
+ },
+ {
+ desc: "underspecified types",
+ src: &Underspecified{},
+ plsErr: "non-concrete interface",
+ },
+ {
+ desc: "mismatch (string)",
+ src: &gae.DSPropertyMap{
+ "K": {mp(199)},
+ "S": {mp([]byte("cats"))},
+ "F": {mp(gae.DSByteString("nurbs"))},
+ },
+ want: &MismatchTypes{},
+ convErr: "type mismatch",
+ },
+ {
+ desc: "mismatch (float)",
+ src: &gae.DSPropertyMap{"F": {mp(gae.BSKey("wot"))}},
+ want: &MismatchTypes{},
+ convErr: "type mismatch",
+ },
+ {
+ desc: "mismatch (float/overflow)",
+ src: &gae.DSPropertyMap{"F": {mp(math.MaxFloat64)}},
+ want: &MismatchTypes{},
+ convErr: "overflows",
+ },
+ {
+ desc: "mismatch (key)",
+ src: &gae.DSPropertyMap{"K": {mp(false)}},
+ want: &MismatchTypes{},
+ convErr: "type mismatch",
+ },
+ {
+ desc: "mismatch (bool)",
+ src: &gae.DSPropertyMap{"B": {mp(testKey0)}},
+ want: &MismatchTypes{},
+ convErr: "type mismatch",
+ },
+ {
+ desc: "mismatch (time)",
+ src: &gae.DSPropertyMap{"T": {mp(gae.DSGeoPoint{})}},
+ want: &MismatchTypes{},
+ convErr: "type mismatch",
+ },
+ {
+ desc: "mismatch (geopoint)",
+ src: &gae.DSPropertyMap{"G": {mp(time.Now().UTC())}},
+ want: &MismatchTypes{},
+ convErr: "type mismatch",
+ },
+ {
+ desc: "slice of structs",
+ src: &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",
+ },
+ want: &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",
+ },
+ },
+ {
+ desc: "structs with slices of structs",
+ src: &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"},
+ },
+ },
+ },
+ want: &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"},
+ },
+ },
+ },
+ },
+ {
+ desc: "save structs load props",
+ src: &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"},
+ },
+ },
+ },
+ want: &gae.DSPropertyMap{
+ "red.S": {mp("rouge")},
+ "red.I": {mp(0)},
+ "red.Nonymous.S": {mp("rosso0"), mp("rosso1")},
+ "red.Nonymous.I": {mp(0), mp(0)},
+ "red.Other": {mp("")},
+ "green.S": {mp("vert")},
+ "green.I": {mp(0)},
+ "green.Nonymous.S": {mp("verde0"), mp("verde1"), mp("verde2")},
+ "green.Nonymous.I": {mp(0), mp(0), mp(0)},
+ "green.Other": {mp("")},
+ "Blue.S": {mp("bleu")},
+ "Blue.I": {mp(0)},
+ "Blue.Nonymous.S": {mp("blu0"), mp("blu1"), mp("blu2"), mp("blu3")},
+ "Blue.Nonymous.I": {mp(0), mp(0), mp(0), mp(0)},
+ "Blue.Other": {mp("")},
+ },
+ },
+ {
+ desc: "save props load structs with ragged fields",
+ src: &gae.DSPropertyMap{
+ "red.S": {mp("rot")},
+ "green.Nonymous.I": {mp(10), mp(11), mp(12), mp(13)},
+ "Blue.Nonymous.S": {mp("blau0"), mp("blau1"), mp("blau2")},
+ "Blue.Nonymous.I": {mp(20), mp(21)},
+ },
+ want: &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"},
+ },
+ },
+ },
+ },
+ {
+ desc: "save structs with noindex tags",
+ src: &struct {
+ A struct {
+ X string `gae:",noindex"`
+ Y string
+ } `gae:",noindex"`
+ B struct {
+ X string `gae:",noindex"`
+ Y string
+ }
+ }{},
+ want: &gae.DSPropertyMap{
+ "B.Y": {mp("")},
+ "A.X": {mp("", true)},
+ "A.Y": {mp("", true)},
+ "B.X": {mp("", true)},
+ },
+ },
+ {
+ desc: "embedded struct with name override",
+ src: &struct {
+ Inner1 `gae:"foo"`
+ }{},
+ want: &gae.DSPropertyMap{
+ "foo.W": {mp(0)},
+ "foo.X": {mp("")},
+ },
+ },
+ {
+ desc: "slice of slices",
+ src: &SliceOfSlices{},
+ plsErr: `flattening nested structs leads to a slice of slices: field "S"`,
+ },
+ {
+ desc: "recursive struct",
+ src: &Recursive{},
+ plsErr: `field "R" is recursively defined`,
+ },
+ {
+ desc: "mutually recursive struct",
+ src: &MutuallyRecursive0{},
+ plsErr: `field "R" has problem: field "R" is recursively defined`,
+ },
+ {
+ desc: "non-exported struct fields",
+ src: &struct {
+ i, J int64
+ }{i: 1, J: 2},
+ want: &gae.DSPropertyMap{
+ "J": {mp(2)},
+ },
+ },
+ {
+ desc: "json.RawMessage",
+ src: &struct {
+ J json.RawMessage
+ }{
+ J: json.RawMessage("rawr"),
+ },
+ want: &gae.DSPropertyMap{
+ "J": {mp([]byte("rawr"))},
+ },
+ },
+ {
+ desc: "json.RawMessage to myBlob",
+ src: &struct {
+ B json.RawMessage
+ }{
+ B: json.RawMessage("rawr"),
+ },
+ want: &B2{B: myBlob("rawr")},
+ },
+}
+
+// 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 ShouldErrLike(actual interface{}, expected ...interface{}) string {
+ e2s := func(o interface{}) (string, bool) {
+ switch x := o.(type) {
+ case nil:
+ return "", true
+ case string:
+ return x, true
+ case error:
+ if x != nil {
+ return x.Error(), true
+ }
+ return "", true
+ }
+ return fmt.Sprintf("unknown argument type %T, expected string, error or nil", actual), false
+ }
+
+ as, ok := e2s(actual)
+ if !ok {
+ return as
+ }
+
+ if len(expected) != 1 {
+ return fmt.Sprintf("Assertion requires 1 expected value, got %d", len(expected))
+ }
+
+ err, ok := e2s(expected[0])
+ if !ok {
+ return err
+ }
+ return ShouldContainSubstring(as, err)
+}
+
+func TestRoundTrip(t *testing.T) {
+ t.Parallel()
+
+ checkErr := func(actual interface{}, expected string) bool {
+ So(actual, ShouldErrLike, expected)
+ return expected != ""
+ }
+
+ Convey("Test round-trip", t, func() {
+ for _, tc := range testCases {
+ tc := tc
+ Convey(tc.desc, func() {
+ pls, err := GetPLS(tc.src)
+ if checkErr(err, tc.plsErr) {
+ return
+ }
+ So(pls, ShouldNotBeNil)
+
+ savedProps, err := pls.Save()
+ if checkErr(err, tc.saveErr) {
+ return
+ }
+ So(savedProps, ShouldNotBeNil)
+
+ if tc.actualNoIndex {
+ for _, props := range savedProps {
+ So(props[0].NoIndex(), ShouldBeTrue)
+ return
+ }
+ So(true, ShouldBeFalse) // shouldn't get here
+ }
+
+ var got interface{}
+ if _, ok := tc.want.(*gae.DSPropertyMap); ok {
+ got = &gae.DSPropertyMap{}
+ } else {
+ got = reflect.New(reflect.TypeOf(tc.want).Elem()).Interface()
+ }
+
+ pls, err = GetPLS(got)
+ if checkErr(err, tc.plsLoadErr) {
+ return
+ }
+ So(pls, ShouldNotBeNil)
+
+ convErrs, err := pls.Load(savedProps)
+ if checkErr(err, tc.loadErr) {
+ return
+ }
+ if len(tc.convErr) == 0 {
+ So(convErrs, ShouldBeNil)
+ } else {
+ So(convErrs[0], ShouldContainSubstring, tc.convErr)
+ }
+ if tc.want == nil {
+ return
+ }
+
+ if gotT, ok := got.(*T); ok {
+ // Round tripping a time.Time can result in a different time.Location: Local instead of UTC.
+ // We therefore test equality explicitly, instead of relying on reflect.DeepEqual.
+ So(gotT.T.Equal(tc.want.(*T).T), ShouldBeTrue)
+ } else {
+ So(got, ShouldResemble, tc.want)
+ }
+ })
+ }
+ })
+}
+
+func TestSpecial(t *testing.T) {
+ t.Parallel()
+
+ Convey("Test special fields", t, func() {
+ Convey("Can retrieve from struct", func() {
+ o := &N0{ID: 100}
+ pls := GetStructPLS(o)
+ val, current, err := pls.GetSpecial("id")
+ So(err, ShouldBeNil)
+ So(val, ShouldEqual, "")
+ So(current, ShouldEqual, 100)
+
+ val, current, err = pls.GetSpecial("kind")
+ So(err, ShouldBeNil)
+ So(val, ShouldEqual, "whatnow")
+ So(current, ShouldEqual, nil)
+ })
+
+ Convey("Getting something not there is an error", func() {
+ o := &N0{ID: 100}
+ pls := GetStructPLS(o)
+ _, _, err := pls.GetSpecial("wat")
+ So(err, ShouldEqual, gae.ErrDSSpecialFieldUnset)
+ })
+
+ Convey("getting/setting from a bad struct is an error", func() {
+ o := &Recursive{}
+ pls := GetStructPLS(o)
+ _, _, err := pls.GetSpecial("wat")
+ So(err, ShouldNotBeNil)
+
+ err = pls.SetSpecial("wat", 100)
+ So(err, ShouldNotBeNil)
+ })
+
+ Convey("can assign values to exported special fields", func() {
+ o := &N0{ID: 100}
+ pls := GetStructPLS(o)
+ err := pls.SetSpecial("id", int64(200))
+ So(err, ShouldBeNil)
+ So(o.ID, ShouldEqual, 200)
+
+ })
+
+ Convey("assigning to unsassiagnable fields is a simple error", func() {
+ o := &N0{ID: 100}
+ pls := GetStructPLS(o)
+ err := pls.SetSpecial("kind", "hi")
+ So(err.Error(), ShouldContainSubstring, "unexported field")
+
+ err = pls.SetSpecial("noob", "hi")
+ So(err, ShouldEqual, gae.ErrDSSpecialFieldUnset)
+ })
+ })
+
+ Convey("StructPLS Miscellaneous", t, func() {
+ Convey("multiple overlapping fields is an error", func() {
+ o := &BadSpecial{}
+ pls := GetStructPLS(o)
+ convErr, err := pls.Load(nil)
+ So(convErr, ShouldBeNil)
+ So(err, ShouldErrLike, "multiple times")
+ e := pls.Problem()
+ _, err = pls.Save()
+ So(err, ShouldEqual, e)
+ _, err = pls.Load(nil)
+ So(err, ShouldEqual, e)
+ })
+
+ Convey("can transform a list of things into a list of PLSs", func() {
+ o := []interface{}{
+ &N0{X0: X0{S: "hi", I: 5}},
+ &N0{Nonymous: X0{S: "hi", I: 5}},
+ &gae.DSPropertyMap{
+ "Nerd": {mp(10), mp(false)},
+ "What": {mp("is"), mp("up")},
+ },
+ }
+ plss, err := MultiGetPLS(o)
+ So(err, ShouldBeNil)
+ for i, pls := range plss {
+ pmap, err := pls.Save()
+ targ := gae.DSPropertyLoadSaver(&gae.DSPropertyMap{})
+ obj := interface{}(targ)
+ if i < 2 {
+ obj = &N0{}
+ targ = GetStructPLS(obj)
+ }
+ convErr, err := targ.Load(pmap)
+ So(err, ShouldBeNil)
+ So(convErr, ShouldBeNil)
+ So(obj, ShouldResemble, o[i])
+ }
+ })
+
+ Convey("list of DSPropertyLoadSavers is a shortcut", func() {
+ o := []gae.DSPropertyLoadSaver{&gae.DSPropertyMap{}}
+ plss, err := MultiGetPLS(o)
+ So(err, ShouldBeNil)
+ So(&plss[0], ShouldEqual, &o[0]) // identical underlying array
+ })
+
+ Convey("attempting to transform a bad object is an error", func() {
+ o := []int{100}
+ _, err := MultiGetPLS(o)
+ So(err, ShouldEqual, gae.ErrDSInvalidEntityType)
+
+ f := false
+ _, err = MultiGetPLS(&f)
+ So(err, ShouldErrLike, "bad type")
+ })
+
+ Convey("empty property names are invalid", func() {
+ So(validPropertyName(""), ShouldBeFalse)
+ })
+ })
+}

Powered by Google App Engine
This is Rietveld 408576698