// Copyright 2011 Google Inc. All Rights Reserved. // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. package datastore import ( "reflect" "sort" "testing" "time" "google.golang.org/appengine" ) func TestValidPropertyName(t *testing.T) { testCases := []struct { name string want bool }{ // Invalid names. {"", false}, {"'", false}, {".", false}, {"..", false}, {".foo", false}, {"0", false}, {"00", false}, {"X.X.4.X.X", false}, {"\n", false}, {"\x00", false}, {"abc\xffz", false}, {"foo.", false}, {"foo..", false}, {"foo..bar", false}, {"β˜ƒ", false}, {`"`, false}, // Valid names. {"AB", true}, {"Abc", true}, {"X.X.X.X.X", true}, {"_", true}, {"_0", true}, {"a", true}, {"a_B", true}, {"f00", true}, {"f0o", true}, {"fo0", true}, {"foo", true}, {"foo.bar", true}, {"foo.bar.baz", true}, {"δΈ–η•Œ", true}, } for _, tc := range testCases { got := validPropertyName(tc.name) if got != tc.want { t.Errorf("%q: got %v, want %v", tc.name, got, tc.want) } } } func TestStructCodec(t *testing.T) { type oStruct struct { O int } type pStruct struct { P int Q int } type rStruct struct { R int S pStruct T oStruct oStruct } type uStruct struct { U int v int } type vStruct struct { V string `datastore:",noindex"` } oStructCodec := &structCodec{ fields: map[string]fieldCodec{ "O": {path: []int{0}}, }, complete: true, } pStructCodec := &structCodec{ fields: map[string]fieldCodec{ "P": {path: []int{0}}, "Q": {path: []int{1}}, }, complete: true, } rStructCodec := &structCodec{ fields: map[string]fieldCodec{ "R": {path: []int{0}}, "S": {path: []int{1}, structCodec: pStructCodec}, "T": {path: []int{2}, structCodec: oStructCodec}, "O": {path: []int{3, 0}}, }, complete: true, } uStructCodec := &structCodec{ fields: map[string]fieldCodec{ "U": {path: []int{0}}, }, complete: true, } vStructCodec := &structCodec{ fields: map[string]fieldCodec{ "V": {path: []int{0}, noIndex: true}, }, complete: true, } testCases := []struct { desc string structValue interface{} want *structCodec }{ { "oStruct", oStruct{}, oStructCodec, }, { "pStruct", pStruct{}, pStructCodec, }, { "rStruct", rStruct{}, rStructCodec, }, { "uStruct", uStruct{}, uStructCodec, }, { "non-basic fields", struct { B appengine.BlobKey K *Key T time.Time }{}, &structCodec{ fields: map[string]fieldCodec{ "B": {path: []int{0}}, "K": {path: []int{1}}, "T": {path: []int{2}}, }, complete: true, }, }, { "struct tags with ignored embed", struct { A int `datastore:"a,noindex"` B int `datastore:"b"` C int `datastore:",noindex"` D int `datastore:""` E int I int `datastore:"-"` J int `datastore:",noindex" json:"j"` oStruct `datastore:"-"` }{}, &structCodec{ fields: map[string]fieldCodec{ "a": {path: []int{0}, noIndex: true}, "b": {path: []int{1}}, "C": {path: []int{2}, noIndex: true}, "D": {path: []int{3}}, "E": {path: []int{4}}, "J": {path: []int{6}, noIndex: true}, }, complete: true, }, }, { "unexported fields", struct { A int b int C int `datastore:"x"` d int `datastore:"Y"` }{}, &structCodec{ fields: map[string]fieldCodec{ "A": {path: []int{0}}, "x": {path: []int{2}}, }, complete: true, }, }, { "nested and embedded structs", struct { A int B int CC oStruct DDD rStruct oStruct }{}, &structCodec{ fields: map[string]fieldCodec{ "A": {path: []int{0}}, "B": {path: []int{1}}, "CC": {path: []int{2}, structCodec: oStructCodec}, "DDD": {path: []int{3}, structCodec: rStructCodec}, "O": {path: []int{4, 0}}, }, complete: true, }, }, { "struct tags with nested and embedded structs", struct { A int `datastore:"-"` B int `datastore:"w"` C oStruct `datastore:"xx"` D rStruct `datastore:"y"` oStruct `datastore:"z"` }{}, &structCodec{ fields: map[string]fieldCodec{ "w": {path: []int{1}}, "xx": {path: []int{2}, structCodec: oStructCodec}, "y": {path: []int{3}, structCodec: rStructCodec}, "z.O": {path: []int{4, 0}}, }, complete: true, }, }, { "unexported nested and embedded structs", struct { a int B int c uStruct D uStruct uStruct }{}, &structCodec{ fields: map[string]fieldCodec{ "B": {path: []int{1}}, "D": {path: []int{3}, structCodec: uStructCodec}, "U": {path: []int{4, 0}}, }, complete: true, }, }, { "noindex nested struct", struct { A oStruct `datastore:",noindex"` }{}, &structCodec{ fields: map[string]fieldCodec{ "A": {path: []int{0}, structCodec: oStructCodec, noIndex: true}, }, complete: true, }, }, { "noindex slice", struct { A []string `datastore:",noindex"` }{}, &structCodec{ fields: map[string]fieldCodec{ "A": {path: []int{0}, noIndex: true}, }, hasSlice: true, complete: true, }, }, { "noindex embedded struct slice", struct { // vStruct has a single field, V, also with noindex. A []vStruct `datastore:",noindex"` }{}, &structCodec{ fields: map[string]fieldCodec{ "A": {path: []int{0}, structCodec: vStructCodec, noIndex: true}, }, hasSlice: true, complete: true, }, }, } for _, tc := range testCases { got, err := getStructCodec(reflect.TypeOf(tc.structValue)) if err != nil { t.Errorf("%s: getStructCodec: %v", tc.desc, err) continue } // can't reflect.DeepEqual b/c element order in fields map may differ if !isEqualStructCodec(got, tc.want) { t.Errorf("%s\ngot %+v\nwant %+v\n", tc.desc, got, tc.want) } } } func isEqualStructCodec(got, want *structCodec) bool { if got.complete != want.complete { return false } if got.hasSlice != want.hasSlice { return false } if len(got.fields) != len(want.fields) { return false } for name, wantF := range want.fields { gotF := got.fields[name] if !reflect.DeepEqual(wantF.path, gotF.path) { return false } if wantF.noIndex != gotF.noIndex { return false } if wantF.structCodec != nil { if gotF.structCodec == nil { return false } if !isEqualStructCodec(gotF.structCodec, wantF.structCodec) { return false } } } return true } func TestRepeatedPropertyName(t *testing.T) { good := []interface{}{ struct { A int `datastore:"-"` }{}, struct { A int `datastore:"b"` B int }{}, struct { A int B int `datastore:"B"` }{}, struct { A int `datastore:"B"` B int `datastore:"-"` }{}, struct { A int `datastore:"-"` B int `datastore:"A"` }{}, struct { A int `datastore:"B"` B int `datastore:"A"` }{}, struct { A int `datastore:"B"` B int `datastore:"C"` C int `datastore:"A"` }{}, struct { A int `datastore:"B"` B int `datastore:"C"` C int `datastore:"D"` }{}, } bad := []interface{}{ struct { A int `datastore:"B"` B int }{}, struct { A int B int `datastore:"A"` }{}, struct { A int `datastore:"C"` B int `datastore:"C"` }{}, struct { A int `datastore:"B"` B int `datastore:"C"` C int `datastore:"B"` }{}, } testGetStructCodec(t, good, bad) } func TestFlatteningNestedStructs(t *testing.T) { type DeepGood struct { A struct { B []struct { C struct { D int } } } } type DeepBad struct { A struct { B []struct { C struct { D []int } } } } type ISay struct { Tomato int } type YouSay struct { Tomato int } type Tweedledee struct { Dee int `datastore:"D"` } type Tweedledum struct { Dum int `datastore:"D"` } good := []interface{}{ struct { X []struct { Y string } }{}, struct { X []struct { Y []byte } }{}, struct { P []int X struct { Y []int } }{}, struct { X struct { Y []int } Q []int }{}, struct { P []int X struct { Y []int } Q []int }{}, struct { DeepGood }{}, struct { DG DeepGood }{}, struct { Foo struct { Z int } `datastore:"A"` Bar struct { Z int } `datastore:"B"` }{}, } bad := []interface{}{ struct { X []struct { Y []string } }{}, struct { X []struct { Y []int } }{}, struct { DeepBad }{}, struct { DB DeepBad }{}, struct { ISay YouSay }{}, struct { Tweedledee Tweedledum }{}, struct { Foo struct { Z int } `datastore:"A"` Bar struct { Z int } `datastore:"A"` }{}, } testGetStructCodec(t, good, bad) } func testGetStructCodec(t *testing.T, good []interface{}, bad []interface{}) { for _, x := range good { if _, err := getStructCodec(reflect.TypeOf(x)); err != nil { t.Errorf("type %T: got non-nil error (%s), want nil", x, err) } } for _, x := range bad { if _, err := getStructCodec(reflect.TypeOf(x)); err == nil { t.Errorf("type %T: got nil error, want non-nil", x) } } } func TestNilKeyIsStored(t *testing.T) { x := struct { K *Key I int }{} p := PropertyList{} // Save x as properties. p1, _ := SaveStruct(&x) p.Load(p1) // Set x's fields to non-zero. x.K = &Key{} x.I = 2 // Load x from properties. p2, _ := p.Save() LoadStruct(&x, p2) // Check that x's fields were set to zero. if x.K != nil { t.Errorf("K field was not zero") } if x.I != 0 { t.Errorf("I field was not zero") } } func TestSaveStructOmitEmpty(t *testing.T) { // Expected props names are sorted alphabetically expectedPropNamesForSingles := []string{"EmptyValue", "NonEmptyValue", "OmitEmptyWithValue"} expectedPropNamesForSlices := []string{"NonEmptyValue", "NonEmptyValue", "OmitEmptyWithValue", "OmitEmptyWithValue"} testOmitted := func(expectedPropNames []string, src interface{}) { // t.Helper() - this is available from Go version 1.9, but we also support Go versions 1.6, 1.7, 1.8 if props, err := SaveStruct(src); err != nil { t.Fatal(err) } else { // Collect names for reporting if diffs from expected and for easier sorting actualPropNames := make([]string, len(props)) for i := range props { actualPropNames[i] = props[i].Name } // Sort actuals for comparing with already sorted expected names sort.Sort(sort.StringSlice(actualPropNames)) if !reflect.DeepEqual(actualPropNames, expectedPropNames) { t.Errorf("Expected this properties: %v, got: %v", expectedPropNames, actualPropNames) } } } testOmitted(expectedPropNamesForSingles, &struct { EmptyValue int NonEmptyValue int OmitEmptyNoValue int `datastore:",omitempty"` OmitEmptyWithValue int `datastore:",omitempty"` }{ NonEmptyValue: 1, OmitEmptyWithValue: 2, }) testOmitted(expectedPropNamesForSlices, &struct { EmptyValue []int NonEmptyValue []int OmitEmptyNoValue []int `datastore:",omitempty"` OmitEmptyWithValue []int `datastore:",omitempty"` }{ NonEmptyValue: []int{1, 2}, OmitEmptyWithValue: []int{3, 4}, }) testOmitted(expectedPropNamesForSingles, &struct { EmptyValue bool NonEmptyValue bool OmitEmptyNoValue bool `datastore:",omitempty"` OmitEmptyWithValue bool `datastore:",omitempty"` }{ NonEmptyValue: true, OmitEmptyWithValue: true, }) testOmitted(expectedPropNamesForSlices, &struct { EmptyValue []bool NonEmptyValue []bool OmitEmptyNoValue []bool `datastore:",omitempty"` OmitEmptyWithValue []bool `datastore:",omitempty"` }{ NonEmptyValue: []bool{true, true}, OmitEmptyWithValue: []bool{true, true}, }) testOmitted(expectedPropNamesForSingles, &struct { EmptyValue string NonEmptyValue string OmitEmptyNoValue string `datastore:",omitempty"` OmitEmptyWithValue string `datastore:",omitempty"` }{ NonEmptyValue: "s", OmitEmptyWithValue: "s", }) testOmitted(expectedPropNamesForSlices, &struct { EmptyValue []string NonEmptyValue []string OmitEmptyNoValue []string `datastore:",omitempty"` OmitEmptyWithValue []string `datastore:",omitempty"` }{ NonEmptyValue: []string{"s1", "s2"}, OmitEmptyWithValue: []string{"s3", "s4"}, }) testOmitted(expectedPropNamesForSingles, &struct { EmptyValue float32 NonEmptyValue float32 OmitEmptyNoValue float32 `datastore:",omitempty"` OmitEmptyWithValue float32 `datastore:",omitempty"` }{ NonEmptyValue: 1.1, OmitEmptyWithValue: 1.2, }) testOmitted(expectedPropNamesForSlices, &struct { EmptyValue []float32 NonEmptyValue []float32 OmitEmptyNoValue []float32 `datastore:",omitempty"` OmitEmptyWithValue []float32 `datastore:",omitempty"` }{ NonEmptyValue: []float32{1.1, 2.2}, OmitEmptyWithValue: []float32{3.3, 4.4}, }) testOmitted(expectedPropNamesForSingles, &struct { EmptyValue time.Time NonEmptyValue time.Time OmitEmptyNoValue time.Time `datastore:",omitempty"` OmitEmptyWithValue time.Time `datastore:",omitempty"` }{ NonEmptyValue: now, OmitEmptyWithValue: now, }) testOmitted(expectedPropNamesForSlices, &struct { EmptyValue []time.Time NonEmptyValue []time.Time OmitEmptyNoValue []time.Time `datastore:",omitempty"` OmitEmptyWithValue []time.Time `datastore:",omitempty"` }{ NonEmptyValue: []time.Time{now, now}, OmitEmptyWithValue: []time.Time{now, now}, }) }