1// Copyright 2011 Google Inc. All Rights Reserved. 2// Use of this source code is governed by the Apache 2.0 3// license that can be found in the LICENSE file. 4 5package datastore 6 7import ( 8 "reflect" 9 "sort" 10 "testing" 11 "time" 12 13 "google.golang.org/appengine" 14) 15 16func TestValidPropertyName(t *testing.T) { 17 testCases := []struct { 18 name string 19 want bool 20 }{ 21 // Invalid names. 22 {"", false}, 23 {"'", false}, 24 {".", false}, 25 {"..", false}, 26 {".foo", false}, 27 {"0", false}, 28 {"00", false}, 29 {"X.X.4.X.X", false}, 30 {"\n", false}, 31 {"\x00", false}, 32 {"abc\xffz", false}, 33 {"foo.", false}, 34 {"foo..", false}, 35 {"foo..bar", false}, 36 {"☃", false}, 37 {`"`, false}, 38 // Valid names. 39 {"AB", true}, 40 {"Abc", true}, 41 {"X.X.X.X.X", true}, 42 {"_", true}, 43 {"_0", true}, 44 {"a", true}, 45 {"a_B", true}, 46 {"f00", true}, 47 {"f0o", true}, 48 {"fo0", true}, 49 {"foo", true}, 50 {"foo.bar", true}, 51 {"foo.bar.baz", true}, 52 {"世界", true}, 53 } 54 for _, tc := range testCases { 55 got := validPropertyName(tc.name) 56 if got != tc.want { 57 t.Errorf("%q: got %v, want %v", tc.name, got, tc.want) 58 } 59 } 60} 61 62func TestStructCodec(t *testing.T) { 63 type oStruct struct { 64 O int 65 } 66 type pStruct struct { 67 P int 68 Q int 69 } 70 type rStruct struct { 71 R int 72 S pStruct 73 T oStruct 74 oStruct 75 } 76 type uStruct struct { 77 U int 78 v int 79 } 80 type vStruct struct { 81 V string `datastore:",noindex"` 82 } 83 oStructCodec := &structCodec{ 84 fields: map[string]fieldCodec{ 85 "O": {path: []int{0}}, 86 }, 87 complete: true, 88 } 89 pStructCodec := &structCodec{ 90 fields: map[string]fieldCodec{ 91 "P": {path: []int{0}}, 92 "Q": {path: []int{1}}, 93 }, 94 complete: true, 95 } 96 rStructCodec := &structCodec{ 97 fields: map[string]fieldCodec{ 98 "R": {path: []int{0}}, 99 "S": {path: []int{1}, structCodec: pStructCodec}, 100 "T": {path: []int{2}, structCodec: oStructCodec}, 101 "O": {path: []int{3, 0}}, 102 }, 103 complete: true, 104 } 105 uStructCodec := &structCodec{ 106 fields: map[string]fieldCodec{ 107 "U": {path: []int{0}}, 108 }, 109 complete: true, 110 } 111 vStructCodec := &structCodec{ 112 fields: map[string]fieldCodec{ 113 "V": {path: []int{0}, noIndex: true}, 114 }, 115 complete: true, 116 } 117 118 testCases := []struct { 119 desc string 120 structValue interface{} 121 want *structCodec 122 }{ 123 { 124 "oStruct", 125 oStruct{}, 126 oStructCodec, 127 }, 128 { 129 "pStruct", 130 pStruct{}, 131 pStructCodec, 132 }, 133 { 134 "rStruct", 135 rStruct{}, 136 rStructCodec, 137 }, 138 { 139 "uStruct", 140 uStruct{}, 141 uStructCodec, 142 }, 143 { 144 "non-basic fields", 145 struct { 146 B appengine.BlobKey 147 K *Key 148 T time.Time 149 }{}, 150 &structCodec{ 151 fields: map[string]fieldCodec{ 152 "B": {path: []int{0}}, 153 "K": {path: []int{1}}, 154 "T": {path: []int{2}}, 155 }, 156 complete: true, 157 }, 158 }, 159 { 160 "struct tags with ignored embed", 161 struct { 162 A int `datastore:"a,noindex"` 163 B int `datastore:"b"` 164 C int `datastore:",noindex"` 165 D int `datastore:""` 166 E int 167 I int `datastore:"-"` 168 J int `datastore:",noindex" json:"j"` 169 oStruct `datastore:"-"` 170 }{}, 171 &structCodec{ 172 fields: map[string]fieldCodec{ 173 "a": {path: []int{0}, noIndex: true}, 174 "b": {path: []int{1}}, 175 "C": {path: []int{2}, noIndex: true}, 176 "D": {path: []int{3}}, 177 "E": {path: []int{4}}, 178 "J": {path: []int{6}, noIndex: true}, 179 }, 180 complete: true, 181 }, 182 }, 183 { 184 "unexported fields", 185 struct { 186 A int 187 b int 188 C int `datastore:"x"` 189 d int `datastore:"Y"` 190 }{}, 191 &structCodec{ 192 fields: map[string]fieldCodec{ 193 "A": {path: []int{0}}, 194 "x": {path: []int{2}}, 195 }, 196 complete: true, 197 }, 198 }, 199 { 200 "nested and embedded structs", 201 struct { 202 A int 203 B int 204 CC oStruct 205 DDD rStruct 206 oStruct 207 }{}, 208 &structCodec{ 209 fields: map[string]fieldCodec{ 210 "A": {path: []int{0}}, 211 "B": {path: []int{1}}, 212 "CC": {path: []int{2}, structCodec: oStructCodec}, 213 "DDD": {path: []int{3}, structCodec: rStructCodec}, 214 "O": {path: []int{4, 0}}, 215 }, 216 complete: true, 217 }, 218 }, 219 { 220 "struct tags with nested and embedded structs", 221 struct { 222 A int `datastore:"-"` 223 B int `datastore:"w"` 224 C oStruct `datastore:"xx"` 225 D rStruct `datastore:"y"` 226 oStruct `datastore:"z"` 227 }{}, 228 &structCodec{ 229 fields: map[string]fieldCodec{ 230 "w": {path: []int{1}}, 231 "xx": {path: []int{2}, structCodec: oStructCodec}, 232 "y": {path: []int{3}, structCodec: rStructCodec}, 233 "z.O": {path: []int{4, 0}}, 234 }, 235 complete: true, 236 }, 237 }, 238 { 239 "unexported nested and embedded structs", 240 struct { 241 a int 242 B int 243 c uStruct 244 D uStruct 245 uStruct 246 }{}, 247 &structCodec{ 248 fields: map[string]fieldCodec{ 249 "B": {path: []int{1}}, 250 "D": {path: []int{3}, structCodec: uStructCodec}, 251 "U": {path: []int{4, 0}}, 252 }, 253 complete: true, 254 }, 255 }, 256 { 257 "noindex nested struct", 258 struct { 259 A oStruct `datastore:",noindex"` 260 }{}, 261 &structCodec{ 262 fields: map[string]fieldCodec{ 263 "A": {path: []int{0}, structCodec: oStructCodec, noIndex: true}, 264 }, 265 complete: true, 266 }, 267 }, 268 { 269 "noindex slice", 270 struct { 271 A []string `datastore:",noindex"` 272 }{}, 273 &structCodec{ 274 fields: map[string]fieldCodec{ 275 "A": {path: []int{0}, noIndex: true}, 276 }, 277 hasSlice: true, 278 complete: true, 279 }, 280 }, 281 { 282 "noindex embedded struct slice", 283 struct { 284 // vStruct has a single field, V, also with noindex. 285 A []vStruct `datastore:",noindex"` 286 }{}, 287 &structCodec{ 288 fields: map[string]fieldCodec{ 289 "A": {path: []int{0}, structCodec: vStructCodec, noIndex: true}, 290 }, 291 hasSlice: true, 292 complete: true, 293 }, 294 }, 295 } 296 297 for _, tc := range testCases { 298 got, err := getStructCodec(reflect.TypeOf(tc.structValue)) 299 if err != nil { 300 t.Errorf("%s: getStructCodec: %v", tc.desc, err) 301 continue 302 } 303 // can't reflect.DeepEqual b/c element order in fields map may differ 304 if !isEqualStructCodec(got, tc.want) { 305 t.Errorf("%s\ngot %+v\nwant %+v\n", tc.desc, got, tc.want) 306 } 307 } 308} 309 310func isEqualStructCodec(got, want *structCodec) bool { 311 if got.complete != want.complete { 312 return false 313 } 314 if got.hasSlice != want.hasSlice { 315 return false 316 } 317 if len(got.fields) != len(want.fields) { 318 return false 319 } 320 for name, wantF := range want.fields { 321 gotF := got.fields[name] 322 if !reflect.DeepEqual(wantF.path, gotF.path) { 323 return false 324 } 325 if wantF.noIndex != gotF.noIndex { 326 return false 327 } 328 if wantF.structCodec != nil { 329 if gotF.structCodec == nil { 330 return false 331 } 332 if !isEqualStructCodec(gotF.structCodec, wantF.structCodec) { 333 return false 334 } 335 } 336 } 337 338 return true 339} 340 341func TestRepeatedPropertyName(t *testing.T) { 342 good := []interface{}{ 343 struct { 344 A int `datastore:"-"` 345 }{}, 346 struct { 347 A int `datastore:"b"` 348 B int 349 }{}, 350 struct { 351 A int 352 B int `datastore:"B"` 353 }{}, 354 struct { 355 A int `datastore:"B"` 356 B int `datastore:"-"` 357 }{}, 358 struct { 359 A int `datastore:"-"` 360 B int `datastore:"A"` 361 }{}, 362 struct { 363 A int `datastore:"B"` 364 B int `datastore:"A"` 365 }{}, 366 struct { 367 A int `datastore:"B"` 368 B int `datastore:"C"` 369 C int `datastore:"A"` 370 }{}, 371 struct { 372 A int `datastore:"B"` 373 B int `datastore:"C"` 374 C int `datastore:"D"` 375 }{}, 376 } 377 bad := []interface{}{ 378 struct { 379 A int `datastore:"B"` 380 B int 381 }{}, 382 struct { 383 A int 384 B int `datastore:"A"` 385 }{}, 386 struct { 387 A int `datastore:"C"` 388 B int `datastore:"C"` 389 }{}, 390 struct { 391 A int `datastore:"B"` 392 B int `datastore:"C"` 393 C int `datastore:"B"` 394 }{}, 395 } 396 testGetStructCodec(t, good, bad) 397} 398 399func TestFlatteningNestedStructs(t *testing.T) { 400 type DeepGood struct { 401 A struct { 402 B []struct { 403 C struct { 404 D int 405 } 406 } 407 } 408 } 409 type DeepBad struct { 410 A struct { 411 B []struct { 412 C struct { 413 D []int 414 } 415 } 416 } 417 } 418 type ISay struct { 419 Tomato int 420 } 421 type YouSay struct { 422 Tomato int 423 } 424 type Tweedledee struct { 425 Dee int `datastore:"D"` 426 } 427 type Tweedledum struct { 428 Dum int `datastore:"D"` 429 } 430 431 good := []interface{}{ 432 struct { 433 X []struct { 434 Y string 435 } 436 }{}, 437 struct { 438 X []struct { 439 Y []byte 440 } 441 }{}, 442 struct { 443 P []int 444 X struct { 445 Y []int 446 } 447 }{}, 448 struct { 449 X struct { 450 Y []int 451 } 452 Q []int 453 }{}, 454 struct { 455 P []int 456 X struct { 457 Y []int 458 } 459 Q []int 460 }{}, 461 struct { 462 DeepGood 463 }{}, 464 struct { 465 DG DeepGood 466 }{}, 467 struct { 468 Foo struct { 469 Z int 470 } `datastore:"A"` 471 Bar struct { 472 Z int 473 } `datastore:"B"` 474 }{}, 475 } 476 bad := []interface{}{ 477 struct { 478 X []struct { 479 Y []string 480 } 481 }{}, 482 struct { 483 X []struct { 484 Y []int 485 } 486 }{}, 487 struct { 488 DeepBad 489 }{}, 490 struct { 491 DB DeepBad 492 }{}, 493 struct { 494 ISay 495 YouSay 496 }{}, 497 struct { 498 Tweedledee 499 Tweedledum 500 }{}, 501 struct { 502 Foo struct { 503 Z int 504 } `datastore:"A"` 505 Bar struct { 506 Z int 507 } `datastore:"A"` 508 }{}, 509 } 510 testGetStructCodec(t, good, bad) 511} 512 513func testGetStructCodec(t *testing.T, good []interface{}, bad []interface{}) { 514 for _, x := range good { 515 if _, err := getStructCodec(reflect.TypeOf(x)); err != nil { 516 t.Errorf("type %T: got non-nil error (%s), want nil", x, err) 517 } 518 } 519 for _, x := range bad { 520 if _, err := getStructCodec(reflect.TypeOf(x)); err == nil { 521 t.Errorf("type %T: got nil error, want non-nil", x) 522 } 523 } 524} 525 526func TestNilKeyIsStored(t *testing.T) { 527 x := struct { 528 K *Key 529 I int 530 }{} 531 p := PropertyList{} 532 // Save x as properties. 533 p1, _ := SaveStruct(&x) 534 p.Load(p1) 535 // Set x's fields to non-zero. 536 x.K = &Key{} 537 x.I = 2 538 // Load x from properties. 539 p2, _ := p.Save() 540 LoadStruct(&x, p2) 541 // Check that x's fields were set to zero. 542 if x.K != nil { 543 t.Errorf("K field was not zero") 544 } 545 if x.I != 0 { 546 t.Errorf("I field was not zero") 547 } 548} 549 550func TestSaveStructOmitEmpty(t *testing.T) { 551 // Expected props names are sorted alphabetically 552 expectedPropNamesForSingles := []string{"EmptyValue", "NonEmptyValue", "OmitEmptyWithValue"} 553 expectedPropNamesForSlices := []string{"NonEmptyValue", "NonEmptyValue", "OmitEmptyWithValue", "OmitEmptyWithValue"} 554 555 testOmitted := func(expectedPropNames []string, src interface{}) { 556 // t.Helper() - this is available from Go version 1.9, but we also support Go versions 1.6, 1.7, 1.8 557 if props, err := SaveStruct(src); err != nil { 558 t.Fatal(err) 559 } else { 560 // Collect names for reporting if diffs from expected and for easier sorting 561 actualPropNames := make([]string, len(props)) 562 for i := range props { 563 actualPropNames[i] = props[i].Name 564 } 565 // Sort actuals for comparing with already sorted expected names 566 sort.Sort(sort.StringSlice(actualPropNames)) 567 if !reflect.DeepEqual(actualPropNames, expectedPropNames) { 568 t.Errorf("Expected this properties: %v, got: %v", expectedPropNames, actualPropNames) 569 } 570 } 571 } 572 573 testOmitted(expectedPropNamesForSingles, &struct { 574 EmptyValue int 575 NonEmptyValue int 576 OmitEmptyNoValue int `datastore:",omitempty"` 577 OmitEmptyWithValue int `datastore:",omitempty"` 578 }{ 579 NonEmptyValue: 1, 580 OmitEmptyWithValue: 2, 581 }) 582 583 testOmitted(expectedPropNamesForSlices, &struct { 584 EmptyValue []int 585 NonEmptyValue []int 586 OmitEmptyNoValue []int `datastore:",omitempty"` 587 OmitEmptyWithValue []int `datastore:",omitempty"` 588 }{ 589 NonEmptyValue: []int{1, 2}, 590 OmitEmptyWithValue: []int{3, 4}, 591 }) 592 593 testOmitted(expectedPropNamesForSingles, &struct { 594 EmptyValue bool 595 NonEmptyValue bool 596 OmitEmptyNoValue bool `datastore:",omitempty"` 597 OmitEmptyWithValue bool `datastore:",omitempty"` 598 }{ 599 NonEmptyValue: true, 600 OmitEmptyWithValue: true, 601 }) 602 603 testOmitted(expectedPropNamesForSlices, &struct { 604 EmptyValue []bool 605 NonEmptyValue []bool 606 OmitEmptyNoValue []bool `datastore:",omitempty"` 607 OmitEmptyWithValue []bool `datastore:",omitempty"` 608 }{ 609 NonEmptyValue: []bool{true, true}, 610 OmitEmptyWithValue: []bool{true, true}, 611 }) 612 613 testOmitted(expectedPropNamesForSingles, &struct { 614 EmptyValue string 615 NonEmptyValue string 616 OmitEmptyNoValue string `datastore:",omitempty"` 617 OmitEmptyWithValue string `datastore:",omitempty"` 618 }{ 619 NonEmptyValue: "s", 620 OmitEmptyWithValue: "s", 621 }) 622 623 testOmitted(expectedPropNamesForSlices, &struct { 624 EmptyValue []string 625 NonEmptyValue []string 626 OmitEmptyNoValue []string `datastore:",omitempty"` 627 OmitEmptyWithValue []string `datastore:",omitempty"` 628 }{ 629 NonEmptyValue: []string{"s1", "s2"}, 630 OmitEmptyWithValue: []string{"s3", "s4"}, 631 }) 632 633 testOmitted(expectedPropNamesForSingles, &struct { 634 EmptyValue float32 635 NonEmptyValue float32 636 OmitEmptyNoValue float32 `datastore:",omitempty"` 637 OmitEmptyWithValue float32 `datastore:",omitempty"` 638 }{ 639 NonEmptyValue: 1.1, 640 OmitEmptyWithValue: 1.2, 641 }) 642 643 testOmitted(expectedPropNamesForSlices, &struct { 644 EmptyValue []float32 645 NonEmptyValue []float32 646 OmitEmptyNoValue []float32 `datastore:",omitempty"` 647 OmitEmptyWithValue []float32 `datastore:",omitempty"` 648 }{ 649 NonEmptyValue: []float32{1.1, 2.2}, 650 OmitEmptyWithValue: []float32{3.3, 4.4}, 651 }) 652 653 testOmitted(expectedPropNamesForSingles, &struct { 654 EmptyValue time.Time 655 NonEmptyValue time.Time 656 OmitEmptyNoValue time.Time `datastore:",omitempty"` 657 OmitEmptyWithValue time.Time `datastore:",omitempty"` 658 }{ 659 NonEmptyValue: now, 660 OmitEmptyWithValue: now, 661 }) 662 663 testOmitted(expectedPropNamesForSlices, &struct { 664 EmptyValue []time.Time 665 NonEmptyValue []time.Time 666 OmitEmptyNoValue []time.Time `datastore:",omitempty"` 667 OmitEmptyWithValue []time.Time `datastore:",omitempty"` 668 }{ 669 NonEmptyValue: []time.Time{now, now}, 670 OmitEmptyWithValue: []time.Time{now, now}, 671 }) 672} 673