Skip to content

Commit

Permalink
GOCBC-710: Improve search tests
Browse files Browse the repository at this point in the history
Motivation
----------
Improve the search tests so that we can have more confidence
that our changes don't break things.

Changes
-------
Add some search tests. Fix minor issues in search options causing
fields to be set incorrectly in the payload.

Change-Id: I70de99e6c07ef87bd80c290b05fec0ffb4d0c715
Reviewed-on: http://review.couchbase.org/123084
Reviewed-by: Brett Lawson <brett19@gmail.com>
Tested-by: Charles Dixon <chvckd@gmail.com>
  • Loading branch information
chvck committed Mar 4, 2020
1 parent dee4f20 commit 696d589
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 21 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ updatemocks:
mockery -name mgmtProvider -output . -testonly -inpkg
mockery -name analyticsProvider -output . -testonly -inpkg
mockery -name queryProvider -output . -testonly -inpkg
mockery -name searchProvider -output . -testonly -inpkg
mockery -name clusterCapabilityProvider -output . -testonly -inpkg
# pendingOp is manually mocked

Expand Down
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (c *stdClient) getSearchProvider() (searchProvider, error) {
if c.agent == nil {
return nil, errors.New("cluster not yet connected")
}
return c.agent, nil
return &searchProviderWrapper{provider: c.agent}, nil
}

