| Index: service/datastore/datastore_test.go
|
| diff --git a/service/datastore/datastore_test.go b/service/datastore/datastore_test.go
|
| index c24efb33e37609becdc1900da71888e4b47bfffd..47d0d26e7a24ca11efffff373417524471a161ba 100644
|
| --- a/service/datastore/datastore_test.go
|
| +++ b/service/datastore/datastore_test.go
|
| @@ -926,3 +926,296 @@ func TestRun(t *testing.T) {
|
| })
|
| })
|
| }
|
| +
|
| +type fixedDataDatastore struct {
|
| + RawInterface
|
| +
|
| + data map[string]PropertyMap
|
| +}
|
| +
|
| +func (d *fixedDataDatastore) GetMulti(keys []*Key, _ MultiMetaGetter, cb GetMultiCB) error {
|
| + for _, k := range keys {
|
| + data, ok := d.data[k.String()]
|
| + if ok {
|
| + cb(data, nil)
|
| + } else {
|
| + cb(nil, ErrNoSuchEntity)
|
| + }
|
| + }
|
| + return nil
|
| +}
|
| +
|
| +func (d *fixedDataDatastore) PutMulti(keys []*Key, vals []PropertyMap, cb PutMultiCB) error {
|
| + if d.data == nil {
|
| + d.data = make(map[string]PropertyMap, len(keys))
|
| + }
|
| + for i, k := range keys {
|
| + if k.Incomplete() {
|
| + panic("key is incomplete, don't do that.")
|
| + }
|
| + d.data[k.String()], _ = vals[i].Save(false)
|
| + cb(k, nil)
|
| + }
|
| + return nil
|
| +}
|
| +
|
| +func TestSchemaChange(t *testing.T) {
|
| + t.Parallel()
|
| +
|
| + Convey("Test changing schemas", t, func() {
|
| + fds := fixedDataDatastore{}
|
| + ds := &datastoreImpl{&fds, "", ""}
|
| +
|
| + Convey("Can add fields", func() {
|
| + initial := PropertyMap{
|
| + "$key": {mpNI(ds.MakeKey("Val", 10))},
|
| + "Val": {mp(100)},
|
| + }
|
| + So(ds.Put(initial), ShouldBeNil)
|
| +
|
| + type Val struct {
|
| + ID int64 `gae:"$id"`
|
| +
|
| + Val int64
|
| + TwoVal int64 // whoa, TWO vals! amazing
|
| + }
|
| + tv := &Val{ID: 10, TwoVal: 2}
|
| + So(ds.Get(tv), ShouldBeNil)
|
| + So(tv, ShouldResemble, &Val{ID: 10, Val: 100, TwoVal: 2})
|
| + })
|
| +
|
| + Convey("Removing fields", func() {
|
| + initial := PropertyMap{
|
| + "$key": {mpNI(ds.MakeKey("Val", 10))},
|
| + "Val": {mp(100)},
|
| + "TwoVal": {mp(200)},
|
| + }
|
| + So(ds.Put(initial), ShouldBeNil)
|
| +
|
| + Convey("is normally an error", func() {
|
| + type Val struct {
|
| + ID int64 `gae:"$id"`
|
| +
|
| + Val int64
|
| + }
|
| + tv := &Val{ID: 10}
|
| + So(ds.Get(tv), ShouldErrLike,
|
| + `gae: cannot load field "TwoVal" into a "datastore.Val`)
|
| + So(tv, ShouldResemble, &Val{ID: 10, Val: 100})
|
| + })
|
| +
|
| + Convey("Unless you have an ,extra field!", func() {
|
| + type Val struct {
|
| + ID int64 `gae:"$id"`
|
| +
|
| + Val int64
|
| + Extra PropertyMap `gae:",extra"`
|
| + }
|
| + tv := &Val{ID: 10}
|
| + So(ds.Get(tv), ShouldBeNil)
|
| + So(tv, ShouldResembleV, &Val{
|
| + ID: 10,
|
| + Val: 100,
|
| + Extra: PropertyMap{
|
| + "TwoVal": {mp(200)},
|
| + },
|
| + })
|
| + })
|
| + })
|
| +
|
| + Convey("Can round-trip extra fields", func() {
|
| + type Expando struct {
|
| + ID int64 `gae:"$id"`
|
| +
|
| + Something int
|
| + Extra PropertyMap `gae:",extra"`
|
| + }
|
| + ex := &Expando{10, 17, PropertyMap{
|
| + "Hello": {mp("Hello")},
|
| + "World": {mp(true)},
|
| + }}
|
| + So(ds.Put(ex), ShouldBeNil)
|
| +
|
| + ex = &Expando{ID: 10}
|
| + So(ds.Get(ex), ShouldBeNil)
|
| + So(ex, ShouldResembleV, &Expando{
|
| + ID: 10,
|
| + Something: 17,
|
| + Extra: PropertyMap{
|
| + "Hello": {mp("Hello")},
|
| + "World": {mp(true)},
|
| + },
|
| + })
|
| + })
|
| +
|
| + Convey("Can read-but-not-write", func() {
|
| + initial := PropertyMap{
|
| + "$key": {mpNI(ds.MakeKey("Convert", 10))},
|
| + "Val": {mp(100)},
|
| + "TwoVal": {mp(200)},
|
| + }
|
| + So(ds.Put(initial), ShouldBeNil)
|
| + type Convert struct {
|
| + ID int64 `gae:"$id"`
|
| +
|
| + Val int64
|
| + NewVal int64
|
| + Extra PropertyMap `gae:"-,extra"`
|
| + }
|
| + c := &Convert{ID: 10}
|
| + So(ds.Get(c), ShouldBeNil)
|
| + So(c, ShouldResembleV, &Convert{
|
| + ID: 10, Val: 100, NewVal: 0, Extra: PropertyMap{"TwoVal": {mp(200)}},
|
| + })
|
| + c.NewVal = c.Extra["TwoVal"][0].Value().(int64)
|
| + So(ds.Put(c), ShouldBeNil)
|
| +
|
| + c = &Convert{ID: 10}
|
| + So(ds.Get(c), ShouldBeNil)
|
| + So(c, ShouldResembleV, &Convert{
|
| + ID: 10, Val: 100, NewVal: 200, Extra: nil,
|
| + })
|
| + })
|
| +
|
| + Convey("Can black hole", func() {
|
| + initial := PropertyMap{
|
| + "$key": {mpNI(ds.MakeKey("BlackHole", 10))},
|
| + "Val": {mp(100)},
|
| + "TwoVal": {mp(200)},
|
| + }
|
| + So(ds.Put(initial), ShouldBeNil)
|
| + type BlackHole struct {
|
| + ID int64 `gae:"$id"`
|
| +
|
| + NewStuff string
|
| + blackHole PropertyMap `gae:"-,extra"`
|
| + }
|
| + b := &BlackHole{ID: 10, NewStuff: "(╯°□°)╯︵ ┻━┻"}
|
| + So(ds.Get(b), ShouldBeNil)
|
| + So(b, ShouldResemble, &BlackHole{ID: 10, NewStuff: "(╯°□°)╯︵ ┻━┻"})
|
| + })
|
| +
|
| + Convey("Can change field types", func() {
|
| + initial := PropertyMap{
|
| + "$key": {mpNI(ds.MakeKey("IntChange", 10))},
|
| + "Val": {mp(100)},
|
| + }
|
| + So(ds.Put(initial), ShouldBeNil)
|
| +
|
| + type IntChange struct {
|
| + ID int64 `gae:"$id"`
|
| + Val string
|
| + Extra PropertyMap `gae:"-,extra"`
|
| + }
|
| + i := &IntChange{ID: 10}
|
| + So(ds.Get(i), ShouldBeNil)
|
| + So(i, ShouldResembleV, &IntChange{ID: 10, Extra: PropertyMap{"Val": {mp(100)}}})
|
| + i.Val = fmt.Sprint(i.Extra["Val"][0].Value())
|
| + So(ds.Put(i), ShouldBeNil)
|
| +
|
| + i = &IntChange{ID: 10}
|
| + So(ds.Get(i), ShouldBeNil)
|
| + So(i, ShouldResembleV, &IntChange{ID: 10, Val: "100"})
|
| + })
|
| +
|
| + Convey("Native fields have priority over Extra fields", func() {
|
| + type Dup struct {
|
| + ID int64 `gae:"$id"`
|
| + Val int64
|
| + Extra PropertyMap `gae:",extra"`
|
| + }
|
| + d := &Dup{ID: 10, Val: 100, Extra: PropertyMap{
|
| + "Val": {mp(200)},
|
| + "Other": {mp("other")},
|
| + }}
|
| + So(ds.Put(d), ShouldBeNil)
|
| +
|
| + d = &Dup{ID: 10}
|
| + So(ds.Get(d), ShouldBeNil)
|
| + So(d, ShouldResembleV, &Dup{
|
| + ID: 10, Val: 100, Extra: PropertyMap{"Other": {mp("other")}},
|
| + })
|
| + })
|
| +
|
| + Convey("Can change repeated field to non-repeating field", func() {
|
| + initial := PropertyMap{
|
| + "$key": {mpNI(ds.MakeKey("NonRepeating", 10))},
|
| + "Val": {mp(100), mp(200), mp(400)},
|
| + }
|
| + So(ds.Put(initial), ShouldBeNil)
|
| +
|
| + type NonRepeating struct {
|
| + ID int64 `gae:"$id"`
|
| + Val int64
|
| + Extra PropertyMap `gae:",extra"`
|
| + }
|
| + n := &NonRepeating{ID: 10}
|
| + So(ds.Get(n), ShouldBeNil)
|
| + So(n, ShouldResembleV, &NonRepeating{
|
| + ID: 10, Val: 0, Extra: PropertyMap{
|
| + "Val": {mp(100), mp(200), mp(400)},
|
| + },
|
| + })
|
| + })
|
| +
|
| + Convey("Deals correctly with recursive types", func() {
|
| + initial := PropertyMap{
|
| + "$key": {mpNI(ds.MakeKey("Outer", 10))},
|
| + "I.A": {mp(1), mp(2), mp(4)},
|
| + "I.B": {mp(10), mp(20), mp(40)},
|
| + "I.C": {mp(100), mp(200), mp(400)},
|
| + }
|
| + So(ds.Put(initial), ShouldBeNil)
|
| + type Inner struct {
|
| + A int64
|
| + B int64
|
| + }
|
| + type Outer struct {
|
| + ID int64 `gae:"$id"`
|
| +
|
| + I []Inner
|
| + Extra PropertyMap `gae:",extra"`
|
| + }
|
| + o := &Outer{ID: 10}
|
| + So(ds.Get(o), ShouldBeNil)
|
| + So(o, ShouldResembleV, &Outer{
|
| + ID: 10,
|
| + I: []Inner{
|
| + {1, 10},
|
| + {2, 20},
|
| + {4, 40},
|
| + },
|
| + Extra: PropertyMap{
|
| + "I.C": {mp(100), mp(200), mp(400)},
|
| + },
|
| + })
|
| + })
|
| +
|
| + Convey("Problems", func() {
|
| + Convey("multiple extra fields", func() {
|
| + type Bad struct {
|
| + A PropertyMap `gae:",extra"`
|
| + B PropertyMap `gae:",extra"`
|
| + }
|
| + So(func() { GetPLS(&Bad{}) }, ShouldPanicLike,
|
| + "multiple fields tagged as 'extra'")
|
| + })
|
| +
|
| + Convey("extra field with name", func() {
|
| + type Bad struct {
|
| + A PropertyMap `gae:"wut,extra"`
|
| + }
|
| + So(func() { GetPLS(&Bad{}) }, ShouldPanicLike,
|
| + "struct 'extra' field has invalid name wut")
|
| + })
|
| +
|
| + Convey("extra field with bad type", func() {
|
| + type Bad struct {
|
| + A int64 `gae:",extra"`
|
| + }
|
| + So(func() { GetPLS(&Bad{}) }, ShouldPanicLike,
|
| + "struct 'extra' field has invalid type int64")
|
| + })
|
| + })
|
| + })
|
| +}
|
|
|