diff options
author | Christopher Speller <crspeller@gmail.com> | 2018-01-29 14:17:40 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-01-29 14:17:40 -0800 |
commit | 961c04cae992eadb42d286d2f85f8a675bdc68c8 (patch) | |
tree | 3408f2d06f847e966c53485e2d54c692cdd037c1 /vendor/google.golang.org/appengine/search/search_test.go | |
parent | 8d66523ba7d9a77129844be476732ebfd5272d64 (diff) | |
download | chat-961c04cae992eadb42d286d2f85f8a675bdc68c8.tar.gz chat-961c04cae992eadb42d286d2f85f8a675bdc68c8.tar.bz2 chat-961c04cae992eadb42d286d2f85f8a675bdc68c8.zip |
Upgrading server dependancies (#8154)
Diffstat (limited to 'vendor/google.golang.org/appengine/search/search_test.go')
-rw-r--r-- | vendor/google.golang.org/appengine/search/search_test.go | 1270 |
1 files changed, 1270 insertions, 0 deletions
diff --git a/vendor/google.golang.org/appengine/search/search_test.go b/vendor/google.golang.org/appengine/search/search_test.go new file mode 100644 index 000000000..ef1409c19 --- /dev/null +++ b/vendor/google.golang.org/appengine/search/search_test.go @@ -0,0 +1,1270 @@ +// Copyright 2012 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 search + +import ( + "errors" + "fmt" + "reflect" + "strings" + "testing" + "time" + + "github.com/golang/protobuf/proto" + + "google.golang.org/appengine" + "google.golang.org/appengine/internal/aetesting" + pb "google.golang.org/appengine/internal/search" +) + +type TestDoc struct { + String string + Atom Atom + HTML HTML + Float float64 + Location appengine.GeoPoint + Time time.Time +} + +type FieldListWithMeta struct { + Fields FieldList + Meta *DocumentMetadata +} + +func (f *FieldListWithMeta) Load(fields []Field, meta *DocumentMetadata) error { + f.Meta = meta + return f.Fields.Load(fields, nil) +} + +func (f *FieldListWithMeta) Save() ([]Field, *DocumentMetadata, error) { + fields, _, err := f.Fields.Save() + return fields, f.Meta, err +} + +// Assert that FieldListWithMeta satisfies FieldLoadSaver +var _ FieldLoadSaver = &FieldListWithMeta{} + +var ( + float = 3.14159 + floatOut = "3.14159e+00" + latitude = 37.3894 + longitude = 122.0819 + testGeo = appengine.GeoPoint{latitude, longitude} + testString = "foo<b>bar" + testTime = time.Unix(1337324400, 0) + testTimeOut = "1337324400000" + searchMeta = &DocumentMetadata{ + Rank: 42, + } + searchDoc = TestDoc{ + String: testString, + Atom: Atom(testString), + HTML: HTML(testString), + Float: float, + Location: testGeo, + Time: testTime, + } + searchFields = FieldList{ + Field{Name: "String", Value: testString}, + Field{Name: "Atom", Value: Atom(testString)}, + Field{Name: "HTML", Value: HTML(testString)}, + Field{Name: "Float", Value: float}, + Field{Name: "Location", Value: testGeo}, + Field{Name: "Time", Value: testTime}, + } + // searchFieldsWithLang is a copy of the searchFields with the Language field + // set on text/HTML Fields. + searchFieldsWithLang = FieldList{} + protoFields = []*pb.Field{ + newStringValueField("String", testString, pb.FieldValue_TEXT), + newStringValueField("Atom", testString, pb.FieldValue_ATOM), + newStringValueField("HTML", testString, pb.FieldValue_HTML), + newStringValueField("Float", floatOut, pb.FieldValue_NUMBER), + { + Name: proto.String("Location"), + Value: &pb.FieldValue{ + Geo: &pb.FieldValue_Geo{ + Lat: proto.Float64(latitude), + Lng: proto.Float64(longitude), + }, + Type: pb.FieldValue_GEO.Enum(), + }, + }, + newStringValueField("Time", testTimeOut, pb.FieldValue_DATE), + } +) + +func init() { + for _, f := range searchFields { + if f.Name == "String" || f.Name == "HTML" { + f.Language = "en" + } + searchFieldsWithLang = append(searchFieldsWithLang, f) + } +} + +func newStringValueField(name, value string, valueType pb.FieldValue_ContentType) *pb.Field { + return &pb.Field{ + Name: proto.String(name), + Value: &pb.FieldValue{ + StringValue: proto.String(value), + Type: valueType.Enum(), + }, + } +} + +func newFacet(name, value string, valueType pb.FacetValue_ContentType) *pb.Facet { + return &pb.Facet{ + Name: proto.String(name), + Value: &pb.FacetValue{ + StringValue: proto.String(value), + Type: valueType.Enum(), + }, + } +} + +func TestValidIndexNameOrDocID(t *testing.T) { + testCases := []struct { + s string + want bool + }{ + {"", true}, + {"!", false}, + {"$", true}, + {"!bad", false}, + {"good!", true}, + {"alsoGood", true}, + {"has spaces", false}, + {"is_inva\xffid_UTF-8", false}, + {"is_non-ASCïI", false}, + {"underscores_are_ok", true}, + } + for _, tc := range testCases { + if got := validIndexNameOrDocID(tc.s); got != tc.want { + t.Errorf("%q: got %v, want %v", tc.s, got, tc.want) + } + } +} + +func TestLoadDoc(t *testing.T) { + got, want := TestDoc{}, searchDoc + if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil { + t.Fatalf("loadDoc: %v", err) + } + if got != want { + t.Errorf("loadDoc: got %v, wanted %v", got, want) + } +} + +func TestSaveDoc(t *testing.T) { + got, err := saveDoc(&searchDoc) + if err != nil { + t.Fatalf("saveDoc: %v", err) + } + want := protoFields + if !reflect.DeepEqual(got.Field, want) { + t.Errorf("\ngot %v\nwant %v", got, want) + } +} + +func TestSaveDocUsesDefaultedRankIfNotSpecified(t *testing.T) { + got, err := saveDoc(&searchDoc) + if err != nil { + t.Fatalf("saveDoc: %v", err) + } + orderIdSource := got.GetOrderIdSource() + if orderIdSource != pb.Document_DEFAULTED { + t.Errorf("OrderIdSource: got %v, wanted DEFAULTED", orderIdSource) + } +} + +func TestLoadFieldList(t *testing.T) { + var got FieldList + want := searchFieldsWithLang + if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil { + t.Fatalf("loadDoc: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("\ngot %v\nwant %v", got, want) + } +} + +func TestLangFields(t *testing.T) { + fl := &FieldList{ + {Name: "Foo", Value: "I am English", Language: "en"}, + {Name: "Bar", Value: "私は日本人だ", Language: "jp"}, + } + var got FieldList + doc, err := saveDoc(fl) + if err != nil { + t.Fatalf("saveDoc: %v", err) + } + if err := loadDoc(&got, doc, nil); err != nil { + t.Fatalf("loadDoc: %v", err) + } + if want := fl; !reflect.DeepEqual(&got, want) { + t.Errorf("got %v\nwant %v", got, want) + } +} + +func TestSaveFieldList(t *testing.T) { + got, err := saveDoc(&searchFields) + if err != nil { + t.Fatalf("saveDoc: %v", err) + } + want := protoFields + if !reflect.DeepEqual(got.Field, want) { + t.Errorf("\ngot %v\nwant %v", got, want) + } +} + +func TestLoadFieldAndExprList(t *testing.T) { + var got, want FieldList + for i, f := range searchFieldsWithLang { + f.Derived = (i >= 2) // First 2 elements are "fields", next are "expressions". + want = append(want, f) + } + doc, expr := &pb.Document{Field: protoFields[:2]}, protoFields[2:] + if err := loadDoc(&got, doc, expr); err != nil { + t.Fatalf("loadDoc: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v\nwant %v", got, want) + } +} + +func TestLoadMeta(t *testing.T) { + var got FieldListWithMeta + want := FieldListWithMeta{ + Meta: searchMeta, + Fields: searchFieldsWithLang, + } + doc := &pb.Document{ + Field: protoFields, + OrderId: proto.Int32(42), + OrderIdSource: pb.Document_SUPPLIED.Enum(), + } + if err := loadDoc(&got, doc, nil); err != nil { + t.Fatalf("loadDoc: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("\ngot %v\nwant %v", got, want) + } +} + +func TestSaveMeta(t *testing.T) { + got, err := saveDoc(&FieldListWithMeta{ + Meta: searchMeta, + Fields: searchFields, + }) + if err != nil { + t.Fatalf("saveDoc: %v", err) + } + want := &pb.Document{ + Field: protoFields, + OrderId: proto.Int32(42), + OrderIdSource: pb.Document_SUPPLIED.Enum(), + } + if !proto.Equal(got, want) { + t.Errorf("\ngot %v\nwant %v", got, want) + } +} + +func TestSaveMetaWithDefaultedRank(t *testing.T) { + metaWithoutRank := &DocumentMetadata{ + Rank: 0, + } + got, err := saveDoc(&FieldListWithMeta{ + Meta: metaWithoutRank, + Fields: searchFields, + }) + if err != nil { + t.Fatalf("saveDoc: %v", err) + } + want := &pb.Document{ + Field: protoFields, + OrderId: got.OrderId, + OrderIdSource: pb.Document_DEFAULTED.Enum(), + } + if !proto.Equal(got, want) { + t.Errorf("\ngot %v\nwant %v", got, want) + } +} + +func TestSaveWithoutMetaUsesDefaultedRank(t *testing.T) { + got, err := saveDoc(&FieldListWithMeta{ + Fields: searchFields, + }) + if err != nil { + t.Fatalf("saveDoc: %v", err) + } + want := &pb.Document{ + Field: protoFields, + OrderId: got.OrderId, + OrderIdSource: pb.Document_DEFAULTED.Enum(), + } + if !proto.Equal(got, want) { + t.Errorf("\ngot %v\nwant %v", got, want) + } +} + +func TestLoadSaveWithStruct(t *testing.T) { + type gopher struct { + Name string + Info string `search:"about"` + Legs float64 `search:",facet"` + Fuzz Atom `search:"Fur,facet"` + } + + doc := gopher{"Gopher", "Likes slide rules.", 4, Atom("furry")} + pb := &pb.Document{ + Field: []*pb.Field{ + newStringValueField("Name", "Gopher", pb.FieldValue_TEXT), + newStringValueField("about", "Likes slide rules.", pb.FieldValue_TEXT), + }, + Facet: []*pb.Facet{ + newFacet("Legs", "4e+00", pb.FacetValue_NUMBER), + newFacet("Fur", "furry", pb.FacetValue_ATOM), + }, + } + + var gotDoc gopher + if err := loadDoc(&gotDoc, pb, nil); err != nil { + t.Fatalf("loadDoc: %v", err) + } + if !reflect.DeepEqual(gotDoc, doc) { + t.Errorf("loading doc\ngot %v\nwant %v", gotDoc, doc) + } + + gotPB, err := saveDoc(&doc) + if err != nil { + t.Fatalf("saveDoc: %v", err) + } + gotPB.OrderId = nil // Don't test: it's time dependent. + gotPB.OrderIdSource = nil // Don't test because it's contingent on OrderId. + if !proto.Equal(gotPB, pb) { + t.Errorf("saving doc\ngot %v\nwant %v", gotPB, pb) + } +} + +func TestValidFieldNames(t *testing.T) { + testCases := []struct { + name string + valid bool + }{ + {"Normal", true}, + {"Also_OK_123", true}, + {"Not so great", false}, + {"lower_case", true}, + {"Exclaim!", false}, + {"Hello세상아 안녕", false}, + {"", false}, + {"Hεllo", false}, + {strings.Repeat("A", 500), true}, + {strings.Repeat("A", 501), false}, + } + + for _, tc := range testCases { + _, err := saveDoc(&FieldList{ + Field{Name: tc.name, Value: "val"}, + }) + if err != nil && !strings.Contains(err.Error(), "invalid field name") { + t.Errorf("unexpected err %q for field name %q", err, tc.name) + } + if (err == nil) != tc.valid { + t.Errorf("field %q: expected valid %t, received err %v", tc.name, tc.valid, err) + } + } +} + +func TestValidLangs(t *testing.T) { + testCases := []struct { + field Field + valid bool + }{ + {Field{Name: "Foo", Value: "String", Language: ""}, true}, + {Field{Name: "Foo", Value: "String", Language: "en"}, true}, + {Field{Name: "Foo", Value: "String", Language: "aussie"}, false}, + {Field{Name: "Foo", Value: "String", Language: "12"}, false}, + {Field{Name: "Foo", Value: HTML("String"), Language: "en"}, true}, + {Field{Name: "Foo", Value: Atom("String"), Language: "en"}, false}, + {Field{Name: "Foo", Value: 42, Language: "en"}, false}, + } + + for _, tt := range testCases { + _, err := saveDoc(&FieldList{tt.field}) + if err == nil != tt.valid { + t.Errorf("Field %v, got error %v, wanted valid %t", tt.field, err, tt.valid) + } + } +} + +func TestDuplicateFields(t *testing.T) { + testCases := []struct { + desc string + fields FieldList + errMsg string // Non-empty if we expect an error + }{ + { + desc: "multi string", + fields: FieldList{{Name: "FieldA", Value: "val1"}, {Name: "FieldA", Value: "val2"}, {Name: "FieldA", Value: "val3"}}, + }, + { + desc: "multi atom", + fields: FieldList{{Name: "FieldA", Value: Atom("val1")}, {Name: "FieldA", Value: Atom("val2")}, {Name: "FieldA", Value: Atom("val3")}}, + }, + { + desc: "mixed", + fields: FieldList{{Name: "FieldA", Value: testString}, {Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: float}}, + }, + { + desc: "multi time", + fields: FieldList{{Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: testTime}}, + errMsg: `duplicate time field "FieldA"`, + }, + { + desc: "multi num", + fields: FieldList{{Name: "FieldA", Value: float}, {Name: "FieldA", Value: float}}, + errMsg: `duplicate numeric field "FieldA"`, + }, + } + for _, tc := range testCases { + _, err := saveDoc(&tc.fields) + if (err == nil) != (tc.errMsg == "") || (err != nil && !strings.Contains(err.Error(), tc.errMsg)) { + t.Errorf("%s: got err %v, wanted %q", tc.desc, err, tc.errMsg) + } + } +} + +func TestLoadErrFieldMismatch(t *testing.T) { + testCases := []struct { + desc string + dst interface{} + src []*pb.Field + err error + }{ + { + desc: "missing", + dst: &struct{ One string }{}, + src: []*pb.Field{newStringValueField("Two", "woop!", pb.FieldValue_TEXT)}, + err: &ErrFieldMismatch{ + FieldName: "Two", + Reason: "no such struct field", + }, + }, + { + desc: "wrong type", + dst: &struct{ Num float64 }{}, + src: []*pb.Field{newStringValueField("Num", "woop!", pb.FieldValue_TEXT)}, + err: &ErrFieldMismatch{ + FieldName: "Num", + Reason: "type mismatch: float64 for string data", + }, + }, + { + desc: "unsettable", + dst: &struct{ lower string }{}, + src: []*pb.Field{newStringValueField("lower", "woop!", pb.FieldValue_TEXT)}, + err: &ErrFieldMismatch{ + FieldName: "lower", + Reason: "cannot set struct field", + }, + }, + } + for _, tc := range testCases { + err := loadDoc(tc.dst, &pb.Document{Field: tc.src}, nil) + if !reflect.DeepEqual(err, tc.err) { + t.Errorf("%s, got err %v, wanted %v", tc.desc, err, tc.err) + } + } +} + +func TestLimit(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, res *pb.SearchResponse) error { + limit := 20 // Default per page. + if req.Params.Limit != nil { + limit = int(*req.Params.Limit) + } + res.Status = &pb.RequestStatus{Code: pb.SearchServiceError_OK.Enum()} + res.MatchedCount = proto.Int64(int64(limit)) + for i := 0; i < limit; i++ { + res.Result = append(res.Result, &pb.SearchResult{Document: &pb.Document{}}) + res.Cursor = proto.String("moreresults") + } + return nil + }) + + const maxDocs = 500 // Limit maximum number of docs. + testCases := []struct { + limit, want int + }{ + {limit: 0, want: maxDocs}, + {limit: 42, want: 42}, + {limit: 100, want: 100}, + {limit: 1000, want: maxDocs}, + } + + for _, tt := range testCases { + it := index.Search(c, "gopher", &SearchOptions{Limit: tt.limit, IDsOnly: true}) + count := 0 + for ; count < maxDocs; count++ { + _, err := it.Next(nil) + if err == Done { + break + } + if err != nil { + t.Fatalf("err after %d: %v", count, err) + } + } + if count != tt.want { + t.Errorf("got %d results, expected %d", count, tt.want) + } + } +} + +func TestPut(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { + expectedIn := &pb.IndexDocumentRequest{ + Params: &pb.IndexDocumentParams{ + Document: []*pb.Document{ + {Field: protoFields, OrderId: proto.Int32(42), OrderIdSource: pb.Document_SUPPLIED.Enum()}, + }, + IndexSpec: &pb.IndexSpec{ + Name: proto.String("Doc"), + }, + }, + } + if !proto.Equal(in, expectedIn) { + return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn) + } + *out = pb.IndexDocumentResponse{ + Status: []*pb.RequestStatus{ + {Code: pb.SearchServiceError_OK.Enum()}, + }, + DocId: []string{ + "doc_id", + }, + } + return nil + }) + + id, err := index.Put(c, "", &FieldListWithMeta{ + Meta: searchMeta, + Fields: searchFields, + }) + if err != nil { + t.Fatal(err) + } + if want := "doc_id"; id != want { + t.Errorf("Got doc ID %q, want %q", id, want) + } +} + +func TestPutAutoOrderID(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { + if len(in.Params.GetDocument()) < 1 { + return fmt.Errorf("expected at least one Document, got %v", in) + } + got, want := in.Params.Document[0].GetOrderId(), int32(time.Since(orderIDEpoch).Seconds()) + if d := got - want; -5 > d || d > 5 { + return fmt.Errorf("got OrderId %d, want near %d", got, want) + } + *out = pb.IndexDocumentResponse{ + Status: []*pb.RequestStatus{ + {Code: pb.SearchServiceError_OK.Enum()}, + }, + DocId: []string{ + "doc_id", + }, + } + return nil + }) + + if _, err := index.Put(c, "", &searchFields); err != nil { + t.Fatal(err) + } +} + +func TestPutBadStatus(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(_ *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { + *out = pb.IndexDocumentResponse{ + Status: []*pb.RequestStatus{ + { + Code: pb.SearchServiceError_INVALID_REQUEST.Enum(), + ErrorDetail: proto.String("insufficient gophers"), + }, + }, + } + return nil + }) + + wantErr := "search: INVALID_REQUEST: insufficient gophers" + if _, err := index.Put(c, "", &searchFields); err == nil || err.Error() != wantErr { + t.Fatalf("Put: got %v error, want %q", err, wantErr) + } +} + +func TestPutMultiNilIDSlice(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { + if len(in.Params.GetDocument()) < 1 { + return fmt.Errorf("got %v, want at least 1 document", in) + } + got, want := in.Params.Document[0].GetOrderId(), int32(time.Since(orderIDEpoch).Seconds()) + if d := got - want; -5 > d || d > 5 { + return fmt.Errorf("got OrderId %d, want near %d", got, want) + } + *out = pb.IndexDocumentResponse{ + Status: []*pb.RequestStatus{ + {Code: pb.SearchServiceError_OK.Enum()}, + }, + DocId: []string{ + "doc_id", + }, + } + return nil + }) + + if _, err := index.PutMulti(c, nil, []interface{}{&searchFields}); err != nil { + t.Fatal(err) + } +} + +func TestPutMultiError(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { + *out = pb.IndexDocumentResponse{ + Status: []*pb.RequestStatus{ + {Code: pb.SearchServiceError_OK.Enum()}, + {Code: pb.SearchServiceError_PERMISSION_DENIED.Enum(), ErrorDetail: proto.String("foo")}, + }, + DocId: []string{ + "id1", + "", + }, + } + return nil + }) + + switch _, err := index.PutMulti(c, nil, []interface{}{&searchFields, &searchFields}); { + case err == nil: + t.Fatalf("got nil, want error") + case err.(appengine.MultiError)[0] != nil: + t.Fatalf("got %v, want nil MultiError[0]", err.(appengine.MultiError)[0]) + case err.(appengine.MultiError)[1] == nil: + t.Fatalf("got nil, want not-nill MultiError[1]") + } +} + +func TestPutMultiWrongNumberOfIDs(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { + return nil + }) + + if _, err := index.PutMulti(c, []string{"a"}, []interface{}{&searchFields, &searchFields}); err == nil { + t.Fatal("got success, want error") + } +} + +func TestPutMultiTooManyDocs(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { + return nil + }) + + srcs := make([]interface{}, 201) + for i, _ := range srcs { + srcs[i] = &searchFields + } + + if _, err := index.PutMulti(c, nil, srcs); err != ErrTooManyDocuments { + t.Fatalf("got %v, want ErrTooManyDocuments", err) + } +} + +func TestSortOptions(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + noErr := errors.New("") // Sentinel err to return to prevent sending request. + + testCases := []struct { + desc string + sort *SortOptions + wantSort []*pb.SortSpec + wantScorer *pb.ScorerSpec + wantErr string + }{ + { + desc: "No SortOptions", + }, + { + desc: "Basic", + sort: &SortOptions{ + Expressions: []SortExpression{ + {Expr: "dog"}, + {Expr: "cat", Reverse: true}, + {Expr: "gopher", Default: "blue"}, + {Expr: "fish", Default: 2.0}, + }, + Limit: 42, + Scorer: MatchScorer, + }, + wantSort: []*pb.SortSpec{ + {SortExpression: proto.String("dog")}, + {SortExpression: proto.String("cat"), SortDescending: proto.Bool(false)}, + {SortExpression: proto.String("gopher"), DefaultValueText: proto.String("blue")}, + {SortExpression: proto.String("fish"), DefaultValueNumeric: proto.Float64(2)}, + }, + wantScorer: &pb.ScorerSpec{ + Limit: proto.Int32(42), + Scorer: pb.ScorerSpec_MATCH_SCORER.Enum(), + }, + }, + { + desc: "Bad expression default", + sort: &SortOptions{ + Expressions: []SortExpression{ + {Expr: "dog", Default: true}, + }, + }, + wantErr: `search: invalid Default type bool for expression "dog"`, + }, + { + desc: "RescoringMatchScorer", + sort: &SortOptions{Scorer: RescoringMatchScorer}, + wantScorer: &pb.ScorerSpec{Scorer: pb.ScorerSpec_RESCORING_MATCH_SCORER.Enum()}, + }, + } + + for _, tt := range testCases { + c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error { + params := req.Params + if !reflect.DeepEqual(params.SortSpec, tt.wantSort) { + t.Errorf("%s: params.SortSpec=%v; want %v", tt.desc, params.SortSpec, tt.wantSort) + } + if !reflect.DeepEqual(params.ScorerSpec, tt.wantScorer) { + t.Errorf("%s: params.ScorerSpec=%v; want %v", tt.desc, params.ScorerSpec, tt.wantScorer) + } + return noErr // Always return some error to prevent response parsing. + }) + + it := index.Search(c, "gopher", &SearchOptions{Sort: tt.sort}) + _, err := it.Next(nil) + if err == nil { + t.Fatalf("%s: err==nil; should not happen", tt.desc) + } + if err.Error() != tt.wantErr { + t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr) + } + } +} + +func TestFieldSpec(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + errFoo := errors.New("foo") // sentinel error when there isn't one. + + testCases := []struct { + desc string + opts *SearchOptions + want *pb.FieldSpec + }{ + { + desc: "No options", + want: &pb.FieldSpec{}, + }, + { + desc: "Fields", + opts: &SearchOptions{ + Fields: []string{"one", "two"}, + }, + want: &pb.FieldSpec{ + Name: []string{"one", "two"}, + }, + }, + { + desc: "Expressions", + opts: &SearchOptions{ + Expressions: []FieldExpression{ + {Name: "one", Expr: "price * quantity"}, + {Name: "two", Expr: "min(daily_use, 10) * rate"}, + }, + }, + want: &pb.FieldSpec{ + Expression: []*pb.FieldSpec_Expression{ + {Name: proto.String("one"), Expression: proto.String("price * quantity")}, + {Name: proto.String("two"), Expression: proto.String("min(daily_use, 10) * rate")}, + }, + }, + }, + } + + for _, tt := range testCases { + c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error { + params := req.Params + if !reflect.DeepEqual(params.FieldSpec, tt.want) { + t.Errorf("%s: params.FieldSpec=%v; want %v", tt.desc, params.FieldSpec, tt.want) + } + return errFoo // Always return some error to prevent response parsing. + }) + + it := index.Search(c, "gopher", tt.opts) + if _, err := it.Next(nil); err != errFoo { + t.Fatalf("%s: got error %v; want %v", tt.desc, err, errFoo) + } + } +} + +func TestBasicSearchOpts(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + noErr := errors.New("") // Sentinel err to return to prevent sending request. + + testCases := []struct { + desc string + facetOpts []FacetSearchOption + cursor Cursor + offset int + countAccuracy int + want *pb.SearchParams + wantErr string + }{ + { + desc: "No options", + want: &pb.SearchParams{}, + }, + { + desc: "Default auto discovery", + facetOpts: []FacetSearchOption{ + AutoFacetDiscovery(0, 0), + }, + want: &pb.SearchParams{ + AutoDiscoverFacetCount: proto.Int32(10), + }, + }, + { + desc: "Auto discovery", + facetOpts: []FacetSearchOption{ + AutoFacetDiscovery(7, 12), + }, + want: &pb.SearchParams{ + AutoDiscoverFacetCount: proto.Int32(7), + FacetAutoDetectParam: &pb.FacetAutoDetectParam{ + ValueLimit: proto.Int32(12), + }, + }, + }, + { + desc: "Param Depth", + facetOpts: []FacetSearchOption{ + AutoFacetDiscovery(7, 12), + }, + want: &pb.SearchParams{ + AutoDiscoverFacetCount: proto.Int32(7), + FacetAutoDetectParam: &pb.FacetAutoDetectParam{ + ValueLimit: proto.Int32(12), + }, + }, + }, + { + desc: "Doc depth", + facetOpts: []FacetSearchOption{ + FacetDocumentDepth(123), + }, + want: &pb.SearchParams{ + FacetDepth: proto.Int32(123), + }, + }, + { + desc: "Facet discovery", + facetOpts: []FacetSearchOption{ + FacetDiscovery("colour"), + FacetDiscovery("size", Atom("M"), Atom("L")), + FacetDiscovery("price", LessThan(7), Range{7, 14}, AtLeast(14)), + }, + want: &pb.SearchParams{ + IncludeFacet: []*pb.FacetRequest{ + {Name: proto.String("colour")}, + {Name: proto.String("size"), Params: &pb.FacetRequestParam{ + ValueConstraint: []string{"M", "L"}, + }}, + {Name: proto.String("price"), Params: &pb.FacetRequestParam{ + Range: []*pb.FacetRange{ + {End: proto.String("7e+00")}, + {Start: proto.String("7e+00"), End: proto.String("1.4e+01")}, + {Start: proto.String("1.4e+01")}, + }, + }}, + }, + }, + }, + { + desc: "Facet discovery - bad value", + facetOpts: []FacetSearchOption{ + FacetDiscovery("colour", true), + }, + wantErr: "bad FacetSearchOption: unsupported value type bool", + }, + { + desc: "Facet discovery - mix value types", + facetOpts: []FacetSearchOption{ + FacetDiscovery("colour", Atom("blue"), AtLeast(7)), + }, + wantErr: "bad FacetSearchOption: values must all be Atom, or must all be Range", + }, + { + desc: "Facet discovery - invalid range", + facetOpts: []FacetSearchOption{ + FacetDiscovery("colour", Range{negInf, posInf}), + }, + wantErr: "bad FacetSearchOption: invalid range: either Start or End must be finite", + }, + { + desc: "Cursor", + cursor: Cursor("mycursor"), + want: &pb.SearchParams{ + Cursor: proto.String("mycursor"), + }, + }, + { + desc: "Offset", + offset: 121, + want: &pb.SearchParams{ + Offset: proto.Int32(121), + }, + }, + { + desc: "Cursor and Offset set", + cursor: Cursor("mycursor"), + offset: 121, + wantErr: "at most one of Cursor and Offset may be specified", + }, + { + desc: "Count accuracy", + countAccuracy: 100, + want: &pb.SearchParams{ + MatchedCountAccuracy: proto.Int32(100), + }, + }, + } + + for _, tt := range testCases { + c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error { + if tt.want == nil { + t.Errorf("%s: expected call to fail", tt.desc) + return nil + } + // Set default fields. + tt.want.Query = proto.String("gopher") + tt.want.IndexSpec = &pb.IndexSpec{Name: proto.String("Doc")} + tt.want.CursorType = pb.SearchParams_PER_RESULT.Enum() + tt.want.FieldSpec = &pb.FieldSpec{} + if got := req.Params; !reflect.DeepEqual(got, tt.want) { + t.Errorf("%s: params=%v; want %v", tt.desc, got, tt.want) + } + return noErr // Always return some error to prevent response parsing. + }) + + it := index.Search(c, "gopher", &SearchOptions{ + Facets: tt.facetOpts, + Cursor: tt.cursor, + Offset: tt.offset, + CountAccuracy: tt.countAccuracy, + }) + _, err := it.Next(nil) + if err == nil { + t.Fatalf("%s: err==nil; should not happen", tt.desc) + } + if err.Error() != tt.wantErr { + t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr) + } + } +} + +func TestFacetRefinements(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + noErr := errors.New("") // Sentinel err to return to prevent sending request. + + testCases := []struct { + desc string + refine []Facet + want []*pb.FacetRefinement + wantErr string + }{ + { + desc: "No refinements", + }, + { + desc: "Basic", + refine: []Facet{ + {Name: "fur", Value: Atom("fluffy")}, + {Name: "age", Value: LessThan(123)}, + {Name: "age", Value: AtLeast(0)}, + {Name: "legs", Value: Range{Start: 3, End: 5}}, + }, + want: []*pb.FacetRefinement{ + {Name: proto.String("fur"), Value: proto.String("fluffy")}, + {Name: proto.String("age"), Range: &pb.FacetRefinement_Range{End: proto.String("1.23e+02")}}, + {Name: proto.String("age"), Range: &pb.FacetRefinement_Range{Start: proto.String("0e+00")}}, + {Name: proto.String("legs"), Range: &pb.FacetRefinement_Range{Start: proto.String("3e+00"), End: proto.String("5e+00")}}, + }, + }, + { + desc: "Infinite range", + refine: []Facet{ + {Name: "age", Value: Range{Start: negInf, End: posInf}}, + }, + wantErr: `search: refinement for facet "age": either Start or End must be finite`, + }, + { + desc: "Bad End value in range", + refine: []Facet{ + {Name: "age", Value: LessThan(2147483648)}, + }, + wantErr: `search: refinement for facet "age": invalid value for End`, + }, + { + desc: "Bad Start value in range", + refine: []Facet{ + {Name: "age", Value: AtLeast(-2147483649)}, + }, + wantErr: `search: refinement for facet "age": invalid value for Start`, + }, + { + desc: "Unknown value type", + refine: []Facet{ + {Name: "age", Value: "you can't use strings!"}, + }, + wantErr: `search: unsupported refinement for facet "age" of type string`, + }, + } + + for _, tt := range testCases { + c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error { + if got := req.Params.FacetRefinement; !reflect.DeepEqual(got, tt.want) { + t.Errorf("%s: params.FacetRefinement=%v; want %v", tt.desc, got, tt.want) + } + return noErr // Always return some error to prevent response parsing. + }) + + it := index.Search(c, "gopher", &SearchOptions{Refinements: tt.refine}) + _, err := it.Next(nil) + if err == nil { + t.Fatalf("%s: err==nil; should not happen", tt.desc) + } + if err.Error() != tt.wantErr { + t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr) + } + } +} + +func TestNamespaceResetting(t *testing.T) { + namec := make(chan *string, 1) + c0 := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(req *pb.IndexDocumentRequest, res *pb.IndexDocumentResponse) error { + namec <- req.Params.IndexSpec.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) + } + + i := (&Index{}) + + i.Put(c0, "something", &searchDoc) + if ns := <-namec; ns != nil { + t.Errorf(`Put with c0: ns = %q, want nil`, *ns) + } + + i.Put(c1, "something", &searchDoc) + if ns := <-namec; ns == nil { + t.Error(`Put with c1: ns = nil, want "A"`) + } else if *ns != "A" { + t.Errorf(`Put with c1: ns = %q, want "A"`, *ns) + } + + i.Put(c2, "something", &searchDoc) + if ns := <-namec; ns != nil { + t.Errorf(`Put with c2: ns = %q, want nil`, *ns) + } +} + +func TestDelete(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error { + expectedIn := &pb.DeleteDocumentRequest{ + Params: &pb.DeleteDocumentParams{ + DocId: []string{"id"}, + IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")}, + }, + } + if !proto.Equal(in, expectedIn) { + return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn) + } + *out = pb.DeleteDocumentResponse{ + Status: []*pb.RequestStatus{ + {Code: pb.SearchServiceError_OK.Enum()}, + }, + } + return nil + }) + + if err := index.Delete(c, "id"); err != nil { + t.Fatal(err) + } +} + +func TestDeleteMulti(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error { + expectedIn := &pb.DeleteDocumentRequest{ + Params: &pb.DeleteDocumentParams{ + DocId: []string{"id1", "id2"}, + IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")}, + }, + } + if !proto.Equal(in, expectedIn) { + return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn) + } + *out = pb.DeleteDocumentResponse{ + Status: []*pb.RequestStatus{ + {Code: pb.SearchServiceError_OK.Enum()}, + {Code: pb.SearchServiceError_OK.Enum()}, + }, + } + return nil + }) + + if err := index.DeleteMulti(c, []string{"id1", "id2"}); err != nil { + t.Fatal(err) + } +} + +func TestDeleteWrongNumberOfResults(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error { + expectedIn := &pb.DeleteDocumentRequest{ + Params: &pb.DeleteDocumentParams{ + DocId: []string{"id1", "id2"}, + IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")}, + }, + } + if !proto.Equal(in, expectedIn) { + return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn) + } + *out = pb.DeleteDocumentResponse{ + Status: []*pb.RequestStatus{ + {Code: pb.SearchServiceError_OK.Enum()}, + }, + } + return nil + }) + + if err := index.DeleteMulti(c, []string{"id1", "id2"}); err == nil { + t.Fatalf("got nil, want error") + } +} + +func TestDeleteMultiError(t *testing.T) { + index, err := Open("Doc") + if err != nil { + t.Fatalf("err from Open: %v", err) + } + + c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error { + expectedIn := &pb.DeleteDocumentRequest{ + Params: &pb.DeleteDocumentParams{ + DocId: []string{"id1", "id2"}, + IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")}, + }, + } + if !proto.Equal(in, expectedIn) { + return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn) + } + *out = pb.DeleteDocumentResponse{ + Status: []*pb.RequestStatus{ + {Code: pb.SearchServiceError_OK.Enum()}, + {Code: pb.SearchServiceError_PERMISSION_DENIED.Enum(), ErrorDetail: proto.String("foo")}, + }, + } + return nil + }) + + switch err := index.DeleteMulti(c, []string{"id1", "id2"}); { + case err == nil: + t.Fatalf("got nil, want error") + case err.(appengine.MultiError)[0] != nil: + t.Fatalf("got %v, want nil MultiError[0]", err.(appengine.MultiError)[0]) + case err.(appengine.MultiError)[1] == nil: + t.Fatalf("got nil, want not-nill MultiError[1]") + } +} |