go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/impl/cloud/datastore_test.go (about) 1 // Copyright 2016 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 package cloud 16 17 import ( 18 "context" 19 "crypto/rand" 20 "encoding/hex" 21 "fmt" 22 "os" 23 "testing" 24 "time" 25 26 "cloud.google.com/go/datastore" 27 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/common/errors" 30 31 ds "go.chromium.org/luci/gae/service/datastore" 32 "go.chromium.org/luci/gae/service/info" 33 34 . "github.com/smartystreets/goconvey/convey" 35 ) 36 37 func mkProperties(index bool, forceMulti bool, vals ...any) ds.PropertyData { 38 indexSetting := ds.ShouldIndex 39 if !index { 40 indexSetting = ds.NoIndex 41 } 42 43 if len(vals) == 1 && !forceMulti { 44 var prop ds.Property 45 prop.SetValue(vals[0], indexSetting) 46 return prop 47 } 48 49 result := make(ds.PropertySlice, len(vals)) 50 for i, v := range vals { 51 result[i].SetValue(v, indexSetting) 52 } 53 return result 54 } 55 56 func mkp(vals ...any) ds.PropertyData { return mkProperties(true, false, vals...) } 57 func mkpNI(vals ...any) ds.PropertyData { return mkProperties(false, false, vals...) } 58 59 // shouldBeUntypedNil asserts that actual is nil, with a nil type pointer 60 // For example: https://play.golang.org/p/qN25iAYQw5Z 61 func shouldBeUntypedNil(actual any, _ ...any) string { 62 if actual == nil { 63 return "" 64 } 65 return fmt.Sprintf(`Expected: (%T, %v) 66 Actual: (%T, %v)`, nil, nil, actual, actual) 67 } 68 69 func TestBoundDatastore(t *testing.T) { 70 t.Parallel() 71 72 Convey("boundDatastore", t, func() { 73 kc := ds.KeyContext{ 74 AppID: "app-id", 75 Namespace: "ns", 76 } 77 78 Convey("*datastore.Entity", func() { 79 ent := &datastore.Entity{ 80 Key: &datastore.Key{ 81 ID: 1, 82 Kind: "Kind", 83 Namespace: "ns", 84 Parent: &datastore.Key{ 85 Name: "p", 86 Kind: "Parent", 87 Namespace: "ns", 88 }, 89 }, 90 Properties: []datastore.Property{ 91 { 92 Name: "bool", 93 Value: true, 94 }, 95 { 96 Name: "entity", 97 Value: &datastore.Entity{ 98 Properties: []datastore.Property{ 99 { 100 Name: "[]byte", 101 NoIndex: true, 102 Value: []byte("byte"), 103 }, 104 { 105 Name: "[]interface", 106 NoIndex: true, 107 Value: []any{"interface"}, 108 }, 109 { 110 Name: "geopoint", 111 NoIndex: true, 112 Value: datastore.GeoPoint{ 113 Lat: 1, 114 Lng: 1, 115 }, 116 }, 117 { 118 Name: "indexed", 119 Value: "hi", 120 }, 121 }, 122 }, 123 }, 124 { 125 Name: "float64", 126 Value: 1.0, 127 }, 128 { 129 Name: "int64", 130 Value: int64(1), 131 }, 132 { 133 Name: "key", 134 Value: &datastore.Key{ 135 ID: 2, 136 Kind: "kind", 137 Namespace: "ns", 138 }, 139 }, 140 { 141 Name: "string", 142 Value: "string", 143 }, 144 { 145 Name: "time", 146 Value: ds.RoundTime(testclock.TestRecentTimeUTC), 147 }, 148 { 149 Name: "unindexed_entity", 150 NoIndex: true, 151 Value: &datastore.Entity{ 152 Properties: []datastore.Property{ 153 { 154 Name: "field", 155 NoIndex: true, 156 Value: "ho", 157 }, 158 }, 159 }, 160 }, 161 }, 162 } 163 164 parent := kc.NewKey("Parent", "p", 0, nil) 165 key := kc.NewKey("Kind", "", 1, parent) 166 167 pm := ds.PropertyMap{ 168 "$key": ds.MkPropertyNI(key), 169 "$parent": ds.MkPropertyNI(parent), 170 "$kind": ds.MkPropertyNI("Kind"), 171 "$id": ds.MkPropertyNI(1), 172 "bool": ds.MkProperty(true), 173 "entity": ds.MkProperty(ds.PropertyMap{ 174 "[]byte": ds.MkPropertyNI([]byte("byte")), 175 "[]interface": ds.PropertySlice{ 176 ds.MkPropertyNI("interface"), 177 }, 178 "geopoint": ds.MkPropertyNI(ds.GeoPoint{Lat: 1, Lng: 1}), 179 "indexed": ds.MkProperty("hi"), 180 }), 181 "unindexed_entity": ds.MkPropertyNI(ds.PropertyMap{ 182 "field": ds.MkPropertyNI("ho"), 183 }), 184 "float64": ds.MkProperty(1.0), 185 "int64": ds.MkProperty(int64(1)), 186 "key": ds.MkProperty(kc.NewKey("kind", "", 2, nil)), 187 "string": ds.MkProperty("string"), 188 "time": ds.MkProperty(ds.RoundTime(testclock.TestRecentTimeUTC)), 189 } 190 191 Convey("gaeEntityToNative", func() { 192 So(gaeEntityToNative(kc, pm), ShouldResemble, ent) 193 }) 194 195 Convey("nativeEntityToGAE", func() { 196 So(nativeEntityToGAE(kc, ent), ShouldResemble, pm) 197 }) 198 199 Convey("gaeEntityToNative, nativeEntityToGAE", func() { 200 So(nativeEntityToGAE(kc, gaeEntityToNative(kc, pm)), ShouldResemble, pm) 201 }) 202 203 Convey("nativeEntityToGAE, gaeEntityToNative", func() { 204 So(gaeEntityToNative(kc, nativeEntityToGAE(kc, ent)), ShouldResemble, ent) 205 }) 206 }) 207 }) 208 } 209 210 // TestDatastore tests the cloud datastore implementation. 211 // 212 // Run the emulator: 213 // $ gcloud beta emulators datastore start --use-firestore-in-datastore-mode 214 // 215 // Export the DATASTORE_EMULATOR_HOST environment variable, which the above 216 // command printed. 217 // 218 // If the emulator environment is not detected, this test will be skipped. 219 func TestDatastore(t *testing.T) { 220 t.Parallel() 221 222 // See if an emulator is running. If no emulator is running, we will skip this 223 // test suite. 224 emulatorHost := os.Getenv("DATASTORE_EMULATOR_HOST") 225 if emulatorHost == "" { 226 t.Skip("No emulator detected (DATASTORE_EMULATOR_HOST). Skipping test suite.") 227 return 228 } 229 230 Convey(fmt.Sprintf(`A cloud installation using datastore emulator %q`, emulatorHost), t, func() { 231 c := context.Background() 232 client, err := datastore.NewClient(c, "luci-gae-test") 233 So(err, ShouldBeNil) 234 defer client.Close() 235 236 testTime := ds.RoundTime(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)) 237 _ = testTime 238 239 cfg := ConfigLite{ProjectID: "luci-gae-test", DS: client} 240 c = cfg.Use(c) 241 242 Convey(`Supports namespaces`, func() { 243 namespaces := []string{"foo", "bar", "baz"} 244 245 // Clear all used entities from all namespaces. 246 for _, ns := range namespaces { 247 nsCtx := info.MustNamespace(c, ns) 248 249 keys := make([]*ds.Key, len(namespaces)) 250 for i := range keys { 251 keys[i] = ds.MakeKey(nsCtx, "Test", i+1) 252 } 253 So(errors.Filter(ds.Delete(nsCtx, keys), ds.ErrNoSuchEntity), ShouldBeNil) 254 } 255 256 // Put one entity per namespace. 257 for i, ns := range namespaces { 258 nsCtx := info.MustNamespace(c, ns) 259 260 pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp(i + 1), "Value": mkp(i)} 261 So(ds.Put(nsCtx, pmap), ShouldBeNil) 262 } 263 264 // Make sure that entity only exists in that namespace. 265 for _, ns := range namespaces { 266 nsCtx := info.MustNamespace(c, ns) 267 268 for i := range namespaces { 269 pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp(i + 1)} 270 err := ds.Get(nsCtx, pmap) 271 272 if namespaces[i] == ns { 273 So(err, ShouldBeNil) 274 } else { 275 So(err, ShouldEqual, ds.ErrNoSuchEntity) 276 } 277 } 278 } 279 }) 280 281 Convey(`In a clean random testing namespace`, func() { 282 // Enter a namespace for this round of tests. 283 randNamespace := make([]byte, 32) 284 if _, err := rand.Read(randNamespace); err != nil { 285 panic(err) 286 } 287 c = info.MustNamespace(c, fmt.Sprintf("testing-%s", hex.EncodeToString(randNamespace))) 288 289 // Execute a kindless query to clear the namespace. 290 q := ds.NewQuery("").KeysOnly(true) 291 var allKeys []*ds.Key 292 So(ds.GetAll(c, q, &allKeys), ShouldBeNil) 293 So(ds.Delete(c, allKeys), ShouldBeNil) 294 295 Convey(`Can allocate an ID range`, func() { 296 var keys []*ds.Key 297 keys = append(keys, ds.NewIncompleteKeys(c, 10, "Bar", ds.MakeKey(c, "Foo", 12))...) 298 keys = append(keys, ds.NewIncompleteKeys(c, 10, "Baz", ds.MakeKey(c, "Foo", 12))...) 299 300 seen := map[string]struct{}{} 301 So(ds.AllocateIDs(c, keys), ShouldBeNil) 302 for _, k := range keys { 303 So(k.IsIncomplete(), ShouldBeFalse) 304 seen[k.String()] = struct{}{} 305 } 306 307 So(ds.AllocateIDs(c, keys), ShouldBeNil) 308 for _, k := range keys { 309 So(k.IsIncomplete(), ShouldBeFalse) 310 311 _, ok := seen[k.String()] 312 So(ok, ShouldBeFalse) 313 } 314 }) 315 316 Convey(`Can get, put, and delete entities`, func() { 317 // Put: "foo", "bar", "baz". 318 put := []ds.PropertyMap{ 319 {"$kind": mkp("test"), "$id": mkp("foo"), "Value": mkp(1337)}, 320 {"$kind": mkp("test"), "$id": mkp("bar"), "Value": mkp(42)}, 321 {"$kind": mkp("test"), "$id": mkp("baz"), "Value": mkp(0xd065)}, 322 } 323 So(ds.Put(c, put), ShouldBeNil) 324 325 // Delete: "bar". 326 So(ds.Delete(c, ds.MakeKey(c, "test", "bar")), ShouldBeNil) 327 328 // Get: "foo", "bar", "baz" 329 get := []ds.PropertyMap{ 330 {"$kind": mkp("test"), "$id": mkp("foo")}, 331 {"$kind": mkp("test"), "$id": mkp("bar")}, 332 {"$kind": mkp("test"), "$id": mkp("baz")}, 333 } 334 335 err := ds.Get(c, get) 336 So(err, ShouldHaveSameTypeAs, errors.MultiError(nil)) 337 338 merr := err.(errors.MultiError) 339 So(len(merr), ShouldEqual, 3) 340 So(merr[0], ShouldBeNil) 341 So(merr[1], ShouldEqual, ds.ErrNoSuchEntity) 342 So(merr[2], ShouldBeNil) 343 344 // put[1] will not be retrieved (delete) 345 put[1] = get[1] 346 So(get, ShouldResemble, put) 347 }) 348 349 Convey(`Can put and get all supported entity fields.`, func() { 350 put := ds.PropertyMap{ 351 "$id": mkpNI("foo"), 352 "$kind": mkpNI("FooType"), 353 354 "Number": mkp(1337), 355 "String": mkpNI("hello"), 356 "Bytes": mkp([]byte("world")), 357 "Time": mkp(testTime), 358 "Float": mkpNI(3.14), 359 "Key": mkp(ds.MakeKey(c, "Parent", "ParentID", "Child", 1337)), 360 "Null": mkp(nil), 361 "NullSlice": mkp(nil, nil), 362 363 "ComplexSlice": mkp(1337, "string", []byte("bytes"), testTime, float32(3.14), 364 float64(2.71), true, nil, ds.MakeKey(c, "SomeKey", "SomeID")), 365 366 "Single": mkp("single"), 367 "SingleSlice": mkProperties(true, true, "single"), // Force a single "multi" value. 368 "EmptySlice": ds.PropertySlice(nil), 369 370 "SingleEntity": mkp(ds.PropertyMap{ 371 "$id": mkpNI("inner"), 372 "$kind": mkpNI("Inner"), 373 "$key": mkpNI(ds.MakeKey(c, "Inner", "inner")), 374 "$parent": mkpNI(nil), 375 "prop": mkp(1), 376 "deeper": mkp(ds.PropertyMap{"deep": mkp(123)}), 377 }), 378 "SliceOfEntities": mkp( 379 ds.PropertyMap{"prop": mkp(2)}, 380 ds.PropertyMap{"prop": mkp(3)}, 381 ), 382 } 383 So(ds.Put(c, put), ShouldBeNil) 384 385 get := ds.PropertyMap{ 386 "$id": mkpNI("foo"), 387 "$kind": mkpNI("FooType"), 388 } 389 So(ds.Get(c, get), ShouldBeNil) 390 So(get, ShouldResemble, put) 391 }) 392 393 Convey(`Can Get empty []byte slice as nil`, func() { 394 put := ds.PropertyMap{ 395 "$id": mkpNI("foo"), 396 "$kind": mkpNI("FooType"), 397 "Empty": mkp([]byte(nil)), 398 "Nilly": mkp([]byte{}), 399 } 400 get := ds.PropertyMap{ 401 "$id": put["$id"], 402 "$kind": put["$kind"], 403 } 404 exp := put.Clone() 405 exp["Nilly"] = mkp([]byte(nil)) 406 407 So(ds.Put(c, put), ShouldBeNil) 408 So(ds.Get(c, get), ShouldBeNil) 409 So(get, ShouldResemble, exp) 410 }) 411 412 Convey(`With several entities installed`, func() { 413 So(ds.Put(c, []ds.PropertyMap{ 414 {"$kind": mkp("Test"), "$id": mkp("foo"), "FooBar": mkp(true)}, 415 {"$kind": mkp("Test"), "$id": mkp("bar"), "FooBar": mkp(true)}, 416 {"$kind": mkp("Test"), "$id": mkp("baz")}, 417 {"$kind": mkp("Test"), "$id": mkp("qux")}, 418 {"$kind": mkp("Test"), "$id": mkp("quux"), "$parent": mkp(ds.MakeKey(c, "Test", "baz"))}, 419 {"$kind": mkp("Test"), "$id": mkp("quuz"), "$parent": mkp(ds.MakeKey(c, "Test", "baz"))}, 420 // Entities for checking IN query. 421 {"$kind": mkp("AAA"), "$id": mkp("e1"), "Slice": mkp("a", "b")}, 422 {"$kind": mkp("AAA"), "$id": mkp("e2"), "Slice": mkp("a", "c")}, 423 }), ShouldBeNil) 424 425 withAllMeta := func(pm ds.PropertyMap) ds.PropertyMap { 426 prop := pm["$key"].(ds.Property) 427 key := prop.Value().(*ds.Key) 428 pm["$id"] = mkpNI(key.StringID()) 429 pm["$kind"] = mkpNI(key.Kind()) 430 pm["$parent"] = mkpNI(key.Parent()) 431 return pm 432 } 433 434 q := ds.NewQuery("Test") 435 436 Convey(`Can query for entities with FooBar == true.`, func() { 437 var results []ds.PropertyMap 438 q = q.Eq("FooBar", true) 439 So(ds.GetAll(c, q, &results), ShouldBeNil) 440 441 So(results, ShouldResemble, []ds.PropertyMap{ 442 withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "bar")), "FooBar": mkp(true)}), 443 withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "foo")), "FooBar": mkp(true)}), 444 }) 445 }) 446 447 Convey(`Can query for entities whose __key__ > "baz".`, func() { 448 var results []ds.PropertyMap 449 q = q.Gt("__key__", ds.MakeKey(c, "Test", "baz")) 450 So(ds.GetAll(c, q, &results), ShouldBeNil) 451 452 So(results, ShouldResemble, []ds.PropertyMap{ 453 withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "baz", "Test", "quux"))}), 454 withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "baz", "Test", "quuz"))}), 455 withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "foo")), "FooBar": mkp(true)}), 456 withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "qux"))}), 457 }) 458 }) 459 460 Convey(`Can query for entities whose ancestor is "baz".`, func() { 461 var results []ds.PropertyMap 462 q := ds.NewQuery("Test").Ancestor(ds.MakeKey(c, "Test", "baz")) 463 So(ds.GetAll(c, q, &results), ShouldBeNil) 464 465 So(results, ShouldResemble, []ds.PropertyMap{ 466 withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "baz"))}), 467 withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "baz", "Test", "quux"))}), 468 withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "baz", "Test", "quuz"))}), 469 }) 470 }) 471 472 // TODO(vadimsh): Unfortunately Cloud Datastore emulator doesn't 473 // support IN queries, see https://cloud.google.com/datastore/docs/tools/datastore-emulator#known_issues 474 SkipConvey(`Can use IN in queries`, func() { 475 var results []*ds.Key 476 q := ds.NewQuery("AAA").In("Slice", "b", "c").KeysOnly(true) 477 So(ds.GetAll(c, q, &results), ShouldBeNil) 478 So(results, ShouldResemble, []*ds.Key{ 479 ds.MakeKey(c, "AAA", "e1"), 480 ds.MakeKey(c, "AAA", "e2"), 481 }) 482 }) 483 484 Convey(`Can transactionally get and put.`, func() { 485 err := ds.RunInTransaction(c, func(c context.Context) error { 486 pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("qux")} 487 if err := ds.Get(c, pmap); err != nil { 488 return err 489 } 490 491 pmap["ExtraField"] = mkp("Present!") 492 return ds.Put(c, pmap) 493 }, nil) 494 So(err, ShouldBeNil) 495 496 pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("qux")} 497 err = ds.RunInTransaction(c, func(c context.Context) error { 498 return ds.Get(c, pmap) 499 }, nil) 500 So(err, ShouldBeNil) 501 So(pmap, ShouldResemble, ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("qux"), "ExtraField": mkp("Present!")}) 502 }) 503 504 Convey(`Can fail in a transaction with no effect.`, func() { 505 testError := errors.New("test error") 506 507 noTxnPM := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("no txn")} 508 err := ds.RunInTransaction(c, func(c context.Context) error { 509 So(ds.CurrentTransaction(c), ShouldNotBeNil) 510 511 pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("quux")} 512 if err := ds.Put(c, pmap); err != nil { 513 return err 514 } 515 516 // Put an entity outside of the transaction so we can confirm that 517 // it was added even when the transaction fails. 518 if err := ds.Put(ds.WithoutTransaction(c), noTxnPM); err != nil { 519 return err 520 } 521 return testError 522 }, nil) 523 So(err, ShouldEqual, testError) 524 525 // Confirm that noTxnPM was added. 526 So(ds.CurrentTransaction(c), shouldBeUntypedNil) 527 So(ds.Get(c, noTxnPM), ShouldBeNil) 528 529 pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("quux")} 530 err = ds.RunInTransaction(c, func(c context.Context) error { 531 return ds.Get(c, pmap) 532 }, nil) 533 So(err, ShouldEqual, ds.ErrNoSuchEntity) 534 }) 535 }) 536 }) 537 }) 538 }