go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/impl/prod/everything_test.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:build appengine 16 // +build appengine 17 18 package prod 19 20 import ( 21 "context" 22 "testing" 23 "time" 24 25 "go.chromium.org/luci/gae/service/blobstore" 26 ds "go.chromium.org/luci/gae/service/datastore" 27 "go.chromium.org/luci/gae/service/info" 28 mc "go.chromium.org/luci/gae/service/memcache" 29 30 "go.chromium.org/luci/common/logging" 31 "google.golang.org/appengine/aetest" 32 33 . "github.com/smartystreets/goconvey/convey" 34 ) 35 36 var ( 37 mp = ds.MkProperty 38 mpNI = ds.MkPropertyNI 39 ) 40 41 type TestStruct struct { 42 ID int64 `gae:"$id"` 43 44 ValueI []int64 45 ValueB []bool 46 ValueS []string 47 ValueF []float64 48 ValueBS [][]byte // "ByteString" 49 ValueK []*ds.Key 50 ValueBK []blobstore.Key 51 ValueGP []ds.GeoPoint 52 ValueSingle string 53 ValueSingleSlice []string 54 } 55 56 func TestBasicDatastore(t *testing.T) { 57 t.Parallel() 58 59 Convey("basic", t, func() { 60 inst, err := aetest.NewInstance(&aetest.Options{ 61 StronglyConsistentDatastore: true, 62 }) 63 So(err, ShouldBeNil) 64 defer inst.Close() 65 66 req, err := inst.NewRequest("GET", "/", nil) 67 So(err, ShouldBeNil) 68 69 ctx := Use(context.Background(), req) 70 71 Convey("logging allows you to tweak the level", func() { 72 // You have to visually confirm that this actually happens in the stdout 73 // of the test... yeah I know. 74 logging.Debugf(ctx, "SHOULD NOT SEE") 75 logging.Infof(ctx, "SHOULD SEE") 76 77 ctx = logging.SetLevel(ctx, logging.Debug) 78 logging.Debugf(ctx, "SHOULD SEE") 79 logging.Infof(ctx, "SHOULD SEE (2)") 80 }) 81 82 Convey("Can probe/change Namespace", func() { 83 So(info.GetNamespace(ctx), ShouldEqual, "") 84 85 ctx, err = info.Namespace(ctx, "wat") 86 So(err, ShouldBeNil) 87 88 So(info.GetNamespace(ctx), ShouldEqual, "wat") 89 So(ds.MakeKey(ctx, "Hello", "world").Namespace(), ShouldEqual, "wat") 90 }) 91 92 Convey("Can get non-transactional context", func() { 93 ctx, err := info.Namespace(ctx, "foo") 94 So(err, ShouldBeNil) 95 96 So(ds.CurrentTransaction(ctx), ShouldBeNil) 97 98 ds.RunInTransaction(ctx, func(ctx context.Context) error { 99 So(ds.CurrentTransaction(ctx), ShouldNotBeNil) 100 So(ds.MakeKey(ctx, "Foo", "bar").Namespace(), ShouldEqual, "foo") 101 102 So(ds.Put(ctx, &TestStruct{ValueI: []int64{100}}), ShouldBeNil) 103 104 noTxnCtx := ds.WithoutTransaction(ctx) 105 So(ds.CurrentTransaction(noTxnCtx), ShouldBeNil) 106 107 err = ds.RunInTransaction(noTxnCtx, func(ctx context.Context) error { 108 So(ds.CurrentTransaction(ctx), ShouldNotBeNil) 109 So(ds.MakeKey(ctx, "Foo", "bar").Namespace(), ShouldEqual, "foo") 110 So(ds.Put(ctx, &TestStruct{ValueI: []int64{100}}), ShouldBeNil) 111 return nil 112 }, nil) 113 So(err, ShouldBeNil) 114 115 return nil 116 }, nil) 117 }) 118 119 Convey("Can Put/Get", func() { 120 orig := TestStruct{ 121 ValueI: []int64{1, 7, 946688461000000, 996688461000000}, 122 ValueB: []bool{true, false}, 123 ValueS: []string{"hello", "world"}, 124 ValueF: []float64{1.0, 7.0, 946688461000000.0, 996688461000000.0}, 125 ValueBS: [][]byte{ 126 []byte("allo"), 127 []byte("hello"), 128 []byte("world"), 129 []byte("zurple"), 130 }, 131 ValueK: []*ds.Key{ 132 ds.NewKey(ctx, "Something", "Cool", 0, nil), 133 ds.NewKey(ctx, "Something", "", 1, nil), 134 ds.NewKey(ctx, "Something", "Recursive", 0, 135 ds.NewKey(ctx, "Parent", "", 2, nil)), 136 }, 137 ValueBK: []blobstore.Key{"bellow", "hello"}, 138 ValueGP: []ds.GeoPoint{ 139 {Lat: 120.7, Lng: 95.5}, 140 }, 141 ValueSingle: "ohai", 142 ValueSingleSlice: []string{"kthxbye"}, 143 } 144 So(ds.Put(ctx, &orig), ShouldBeNil) 145 146 ret := TestStruct{ID: orig.ID} 147 So(ds.Get(ctx, &ret), ShouldBeNil) 148 So(ret, ShouldResemble, orig) 149 150 // make sure single- and multi- properties are preserved. 151 pmap := ds.PropertyMap{ 152 "$id": mpNI(orig.ID), 153 "$kind": mpNI("TestStruct"), 154 } 155 So(ds.Get(ctx, pmap), ShouldBeNil) 156 So(pmap["ValueSingle"], ShouldHaveSameTypeAs, ds.Property{}) 157 So(pmap["ValueSingleSlice"], ShouldHaveSameTypeAs, ds.PropertySlice(nil)) 158 159 // can't be sure the indexes have caught up... so sleep 160 time.Sleep(time.Second) 161 162 Convey("Can query", func() { 163 q := ds.NewQuery("TestStruct") 164 ds.Run(ctx, q, func(ts *TestStruct) { 165 So(*ts, ShouldResemble, orig) 166 }) 167 count, err := ds.Count(ctx, q) 168 So(err, ShouldBeNil) 169 So(count, ShouldEqual, 1) 170 }) 171 172 Convey("Can query for bytes", func() { 173 q := ds.NewQuery("TestStruct").Eq("ValueBS", []byte("allo")) 174 ds.Run(ctx, q, func(ts *TestStruct) { 175 So(*ts, ShouldResemble, orig) 176 }) 177 count, err := ds.Count(ctx, q) 178 So(err, ShouldBeNil) 179 So(count, ShouldEqual, 1) 180 }) 181 182 Convey("Can project", func() { 183 q := ds.NewQuery("TestStruct").Project("ValueS") 184 rslts := []ds.PropertyMap{} 185 So(ds.GetAll(ctx, q, &rslts), ShouldBeNil) 186 So(rslts, ShouldResemble, []ds.PropertyMap{ 187 { 188 "$key": mpNI(ds.KeyForObj(ctx, &orig)), 189 "ValueS": mp("hello"), 190 }, 191 { 192 "$key": mpNI(ds.KeyForObj(ctx, &orig)), 193 "ValueS": mp("world"), 194 }, 195 }) 196 197 q = ds.NewQuery("TestStruct").Project("ValueBS") 198 rslts = []ds.PropertyMap{} 199 So(ds.GetAll(ctx, q, &rslts), ShouldBeNil) 200 So(rslts, ShouldResemble, []ds.PropertyMap{ 201 { 202 "$key": mpNI(ds.KeyForObj(ctx, &orig)), 203 "ValueBS": mp("allo"), 204 }, 205 { 206 "$key": mpNI(ds.KeyForObj(ctx, &orig)), 207 "ValueBS": mp("hello"), 208 }, 209 { 210 "$key": mpNI(ds.KeyForObj(ctx, &orig)), 211 "ValueBS": mp("world"), 212 }, 213 { 214 "$key": mpNI(ds.KeyForObj(ctx, &orig)), 215 "ValueBS": mp("zurple"), 216 }, 217 }) 218 219 count, err := ds.Count(ctx, q) 220 So(err, ShouldBeNil) 221 So(count, ShouldEqual, 4) 222 223 q = ds.NewQuery("TestStruct").Lte("ValueI", 7).Project("ValueS").Distinct(true) 224 rslts = []ds.PropertyMap{} 225 So(ds.GetAll(ctx, q, &rslts), ShouldBeNil) 226 So(rslts, ShouldResemble, []ds.PropertyMap{ 227 { 228 "$key": mpNI(ds.KeyForObj(ctx, &orig)), 229 "ValueI": mp(1), 230 "ValueS": mp("hello"), 231 }, 232 { 233 "$key": mpNI(ds.KeyForObj(ctx, &orig)), 234 "ValueI": mp(1), 235 "ValueS": mp("world"), 236 }, 237 { 238 "$key": mpNI(ds.KeyForObj(ctx, &orig)), 239 "ValueI": mp(7), 240 "ValueS": mp("hello"), 241 }, 242 { 243 "$key": mpNI(ds.KeyForObj(ctx, &orig)), 244 "ValueI": mp(7), 245 "ValueS": mp("world"), 246 }, 247 }) 248 249 count, err = ds.Count(ctx, q) 250 So(err, ShouldBeNil) 251 So(count, ShouldEqual, 4) 252 }) 253 }) 254 255 Convey("Can Put/Get (time)", func() { 256 // time comparisons in Go are wonky, so this is pulled out 257 pm := ds.PropertyMap{ 258 "$key": mpNI(ds.NewKey(ctx, "Something", "value", 0, nil)), 259 "Time": ds.PropertySlice{ 260 mp(time.Date(1938, time.January, 1, 1, 1, 1, 1, time.UTC)), 261 mp(time.Time{}), 262 }, 263 } 264 So(ds.Put(ctx, &pm), ShouldBeNil) 265 266 rslt := ds.PropertyMap{} 267 rslt.SetMeta("key", ds.KeyForObj(ctx, pm)) 268 So(ds.Get(ctx, &rslt), ShouldBeNil) 269 270 So(pm.Slice("Time")[0].Value(), ShouldResemble, rslt.Slice("Time")[0].Value()) 271 272 q := ds.NewQuery("Something").Project("Time") 273 all := []ds.PropertyMap{} 274 So(ds.GetAll(ctx, q, &all), ShouldBeNil) 275 So(len(all), ShouldEqual, 2) 276 prop := all[0].Slice("Time")[0] 277 So(prop.Type(), ShouldEqual, ds.PTInt) 278 279 tval, err := prop.Project(ds.PTTime) 280 So(err, ShouldBeNil) 281 So(tval, ShouldResemble, time.Time{}.UTC()) 282 283 tval, err = all[1].Slice("Time")[0].Project(ds.PTTime) 284 So(err, ShouldBeNil) 285 So(tval, ShouldResemble, pm.Slice("Time")[0].Value()) 286 287 ent := ds.PropertyMap{ 288 "$key": mpNI(ds.MakeKey(ctx, "Something", "value")), 289 } 290 So(ds.Get(ctx, &ent), ShouldBeNil) 291 So(ent["Time"], ShouldResemble, pm["Time"]) 292 }) 293 294 Convey(`Can Get empty []byte slice as nil`, func() { 295 put := ds.PropertyMap{ 296 "$id": mpNI("foo"), 297 "$kind": mpNI("FooType"), 298 "Empty": mp([]byte(nil)), 299 "Nilly": mp([]byte{}), 300 } 301 get := ds.PropertyMap{ 302 "$id": put["$id"], 303 "$kind": put["$kind"], 304 } 305 exp := put.Clone() 306 exp["Nilly"] = mp([]byte(nil)) 307 308 So(ds.Put(ctx, put), ShouldBeNil) 309 So(ds.Get(ctx, get), ShouldBeNil) 310 So(get, ShouldResemble, exp) 311 }) 312 313 Convey("memcache: Set (nil) is the same as Set ([]byte{})", func() { 314 So(mc.Set(ctx, mc.NewItem(ctx, "bob")), ShouldBeNil) // normally would panic because Value is nil 315 316 bob, err := mc.GetKey(ctx, "bob") 317 So(err, ShouldBeNil) 318 So(bob.Value(), ShouldResemble, []byte{}) 319 }) 320 }) 321 } 322 323 func BenchmarkTransactionsParallel(b *testing.B) { 324 type Counter struct { 325 ID int `gae:"$id"` 326 Count int 327 } 328 329 inst, err := aetest.NewInstance(&aetest.Options{ 330 StronglyConsistentDatastore: true, 331 }) 332 if err != nil { 333 b.Fatalf("failed to initialize aetest: %v", err) 334 } 335 defer inst.Close() 336 337 req, err := inst.NewRequest("GET", "/", nil) 338 if err != nil { 339 b.Fatalf("failed to create GET request: %v", err) 340 } 341 342 ctx := Use(context.Background(), req) 343 344 b.ResetTimer() 345 346 b.RunParallel(func(pb *testing.PB) { 347 for pb.Next() { 348 ctr := Counter{ID: 1} 349 350 err := ds.RunInTransaction(ctx, func(ctx context.Context) error { 351 switch err := ds.Get(ctx, &ctr); err { 352 case nil, ds.ErrNoSuchEntity: 353 ctr.Count++ 354 return ds.Put(ctx, &ctr) 355 356 default: 357 return err 358 } 359 }, &ds.TransactionOptions{Attempts: 9999999}) 360 if err != nil { 361 b.Fatalf("failed to run transaction: %v", err) 362 } 363 } 364 }) 365 }