blob: 70fc4c5185f3a19020c5a229d311a5a7f705f39f [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// adapted from github.com/golang/appengine/datastore
package datastore
import (
"bytes"
"encoding/json"
"fmt"
"math"
"reflect"
"strconv"
"strings"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"go.chromium.org/gae/service/blobstore"
. "go.chromium.org/luci/common/testing/assertions"
)
var (
mp = MkProperty
mpNI = MkPropertyNI
)
const testAppID = "testApp"
type (
myBlob []byte
myByte byte
myString string
)
func makeMyByteSlice(n int) []myByte {
b := make([]myByte, n)
for i := range b {
b[i] = myByte(i)
}
return b
}
func makeInt8Slice(n int) []int8 {
b := make([]int8, n)
for i := range b {
b[i] = int8(i)
}
return b
}
func makeUint8Slice(n int) []uint8 {
b := make([]uint8, n)
for i := range b {
b[i] = uint8(i)
}
return b
}
var (
testKey0 = mkKey("kind", "name0")
testKey1a = mkKey("kind", "name1")
testKey1b = mkKey("kind", "name1")
testKey2a = mkKey("kind", "name0", "kind", "name2")
testKey2b = mkKey("kind", "name0", "kind", "name2")
testGeoPt0 = GeoPoint{Lat: 1.2, Lng: 3.4}
testGeoPt1 = GeoPoint{Lat: 5, Lng: 10}
testBadGeoPt = GeoPoint{Lat: 1000, Lng: 34}
)
type B0 struct {
B []byte `gae:",noindex"`
}
type B1 struct {
B []int8
}
type B2 struct {
B myBlob
}
type B3 struct {
B []myByte
}
type B4 struct {
B [][]byte `gae:",noindex"`
}
type B5 struct {
B []byte
}
type C0 struct {
I int
C chan int
}
type C1 struct {
I int
C *chan int
}
type C2 struct {
I int
C []chan int
}
type C3 struct {
C string
}
type E struct{}
type G0 struct {
G GeoPoint
}
type G1 struct {
G []GeoPoint
}
type K0 struct {
K *Key
}
type K1 struct {
K []*Key
}
type N0 struct {
X0
ID int64 `gae:"$id"`
_kind string `gae:"$kind,whatnow"`
Nonymous X0
Ignore string `gae:"-"`
Other string
}
type N1 struct {
X0
Nonymous []X0
Ignore string `gae:"-"`
Other string
}
type N2 struct {
N1 `gae:"red"`
Green N1 `gae:"green"`
Blue N1
White N1 `gae:"-"`
}
type N3 struct {
ID uint32 `gae:"$id,200"`
}
type O0 struct {
I int64
}
type O1 struct {
I int32
}
type U0 struct {
U uint32
}
type U1 struct {
U byte
}
type U2 struct {
U int64
}
type T struct {
T time.Time
}
type X0 struct {
S string
I int
i int
}
type X1 struct {
S myString
I int32
J int64
}
type X2 struct {
Z string
i int
}
type X3 struct {
S bool
I int
}
type Y0 struct {
B bool
F []float64
G []float64
}
type Y1 struct {
B bool
F float64
}
type Y2 struct {
B bool
F []int64
}
type Y3 struct {
B bool
F int64
}
type Tagged struct {
A int `gae:"a,noindex"`
B []int `gae:"b1"`
C int `gae:",noindex"`
D int `gae:""`
E int
I int `gae:"-"`
J int `gae:",noindex" json:"j"`
Y0 `gae:"-"`
Z chan int `gae:"-,"`
}
type InvalidTagged1 struct {
I int `gae:"\t"`
}
type InvalidTagged2 struct {
I int
J int `gae:"I"`
}
type InvalidTagged3 struct {
I int `gae:"a\t"`
}
type InvalidTagged4 struct {
I int `gae:"a."`
}
type InvalidTaggedSub struct {
I int
}
type InvalidTagged5 struct {
I int `gae:"V.I"`
V []InvalidTaggedSub
}
type Inner1 struct {
W int32
X string
}
type Inner2 struct {
Y float64
}
type Inner3 struct {
Z bool
}
type Outer struct {
A int16
I []Inner1
J Inner2
Inner3
}
type OuterEquivalent struct {
A int16
IDotW []int32 `gae:"I.W"`
IDotX []string `gae:"I.X"`
JDotY float64 `gae:"J.Y"`
Z bool
}
type Dotted struct {
A DottedA `gae:"A0.A1.A2"`
}
type DottedA struct {
B DottedB `gae:"B3"`
}
type DottedB struct {
C int `gae:"C4.C5"`
}
type SliceOfSlices struct {
I int
S []struct {
J int
F []float64
}
}
type Recursive struct {
I int
R []Recursive
}
type MutuallyRecursive0 struct {
I int
R []MutuallyRecursive1
}
type MutuallyRecursive1 struct {
I int
R []MutuallyRecursive0
}
type ExoticTypes struct {
BS blobstore.Key
}
type Underspecified struct {
Iface PropertyConverter
}
type MismatchTypes struct {
S string
B bool
F float32
K *Key
T time.Time
G GeoPoint
IS []int
}
type BadMeta struct {
ID int64 `gae:"$id"`
id string `gae:"$id"`
}
type Doubler struct {
S string
I int64
B bool
}
func (d *Doubler) Load(props PropertyMap) error {
return GetPLS(d).Load(props)
}
func (d *Doubler) Save(withMeta bool) (PropertyMap, error) {
pls := GetPLS(d)
propMap, err := pls.Save(withMeta)
if err != nil {
return nil, err
}
double := func(prop *Property) {
switch v := prop.Value().(type) {
case string:
// + means string concatenation.
So(prop.SetValue(v+v, prop.IndexSetting()), ShouldBeNil)
case int64:
// + means integer addition.
So(prop.SetValue(v+v, prop.IndexSetting()), ShouldBeNil)
}
}
// Edit that map and send it on.
for k, v := range propMap {
if prop, ok := v.(Property); ok {
double(&prop)
propMap[k] = prop
}
}
return propMap, nil
}
func (d *Doubler) Problem() error { return nil }
var _ PropertyLoadSaver = (*Doubler)(nil)
type Deriver struct {
S, Derived, Ignored string
}
func (d *Deriver) Load(props PropertyMap) error {
for name := range props {
if name != "S" {
continue
}
d.S = props.Slice(name)[0].Value().(string)
d.Derived = "derived+" + d.S
}
return nil
}
func (d *Deriver) Save(withMeta bool) (PropertyMap, error) {
return map[string]PropertyData{
"S": mp(d.S),
}, nil
}
func (d *Deriver) Problem() error { return nil }
var _ PropertyLoadSaver = (*Deriver)(nil)
type Augmenter struct {
S string
g string `gae:"-"`
}
func (a *Augmenter) Load(props PropertyMap) error {
if e := props.Slice("Extra"); len(e) > 0 {
a.g = e[0].Value().(string)
delete(props, "Extra")
}
if err := GetPLS(a).Load(props); err != nil {
return err
}
return nil
}
func (a *Augmenter) Save(withMeta bool) (PropertyMap, error) {
props, err := GetPLS(a).Save(withMeta)
if err != nil {
return nil, err
}
props["Extra"] = MkProperty("ohai!")
return props, nil
}
func (a *Augmenter) Problem() error { return nil }
var _ PropertyLoadSaver = (*Augmenter)(nil)
type BK struct {
Key blobstore.Key
}
type Convertable []int64
var _ PropertyConverter = (*Convertable)(nil)
func (c *Convertable) ToProperty() (ret Property, err error) {
buf := make([]string, len(*c))
for i, v := range *c {
buf[i] = strconv.FormatInt(v, 10)
}
err = ret.SetValue(strings.Join(buf, ","), NoIndex)
return
}
func (c *Convertable) FromProperty(pv Property) error {
if sval, ok := pv.Value().(string); ok {
for _, t := range strings.Split(sval, ",") {
ival, err := strconv.ParseInt(t, 10, 64)
if err != nil {
return err
}
*c = append(*c, ival)
}
return nil
}
return fmt.Errorf("nope")
}
type Impossible struct {
Nested []ImpossibleInner
}
type ImpossibleInner struct {
Ints Convertable `gae:"wot"`
}
type Convertable2 struct {
Data string
Exploded []string
}
func (c *Convertable2) ToProperty() (ret Property, err error) {
err = ret.SetValue(c.Data, ShouldIndex)
return
}
func (c *Convertable2) FromProperty(pv Property) error {
if sval, ok := pv.Value().(string); ok {
c.Data = sval
c.Exploded = []string{"turn", "down", "for", "what"}
return nil
}
return fmt.Errorf("nope")
}
type Impossible2 struct {
Nested []ImpossibleInner2
}
type ImpossibleInner2 struct {
Thingy Convertable2 `gae:"nerb"`
}
type JSONKVProp map[string]interface{}
var _ PropertyConverter = (*JSONKVProp)(nil)
func (j *JSONKVProp) ToProperty() (ret Property, err error) {
data, err := json.Marshal(map[string]interface{}(*j))
if err != nil {
return
}
err = ret.SetValue(data, NoIndex)
return
}
func (j *JSONKVProp) FromProperty(pv Property) error {
if bval, ok := pv.Value().([]byte); ok {
dec := json.NewDecoder(bytes.NewBuffer(bval))
dec.UseNumber()
return dec.Decode((*map[string]interface{})(j))
}
return fmt.Errorf("nope")
}
type Impossible3 struct {
KMap JSONKVProp `gae:"kewelmap"`
}
type Complex complex128
var _ PropertyConverter = (*Complex)(nil)
func (c *Complex) ToProperty() (ret Property, err error) {
// cheat hardkore and usurp GeoPoint so datastore will index these suckers
// (note that this won't REALLY work, since GeoPoints are limited to a very
// limited range of values, but it's nice to pretend ;)). You'd probably
// really end up with a packed binary representation.
err = ret.SetValue(GeoPoint{Lat: real(*c), Lng: imag(*c)}, ShouldIndex)
return
}
func (c *Complex) FromProperty(p Property) error {
if gval, ok := p.Value().(GeoPoint); ok {
*c = Complex(complex(gval.Lat, gval.Lng))
return nil
}
return fmt.Errorf("nope")
}
type Impossible4 struct {
Values []Complex
}
type DerivedKey struct {
K *Key
}
type IfaceKey struct {
K *Key
}
type IDParser struct {
_kind string `gae:"$kind,CoolKind"`
// real $id is myParentName|myID
parent string `gae:"-"`
id int64 `gae:"-"`
}
var _ MetaGetterSetter = (*IDParser)(nil)
func (i *IDParser) getFullID() string {
return fmt.Sprintf("%s|%d", i.parent, i.id)
}
func (i *IDParser) GetAllMeta() PropertyMap {
pm := GetPLS(i).GetAllMeta()
pm.SetMeta("id", i.getFullID())
return pm
}
func (i *IDParser) GetMeta(key string) (interface{}, bool) {
if key == "id" {
return i.getFullID(), true
}
return GetPLS(i).GetMeta(key)
}
func (i *IDParser) SetMeta(key string, value interface{}) bool {
if key == "id" {
// let the panics flooowwww
vS := strings.SplitN(value.(string), "|", 2)
i.parent = vS[0]
var err error
i.id, err = strconv.ParseInt(vS[1], 10, 64)
if err != nil {
panic(err)
}
return true
}
return GetPLS(i).SetMeta(key, value)
}
type KindOverride struct {
ID int64 `gae:"$id"`
customKind string `gae:"-"`
}
var _ MetaGetterSetter = (*KindOverride)(nil)
func (i *KindOverride) GetAllMeta() PropertyMap {
pm := GetPLS(i).GetAllMeta()
if i.customKind != "" {
pm.SetMeta("kind", i.customKind)
}
return pm
}
func (i *KindOverride) GetMeta(key string) (interface{}, bool) {
if key == "kind" && i.customKind != "" {
return i.customKind, true
}
return GetPLS(i).GetMeta(key)
}
func (i *KindOverride) SetMeta(key string, value interface{}) bool {
if key == "kind" {
kind := value.(string)
if kind != "KindOverride" {
i.customKind = kind
} else {
i.customKind = ""
}
return true
}
return GetPLS(i).SetMeta(key, value)
}
type EmbeddedID struct {
Thing string
Val int
}
var _ PropertyConverter = (*EmbeddedID)(nil)
func (e *EmbeddedID) ToProperty() (ret Property, err error) {
return mpNI(fmt.Sprintf("%s|%d", e.Thing, e.Val)), nil
}
func (e *EmbeddedID) FromProperty(val Property) error {
if val.Type() != PTString {
return fmt.Errorf("gotta have a string")
}
toks := strings.SplitN(val.Value().(string), "|", 2)
if len(toks) != 2 {
return fmt.Errorf("gotta have two parts")
}
v, err := strconv.Atoi(toks[1])
if err != nil {
return err
}
e.Thing = toks[0]
e.Val = v
return nil
}
type IDEmbedder struct {
EmbeddedID `gae:"$id"`
}
type Simple struct{}
type testCase struct {
desc string
src interface{}
want interface{}
plsErr string
saveErr string
plsLoadErr string
loadErr string
}
var testCases = []testCase{
{
desc: "chan save fails",
src: &C0{I: -1},
plsErr: `field "C" has invalid type: chan int`,
},
{
desc: "*chan save fails",
src: &C1{I: -1},
plsErr: `field "C" has invalid type: *chan int`,
},
{
desc: "[]chan save fails",
src: &C2{I: -1, C: make([]chan int, 8)},
plsErr: `field "C" has invalid type: []chan int`,
},
{
desc: "chan load fails",
src: &C3{C: "not a chan"},
want: &C0{},
plsLoadErr: `field "C" has invalid type: chan int`,
},
{
desc: "*chan load fails",
src: &C3{C: "not a *chan"},
want: &C1{},
plsLoadErr: `field "C" has invalid type: *chan int`,
},
{
desc: "[]chan load fails",
src: &C3{C: "not a []chan"},
want: &C2{},
plsLoadErr: `field "C" has invalid type: []chan int`,
},
{
desc: "empty struct",
src: &E{},
want: &E{},
},
{
desc: "geopoint",
src: &G0{G: testGeoPt0},
want: &G0{G: testGeoPt0},
},
{
desc: "geopoint invalid",
src: &G0{G: testBadGeoPt},
saveErr: "invalid GeoPoint value",
},
{
desc: "geopoint as props",
src: &G0{G: testGeoPt0},
want: PropertyMap{
"G": mp(testGeoPt0),
},
},
{
desc: "geopoint slice",
src: &G1{G: []GeoPoint{testGeoPt0, testGeoPt1}},
want: &G1{G: []GeoPoint{testGeoPt0, testGeoPt1}},
},
{
desc: "key",
src: &K0{K: testKey1a},
want: &K0{K: testKey1b},
},
{
desc: "key with parent",
src: &K0{K: testKey2a},
want: &K0{K: testKey2b},
},
{
desc: "nil key",
src: &K0{},
want: &K0{},
},
{
desc: "all nil keys in slice",
src: &K1{[]*Key{nil, nil}},
want: &K1{[]*Key{nil, nil}},
},
{
desc: "some nil keys in slice",
src: &K1{[]*Key{testKey1a, nil, testKey2a}},
want: &K1{[]*Key{testKey1b, nil, testKey2b}},
},
{
desc: "overflow",
src: &O0{I: 1 << 48},
want: &O1{},
loadErr: "overflow",
},
{
desc: "underflow",
src: &O0{I: math.MaxInt64},
want: &O1{},
loadErr: "overflow",
},
{
desc: "time",
src: &T{T: time.Unix(1e9, 0).UTC()},
want: &T{T: time.Unix(1e9, 0).UTC()},
},
{
desc: "time as props",
src: &T{T: time.Unix(1e9, 0).UTC()},
want: PropertyMap{
"T": mp(time.Unix(1e9, 0).UTC()),
},
},
{
desc: "uint32 save",
src: &U0{U: 1},
want: PropertyMap{
"U": mp(1),
},
},
{
desc: "uint32 load",
src: &U2{U: 100},
want: &U0{U: 100},
},
{
desc: "uint32 load oob (neg)",
src: &U2{U: -1},
want: &U0{},
loadErr: "overflow",
},
{
desc: "uint32 load oob (huge)",
src: &U2{U: math.MaxInt64},
want: &U0{},
loadErr: "overflow",
},
{
desc: "byte save",
src: &U1{U: 1},
want: PropertyMap{
"U": mp(1),
},
},
{
desc: "byte load",
src: &U2{U: 100},
want: &U1{U: 100},
},
{
desc: "byte load oob (neg)",
src: &U2{U: -1},
want: &U1{},
loadErr: "overflow",
},
{
desc: "byte load oob (huge)",
src: &U2{U: math.MaxInt64},
want: &U1{},
loadErr: "overflow",
},
{
desc: "zero",
src: &X0{},
want: &X0{},
},
{
desc: "basic",
src: &X0{S: "one", I: 2, i: 3},
want: &X0{S: "one", I: 2},
},
{
desc: "save string/int load myString/int32",
src: &X0{S: "one", I: 2, i: 3},
want: &X1{S: "one", I: 2},
},
{
desc: "missing fields",
src: &X0{S: "one", I: 2, i: 3},
want: &X2{},
loadErr: "no such struct field",
},
{
desc: "save string load bool",
src: &X0{S: "one", I: 2, i: 3},
want: &X3{I: 2},
loadErr: "type mismatch",
},
{
desc: "basic slice",
src: &Y0{B: true, F: []float64{7, 8, 9}},
want: &Y0{B: true, F: []float64{7, 8, 9}},
},
{
desc: "save []float64 load float64",
src: &Y0{B: true, F: []float64{7, 8, 9}},
want: &Y1{B: true},
loadErr: "requires a slice",
},
{
desc: "save single []int64 load int64",
src: &Y2{B: true, F: []int64{7}},
want: &Y3{B: true, F: 7},
},
{
desc: "save int64 load single []int64",
src: &Y3{B: true, F: 7},
want: &Y2{B: true, F: []int64{7}},
},
{
desc: "use convertable slice",
src: &Impossible{[]ImpossibleInner{{Convertable{1, 5, 9}}, {Convertable{2, 4, 6}}}},
want: &Impossible{[]ImpossibleInner{{Convertable{1, 5, 9}}, {Convertable{2, 4, 6}}}},
},
{
desc: "use convertable slice (to map)",
src: &Impossible{[]ImpossibleInner{{Convertable{1, 5, 9}}, {Convertable{2, 4, 6}}}},
want: PropertyMap{
"Nested.wot": PropertySlice{mpNI("1,5,9"), mpNI("2,4,6")},
},
},
{
desc: "convertable slice (bad load)",
src: PropertyMap{"Nested.wot": mpNI([]byte("ohai"))},
want: &Impossible{[]ImpossibleInner{{}}},
loadErr: "nope",
},
{
desc: "use convertable struct",
src: &Impossible2{
[]ImpossibleInner2{
{Convertable2{"nerb", nil}},
},
},
want: &Impossible2{
[]ImpossibleInner2{
{Convertable2{"nerb", []string{"turn", "down", "for", "what"}}},
},
},
},
{
desc: "convertable json KVMap",
src: &Impossible3{
JSONKVProp{
"epic": "success",
"no_way!": []interface{}{true, "story"},
"what": []interface{}{"is", "really", 100},
},
},
want: &Impossible3{
JSONKVProp{
"epic": "success",
"no_way!": []interface{}{true, "story"},
"what": []interface{}{"is", "really", json.Number("100")},
},
},
},
{
desc: "convertable json KVMap (to map)",
src: &Impossible3{
JSONKVProp{
"epic": "success",
"no_way!": []interface{}{true, "story"},
"what": []interface{}{"is", "really", 100},
},
},
want: PropertyMap{
"kewelmap": mpNI([]byte(
`{"epic":"success","no_way!":[true,"story"],"what":["is","really",100]}`)),
},
},
{
desc: "convertable complex slice",
src: &Impossible4{
[]Complex{complex(1, 2), complex(3, 4)},
},
want: &Impossible4{
[]Complex{complex(1, 2), complex(3, 4)},
},
},
{
desc: "convertable complex slice (to map)",
src: &Impossible4{
[]Complex{complex(1, 2), complex(3, 4)},
},
want: PropertyMap{
"Values": PropertySlice{mp(GeoPoint{Lat: 1, Lng: 2}), mp(GeoPoint{Lat: 3, Lng: 4})},
},
},
{
desc: "convertable complex slice (bad load)",
src: PropertyMap{"Values": mp("hello")},
want: &Impossible4{[]Complex(nil)},
loadErr: "nope",
},
{
desc: "allow concrete *Key implementors (save)",
src: &DerivedKey{testKey2a},
want: &IfaceKey{testKey2b},
},
{
desc: "allow concrete *Key implementors (load)",
src: &IfaceKey{testKey2b},
want: &DerivedKey{testKey2a},
},
{
desc: "save []float64 load []int64",
src: &Y0{B: true, F: []float64{7, 8, 9}},
want: &Y2{B: true},
loadErr: "type mismatch",
},
{
desc: "single slice is too long",
src: &Y0{F: make([]float64, maxIndexedProperties+1)},
want: &Y0{},
saveErr: "gae: too many indexed properties",
},
{
desc: "two slices are too long",
src: &Y0{F: make([]float64, maxIndexedProperties), G: make([]float64, maxIndexedProperties)},
want: &Y0{},
saveErr: "gae: too many indexed properties",
},
{
desc: "one slice and one scalar are too long",
src: &Y0{F: make([]float64, maxIndexedProperties), B: true},
want: &Y0{},
saveErr: "gae: too many indexed properties",
},
{
desc: "long blob",
src: &B0{B: makeUint8Slice(maxIndexedProperties + 1)},
want: &B0{B: makeUint8Slice(maxIndexedProperties + 1)},
},
{
desc: "long []int8 is too long",
src: &B1{B: makeInt8Slice(maxIndexedProperties + 1)},
want: &B1{},
saveErr: "gae: too many indexed properties",
},
{
desc: "short []int8",
src: &B1{B: makeInt8Slice(3)},
want: &B1{B: makeInt8Slice(3)},
},
{
desc: "long myBlob",
src: &B2{B: makeUint8Slice(maxIndexedProperties + 1)},
want: &B2{B: makeUint8Slice(maxIndexedProperties + 1)},
},
{
desc: "short myBlob",
src: &B2{B: makeUint8Slice(3)},
want: &B2{B: makeUint8Slice(3)},
},
{
desc: "long []myByte",
src: &B3{B: makeMyByteSlice(maxIndexedProperties + 1)},
want: &B3{B: makeMyByteSlice(maxIndexedProperties + 1)},
},
{
desc: "short []myByte",
src: &B3{B: makeMyByteSlice(3)},
want: &B3{B: makeMyByteSlice(3)},
},
{
desc: "slice of blobs",
src: &B4{B: [][]byte{
makeUint8Slice(3),
makeUint8Slice(4),
makeUint8Slice(5),
}},
want: &B4{B: [][]byte{
makeUint8Slice(3),
makeUint8Slice(4),
makeUint8Slice(5),
}},
},
{
desc: "short []byte",
src: &B5{B: makeUint8Slice(3)},
want: &B5{B: makeUint8Slice(3)},
},
{
desc: "short ByteString as props",
src: &B5{B: makeUint8Slice(3)},
want: PropertyMap{
"B": mp(makeUint8Slice(3)),
},
},
{
desc: "save tagged load props",
src: &Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, I: 6, J: 7},
want: PropertyMap{
// A and B are renamed to a and b; A and C are noindex, I is ignored.
// Indexed properties are loaded before raw properties. Thus, the
// result is: b, b, b, D, E, a, c.
"b1": PropertySlice{
mp(21),
mp(22),
mp(23),
},
"D": mp(4),
"E": mp(5),
"a": mpNI(1),
"C": mpNI(3),
"J": mpNI(7),
},
},
{
desc: "save tagged load tagged",
src: &Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, I: 6, J: 7},
want: &Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, J: 7},
},
{
desc: "save props load tagged",
src: PropertyMap{
"A": mpNI(11),
"a": mpNI(12),
},
want: &Tagged{A: 12},
loadErr: `cannot load field "A"`,
},
{
desc: "invalid tagged1",
src: &InvalidTagged1{I: 1},
plsErr: `struct tag has invalid property name: "\t"`,
},
{
desc: "invalid tagged2",
src: &InvalidTagged2{I: 1, J: 2},
want: &InvalidTagged2{},
plsErr: `struct tag has repeated property name: "I"`,
},
{
desc: "invalid tagged3",
src: &InvalidTagged3{I: 1},
plsErr: `struct tag has invalid property name: "a\t"`,
},
{
desc: "invalid tagged4",
src: &InvalidTagged4{I: 1},
plsErr: `struct tag has invalid property name: "a."`,
},
{
desc: "invalid tagged5",
src: &InvalidTagged5{I: 19, V: []InvalidTaggedSub{{1}}},
plsErr: `struct tag has repeated property name: "V.I"`,
},
{
desc: "doubler",
src: &Doubler{S: "s", I: 1, B: true},
want: &Doubler{S: "ss", I: 2, B: true},
},
{
desc: "save struct load props",
src: &X0{S: "s", I: 1},
want: PropertyMap{
"S": mp("s"),
"I": mp(1),
},
},
{
desc: "save props load struct",
src: PropertyMap{
"S": mp("s"),
"I": mp(1),
},
want: &X0{S: "s", I: 1},
},
{
desc: "nil-value props",
src: PropertyMap{
"I": mp(nil),
"B": mp(nil),
"S": mp(nil),
"F": mp(nil),
"K": mp(nil),
"T": mp(nil),
"J": PropertySlice{
mp(nil),
mp(7),
mp(nil),
},
},
want: &struct {
I int64
B bool
S string
F float64
K *Key
T time.Time
J []int64
}{
J: []int64{0, 7, 0},
},
},
{
desc: "save outer load props",
src: &Outer{
A: 1,
I: []Inner1{
{10, "ten"},
{20, "twenty"},
{30, "thirty"},
},
J: Inner2{
Y: 3.14,
},
Inner3: Inner3{
Z: true,
},
},
want: PropertyMap{
"A": mp(1),
"I.W": PropertySlice{
mp(10),
mp(20),
mp(30),
},
"I.X": PropertySlice{
mp("ten"),
mp("twenty"),
mp("thirty"),
},
"J.Y": mp(3.14),
"Z": mp(true),
},
},
{
desc: "save props load outer-equivalent",
src: PropertyMap{
"A": mp(1),
"I.W": PropertySlice{
mp(10),
mp(20),
mp(30),
},
"I.X": PropertySlice{
mp("ten"),
mp("twenty"),
mp("thirty"),
},
"J.Y": mp(3.14),
"Z": mp(true),
},
want: &OuterEquivalent{
A: 1,
IDotW: []int32{10, 20, 30},
IDotX: []string{"ten", "twenty", "thirty"},
JDotY: 3.14,
Z: true,
},
},
{
desc: "save outer-equivalent load outer",
src: &OuterEquivalent{
A: 1,
IDotW: []int32{10, 20, 30},
IDotX: []string{"ten", "twenty", "thirty"},
JDotY: 3.14,
Z: true,
},
want: &Outer{
A: 1,
I: []Inner1{
{10, "ten"},
{20, "twenty"},
{30, "thirty"},
},
J: Inner2{
Y: 3.14,
},
Inner3: Inner3{
Z: true,
},
},
},
{
desc: "dotted names save",
src: &Dotted{A: DottedA{B: DottedB{C: 88}}},
want: PropertyMap{
"A0.A1.A2.B3.C4.C5": mp(88),
},
},
{
desc: "dotted names load",
src: PropertyMap{
"A0.A1.A2.B3.C4.C5": mp(99),
},
want: &Dotted{A: DottedA{B: DottedB{C: 99}}},
},
{
desc: "save struct load deriver",
src: &X0{S: "s", I: 1},
want: &Deriver{S: "s", Derived: "derived+s"},
},
{
desc: "save deriver load struct",
src: &Deriver{S: "s", Derived: "derived+s", Ignored: "ignored"},
want: &X0{S: "s"},
},
{
desc: "augmenter save",
src: &Augmenter{S: "s"},
want: PropertyMap{
"S": mp("s"),
"Extra": mp("ohai!"),
},
},
{
desc: "augmenter load",
src: PropertyMap{
"S": mp("s"),
"Extra": mp("kthxbye!"),
},
want: &Augmenter{S: "s", g: "kthxbye!"},
},
// Regression: CL 25062824 broke handling of appengine.BlobKey fields.
{
desc: "appengine.BlobKey",
src: &BK{Key: "blah"},
want: &BK{Key: "blah"},
},
{
desc: "zero time.Time",
src: &T{T: time.Time{}},
want: &T{T: time.Time{}},
},
{
desc: "time.Time near Unix zero time",
src: &T{T: time.Unix(0, 4e3).UTC()},
want: &T{T: time.Unix(0, 4e3).UTC()},
},
{
desc: "time.Time, far in the future",
src: &T{T: time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC)},
want: &T{T: time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC)},
},
{
desc: "time.Time, very far in the past",
src: &T{T: time.Date(-300000, 1, 1, 0, 0, 0, 0, time.UTC)},
want: &T{},
saveErr: "time value out of range",
},
{
desc: "time.Time, very far in the future",
src: &T{T: time.Date(294248, 1, 1, 0, 0, 0, 0, time.UTC)},
want: &T{},
saveErr: "time value out of range",
},
{
desc: "structs",
src: &N0{
X0: X0{S: "one", I: 2, i: 3},
Nonymous: X0{S: "four", I: 5, i: 6},
Ignore: "ignore",
Other: "other",
},
want: &N0{
X0: X0{S: "one", I: 2},
Nonymous: X0{S: "four", I: 5},
Other: "other",
},
},
{
desc: "exotic types",
src: &ExoticTypes{
BS: "sup",
},
want: &ExoticTypes{
BS: "sup",
},
},
{
desc: "exotic type projection",
src: PropertyMap{
"BS": mp([]byte("I'mABlobKey")),
},
want: &ExoticTypes{
BS: "I'mABlobKey",
},
},
{
desc: "underspecified types",
src: &Underspecified{},
plsErr: "non-concrete interface",
},
{
desc: "mismatch (string)",
src: PropertyMap{
"K": mp(199),
"S": mp([]byte("cats")),
"F": mp("nurbs"),
},
want: &MismatchTypes{},
loadErr: "type mismatch",
},
{
desc: "mismatch (float)",
src: PropertyMap{"F": mp(blobstore.Key("wot"))},
want: &MismatchTypes{},
loadErr: "type mismatch",
},
{
desc: "mismatch (float/overflow)",
src: PropertyMap{"F": mp(math.MaxFloat64)},
want: &MismatchTypes{},
loadErr: "overflows",
},
{
desc: "mismatch (key)",
src: PropertyMap{"K": mp(false)},
want: &MismatchTypes{},
loadErr: "type mismatch",
},
{
desc: "mismatch (bool)",
src: PropertyMap{"B": mp(testKey0)},
want: &MismatchTypes{},
loadErr: "type mismatch",
},
{
desc: "mismatch (time)",
src: PropertyMap{"T": mp(GeoPoint{})},
want: &MismatchTypes{},
loadErr: "type mismatch",
},
{
desc: "mismatch (geopoint)",
src: PropertyMap{"G": mp(time.Now().UTC())},
want: &MismatchTypes{},
loadErr: "type mismatch",
},
{
desc: "slice of structs",
src: &N1{
X0: X0{S: "one", I: 2, i: 3},
Nonymous: []X0{
{S: "four", I: 5, i: 6},
{S: "seven", I: 8, i: 9},
{S: "ten", I: 11, i: 12},
{S: "thirteen", I: 14, i: 15},
},
Ignore: "ignore",
Other: "other",
},
want: &N1{
X0: X0{S: "one", I: 2},
Nonymous: []X0{
{S: "four", I: 5},
{S: "seven", I: 8},
{S: "ten", I: 11},
{S: "thirteen", I: 14},
},
Other: "other",
},
},
{
desc: "structs with slices of structs",
src: &N2{
N1: N1{
X0: X0{S: "rouge"},
Nonymous: []X0{
{S: "rosso0"},
{S: "rosso1"},
},
},
Green: N1{
X0: X0{S: "vert"},
Nonymous: []X0{
{S: "verde0"},
{S: "verde1"},
{S: "verde2"},
},
},
Blue: N1{
X0: X0{S: "bleu"},
Nonymous: []X0{
{S: "blu0"},
{S: "blu1"},
{S: "blu2"},
{S: "blu3"},
},
},
},
want: &N2{
N1: N1{
X0: X0{S: "rouge"},
Nonymous: []X0{
{S: "rosso0"},
{S: "rosso1"},
},
},
Green: N1{
X0: X0{S: "vert"},
Nonymous: []X0{
{S: "verde0"},
{S: "verde1"},
{S: "verde2"},
},
},
Blue: N1{
X0: X0{S: "bleu"},
Nonymous: []X0{
{S: "blu0"},
{S: "blu1"},
{S: "blu2"},
{S: "blu3"},
},
},
},
},
{
desc: "save structs load props",
src: &N2{
N1: N1{
X0: X0{S: "rouge"},
Nonymous: []X0{
{S: "rosso0"},
{S: "rosso1"},
},
},
Green: N1{
X0: X0{S: "vert"},
Nonymous: []X0{
{S: "verde0"},
{S: "verde1"},
{S: "verde2"},
},
},
Blue: N1{
X0: X0{S: "bleu"},
Nonymous: []X0{
{S: "blu0"},
{S: "blu1"},
{S: "blu2"},
{S: "blu3"},
},
},
},
want: PropertyMap{
"red.S": mp("rouge"),
"red.I": mp(0),
"red.Nonymous.S": PropertySlice{mp("rosso0"), mp("rosso1")},
"red.Nonymous.I": PropertySlice{mp(0), mp(0)},
"red.Other": mp(""),
"green.S": mp("vert"),
"green.I": mp(0),
"green.Nonymous.S": PropertySlice{mp("verde0"), mp("verde1"), mp("verde2")},
"green.Nonymous.I": PropertySlice{mp(0), mp(0), mp(0)},
"green.Other": mp(""),
"Blue.S": mp("bleu"),
"Blue.I": mp(0),
"Blue.Nonymous.S": PropertySlice{mp("blu0"), mp("blu1"), mp("blu2"), mp("blu3")},
"Blue.Nonymous.I": PropertySlice{mp(0), mp(0), mp(0), mp(0)},
"Blue.Other": mp(""),
},
},
{
desc: "save props load structs with ragged fields",
src: PropertyMap{
"red.S": mp("rot"),
"green.Nonymous.I": PropertySlice{mp(10), mp(11), mp(12), mp(13)},
"Blue.Nonymous.S": PropertySlice{mp("blau0"), mp("blau1"), mp("blau2")},
"Blue.Nonymous.I": PropertySlice{mp(20), mp(21)},
},
want: &N2{
N1: N1{
X0: X0{S: "rot"},
},
Green: N1{
Nonymous: []X0{
{I: 10},
{I: 11},
{I: 12},
{I: 13},
},
},
Blue: N1{
Nonymous: []X0{
{S: "blau0", I: 20},
{S: "blau1", I: 21},
{S: "blau2"},
},
},
},
},
{
desc: "save structs with noindex tags",
src: &struct {
A struct {
X string `gae:",noindex"`
Y string
} `gae:",noindex"`
B struct {
X string `gae:",noindex"`
Y string
}
}{},
want: PropertyMap{
"B.Y": mp(""),
"A.X": mpNI(""),
"A.Y": mpNI(""),
"B.X": mpNI(""),
},
},
{
desc: "embedded struct with name override",
src: &struct {
Inner1 `gae:"foo"`
}{},
want: PropertyMap{
"foo.W": mp(0),
"foo.X": mp(""),
},
},
{
desc: "slice of slices",
src: &SliceOfSlices{},
plsErr: `flattening nested structs leads to a slice of slices: field "S"`,
},
{
desc: "recursive struct",
src: &Recursive{},
plsErr: `field "R" is recursively defined`,
},
{
desc: "mutually recursive struct",
src: &MutuallyRecursive0{},
plsErr: `field "R" has problem: field "R" is recursively defined`,
},
{
desc: "non-exported struct fields",
src: &struct {
i, J int64
}{i: 1, J: 2},
want: PropertyMap{
"J": mp(2),
},
},
{
desc: "json.RawMessage",
src: &struct {
J json.RawMessage
}{
J: json.RawMessage("rawr"),
},
want: PropertyMap{
"J": mp([]byte("rawr")),
},
},
{
desc: "json.RawMessage to myBlob",
src: &struct {
B json.RawMessage
}{
B: json.RawMessage("rawr"),
},
want: &B2{B: myBlob("rawr")},
},
}
func TestRoundTrip(t *testing.T) {
t.Parallel()
getPLSErr := func(obj interface{}) (pls PropertyLoadSaver, err error) {
defer func() {
if v := recover(); v != nil {
err = v.(error)
}
}()
pls = GetPLS(obj)
return
}
Convey("Test round-trip", t, func() {
for _, tc := range testCases {
tc := tc
Convey(tc.desc, func() {
pls, ok := tc.src.(PropertyLoadSaver)
if !ok {
var err error
pls, err = getPLSErr(tc.src)
if tc.plsErr != "" {
So(err, ShouldErrLike, tc.plsErr)
return
}
}
So(pls, ShouldNotBeNil)
savedProps, err := pls.Save(false)
if tc.saveErr != "" {
So(err, ShouldErrLike, tc.saveErr)
return
}
So(err, ShouldBeNil)
So(savedProps, ShouldNotBeNil)
var got interface{}
if _, ok := tc.want.(PropertyMap); ok {
pls = PropertyMap{}
got = pls
} else {
got = reflect.New(reflect.TypeOf(tc.want).Elem()).Interface()
if pls, ok = got.(PropertyLoadSaver); !ok {
var err error
pls, err = getPLSErr(got)
if tc.plsLoadErr != "" {
So(err, ShouldErrLike, tc.plsLoadErr)
return
}
}
}
So(pls, ShouldNotBeNil)
err = pls.Load(savedProps)
if tc.loadErr != "" {
So(err, ShouldErrLike, tc.loadErr)
return
}
if tc.want == nil {
return
}
if gotT, ok := got.(*T); ok {
// Round tripping a time.Time can result in a different time.Location: Local instead of UTC.
// We therefore test equality explicitly, instead of relying on reflect.DeepEqual.
So(gotT.T.Equal(tc.want.(*T).T), ShouldBeTrue)
} else {
So(got, ShouldResemble, tc.want)
}
})
}
})
}
func TestMeta(t *testing.T) {
t.Parallel()
Convey("Test meta fields", t, func() {
Convey("Can retrieve from struct", func() {
o := &N0{ID: 100}
mgs := getMGS(o)
val, ok := mgs.GetMeta("id")
So(ok, ShouldBeTrue)
So(val, ShouldEqual, 100)
val, ok = mgs.GetMeta("kind")
So(ok, ShouldBeTrue)
So(val, ShouldEqual, "whatnow")
So(GetMetaDefault(mgs, "kind", "zappo"), ShouldEqual, "whatnow")
So(GetMetaDefault(mgs, "id", "stringID"), ShouldEqual, "stringID")
So(GetMetaDefault(mgs, "id", 6), ShouldEqual, 100)
})
Convey("Getting something not there is an error", func() {
o := &N0{ID: 100}
mgs := getMGS(o)
_, ok := mgs.GetMeta("wat")
So(ok, ShouldBeFalse)
})
Convey("Default works for missing fields", func() {
o := &N0{ID: 100}
mgs := getMGS(o)
So(GetMetaDefault(mgs, "whozit", 10), ShouldEqual, 10)
})
Convey("getting mgs for bad struct is an error", func() {
So(func() { getMGS(&Recursive{}) }, ShouldPanicLike,
`field "R" is recursively defined`)
})
Convey("can assign values to exported meta fields", func() {
o := &N0{ID: 100}
mgs := getMGS(o)
So(mgs.SetMeta("id", int64(200)), ShouldBeTrue)
So(o.ID, ShouldEqual, 200)
})
Convey("assigning to unsassiagnable fields returns !ok", func() {
o := &N0{ID: 100}
mgs := getMGS(o)
So(mgs.SetMeta("kind", "hi"), ShouldBeFalse)
So(mgs.SetMeta("noob", "hi"), ShouldBeFalse)
})
Convey("unsigned int meta fields work", func() {
o := &N3{}
mgs := getMGS(o)
v, ok := mgs.GetMeta("id")
So(v, ShouldEqual, int64(200))
So(ok, ShouldBeTrue)
So(mgs.SetMeta("id", 20), ShouldBeTrue)
So(o.ID, ShouldEqual, 20)
So(mgs.SetMeta("id", math.MaxInt64), ShouldBeFalse)
So(o.ID, ShouldEqual, 20)
So(mgs.SetMeta("id", math.MaxUint32), ShouldBeTrue)
So(o.ID, ShouldEqual, math.MaxUint32)
})
})
Convey("StructPLS Miscellaneous", t, func() {
Convey("a simple struct has a default $kind", func() {
So(GetPLS(&Simple{}).GetAllMeta(), ShouldResemble, PropertyMap{
"$kind": mpNI("Simple"),
})
})
Convey("multiple overlapping fields is an error", func() {
o := &BadMeta{}
So(func() { GetPLS(o) }, ShouldPanicLike, "multiple times")
})
Convey("empty property names are invalid", func() {
So(validPropertyName(""), ShouldBeFalse)
})
Convey("attempting to get a PLS for a non *struct is an error", func() {
s := []string{}
So(func() { GetPLS(&s) }, ShouldPanicLike,
"cannot GetPLS(*[]string): not a pointer-to-struct")
})
Convey("attempting to get a PLS for a nil pointer-to-struct is an error", func() {
var s *Simple
So(func() { GetPLS(s) }, ShouldPanicLike,
"cannot GetPLS(*datastore.Simple): pointer is nil")
})
Convey("convertible meta default types", func() {
type OKDefaults struct {
When string `gae:"$when,tomorrow"`
Amount int64 `gae:"$amt,100"`
DoIt Toggle `gae:"$doit,on"`
}
okd := &OKDefaults{}
mgs := getMGS(okd)
v, ok := mgs.GetMeta("when")
So(ok, ShouldBeTrue)
So(v, ShouldEqual, "tomorrow")
v, ok = mgs.GetMeta("amt")
So(ok, ShouldBeTrue)
So(v, ShouldEqual, int64(100))
So(okd.DoIt, ShouldEqual, Auto)
v, ok = mgs.GetMeta("doit")
So(ok, ShouldBeTrue)
So(v, ShouldBeTrue)
So(mgs.SetMeta("doit", false), ShouldBeTrue)
v, ok = mgs.GetMeta("doit")
So(ok, ShouldBeTrue)
So(v, ShouldBeFalse)
So(okd.DoIt, ShouldEqual, Off)
So(mgs.SetMeta("doit", true), ShouldBeTrue)
v, ok = mgs.GetMeta("doit")
So(ok, ShouldBeTrue)
So(v, ShouldBeTrue)
So(okd.DoIt, ShouldEqual, On)
Convey("Toggle fields REQUIRE a default", func() {
type BadToggle struct {
Bad Toggle `gae:"$wut"`
}
So(func() { GetPLS(&BadToggle{}) }, ShouldPanicLike, "bad/missing default")
})
})
Convey("meta fields can be saved", func() {
type OKDefaults struct {
When string `gae:"$when,tomorrow"`
Amount int64 `gae:"$amt,100"`
}
pls := GetPLS(&OKDefaults{})
pm, err := pls.Save(true)
So(err, ShouldBeNil)
So(pm, ShouldResemble, PropertyMap{
"$when": mpNI("tomorrow"),
"$amt": mpNI(100),
"$kind": mpNI("OKDefaults"),
})
v, ok := pm.GetMeta("when")
So(ok, ShouldBeTrue)
So(v, ShouldEqual, "tomorrow")
v, ok = pm.GetMeta("amt")
So(ok, ShouldBeTrue)
So(v, ShouldEqual, int64(100))
})
Convey("default are optional", func() {
type OverrideDefault struct {
Val int64 `gae:"$val"`
}
o := &OverrideDefault{}
mgs := getMGS(o)
v, ok := mgs.GetMeta("val")
So(ok, ShouldBeTrue)
So(v, ShouldEqual, int64(0))
})
Convey("overridable defaults", func() {
type OverrideDefault struct {
Val int64 `gae:"$val,100"`
}
o := &OverrideDefault{}
mgs := getMGS(o)
v, ok := mgs.GetMeta("val")
So(ok, ShouldBeTrue)
So(v, ShouldEqual, int64(100))
o.Val = 10
v, ok = mgs.GetMeta("val")
So(ok, ShouldBeTrue)
So(v, ShouldEqual, int64(10))
})
Convey("underflow", func() {
type UnderflowMeta struct {
ID int16 `gae:"$id"`
}
um := &UnderflowMeta{}
mgs := getMGS(um)
So(mgs.SetMeta("id", -20), ShouldBeTrue)
So(mgs.SetMeta("id", math.MinInt64), ShouldBeFalse)
})
Convey("negative default", func() {
type UnderflowMeta struct {
ID int16 `gae:"$id,-30"`
}
um := &UnderflowMeta{}
mgs := getMGS(um)
val, ok := mgs.GetMeta("id")
So(ok, ShouldBeTrue)
So(val, ShouldEqual, -30)
})
Convey("Derived metadata fields", func() {
type DerivedString string
type DerivedInt int16
type DerivedStruct struct {
ID DerivedString `gae:"$id"`
Foo DerivedInt `gae:"$foo"`
}
o := &DerivedStruct{"hello", 10}
mgs := getMGS(o)
v, ok := mgs.GetMeta("id")
So(ok, ShouldBeTrue)
So(v, ShouldEqual, "hello")
v, ok = mgs.GetMeta("foo")
So(ok, ShouldBeTrue)
So(v, ShouldEqual, int64(10))
So(mgs.SetMeta("id", "nerds"), ShouldBeTrue)
So(mgs.SetMeta("foo", 20), ShouldBeTrue)
So(o.ID, ShouldEqual, DerivedString("nerds"))
So(o.Foo, ShouldEqual, DerivedInt(20))
})
Convey("Bad default meta type", func() {
type BadDefault struct {
Val time.Time `gae:"$meta,tomorrow"`
}
So(func() { GetPLS(&BadDefault{}) }, ShouldPanicLike, "bad type")
})
Convey("MetaGetterSetter implementation (IDParser)", func() {
idp := &IDParser{parent: "moo", id: 100}
mgs := getMGS(idp)
So(GetMetaDefault(mgs, "id", ""), ShouldEqual, "moo|100")
So(GetMetaDefault(mgs, "kind", ""), ShouldEqual, "CoolKind")
So(mgs.SetMeta("kind", "Something"), ShouldBeFalse)
So(mgs.SetMeta("id", "happy|27"), ShouldBeTrue)
So(idp.parent, ShouldEqual, "happy")
So(idp.id, ShouldEqual, 27)
So(mgs.GetAllMeta(), ShouldResemble, PropertyMap{
"$id": mpNI("happy|27"),
"$kind": mpNI("CoolKind"),
})
})
Convey("MetaGetterSetter implementation (KindOverride)", func() {
ko := &KindOverride{ID: 20}
mgs := getMGS(ko)
So(GetMetaDefault(mgs, "kind", ""), ShouldEqual, "KindOverride")
ko.customKind = "something"
So(GetMetaDefault(mgs, "kind", ""), ShouldEqual, "something")
So(mgs.SetMeta("kind", "Nerp"), ShouldBeTrue)
So(ko.customKind, ShouldEqual, "Nerp")
So(mgs.SetMeta("kind", "KindOverride"), ShouldBeTrue)
So(ko.customKind, ShouldEqual, "")
So(mgs.GetAllMeta(), ShouldResemble, PropertyMap{
"$id": mpNI(20),
"$kind": mpNI("KindOverride"),
})
ko.customKind = "wut"
So(mgs.GetAllMeta(), ShouldResemble, PropertyMap{
"$id": mpNI(20),
"$kind": mpNI("wut"),
})
props, err := GetPLS(ko).Save(true)
So(err, ShouldBeNil)
So(props, ShouldResemble, PropertyMap{
"$id": mpNI(20),
"$kind": mpNI("wut"),
})
})
Convey("Embeddable Metadata structs", func() {
ide := &IDEmbedder{EmbeddedID{"hello", 10}}
pls := GetPLS(ide)
val, ok := pls.GetMeta("id")
So(ok, ShouldBeTrue)
So(val, ShouldEqual, "hello|10")
So(pls.SetMeta("id", "sup|1337"), ShouldBeTrue)
So(ide.EmbeddedID, ShouldResemble, EmbeddedID{"sup", 1337})
So(pls.GetAllMeta(), ShouldResemble, PropertyMap{
"$id": mpNI("sup|1337"),
"$kind": mpNI("IDEmbedder"),
})
})
})
}