func (c *stdClient) getHTTPProvider() (httpProvider, error) {
Expand Down
11 changes: 9 additions & 2 deletions cluster_searchquery.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,21 @@ func (sr *SearchRow) Fields(valuePtr interface{}) error {
return json.Unmarshal(sr.fieldsBytes, valuePtr)
}

type searchRowReader interface {
NextRow() []byte
Err() error
MetaData() ([]byte, error)
Close() error
}

// SearchResult allows access to the results of a search query.
type SearchResult struct {
reader *gocbcore.SearchRowReader
reader searchRowReader

currentRow SearchRow
}

func newSearchResult(reader *gocbcore.SearchRowReader) (*SearchResult, error) {
func newSearchResult(reader searchRowReader) (*SearchResult, error) {
return &SearchResult{
reader: reader,
}, nil
Expand Down
260 changes: 260 additions & 0 deletions cluster_searchquery_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
package gocb

import (
"encoding/json"
"time"

"github.com/couchbase/gocbcore/v8"
"github.com/stretchr/testify/mock"

"github.com/couchbase/gocb/v2/search"
)

func (suite *IntegrationTestSuite) TestSearch() {
if globalCluster.NotSupportsFeature(SearchFeature) {
suite.T().Skip("Skipping test as search not supported.")
}

n := suite.setupSearch()
suite.runSearchTest(n)
}

func (suite *IntegrationTestSuite) runSearchTest(n int) {
deadline := time.Now().Add(10 * time.Second)
query := search.NewTermQuery("search").Field("service")
var result *SearchResult
for {
var err error
result, err = globalCluster.SearchQuery("search_test_index", query, &SearchOptions{
Timeout: 1 * time.Second,
Facets: map[string]search.Facet{
"type": search.NewTermFacet("country", 5),
},
})
if err != nil {
sleepDeadline := time.Now().Add(1000 * time.Millisecond)
if sleepDeadline.After(deadline) {
sleepDeadline = deadline
}
time.Sleep(sleepDeadline.Sub(time.Now()))

if sleepDeadline == deadline {
suite.T().Fatalf("timed out waiting for indexing")
}
continue
}

var ids []string
for result.Next() {
row := result.Row()
ids = append(ids, row.ID)
}

err = result.Err()
suite.Require().Nil(err, err)

if n == len(ids) {
break
}

sleepDeadline := time.Now().Add(1000 * time.Millisecond)
if sleepDeadline.After(deadline) {
sleepDeadline = deadline
}
time.Sleep(sleepDeadline.Sub(time.Now()))

if sleepDeadline == deadline {
suite.T().Fatalf("timed out waiting for indexing")
}
}

metadata, err := result.MetaData()
suite.Require().Nil(err, err)

suite.Assert().NotEmpty(metadata.Metrics.TotalRows)
suite.Assert().NotEmpty(metadata.Metrics.Took)
suite.Assert().NotEmpty(metadata.Metrics.MaxScore)

facets, err := result.Facets()
suite.Require().Nil(err, err)
if suite.Assert().Contains(facets, "type") {
f := facets["type"]
suite.Assert().Equal("country", f.Field)
suite.Assert().NotEmpty(f.Total)
}
}

func (suite *IntegrationTestSuite) setupSearch() int {
n, err := suite.createBreweryDataset("beer_sample_brewery_five", "search")
suite.Require().Nil(err, err)

mgr := globalCluster.SearchIndexes()
err = mgr.UpsertIndex(SearchIndex{
Name: "search_test_index",
SourceName: globalBucket.Name(),
SourceType: "couchbase",
Type: "fulltext-index",
}, &UpsertSearchIndexOptions{
Timeout: 1 * time.Second,
})
suite.Require().Nil(err, err)

return n
}

// We have to manually mock this because testify won't let return something which can iterate.
type mockSearchRowReader struct {
Dataset []jsonSearchRow
Meta []byte
MetaErr error
CloseErr error
RowsErr error

Suite *UnitTestSuite

idx int
}

func (arr *mockSearchRowReader) NextRow() []byte {
if arr.idx == len(arr.Dataset) {
return nil
}

idx := arr.idx
arr.idx++

return arr.Suite.mustConvertToBytes(arr.Dataset[idx])
}

func (arr *mockSearchRowReader) MetaData() ([]byte, error) {
return arr.Meta, arr.MetaErr
}

func (arr *mockSearchRowReader) Close() error {
return arr.CloseErr
}

func (arr *mockSearchRowReader) Err() error {
return arr.RowsErr
}

type testSearchDataset struct {
Hits []jsonSearchRow
jsonSearchResponse
}

func (suite *UnitTestSuite) searchCluster(reader searchRowReader, runFn func(args mock.Arguments)) *Cluster {
cluster := clusterFromOptions(ClusterOptions{})

provider := new(mockSearchProvider)
provider.
On("SearchQuery", mock.AnythingOfType("gocbcore.SearchQueryOptions")).
Run(runFn).
Return(reader, nil)

cli := new(mockClient)
cli.On("getSearchProvider").Return(provider, nil)
cli.On("supportsGCCCP").Return(true)

cluster.clusterClient = cli

return cluster
}

func (suite *UnitTestSuite) TestSearchQuery() {
var dataset testSearchDataset
err := loadJSONTestDataset("beer_sample_search_dataset", &dataset)
suite.Require().Nil(err, err)

reader := &mockSearchRowReader{
Dataset: dataset.Hits,
Meta: suite.mustConvertToBytes(dataset.jsonSearchResponse),
Suite: suite,
}

query := search.NewTermQuery("term").Field("field").Fuzziness(1).Boost(2).PrefixLength(3)

var cluster *Cluster
cluster = suite.searchCluster(reader, func(args mock.Arguments) {
opts := args.Get(0).(gocbcore.SearchQueryOptions)
suite.Assert().Equal(cluster.sb.RetryStrategyWrapper, opts.RetryStrategy)
now := time.Now()
if opts.Deadline.Before(now.Add(70*time.Second)) || opts.Deadline.After(now.Add(75*time.Second)) {
suite.Fail("Deadline should have been <75s and >70s but was %s", opts.Deadline)
}

var actualOptions map[string]interface{}
err := json.Unmarshal(opts.Payload, &actualOptions)
suite.Require().Nil(err)

if suite.Assert().Contains(actualOptions, "fields") {
suite.Assert().Equal([]interface{}{"name"}, actualOptions["fields"])
}

if suite.Assert().Contains(actualOptions, "query") {
q := actualOptions["query"].(map[string]interface{})
suite.Assert().Equal("term", q["term"])
suite.Assert().Equal("field", q["field"])
suite.Assert().Equal(float64(1), q["fuzziness"])
suite.Assert().Equal(float64(2), q["boost"])
suite.Assert().Equal(float64(3), q["prefix_length"])
}

if suite.Assert().Contains(actualOptions, "sort") {
s := actualOptions["sort"].([]interface{})
suite.Require().Len(s, 1)
srt := s[0].(map[string]interface{})
suite.Assert().Equal("id", srt["by"])
suite.Assert().Equal(true, srt["desc"])
}
})

result, err := cluster.SearchQuery("testindex", query, &SearchOptions{
Fields: []string{"name"},
Facets: map[string]search.Facet{
"type": search.NewTermFacet("country", 5),
},
Sort: []search.Sort{search.NewSearchSortID().Descending(true)},
})
suite.Require().Nil(err, err)
suite.Require().NotNil(result)

var hits []SearchRow
for result.Next() {
hit := result.Row()
hits = append(hits, hit)
var field struct {
Name string
}
err := hit.Fields(&field)
suite.Require().Nil(err, err)
suite.Assert().NotEmpty(field.Name)
}

err = result.Err()
suite.Require().Nil(err, err)

suite.Assert().Len(hits, len(dataset.Hits))

metadata, err := result.MetaData()
suite.Require().Nil(err, err)

var meta SearchMetaData
err = meta.fromData(dataset.jsonSearchResponse)
suite.Require().Nil(err, err)
suite.Assert().Equal(&meta, metadata)

facets, err := result.Facets()
suite.Require().Nil(err, err)

expectedFacets := make(map[string]SearchFacetResult)
for facetName, facetData := range dataset.Facets {
var facet SearchFacetResult
err := facet.fromData(facetData)
suite.Require().Nil(err, err)

expectedFacets[facetName] = facet
}

suite.Assert().Equal(expectedFacets, facets)
}
34 changes: 34 additions & 0 deletions mock_searchProvider_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type analyticsProvider interface {
}

type searchProvider interface {
SearchQuery(opts gocbcore.SearchQueryOptions) (*gocbcore.SearchRowReader, error)
SearchQuery(opts gocbcore.SearchQueryOptions) (searchRowReader, error)
}

type clusterCapabilityProvider interface {
Expand All @@ -47,3 +47,11 @@ type queryProviderWrapper struct {
func (apw *queryProviderWrapper) N1QLQuery(opts gocbcore.N1QLQueryOptions) (queryRowReader, error) {
return apw.provider.N1QLQuery(opts)
}

type searchProviderWrapper struct {
provider *gocbcore.Agent
}

func (apw *searchProviderWrapper) SearchQuery(opts gocbcore.SearchQueryOptions) (searchRowReader, error) {
return apw.provider.SearchQuery(opts)
}
Loading

0 comments on commit 696d589

Please sign in to comment.