1537 lines
30 KiB
Go
1537 lines
30 KiB
Go
// 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 (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/appengine"
|
|
"google.golang.org/appengine/internal/aetesting"
|
|
pb "google.golang.org/appengine/internal/datastore"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
func newKey(stringID string, parent *Key) *Key {
|
|
return &Key{
|
|
kind: "kind",
|
|
stringID: stringID,
|
|
intID: 0,
|
|
parent: parent,
|
|
appID: testAppID,
|
|
}
|
|
}
|
|
|
|
var (
|
|
testKey0 = newKey("name0", nil)
|
|
testKey1a = newKey("name1", nil)
|
|
testKey1b = newKey("name1", nil)
|
|
testKey2a = newKey("name2", testKey0)
|
|
testKey2b = newKey("name2", testKey0)
|
|
testGeoPt0 = appengine.GeoPoint{Lat: 1.2, Lng: 3.4}
|
|
testGeoPt1 = appengine.GeoPoint{Lat: 5, Lng: 10}
|
|
testBadGeoPt = appengine.GeoPoint{Lat: 1000, Lng: 34}
|
|
)
|
|
|
|
type B0 struct {
|
|
B []byte
|
|
}
|
|
|
|
type B1 struct {
|
|
B []int8
|
|
}
|
|
|
|
type B2 struct {
|
|
B myBlob
|
|
}
|
|
|
|
type B3 struct {
|
|
B []myByte
|
|
}
|
|
|
|
type B4 struct {
|
|
B [][]byte
|
|
}
|
|
|
|
type B5 struct {
|
|
B ByteString
|
|
}
|
|
|
|
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 appengine.GeoPoint
|
|
}
|
|
|
|
type G1 struct {
|
|
G []appengine.GeoPoint
|
|
}
|
|
|
|
type K0 struct {
|
|
K *Key
|
|
}
|
|
|
|
type K1 struct {
|
|
K []*Key
|
|
}
|
|
|
|
type N0 struct {
|
|
X0
|
|
Nonymous X0
|
|
Ignore string `datastore:"-"`
|
|
Other string
|
|
}
|
|
|
|
type N1 struct {
|
|
X0
|
|
Nonymous []X0
|
|
Ignore string `datastore:"-"`
|
|
Other string
|
|
}
|
|
|
|
type N2 struct {
|
|
N1 `datastore:"red"`
|
|
Green N1 `datastore:"green"`
|
|
Blue N1
|
|
White N1 `datastore:"-"`
|
|
}
|
|
|
|
type O0 struct {
|
|
I int64
|
|
}
|
|
|
|
type O1 struct {
|
|
I int32
|
|
}
|
|
|
|
type U0 struct {
|
|
U uint
|
|
}
|
|
|
|
type U1 struct {
|
|
U string
|
|
}
|
|
|
|
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 Tagged 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"`
|
|
|
|
Y0 `datastore:"-"`
|
|
Z chan int `datastore:"-,"`
|
|
}
|
|
|
|
type InvalidTagged1 struct {
|
|
I int `datastore:"\t"`
|
|
}
|
|
|
|
type InvalidTagged2 struct {
|
|
I int
|
|
J int `datastore:"I"`
|
|
}
|
|
|
|
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 `datastore:"I.W"`
|
|
IDotX []string `datastore:"I.X"`
|
|
JDotY float64 `datastore:"J.Y"`
|
|
Z bool
|
|
}
|
|
|
|
type Dotted struct {
|
|
A DottedA `datastore:"A0.A1.A2"`
|
|
}
|
|
|
|
type DottedA struct {
|
|
B DottedB `datastore:"B3"`
|
|
}
|
|
|
|
type DottedB struct {
|
|
C int `datastore:"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 Doubler struct {
|
|
S string
|
|
I int64
|
|
B bool
|
|
}
|
|
|
|
func (d *Doubler) Load(props []Property) error {
|
|
return LoadStruct(d, props)
|
|
}
|
|
|
|
func (d *Doubler) Save() ([]Property, error) {
|
|
// Save the default Property slice to an in-memory buffer (a PropertyList).
|
|
props, err := SaveStruct(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var list PropertyList
|
|
if err := list.Load(props); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Edit that PropertyList, and send it on.
|
|
for i := range list {
|
|
switch v := list[i].Value.(type) {
|
|
case string:
|
|
// + means string concatenation.
|
|
list[i].Value = v + v
|
|
case int64:
|
|
// + means integer addition.
|
|
list[i].Value = v + v
|
|
}
|
|
}
|
|
return list.Save()
|
|
}
|
|
|
|
var _ PropertyLoadSaver = (*Doubler)(nil)
|
|
|
|
type Deriver struct {
|
|
S, Derived, Ignored string
|
|
}
|
|
|
|
func (e *Deriver) Load(props []Property) error {
|
|
for _, p := range props {
|
|
if p.Name != "S" {
|
|
continue
|
|
}
|
|
e.S = p.Value.(string)
|
|
e.Derived = "derived+" + e.S
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Deriver) Save() ([]Property, error) {
|
|
return []Property{
|
|
{
|
|
Name: "S",
|
|
Value: e.S,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
var _ PropertyLoadSaver = (*Deriver)(nil)
|
|
|
|
type BadMultiPropEntity struct{}
|
|
|
|
func (e *BadMultiPropEntity) Load(props []Property) error {
|
|
return errors.New("unimplemented")
|
|
}
|
|
|
|
func (e *BadMultiPropEntity) Save() ([]Property, error) {
|
|
// Write multiple properties with the same name "I", but Multiple is false.
|
|
var props []Property
|
|
for i := 0; i < 3; i++ {
|
|
props = append(props, Property{
|
|
Name: "I",
|
|
Value: int64(i),
|
|
})
|
|
}
|
|
return props, nil
|
|
}
|
|
|
|
var _ PropertyLoadSaver = (*BadMultiPropEntity)(nil)
|
|
|
|
type BK struct {
|
|
Key appengine.BlobKey
|
|
}
|
|
|
|
type testCase struct {
|
|
desc string
|
|
src interface{}
|
|
want interface{}
|
|
putErr string
|
|
getErr string
|
|
}
|
|
|
|
var testCases = []testCase{
|
|
{
|
|
"chan save fails",
|
|
&C0{I: -1},
|
|
&E{},
|
|
"unsupported struct field",
|
|
"",
|
|
},
|
|
{
|
|
"*chan save fails",
|
|
&C1{I: -1},
|
|
&E{},
|
|
"unsupported struct field",
|
|
"",
|
|
},
|
|
{
|
|
"[]chan save fails",
|
|
&C2{I: -1, C: make([]chan int, 8)},
|
|
&E{},
|
|
"unsupported struct field",
|
|
"",
|
|
},
|
|
{
|
|
"chan load fails",
|
|
&C3{C: "not a chan"},
|
|
&C0{},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"*chan load fails",
|
|
&C3{C: "not a *chan"},
|
|
&C1{},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"[]chan load fails",
|
|
&C3{C: "not a []chan"},
|
|
&C2{},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"empty struct",
|
|
&E{},
|
|
&E{},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"geopoint",
|
|
&G0{G: testGeoPt0},
|
|
&G0{G: testGeoPt0},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"geopoint invalid",
|
|
&G0{G: testBadGeoPt},
|
|
&G0{},
|
|
"invalid GeoPoint value",
|
|
"",
|
|
},
|
|
{
|
|
"geopoint as props",
|
|
&G0{G: testGeoPt0},
|
|
&PropertyList{
|
|
Property{Name: "G", Value: testGeoPt0, NoIndex: false, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"geopoint slice",
|
|
&G1{G: []appengine.GeoPoint{testGeoPt0, testGeoPt1}},
|
|
&G1{G: []appengine.GeoPoint{testGeoPt0, testGeoPt1}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"key",
|
|
&K0{K: testKey1a},
|
|
&K0{K: testKey1b},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"key with parent",
|
|
&K0{K: testKey2a},
|
|
&K0{K: testKey2b},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"nil key",
|
|
&K0{},
|
|
&K0{},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"all nil keys in slice",
|
|
&K1{[]*Key{nil, nil}},
|
|
&K1{[]*Key{nil, nil}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"some nil keys in slice",
|
|
&K1{[]*Key{testKey1a, nil, testKey2a}},
|
|
&K1{[]*Key{testKey1b, nil, testKey2b}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"overflow",
|
|
&O0{I: 1 << 48},
|
|
&O1{},
|
|
"",
|
|
"overflow",
|
|
},
|
|
{
|
|
"time",
|
|
&T{T: time.Unix(1e9, 0)},
|
|
&T{T: time.Unix(1e9, 0)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"time as props",
|
|
&T{T: time.Unix(1e9, 0)},
|
|
&PropertyList{
|
|
Property{Name: "T", Value: time.Unix(1e9, 0), NoIndex: false, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"uint save",
|
|
&U0{U: 1},
|
|
&U0{},
|
|
"unsupported struct field",
|
|
"",
|
|
},
|
|
{
|
|
"uint load",
|
|
&U1{U: "not a uint"},
|
|
&U0{},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"zero",
|
|
&X0{},
|
|
&X0{},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"basic",
|
|
&X0{S: "one", I: 2, i: 3},
|
|
&X0{S: "one", I: 2},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save string/int load myString/int32",
|
|
&X0{S: "one", I: 2, i: 3},
|
|
&X1{S: "one", I: 2},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"missing fields",
|
|
&X0{S: "one", I: 2, i: 3},
|
|
&X2{},
|
|
"",
|
|
"no such struct field",
|
|
},
|
|
{
|
|
"save string load bool",
|
|
&X0{S: "one", I: 2, i: 3},
|
|
&X3{I: 2},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"basic slice",
|
|
&Y0{B: true, F: []float64{7, 8, 9}},
|
|
&Y0{B: true, F: []float64{7, 8, 9}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save []float64 load float64",
|
|
&Y0{B: true, F: []float64{7, 8, 9}},
|
|
&Y1{B: true},
|
|
"",
|
|
"requires a slice",
|
|
},
|
|
{
|
|
"save []float64 load []int64",
|
|
&Y0{B: true, F: []float64{7, 8, 9}},
|
|
&Y2{B: true},
|
|
"",
|
|
"type mismatch",
|
|
},
|
|
{
|
|
"single slice is too long",
|
|
&Y0{F: make([]float64, maxIndexedProperties+1)},
|
|
&Y0{},
|
|
"too many indexed properties",
|
|
"",
|
|
},
|
|
{
|
|
"two slices are too long",
|
|
&Y0{F: make([]float64, maxIndexedProperties), G: make([]float64, maxIndexedProperties)},
|
|
&Y0{},
|
|
"too many indexed properties",
|
|
"",
|
|
},
|
|
{
|
|
"one slice and one scalar are too long",
|
|
&Y0{F: make([]float64, maxIndexedProperties), B: true},
|
|
&Y0{},
|
|
"too many indexed properties",
|
|
"",
|
|
},
|
|
{
|
|
"long blob",
|
|
&B0{B: makeUint8Slice(maxIndexedProperties + 1)},
|
|
&B0{B: makeUint8Slice(maxIndexedProperties + 1)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"long []int8 is too long",
|
|
&B1{B: makeInt8Slice(maxIndexedProperties + 1)},
|
|
&B1{},
|
|
"too many indexed properties",
|
|
"",
|
|
},
|
|
{
|
|
"short []int8",
|
|
&B1{B: makeInt8Slice(3)},
|
|
&B1{B: makeInt8Slice(3)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"long myBlob",
|
|
&B2{B: makeUint8Slice(maxIndexedProperties + 1)},
|
|
&B2{B: makeUint8Slice(maxIndexedProperties + 1)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"short myBlob",
|
|
&B2{B: makeUint8Slice(3)},
|
|
&B2{B: makeUint8Slice(3)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"long []myByte",
|
|
&B3{B: makeMyByteSlice(maxIndexedProperties + 1)},
|
|
&B3{B: makeMyByteSlice(maxIndexedProperties + 1)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"short []myByte",
|
|
&B3{B: makeMyByteSlice(3)},
|
|
&B3{B: makeMyByteSlice(3)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"slice of blobs",
|
|
&B4{B: [][]byte{
|
|
makeUint8Slice(3),
|
|
makeUint8Slice(4),
|
|
makeUint8Slice(5),
|
|
}},
|
|
&B4{B: [][]byte{
|
|
makeUint8Slice(3),
|
|
makeUint8Slice(4),
|
|
makeUint8Slice(5),
|
|
}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"short ByteString",
|
|
&B5{B: ByteString(makeUint8Slice(3))},
|
|
&B5{B: ByteString(makeUint8Slice(3))},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"short ByteString as props",
|
|
&B5{B: ByteString(makeUint8Slice(3))},
|
|
&PropertyList{
|
|
Property{Name: "B", Value: ByteString(makeUint8Slice(3)), NoIndex: false, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"short ByteString into string",
|
|
&B5{B: ByteString("legacy")},
|
|
&struct{ B string }{"legacy"},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"[]byte must be noindex",
|
|
&PropertyList{
|
|
Property{Name: "B", Value: makeUint8Slice(3), NoIndex: false},
|
|
},
|
|
nil,
|
|
"cannot index a []byte valued Property",
|
|
"",
|
|
},
|
|
{
|
|
"save tagged load props",
|
|
&Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, I: 6, J: 7},
|
|
&PropertyList{
|
|
// 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.
|
|
Property{Name: "b", Value: int64(21), NoIndex: false, Multiple: true},
|
|
Property{Name: "b", Value: int64(22), NoIndex: false, Multiple: true},
|
|
Property{Name: "b", Value: int64(23), NoIndex: false, Multiple: true},
|
|
Property{Name: "D", Value: int64(4), NoIndex: false, Multiple: false},
|
|
Property{Name: "E", Value: int64(5), NoIndex: false, Multiple: false},
|
|
Property{Name: "a", Value: int64(1), NoIndex: true, Multiple: false},
|
|
Property{Name: "C", Value: int64(3), NoIndex: true, Multiple: false},
|
|
Property{Name: "J", Value: int64(7), NoIndex: true, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save tagged load tagged",
|
|
&Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, I: 6, J: 7},
|
|
&Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, J: 7},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save props load tagged",
|
|
&PropertyList{
|
|
Property{Name: "A", Value: int64(11), NoIndex: true, Multiple: false},
|
|
Property{Name: "a", Value: int64(12), NoIndex: true, Multiple: false},
|
|
},
|
|
&Tagged{A: 12},
|
|
"",
|
|
`cannot load field "A"`,
|
|
},
|
|
{
|
|
"invalid tagged1",
|
|
&InvalidTagged1{I: 1},
|
|
&InvalidTagged1{},
|
|
"struct tag has invalid property name",
|
|
"",
|
|
},
|
|
{
|
|
"invalid tagged2",
|
|
&InvalidTagged2{I: 1, J: 2},
|
|
&InvalidTagged2{},
|
|
"struct tag has repeated property name",
|
|
"",
|
|
},
|
|
{
|
|
"doubler",
|
|
&Doubler{S: "s", I: 1, B: true},
|
|
&Doubler{S: "ss", I: 2, B: true},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save struct load props",
|
|
&X0{S: "s", I: 1},
|
|
&PropertyList{
|
|
Property{Name: "S", Value: "s", NoIndex: false, Multiple: false},
|
|
Property{Name: "I", Value: int64(1), NoIndex: false, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save props load struct",
|
|
&PropertyList{
|
|
Property{Name: "S", Value: "s", NoIndex: false, Multiple: false},
|
|
Property{Name: "I", Value: int64(1), NoIndex: false, Multiple: false},
|
|
},
|
|
&X0{S: "s", I: 1},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"nil-value props",
|
|
&PropertyList{
|
|
Property{Name: "I", Value: nil, NoIndex: false, Multiple: false},
|
|
Property{Name: "B", Value: nil, NoIndex: false, Multiple: false},
|
|
Property{Name: "S", Value: nil, NoIndex: false, Multiple: false},
|
|
Property{Name: "F", Value: nil, NoIndex: false, Multiple: false},
|
|
Property{Name: "K", Value: nil, NoIndex: false, Multiple: false},
|
|
Property{Name: "T", Value: nil, NoIndex: false, Multiple: false},
|
|
Property{Name: "J", Value: nil, NoIndex: false, Multiple: true},
|
|
Property{Name: "J", Value: int64(7), NoIndex: false, Multiple: true},
|
|
Property{Name: "J", Value: nil, NoIndex: false, Multiple: true},
|
|
},
|
|
&struct {
|
|
I int64
|
|
B bool
|
|
S string
|
|
F float64
|
|
K *Key
|
|
T time.Time
|
|
J []int64
|
|
}{
|
|
J: []int64{0, 7, 0},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save outer load props",
|
|
&Outer{
|
|
A: 1,
|
|
I: []Inner1{
|
|
{10, "ten"},
|
|
{20, "twenty"},
|
|
{30, "thirty"},
|
|
},
|
|
J: Inner2{
|
|
Y: 3.14,
|
|
},
|
|
Inner3: Inner3{
|
|
Z: true,
|
|
},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "A", Value: int64(1), NoIndex: false, Multiple: false},
|
|
Property{Name: "I.W", Value: int64(10), NoIndex: false, Multiple: true},
|
|
Property{Name: "I.X", Value: "ten", NoIndex: false, Multiple: true},
|
|
Property{Name: "I.W", Value: int64(20), NoIndex: false, Multiple: true},
|
|
Property{Name: "I.X", Value: "twenty", NoIndex: false, Multiple: true},
|
|
Property{Name: "I.W", Value: int64(30), NoIndex: false, Multiple: true},
|
|
Property{Name: "I.X", Value: "thirty", NoIndex: false, Multiple: true},
|
|
Property{Name: "J.Y", Value: float64(3.14), NoIndex: false, Multiple: false},
|
|
Property{Name: "Z", Value: true, NoIndex: false, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save props load outer-equivalent",
|
|
&PropertyList{
|
|
Property{Name: "A", Value: int64(1), NoIndex: false, Multiple: false},
|
|
Property{Name: "I.W", Value: int64(10), NoIndex: false, Multiple: true},
|
|
Property{Name: "I.X", Value: "ten", NoIndex: false, Multiple: true},
|
|
Property{Name: "I.W", Value: int64(20), NoIndex: false, Multiple: true},
|
|
Property{Name: "I.X", Value: "twenty", NoIndex: false, Multiple: true},
|
|
Property{Name: "I.W", Value: int64(30), NoIndex: false, Multiple: true},
|
|
Property{Name: "I.X", Value: "thirty", NoIndex: false, Multiple: true},
|
|
Property{Name: "J.Y", Value: float64(3.14), NoIndex: false, Multiple: false},
|
|
Property{Name: "Z", Value: true, NoIndex: false, Multiple: false},
|
|
},
|
|
&OuterEquivalent{
|
|
A: 1,
|
|
IDotW: []int32{10, 20, 30},
|
|
IDotX: []string{"ten", "twenty", "thirty"},
|
|
JDotY: 3.14,
|
|
Z: true,
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save outer-equivalent load outer",
|
|
&OuterEquivalent{
|
|
A: 1,
|
|
IDotW: []int32{10, 20, 30},
|
|
IDotX: []string{"ten", "twenty", "thirty"},
|
|
JDotY: 3.14,
|
|
Z: true,
|
|
},
|
|
&Outer{
|
|
A: 1,
|
|
I: []Inner1{
|
|
{10, "ten"},
|
|
{20, "twenty"},
|
|
{30, "thirty"},
|
|
},
|
|
J: Inner2{
|
|
Y: 3.14,
|
|
},
|
|
Inner3: Inner3{
|
|
Z: true,
|
|
},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"dotted names save",
|
|
&Dotted{A: DottedA{B: DottedB{C: 88}}},
|
|
&PropertyList{
|
|
Property{Name: "A0.A1.A2.B3.C4.C5", Value: int64(88), NoIndex: false, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"dotted names load",
|
|
&PropertyList{
|
|
Property{Name: "A0.A1.A2.B3.C4.C5", Value: int64(99), NoIndex: false, Multiple: false},
|
|
},
|
|
&Dotted{A: DottedA{B: DottedB{C: 99}}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save struct load deriver",
|
|
&X0{S: "s", I: 1},
|
|
&Deriver{S: "s", Derived: "derived+s"},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save deriver load struct",
|
|
&Deriver{S: "s", Derived: "derived+s", Ignored: "ignored"},
|
|
&X0{S: "s"},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"bad multi-prop entity",
|
|
&BadMultiPropEntity{},
|
|
&BadMultiPropEntity{},
|
|
"Multiple is false",
|
|
"",
|
|
},
|
|
// Regression: CL 25062824 broke handling of appengine.BlobKey fields.
|
|
{
|
|
"appengine.BlobKey",
|
|
&BK{Key: "blah"},
|
|
&BK{Key: "blah"},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"zero time.Time",
|
|
&T{T: time.Time{}},
|
|
&T{T: time.Time{}},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"time.Time near Unix zero time",
|
|
&T{T: time.Unix(0, 4e3)},
|
|
&T{T: time.Unix(0, 4e3)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"time.Time, far in the future",
|
|
&T{T: time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
&T{T: time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"time.Time, very far in the past",
|
|
&T{T: time.Date(-300000, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
&T{},
|
|
"time value out of range",
|
|
"",
|
|
},
|
|
{
|
|
"time.Time, very far in the future",
|
|
&T{T: time.Date(294248, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
&T{},
|
|
"time value out of range",
|
|
"",
|
|
},
|
|
{
|
|
"structs",
|
|
&N0{
|
|
X0: X0{S: "one", I: 2, i: 3},
|
|
Nonymous: X0{S: "four", I: 5, i: 6},
|
|
Ignore: "ignore",
|
|
Other: "other",
|
|
},
|
|
&N0{
|
|
X0: X0{S: "one", I: 2},
|
|
Nonymous: X0{S: "four", I: 5},
|
|
Other: "other",
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"slice of structs",
|
|
&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",
|
|
},
|
|
&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",
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"structs with slices of structs",
|
|
&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"},
|
|
},
|
|
},
|
|
},
|
|
&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"},
|
|
},
|
|
},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save structs load props",
|
|
&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"},
|
|
},
|
|
},
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "red.S", Value: "rouge", NoIndex: false, Multiple: false},
|
|
Property{Name: "red.I", Value: int64(0), NoIndex: false, Multiple: false},
|
|
Property{Name: "red.Nonymous.S", Value: "rosso0", NoIndex: false, Multiple: true},
|
|
Property{Name: "red.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
|
|
Property{Name: "red.Nonymous.S", Value: "rosso1", NoIndex: false, Multiple: true},
|
|
Property{Name: "red.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
|
|
Property{Name: "red.Other", Value: "", NoIndex: false, Multiple: false},
|
|
Property{Name: "green.S", Value: "vert", NoIndex: false, Multiple: false},
|
|
Property{Name: "green.I", Value: int64(0), NoIndex: false, Multiple: false},
|
|
Property{Name: "green.Nonymous.S", Value: "verde0", NoIndex: false, Multiple: true},
|
|
Property{Name: "green.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
|
|
Property{Name: "green.Nonymous.S", Value: "verde1", NoIndex: false, Multiple: true},
|
|
Property{Name: "green.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
|
|
Property{Name: "green.Nonymous.S", Value: "verde2", NoIndex: false, Multiple: true},
|
|
Property{Name: "green.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
|
|
Property{Name: "green.Other", Value: "", NoIndex: false, Multiple: false},
|
|
Property{Name: "Blue.S", Value: "bleu", NoIndex: false, Multiple: false},
|
|
Property{Name: "Blue.I", Value: int64(0), NoIndex: false, Multiple: false},
|
|
Property{Name: "Blue.Nonymous.S", Value: "blu0", NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.S", Value: "blu1", NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.S", Value: "blu2", NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.S", Value: "blu3", NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Other", Value: "", NoIndex: false, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save props load structs with ragged fields",
|
|
&PropertyList{
|
|
Property{Name: "red.S", Value: "rot", NoIndex: false, Multiple: false},
|
|
Property{Name: "green.Nonymous.I", Value: int64(10), NoIndex: false, Multiple: true},
|
|
Property{Name: "green.Nonymous.I", Value: int64(11), NoIndex: false, Multiple: true},
|
|
Property{Name: "green.Nonymous.I", Value: int64(12), NoIndex: false, Multiple: true},
|
|
Property{Name: "green.Nonymous.I", Value: int64(13), NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.S", Value: "blau0", NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.I", Value: int64(20), NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.S", Value: "blau1", NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.I", Value: int64(21), NoIndex: false, Multiple: true},
|
|
Property{Name: "Blue.Nonymous.S", Value: "blau2", NoIndex: false, Multiple: true},
|
|
},
|
|
&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"},
|
|
},
|
|
},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"save structs with noindex tags",
|
|
&struct {
|
|
A struct {
|
|
X string `datastore:",noindex"`
|
|
Y string
|
|
} `datastore:",noindex"`
|
|
B struct {
|
|
X string `datastore:",noindex"`
|
|
Y string
|
|
}
|
|
}{},
|
|
&PropertyList{
|
|
Property{Name: "B.Y", Value: "", NoIndex: false, Multiple: false},
|
|
Property{Name: "A.X", Value: "", NoIndex: true, Multiple: false},
|
|
Property{Name: "A.Y", Value: "", NoIndex: true, Multiple: false},
|
|
Property{Name: "B.X", Value: "", NoIndex: true, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"embedded struct with name override",
|
|
&struct {
|
|
Inner1 `datastore:"foo"`
|
|
}{},
|
|
&PropertyList{
|
|
Property{Name: "foo.W", Value: int64(0), NoIndex: false, Multiple: false},
|
|
Property{Name: "foo.X", Value: "", NoIndex: false, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"slice of slices",
|
|
&SliceOfSlices{},
|
|
nil,
|
|
"flattening nested structs leads to a slice of slices",
|
|
"",
|
|
},
|
|
{
|
|
"recursive struct",
|
|
&Recursive{},
|
|
nil,
|
|
"recursive struct",
|
|
"",
|
|
},
|
|
{
|
|
"mutually recursive struct",
|
|
&MutuallyRecursive0{},
|
|
nil,
|
|
"recursive struct",
|
|
"",
|
|
},
|
|
{
|
|
"non-exported struct fields",
|
|
&struct {
|
|
i, J int64
|
|
}{i: 1, J: 2},
|
|
&PropertyList{
|
|
Property{Name: "J", Value: int64(2), NoIndex: false, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"json.RawMessage",
|
|
&struct {
|
|
J json.RawMessage
|
|
}{
|
|
J: json.RawMessage("rawr"),
|
|
},
|
|
&PropertyList{
|
|
Property{Name: "J", Value: []byte("rawr"), NoIndex: true, Multiple: false},
|
|
},
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"json.RawMessage to myBlob",
|
|
&struct {
|
|
B json.RawMessage
|
|
}{
|
|
B: json.RawMessage("rawr"),
|
|
},
|
|
&B2{B: myBlob("rawr")},
|
|
"",
|
|
"",
|
|
},
|
|
}
|
|
|
|
// checkErr returns the empty string if either both want and err are zero,
|
|
// or if want is a non-empty substring of err's string representation.
|
|
func checkErr(want string, err error) string {
|
|
if err != nil {
|
|
got := err.Error()
|
|
if want == "" || strings.Index(got, want) == -1 {
|
|
return got
|
|
}
|
|
} else if want != "" {
|
|
return fmt.Sprintf("want error %q", want)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func TestRoundTrip(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
p, err := saveEntity(testAppID, testKey0, tc.src)
|
|
if s := checkErr(tc.putErr, err); s != "" {
|
|
t.Errorf("%s: save: %s", tc.desc, s)
|
|
continue
|
|
}
|
|
if p == nil {
|
|
continue
|
|
}
|
|
var got interface{}
|
|
if _, ok := tc.want.(*PropertyList); ok {
|
|
got = new(PropertyList)
|
|
} else {
|
|
got = reflect.New(reflect.TypeOf(tc.want).Elem()).Interface()
|
|
}
|
|
err = loadEntity(got, p)
|
|
if s := checkErr(tc.getErr, err); s != "" {
|
|
t.Errorf("%s: load: %s", tc.desc, s)
|
|
continue
|
|
}
|
|
equal := false
|
|
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.
|
|
equal = gotT.T.Equal(tc.want.(*T).T)
|
|
} else {
|
|
equal = reflect.DeepEqual(got, tc.want)
|
|
}
|
|
if !equal {
|
|
t.Errorf("%s: compare: got %v want %v", tc.desc, got, tc.want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQueryConstruction(t *testing.T) {
|
|
tests := []struct {
|
|
q, exp *Query
|
|
err string
|
|
}{
|
|
{
|
|
q: NewQuery("Foo"),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Regular filtered query with standard spacing.
|
|
q: NewQuery("Foo").Filter("foo >", 7),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
filter: []filter{
|
|
{
|
|
FieldName: "foo",
|
|
Op: greaterThan,
|
|
Value: 7,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Filtered query with no spacing.
|
|
q: NewQuery("Foo").Filter("foo=", 6),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
filter: []filter{
|
|
{
|
|
FieldName: "foo",
|
|
Op: equal,
|
|
Value: 6,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Filtered query with funky spacing.
|
|
q: NewQuery("Foo").Filter(" foo< ", 8),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
filter: []filter{
|
|
{
|
|
FieldName: "foo",
|
|
Op: lessThan,
|
|
Value: 8,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Filtered query with multicharacter op.
|
|
q: NewQuery("Foo").Filter("foo >=", 9),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
filter: []filter{
|
|
{
|
|
FieldName: "foo",
|
|
Op: greaterEq,
|
|
Value: 9,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Query with ordering.
|
|
q: NewQuery("Foo").Order("bar"),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
order: []order{
|
|
{
|
|
FieldName: "bar",
|
|
Direction: ascending,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Query with reverse ordering, and funky spacing.
|
|
q: NewQuery("Foo").Order(" - bar"),
|
|
exp: &Query{
|
|
kind: "Foo",
|
|
order: []order{
|
|
{
|
|
FieldName: "bar",
|
|
Direction: descending,
|
|
},
|
|
},
|
|
limit: -1,
|
|
},
|
|
},
|
|
{
|
|
// Query with an empty ordering.
|
|
q: NewQuery("Foo").Order(""),
|
|
err: "empty order",
|
|
},
|
|
{
|
|
// Query with a + ordering.
|
|
q: NewQuery("Foo").Order("+bar"),
|
|
err: "invalid order",
|
|
},
|
|
}
|
|
for i, test := range tests {
|
|
if test.q.err != nil {
|
|
got := test.q.err.Error()
|
|
if !strings.Contains(got, test.err) {
|
|
t.Errorf("%d: error mismatch: got %q want something containing %q", i, got, test.err)
|
|
}
|
|
continue
|
|
}
|
|
if !reflect.DeepEqual(test.q, test.exp) {
|
|
t.Errorf("%d: mismatch: got %v want %v", i, test.q, test.exp)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStringMeaning(t *testing.T) {
|
|
var xx [4]interface{}
|
|
xx[0] = &struct {
|
|
X string
|
|
}{"xx0"}
|
|
xx[1] = &struct {
|
|
X string `datastore:",noindex"`
|
|
}{"xx1"}
|
|
xx[2] = &struct {
|
|
X []byte
|
|
}{[]byte("xx2")}
|
|
xx[3] = &struct {
|
|
X []byte `datastore:",noindex"`
|
|
}{[]byte("xx3")}
|
|
|
|
indexed := [4]bool{
|
|
true,
|
|
false,
|
|
false, // A []byte is always no-index.
|
|
false,
|
|
}
|
|
want := [4]pb.Property_Meaning{
|
|
pb.Property_NO_MEANING,
|
|
pb.Property_TEXT,
|
|
pb.Property_BLOB,
|
|
pb.Property_BLOB,
|
|
}
|
|
|
|
for i, x := range xx {
|
|
props, err := SaveStruct(x)
|
|
if err != nil {
|
|
t.Errorf("i=%d: SaveStruct: %v", i, err)
|
|
continue
|
|
}
|
|
e, err := propertiesToProto("appID", testKey0, props)
|
|
if err != nil {
|
|
t.Errorf("i=%d: propertiesToProto: %v", i, err)
|
|
continue
|
|
}
|
|
var p *pb.Property
|
|
switch {
|
|
case indexed[i] && len(e.Property) == 1:
|
|
p = e.Property[0]
|
|
case !indexed[i] && len(e.RawProperty) == 1:
|
|
p = e.RawProperty[0]
|
|
default:
|
|
t.Errorf("i=%d: EntityProto did not have expected property slice", i)
|
|
continue
|
|
}
|
|
if got := p.GetMeaning(); got != want[i] {
|
|
t.Errorf("i=%d: meaning: got %v, want %v", i, got, want[i])
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNamespaceResetting(t *testing.T) {
|
|
namec := make(chan *string, 1)
|
|
c0 := aetesting.FakeSingleContext(t, "datastore_v3", "RunQuery", func(req *pb.Query, res *pb.QueryResult) error {
|
|
namec <- req.NameSpace
|
|
return fmt.Errorf("RPC error")
|
|
})
|
|
|
|
// Check that wrapping c0 in a namespace twice works correctly.
|
|
c1, err := appengine.Namespace(c0, "A")
|
|
if err != nil {
|
|
t.Fatalf("appengine.Namespace: %v", err)
|
|
}
|
|
c2, err := appengine.Namespace(c1, "") // should act as the original context
|
|
if err != nil {
|
|
t.Fatalf("appengine.Namespace: %v", err)
|
|
}
|
|
|
|
q := NewQuery("SomeKind")
|
|
|
|
q.Run(c0)
|
|
if ns := <-namec; ns != nil {
|
|
t.Errorf(`RunQuery with c0: ns = %q, want nil`, *ns)
|
|
}
|
|
|
|
q.Run(c1)
|
|
if ns := <-namec; ns == nil {
|
|
t.Error(`RunQuery with c1: ns = nil, want "A"`)
|
|
} else if *ns != "A" {
|
|
t.Errorf(`RunQuery with c1: ns = %q, want "A"`, *ns)
|
|
}
|
|
|
|
q.Run(c2)
|
|
if ns := <-namec; ns != nil {
|
|
t.Errorf(`RunQuery with c2: ns = %q, want nil`, *ns)
|
|
}
|
|
}
|