go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/service/datastore/serialize_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 package datastore 16 17 import ( 18 "bytes" 19 "io" 20 "testing" 21 "time" 22 23 "go.chromium.org/luci/common/data/cmpbin" 24 25 "go.chromium.org/luci/gae/service/blobstore" 26 27 . "github.com/smartystreets/goconvey/convey" 28 . "go.chromium.org/luci/common/testing/assertions" 29 ) 30 31 func init() { 32 WritePropertyMapDeterministic = true 33 } 34 35 type dspmapTC struct { 36 name string 37 props PropertyMap 38 } 39 40 func mkKeyCtx(appID, namespace string, elems ...any) *Key { 41 return MkKeyContext(appID, namespace).MakeKey(elems...) 42 } 43 44 func mkBuf(data []byte) cmpbin.WriteableBytesBuffer { 45 return cmpbin.Invertible(bytes.NewBuffer(data)) 46 } 47 48 func TestPropertyMapSerialization(t *testing.T) { 49 t.Parallel() 50 51 now := time.Now().UTC() 52 tests := []dspmapTC{ 53 { 54 "basic", 55 PropertyMap{ 56 "R": PropertySlice{mp(false), mp(2.1), mpNI(3)}, 57 "S": PropertySlice{mp("hello"), mp("world")}, 58 }, 59 }, 60 { 61 "keys", 62 PropertyMap{ 63 "DS": PropertySlice{ 64 mp(mkKeyCtx("appy", "ns", "Foo", 7)), 65 mp(mkKeyCtx("other", "", "Yot", "wheeep")), 66 mp((*Key)(nil)), 67 }, 68 "blobstore": PropertySlice{mp(blobstore.Key("sup")), mp(blobstore.Key("nerds"))}, 69 }, 70 }, 71 { 72 "property map", 73 PropertyMap{ 74 "pm": PropertySlice{ 75 mp(PropertyMap{ 76 "$key": mpNI(mkKeyCtx("app", "ns", "entity", "id")), 77 "$kind": mpNI("entity"), 78 "$id": mpNI("id"), 79 "$parent": mpNI(nil), 80 "indexed": mp("indexed"), 81 "map": mpNI(PropertyMap{ 82 "b": mpNI([]byte("byte")), 83 }), 84 "str": mpNI("string")}, 85 ), 86 }, 87 }, 88 }, 89 { 90 "geo", 91 PropertyMap{ 92 "G": mp(GeoPoint{Lat: 1, Lng: 2}), 93 }, 94 }, 95 { 96 "data", 97 PropertyMap{ 98 "S": PropertySlice{mp("sup"), mp("fool"), mp("nerd")}, 99 "Deserialize.Foo.Nerd": PropertySlice{mp([]byte("sup")), mp([]byte("fool"))}, 100 }, 101 }, 102 { 103 "time", 104 PropertyMap{ 105 "T": PropertySlice{ 106 mp(now), 107 mp(now.Add(time.Second)), 108 }, 109 }, 110 }, 111 { 112 "empty vals", 113 PropertyMap{ 114 "T": PropertySlice{mp(true), mp(true)}, 115 "F": PropertySlice{mp(false), mp(false)}, 116 "N": PropertySlice{mp(nil), mp(nil)}, 117 "E": PropertySlice{}, 118 }, 119 }, 120 } 121 122 Convey("PropertyMap serialization", t, func() { 123 Convey("round trip", func() { 124 for _, tc := range tests { 125 tc := tc 126 Convey(tc.name, func() { 127 data := SerializeKC.ToBytes(tc.props) 128 dec, err := Deserialize.PropertyMap(mkBuf(data)) 129 So(err, ShouldBeNil) 130 So(dec, ShouldResemble, tc.props) 131 }) 132 } 133 }) 134 }) 135 } 136 137 func die(err error) { 138 if err != nil { 139 panic(err) 140 } 141 } 142 143 func wf(w io.Writer, v float64) int { 144 ret, err := cmpbin.WriteFloat64(w, v) 145 die(err) 146 return ret 147 } 148 149 func ws(w io.ByteWriter, s string) int { 150 ret, err := cmpbin.WriteString(w, s) 151 die(err) 152 return ret 153 } 154 155 func wi(w io.ByteWriter, i int64) int { 156 ret, err := cmpbin.WriteInt(w, i) 157 die(err) 158 return ret 159 } 160 161 func wui(w io.ByteWriter, i uint64) int { 162 ret, err := cmpbin.WriteUint(w, i) 163 die(err) 164 return ret 165 } 166 167 func TestSerializationReadMisc(t *testing.T) { 168 t.Parallel() 169 170 Convey("Misc Serialization tests", t, func() { 171 Convey("GeoPoint", func() { 172 buf := mkBuf(nil) 173 wf(buf, 10) 174 wf(buf, 20) 175 So(string(Serialize.ToBytes(GeoPoint{Lat: 10, Lng: 20})), ShouldEqual, buf.String()) 176 }) 177 178 Convey("IndexColumn", func() { 179 buf := mkBuf(nil) 180 die(buf.WriteByte(1)) 181 ws(buf, "hi") 182 So(string(Serialize.ToBytes(IndexColumn{Property: "hi", Descending: true})), 183 ShouldEqual, buf.String()) 184 }) 185 186 Convey("KeyTok", func() { 187 buf := mkBuf(nil) 188 ws(buf, "foo") 189 die(buf.WriteByte(byte(PTInt))) 190 wi(buf, 20) 191 So(string(Serialize.ToBytes(KeyTok{Kind: "foo", IntID: 20})), 192 ShouldEqual, buf.String()) 193 }) 194 195 Convey("Property", func() { 196 buf := mkBuf(nil) 197 die(buf.WriteByte(0x80 | byte(PTString))) 198 ws(buf, "nerp") 199 So(string(Serialize.ToBytes(mp("nerp"))), 200 ShouldEqual, buf.String()) 201 }) 202 203 Convey("Time", func() { 204 tp := mp(time.Now().UTC()) 205 So(string(Serialize.ToBytes(tp.Value())), ShouldEqual, string(Serialize.ToBytes(tp)[1:])) 206 }) 207 208 Convey("Zero time", func() { 209 buf := mkBuf(nil) 210 So(Serialize.Time(buf, time.Time{}), ShouldBeNil) 211 t, err := Deserialize.Time(mkBuf(buf.Bytes())) 212 So(err, ShouldBeNil) 213 So(t.Equal(time.Time{}), ShouldBeTrue) 214 }) 215 216 Convey("ReadKey", func() { 217 Convey("good cases", func() { 218 dwc := Deserializer{MkKeyContext("spam", "nerd")} 219 220 Convey("w/ ctx decodes normally w/ ctx", func() { 221 k := mkKeyCtx("aid", "ns", "knd", "yo", "other", 10) 222 data := SerializeKC.ToBytes(k) 223 dk, err := Deserialize.Key(mkBuf(data)) 224 So(err, ShouldBeNil) 225 So(dk, ShouldEqualKey, k) 226 }) 227 Convey("w/ ctx decodes normally w/o ctx", func() { 228 k := mkKeyCtx("aid", "ns", "knd", "yo", "other", 10) 229 data := SerializeKC.ToBytes(k) 230 dk, err := dwc.Key(mkBuf(data)) 231 So(err, ShouldBeNil) 232 So(dk, ShouldEqualKey, mkKeyCtx("spam", "nerd", "knd", "yo", "other", 10)) 233 }) 234 Convey("w/o ctx decodes normally w/ ctx", func() { 235 k := mkKeyCtx("aid", "ns", "knd", "yo", "other", 10) 236 data := Serialize.ToBytes(k) 237 dk, err := Deserialize.Key(mkBuf(data)) 238 So(err, ShouldBeNil) 239 So(dk, ShouldEqualKey, mkKeyCtx("", "", "knd", "yo", "other", 10)) 240 }) 241 Convey("w/o ctx decodes normally w/o ctx", func() { 242 k := mkKeyCtx("aid", "ns", "knd", "yo", "other", 10) 243 data := Serialize.ToBytes(k) 244 dk, err := dwc.Key(mkBuf(data)) 245 So(err, ShouldBeNil) 246 So(dk, ShouldEqualKey, mkKeyCtx("spam", "nerd", "knd", "yo", "other", 10)) 247 }) 248 Convey("IntIDs always sort before StringIDs", func() { 249 // -1 writes as almost all 1's in the first byte under cmpbin, even 250 // though it's technically not a valid key. 251 k := mkKeyCtx("aid", "ns", "knd", -1) 252 data := Serialize.ToBytes(k) 253 254 k = mkKeyCtx("aid", "ns", "knd", "hat") 255 data2 := Serialize.ToBytes(k) 256 257 So(string(data), ShouldBeLessThan, string(data2)) 258 }) 259 }) 260 261 Convey("err cases", func() { 262 buf := mkBuf(nil) 263 264 Convey("nil", func() { 265 _, err := Deserialize.Key(buf) 266 So(err, ShouldEqual, io.EOF) 267 }) 268 Convey("str", func() { 269 _, err := buf.WriteString("sup") 270 die(err) 271 _, err = Deserialize.Key(buf) 272 So(err, ShouldErrLike, "expected actualCtx") 273 }) 274 Convey("truncated 1", func() { 275 die(buf.WriteByte(1)) // actualCtx == 1 276 _, err := Deserialize.Key(buf) 277 So(err, ShouldEqual, io.EOF) 278 }) 279 Convey("truncated 2", func() { 280 die(buf.WriteByte(1)) // actualCtx == 1 281 ws(buf, "aid") 282 _, err := Deserialize.Key(buf) 283 So(err, ShouldEqual, io.EOF) 284 }) 285 Convey("truncated 3", func() { 286 die(buf.WriteByte(1)) // actualCtx == 1 287 ws(buf, "aid") 288 ws(buf, "ns") 289 _, err := Deserialize.Key(buf) 290 So(err, ShouldEqual, io.EOF) 291 }) 292 Convey("huge key", func() { 293 die(buf.WriteByte(1)) // actualCtx == 1 294 ws(buf, "aid") 295 ws(buf, "ns") 296 for i := 1; i < 60; i++ { 297 die(buf.WriteByte(1)) 298 die(Serialize.KeyTok(buf, KeyTok{Kind: "sup", IntID: int64(i)})) 299 } 300 die(buf.WriteByte(0)) 301 _, err := Deserialize.Key(buf) 302 So(err, ShouldErrLike, "huge key") 303 }) 304 Convey("insufficient tokens", func() { 305 die(buf.WriteByte(1)) // actualCtx == 1 306 ws(buf, "aid") 307 ws(buf, "ns") 308 wui(buf, 2) 309 _, err := Deserialize.Key(buf) 310 So(err, ShouldEqual, io.EOF) 311 }) 312 Convey("partial token 1", func() { 313 die(buf.WriteByte(1)) // actualCtx == 1 314 ws(buf, "aid") 315 ws(buf, "ns") 316 die(buf.WriteByte(1)) 317 ws(buf, "hi") 318 _, err := Deserialize.Key(buf) 319 So(err, ShouldEqual, io.EOF) 320 }) 321 Convey("partial token 2", func() { 322 die(buf.WriteByte(1)) // actualCtx == 1 323 ws(buf, "aid") 324 ws(buf, "ns") 325 die(buf.WriteByte(1)) 326 ws(buf, "hi") 327 die(buf.WriteByte(byte(PTString))) 328 _, err := Deserialize.Key(buf) 329 So(err, ShouldEqual, io.EOF) 330 }) 331 Convey("bad token (invalid type)", func() { 332 die(buf.WriteByte(1)) // actualCtx == 1 333 ws(buf, "aid") 334 ws(buf, "ns") 335 die(buf.WriteByte(1)) 336 ws(buf, "hi") 337 die(buf.WriteByte(byte(PTBlobKey))) 338 _, err := Deserialize.Key(buf) 339 So(err, ShouldErrLike, "invalid type PTBlobKey") 340 }) 341 Convey("bad token (invalid IntID)", func() { 342 die(buf.WriteByte(1)) // actualCtx == 1 343 ws(buf, "aid") 344 ws(buf, "ns") 345 die(buf.WriteByte(1)) 346 ws(buf, "hi") 347 die(buf.WriteByte(byte(PTInt))) 348 wi(buf, -2) 349 _, err := Deserialize.Key(buf) 350 So(err, ShouldErrLike, "zero/negative") 351 }) 352 }) 353 }) 354 355 Convey("ReadGeoPoint", func() { 356 buf := mkBuf(nil) 357 Convey("trunc 1", func() { 358 _, err := Deserialize.GeoPoint(buf) 359 So(err, ShouldEqual, io.EOF) 360 }) 361 Convey("trunc 2", func() { 362 wf(buf, 100) 363 _, err := Deserialize.GeoPoint(buf) 364 So(err, ShouldEqual, io.EOF) 365 }) 366 Convey("invalid", func() { 367 wf(buf, 100) 368 wf(buf, 1000) 369 _, err := Deserialize.GeoPoint(buf) 370 So(err, ShouldErrLike, "invalid GeoPoint") 371 }) 372 }) 373 374 Convey("WriteTime", func() { 375 Convey("in non-UTC!", func() { 376 pst, err := time.LoadLocation("America/Los_Angeles") 377 So(err, ShouldBeNil) 378 So(func() { 379 die(Serialize.Time(mkBuf(nil), time.Now().In(pst))) 380 }, ShouldPanic) 381 }) 382 }) 383 384 Convey("ReadTime", func() { 385 Convey("trunc 1", func() { 386 _, err := Deserialize.Time(mkBuf(nil)) 387 So(err, ShouldEqual, io.EOF) 388 }) 389 }) 390 391 Convey("ReadProperty", func() { 392 buf := mkBuf(nil) 393 Convey("trunc 1", func() { 394 p, err := Deserialize.Property(buf) 395 So(err, ShouldEqual, io.EOF) 396 So(p.Type(), ShouldEqual, PTNull) 397 So(p.Value(), ShouldBeNil) 398 }) 399 Convey("trunc (PTBytes)", func() { 400 die(buf.WriteByte(byte(PTBytes))) 401 _, err := Deserialize.Property(buf) 402 So(err, ShouldEqual, io.EOF) 403 }) 404 Convey("trunc (PTBlobKey)", func() { 405 die(buf.WriteByte(byte(PTBlobKey))) 406 _, err := Deserialize.Property(buf) 407 So(err, ShouldEqual, io.EOF) 408 }) 409 Convey("invalid type", func() { 410 die(buf.WriteByte(byte(PTUnknown + 1))) 411 _, err := Deserialize.Property(buf) 412 So(err, ShouldErrLike, "unknown type!") 413 }) 414 }) 415 416 Convey("ReadPropertyMap", func() { 417 buf := mkBuf(nil) 418 Convey("trunc 1", func() { 419 _, err := Deserialize.PropertyMap(buf) 420 So(err, ShouldEqual, io.EOF) 421 }) 422 Convey("too many rows", func() { 423 wui(buf, 1000000) 424 _, err := Deserialize.PropertyMap(buf) 425 So(err, ShouldErrLike, "huge number of rows") 426 }) 427 Convey("trunc 2", func() { 428 wui(buf, 10) 429 _, err := Deserialize.PropertyMap(buf) 430 So(err, ShouldEqual, io.EOF) 431 }) 432 Convey("trunc 3", func() { 433 wui(buf, 10) 434 ws(buf, "ohai") 435 _, err := Deserialize.PropertyMap(buf) 436 So(err, ShouldEqual, io.EOF) 437 }) 438 Convey("too many values", func() { 439 wui(buf, 10) 440 ws(buf, "ohai") 441 wui(buf, 100000) 442 _, err := Deserialize.PropertyMap(buf) 443 So(err, ShouldErrLike, "huge number of properties") 444 }) 445 Convey("trunc 4", func() { 446 wui(buf, 10) 447 ws(buf, "ohai") 448 wui(buf, 10) 449 _, err := Deserialize.PropertyMap(buf) 450 So(err, ShouldEqual, io.EOF) 451 }) 452 }) 453 454 Convey("IndexDefinition", func() { 455 id := IndexDefinition{Kind: "kind"} 456 data := Serialize.ToBytes(*id.PrepForIdxTable()) 457 newID, err := Deserialize.IndexDefinition(mkBuf(data)) 458 So(err, ShouldBeNil) 459 So(newID.Flip(), ShouldResemble, id.Normalize()) 460 461 id.SortBy = append(id.SortBy, IndexColumn{Property: "prop"}) 462 data = Serialize.ToBytes(*id.PrepForIdxTable()) 463 newID, err = Deserialize.IndexDefinition(mkBuf(data)) 464 So(err, ShouldBeNil) 465 So(newID.Flip(), ShouldResemble, id.Normalize()) 466 467 id.SortBy = append(id.SortBy, IndexColumn{Property: "other", Descending: true}) 468 id.Ancestor = true 469 data = Serialize.ToBytes(*id.PrepForIdxTable()) 470 newID, err = Deserialize.IndexDefinition(mkBuf(data)) 471 So(err, ShouldBeNil) 472 So(newID.Flip(), ShouldResemble, id.Normalize()) 473 474 // invalid 475 id.SortBy = append(id.SortBy, IndexColumn{Property: "", Descending: true}) 476 data = Serialize.ToBytes(*id.PrepForIdxTable()) 477 newID, err = Deserialize.IndexDefinition(mkBuf(data)) 478 So(err, ShouldBeNil) 479 So(newID.Flip(), ShouldResemble, id.Normalize()) 480 481 Convey("too many", func() { 482 id := IndexDefinition{Kind: "wat"} 483 for i := 0; i < maxIndexColumns+1; i++ { 484 id.SortBy = append(id.SortBy, IndexColumn{Property: "Hi", Descending: true}) 485 } 486 data := Serialize.ToBytes(*id.PrepForIdxTable()) 487 newID, err = Deserialize.IndexDefinition(mkBuf(data)) 488 So(err, ShouldErrLike, "over 64 sort orders") 489 }) 490 }) 491 }) 492 } 493 494 func TestIndexedProperties(t *testing.T) { 495 t.Parallel() 496 497 fakeKey := mkKeyCtx("dev~app", "ns", "parentKind", "sid", "knd", 10) 498 499 innerKey1 := mkKeyCtx("dev~app", "ns", "parentKind", "sid", "innerKey", 1) 500 innerKey2 := mkKeyCtx("dev~app", "ns", "parentKind", "sid", "innerKey", 2) 501 innerKey3 := mkKeyCtx("dev~app", "ns", "parentKind", "sid", "innerKey", 3) 502 503 Convey("TestIndexedProperties", t, func() { 504 pm := PropertyMap{ 505 "wat": PropertySlice{mpNI("thing"), mp("hat"), mp(100)}, 506 "nerd": mp(103.7), 507 "spaz": mpNI(false), 508 "nested": PropertySlice{ 509 mp(PropertyMap{ 510 "$key": mpNI(innerKey1), 511 "prop": mp(123), 512 "slice": PropertySlice{mp(123), mp(456)}, 513 "ni": mpNI(666), 514 }), 515 mpNI(PropertyMap{ // will be skipped, not indexed 516 "$key": mpNI(innerKey2), 517 "prop": mp(123), 518 "slice": PropertySlice{mp(123), mp(456)}, 519 }), 520 mp(PropertyMap{ 521 "$kind": mpNI("innerKey"), 522 "$id": mpNI(3), 523 "$parent": mpNI(innerKey3.Parent()), 524 "prop": mp(456), 525 "slice": PropertySlice{mp(456), mp(789)}, 526 "ni": mpNI(666), 527 }), 528 mp(PropertyMap{ // key is optional 529 "prop": mp(777), 530 "deeper": mp(PropertyMap{ 531 "deep": mp(888), 532 }), 533 }), 534 }, 535 } 536 537 Convey("IndexedProperties", func() { 538 sip := Serialize.IndexedProperties(fakeKey, pm) 539 So(len(sip), ShouldEqual, 8) 540 sip.Sort() 541 542 So(sip, ShouldResemble, IndexedProperties{ 543 "wat": { 544 Serialize.ToBytes(mp(100)), 545 Serialize.ToBytes(mp("hat")), 546 // 'thing' is skipped, because it's not NoIndex 547 }, 548 "nerd": { 549 Serialize.ToBytes(mp(103.7)), 550 }, 551 "nested.__key__": { 552 Serialize.ToBytes(mp(innerKey1)), 553 Serialize.ToBytes(mp(innerKey3)), 554 }, 555 "nested.deeper.deep": { 556 Serialize.ToBytes(mp(888)), 557 }, 558 "nested.prop": { 559 Serialize.ToBytes(mp(123)), 560 Serialize.ToBytes(mp(456)), 561 Serialize.ToBytes(mp(777)), 562 }, 563 "nested.slice": { 564 Serialize.ToBytes(mp(123)), 565 Serialize.ToBytes(mp(456)), 566 Serialize.ToBytes(mp(789)), 567 }, 568 "__key__": { 569 Serialize.ToBytes(mp(fakeKey)), 570 }, 571 "__ancestor__": { 572 Serialize.ToBytes(mp(fakeKey.Parent())), 573 Serialize.ToBytes(mp(fakeKey)), 574 }, 575 }) 576 }) 577 578 Convey("IndexedPropertiesForIndicies", func() { 579 sip := Serialize.IndexedPropertiesForIndicies(fakeKey, pm, []IndexColumn{ 580 {Property: "wat"}, 581 {Property: "wat", Descending: true}, 582 {Property: "unknown"}, 583 {Property: "nested.__key__"}, 584 }) 585 So(len(sip), ShouldEqual, 4) 586 sip.Sort() 587 588 So(sip, ShouldResemble, IndexedProperties{ 589 "wat": { 590 Serialize.ToBytes(mp(100)), 591 Serialize.ToBytes(mp("hat")), 592 }, 593 "nested.__key__": { 594 Serialize.ToBytes(mp(innerKey1)), 595 Serialize.ToBytes(mp(innerKey3)), 596 }, 597 "__key__": { 598 Serialize.ToBytes(mp(fakeKey)), 599 }, 600 "__ancestor__": { 601 Serialize.ToBytes(mp(fakeKey.Parent())), 602 Serialize.ToBytes(mp(fakeKey)), 603 }, 604 }) 605 }) 606 }) 607 }