| /* |
| * Copyright (c) 2013 Matt Jibson <matt.jibson@gmail.com> |
| * |
| * Permission to use, copy, modify, and distribute this software for any |
| * purpose with or without fee is hereby granted, provided that the above |
| * copyright notice and this permission notice appear in all copies. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| */ |
| |
| package goon |
| |
| import ( |
| "reflect" |
| "sync" |
| "testing" |
| "time" |
| |
| "golang.org/x/net/context" |
| "google.golang.org/appengine" |
| "google.golang.org/appengine/aetest" |
| "google.golang.org/appengine/datastore" |
| "google.golang.org/appengine/memcache" |
| ) |
| |
| // *[]S, *[]*S, *[]I, []S, []*S, []I |
| const ( |
| ivTypePtrToSliceOfStructs = iota |
| ivTypePtrToSliceOfPtrsToStruct |
| ivTypePtrToSliceOfInterfaces |
| ivTypeSliceOfStructs |
| ivTypeSliceOfPtrsToStruct |
| ivTypeSliceOfInterfaces |
| ivTypeTotal |
| ) |
| |
| const ( |
| ivModeDatastore = iota |
| ivModeMemcache |
| ivModeMemcacheAndDatastore |
| ivModeLocalcache |
| ivModeLocalcacheAndMemcache |
| ivModeLocalcacheAndDatastore |
| ivModeLocalcacheAndMemcacheAndDatastore |
| ivModeTotal |
| ) |
| |
| // Have a bunch of different supported types to detect any wild errors |
| // https://developers.google.com/appengine/docs/go/datastore/reference |
| type ivItem struct { |
| Id int64 `datastore:"-" goon:"id"` |
| Int int `datastore:"int,noindex"` |
| Int8 int8 `datastore:"int8,noindex"` |
| Int16 int16 `datastore:"int16,noindex"` |
| Int32 int32 `datastore:"int32,noindex"` |
| Int64 int64 `datastore:"int64,noindex"` |
| Float32 float32 `datastore:"float32,noindex"` |
| Float64 float64 `datastore:"float64,noindex"` |
| Bool bool `datastore:"bool,noindex"` |
| String string `datastore:"string,noindex"` |
| CustomTypes ivItemCustom `datastore:"custom,noindex"` |
| SliceTypes ivItemSlice `datastore:"slice,noindex"` |
| ByteSlice []byte `datastore:"byte_slice,noindex"` |
| BSSlice [][]byte `datastore:"bs_slice,noindex"` |
| Time time.Time `datastore:"time,noindex"` |
| TimeSlice []time.Time `datastore:"time_slice,noindex"` |
| NoIndex int `datastore:",noindex"` |
| Casual string |
| Ζεύς string |
| Key *datastore.Key |
| ChildKey *datastore.Key |
| ZeroKey *datastore.Key |
| KeySlice []*datastore.Key |
| KeySliceNil []*datastore.Key |
| BlobKey appengine.BlobKey |
| BKSlice []appengine.BlobKey |
| Sub ivItemSub |
| Subs []ivItemSubs |
| ZZZV []ivZZZV |
| } |
| |
| type ivItemInt int |
| type ivItemInt8 int8 |
| type ivItemInt16 int16 |
| type ivItemInt32 int32 |
| type ivItemInt64 int64 |
| type ivItemFloat32 float32 |
| type ivItemFloat64 float64 |
| type ivItemBool bool |
| type ivItemString string |
| |
| type ivItemDeepInt ivItemInt |
| |
| type ivItemCustom struct { |
| Int ivItemInt |
| Int8 ivItemInt8 |
| Int16 ivItemInt16 |
| Int32 ivItemInt32 |
| Int64 ivItemInt64 |
| Float32 ivItemFloat32 |
| Float64 ivItemFloat64 |
| Bool ivItemBool |
| String ivItemString |
| DeepInt ivItemDeepInt |
| } |
| |
| type ivItemSlice struct { |
| Int []int |
| Int8 []int8 |
| Int16 []int16 |
| Int32 []int32 |
| Int64 []int64 |
| Float32 []float32 |
| Float64 []float64 |
| Bool []bool |
| String []string |
| IntC []ivItemInt |
| Int8C []ivItemInt8 |
| Int16C []ivItemInt16 |
| Int32C []ivItemInt32 |
| Int64C []ivItemInt64 |
| Float32C []ivItemFloat32 |
| Float64C []ivItemFloat64 |
| BoolC []ivItemBool |
| StringC []ivItemString |
| DeepInt []ivItemDeepInt |
| } |
| |
| type ivItemSub struct { |
| Data string `datastore:"data,noindex"` |
| Ints []int `datastore:"ints,noindex"` |
| } |
| |
| type ivItemSubs struct { |
| Data string `datastore:"data,noindex"` |
| Extra string `datastore:",noindex"` |
| } |
| |
| type ivZZZV struct { |
| Key *datastore.Key `datastore:"key,noindex"` |
| Data string `datastore:"data,noindex"` |
| } |
| |
| func (ivi *ivItem) ForInterface() {} |
| |
| type ivItemI interface { |
| ForInterface() |
| } |
| |
| var ivItems []ivItem |
| |
| func initializeIvItems(c context.Context) { |
| // We force UTC, because the datastore API will always return UTC |
| t1 := time.Now().UTC().Truncate(time.Microsecond) |
| t2 := t1.Add(time.Second * 1) |
| t3 := t1.Add(time.Second * 2) |
| |
| ivItems = []ivItem{ |
| {Id: 1, Int: 123, Int8: 77, Int16: 13001, Int32: 1234567890, Int64: 123456789012345, |
| Float32: (float32(10) / float32(3)), Float64: (float64(10000000) / float64(9998)), |
| Bool: true, String: "one", |
| CustomTypes: ivItemCustom{Int: 123, Int8: 77, Int16: 13001, Int32: 1234567890, Int64: 123456789012345, |
| Float32: ivItemFloat32(float32(10) / float32(3)), Float64: ivItemFloat64(float64(10000000) / float64(9998)), |
| Bool: true, String: "one", DeepInt: 1}, |
| SliceTypes: ivItemSlice{Int: []int{1, 2}, Int8: []int8{1, 2}, Int16: []int16{1, 2}, Int32: []int32{1, 2}, Int64: []int64{1, 2}, |
| Float32: []float32{1.0, 2.0}, Float64: []float64{1.0, 2.0}, Bool: []bool{true, false}, String: []string{"one", "two"}, |
| IntC: []ivItemInt{1, 2}, Int8C: []ivItemInt8{1, 2}, Int16C: []ivItemInt16{1, 2}, Int32C: []ivItemInt32{1, 2}, Int64C: []ivItemInt64{1, 2}, |
| Float32C: []ivItemFloat32{1.0, 2.0}, Float64C: []ivItemFloat64{1.0, 2.0}, |
| BoolC: []ivItemBool{true, false}, StringC: []ivItemString{"one", "two"}, DeepInt: []ivItemDeepInt{1, 2}}, |
| ByteSlice: []byte{0xDE, 0xAD}, BSSlice: [][]byte{{0x01, 0x02}, {0x03, 0x04}}, |
| Time: t1, TimeSlice: []time.Time{t1, t2, t3}, NoIndex: 1, |
| Casual: "clothes", Ζεύς: "Zeus", |
| Key: datastore.NewKey(c, "Fruit", "Apple", 0, nil), |
| ChildKey: datastore.NewKey(c, "Person", "Jane", 0, datastore.NewKey(c, "Person", "John", 0, datastore.NewKey(c, "Person", "Jack", 0, nil))), |
| KeySlice: []*datastore.Key{datastore.NewKey(c, "Key", "", 1, nil), datastore.NewKey(c, "Key", "", 2, nil), datastore.NewKey(c, "Key", "", 3, nil)}, |
| KeySliceNil: []*datastore.Key{datastore.NewKey(c, "Number", "", 1, nil), nil, datastore.NewKey(c, "Number", "", 2, nil)}, |
| BlobKey: "fake #1", BKSlice: []appengine.BlobKey{"fake #1.1", "fake #1.2"}, |
| Sub: ivItemSub{Data: "yay #1", Ints: []int{1, 2, 3}}, |
| Subs: []ivItemSubs{ |
| {Data: "sub #1.1", Extra: "xtra #1.1"}, |
| {Data: "sub #1.2", Extra: "xtra #1.2"}, |
| {Data: "sub #1.3", Extra: "xtra #1.3"}}, |
| ZZZV: []ivZZZV{{Data: "None"}, {Key: datastore.NewKey(c, "Fruit", "Banana", 0, nil)}}}, |
| {Id: 2, Int: 124, Int8: 78, Int16: 13002, Int32: 1234567891, Int64: 123456789012346, |
| Float32: (float32(10) / float32(3)), Float64: (float64(10000000) / float64(9998)), |
| Bool: true, String: "two", |
| CustomTypes: ivItemCustom{Int: 124, Int8: 78, Int16: 13002, Int32: 1234567891, Int64: 123456789012346, |
| Float32: ivItemFloat32(float32(10) / float32(3)), Float64: ivItemFloat64(float64(10000000) / float64(9998)), |
| Bool: true, String: "two", DeepInt: 2}, |
| SliceTypes: ivItemSlice{Int: []int{1, 2}, Int8: []int8{1, 2}, Int16: []int16{1, 2}, Int32: []int32{1, 2}, Int64: []int64{1, 2}, |
| Float32: []float32{1.0, 2.0}, Float64: []float64{1.0, 2.0}, Bool: []bool{true, false}, String: []string{"one", "two"}, |
| IntC: []ivItemInt{1, 2}, Int8C: []ivItemInt8{1, 2}, Int16C: []ivItemInt16{1, 2}, Int32C: []ivItemInt32{1, 2}, Int64C: []ivItemInt64{1, 2}, |
| Float32C: []ivItemFloat32{1.0, 2.0}, Float64C: []ivItemFloat64{1.0, 2.0}, |
| BoolC: []ivItemBool{true, false}, StringC: []ivItemString{"one", "two"}, DeepInt: []ivItemDeepInt{1, 2}}, |
| ByteSlice: []byte{0xBE, 0xEF}, BSSlice: [][]byte{{0x05, 0x06}, {0x07, 0x08}}, |
| Time: t2, TimeSlice: []time.Time{t2, t3, t1}, NoIndex: 2, |
| Casual: "manners", Ζεύς: "Alcmene", |
| Key: datastore.NewKey(c, "Fruit", "Banana", 0, nil), |
| ChildKey: datastore.NewKey(c, "Person", "Jane", 0, datastore.NewKey(c, "Person", "John", 0, datastore.NewKey(c, "Person", "Jack", 0, nil))), |
| KeySlice: []*datastore.Key{datastore.NewKey(c, "Key", "", 4, nil), datastore.NewKey(c, "Key", "", 5, nil), datastore.NewKey(c, "Key", "", 6, nil)}, |
| KeySliceNil: []*datastore.Key{datastore.NewKey(c, "Number", "", 3, nil), nil, datastore.NewKey(c, "Number", "", 4, nil)}, |
| BlobKey: "fake #2", BKSlice: []appengine.BlobKey{"fake #2.1", "fake #2.2"}, |
| Sub: ivItemSub{Data: "yay #2", Ints: []int{4, 5, 6}}, |
| Subs: []ivItemSubs{ |
| {Data: "sub #2.1", Extra: "xtra #2.1"}, |
| {Data: "sub #2.2", Extra: "xtra #2.2"}, |
| {Data: "sub #2.3", Extra: "xtra #2.3"}}, |
| ZZZV: []ivZZZV{{Data: "None"}, {Key: datastore.NewKey(c, "Fruit", "Banana", 0, nil)}}}, |
| {Id: 3, Int: 125, Int8: 79, Int16: 13003, Int32: 1234567892, Int64: 123456789012347, |
| Float32: (float32(10) / float32(3)), Float64: (float64(10000000) / float64(9998)), |
| Bool: true, String: "tri", |
| CustomTypes: ivItemCustom{Int: 125, Int8: 79, Int16: 13003, Int32: 1234567892, Int64: 123456789012347, |
| Float32: ivItemFloat32(float32(10) / float32(3)), Float64: ivItemFloat64(float64(10000000) / float64(9998)), |
| Bool: true, String: "tri", DeepInt: 3}, |
| SliceTypes: ivItemSlice{Int: []int{1, 2}, Int8: []int8{1, 2}, Int16: []int16{1, 2}, Int32: []int32{1, 2}, Int64: []int64{1, 2}, |
| Float32: []float32{1.0, 2.0}, Float64: []float64{1.0, 2.0}, Bool: []bool{true, false}, String: []string{"one", "two"}, |
| IntC: []ivItemInt{1, 2}, Int8C: []ivItemInt8{1, 2}, Int16C: []ivItemInt16{1, 2}, Int32C: []ivItemInt32{1, 2}, Int64C: []ivItemInt64{1, 2}, |
| Float32C: []ivItemFloat32{1.0, 2.0}, Float64C: []ivItemFloat64{1.0, 2.0}, |
| BoolC: []ivItemBool{true, false}, StringC: []ivItemString{"one", "two"}, DeepInt: []ivItemDeepInt{1, 2}}, |
| ByteSlice: []byte{0xF0, 0x0D}, BSSlice: [][]byte{{0x09, 0x0A}, {0x0B, 0x0C}}, |
| Time: t3, TimeSlice: []time.Time{t3, t1, t2}, NoIndex: 3, |
| Casual: "weather", Ζεύς: "Hercules", |
| Key: datastore.NewKey(c, "Fruit", "Cherry", 0, nil), |
| ChildKey: datastore.NewKey(c, "Person", "Jane", 0, datastore.NewKey(c, "Person", "John", 0, datastore.NewKey(c, "Person", "Jack", 0, nil))), |
| KeySlice: []*datastore.Key{datastore.NewKey(c, "Key", "", 7, nil), datastore.NewKey(c, "Key", "", 8, nil), datastore.NewKey(c, "Key", "", 9, nil)}, |
| KeySliceNil: []*datastore.Key{datastore.NewKey(c, "Number", "", 5, nil), nil, datastore.NewKey(c, "Number", "", 6, nil)}, |
| BlobKey: "fake #3", BKSlice: []appengine.BlobKey{"fake #3.1", "fake #3.2"}, |
| Sub: ivItemSub{Data: "yay #3", Ints: []int{7, 8, 9}}, |
| Subs: []ivItemSubs{ |
| {Data: "sub #3.1", Extra: "xtra #3.1"}, |
| {Data: "sub #3.2", Extra: "xtra #3.2"}, |
| {Data: "sub #3.3", Extra: "xtra #3.3"}}, |
| ZZZV: []ivZZZV{{Data: "None"}, {Key: datastore.NewKey(c, "Fruit", "Banana", 0, nil)}}}} |
| } |
| |
| func getIVItemCopy(g *Goon, index int) *ivItem { |
| // All basic value types are copied easily |
| ivi := ivItems[index] |
| |
| // .. but pointer based types require extra work |
| ivi.SliceTypes.Int = []int{} |
| for _, v := range ivItems[index].SliceTypes.Int { |
| ivi.SliceTypes.Int = append(ivi.SliceTypes.Int, v) |
| } |
| |
| ivi.SliceTypes.Int8 = []int8{} |
| for _, v := range ivItems[index].SliceTypes.Int8 { |
| ivi.SliceTypes.Int8 = append(ivi.SliceTypes.Int8, v) |
| } |
| |
| ivi.SliceTypes.Int16 = []int16{} |
| for _, v := range ivItems[index].SliceTypes.Int16 { |
| ivi.SliceTypes.Int16 = append(ivi.SliceTypes.Int16, v) |
| } |
| |
| ivi.SliceTypes.Int32 = []int32{} |
| for _, v := range ivItems[index].SliceTypes.Int32 { |
| ivi.SliceTypes.Int32 = append(ivi.SliceTypes.Int32, v) |
| } |
| |
| ivi.SliceTypes.Int64 = []int64{} |
| for _, v := range ivItems[index].SliceTypes.Int64 { |
| ivi.SliceTypes.Int64 = append(ivi.SliceTypes.Int64, v) |
| } |
| |
| ivi.SliceTypes.Float32 = []float32{} |
| for _, v := range ivItems[index].SliceTypes.Float32 { |
| ivi.SliceTypes.Float32 = append(ivi.SliceTypes.Float32, v) |
| } |
| |
| ivi.SliceTypes.Float64 = []float64{} |
| for _, v := range ivItems[index].SliceTypes.Float64 { |
| ivi.SliceTypes.Float64 = append(ivi.SliceTypes.Float64, v) |
| } |
| |
| ivi.SliceTypes.Bool = []bool{} |
| for _, v := range ivItems[index].SliceTypes.Bool { |
| ivi.SliceTypes.Bool = append(ivi.SliceTypes.Bool, v) |
| } |
| |
| ivi.SliceTypes.String = []string{} |
| for _, v := range ivItems[index].SliceTypes.String { |
| ivi.SliceTypes.String = append(ivi.SliceTypes.String, v) |
| } |
| |
| ivi.SliceTypes.IntC = []ivItemInt{} |
| for _, v := range ivItems[index].SliceTypes.IntC { |
| ivi.SliceTypes.IntC = append(ivi.SliceTypes.IntC, v) |
| } |
| |
| ivi.SliceTypes.Int8C = []ivItemInt8{} |
| for _, v := range ivItems[index].SliceTypes.Int8C { |
| ivi.SliceTypes.Int8C = append(ivi.SliceTypes.Int8C, v) |
| } |
| |
| ivi.SliceTypes.Int16C = []ivItemInt16{} |
| for _, v := range ivItems[index].SliceTypes.Int16C { |
| ivi.SliceTypes.Int16C = append(ivi.SliceTypes.Int16C, v) |
| } |
| |
| ivi.SliceTypes.Int32C = []ivItemInt32{} |
| for _, v := range ivItems[index].SliceTypes.Int32C { |
| ivi.SliceTypes.Int32C = append(ivi.SliceTypes.Int32C, v) |
| } |
| |
| ivi.SliceTypes.Int64C = []ivItemInt64{} |
| for _, v := range ivItems[index].SliceTypes.Int64C { |
| ivi.SliceTypes.Int64C = append(ivi.SliceTypes.Int64C, v) |
| } |
| |
| ivi.SliceTypes.Float32C = []ivItemFloat32{} |
| for _, v := range ivItems[index].SliceTypes.Float32C { |
| ivi.SliceTypes.Float32C = append(ivi.SliceTypes.Float32C, v) |
| } |
| |
| ivi.SliceTypes.Float64C = []ivItemFloat64{} |
| for _, v := range ivItems[index].SliceTypes.Float64C { |
| ivi.SliceTypes.Float64C = append(ivi.SliceTypes.Float64C, v) |
| } |
| |
| ivi.SliceTypes.BoolC = []ivItemBool{} |
| for _, v := range ivItems[index].SliceTypes.BoolC { |
| ivi.SliceTypes.BoolC = append(ivi.SliceTypes.BoolC, v) |
| } |
| |
| ivi.SliceTypes.StringC = []ivItemString{} |
| for _, v := range ivItems[index].SliceTypes.StringC { |
| ivi.SliceTypes.StringC = append(ivi.SliceTypes.StringC, v) |
| } |
| |
| ivi.SliceTypes.DeepInt = []ivItemDeepInt{} |
| for _, v := range ivItems[index].SliceTypes.DeepInt { |
| ivi.SliceTypes.DeepInt = append(ivi.SliceTypes.DeepInt, v) |
| } |
| |
| ivi.ByteSlice = []byte{} |
| for _, v := range ivItems[index].ByteSlice { |
| ivi.ByteSlice = append(ivi.ByteSlice, v) |
| } |
| |
| ivi.BSSlice = [][]byte{} |
| for _, v := range ivItems[index].BSSlice { |
| vCopy := []byte{} |
| for _, v := range v { |
| vCopy = append(vCopy, v) |
| } |
| ivi.BSSlice = append(ivi.BSSlice, vCopy) |
| } |
| |
| ivi.TimeSlice = []time.Time{} |
| for _, v := range ivItems[index].TimeSlice { |
| ivi.TimeSlice = append(ivi.TimeSlice, v) |
| } |
| |
| ivi.Key = datastore.NewKey(g.Context, ivItems[index].Key.Kind(), ivItems[index].Key.StringID(), ivItems[index].Key.IntID(), nil) |
| |
| ivi.ChildKey = datastore.NewKey(g.Context, ivItems[index].ChildKey.Kind(), ivItems[index].ChildKey.StringID(), ivItems[index].ChildKey.IntID(), |
| datastore.NewKey(g.Context, ivItems[index].ChildKey.Parent().Kind(), ivItems[index].ChildKey.Parent().StringID(), ivItems[index].ChildKey.Parent().IntID(), |
| datastore.NewKey(g.Context, ivItems[index].ChildKey.Parent().Parent().Kind(), ivItems[index].ChildKey.Parent().Parent().StringID(), ivItems[index].ChildKey.Parent().Parent().IntID(), nil))) |
| |
| ivi.KeySlice = []*datastore.Key{} |
| for _, key := range ivItems[index].KeySlice { |
| ivi.KeySlice = append(ivi.KeySlice, datastore.NewKey(g.Context, key.Kind(), key.StringID(), key.IntID(), nil)) |
| } |
| |
| ivi.KeySliceNil = []*datastore.Key{} |
| for _, key := range ivItems[index].KeySliceNil { |
| if key == nil { |
| ivi.KeySliceNil = append(ivi.KeySliceNil, nil) |
| } else { |
| ivi.KeySliceNil = append(ivi.KeySliceNil, datastore.NewKey(g.Context, key.Kind(), key.StringID(), key.IntID(), nil)) |
| } |
| } |
| |
| ivi.BKSlice = []appengine.BlobKey{} |
| for _, v := range ivItems[index].BKSlice { |
| ivi.BKSlice = append(ivi.BKSlice, v) |
| } |
| |
| ivi.Sub = ivItemSub{} |
| ivi.Sub.Data = ivItems[index].Sub.Data |
| for _, v := range ivItems[index].Sub.Ints { |
| ivi.Sub.Ints = append(ivi.Sub.Ints, v) |
| } |
| |
| ivi.Subs = []ivItemSubs{} |
| for _, sub := range ivItems[index].Subs { |
| ivi.Subs = append(ivi.Subs, ivItemSubs{Data: sub.Data, Extra: sub.Extra}) |
| } |
| |
| ivi.ZZZV = []ivZZZV{} |
| for _, zzzv := range ivItems[index].ZZZV { |
| ivi.ZZZV = append(ivi.ZZZV, ivZZZV{Key: zzzv.Key, Data: zzzv.Data}) |
| } |
| |
| return &ivi |
| } |
| |
| func getInputVarietySrc(t *testing.T, g *Goon, ivType int, indices ...int) interface{} { |
| if ivType >= ivTypeTotal { |
| t.Fatalf("Invalid input variety type! %v >= %v", ivType, ivTypeTotal) |
| return nil |
| } |
| |
| var result interface{} |
| |
| switch ivType { |
| case ivTypePtrToSliceOfStructs: |
| s := []ivItem{} |
| for _, index := range indices { |
| s = append(s, *getIVItemCopy(g, index)) |
| } |
| result = &s |
| case ivTypePtrToSliceOfPtrsToStruct: |
| s := []*ivItem{} |
| for _, index := range indices { |
| s = append(s, getIVItemCopy(g, index)) |
| } |
| result = &s |
| case ivTypePtrToSliceOfInterfaces: |
| s := []ivItemI{} |
| for _, index := range indices { |
| s = append(s, getIVItemCopy(g, index)) |
| } |
| result = &s |
| case ivTypeSliceOfStructs: |
| s := []ivItem{} |
| for _, index := range indices { |
| s = append(s, *getIVItemCopy(g, index)) |
| } |
| result = s |
| case ivTypeSliceOfPtrsToStruct: |
| s := []*ivItem{} |
| for _, index := range indices { |
| s = append(s, getIVItemCopy(g, index)) |
| } |
| result = s |
| case ivTypeSliceOfInterfaces: |
| s := []ivItemI{} |
| for _, index := range indices { |
| s = append(s, getIVItemCopy(g, index)) |
| } |
| result = s |
| } |
| |
| return result |
| } |
| |
| func getInputVarietyDst(t *testing.T, ivType int) interface{} { |
| if ivType >= ivTypeTotal { |
| t.Fatalf("Invalid input variety type! %v >= %v", ivType, ivTypeTotal) |
| return nil |
| } |
| |
| var result interface{} |
| |
| switch ivType { |
| case ivTypePtrToSliceOfStructs: |
| result = &[]ivItem{{Id: ivItems[0].Id}, {Id: ivItems[1].Id}, {Id: ivItems[2].Id}} |
| case ivTypePtrToSliceOfPtrsToStruct: |
| result = &[]*ivItem{{Id: ivItems[0].Id}, {Id: ivItems[1].Id}, {Id: ivItems[2].Id}} |
| case ivTypePtrToSliceOfInterfaces: |
| result = &[]ivItemI{&ivItem{Id: ivItems[0].Id}, &ivItem{Id: ivItems[1].Id}, &ivItem{Id: ivItems[2].Id}} |
| case ivTypeSliceOfStructs: |
| result = []ivItem{{Id: ivItems[0].Id}, {Id: ivItems[1].Id}, {Id: ivItems[2].Id}} |
| case ivTypeSliceOfPtrsToStruct: |
| result = []*ivItem{{Id: ivItems[0].Id}, {Id: ivItems[1].Id}, {Id: ivItems[2].Id}} |
| case ivTypeSliceOfInterfaces: |
| result = []ivItemI{&ivItem{Id: ivItems[0].Id}, &ivItem{Id: ivItems[1].Id}, &ivItem{Id: ivItems[2].Id}} |
| } |
| |
| return result |
| } |
| |
| func getPrettyIVMode(ivMode int) string { |
| result := "N/A" |
| |
| switch ivMode { |
| case ivModeDatastore: |
| result = "DS" |
| case ivModeMemcache: |
| result = "MC" |
| case ivModeMemcacheAndDatastore: |
| result = "DS+MC" |
| case ivModeLocalcache: |
| result = "LC" |
| case ivModeLocalcacheAndMemcache: |
| result = "MC+LC" |
| case ivModeLocalcacheAndDatastore: |
| result = "DS+LC" |
| case ivModeLocalcacheAndMemcacheAndDatastore: |
| result = "DS+MC+LC" |
| } |
| |
| return result |
| } |
| |
| func getPrettyIVType(ivType int) string { |
| result := "N/A" |
| |
| switch ivType { |
| case ivTypePtrToSliceOfStructs: |
| result = "*[]S" |
| case ivTypePtrToSliceOfPtrsToStruct: |
| result = "*[]*S" |
| case ivTypePtrToSliceOfInterfaces: |
| result = "*[]I" |
| case ivTypeSliceOfStructs: |
| result = "[]S" |
| case ivTypeSliceOfPtrsToStruct: |
| result = "[]*S" |
| case ivTypeSliceOfInterfaces: |
| result = "[]I" |
| } |
| |
| return result |
| } |
| |
| func ivWipe(t *testing.T, g *Goon, prettyInfo string) { |
| // Make sure the datastore is clear of any previous tests |
| // TODO: Batch this once goon gets more convenient batch delete support |
| for _, ivi := range ivItems { |
| if err := g.Delete(g.Key(ivi)); err != nil { |
| t.Errorf("%s > Unexpected error on delete - %v", prettyInfo, err) |
| } |
| } |
| |
| // Make sure the caches are clear, so any caching is done by our specific test |
| g.FlushLocalCache() |
| memcache.Flush(g.Context) |
| } |
| |
| func ivGetMulti(t *testing.T, g *Goon, ref, dst interface{}, prettyInfo string) error { |
| // Get our data back and make sure it's correct |
| if err := g.GetMulti(dst); err != nil { |
| t.Errorf("%s > Unexpected error on GetMulti - %v", prettyInfo, err) |
| return err |
| } else { |
| dstLen := reflect.Indirect(reflect.ValueOf(dst)).Len() |
| refLen := reflect.Indirect(reflect.ValueOf(ref)).Len() |
| |
| if dstLen != refLen { |
| t.Errorf("%s > Unexpected dst len (%v) doesn't match ref len (%v)", prettyInfo, dstLen, refLen) |
| } else if !reflect.DeepEqual(ref, dst) { |
| t.Errorf("%s > Expected - %v, %v, %v - got %v, %v, %v", prettyInfo, |
| reflect.Indirect(reflect.ValueOf(ref)).Index(0).Interface(), |
| reflect.Indirect(reflect.ValueOf(ref)).Index(1).Interface(), |
| reflect.Indirect(reflect.ValueOf(ref)).Index(2).Interface(), |
| reflect.Indirect(reflect.ValueOf(dst)).Index(0).Interface(), |
| reflect.Indirect(reflect.ValueOf(dst)).Index(1).Interface(), |
| reflect.Indirect(reflect.ValueOf(dst)).Index(2).Interface()) |
| } |
| } |
| |
| return nil |
| } |
| |
| func validateInputVariety(t *testing.T, g *Goon, srcType, dstType, mode int) { |
| if mode >= ivModeTotal { |
| t.Fatalf("Invalid input variety mode! %v >= %v", mode, ivModeTotal) |
| return |
| } |
| |
| // Generate a nice debug info string for clear logging |
| prettyInfo := getPrettyIVType(srcType) + " " + getPrettyIVType(dstType) + " " + getPrettyIVMode(mode) |
| |
| // This function just gets the entities based on a predefined list, helper for cache population |
| loadIVItem := func(indices ...int) { |
| for _, index := range indices { |
| ivi := &ivItem{Id: ivItems[index].Id} |
| if err := g.Get(ivi); err != nil { |
| t.Errorf("%s > Unexpected error on get - %v", prettyInfo, err) |
| } else if !reflect.DeepEqual(ivItems[index], *ivi) { |
| t.Errorf("%s > Expected - %v, got %v", prettyInfo, ivItems[index], *ivi) |
| } |
| } |
| } |
| |
| // Start with a clean slate |
| ivWipe(t, g, prettyInfo) |
| |
| // Generate test data with the specified types |
| src := getInputVarietySrc(t, g, srcType, 0, 1, 2) |
| ref := getInputVarietySrc(t, g, dstType, 0, 1, 2) |
| dst := getInputVarietyDst(t, dstType) |
| |
| // Save our test data |
| if _, err := g.PutMulti(src); err != nil { |
| t.Errorf("%s > Unexpected error on PutMulti - %v", prettyInfo, err) |
| } |
| |
| // Clear the caches, as we're going to precisely set the caches via Get |
| g.FlushLocalCache() |
| memcache.Flush(g.Context) |
| |
| // Set the caches into proper state based on given mode |
| switch mode { |
| case ivModeDatastore: |
| // Caches already clear |
| case ivModeMemcache: |
| loadIVItem(0, 1, 2) // Left in memcache |
| g.FlushLocalCache() |
| case ivModeMemcacheAndDatastore: |
| loadIVItem(0, 1) // Left in memcache |
| g.FlushLocalCache() |
| case ivModeLocalcache: |
| loadIVItem(0, 1, 2) // Left in local cache |
| case ivModeLocalcacheAndMemcache: |
| loadIVItem(0) // Left in memcache |
| g.FlushLocalCache() |
| loadIVItem(1, 2) // Left in local cache |
| case ivModeLocalcacheAndDatastore: |
| loadIVItem(0, 1) // Left in local cache |
| case ivModeLocalcacheAndMemcacheAndDatastore: |
| loadIVItem(0) // Left in memcache |
| g.FlushLocalCache() |
| loadIVItem(1) // Left in local cache |
| } |
| |
| // Get our data back and make sure it's correct |
| ivGetMulti(t, g, ref, dst, prettyInfo) |
| } |
| |
| func validateInputVarietyTXNPut(t *testing.T, g *Goon, srcType, dstType, mode int) { |
| if mode >= ivModeTotal { |
| t.Fatalf("Invalid input variety mode! %v >= %v", mode, ivModeTotal) |
| return |
| } |
| |
| // The following modes are redundant with the current goon transaction implementation |
| switch mode { |
| case ivModeMemcache: |
| return |
| case ivModeMemcacheAndDatastore: |
| return |
| case ivModeLocalcacheAndMemcache: |
| return |
| case ivModeLocalcacheAndMemcacheAndDatastore: |
| return |
| } |
| |
| // Generate a nice debug info string for clear logging |
| prettyInfo := getPrettyIVType(srcType) + " " + getPrettyIVType(dstType) + " " + getPrettyIVMode(mode) + " TXNPut" |
| |
| // Start with a clean slate |
| ivWipe(t, g, prettyInfo) |
| |
| // Generate test data with the specified types |
| src := getInputVarietySrc(t, g, srcType, 0, 1, 2) |
| ref := getInputVarietySrc(t, g, dstType, 0, 1, 2) |
| dst := getInputVarietyDst(t, dstType) |
| |
| // Save our test data |
| if err := g.RunInTransaction(func(tg *Goon) error { |
| _, err := tg.PutMulti(src) |
| return err |
| }, &datastore.TransactionOptions{XG: true}); err != nil { |
| t.Errorf("%s > Unexpected error on PutMulti - %v", prettyInfo, err) |
| } |
| |
| // Set the caches into proper state based on given mode |
| switch mode { |
| case ivModeDatastore: |
| g.FlushLocalCache() |
| memcache.Flush(g.Context) |
| case ivModeLocalcache: |
| // Entities already in local cache |
| case ivModeLocalcacheAndDatastore: |
| g.FlushLocalCache() |
| memcache.Flush(g.Context) |
| |
| subSrc := getInputVarietySrc(t, g, srcType, 0) |
| |
| if err := g.RunInTransaction(func(tg *Goon) error { |
| _, err := tg.PutMulti(subSrc) |
| return err |
| }, &datastore.TransactionOptions{XG: true}); err != nil { |
| t.Errorf("%s > Unexpected error on PutMulti - %v", prettyInfo, err) |
| } |
| } |
| |
| // Get our data back and make sure it's correct |
| ivGetMulti(t, g, ref, dst, prettyInfo) |
| } |
| |
| func validateInputVarietyTXNGet(t *testing.T, g *Goon, srcType, dstType, mode int) { |
| if mode >= ivModeTotal { |
| t.Fatalf("Invalid input variety mode! %v >= %v", mode, ivModeTotal) |
| return |
| } |
| |
| // The following modes are redundant with the current goon transaction implementation |
| switch mode { |
| case ivModeMemcache: |
| return |
| case ivModeMemcacheAndDatastore: |
| return |
| case ivModeLocalcache: |
| return |
| case ivModeLocalcacheAndMemcache: |
| return |
| case ivModeLocalcacheAndDatastore: |
| return |
| case ivModeLocalcacheAndMemcacheAndDatastore: |
| return |
| } |
| |
| // Generate a nice debug info string for clear logging |
| prettyInfo := getPrettyIVType(srcType) + " " + getPrettyIVType(dstType) + " " + getPrettyIVMode(mode) + " TXNGet" |
| |
| // Start with a clean slate |
| ivWipe(t, g, prettyInfo) |
| |
| // Generate test data with the specified types |
| src := getInputVarietySrc(t, g, srcType, 0, 1, 2) |
| ref := getInputVarietySrc(t, g, dstType, 0, 1, 2) |
| dst := getInputVarietyDst(t, dstType) |
| |
| // Save our test data |
| if _, err := g.PutMulti(src); err != nil { |
| t.Errorf("%s > Unexpected error on PutMulti - %v", prettyInfo, err) |
| } |
| |
| // Set the caches into proper state based on given mode |
| // TODO: Instead of clear, fill the caches with invalid data, because we're supposed to always fetch from the datastore |
| switch mode { |
| case ivModeDatastore: |
| g.FlushLocalCache() |
| memcache.Flush(g.Context) |
| } |
| |
| // Get our data back and make sure it's correct |
| if err := g.RunInTransaction(func(tg *Goon) error { |
| return ivGetMulti(t, tg, ref, dst, prettyInfo) |
| }, &datastore.TransactionOptions{XG: true}); err != nil { |
| t.Errorf("%s > Unexpected error on transaction - %v", prettyInfo, err) |
| } |
| } |
| |
| func TestInputVariety(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| g := FromContext(c) |
| |
| initializeIvItems(c) |
| |
| for srcType := 0; srcType < ivTypeTotal; srcType++ { |
| for dstType := 0; dstType < ivTypeTotal; dstType++ { |
| for mode := 0; mode < ivModeTotal; mode++ { |
| validateInputVariety(t, g, srcType, dstType, mode) |
| validateInputVarietyTXNPut(t, g, srcType, dstType, mode) |
| validateInputVarietyTXNGet(t, g, srcType, dstType, mode) |
| } |
| } |
| } |
| } |
| |
| type MigrationA struct { |
| _kind string `goon:"kind,Migration"` |
| Id int64 `datastore:"-" goon:"id"` |
| Number int32 `datastore:"number,noindex"` |
| Word string `datastore:"word,noindex"` |
| Car string `datastore:"car,noindex"` |
| Holiday time.Time `datastore:"holiday,noindex"` |
| α int `datastore:",noindex"` |
| Level MigrationIntA `datastore:"level,noindex"` |
| Floor MigrationIntA `datastore:"floor,noindex"` |
| Sub MigrationSub `datastore:"sub,noindex"` |
| Son MigrationPerson `datastore:"son,noindex"` |
| Daughter MigrationPerson `datastore:"daughter,noindex"` |
| Parents []MigrationPerson `datastore:"parents,noindex"` |
| DeepSlice MigrationDeepA `datastore:"deep,noindex"` |
| ZZs []ZigZag `datastore:"zigzag,noindex"` |
| ZeroKey *datastore.Key `datastore:",noindex"` |
| File []byte |
| } |
| |
| type MigrationSub struct { |
| Data string `datastore:"data,noindex"` |
| Noise []int `datastore:"noise,noindex"` |
| Sub MigrationSubSub `datastore:"sub,noindex"` |
| } |
| |
| type MigrationSubSub struct { |
| Data string `datastore:"data,noindex"` |
| } |
| |
| type MigrationPerson struct { |
| Name string `datastore:"name,noindex"` |
| Age int `datastore:"age,noindex"` |
| } |
| |
| type MigrationDeepA struct { |
| Deep MigrationDeepB `datastore:"deep,noindex"` |
| } |
| |
| type MigrationDeepB struct { |
| Deep MigrationDeepC `datastore:"deep,noindex"` |
| } |
| |
| type MigrationDeepC struct { |
| Slice []int `datastore:"slice,noindex"` |
| } |
| |
| type ZigZag struct { |
| Zig int `datastore:"zig,noindex"` |
| Zag int `datastore:"zag,noindex"` |
| } |
| |
| type ZigZags struct { |
| Zig []int `datastore:"zig,noindex"` |
| Zag []int `datastore:"zag,noindex"` |
| } |
| |
| type MigrationIntA int |
| type MigrationIntB int |
| |
| type MigrationB struct { |
| _kind string `goon:"kind,Migration"` |
| Identification int64 `datastore:"-" goon:"id"` |
| FancyNumber int32 `datastore:"number,noindex"` |
| Slang string `datastore:"word,noindex"` |
| Cars []string `datastore:"car,noindex"` |
| Holidays []time.Time `datastore:"holiday,noindex"` |
| β int `datastore:"α,noindex"` |
| Level MigrationIntB `datastore:"level,noindex"` |
| Floors []MigrationIntB `datastore:"floor,noindex"` |
| Animal string `datastore:"sub.data,noindex"` |
| Music []int `datastore:"sub.noise,noindex"` |
| Flower string `datastore:"sub.sub.data,noindex"` |
| Sons []MigrationPerson `datastore:"son,noindex"` |
| DaughterName string `datastore:"daughter.name,noindex"` |
| DaughterAge int `datastore:"daughter.age,noindex"` |
| OldFolks []MigrationPerson `datastore:"parents,noindex"` |
| FarSlice MigrationDeepA `datastore:"deep,noindex"` |
| ZZs ZigZags `datastore:"zigzag,noindex"` |
| Keys []*datastore.Key `datastore:"ZeroKey,noindex"` |
| Files [][]byte `datastore:"File,noindex"` |
| } |
| |
| func TestMigration(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| g := FromContext(c) |
| |
| // Create & save an entity with the original structure |
| migA := &MigrationA{Id: 1, Number: 123, Word: "rabbit", Car: "BMW", |
| Holiday: time.Now().UTC().Truncate(time.Microsecond), α: 1, Level: 9001, Floor: 5, |
| Sub: MigrationSub{Data: "fox", Noise: []int{1, 2, 3}, Sub: MigrationSubSub{Data: "rose"}}, |
| Son: MigrationPerson{Name: "John", Age: 5}, Daughter: MigrationPerson{Name: "Nancy", Age: 6}, |
| Parents: []MigrationPerson{{Name: "Sven", Age: 56}, {Name: "Sonya", Age: 49}}, |
| DeepSlice: MigrationDeepA{Deep: MigrationDeepB{Deep: MigrationDeepC{Slice: []int{1, 2, 3}}}}, |
| ZZs: []ZigZag{{Zig: 1}, {Zag: 1}}, File: []byte{0xF0, 0x0D}} |
| if _, err := g.Put(migA); err != nil { |
| t.Errorf("Unexpected error on Put: %v", err) |
| } |
| |
| // Clear the local cache, because we want this data in memcache |
| g.FlushLocalCache() |
| |
| // Get it back, so it's in the cache |
| migA = &MigrationA{Id: 1} |
| if err := g.Get(migA); err != nil { |
| t.Errorf("Unexpected error on Get: %v", err) |
| } |
| |
| // Clear the local cache, because it doesn't need to support migration |
| g.FlushLocalCache() |
| |
| // Test whether memcache supports migration |
| verifyMigration(t, g, migA, "MC") |
| |
| // Clear all the caches |
| g.FlushLocalCache() |
| memcache.Flush(c) |
| |
| // Test whether datastore supports migration |
| verifyMigration(t, g, migA, "DS") |
| } |
| |
| func verifyMigration(t *testing.T, g *Goon, migA *MigrationA, debugInfo string) { |
| migB := &MigrationB{Identification: migA.Id} |
| if err := g.Get(migB); err != nil { |
| t.Errorf("%v > Unexpected error on Get: %v", debugInfo, err) |
| } else if migA.Id != migB.Identification { |
| t.Errorf("%v > Ids don't match: %v != %v", debugInfo, migA.Id, migB.Identification) |
| } else if migA.Number != migB.FancyNumber { |
| t.Errorf("%v > Numbers don't match: %v != %v", debugInfo, migA.Number, migB.FancyNumber) |
| } else if migA.Word != migB.Slang { |
| t.Errorf("%v > Words don't match: %v != %v", debugInfo, migA.Word, migB.Slang) |
| } else if len(migB.Cars) != 1 { |
| t.Errorf("%v > Expected 1 car! Got: %v", debugInfo, len(migB.Cars)) |
| } else if migA.Car != migB.Cars[0] { |
| t.Errorf("%v > Cars don't match: %v != %v", debugInfo, migA.Car, migB.Cars[0]) |
| } else if len(migB.Holidays) != 1 { |
| t.Errorf("%v > Expected 1 holiday! Got: %v", debugInfo, len(migB.Holidays)) |
| } else if migA.Holiday != migB.Holidays[0] { |
| t.Errorf("%v > Holidays don't match: %v != %v", debugInfo, migA.Holiday, migB.Holidays[0]) |
| } else if migA.α != migB.β { |
| t.Errorf("%v > Greek doesn't match: %v != %v", debugInfo, migA.α, migB.β) |
| } else if int(migA.Level) != int(migB.Level) { |
| t.Errorf("%v > Level doesn't match: %v != %v", debugInfo, migA.Level, migB.Level) |
| } else if len(migB.Floors) != 1 { |
| t.Errorf("%v > Expected 1 floor! Got: %v", debugInfo, len(migB.Floors)) |
| } else if int(migA.Floor) != int(migB.Floors[0]) { |
| t.Errorf("%v > Floor doesn't match: %v != %v", debugInfo, migA.Floor, migB.Floors[0]) |
| } else if migA.Sub.Data != migB.Animal { |
| t.Errorf("%v > Animal doesn't match: %v != %v", debugInfo, migA.Sub.Data, migB.Animal) |
| } else if !reflect.DeepEqual(migA.Sub.Noise, migB.Music) { |
| t.Errorf("%v > Music doesn't match: %v != %v", debugInfo, migA.Sub.Noise, migB.Music) |
| } else if migA.Sub.Sub.Data != migB.Flower { |
| t.Errorf("%v > Flower doesn't match: %v != %v", debugInfo, migA.Sub.Sub.Data, migB.Flower) |
| } else if len(migB.Sons) != 1 { |
| t.Errorf("%v > Expected 1 son! Got: %v", debugInfo, len(migB.Sons)) |
| } else if migA.Son.Name != migB.Sons[0].Name { |
| t.Errorf("%v > Son names don't match: %v != %v", debugInfo, migA.Son.Name, migB.Sons[0].Name) |
| } else if migA.Son.Age != migB.Sons[0].Age { |
| t.Errorf("%v > Son ages don't match: %v != %v", debugInfo, migA.Son.Age, migB.Sons[0].Age) |
| } else if migA.Daughter.Name != migB.DaughterName { |
| t.Errorf("%v > Daughter names don't match: %v != %v", debugInfo, migA.Daughter.Name, migB.DaughterName) |
| } else if migA.Daughter.Age != migB.DaughterAge { |
| t.Errorf("%v > Daughter ages don't match: %v != %v", debugInfo, migA.Daughter.Age, migB.DaughterAge) |
| } else if !reflect.DeepEqual(migA.Parents, migB.OldFolks) { |
| t.Errorf("%v > Parents don't match: %v != %v", debugInfo, migA.Parents, migB.OldFolks) |
| } else if !reflect.DeepEqual(migA.DeepSlice, migB.FarSlice) { |
| t.Errorf("%v > Deep slice doesn't match: %v != %v", debugInfo, migA.DeepSlice, migB.FarSlice) |
| } else if len(migB.ZZs.Zig) != 2 { |
| t.Errorf("%v > Expected 2 Zigs, got: %v", debugInfo, len(migB.ZZs.Zig)) |
| } else if len(migB.ZZs.Zag) != 2 { |
| t.Errorf("%v > Expected 2 Zags, got: %v", debugInfo, len(migB.ZZs.Zag)) |
| } else if migA.ZZs[0].Zig != migB.ZZs.Zig[0] { |
| t.Errorf("%v > Invalid zig #1: %v != %v", debugInfo, migA.ZZs[0].Zig, migB.ZZs.Zig[0]) |
| } else if migA.ZZs[1].Zig != migB.ZZs.Zig[1] { |
| t.Errorf("%v > Invalid zig #2: %v != %v", debugInfo, migA.ZZs[1].Zig, migB.ZZs.Zig[1]) |
| } else if migA.ZZs[0].Zag != migB.ZZs.Zag[0] { |
| t.Errorf("%v > Invalid zag #1: %v != %v", debugInfo, migA.ZZs[0].Zag, migB.ZZs.Zag[0]) |
| } else if migA.ZZs[1].Zag != migB.ZZs.Zag[1] { |
| t.Errorf("%v > Invalid zag #2: %v != %v", debugInfo, migA.ZZs[1].Zag, migB.ZZs.Zag[1]) |
| } else if len(migB.Keys) != 1 { |
| t.Errorf("%v > Expected 1 keys, got %v", debugInfo, len(migB.Keys)) |
| } else if len(migB.Files) != 1 { |
| t.Errorf("%v > Expected 1 file, got %v", debugInfo, len(migB.Files)) |
| } else if !reflect.DeepEqual(migA.File, migB.Files[0]) { |
| t.Errorf("%v > Files don't match: %v != %v", debugInfo, migA.File, migB.Files[0]) |
| } |
| } |
| |
| func TestTXNRace(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| g := FromContext(c) |
| |
| // Create & store some test data |
| hid := &HasId{Id: 1, Name: "foo"} |
| if _, err := g.Put(hid); err != nil { |
| t.Errorf("Unexpected error on Put %v", err) |
| } |
| |
| // Get this data back, to populate caches |
| if err := g.Get(hid); err != nil { |
| t.Errorf("Unexpected error on Get %v", err) |
| } |
| |
| // Clear the local cache, as we are testing for proper memcache usage |
| g.FlushLocalCache() |
| |
| // Update the test data inside a transction |
| if err := g.RunInTransaction(func(tg *Goon) error { |
| // Get the current data |
| thid := &HasId{Id: 1} |
| if err := tg.Get(thid); err != nil { |
| t.Errorf("Unexpected error on TXN Get %v", err) |
| return err |
| } |
| |
| // Update the data |
| thid.Name = "bar" |
| if _, err := tg.Put(thid); err != nil { |
| t.Errorf("Unexpected error on TXN Put %v", err) |
| return err |
| } |
| |
| // Concurrent request emulation |
| // We are running this inside the transaction block to always get the correct timing for testing. |
| // In the real world, this concurrent request may run in another instance. |
| // The transaction block may contain multiple other operations after the preceding Put(), |
| // allowing for ample time for the concurrent request to run before the transaction is committed. |
| if err := g.Get(hid); err != nil { |
| t.Errorf("Unexpected error on Get %v", err) |
| } else if hid.Name != "foo" { |
| t.Errorf("Expected 'foo', got %v", hid.Name) |
| } |
| |
| // Commit the transaction |
| return nil |
| }, &datastore.TransactionOptions{XG: false}); err != nil { |
| t.Errorf("Unexpected error with TXN - %v", err) |
| } |
| |
| // Clear the local cache, as we are testing for proper memcache usage |
| g.FlushLocalCache() |
| |
| // Get the data back again, to confirm it was changed in the transaction |
| if err := g.Get(hid); err != nil { |
| t.Errorf("Unexpected error on Get %v", err) |
| } else if hid.Name != "bar" { |
| t.Errorf("Expected 'bar', got %v", hid.Name) |
| } |
| |
| // Clear the local cache, as we are testing for proper memcache usage |
| g.FlushLocalCache() |
| |
| // Delete the test data inside a transction |
| if err := g.RunInTransaction(func(tg *Goon) error { |
| thid := &HasId{Id: 1} |
| if err := tg.Delete(tg.Key(thid)); err != nil { |
| t.Errorf("Unexpected error on TXN Delete %v", err) |
| return err |
| } |
| |
| // Concurrent request emulation |
| if err := g.Get(hid); err != nil { |
| t.Errorf("Unexpected error on Get %v", err) |
| } else if hid.Name != "bar" { |
| t.Errorf("Expected 'bar', got %v", hid.Name) |
| } |
| |
| // Commit the transaction |
| return nil |
| }, &datastore.TransactionOptions{XG: false}); err != nil { |
| t.Errorf("Unexpected error with TXN - %v", err) |
| } |
| |
| // Clear the local cache, as we are testing for proper memcache usage |
| g.FlushLocalCache() |
| |
| // Attempt to get the data back again, to confirm it was deleted in the transaction |
| if err := g.Get(hid); err != datastore.ErrNoSuchEntity { |
| t.Errorf("Expected ErrNoSuchEntity, got %v", err) |
| } |
| } |
| |
| func TestNegativeCacheHit(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| g := FromContext(c) |
| |
| hid := &HasId{Id: 1} |
| |
| if err := g.Get(hid); err != datastore.ErrNoSuchEntity { |
| t.Errorf("Expected ErrNoSuchEntity, got %v", err) |
| } |
| |
| // Do a sneaky save straight to the datastore |
| if _, err := datastore.Put(c, datastore.NewKey(c, "HasId", "", 1, nil), &HasId{Id: 1, Name: "one"}); err != nil { |
| t.Errorf("Unexpected error on datastore.Put: %v", err) |
| } |
| |
| // Get the entity again via goon, to make sure we cached the non-existance |
| if err := g.Get(hid); err != datastore.ErrNoSuchEntity { |
| t.Errorf("Expected ErrNoSuchEntity, got %v", err) |
| } |
| } |
| |
| func TestCaches(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| g := FromContext(c) |
| |
| // Put *struct{} |
| phid := &HasId{Name: "cacheFail"} |
| _, err = g.Put(phid) |
| if err != nil { |
| t.Errorf("Unexpected error on put - %v", err) |
| } |
| |
| // fetch *struct{} from cache |
| ghid := &HasId{Id: phid.Id} |
| err = g.Get(ghid) |
| if err != nil { |
| t.Errorf("Unexpected error on get - %v", err) |
| } |
| if !reflect.DeepEqual(phid, ghid) { |
| t.Errorf("Expected - %v, got %v", phid, ghid) |
| } |
| |
| // fetch []struct{} from cache |
| ghids := []HasId{{Id: phid.Id}} |
| err = g.GetMulti(&ghids) |
| if err != nil { |
| t.Errorf("Unexpected error on get - %v", err) |
| } |
| if !reflect.DeepEqual(*phid, ghids[0]) { |
| t.Errorf("Expected - %v, got %v", *phid, ghids[0]) |
| } |
| |
| // Now flush localcache and fetch them again |
| g.FlushLocalCache() |
| // fetch *struct{} from memcache |
| ghid = &HasId{Id: phid.Id} |
| err = g.Get(ghid) |
| if err != nil { |
| t.Errorf("Unexpected error on get - %v", err) |
| } |
| if !reflect.DeepEqual(phid, ghid) { |
| t.Errorf("Expected - %v, got %v", phid, ghid) |
| } |
| |
| g.FlushLocalCache() |
| // fetch []struct{} from memcache |
| ghids = []HasId{{Id: phid.Id}} |
| err = g.GetMulti(&ghids) |
| if err != nil { |
| t.Errorf("Unexpected error on get - %v", err) |
| } |
| if !reflect.DeepEqual(*phid, ghids[0]) { |
| t.Errorf("Expected - %v, got %v", *phid, ghids[0]) |
| } |
| } |
| |
| func TestGoon(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| n := FromContext(c) |
| |
| // Don't want any of these tests to hit the timeout threshold on the devapp server |
| MemcacheGetTimeout = time.Second |
| MemcachePutTimeoutLarge = time.Second |
| MemcachePutTimeoutSmall = time.Second |
| |
| // key tests |
| noid := NoId{} |
| if k, err := n.KeyError(noid); err == nil && !k.Incomplete() { |
| t.Error("expected incomplete on noid") |
| } |
| if n.Key(noid) == nil { |
| t.Error("expected to find a key") |
| } |
| |
| var keyTests = []keyTest{ |
| { |
| HasDefaultKind{}, |
| datastore.NewKey(c, "DefaultKind", "", 0, nil), |
| }, |
| { |
| HasId{Id: 1}, |
| datastore.NewKey(c, "HasId", "", 1, nil), |
| }, |
| { |
| HasKind{Id: 1, Kind: "OtherKind"}, |
| datastore.NewKey(c, "OtherKind", "", 1, nil), |
| }, |
| |
| { |
| HasDefaultKind{Id: 1, Kind: "OtherKind"}, |
| datastore.NewKey(c, "OtherKind", "", 1, nil), |
| }, |
| { |
| HasDefaultKind{Id: 1}, |
| datastore.NewKey(c, "DefaultKind", "", 1, nil), |
| }, |
| { |
| HasString{Id: "new"}, |
| datastore.NewKey(c, "HasString", "new", 0, nil), |
| }, |
| } |
| |
| for _, kt := range keyTests { |
| if k, err := n.KeyError(kt.obj); err != nil { |
| t.Errorf("error: %v", err) |
| } else if !k.Equal(kt.key) { |
| t.Errorf("keys not equal") |
| } |
| } |
| |
| if _, err := n.KeyError(TwoId{IntId: 1, StringId: "1"}); err == nil { |
| t.Errorf("expected key error") |
| } |
| |
| // datastore tests |
| keys, _ := datastore.NewQuery("HasId").KeysOnly().GetAll(c, nil) |
| datastore.DeleteMulti(c, keys) |
| memcache.Flush(c) |
| if err := n.Get(&HasId{Id: 0}); err == nil { |
| t.Errorf("ds: expected error, we're fetching from the datastore on an incomplete key!") |
| } |
| if err := n.Get(&HasId{Id: 1}); err != datastore.ErrNoSuchEntity { |
| t.Errorf("ds: expected no such entity") |
| } |
| // run twice to make sure autocaching works correctly |
| if err := n.Get(&HasId{Id: 1}); err != datastore.ErrNoSuchEntity { |
| t.Errorf("ds: expected no such entity") |
| } |
| es := []*HasId{ |
| {Id: 1, Name: "one"}, |
| {Id: 2, Name: "two"}, |
| } |
| var esk []*datastore.Key |
| for _, e := range es { |
| esk = append(esk, n.Key(e)) |
| } |
| nes := []*HasId{ |
| {Id: 1}, |
| {Id: 2}, |
| } |
| if err := n.GetMulti(es); err == nil { |
| t.Errorf("ds: expected error") |
| } else if !NotFound(err, 0) { |
| t.Errorf("ds: not found error 0") |
| } else if !NotFound(err, 1) { |
| t.Errorf("ds: not found error 1") |
| } else if NotFound(err, 2) { |
| t.Errorf("ds: not found error 2") |
| } |
| |
| if keys, err := n.PutMulti(es); err != nil { |
| t.Errorf("put: unexpected error") |
| } else if len(keys) != len(esk) { |
| t.Errorf("put: got unexpected number of keys") |
| } else { |
| for i, k := range keys { |
| if !k.Equal(esk[i]) { |
| t.Errorf("put: got unexpected keys") |
| } |
| } |
| } |
| if err := n.GetMulti(nes); err != nil { |
| t.Errorf("put: unexpected error") |
| } else if *es[0] != *nes[0] || *es[1] != *nes[1] { |
| t.Errorf("put: bad results") |
| } else { |
| nesk0 := n.Key(nes[0]) |
| if !nesk0.Equal(datastore.NewKey(c, "HasId", "", 1, nil)) { |
| t.Errorf("put: bad key") |
| } |
| nesk1 := n.Key(nes[1]) |
| if !nesk1.Equal(datastore.NewKey(c, "HasId", "", 2, nil)) { |
| t.Errorf("put: bad key") |
| } |
| } |
| if _, err := n.Put(HasId{Id: 3}); err == nil { |
| t.Errorf("put: expected error") |
| } |
| // force partial fetch from memcache and then datastore |
| memcache.Flush(c) |
| if err := n.Get(nes[0]); err != nil { |
| t.Errorf("get: unexpected error") |
| } |
| if err := n.GetMulti(nes); err != nil { |
| t.Errorf("get: unexpected error") |
| } |
| |
| // put a HasId resource, then test pulling it from memory, memcache, and datastore |
| hi := &HasId{Name: "hasid"} // no id given, should be automatically created by the datastore |
| if _, err := n.Put(hi); err != nil { |
| t.Errorf("put: unexpected error - %v", err) |
| } |
| if n.Key(hi) == nil { |
| t.Errorf("key should not be nil") |
| } else if n.Key(hi).Incomplete() { |
| t.Errorf("key should not be incomplete") |
| } |
| |
| hi2 := &HasId{Id: hi.Id} |
| if err := n.Get(hi2); err != nil { |
| t.Errorf("get: unexpected error - %v", err) |
| } |
| if hi2.Name != hi.Name { |
| t.Errorf("Could not fetch HasId object from memory - %#v != %#v, memory=%#v", hi, hi2, n.cache[memkey(n.Key(hi2))]) |
| } |
| |
| hi3 := &HasId{Id: hi.Id} |
| delete(n.cache, memkey(n.Key(hi))) |
| if err := n.Get(hi3); err != nil { |
| t.Errorf("get: unexpected error - %v", err) |
| } |
| if hi3.Name != hi.Name { |
| t.Errorf("Could not fetch HasId object from memory - %#v != %#v", hi, hi3) |
| } |
| |
| hi4 := &HasId{Id: hi.Id} |
| delete(n.cache, memkey(n.Key(hi4))) |
| if memcache.Flush(n.Context) != nil { |
| t.Errorf("Unable to flush memcache") |
| } |
| if err := n.Get(hi4); err != nil { |
| t.Errorf("get: unexpected error - %v", err) |
| } |
| if hi4.Name != hi.Name { |
| t.Errorf("Could not fetch HasId object from datastore- %#v != %#v", hi, hi4) |
| } |
| |
| // Now do the opposite also using hi |
| // Test pulling from local cache and memcache when datastore result is different |
| // Note that this shouldn't happen with real goon usage, |
| // but this tests that goon isn't still pulling from the datastore (or memcache) unnecessarily |
| // hi in datastore Name = hasid |
| hiPull := &HasId{Id: hi.Id} |
| n.cacheLock.Lock() |
| n.cache[memkey(n.Key(hi))].(*HasId).Name = "changedincache" |
| n.cacheLock.Unlock() |
| if err := n.Get(hiPull); err != nil { |
| t.Errorf("get: unexpected error - %v", err) |
| } |
| if hiPull.Name != "changedincache" { |
| t.Errorf("hiPull.Name should be 'changedincache' but got %s", hiPull.Name) |
| } |
| |
| hiPush := &HasId{Id: hi.Id, Name: "changedinmemcache"} |
| n.putMemcache([]interface{}{hiPush}, []byte{1}) |
| n.cacheLock.Lock() |
| delete(n.cache, memkey(n.Key(hi))) |
| n.cacheLock.Unlock() |
| |
| hiPull = &HasId{Id: hi.Id} |
| if err := n.Get(hiPull); err != nil { |
| t.Errorf("get: unexpected error - %v", err) |
| } |
| if hiPull.Name != "changedinmemcache" { |
| t.Errorf("hiPull.Name should be 'changedinmemcache' but got %s", hiPull.Name) |
| } |
| |
| // Since the datastore can't assign a key to a String ID, test to make sure goon stops it from happening |
| hasString := new(HasString) |
| _, err = n.Put(hasString) |
| if err == nil { |
| t.Errorf("Cannot put an incomplete string Id object as the datastore will populate an int64 id instead- %v", hasString) |
| } |
| hasString.Id = "hello" |
| _, err = n.Put(hasString) |
| if err != nil { |
| t.Errorf("Error putting hasString object - %v", hasString) |
| } |
| |
| // Test queries! |
| |
| // Test that zero result queries work properly |
| qiZRes := []QueryItem{} |
| if dskeys, err := n.GetAll(datastore.NewQuery("QueryItem"), &qiZRes); err != nil { |
| t.Errorf("GetAll Zero: unexpected error: %v", err) |
| } else if len(dskeys) != 0 { |
| t.Errorf("GetAll Zero: expected 0 keys, got %v", len(dskeys)) |
| } |
| |
| // Create some entities that we will query for |
| if getKeys, err := n.PutMulti([]*QueryItem{{Id: 1, Data: "one"}, {Id: 2, Data: "two"}}); err != nil { |
| t.Errorf("PutMulti: unexpected error: %v", err) |
| } else { |
| // do a datastore Get by *Key so that data is written to the datstore and indexes generated before subsequent query |
| if err := datastore.GetMulti(c, getKeys, make([]QueryItem, 2)); err != nil { |
| t.Error(err) |
| } |
| } |
| |
| // Clear the local memory cache, because we want to test it being filled correctly by GetAll |
| n.FlushLocalCache() |
| |
| // Get the entity using a slice of structs |
| qiSRes := []QueryItem{} |
| if dskeys, err := n.GetAll(datastore.NewQuery("QueryItem").Filter("data=", "one"), &qiSRes); err != nil { |
| t.Errorf("GetAll SoS: unexpected error: %v", err) |
| } else if len(dskeys) != 1 { |
| t.Errorf("GetAll SoS: expected 1 key, got %v", len(dskeys)) |
| } else if dskeys[0].IntID() != 1 { |
| t.Errorf("GetAll SoS: expected key IntID to be 1, got %v", dskeys[0].IntID()) |
| } else if len(qiSRes) != 1 { |
| t.Errorf("GetAll SoS: expected 1 result, got %v", len(qiSRes)) |
| } else if qiSRes[0].Id != 1 { |
| t.Errorf("GetAll SoS: expected entity id to be 1, got %v", qiSRes[0].Id) |
| } else if qiSRes[0].Data != "one" { |
| t.Errorf("GetAll SoS: expected entity data to be 'one', got '%v'", qiSRes[0].Data) |
| } |
| |
| // Get the entity using normal Get to test local cache (provided the local cache actually got saved) |
| qiS := &QueryItem{Id: 1} |
| if err := n.Get(qiS); err != nil { |
| t.Errorf("Get SoS: unexpected error: %v", err) |
| } else if qiS.Id != 1 { |
| t.Errorf("Get SoS: expected entity id to be 1, got %v", qiS.Id) |
| } else if qiS.Data != "one" { |
| t.Errorf("Get SoS: expected entity data to be 'one', got '%v'", qiS.Data) |
| } |
| |
| // Clear the local memory cache, because we want to test it being filled correctly by GetAll |
| n.FlushLocalCache() |
| |
| // Get the entity using a slice of pointers to struct |
| qiPRes := []*QueryItem{} |
| if dskeys, err := n.GetAll(datastore.NewQuery("QueryItem").Filter("data=", "one"), &qiPRes); err != nil { |
| t.Errorf("GetAll SoPtS: unexpected error: %v", err) |
| } else if len(dskeys) != 1 { |
| t.Errorf("GetAll SoPtS: expected 1 key, got %v", len(dskeys)) |
| } else if dskeys[0].IntID() != 1 { |
| t.Errorf("GetAll SoPtS: expected key IntID to be 1, got %v", dskeys[0].IntID()) |
| } else if len(qiPRes) != 1 { |
| t.Errorf("GetAll SoPtS: expected 1 result, got %v", len(qiPRes)) |
| } else if qiPRes[0].Id != 1 { |
| t.Errorf("GetAll SoPtS: expected entity id to be 1, got %v", qiPRes[0].Id) |
| } else if qiPRes[0].Data != "one" { |
| t.Errorf("GetAll SoPtS: expected entity data to be 'one', got '%v'", qiPRes[0].Data) |
| } |
| |
| // Get the entity using normal Get to test local cache (provided the local cache actually got saved) |
| qiP := &QueryItem{Id: 1} |
| if err := n.Get(qiP); err != nil { |
| t.Errorf("Get SoPtS: unexpected error: %v", err) |
| } else if qiP.Id != 1 { |
| t.Errorf("Get SoPtS: expected entity id to be 1, got %v", qiP.Id) |
| } else if qiP.Data != "one" { |
| t.Errorf("Get SoPtS: expected entity data to be 'one', got '%v'", qiP.Data) |
| } |
| |
| // Clear the local memory cache, because we want to test it being filled correctly by Next |
| n.FlushLocalCache() |
| |
| // Get the entity using an iterator |
| qiIt := n.Run(datastore.NewQuery("QueryItem").Filter("data=", "one")) |
| |
| qiItRes := &QueryItem{} |
| if dskey, err := qiIt.Next(qiItRes); err != nil { |
| t.Errorf("Next: unexpected error: %v", err) |
| } else if dskey.IntID() != 1 { |
| t.Errorf("Next: expected key IntID to be 1, got %v", dskey.IntID()) |
| } else if qiItRes.Id != 1 { |
| t.Errorf("Next: expected entity id to be 1, got %v", qiItRes.Id) |
| } else if qiItRes.Data != "one" { |
| t.Errorf("Next: expected entity data to be 'one', got '%v'", qiItRes.Data) |
| } |
| |
| // Make sure the iterator ends correctly |
| if _, err := qiIt.Next(&QueryItem{}); err != datastore.Done { |
| t.Errorf("Next: expected iterator to end with the error datastore.Done, got %v", err) |
| } |
| |
| // Get the entity using normal Get to test local cache (provided the local cache actually got saved) |
| qiI := &QueryItem{Id: 1} |
| if err := n.Get(qiI); err != nil { |
| t.Errorf("Get Iterator: unexpected error: %v", err) |
| } else if qiI.Id != 1 { |
| t.Errorf("Get Iterator: expected entity id to be 1, got %v", qiI.Id) |
| } else if qiI.Data != "one" { |
| t.Errorf("Get Iterator: expected entity data to be 'one', got '%v'", qiI.Data) |
| } |
| |
| // Clear the local memory cache, because we want to test it not being filled incorrectly when supplying a non-zero slice |
| n.FlushLocalCache() |
| |
| // Get the entity using a non-zero slice of structs |
| qiNZSRes := []QueryItem{{Id: 1, Data: "invalid cache"}} |
| if dskeys, err := n.GetAll(datastore.NewQuery("QueryItem").Filter("data=", "two"), &qiNZSRes); err != nil { |
| t.Errorf("GetAll NZSoS: unexpected error: %v", err) |
| } else if len(dskeys) != 1 { |
| t.Errorf("GetAll NZSoS: expected 1 key, got %v", len(dskeys)) |
| } else if dskeys[0].IntID() != 2 { |
| t.Errorf("GetAll NZSoS: expected key IntID to be 2, got %v", dskeys[0].IntID()) |
| } else if len(qiNZSRes) != 2 { |
| t.Errorf("GetAll NZSoS: expected slice len to be 2, got %v", len(qiNZSRes)) |
| } else if qiNZSRes[0].Id != 1 { |
| t.Errorf("GetAll NZSoS: expected entity id to be 1, got %v", qiNZSRes[0].Id) |
| } else if qiNZSRes[0].Data != "invalid cache" { |
| t.Errorf("GetAll NZSoS: expected entity data to be 'invalid cache', got '%v'", qiNZSRes[0].Data) |
| } else if qiNZSRes[1].Id != 2 { |
| t.Errorf("GetAll NZSoS: expected entity id to be 2, got %v", qiNZSRes[1].Id) |
| } else if qiNZSRes[1].Data != "two" { |
| t.Errorf("GetAll NZSoS: expected entity data to be 'two', got '%v'", qiNZSRes[1].Data) |
| } |
| |
| // Get the entities using normal GetMulti to test local cache |
| qiNZSs := []QueryItem{{Id: 1}, {Id: 2}} |
| if err := n.GetMulti(qiNZSs); err != nil { |
| t.Errorf("GetMulti NZSoS: unexpected error: %v", err) |
| } else if len(qiNZSs) != 2 { |
| t.Errorf("GetMulti NZSoS: expected slice len to be 2, got %v", len(qiNZSs)) |
| } else if qiNZSs[0].Id != 1 { |
| t.Errorf("GetMulti NZSoS: expected entity id to be 1, got %v", qiNZSs[0].Id) |
| } else if qiNZSs[0].Data != "one" { |
| t.Errorf("GetMulti NZSoS: expected entity data to be 'one', got '%v'", qiNZSs[0].Data) |
| } else if qiNZSs[1].Id != 2 { |
| t.Errorf("GetMulti NZSoS: expected entity id to be 2, got %v", qiNZSs[1].Id) |
| } else if qiNZSs[1].Data != "two" { |
| t.Errorf("GetMulti NZSoS: expected entity data to be 'two', got '%v'", qiNZSs[1].Data) |
| } |
| |
| // Clear the local memory cache, because we want to test it not being filled incorrectly when supplying a non-zero slice |
| n.FlushLocalCache() |
| |
| // Get the entity using a non-zero slice of pointers to struct |
| qiNZPRes := []*QueryItem{{Id: 1, Data: "invalid cache"}} |
| if dskeys, err := n.GetAll(datastore.NewQuery("QueryItem").Filter("data=", "two"), &qiNZPRes); err != nil { |
| t.Errorf("GetAll NZSoPtS: unexpected error: %v", err) |
| } else if len(dskeys) != 1 { |
| t.Errorf("GetAll NZSoPtS: expected 1 key, got %v", len(dskeys)) |
| } else if dskeys[0].IntID() != 2 { |
| t.Errorf("GetAll NZSoPtS: expected key IntID to be 2, got %v", dskeys[0].IntID()) |
| } else if len(qiNZPRes) != 2 { |
| t.Errorf("GetAll NZSoPtS: expected slice len to be 2, got %v", len(qiNZPRes)) |
| } else if qiNZPRes[0].Id != 1 { |
| t.Errorf("GetAll NZSoPtS: expected entity id to be 1, got %v", qiNZPRes[0].Id) |
| } else if qiNZPRes[0].Data != "invalid cache" { |
| t.Errorf("GetAll NZSoPtS: expected entity data to be 'invalid cache', got '%v'", qiNZPRes[0].Data) |
| } else if qiNZPRes[1].Id != 2 { |
| t.Errorf("GetAll NZSoPtS: expected entity id to be 2, got %v", qiNZPRes[1].Id) |
| } else if qiNZPRes[1].Data != "two" { |
| t.Errorf("GetAll NZSoPtS: expected entity data to be 'two', got '%v'", qiNZPRes[1].Data) |
| } |
| |
| // Get the entities using normal GetMulti to test local cache |
| qiNZPs := []*QueryItem{{Id: 1}, {Id: 2}} |
| if err := n.GetMulti(qiNZPs); err != nil { |
| t.Errorf("GetMulti NZSoPtS: unexpected error: %v", err) |
| } else if len(qiNZPs) != 2 { |
| t.Errorf("GetMulti NZSoPtS: expected slice len to be 2, got %v", len(qiNZPs)) |
| } else if qiNZPs[0].Id != 1 { |
| t.Errorf("GetMulti NZSoPtS: expected entity id to be 1, got %v", qiNZPs[0].Id) |
| } else if qiNZPs[0].Data != "one" { |
| t.Errorf("GetMulti NZSoPtS: expected entity data to be 'one', got '%v'", qiNZPs[0].Data) |
| } else if qiNZPs[1].Id != 2 { |
| t.Errorf("GetMulti NZSoPtS: expected entity id to be 2, got %v", qiNZPs[1].Id) |
| } else if qiNZPs[1].Data != "two" { |
| t.Errorf("GetMulti NZSoPtS: expected entity data to be 'two', got '%v'", qiNZPs[1].Data) |
| } |
| |
| // Clear the local memory cache, because we want to test it not being filled incorrectly by a keys-only query |
| n.FlushLocalCache() |
| |
| // Test the simplest keys-only query |
| if dskeys, err := n.GetAll(datastore.NewQuery("QueryItem").Filter("data=", "one").KeysOnly(), nil); err != nil { |
| t.Errorf("GetAll KeysOnly: unexpected error: %v", err) |
| } else if len(dskeys) != 1 { |
| t.Errorf("GetAll KeysOnly: expected 1 key, got %v", len(dskeys)) |
| } else if dskeys[0].IntID() != 1 { |
| t.Errorf("GetAll KeysOnly: expected key IntID to be 1, got %v", dskeys[0].IntID()) |
| } |
| |
| // Get the entity using normal Get to test that the local cache wasn't filled with incomplete data |
| qiKO := &QueryItem{Id: 1} |
| if err := n.Get(qiKO); err != nil { |
| t.Errorf("Get KeysOnly: unexpected error: %v", err) |
| } else if qiKO.Id != 1 { |
| t.Errorf("Get KeysOnly: expected entity id to be 1, got %v", qiKO.Id) |
| } else if qiKO.Data != "one" { |
| t.Errorf("Get KeysOnly: expected entity data to be 'one', got '%v'", qiKO.Data) |
| } |
| |
| // Clear the local memory cache, because we want to test it not being filled incorrectly by a keys-only query |
| n.FlushLocalCache() |
| |
| // Test the keys-only query with slice of structs |
| qiKOSRes := []QueryItem{} |
| if dskeys, err := n.GetAll(datastore.NewQuery("QueryItem").Filter("data=", "one").KeysOnly(), &qiKOSRes); err != nil { |
| t.Errorf("GetAll KeysOnly SoS: unexpected error: %v", err) |
| } else if len(dskeys) != 1 { |
| t.Errorf("GetAll KeysOnly SoS: expected 1 key, got %v", len(dskeys)) |
| } else if dskeys[0].IntID() != 1 { |
| t.Errorf("GetAll KeysOnly SoS: expected key IntID to be 1, got %v", dskeys[0].IntID()) |
| } else if len(qiKOSRes) != 1 { |
| t.Errorf("GetAll KeysOnly SoS: expected 1 result, got %v", len(qiKOSRes)) |
| } else if k := reflect.TypeOf(qiKOSRes[0]).Kind(); k != reflect.Struct { |
| t.Errorf("GetAll KeysOnly SoS: expected struct, got %v", k) |
| } else if qiKOSRes[0].Id != 1 { |
| t.Errorf("GetAll KeysOnly SoS: expected entity id to be 1, got %v", qiKOSRes[0].Id) |
| } else if qiKOSRes[0].Data != "" { |
| t.Errorf("GetAll KeysOnly SoS: expected entity data to be empty, got '%v'", qiKOSRes[0].Data) |
| } |
| |
| // Get the entity using normal Get to test that the local cache wasn't filled with incomplete data |
| if err := n.GetMulti(qiKOSRes); err != nil { |
| t.Errorf("Get KeysOnly SoS: unexpected error: %v", err) |
| } else if qiKOSRes[0].Id != 1 { |
| t.Errorf("Get KeysOnly SoS: expected entity id to be 1, got %v", qiKOSRes[0].Id) |
| } else if qiKOSRes[0].Data != "one" { |
| t.Errorf("Get KeysOnly SoS: expected entity data to be 'one', got '%v'", qiKOSRes[0].Data) |
| } |
| |
| // Clear the local memory cache, because we want to test it not being filled incorrectly by a keys-only query |
| n.FlushLocalCache() |
| |
| // Test the keys-only query with slice of pointers to struct |
| qiKOPRes := []*QueryItem{} |
| if dskeys, err := n.GetAll(datastore.NewQuery("QueryItem").Filter("data=", "one").KeysOnly(), &qiKOPRes); err != nil { |
| t.Errorf("GetAll KeysOnly SoPtS: unexpected error: %v", err) |
| } else if len(dskeys) != 1 { |
| t.Errorf("GetAll KeysOnly SoPtS: expected 1 key, got %v", len(dskeys)) |
| } else if dskeys[0].IntID() != 1 { |
| t.Errorf("GetAll KeysOnly SoPtS: expected key IntID to be 1, got %v", dskeys[0].IntID()) |
| } else if len(qiKOPRes) != 1 { |
| t.Errorf("GetAll KeysOnly SoPtS: expected 1 result, got %v", len(qiKOPRes)) |
| } else if k := reflect.TypeOf(qiKOPRes[0]).Kind(); k != reflect.Ptr { |
| t.Errorf("GetAll KeysOnly SoPtS: expected pointer, got %v", k) |
| } else if qiKOPRes[0].Id != 1 { |
| t.Errorf("GetAll KeysOnly SoPtS: expected entity id to be 1, got %v", qiKOPRes[0].Id) |
| } else if qiKOPRes[0].Data != "" { |
| t.Errorf("GetAll KeysOnly SoPtS: expected entity data to be empty, got '%v'", qiKOPRes[0].Data) |
| } |
| |
| // Get the entity using normal Get to test that the local cache wasn't filled with incomplete data |
| if err := n.GetMulti(qiKOPRes); err != nil { |
| t.Errorf("Get KeysOnly SoPtS: unexpected error: %v", err) |
| } else if qiKOPRes[0].Id != 1 { |
| t.Errorf("Get KeysOnly SoPtS: expected entity id to be 1, got %v", qiKOPRes[0].Id) |
| } else if qiKOPRes[0].Data != "one" { |
| t.Errorf("Get KeysOnly SoPtS: expected entity data to be 'one', got '%v'", qiKOPRes[0].Data) |
| } |
| |
| // Clear the local memory cache, because we want to test it not being filled incorrectly when supplying a non-zero slice |
| n.FlushLocalCache() |
| |
| // Test the keys-only query with non-zero slice of structs |
| qiKONZSRes := []QueryItem{{Id: 1, Data: "invalid cache"}} |
| if dskeys, err := n.GetAll(datastore.NewQuery("QueryItem").Filter("data=", "two").KeysOnly(), &qiKONZSRes); err != nil { |
| t.Errorf("GetAll KeysOnly NZSoS: unexpected error: %v", err) |
| } else if len(dskeys) != 1 { |
| t.Errorf("GetAll KeysOnly NZSoS: expected 1 key, got %v", len(dskeys)) |
| } else if dskeys[0].IntID() != 2 { |
| t.Errorf("GetAll KeysOnly NZSoS: expected key IntID to be 2, got %v", dskeys[0].IntID()) |
| } else if len(qiKONZSRes) != 2 { |
| t.Errorf("GetAll KeysOnly NZSoS: expected slice len to be 2, got %v", len(qiKONZSRes)) |
| } else if qiKONZSRes[0].Id != 1 { |
| t.Errorf("GetAll KeysOnly NZSoS: expected entity id to be 1, got %v", qiKONZSRes[0].Id) |
| } else if qiKONZSRes[0].Data != "invalid cache" { |
| t.Errorf("GetAll KeysOnly NZSoS: expected entity data to be 'invalid cache', got '%v'", qiKONZSRes[0].Data) |
| } else if k := reflect.TypeOf(qiKONZSRes[1]).Kind(); k != reflect.Struct { |
| t.Errorf("GetAll KeysOnly NZSoS: expected struct, got %v", k) |
| } else if qiKONZSRes[1].Id != 2 { |
| t.Errorf("GetAll KeysOnly NZSoS: expected entity id to be 2, got %v", qiKONZSRes[1].Id) |
| } else if qiKONZSRes[1].Data != "" { |
| t.Errorf("GetAll KeysOnly NZSoS: expected entity data to be empty, got '%v'", qiKONZSRes[1].Data) |
| } |
| |
| // Get the entities using normal GetMulti to test local cache |
| if err := n.GetMulti(qiKONZSRes); err != nil { |
| t.Errorf("GetMulti NZSoS: unexpected error: %v", err) |
| } else if len(qiKONZSRes) != 2 { |
| t.Errorf("GetMulti NZSoS: expected slice len to be 2, got %v", len(qiKONZSRes)) |
| } else if qiKONZSRes[0].Id != 1 { |
| t.Errorf("GetMulti NZSoS: expected entity id to be 1, got %v", qiKONZSRes[0].Id) |
| } else if qiKONZSRes[0].Data != "one" { |
| t.Errorf("GetMulti NZSoS: expected entity data to be 'one', got '%v'", qiKONZSRes[0].Data) |
| } else if qiKONZSRes[1].Id != 2 { |
| t.Errorf("GetMulti NZSoS: expected entity id to be 2, got %v", qiKONZSRes[1].Id) |
| } else if qiKONZSRes[1].Data != "two" { |
| t.Errorf("GetMulti NZSoS: expected entity data to be 'two', got '%v'", qiKONZSRes[1].Data) |
| } |
| |
| // Clear the local memory cache, because we want to test it not being filled incorrectly when supplying a non-zero slice |
| n.FlushLocalCache() |
| |
| // Test the keys-only query with non-zero slice of pointers to struct |
| qiKONZPRes := []*QueryItem{{Id: 1, Data: "invalid cache"}} |
| if dskeys, err := n.GetAll(datastore.NewQuery("QueryItem").Filter("data=", "two").KeysOnly(), &qiKONZPRes); err != nil { |
| t.Errorf("GetAll KeysOnly NZSoPtS: unexpected error: %v", err) |
| } else if len(dskeys) != 1 { |
| t.Errorf("GetAll KeysOnly NZSoPtS: expected 1 key, got %v", len(dskeys)) |
| } else if dskeys[0].IntID() != 2 { |
| t.Errorf("GetAll KeysOnly NZSoPtS: expected key IntID to be 2, got %v", dskeys[0].IntID()) |
| } else if len(qiKONZPRes) != 2 { |
| t.Errorf("GetAll KeysOnly NZSoPtS: expected slice len to be 2, got %v", len(qiKONZPRes)) |
| } else if qiKONZPRes[0].Id != 1 { |
| t.Errorf("GetAll KeysOnly NZSoPtS: expected entity id to be 1, got %v", qiKONZPRes[0].Id) |
| } else if qiKONZPRes[0].Data != "invalid cache" { |
| t.Errorf("GetAll KeysOnly NZSoPtS: expected entity data to be 'invalid cache', got '%v'", qiKONZPRes[0].Data) |
| } else if k := reflect.TypeOf(qiKONZPRes[1]).Kind(); k != reflect.Ptr { |
| t.Errorf("GetAll KeysOnly NZSoPtS: expected pointer, got %v", k) |
| } else if qiKONZPRes[1].Id != 2 { |
| t.Errorf("GetAll KeysOnly NZSoPtS: expected entity id to be 2, got %v", qiKONZPRes[1].Id) |
| } else if qiKONZPRes[1].Data != "" { |
| t.Errorf("GetAll KeysOnly NZSoPtS: expected entity data to be empty, got '%v'", qiKONZPRes[1].Data) |
| } |
| |
| // Get the entities using normal GetMulti to test local cache |
| if err := n.GetMulti(qiKONZPRes); err != nil { |
| t.Errorf("GetMulti NZSoPtS: unexpected error: %v", err) |
| } else if len(qiKONZPRes) != 2 { |
| t.Errorf("GetMulti NZSoPtS: expected slice len to be 2, got %v", len(qiKONZPRes)) |
| } else if qiKONZPRes[0].Id != 1 { |
| t.Errorf("GetMulti NZSoPtS: expected entity id to be 1, got %v", qiKONZPRes[0].Id) |
| } else if qiKONZPRes[0].Data != "one" { |
| t.Errorf("GetMulti NZSoPtS: expected entity data to be 'one', got '%v'", qiKONZPRes[0].Data) |
| } else if qiKONZPRes[1].Id != 2 { |
| t.Errorf("GetMulti NZSoPtS: expected entity id to be 2, got %v", qiKONZPRes[1].Id) |
| } else if qiKONZPRes[1].Data != "two" { |
| t.Errorf("GetMulti NZSoPtS: expected entity data to be 'two', got '%v'", qiKONZPRes[1].Data) |
| } |
| } |
| |
| type keyTest struct { |
| obj interface{} |
| key *datastore.Key |
| } |
| |
| type NoId struct { |
| } |
| |
| type HasId struct { |
| Id int64 `datastore:"-" goon:"id"` |
| Name string |
| } |
| |
| type HasKind struct { |
| Id int64 `datastore:"-" goon:"id"` |
| Kind string `datastore:"-" goon:"kind"` |
| Name string |
| } |
| |
| type HasDefaultKind struct { |
| Id int64 `datastore:"-" goon:"id"` |
| Kind string `datastore:"-" goon:"kind,DefaultKind"` |
| Name string |
| } |
| |
| type QueryItem struct { |
| Id int64 `datastore:"-" goon:"id"` |
| Data string `datastore:"data"` |
| } |
| |
| type HasString struct { |
| Id string `datastore:"-" goon:"id"` |
| } |
| |
| type TwoId struct { |
| IntId int64 `goon:"id"` |
| StringId string `goon:"id"` |
| } |
| |
| type PutGet struct { |
| ID int64 `datastore:"-" goon:"id"` |
| Value int32 |
| } |
| |
| // Commenting out for issue https://code.google.com/p/googleappengine/issues/detail?id=10493 |
| //func TestMemcachePutTimeout(t *testing.T) { |
| // c, done, err := aetest.NewContext() |
| // if err != nil { |
| // t.Fatalf("Could not start aetest - %v", err) |
| // } |
| // defer done() |
| // g := FromContext(c) |
| // MemcachePutTimeoutSmall = time.Second |
| // // put a HasId resource, then test pulling it from memory, memcache, and datastore |
| // hi := &HasId{Name: "hasid"} // no id given, should be automatically created by the datastore |
| // if _, err := g.Put(hi); err != nil { |
| // t.Errorf("put: unexpected error - %v", err) |
| // } |
| |
| // MemcachePutTimeoutSmall = 0 |
| // MemcacheGetTimeout = 0 |
| // if err := g.putMemcache([]interface{}{hi}); !appengine.IsTimeoutError(err) { |
| // t.Errorf("Request should timeout - err = %v", err) |
| // } |
| // MemcachePutTimeoutSmall = time.Second |
| // MemcachePutTimeoutThreshold = 0 |
| // MemcachePutTimeoutLarge = 0 |
| // if err := g.putMemcache([]interface{}{hi}); !appengine.IsTimeoutError(err) { |
| // t.Errorf("Request should timeout - err = %v", err) |
| // } |
| |
| // MemcachePutTimeoutLarge = time.Second |
| // if err := g.putMemcache([]interface{}{hi}); err != nil { |
| // t.Errorf("putMemcache: unexpected error - %v", err) |
| // } |
| |
| // g.FlushLocalCache() |
| // memcache.Flush(c) |
| // // time out Get |
| // MemcacheGetTimeout = 0 |
| // // time out Put too |
| // MemcachePutTimeoutSmall = 0 |
| // MemcachePutTimeoutThreshold = 0 |
| // MemcachePutTimeoutLarge = 0 |
| // hiResult := &HasId{Id: hi.Id} |
| // if err := g.Get(hiResult); err != nil { |
| // t.Errorf("Request should not timeout cause we'll fetch from the datastore but got error %v", err) |
| // // Put timing out should also error, but it won't be returned here, just logged |
| // } |
| // if !reflect.DeepEqual(hi, hiResult) { |
| // t.Errorf("Fetched object isn't accurate - want %v, fetched %v", hi, hiResult) |
| // } |
| |
| // hiResult = &HasId{Id: hi.Id} |
| // g.FlushLocalCache() |
| // MemcacheGetTimeout = time.Second |
| // if err := g.Get(hiResult); err != nil { |
| // t.Errorf("Request should not timeout cause we'll fetch from memcache successfully but got error %v", err) |
| // } |
| // if !reflect.DeepEqual(hi, hiResult) { |
| // t.Errorf("Fetched object isn't accurate - want %v, fetched %v", hi, hiResult) |
| // } |
| //} |
| |
| // This test won't fail but if run with -race flag, it will show known race conditions |
| // Using multiple goroutines per http request is recommended here: |
| // http://talks.golang.org/2013/highperf.slide#22 |
| func TestRace(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| g := FromContext(c) |
| |
| var hasIdSlice []*HasId |
| for x := 1; x <= 4000; x++ { |
| hasIdSlice = append(hasIdSlice, &HasId{Id: int64(x), Name: "Race"}) |
| } |
| _, err = g.PutMulti(hasIdSlice) |
| if err != nil { |
| t.Fatalf("Could not put Race entities - %v", err) |
| } |
| hasIdSlice = hasIdSlice[:0] |
| for x := 1; x <= 4000; x++ { |
| hasIdSlice = append(hasIdSlice, &HasId{Id: int64(x)}) |
| } |
| var wg sync.WaitGroup |
| wg.Add(3) |
| go func() { |
| err := g.Get(hasIdSlice[0]) |
| if err != nil { |
| t.Errorf("Error fetching id #0 - %v", err) |
| } |
| wg.Done() |
| }() |
| go func() { |
| err := g.GetMulti(hasIdSlice[1:1500]) |
| if err != nil { |
| t.Errorf("Error fetching ids 1 through 1499 - %v", err) |
| } |
| wg.Done() |
| }() |
| go func() { |
| err := g.GetMulti(hasIdSlice[1500:]) |
| if err != nil { |
| t.Errorf("Error fetching id #1500 through 4000 - %v", err) |
| } |
| wg.Done() |
| }() |
| wg.Wait() |
| for x, hi := range hasIdSlice { |
| if hi.Name != "Race" { |
| t.Errorf("Object #%d not fetched properly, fetched instead - %v", x, hi) |
| } |
| } |
| } |
| |
| func TestPutGet(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| g := FromContext(c) |
| |
| key, err := g.Put(&PutGet{ID: 12, Value: 15}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if key.IntID() != 12 { |
| t.Fatal("ID should be 12 but is", key.IntID()) |
| } |
| |
| // Datastore Get |
| dsPutGet := &PutGet{} |
| err = datastore.Get(c, |
| datastore.NewKey(c, "PutGet", "", 12, nil), dsPutGet) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if dsPutGet.Value != 15 { |
| t.Fatal("dsPutGet.Value should be 15 but is", |
| dsPutGet.Value) |
| } |
| |
| // Goon Get |
| goonPutGet := &PutGet{ID: 12} |
| err = g.Get(goonPutGet) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if goonPutGet.ID != 12 { |
| t.Fatal("goonPutGet.ID should be 12 but is", goonPutGet.ID) |
| } |
| if goonPutGet.Value != 15 { |
| t.Fatal("goonPutGet.Value should be 15 but is", |
| goonPutGet.Value) |
| } |
| } |
| |
| func prefixKindName(src interface{}) string { |
| return "prefix." + DefaultKindName(src) |
| } |
| |
| func TestCustomKindName(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| g := FromContext(c) |
| |
| hi := HasId{Name: "Foo"} |
| |
| //gate |
| if kind := g.Kind(hi); kind != "HasId" { |
| t.Fatal("HasId King should not have a prefix, but instead is, ", kind) |
| } |
| |
| g.KindNameResolver = prefixKindName |
| |
| if kind := g.Kind(hi); kind != "prefix.HasId" { |
| t.Fatal("HasId King should have a prefix, but instead is, ", kind) |
| } |
| |
| _, err = g.Put(&hi) |
| |
| if err != nil { |
| t.Fatal("Should be able to put a record: ", err) |
| } |
| |
| // Due to eventual consistency, we need to wait a bit. The old aetest package |
| // had an option to enable strong consistency that has been removed. This |
| // is currently the best way I'm aware of to do this. |
| time.Sleep(time.Second) |
| reget1 := []HasId{} |
| query := datastore.NewQuery("prefix.HasId") |
| query.GetAll(c, ®et1) |
| if len(reget1) != 1 { |
| t.Fatal("Should have 1 record stored in datastore ", reget1) |
| } |
| if reget1[0].Name != "Foo" { |
| t.Fatal("Name should be Foo ", reget1[0].Name) |
| } |
| } |
| |
| func TestMultis(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| n := FromContext(c) |
| |
| testAmounts := []int{1, 999, 1000, 1001, 1999, 2000, 2001, 2510} |
| for _, x := range testAmounts { |
| memcache.Flush(c) |
| objects := make([]*HasId, x) |
| for y := 0; y < x; y++ { |
| objects[y] = &HasId{Id: int64(y + 1)} |
| } |
| if _, err := n.PutMulti(objects); err != nil { |
| t.Fatalf("Error in PutMulti for %d objects - %v", x, err) |
| } |
| n.FlushLocalCache() // Put just put them in the local cache, get rid of it before doing the Get |
| if err := n.GetMulti(objects); err != nil { |
| t.Fatalf("Error in GetMulti - %v", err) |
| } |
| } |
| |
| // do it again, but only write numbers divisible by 100 |
| for _, x := range testAmounts { |
| memcache.Flush(c) |
| getobjects := make([]*HasId, 0, x) |
| putobjects := make([]*HasId, 0, x/100+1) |
| keys := make([]*datastore.Key, x) |
| for y := 0; y < x; y++ { |
| keys[y] = datastore.NewKey(c, "HasId", "", int64(y+1), nil) |
| } |
| if err := n.DeleteMulti(keys); err != nil { |
| t.Fatalf("Error deleting keys - %v", err) |
| } |
| for y := 0; y < x; y++ { |
| getobjects = append(getobjects, &HasId{Id: int64(y + 1)}) |
| if y%100 == 0 { |
| putobjects = append(putobjects, &HasId{Id: int64(y + 1)}) |
| } |
| } |
| |
| _, err := n.PutMulti(putobjects) |
| if err != nil { |
| t.Fatalf("Error in PutMulti for %d objects - %v", x, err) |
| } |
| n.FlushLocalCache() // Put just put them in the local cache, get rid of it before doing the Get |
| err = n.GetMulti(getobjects) |
| if err == nil && x != 1 { // a test size of 1 has no objects divisible by 100, so there's no cache miss to return |
| t.Errorf("Should be receiving a multiError on %d objects, but got no errors", x) |
| continue |
| } |
| |
| merr, ok := err.(appengine.MultiError) |
| if ok { |
| if len(merr) != len(getobjects) { |
| t.Errorf("Should have received a MultiError object of length %d but got length %d instead", len(getobjects), len(merr)) |
| } |
| for x := range merr { |
| switch { // record good conditions, fail in other conditions |
| case merr[x] == nil && x%100 == 0: |
| case merr[x] != nil && x%100 != 0: |
| default: |
| t.Errorf("Found bad condition on object[%d] and error %v", x+1, merr[x]) |
| } |
| } |
| } else if x != 1 { |
| t.Errorf("Did not return a multierror on fetch but when fetching %d objects, received - %v", x, merr) |
| } |
| } |
| } |
| |
| type root struct { |
| Id int64 `datastore:"-" goon:"id"` |
| Data int |
| } |
| |
| type normalChild struct { |
| Id int64 `datastore:"-" goon:"id"` |
| Parent *datastore.Key `datastore:"-" goon:"parent"` |
| Data int |
| } |
| |
| type coolKey *datastore.Key |
| |
| type derivedChild struct { |
| Id int64 `datastore:"-" goon:"id"` |
| Parent coolKey `datastore:"-" goon:"parent"` |
| Data int |
| } |
| |
| func TestParents(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| n := FromContext(c) |
| |
| r := &root{1, 10} |
| rootKey, err := n.Put(r) |
| if err != nil { |
| t.Fatalf("couldn't Put(%+v)", r) |
| } |
| |
| // Put exercises both get and set, since Id is uninitialized |
| nc := &normalChild{0, rootKey, 20} |
| nk, err := n.Put(nc) |
| if err != nil { |
| t.Fatalf("couldn't Put(%+v)", nc) |
| } |
| if nc.Parent == rootKey { |
| t.Fatalf("derived parent key pointer value didn't change") |
| } |
| if !(*datastore.Key)(nc.Parent).Equal(rootKey) { |
| t.Fatalf("parent of key not equal '%s' v '%s'! ", (*datastore.Key)(nc.Parent), rootKey) |
| } |
| if !nk.Parent().Equal(rootKey) { |
| t.Fatalf("parent of key not equal '%s' v '%s'! ", nk, rootKey) |
| } |
| |
| dc := &derivedChild{0, (coolKey)(rootKey), 12} |
| dk, err := n.Put(dc) |
| if err != nil { |
| t.Fatalf("couldn't Put(%+v)", dc) |
| } |
| if dc.Parent == rootKey { |
| t.Fatalf("derived parent key pointer value didn't change") |
| } |
| if !(*datastore.Key)(dc.Parent).Equal(rootKey) { |
| t.Fatalf("parent of key not equal '%s' v '%s'! ", (*datastore.Key)(dc.Parent), rootKey) |
| } |
| if !dk.Parent().Equal(rootKey) { |
| t.Fatalf("parent of key not equal '%s' v '%s'! ", dk, rootKey) |
| } |
| } |
| |
| type ContainerStruct struct { |
| Id string `datastore:"-" goon:"id"` |
| embeddedStruct |
| } |
| |
| type embeddedStruct struct { |
| X int |
| y int |
| } |
| |
| func TestEmbeddedStruct(t *testing.T) { |
| c, done, err := aetest.NewContext() |
| if err != nil { |
| t.Fatalf("Could not start aetest - %v", err) |
| } |
| defer done() |
| g := FromContext(c) |
| |
| // Store some data with an embedded unexported struct |
| pcs := &ContainerStruct{Id: "foo"} |
| pcs.X = 1 |
| pcs.y = 2 |
| _, err = g.Put(pcs) |
| if err != nil { |
| t.Errorf("Unexpected error on put - %v", err) |
| } |
| |
| // First run fetches from the datastore (as Put() only caches to the local cache) |
| // Second run fetches from memcache (as our first run here called Get() which caches into memcache) |
| for i := 1; i <= 2; i++ { |
| // Clear the local cache |
| g.FlushLocalCache() |
| |
| // Fetch it and confirm the values |
| gcs := &ContainerStruct{Id: pcs.Id} |
| err = g.Get(gcs) |
| if err != nil { |
| t.Errorf("#%v - Unexpected error on get - %v", i, err) |
| } |
| // The exported field must have the correct value |
| if gcs.X != pcs.X { |
| t.Errorf("#%v - Expected - %v, got %v", i, pcs.X, gcs.X) |
| } |
| // The unexported field must be zero-valued |
| if gcs.y != 0 { |
| t.Errorf("#%v - Expected - %v, got %v", i, 0, gcs.y) |
| } |
| } |
| } |