func fakeDatastoreFactory(c context.Context, wantTxn bool) RawInterface { i := info.Get(c) return &fakeDatastore{ aid: i.FullyQualifiedAppID(), ns: i.GetNamespace(), } }
// SetGlobalEnable is a convenience function for manipulating the GlobalConfig. // // It's meant to be called from admin handlers on your app to turn dscache // functionality on or off in emergencies. func SetGlobalEnable(c context.Context, memcacheEnabled bool) error { // always go to the default namespace c, err := info.Get(c).Namespace("") if err != nil { return err } return datastore.Get(c).RunInTransaction(func(c context.Context) error { ds := datastore.Get(c) cfg := &GlobalConfig{Enable: true} if err := ds.Get(cfg); err != nil && err != datastore.ErrNoSuchEntity { return err } if cfg.Enable == memcacheEnabled { return nil } cfg.Enable = memcacheEnabled if memcacheEnabled { // when going false -> true, wipe memcache. if err := memcache.Get(c).Flush(); err != nil { return err } } return ds.Put(cfg) }, nil) }
func withTxnBuf(ctx context.Context, cb func(context.Context) error, opts *datastore.TransactionOptions) error { inf := info.Get(ctx) ns := inf.GetNamespace() parentState, _ := ctx.Value(dsTxnBufParent).(*txnBufState) roots := stringset.New(0) rootLimit := 1 if opts != nil && opts.XG { rootLimit = XGTransactionGroupLimit } sizeBudget := DefaultSizeBudget if parentState != nil { // TODO(riannucci): this is a bit wonky since it means that a child // transaction declaring XG=true will only get to modify 25 groups IF // they're same groups affected by the parent transactions. So instead of // respecting opts.XG for inner transactions, we just dup everything from // the parent transaction. roots = parentState.roots.Dup() rootLimit = parentState.rootLimit sizeBudget = parentState.sizeBudget - parentState.entState.total if sizeBudget < DefaultSizeThreshold { return ErrTransactionTooLarge } } bufDS, err := memory.NewDatastore(inf.FullyQualifiedAppID(), ns) if err != nil { return err } state := &txnBufState{ entState: &sizeTracker{}, bufDS: bufDS.Raw(), roots: roots, rootLimit: rootLimit, ns: ns, aid: inf.AppID(), parentDS: datastore.Get(context.WithValue(ctx, dsTxnBufHaveLock, true)).Raw(), sizeBudget: sizeBudget, } if err = cb(context.WithValue(ctx, dsTxnBufParent, state)); err != nil { return err } // no reason to unlock this ever. At this point it's toast. state.Lock() if parentState == nil { return commitToReal(state) } if err = parentState.canApplyLocked(state); err != nil { return err } parentState.commitLocked(state) return nil }
// GetNoTxn gets the Interface implementation from context. If there's a // currently active transaction, this will return a non-transactional connection // to the datastore, otherwise this is the same as GetRaw. // Get gets the Interface implementation from context. func GetNoTxn(c context.Context) Interface { inf := info.Get(c) return &datastoreImpl{ GetRawNoTxn(c), inf.FullyQualifiedAppID(), inf.GetNamespace(), } }
// NewDatastore creates a new standalone memory implementation of the datastore, // suitable for embedding for doing in-memory data organization. // // It's configured by default with the following settings: // * AutoIndex(true) // * Consistent(true) // * DisableSpecialEntities(true) // // These settings can of course be changed by using the Testable() interface. func NewDatastore(aid, ns string) (ds.Interface, error) { ctx := UseWithAppID(context.Background(), aid) ctx, err := info.Get(ctx).Namespace(ns) if err != nil { return nil, err } ret := ds.Get(ctx) t := ret.Testable() t.AutoIndex(true) t.Consistent(true) t.DisableSpecialEntities(true) return ret, nil }
// AEContextNoTxn retrieves the raw "google.golang.org/appengine" compatible // Context that's not part of a transaction. func AEContextNoTxn(c context.Context) context.Context { aeCtx, _ := c.Value(prodContextNoTxnKey).(context.Context) if aeCtx == nil { return nil } aeCtx, err := appengine.Namespace(aeCtx, info.Get(c).GetNamespace()) if err != nil { panic(err) } if deadline, ok := c.Deadline(); ok { aeCtx, _ = context.WithDeadline(aeCtx, deadline) } return aeCtx }
// useRDS adds a gae.RawDatastore implementation to context, accessible // by gae.GetDS(c) func useRDS(c context.Context) context.Context { return ds.SetRawFactory(c, func(ci context.Context, wantTxn bool) ds.RawInterface { ns := info.Get(ci).GetNamespace() maybeTxnCtx := AEContext(ci) if wantTxn { return rdsImpl{ci, maybeTxnCtx, ns} } aeCtx := AEContextNoTxn(ci) if maybeTxnCtx != aeCtx { ci = context.WithValue(ci, prodContextKey, aeCtx) } return rdsImpl{ci, aeCtx, ns} }) }
// AlwaysFilterRDS installs a caching RawDatastore filter in the context. // // Unlike FilterRDS it doesn't check GlobalConfig via IsGloballyEnabled call, // assuming caller already knows whether filter should be applied or not. func AlwaysFilterRDS(c context.Context, shardsForKey func(*ds.Key) int) context.Context { return ds.AddRawFilters(c, func(c context.Context, ds ds.RawInterface) ds.RawInterface { i := info.Get(c) sc := &supportContext{ i.AppID(), i.GetNamespace(), c, mc.Get(c), mathrand.Get(c), shardsForKey, } v := c.Value(dsTxnCacheKey) if v == nil { return &dsCache{ds, sc} } return &dsTxnCache{ds, v.(*dsTxnState), sc} }) }
// IsGloballyEnabled checks to see if this filter is enabled globally. // // This checks InstanceEnabledStatic, as well as polls the datastore entity // /dscache,1 (a GlobalConfig instance) // Once every GlobalEnabledCheckInterval. // // For correctness, any error encountered returns true. If this assumed false, // then Put operations might incorrectly invalidate the cache. func IsGloballyEnabled(c context.Context) bool { if !InstanceEnabledStatic { return false } now := clock.Now(c) globalEnabledLock.RLock() nextCheck := globalEnabledNextCheck enabledVal := globalEnabled globalEnabledLock.RUnlock() if now.Before(nextCheck) { return enabledVal } globalEnabledLock.Lock() defer globalEnabledLock.Unlock() // just in case we raced if now.Before(globalEnabledNextCheck) { return globalEnabled } // always go to the default namespace c, err := info.Get(c).Namespace("") if err != nil { return true } cfg := &GlobalConfig{Enable: true} if err := datastore.Get(c).Get(cfg); err != nil && err != datastore.ErrNoSuchEntity { return true } globalEnabled = cfg.Enable globalEnabledNextCheck = now.Add(GlobalEnabledCheckInterval) return globalEnabled }
func TestCount(t *testing.T) { t.Parallel() Convey("Test Count filter", t, func() { c, fb := featureBreaker.FilterRDS(memory.Use(context.Background()), nil) c, ctr := FilterRDS(c) So(c, ShouldNotBeNil) So(ctr, ShouldNotBeNil) ds := datastore.Get(c) vals := []datastore.PropertyMap{{ "Val": {datastore.MkProperty(100)}, "$key": {datastore.MkPropertyNI(ds.NewKey("Kind", "", 1, nil))}, }} Convey("Calling a ds function should reflect in counter", func() { So(ds.PutMulti(vals), ShouldBeNil) So(ctr.NewKey.Successes(), ShouldEqual, 1) So(ctr.PutMulti.Successes(), ShouldEqual, 1) Convey("effects are cumulative", func() { So(ds.PutMulti(vals), ShouldBeNil) So(ctr.PutMulti.Successes(), ShouldEqual, 2) Convey("even within transactions", func() { ds.RunInTransaction(func(c context.Context) error { ds := datastore.Get(c) So(ds.PutMulti(append(vals, vals[0])), ShouldBeNil) return nil }, nil) }) }) }) Convey("errors count against errors", func() { fb.BreakFeatures(nil, "GetMulti") ds.GetMulti(vals) So(ctr.GetMulti.Errors(), ShouldEqual, 1) fb.UnbreakFeatures("GetMulti") So(ds.PutMulti(vals), ShouldBeNil) ds.GetMulti(vals) So(ctr.GetMulti.Errors(), ShouldEqual, 1) So(ctr.GetMulti.Successes(), ShouldEqual, 1) So(ctr.GetMulti.Total(), ShouldEqual, 2) }) }) Convey("works for memcache", t, func() { c, ctr := FilterMC(memory.Use(context.Background())) So(c, ShouldNotBeNil) So(ctr, ShouldNotBeNil) mc := memcache.Get(c) mc.Set(mc.NewItem("hello").SetValue([]byte("sup"))) So(mc.Get(mc.NewItem("Wat")), ShouldNotBeNil) mc.Get(mc.NewItem("hello")) So(ctr.SetMulti, shouldHaveSuccessesAndErrors, 1, 0) So(ctr.GetMulti, shouldHaveSuccessesAndErrors, 2, 0) So(ctr.NewItem, shouldHaveSuccessesAndErrors, 3, 0) }) Convey("works for taskqueue", t, func() { c, ctr := FilterTQ(memory.Use(context.Background())) So(c, ShouldNotBeNil) So(ctr, ShouldNotBeNil) tq := taskqueue.Get(c) tq.Add(&taskqueue.Task{Name: "wat"}, "") tq.Add(&taskqueue.Task{Name: "wat"}, "DNE_QUEUE") So(ctr.AddMulti, shouldHaveSuccessesAndErrors, 1, 1) }) Convey("works for global info", t, func() { c, fb := featureBreaker.FilterGI(memory.Use(context.Background()), nil) c, ctr := FilterGI(c) So(c, ShouldNotBeNil) So(ctr, ShouldNotBeNil) gi := info.Get(c) gi.Namespace("foo") fb.BreakFeatures(nil, "Namespace") gi.Namespace("boom") So(ctr.Namespace, shouldHaveSuccessesAndErrors, 1, 1) }) }
func applyCheckFilter(c context.Context, i RawInterface) RawInterface { inf := info.Get(c) return &checkFilter{i, inf.FullyQualifiedAppID(), inf.GetNamespace()} }
func TestTaskQueue(t *testing.T) { t.Parallel() Convey("TaskQueue", t, func() { now := time.Date(2000, time.January, 1, 1, 1, 1, 1, time.UTC) c, tc := testclock.UseTime(context.Background(), now) c = mathrand.Set(c, rand.New(rand.NewSource(clock.Now(c).UnixNano()))) c = Use(c) tq := tqS.Get(c) tqt := tq.Raw().Testable() So(tqt, ShouldNotBeNil) So(tq, ShouldNotBeNil) Convey("implements TQMultiReadWriter", func() { Convey("Add", func() { t := tq.NewTask("/hello/world") Convey("works", func() { t.Delay = 4 * time.Second t.Header = http.Header{} t.Header.Add("Cat", "tabby") t.Payload = []byte("watwatwat") t.RetryOptions = &tqS.RetryOptions{AgeLimit: 7 * time.Second} So(tq.Add(t, ""), ShouldBeNil) name := "Z_UjshxM9ecyMQfGbZmUGOEcgxWU0_5CGLl_-RntudwAw2DqQ5-58bzJiWQN4OKzeuUb9O4JrPkUw2rOvk2Ax46THojnQ6avBQgZdrKcJmrwQ6o4qKfJdiyUbGXvy691yRfzLeQhs6cBhWrgf3wH-VPMcA4SC-zlbJ2U8An7I0zJQA5nBFnMNoMgT-2peGoay3rCSbj4z9VFFm9kS_i6JCaQH518ujLDSNCYdjTq6B6lcWrZAh0U_q3a1S2nXEwrKiw_t9MTNQFgAQZWyGBbvZQPmeRYtu8SPaWzTfd25v_YWgBuVL2rRSPSMvlDwE04nNdtvVzE8vNNiA1zRimmdzKeqATQF9_ReUvj4D7U8dcS703DZWfKMBLgBffY9jqCassOOOw77V72Oq5EVauUw3Qw0L6bBsfM9FtahTKUdabzRZjXUoze3EK4KXPt3-wdidau-8JrVf2XFocjjZbwHoxcGvbtT3b4nGLDlgwdC00bwaFBZWff" So(tqt.GetScheduledTasks()["default"][name], ShouldResemble, &tqS.Task{ ETA: now.Add(4 * time.Second), Header: http.Header{"Cat": []string{"tabby"}}, Method: "POST", Name: name, Path: "/hello/world", Payload: []byte("watwatwat"), RetryOptions: &tqS.RetryOptions{AgeLimit: 7 * time.Second}, }) }) Convey("picks up namespace", func() { c, err := info.Get(c).Namespace("coolNamespace") So(err, ShouldBeNil) tq = tqS.Get(c) t := tq.NewTask("") So(tq.Add(t, ""), ShouldBeNil) So(t.Header, ShouldResemble, http.Header{ "X-Appengine-Current-Namespace": {"coolNamespace"}, }) }) Convey("cannot add to bad queues", func() { So(tq.Add(nil, "waaat").Error(), ShouldContainSubstring, "UNKNOWN_QUEUE") Convey("but you can add Queues when testing", func() { tqt.CreateQueue("waaat") So(tq.Add(t, "waaat"), ShouldBeNil) Convey("you just can't add them twice", func() { So(func() { tqt.CreateQueue("waaat") }, ShouldPanic) }) }) }) Convey("supplies a URL if it's missing", func() { t.Path = "" So(tq.Add(t, ""), ShouldBeNil) So(t.Path, ShouldEqual, "/_ah/queue/default") }) Convey("cannot add twice", func() { t.Name = "bob" So(tq.Add(t, ""), ShouldBeNil) // can't add the same one twice! So(tq.Add(t, ""), ShouldEqual, tqS.ErrTaskAlreadyAdded) }) Convey("cannot add deleted task", func() { t.Name = "bob" So(tq.Add(t, ""), ShouldBeNil) So(tq.Delete(t, ""), ShouldBeNil) // can't add a deleted task! So(tq.Add(t, ""), ShouldEqual, tqS.ErrTaskAlreadyAdded) }) Convey("cannot set ETA+Delay", func() { t.ETA = clock.Now(c).Add(time.Hour) tc.Add(time.Second) t.Delay = time.Hour So(func() { tq.Add(t, "") }, ShouldPanic) }) Convey("must use a reasonable method", func() { t.Method = "Crystal" So(tq.Add(t, "").Error(), ShouldContainSubstring, "bad method") }) Convey("payload gets dumped for non POST/PUT methods", func() { t.Method = "HEAD" t.Payload = []byte("coool") So(tq.Add(t, ""), ShouldBeNil) So(t.Payload, ShouldBeNil) }) Convey("invalid names are rejected", func() { t.Name = "happy times" So(tq.Add(t, "").Error(), ShouldContainSubstring, "INVALID_TASK_NAME") }) Convey("AddMulti also works", func() { t2 := t.Duplicate() t2.Path = "/hi/city" expect := []*tqS.Task{t, t2} So(tq.AddMulti(expect, "default"), ShouldBeNil) So(len(expect), ShouldEqual, 2) So(len(tqt.GetScheduledTasks()["default"]), ShouldEqual, 2) for i := range expect { Convey(fmt.Sprintf("task %d: %s", i, expect[i].Path), func() { So(expect[i].Method, ShouldEqual, "POST") So(expect[i].ETA, ShouldHappenOnOrBefore, now) So(len(expect[i].Name), ShouldEqual, 500) }) } Convey("stats work too", func() { delay := -time.Second * 400 t := tq.NewTask("/somewhere") t.Delay = delay So(tq.Add(t, ""), ShouldBeNil) stats, err := tq.Stats("") So(err, ShouldBeNil) So(stats[0].Tasks, ShouldEqual, 3) So(stats[0].OldestETA, ShouldHappenOnOrBefore, clock.Now(c).Add(delay)) _, err = tq.Stats("noexist") So(err.Error(), ShouldContainSubstring, "UNKNOWN_QUEUE") }) Convey("can purge all tasks", func() { So(tq.Add(&tqS.Task{Path: "/wut/nerbs"}, ""), ShouldBeNil) So(tq.Purge(""), ShouldBeNil) So(len(tqt.GetScheduledTasks()["default"]), ShouldEqual, 0) So(len(tqt.GetTombstonedTasks()["default"]), ShouldEqual, 0) So(len(tqt.GetTransactionTasks()["default"]), ShouldEqual, 0) Convey("purging a queue which DNE fails", func() { So(tq.Purge("noexist").Error(), ShouldContainSubstring, "UNKNOWN_QUEUE") }) }) }) }) Convey("Delete", func() { t := &tqS.Task{Path: "/hello/world"} So(tq.Add(t, ""), ShouldBeNil) Convey("works", func() { err := tq.Delete(t, "") So(err, ShouldBeNil) So(len(tqt.GetScheduledTasks()["default"]), ShouldEqual, 0) So(len(tqt.GetTombstonedTasks()["default"]), ShouldEqual, 1) So(tqt.GetTombstonedTasks()["default"][t.Name], ShouldResemble, t) }) Convey("cannot delete a task twice", func() { So(tq.Delete(t, ""), ShouldBeNil) So(tq.Delete(t, "").Error(), ShouldContainSubstring, "TOMBSTONED_TASK") Convey("but you can if you do a reset", func() { tqt.ResetTasks() So(tq.Add(t, ""), ShouldBeNil) So(tq.Delete(t, ""), ShouldBeNil) }) }) Convey("cannot delete from bogus queues", func() { err := tq.Delete(t, "wat") So(err.Error(), ShouldContainSubstring, "UNKNOWN_QUEUE") }) Convey("cannot delete a missing task", func() { t.Name = "tarntioarenstyw" err := tq.Delete(t, "") So(err.Error(), ShouldContainSubstring, "UNKNOWN_TASK") }) Convey("DeleteMulti also works", func() { t2 := t.Duplicate() t2.Name = "" t2.Path = "/hi/city" So(tq.Add(t2, ""), ShouldBeNil) Convey("usually works", func() { So(tq.DeleteMulti([]*tqS.Task{t, t2}, ""), ShouldBeNil) So(len(tqt.GetScheduledTasks()["default"]), ShouldEqual, 0) So(len(tqt.GetTombstonedTasks()["default"]), ShouldEqual, 2) }) }) }) }) Convey("works with transactions", func() { t := &tqS.Task{Path: "/hello/world"} So(tq.Add(t, ""), ShouldBeNil) t2 := &tqS.Task{Path: "/hi/city"} So(tq.Add(t2, ""), ShouldBeNil) So(tq.Delete(t2, ""), ShouldBeNil) Convey("can view regular tasks", func() { dsS.Get(c).RunInTransaction(func(c context.Context) error { tqt := tqS.GetRaw(c).Testable() So(tqt.GetScheduledTasks()["default"][t.Name], ShouldResemble, t) So(tqt.GetTombstonedTasks()["default"][t2.Name], ShouldResemble, t2) So(tqt.GetTransactionTasks()["default"], ShouldBeNil) return nil }, nil) }) Convey("can add a new task", func() { t3 := &tqS.Task{Path: "/sandwitch/victory"} err := dsS.Get(c).RunInTransaction(func(c context.Context) error { tq := tqS.Get(c) tqt := tq.Raw().Testable() So(tq.Add(t3, ""), ShouldBeNil) So(t3.Name, ShouldEqual, "") So(tqt.GetScheduledTasks()["default"][t.Name], ShouldResemble, t) So(tqt.GetTombstonedTasks()["default"][t2.Name], ShouldResemble, t2) So(tqt.GetTransactionTasks()["default"][0], ShouldResemble, t3) return nil }, nil) So(err, ShouldBeNil) for _, tsk := range tqt.GetScheduledTasks()["default"] { if tsk.Name == t.Name { So(tsk, ShouldResemble, t) } else { tsk.Name = "" So(tsk, ShouldResemble, t3) } } So(tqt.GetTombstonedTasks()["default"][t2.Name], ShouldResemble, t2) So(tqt.GetTransactionTasks()["default"], ShouldBeNil) }) Convey("can add a new task (but reset the state in a test)", func() { t3 := &tqS.Task{Path: "/sandwitch/victory"} ttq := tqS.Interface(nil) dsS.Get(c).RunInTransaction(func(c context.Context) error { ttq = tqS.Get(c) tqt := ttq.Raw().Testable() So(ttq.Add(t3, ""), ShouldBeNil) So(tqt.GetScheduledTasks()["default"][t.Name], ShouldResemble, t) So(tqt.GetTombstonedTasks()["default"][t2.Name], ShouldResemble, t2) So(tqt.GetTransactionTasks()["default"][0], ShouldResemble, t3) tqt.ResetTasks() So(len(tqt.GetScheduledTasks()["default"]), ShouldEqual, 0) So(len(tqt.GetTombstonedTasks()["default"]), ShouldEqual, 0) So(len(tqt.GetTransactionTasks()["default"]), ShouldEqual, 0) return nil }, nil) So(len(tqt.GetScheduledTasks()["default"]), ShouldEqual, 0) So(len(tqt.GetTombstonedTasks()["default"]), ShouldEqual, 0) So(len(tqt.GetTransactionTasks()["default"]), ShouldEqual, 0) Convey("and reusing a closed context is bad times", func() { So(ttq.Add(nil, "").Error(), ShouldContainSubstring, "expired") }) }) Convey("you can AddMulti as well", func() { dsS.Get(c).RunInTransaction(func(c context.Context) error { tq := tqS.Get(c) tqt := tq.Raw().Testable() t.Name = "" tasks := []*tqS.Task{t.Duplicate(), t.Duplicate(), t.Duplicate()} So(tq.AddMulti(tasks, ""), ShouldBeNil) So(len(tqt.GetScheduledTasks()["default"]), ShouldEqual, 1) So(len(tqt.GetTransactionTasks()["default"]), ShouldEqual, 3) return nil }, nil) So(len(tqt.GetScheduledTasks()["default"]), ShouldEqual, 4) So(len(tqt.GetTransactionTasks()["default"]), ShouldEqual, 0) }) Convey("unless you add too many things", func() { dsS.Get(c).RunInTransaction(func(c context.Context) error { for i := 0; i < 5; i++ { So(tqS.Get(c).Add(t.Duplicate(), ""), ShouldBeNil) } So(tqS.Get(c).Add(t, "").Error(), ShouldContainSubstring, "BAD_REQUEST") return nil }, nil) }) Convey("unless you Add to a bad queue", func() { dsS.Get(c).RunInTransaction(func(c context.Context) error { So(tqS.Get(c).Add(t, "meat").Error(), ShouldContainSubstring, "UNKNOWN_QUEUE") Convey("unless you add it!", func() { tqS.GetRaw(c).Testable().CreateQueue("meat") So(tqS.Get(c).Add(t, "meat"), ShouldBeNil) }) return nil }, nil) }) Convey("No other features are available, however", func() { dsS.Get(c).RunInTransaction(func(c context.Context) error { So(tqS.Get(c).Delete(t, "").Error(), ShouldContainSubstring, "cannot DeleteMulti from a transaction") So(tqS.Get(c).Purge("").Error(), ShouldContainSubstring, "cannot Purge from a transaction") _, err := tqS.Get(c).Stats("") So(err.Error(), ShouldContainSubstring, "cannot Stats from a transaction") return nil }, nil) }) Convey("adding a new task only happens if we don't errout", func() { dsS.Get(c).RunInTransaction(func(c context.Context) error { t3 := tq.NewTask("/sandwitch/victory") So(tqS.Get(c).Add(t3, ""), ShouldBeNil) return fmt.Errorf("nooooo") }, nil) So(tqt.GetScheduledTasks()["default"][t.Name], ShouldResemble, t) So(tqt.GetTombstonedTasks()["default"][t2.Name], ShouldResemble, t2) So(tqt.GetTransactionTasks()["default"], ShouldBeNil) }) Convey("likewise, a panic doesn't schedule anything", func() { func() { defer func() { recover() }() dsS.Get(c).RunInTransaction(func(c context.Context) error { tq := tqS.Get(c) So(tq.Add(tq.NewTask("/sandwitch/victory"), ""), ShouldBeNil) panic(fmt.Errorf("nooooo")) }, nil) }() So(tqt.GetScheduledTasks()["default"][t.Name], ShouldResemble, t) So(tqt.GetTombstonedTasks()["default"][t2.Name], ShouldResemble, t2) So(tqt.GetTransactionTasks()["default"], ShouldBeNil) }) }) }) }
func TestDatastoreSingleReadWriter(t *testing.T) { t.Parallel() Convey("Datastore single reads and writes", t, func() { c := Use(context.Background()) ds := dsS.Get(c) So(ds, ShouldNotBeNil) Convey("getting objects that DNE is an error", func() { So(ds.Get(&Foo{ID: 1}), ShouldEqual, dsS.ErrNoSuchEntity) }) Convey("bad namespaces fail", func() { _, err := infoS.Get(c).Namespace("$$blzyall") So(err.Error(), ShouldContainSubstring, "namespace \"$$blzyall\" does not match") }) Convey("Can Put stuff", func() { // with an incomplete key! f := &Foo{Val: 10} So(ds.Put(f), ShouldBeNil) k := ds.KeyForObj(f) So(k.String(), ShouldEqual, "dev~app::/Foo,1") Convey("and Get it back", func() { newFoo := &Foo{ID: 1} So(ds.Get(newFoo), ShouldBeNil) So(newFoo, ShouldResemble, f) Convey("but it's hidden from a different namespace", func() { c, err := infoS.Get(c).Namespace("whombat") So(err, ShouldBeNil) ds = dsS.Get(c) So(ds.Get(f), ShouldEqual, dsS.ErrNoSuchEntity) }) Convey("and we can Delete it", func() { So(ds.Delete(k), ShouldBeNil) So(ds.Get(newFoo), ShouldEqual, dsS.ErrNoSuchEntity) }) }) Convey("Deleteing with a bogus key is bad", func() { So(ds.Delete(ds.NewKey("Foo", "wat", 100, nil)), ShouldEqual, dsS.ErrInvalidKey) }) Convey("Deleteing a DNE entity is fine", func() { So(ds.Delete(ds.NewKey("Foo", "wat", 0, nil)), ShouldBeNil) }) Convey("Deleting entities from a nonexistant namespace works", func() { aid := infoS.Get(c).FullyQualifiedAppID() keys := make([]*dsS.Key, 10) for i := range keys { keys[i] = ds.MakeKey(aid, "noexist", "Kind", i+1) } So(ds.DeleteMulti(keys), ShouldBeNil) count := 0 So(ds.Raw().DeleteMulti(keys, func(err error) error { count++ So(err, ShouldBeNil) return nil }), ShouldBeNil) So(count, ShouldEqual, len(keys)) }) Convey("with multiple puts", func() { So(testGetMeta(c, k), ShouldEqual, 1) foos := make([]Foo, 10) for i := range foos { foos[i].Val = 10 foos[i].Parent = k } So(ds.PutMulti(foos), ShouldBeNil) So(testGetMeta(c, k), ShouldEqual, 11) keys := make([]*dsS.Key, len(foos)) for i, f := range foos { keys[i] = ds.KeyForObj(&f) } Convey("ensure that group versions persist across deletes", func() { So(ds.DeleteMulti(append(keys, k)), ShouldBeNil) ds.Testable().CatchupIndexes() count := 0 So(ds.Run(dsS.NewQuery(""), func(_ *dsS.Key) { count++ }), ShouldBeNil) So(count, ShouldEqual, 3) So(testGetMeta(c, k), ShouldEqual, 22) So(ds.Put(&Foo{ID: 1}), ShouldBeNil) So(testGetMeta(c, k), ShouldEqual, 23) }) Convey("can Get", func() { vals := make([]dsS.PropertyMap, len(keys)) for i := range vals { vals[i] = dsS.PropertyMap{} So(vals[i].SetMeta("key", keys[i]), ShouldBeTrue) } So(ds.GetMulti(vals), ShouldBeNil) for i, val := range vals { So(val, ShouldResemble, dsS.PropertyMap{ "Val": {dsS.MkProperty(10)}, "$key": {dsS.MkPropertyNI(keys[i])}, }) } }) }) Convey("allocating ids prevents their use", func() { start, err := ds.AllocateIDs(ds.MakeKey("Foo", 0), 100) So(err, ShouldBeNil) So(start, ShouldEqual, 2) f := &Foo{Val: 10} So(ds.Put(f), ShouldBeNil) k := ds.KeyForObj(f) So(k.String(), ShouldEqual, "dev~app::/Foo,102") }) }) Convey("implements DSTransactioner", func() { Convey("Put", func() { f := &Foo{Val: 10} So(ds.Put(f), ShouldBeNil) k := ds.KeyForObj(f) So(k.String(), ShouldEqual, "dev~app::/Foo,1") Convey("can Put new entity groups", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) f := &Foo{Val: 100} So(ds.Put(f), ShouldBeNil) So(f.ID, ShouldEqual, 2) f.ID = 0 f.Val = 200 So(ds.Put(f), ShouldBeNil) So(f.ID, ShouldEqual, 3) return nil }, &dsS.TransactionOptions{XG: true}) So(err, ShouldBeNil) f := &Foo{ID: 2} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 100) f.ID = 3 So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 200) }) Convey("can Put new entities in a current group", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) f := &Foo{Val: 100, Parent: k} So(ds.Put(f), ShouldBeNil) So(ds.KeyForObj(f).String(), ShouldEqual, "dev~app::/Foo,1/Foo,1") f.ID = 0 f.Val = 200 So(ds.Put(f), ShouldBeNil) So(ds.KeyForObj(f).String(), ShouldEqual, "dev~app::/Foo,1/Foo,2") return nil }, nil) So(err, ShouldBeNil) f := &Foo{ID: 1, Parent: k} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 100) f.ID = 2 So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 200) }) Convey("Deletes work too", func() { err := ds.RunInTransaction(func(c context.Context) error { return dsS.Get(c).Delete(k) }, nil) So(err, ShouldBeNil) So(ds.Get(&Foo{ID: 1}), ShouldEqual, dsS.ErrNoSuchEntity) }) Convey("A Get counts against your group count", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) pm := dsS.PropertyMap{} So(pm.SetMeta("key", ds.NewKey("Foo", "", 20, nil)), ShouldBeTrue) So(ds.Get(pm), ShouldEqual, dsS.ErrNoSuchEntity) So(pm.SetMeta("key", k), ShouldBeTrue) So(ds.Get(pm).Error(), ShouldContainSubstring, "cross-group") return nil }, nil) So(err, ShouldBeNil) }) Convey("Get takes a snapshot", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) // Don't ever do this in a real program unless you want to guarantee // a failed transaction :) f.Val = 11 So(dsS.GetNoTxn(c).Put(f), ShouldBeNil) So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) return nil }, nil) So(err, ShouldBeNil) f := &Foo{ID: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 11) }) Convey("and snapshots are consistent even after Puts", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) f := &Foo{ID: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) // Don't ever do this in a real program unless you want to guarantee // a failed transaction :) f.Val = 11 So(dsS.GetNoTxn(c).Put(f), ShouldBeNil) So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) f.Val = 20 So(ds.Put(f), ShouldBeNil) So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) // still gets 10 return nil }, &dsS.TransactionOptions{Attempts: 1}) So(err.Error(), ShouldContainSubstring, "concurrent") f := &Foo{ID: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 11) }) Convey("Reusing a transaction context is bad news", func() { txnDS := dsS.Interface(nil) err := ds.RunInTransaction(func(c context.Context) error { txnDS = dsS.Get(c) So(txnDS.Get(f), ShouldBeNil) return nil }, nil) So(err, ShouldBeNil) So(txnDS.Get(f).Error(), ShouldContainSubstring, "expired") }) Convey("Nested transactions are rejected", func() { err := ds.RunInTransaction(func(c context.Context) error { err := dsS.Get(c).RunInTransaction(func(c context.Context) error { panic("noooo") }, nil) So(err.Error(), ShouldContainSubstring, "nested transactions") return nil }, nil) So(err, ShouldBeNil) }) Convey("Concurrent transactions only accept one set of changes", func() { // Note: I think this implementation is actually /slightly/ wrong. // According to my read of the docs for appengine, when you open a // transaction it actually (essentially) holds a reference to the // entire datastore. Our implementation takes a snapshot of the // entity group as soon as something observes/affects it. // // That said... I'm not sure if there's really a semantic difference. err := ds.RunInTransaction(func(c context.Context) error { So(dsS.Get(c).Put(&Foo{ID: 1, Val: 21}), ShouldBeNil) err := dsS.GetNoTxn(c).RunInTransaction(func(c context.Context) error { So(dsS.Get(c).Put(&Foo{ID: 1, Val: 27}), ShouldBeNil) return nil }, nil) So(err, ShouldBeNil) return nil }, nil) So(err.Error(), ShouldContainSubstring, "concurrent") f := &Foo{ID: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 27) }) Convey("XG", func() { Convey("Modifying two groups with XG=false is invalid", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) f := &Foo{ID: 1, Val: 200} So(ds.Put(f), ShouldBeNil) f.ID = 2 err := ds.Put(f) So(err.Error(), ShouldContainSubstring, "cross-group") return err }, nil) So(err.Error(), ShouldContainSubstring, "cross-group") }) Convey("Modifying >25 groups with XG=true is invald", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) foos := make([]Foo, 25) for i := int64(1); i < 26; i++ { foos[i-1].ID = i foos[i-1].Val = 200 } So(ds.PutMulti(foos), ShouldBeNil) err := ds.Put(&Foo{ID: 26}) So(err.Error(), ShouldContainSubstring, "too many entity groups") return err }, &dsS.TransactionOptions{XG: true}) So(err.Error(), ShouldContainSubstring, "too many entity groups") }) }) Convey("Errors and panics", func() { Convey("returning an error aborts", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) So(ds.Put(&Foo{ID: 1, Val: 200}), ShouldBeNil) return fmt.Errorf("thingy") }, nil) So(err.Error(), ShouldEqual, "thingy") f := &Foo{ID: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) }) Convey("panicing aborts", func() { So(func() { So(ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) So(ds.Put(&Foo{Val: 200}), ShouldBeNil) panic("wheeeeee") }, nil), ShouldBeNil) }, ShouldPanic) f := &Foo{ID: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) }) }) Convey("Transaction retries", func() { tst := ds.Testable() Reset(func() { tst.SetTransactionRetryCount(0) }) Convey("SetTransactionRetryCount set to zero", func() { tst.SetTransactionRetryCount(0) calls := 0 So(ds.RunInTransaction(func(c context.Context) error { calls++ return nil }, nil), ShouldBeNil) So(calls, ShouldEqual, 1) }) Convey("default TransactionOptions is 3 attempts", func() { tst.SetTransactionRetryCount(100) // more than 3 calls := 0 So(ds.RunInTransaction(func(c context.Context) error { calls++ return nil }, nil), ShouldEqual, dsS.ErrConcurrentTransaction) So(calls, ShouldEqual, 3) }) Convey("non-default TransactionOptions ", func() { tst.SetTransactionRetryCount(100) // more than 20 calls := 0 So(ds.RunInTransaction(func(c context.Context) error { calls++ return nil }, &dsS.TransactionOptions{Attempts: 20}), ShouldEqual, dsS.ErrConcurrentTransaction) So(calls, ShouldEqual, 20) }) Convey("SetTransactionRetryCount is respected", func() { tst.SetTransactionRetryCount(1) // less than 3 calls := 0 So(ds.RunInTransaction(func(c context.Context) error { calls++ return nil }, nil), ShouldBeNil) So(calls, ShouldEqual, 2) }) Convey("fatal errors are not retried", func() { tst.SetTransactionRetryCount(1) calls := 0 So(ds.RunInTransaction(func(c context.Context) error { calls++ return fmt.Errorf("omg") }, nil).Error(), ShouldEqual, "omg") So(calls, ShouldEqual, 1) }) }) }) }) Convey("Testable.Consistent", func() { Convey("false", func() { ds.Testable().Consistent(false) // the default for i := 0; i < 10; i++ { So(ds.Put(&Foo{ID: int64(i + 1), Val: i + 1}), ShouldBeNil) } q := dsS.NewQuery("Foo").Gt("Val", 3) count, err := ds.Count(q) So(err, ShouldBeNil) So(count, ShouldEqual, 0) So(ds.Delete(ds.MakeKey("Foo", 4)), ShouldBeNil) count, err = ds.Count(q) So(err, ShouldBeNil) So(count, ShouldEqual, 0) ds.Testable().Consistent(true) count, err = ds.Count(q) So(err, ShouldBeNil) So(count, ShouldEqual, 6) }) Convey("true", func() { ds.Testable().Consistent(true) for i := 0; i < 10; i++ { So(ds.Put(&Foo{ID: int64(i + 1), Val: i + 1}), ShouldBeNil) } q := dsS.NewQuery("Foo").Gt("Val", 3) count, err := ds.Count(q) So(err, ShouldBeNil) So(count, ShouldEqual, 7) So(ds.Delete(ds.MakeKey("Foo", 4)), ShouldBeNil) count, err = ds.Count(q) So(err, ShouldBeNil) So(count, ShouldEqual, 6) }) }) Convey("Testable.DisableSpecialEntities", func() { ds.Testable().DisableSpecialEntities(true) So(ds.Put(&Foo{}), ShouldErrLike, "allocateIDs is disabled") So(ds.Put(&Foo{ID: 1}), ShouldBeNil) ds.Testable().CatchupIndexes() count, err := ds.Count(dsS.NewQuery("")) So(err, ShouldBeNil) So(count, ShouldEqual, 1) // normally this would include __entity_group__ }) }) }
func TestQueryExecution(t *testing.T) { t.Parallel() Convey("Test query execution", t, func() { c, err := info.Get(Use(context.Background())).Namespace("ns") if err != nil { panic(err) } data := ds.Get(c) testing := data.Raw().Testable() for _, tc := range queryExecutionTests { Convey(tc.name, func() { for i, stage := range tc.test { // outside of Convey, since these must always happen testing.CatchupIndexes() testing.AddIndexes(stage.addIdxs...) if err := data.PutMulti(stage.putEnts); err != nil { // prevent Convey from thinking this assertion should show up in // every test loop. panic(err) } if err := data.DeleteMulti(stage.delEnts); err != nil { panic(err) } Convey(fmt.Sprintf("stage %d", i), func() { for j, expect := range stage.expect { runner := func(f func(ic context.Context) error, _ *ds.TransactionOptions) error { return f(c) } if expect.inTxn { runner = data.RunInTransaction } if expect.keys != nil { runner(func(c context.Context) error { data := ds.Get(c) Convey(fmt.Sprintf("expect %d (keys)", j), func() { rslt := []ds.Key(nil) So(data.GetAll(expect.q, &rslt), ShouldBeNil) So(len(rslt), ShouldEqual, len(expect.keys)) for i, r := range rslt { So(r, ShouldResemble, expect.keys[i]) } }) return nil }, &ds.TransactionOptions{XG: true}) } if expect.get != nil { Convey(fmt.Sprintf("expect %d (data)", j), func() { runner(func(c context.Context) error { rslt := []ds.PropertyMap(nil) So(data.GetAll(expect.q, &rslt), ShouldBeNil) So(len(rslt), ShouldEqual, len(expect.get)) for i, r := range rslt { So(r, ShouldResemble, expect.get[i]) } return nil }, &ds.TransactionOptions{XG: true}) }) } } for j, fn := range stage.extraFns { Convey(fmt.Sprintf("extraFn %d", j), func() { fn(c) }) } }) } }) } }) }
func TestBasicDatastore(t *testing.T) { t.Parallel() Convey("basic", t, func() { inst, err := aetest.NewInstance(&aetest.Options{ StronglyConsistentDatastore: true, }) So(err, ShouldBeNil) defer inst.Close() req, err := inst.NewRequest("GET", "/", nil) So(err, ShouldBeNil) ctx := Use(context.Background(), req) ds := datastore.Get(ctx) mc := memcache.Get(ctx) inf := info.Get(ctx) Convey("logging allows you to tweak the level", func() { // You have to visually confirm that this actually happens in the stdout // of the test... yeah I know. logging.Debugf(ctx, "SHOULD NOT SEE") logging.Infof(ctx, "SHOULD SEE") ctx = logging.SetLevel(ctx, logging.Debug) logging.Debugf(ctx, "SHOULD SEE") logging.Infof(ctx, "SHOULD SEE (2)") }) Convey("Can probe/change Namespace", func() { So(inf.GetNamespace(), ShouldEqual, "") ctx, err = inf.Namespace("wat") So(err, ShouldBeNil) inf = info.Get(ctx) So(inf.GetNamespace(), ShouldEqual, "wat") ds = datastore.Get(ctx) So(ds.MakeKey("Hello", "world").Namespace(), ShouldEqual, "wat") }) Convey("Can get non-transactional context", func() { ctx, err := inf.Namespace("foo") So(err, ShouldBeNil) ds = datastore.Get(ctx) inf = info.Get(ctx) ds.RunInTransaction(func(ctx context.Context) error { So(ds.MakeKey("Foo", "bar").Namespace(), ShouldEqual, "foo") So(ds.Put(&TestStruct{ValueI: []int64{100}}), ShouldBeNil) err = datastore.GetNoTxn(ctx).RunInTransaction(func(ctx context.Context) error { ds = datastore.Get(ctx) So(ds.MakeKey("Foo", "bar").Namespace(), ShouldEqual, "foo") So(ds.Put(&TestStruct{ValueI: []int64{100}}), ShouldBeNil) return nil }, nil) So(err, ShouldBeNil) return nil }, nil) }) Convey("Can Put/Get", func() { orig := TestStruct{ ValueI: []int64{1, 7, 946688461000000, 996688461000000}, ValueB: []bool{true, false}, ValueS: []string{"hello", "world"}, ValueF: []float64{1.0, 7.0, 946688461000000.0, 996688461000000.0}, ValueBS: [][]byte{ []byte("allo"), []byte("hello"), []byte("world"), []byte("zurple"), }, ValueK: []*datastore.Key{ ds.NewKey("Something", "Cool", 0, nil), ds.NewKey("Something", "", 1, nil), ds.NewKey("Something", "Recursive", 0, ds.NewKey("Parent", "", 2, nil)), }, ValueBK: []blobstore.Key{"bellow", "hello"}, ValueGP: []datastore.GeoPoint{ {Lat: 120.7, Lng: 95.5}, }, } So(ds.Put(&orig), ShouldBeNil) ret := TestStruct{ID: orig.ID} So(ds.Get(&ret), ShouldBeNil) So(ret, ShouldResemble, orig) // can't be sure the indexes have caught up... so sleep time.Sleep(time.Second) Convey("Can query", func() { q := datastore.NewQuery("TestStruct") ds.Run(q, func(ts *TestStruct) { So(*ts, ShouldResemble, orig) }) count, err := ds.Count(q) So(err, ShouldBeNil) So(count, ShouldEqual, 1) }) Convey("Can project", func() { q := datastore.NewQuery("TestStruct").Project("ValueS") rslts := []datastore.PropertyMap{} So(ds.GetAll(q, &rslts), ShouldBeNil) So(rslts, ShouldResemble, []datastore.PropertyMap{ { "$key": {mpNI(ds.KeyForObj(&orig))}, "ValueS": {mp("hello")}, }, { "$key": {mpNI(ds.KeyForObj(&orig))}, "ValueS": {mp("world")}, }, }) q = datastore.NewQuery("TestStruct").Project("ValueBS") rslts = []datastore.PropertyMap{} So(ds.GetAll(q, &rslts), ShouldBeNil) So(rslts, ShouldResemble, []datastore.PropertyMap{ { "$key": {mpNI(ds.KeyForObj(&orig))}, "ValueBS": {mp("allo")}, }, { "$key": {mpNI(ds.KeyForObj(&orig))}, "ValueBS": {mp("hello")}, }, { "$key": {mpNI(ds.KeyForObj(&orig))}, "ValueBS": {mp("world")}, }, { "$key": {mpNI(ds.KeyForObj(&orig))}, "ValueBS": {mp("zurple")}, }, }) count, err := ds.Count(q) So(err, ShouldBeNil) So(count, ShouldEqual, 4) }) }) Convey("Can Put/Get (time)", func() { // time comparisons in Go are wonky, so this is pulled out pm := datastore.PropertyMap{ "$key": {mpNI(ds.NewKey("Something", "value", 0, nil))}, "Time": { mp(time.Date(1938, time.January, 1, 1, 1, 1, 1, time.UTC)), mp(time.Time{}), }, } So(ds.Put(&pm), ShouldBeNil) rslt := datastore.PropertyMap{} rslt.SetMeta("key", ds.KeyForObj(pm)) So(ds.Get(&rslt), ShouldBeNil) So(pm["Time"][0].Value(), ShouldResemble, rslt["Time"][0].Value()) q := datastore.NewQuery("Something").Project("Time") all := []datastore.PropertyMap{} So(ds.GetAll(q, &all), ShouldBeNil) So(len(all), ShouldEqual, 2) prop := all[0]["Time"][0] So(prop.Type(), ShouldEqual, datastore.PTInt) tval, err := prop.Project(datastore.PTTime) So(err, ShouldBeNil) So(tval, ShouldResemble, time.Time{}) tval, err = all[1]["Time"][0].Project(datastore.PTTime) So(err, ShouldBeNil) So(tval, ShouldResemble, pm["Time"][0].Value()) ent := datastore.PropertyMap{ "$key": {mpNI(ds.MakeKey("Something", "value"))}, } So(ds.Get(&ent), ShouldBeNil) So(ent["Time"], ShouldResemble, pm["Time"]) }) Convey("memcache: Set (nil) is the same as Set ([]byte{})", func() { So(mc.Set(mc.NewItem("bob")), ShouldBeNil) // normally would panic because Value is nil bob, err := mc.Get("bob") So(err, ShouldBeNil) So(bob.Value(), ShouldResemble, []byte{}) }) }) }
// useRDS adds a gae.RawDatastore implementation to context, accessible // by gae.GetDS(c) func useRDS(c context.Context) context.Context { return ds.SetRawFactory(c, func(ci context.Context) ds.RawInterface { return rdsImpl{ci, info.Get(ci).GetNamespace()} }) }
func TestDatastoreSingleReadWriter(t *testing.T) { t.Parallel() Convey("Datastore single reads and writes", t, func() { c := Use(context.Background()) ds := dsS.Get(c) So(ds, ShouldNotBeNil) Convey("getting objects that DNE is an error", func() { So(ds.Get(&Foo{Id: 1}), ShouldEqual, dsS.ErrNoSuchEntity) }) Convey("bad namespaces fail", func() { _, err := infoS.Get(c).Namespace("$$blzyall") So(err.Error(), ShouldContainSubstring, "namespace \"$$blzyall\" does not match") }) Convey("Can Put stuff", func() { // with an incomplete key! f := &Foo{Val: 10} So(ds.Put(f), ShouldBeNil) k := ds.KeyForObj(f) So(k.String(), ShouldEqual, "/Foo,1") Convey("and Get it back", func() { newFoo := &Foo{Id: 1} So(ds.Get(newFoo), ShouldBeNil) So(newFoo, ShouldResemble, f) Convey("but it's hidden from a different namespace", func() { c, err := infoS.Get(c).Namespace("whombat") So(err, ShouldBeNil) ds = dsS.Get(c) So(ds.Get(f), ShouldEqual, dsS.ErrNoSuchEntity) }) Convey("and we can Delete it", func() { So(ds.Delete(k), ShouldBeNil) So(ds.Get(newFoo), ShouldEqual, dsS.ErrNoSuchEntity) }) }) Convey("Deleteing with a bogus key is bad", func() { So(ds.Delete(ds.NewKey("Foo", "wat", 100, nil)), ShouldEqual, dsS.ErrInvalidKey) }) Convey("Deleteing a DNE entity is fine", func() { So(ds.Delete(ds.NewKey("Foo", "wat", 0, nil)), ShouldBeNil) }) Convey("with multiple puts", func() { So(testGetMeta(c, k), ShouldEqual, 1) foos := make([]Foo, 10) for i := range foos { foos[i].Val = 10 foos[i].Parent = k } So(ds.PutMulti(foos), ShouldBeNil) So(testGetMeta(c, k), ShouldEqual, 11) keys := make([]dsS.Key, len(foos)) for i, f := range foos { keys[i] = ds.KeyForObj(&f) } Convey("ensure that group versions persist across deletes", func() { So(ds.DeleteMulti(append(keys, k)), ShouldBeNil) // TODO(riannucci): replace with a Count query instead of this cast /* ents := ds.(*dsImpl).data.head.GetCollection("ents:") num, _ := ents.GetTotals() // /__entity_root_ids__,Foo // /Foo,1/__entity_group__,1 // /Foo,1/__entity_group_ids__,1 So(num, ShouldEqual, 3) */ So(testGetMeta(c, k), ShouldEqual, 22) So(ds.Put(&Foo{Id: 1}), ShouldBeNil) So(testGetMeta(c, k), ShouldEqual, 23) }) Convey("can Get", func() { vals := make([]dsS.PropertyMap, len(keys)) for i := range vals { vals[i] = dsS.PropertyMap{} vals[i].SetMeta("key", keys[i]) } So(ds.GetMulti(vals), ShouldBeNil) for i, val := range vals { So(val, ShouldResemble, dsS.PropertyMap{ "Val": {dsS.MkProperty(10)}, "$key": {dsS.MkPropertyNI(keys[i])}, }) } }) }) }) Convey("implements DSTransactioner", func() { Convey("Put", func() { f := &Foo{Val: 10} So(ds.Put(f), ShouldBeNil) k := ds.KeyForObj(f) So(k.String(), ShouldEqual, "/Foo,1") Convey("can Put new entity groups", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) f := &Foo{Val: 100} So(ds.Put(f), ShouldBeNil) So(f.Id, ShouldEqual, 2) f.Id = 0 f.Val = 200 So(ds.Put(f), ShouldBeNil) So(f.Id, ShouldEqual, 3) return nil }, &dsS.TransactionOptions{XG: true}) So(err, ShouldBeNil) f := &Foo{Id: 2} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 100) f.Id = 3 So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 200) }) Convey("can Put new entities in a current group", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) f := &Foo{Val: 100, Parent: k} So(ds.Put(f), ShouldBeNil) So(ds.KeyForObj(f).String(), ShouldEqual, "/Foo,1/Foo,1") f.Id = 0 f.Val = 200 So(ds.Put(f), ShouldBeNil) So(ds.KeyForObj(f).String(), ShouldEqual, "/Foo,1/Foo,2") return nil }, nil) So(err, ShouldBeNil) f := &Foo{Id: 1, Parent: k} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 100) f.Id = 2 So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 200) }) Convey("Deletes work too", func() { err := ds.RunInTransaction(func(c context.Context) error { return dsS.Get(c).Delete(k) }, nil) So(err, ShouldBeNil) So(ds.Get(&Foo{Id: 1}), ShouldEqual, dsS.ErrNoSuchEntity) }) Convey("A Get counts against your group count", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) pm := dsS.PropertyMap{} pm.SetMeta("key", ds.NewKey("Foo", "", 20, nil)) So(ds.Get(pm), ShouldEqual, dsS.ErrNoSuchEntity) pm.SetMeta("key", k) So(ds.Get(pm).Error(), ShouldContainSubstring, "cross-group") return nil }, nil) So(err, ShouldBeNil) }) Convey("Get takes a snapshot", func() { err := ds.RunInTransaction(func(c context.Context) error { txnDS := dsS.Get(c) So(txnDS.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) // Don't ever do this in a real program unless you want to guarantee // a failed transaction :) f.Val = 11 So(ds.Put(f), ShouldBeNil) So(txnDS.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) return nil }, nil) So(err, ShouldBeNil) f := &Foo{Id: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 11) }) Convey("and snapshots are consistent even after Puts", func() { err := ds.RunInTransaction(func(c context.Context) error { txnDS := dsS.Get(c) f := &Foo{Id: 1} So(txnDS.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) // Don't ever do this in a real program unless you want to guarantee // a failed transaction :) f.Val = 11 So(ds.Put(f), ShouldBeNil) So(txnDS.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) f.Val = 20 So(txnDS.Put(f), ShouldBeNil) So(txnDS.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) // still gets 10 return nil }, nil) So(err.Error(), ShouldContainSubstring, "concurrent") f := &Foo{Id: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 11) }) Convey("Reusing a transaction context is bad news", func() { txnDS := dsS.Interface(nil) err := ds.RunInTransaction(func(c context.Context) error { txnDS = dsS.Get(c) So(txnDS.Get(f), ShouldBeNil) return nil }, nil) So(err, ShouldBeNil) So(txnDS.Get(f).Error(), ShouldContainSubstring, "expired") }) Convey("Nested transactions are rejected", func() { err := ds.RunInTransaction(func(c context.Context) error { err := dsS.Get(c).RunInTransaction(func(c context.Context) error { panic("noooo") }, nil) So(err.Error(), ShouldContainSubstring, "nested transactions") return nil }, nil) So(err, ShouldBeNil) }) Convey("Concurrent transactions only accept one set of changes", func() { // Note: I think this implementation is actually /slightly/ wrong. // Accorting to my read of the docs for appengine, when you open a // transaction it actually (essentially) holds a reference to the // entire datastore. Our implementation takes a snapshot of the // entity group as soon as something observes/affects it. // // That said... I'm not sure if there's really a semantic difference. err := ds.RunInTransaction(func(c context.Context) error { So(dsS.Get(c).Put(&Foo{Id: 1, Val: 21}), ShouldBeNil) err := ds.RunInTransaction(func(c context.Context) error { So(dsS.Get(c).Put(&Foo{Id: 1, Val: 27}), ShouldBeNil) return nil }, nil) So(err, ShouldBeNil) return nil }, nil) So(err.Error(), ShouldContainSubstring, "concurrent") f := &Foo{Id: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 27) }) Convey("XG", func() { Convey("Modifying two groups with XG=false is invalid", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) f := &Foo{Id: 1, Val: 200} So(ds.Put(f), ShouldBeNil) f.Id = 2 err := ds.Put(f) So(err.Error(), ShouldContainSubstring, "cross-group") return err }, nil) So(err.Error(), ShouldContainSubstring, "cross-group") }) Convey("Modifying >25 groups with XG=true is invald", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) foos := make([]Foo, 25) for i := int64(1); i < 26; i++ { foos[i-1].Id = i foos[i-1].Val = 200 } So(ds.PutMulti(foos), ShouldBeNil) err := ds.Put(&Foo{Id: 26}) So(err.Error(), ShouldContainSubstring, "too many entity groups") return err }, &dsS.TransactionOptions{XG: true}) So(err.Error(), ShouldContainSubstring, "too many entity groups") }) }) Convey("Errors and panics", func() { Convey("returning an error aborts", func() { err := ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) So(ds.Put(&Foo{Id: 1, Val: 200}), ShouldBeNil) return fmt.Errorf("thingy") }, nil) So(err.Error(), ShouldEqual, "thingy") f := &Foo{Id: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) }) Convey("panicing aborts", func() { So(func() { ds.RunInTransaction(func(c context.Context) error { ds := dsS.Get(c) So(ds.Put(&Foo{Val: 200}), ShouldBeNil) panic("wheeeeee") }, nil) }, ShouldPanic) f := &Foo{Id: 1} So(ds.Get(f), ShouldBeNil) So(f.Val, ShouldEqual, 10) }) }) }) }) }) }
func TestCount(t *testing.T) { t.Parallel() Convey("Test Count filter", t, func() { c, fb := featureBreaker.FilterRDS(memory.Use(context.Background()), nil) c, ctr := FilterRDS(c) So(c, ShouldNotBeNil) So(ctr, ShouldNotBeNil) ds := datastore.Get(c) vals := []datastore.PropertyMap{{ "Val": {datastore.MkProperty(100)}, "$key": {datastore.MkPropertyNI(ds.NewKey("Kind", "", 1, nil))}, }} Convey("Calling a ds function should reflect in counter", func() { So(ds.PutMulti(vals), ShouldBeNil) So(ctr.PutMulti.Successes(), ShouldEqual, 1) Convey("effects are cumulative", func() { So(ds.PutMulti(vals), ShouldBeNil) So(ctr.PutMulti.Successes(), ShouldEqual, 2) Convey("even within transactions", func() { die(ds.RunInTransaction(func(c context.Context) error { ds := datastore.Get(c) So(ds.PutMulti(append(vals, vals[0])), ShouldBeNil) return nil }, nil)) }) }) }) Convey("errors count against errors", func() { fb.BreakFeatures(nil, "GetMulti") So(ds.GetMulti(vals), ShouldErrLike, `"GetMulti" is broken`) So(ctr.GetMulti.Errors(), ShouldEqual, 1) fb.UnbreakFeatures("GetMulti") So(ds.PutMulti(vals), ShouldBeNil) die(ds.GetMulti(vals)) So(ctr.GetMulti.Errors(), ShouldEqual, 1) So(ctr.GetMulti.Successes(), ShouldEqual, 1) So(ctr.GetMulti.Total(), ShouldEqual, 2) }) }) Convey("works for memcache", t, func() { c, ctr := FilterMC(memory.Use(context.Background())) So(c, ShouldNotBeNil) So(ctr, ShouldNotBeNil) mc := memcache.Get(c) die(mc.Set(mc.NewItem("hello").SetValue([]byte("sup")))) _, err := mc.Get("Wat") So(err, ShouldNotBeNil) _, err = mc.Get("hello") die(err) So(ctr.SetMulti, shouldHaveSuccessesAndErrors, 1, 0) So(ctr.GetMulti, shouldHaveSuccessesAndErrors, 2, 0) So(ctr.NewItem, shouldHaveSuccessesAndErrors, 3, 0) }) Convey("works for taskqueue", t, func() { c, ctr := FilterTQ(memory.Use(context.Background())) So(c, ShouldNotBeNil) So(ctr, ShouldNotBeNil) tq := taskqueue.Get(c) die(tq.Add(&taskqueue.Task{Name: "wat"}, "")) So(tq.Add(&taskqueue.Task{Name: "wat"}, "DNE_QUEUE"), ShouldErrLike, "UNKNOWN_QUEUE") So(ctr.AddMulti, shouldHaveSuccessesAndErrors, 1, 1) }) Convey("works for global info", t, func() { c, fb := featureBreaker.FilterGI(memory.Use(context.Background()), nil) c, ctr := FilterGI(c) So(c, ShouldNotBeNil) So(ctr, ShouldNotBeNil) gi := info.Get(c) _, err := gi.Namespace("foo") die(err) fb.BreakFeatures(nil, "Namespace") _, err = gi.Namespace("boom") So(err, ShouldErrLike, `"Namespace" is broken`) So(ctr.Namespace, shouldHaveSuccessesAndErrors, 1, 1) }) Convey("works for user", t, func() { c, fb := featureBreaker.FilterUser(memory.Use(context.Background()), nil) c, ctr := FilterUser(c) So(c, ShouldNotBeNil) So(ctr, ShouldNotBeNil) u := user.Get(c) _, err := u.CurrentOAuth("foo") die(err) fb.BreakFeatures(nil, "CurrentOAuth") _, err = u.CurrentOAuth("foo") So(err, ShouldErrLike, `"CurrentOAuth" is broken`) So(ctr.CurrentOAuth, shouldHaveSuccessesAndErrors, 1, 1) }) Convey("works for mail", t, func() { c, fb := featureBreaker.FilterMail(memory.Use(context.Background()), nil) c, ctr := FilterMail(c) So(c, ShouldNotBeNil) So(ctr, ShouldNotBeNil) m := mail.Get(c) err := m.Send(&mail.Message{ Sender: "*****@*****.**", To: []string{"*****@*****.**"}, Body: "hi", }) die(err) fb.BreakFeatures(nil, "Send") err = m.Send(&mail.Message{ Sender: "*****@*****.**", To: []string{"*****@*****.**"}, Body: "hi", }) So(err, ShouldErrLike, `"Send" is broken`) So(ctr.Send, shouldHaveSuccessesAndErrors, 1, 1) }) }
func TestQueryExecution(t *testing.T) { t.Parallel() Convey("Test query execution", t, func() { c, err := info.Get(Use(context.Background())).Namespace("ns") if err != nil { panic(err) } So(info.Get(c).FullyQualifiedAppID(), ShouldEqual, "dev~app") So(info.Get(c).GetNamespace(), ShouldEqual, "ns") data := ds.Get(c) testing := data.Testable() for _, tc := range queryExecutionTests { Convey(tc.name, func() { for i, stage := range tc.test { // outside of Convey, since these must always happen testing.CatchupIndexes() testing.AddIndexes(stage.addIdxs...) if err := data.PutMulti(stage.putEnts); err != nil { // prevent Convey from thinking this assertion should show up in // every test loop. panic(err) } if err := data.DeleteMulti(stage.delEnts); err != nil { panic(err) } Convey(fmt.Sprintf("stage %d", i), func() { for j, expect := range stage.expect { runner := func(f func(ic context.Context) error, _ *ds.TransactionOptions) error { return f(c) } if expect.inTxn { runner = data.RunInTransaction } if expect.count == 0 { if len(expect.keys) > 0 { expect.count = len(expect.keys) } else { expect.count = len(expect.get) } } if expect.keys != nil { Convey(fmt.Sprintf("expect %d (keys)", j), func() { err := runner(func(c context.Context) error { data := ds.Get(c) count, err := data.Count(expect.q) So(err, ShouldBeNil) So(count, ShouldEqual, expect.count) rslt := []*ds.Key(nil) So(data.GetAll(expect.q, &rslt), ShouldBeNil) So(len(rslt), ShouldEqual, len(expect.keys)) for i, r := range rslt { So(r, ShouldResemble, expect.keys[i]) } return nil }, &ds.TransactionOptions{XG: true}) So(err, ShouldBeNil) }) } if expect.get != nil { Convey(fmt.Sprintf("expect %d (data)", j), func() { err := runner(func(c context.Context) error { data := ds.Get(c) count, err := data.Count(expect.q) So(err, ShouldBeNil) So(count, ShouldEqual, expect.count) rslt := []ds.PropertyMap(nil) So(data.GetAll(expect.q, &rslt), ShouldBeNil) So(len(rslt), ShouldEqual, len(expect.get)) for i, r := range rslt { So(r, ShouldResembleV, expect.get[i]) } return nil }, &ds.TransactionOptions{XG: true}) So(err, ShouldBeNil) }) } } for j, fn := range stage.extraFns { Convey(fmt.Sprintf("extraFn %d", j), func() { fn(c) }) } }) } }) } }) Convey("Test AutoIndex", t, func() { c, err := info.Get(Use(context.Background())).Namespace("ns") if err != nil { panic(err) } data := ds.Get(c) testing := data.Testable() testing.Consistent(true) So(data.Put(pmap("$key", key("Kind", 1), Next, "Val", 1, 2, 3, Next, "Extra", "hello", )), ShouldBeNil) So(data.Put(pmap("$key", key("Kind", 2), Next, "Val", 2, 3, 9, Next, "Extra", "ace", "hello", "there", )), ShouldBeNil) q := nq("Kind").Gt("Val", 2).Order("Val", "Extra") count, err := data.Count(q) So(err, ShouldErrLike, "Insufficient indexes") testing.AutoIndex(true) count, err = data.Count(q) So(err, ShouldBeNil) So(count, ShouldEqual, 2) }) }
func TestContextAccess(t *testing.T) { t.Parallel() // p is a function which recovers an error and then immediately panics with // the contained string. It's defer'd in each test so that we can use the // ShouldPanicWith assertion (which does an == comparison and not // a reflect.DeepEquals comparison). p := func() { panic(recover().(error).Error()) } Convey("Context Access", t, func() { c := context.Background() Convey("blank", func() { So(dsS.GetRaw(c), ShouldBeNil) So(mcS.GetRaw(c), ShouldBeNil) So(tqS.GetRaw(c), ShouldBeNil) So(infoS.Get(c), ShouldBeNil) }) // needed for everything else c = infoS.Set(c, Info()) Convey("Info", func() { So(infoS.Get(c), ShouldNotBeNil) So(func() { defer p() infoS.Get(c).Datacenter() }, ShouldPanicWith, "dummy: method Info.Datacenter is not implemented") }) Convey("Datastore", func() { c = dsS.SetRaw(c, Datastore()) So(dsS.Get(c), ShouldNotBeNil) So(func() { defer p() _, _ = dsS.Get(c).DecodeCursor("wut") }, ShouldPanicWith, "dummy: method Datastore.DecodeCursor is not implemented") }) Convey("Memcache", func() { c = mcS.SetRaw(c, Memcache()) So(mcS.Get(c), ShouldNotBeNil) So(func() { defer p() _ = mcS.Get(c).Add(nil) }, ShouldPanicWith, "dummy: method Memcache.AddMulti is not implemented") }) Convey("TaskQueue", func() { c = tqS.SetRaw(c, TaskQueue()) So(tqS.Get(c), ShouldNotBeNil) So(func() { defer p() _ = tqS.Get(c).Purge("") }, ShouldPanicWith, "dummy: method TaskQueue.Purge is not implemented") }) Convey("User", func() { c = userS.Set(c, User()) So(userS.Get(c), ShouldNotBeNil) So(func() { defer p() _ = userS.Get(c).IsAdmin() }, ShouldPanicWith, "dummy: method User.IsAdmin is not implemented") }) Convey("Mail", func() { c = mailS.Set(c, Mail()) So(mailS.Get(c), ShouldNotBeNil) So(func() { defer p() _ = mailS.Get(c).Send(nil) }, ShouldPanicWith, "dummy: method Mail.Send is not implemented") }) }) }