mirror of https://github.com/perkeep/perkeep.git
1140 lines
27 KiB
Go
1140 lines
27 KiB
Go
package search_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"camlistore.org/pkg/blob"
|
|
"camlistore.org/pkg/index"
|
|
"camlistore.org/pkg/index/indextest"
|
|
. "camlistore.org/pkg/search"
|
|
"camlistore.org/pkg/test"
|
|
"camlistore.org/pkg/types"
|
|
)
|
|
|
|
// indexType is one of the three ways we test the query handler code.
|
|
type indexType int
|
|
|
|
var queryType = flag.String("querytype", "", "Empty for all query types, else 'classic', 'scan', or 'build'")
|
|
|
|
const (
|
|
indexClassic indexType = iota // sorted key/value pairs from index.Storage
|
|
indexCorpusScan // *Corpus scanned from key/value pairs on start
|
|
indexCorpusBuild // empty *Corpus, built iteratively as blob received.
|
|
)
|
|
|
|
var (
|
|
allIndexTypes = []indexType{indexClassic, indexCorpusScan, indexCorpusBuild}
|
|
memIndexTypes = []indexType{indexCorpusScan, indexCorpusBuild}
|
|
corpusTypeOnly = []indexType{indexCorpusScan}
|
|
)
|
|
|
|
func (i indexType) String() string {
|
|
switch i {
|
|
case indexClassic:
|
|
return "classic"
|
|
case indexCorpusScan:
|
|
return "scan"
|
|
case indexCorpusBuild:
|
|
return "build"
|
|
default:
|
|
return fmt.Sprintf("unknown-index-type-%d", i)
|
|
}
|
|
}
|
|
|
|
type queryTest struct {
|
|
t test.TB
|
|
id *indextest.IndexDeps
|
|
|
|
Handler func() *Handler
|
|
}
|
|
|
|
func querySetup(t test.TB) (*indextest.IndexDeps, *Handler) {
|
|
idx := index.NewMemoryIndex() // string key-value pairs in memory, as if they were on disk
|
|
id := indextest.NewIndexDeps(idx)
|
|
id.Fataler = t
|
|
h := NewHandler(idx, id.SignerBlobRef)
|
|
return id, h
|
|
}
|
|
|
|
func testQuery(t test.TB, fn func(*queryTest)) {
|
|
testQueryTypes(t, allIndexTypes, fn)
|
|
}
|
|
|
|
func testQueryTypes(t test.TB, types []indexType, fn func(*queryTest)) {
|
|
defer test.TLog(t)()
|
|
for _, it := range types {
|
|
if *queryType == "" || *queryType == it.String() {
|
|
t.Logf("Testing: --querytype=%s ...", it)
|
|
testQueryType(t, fn, it)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testQueryType(t test.TB, fn func(*queryTest), itype indexType) {
|
|
defer index.SetVerboseCorpusLogging(true)
|
|
index.SetVerboseCorpusLogging(false)
|
|
|
|
idx := index.NewMemoryIndex() // string key-value pairs in memory, as if they were on disk
|
|
var err error
|
|
var corpus *index.Corpus
|
|
if itype == indexCorpusBuild {
|
|
if corpus, err = idx.KeepInMemory(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
qt := &queryTest{
|
|
t: t,
|
|
id: indextest.NewIndexDeps(idx),
|
|
}
|
|
qt.id.Fataler = t
|
|
qt.Handler = func() *Handler {
|
|
h := NewHandler(idx, qt.id.SignerBlobRef)
|
|
if itype == indexCorpusScan {
|
|
if corpus, err = idx.KeepInMemory(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
idx.PreventStorageAccessForTesting()
|
|
}
|
|
if corpus != nil {
|
|
h.SetCorpus(corpus)
|
|
}
|
|
return h
|
|
}
|
|
fn(qt)
|
|
}
|
|
|
|
func dumpRes(t *testing.T, res *SearchResult) {
|
|
t.Logf("Got: %#v", res)
|
|
for i, got := range res.Blobs {
|
|
t.Logf(" %d. %s", i, got)
|
|
}
|
|
}
|
|
|
|
func (qt *queryTest) wantRes(req *SearchQuery, wanted ...blob.Ref) {
|
|
res, err := qt.Handler().Query(req)
|
|
if err != nil {
|
|
qt.t.Fatal(err)
|
|
}
|
|
|
|
need := make(map[blob.Ref]bool)
|
|
for _, br := range wanted {
|
|
need[br] = true
|
|
}
|
|
for _, bi := range res.Blobs {
|
|
if !need[bi.Blob] {
|
|
qt.t.Errorf("unexpected search result: %v", bi.Blob)
|
|
} else {
|
|
delete(need, bi.Blob)
|
|
}
|
|
}
|
|
for br := range need {
|
|
qt.t.Errorf("missing from search result: %v", br)
|
|
}
|
|
}
|
|
|
|
func TestQuery(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
fileRef, wholeRef := qt.id.UploadFile("file.txt", "the content", time.Unix(1382073153, 0))
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Anything: true,
|
|
},
|
|
Limit: 0,
|
|
Sort: UnspecifiedSort,
|
|
}
|
|
qt.wantRes(sq, fileRef, wholeRef)
|
|
})
|
|
}
|
|
|
|
func TestQueryCamliType(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
fileRef, _ := qt.id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0))
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
CamliType: "file",
|
|
},
|
|
}
|
|
qt.wantRes(sq, fileRef)
|
|
})
|
|
}
|
|
|
|
func TestQueryAnyCamliType(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
fileRef, _ := qt.id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0))
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
AnyCamliType: true,
|
|
},
|
|
}
|
|
qt.wantRes(sq, fileRef)
|
|
})
|
|
}
|
|
|
|
func TestQueryBlobSize(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
_, smallFileRef := qt.id.UploadFile("file.txt", strings.Repeat("x", 5<<10), time.Unix(1382073153, 0))
|
|
qt.id.UploadFile("file.txt", strings.Repeat("x", 20<<10), time.Unix(1382073153, 0))
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
BlobSize: &IntConstraint{
|
|
Min: 4 << 10,
|
|
Max: 6 << 10,
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, smallFileRef)
|
|
})
|
|
}
|
|
|
|
func TestQueryBlobRefPrefix(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
// foo is 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
|
|
id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0))
|
|
// "bar.." is 08ef767ba2c93f8f40902118fa5260a65a2a4975
|
|
id.UploadFile("file.txt", "bar..", time.Unix(1382073153, 0))
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
BlobRefPrefix: "sha1-0",
|
|
},
|
|
}
|
|
sres, err := qt.Handler().Query(sq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(sres.Blobs) < 2 {
|
|
t.Errorf("expected at least 2 matches; got %d", len(sres.Blobs))
|
|
}
|
|
for _, res := range sres.Blobs {
|
|
brStr := res.Blob.String()
|
|
if !strings.HasPrefix(brStr, "sha1-0") {
|
|
t.Errorf("matched blob %s didn't begin with sha1-0", brStr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestQueryTwoConstraints(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
id.UploadString("a") // 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8
|
|
b := id.UploadString("b") // e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98
|
|
id.UploadString("c4") // e4666a670f042877c67a84473a71675ee0950a08
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
BlobRefPrefix: "sha1-e", // matches b and c4
|
|
BlobSize: &IntConstraint{ // matches a and b
|
|
Min: 1,
|
|
Max: 1,
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, b)
|
|
})
|
|
}
|
|
|
|
func TestQueryLogicalOr(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
// foo is 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
|
|
_, foo := id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0))
|
|
// "bar.." is 08ef767ba2c93f8f40902118fa5260a65a2a4975
|
|
_, bar := id.UploadFile("file.txt", "bar..", time.Unix(1382073153, 0))
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Logical: &LogicalConstraint{
|
|
Op: "or",
|
|
A: &Constraint{
|
|
BlobRefPrefix: "sha1-0beec7b5ea3f0fdbc95d0dd",
|
|
},
|
|
B: &Constraint{
|
|
BlobRefPrefix: "sha1-08ef767ba2c93f8f40",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, foo, bar)
|
|
})
|
|
}
|
|
|
|
func TestQueryLogicalAnd(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
// foo is 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
|
|
_, foo := id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0))
|
|
// "bar.." is 08ef767ba2c93f8f40902118fa5260a65a2a4975
|
|
id.UploadFile("file.txt", "bar..", time.Unix(1382073153, 0))
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Logical: &LogicalConstraint{
|
|
Op: "and",
|
|
A: &Constraint{
|
|
BlobRefPrefix: "sha1-0",
|
|
},
|
|
B: &Constraint{
|
|
BlobSize: &IntConstraint{
|
|
Max: int64(len("foo")), // excludes "bar.."
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, foo)
|
|
})
|
|
}
|
|
|
|
func TestQueryLogicalXor(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
// foo is 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
|
|
_, foo := id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0))
|
|
// "bar.." is 08ef767ba2c93f8f40902118fa5260a65a2a4975
|
|
id.UploadFile("file.txt", "bar..", time.Unix(1382073153, 0))
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Logical: &LogicalConstraint{
|
|
Op: "xor",
|
|
A: &Constraint{
|
|
BlobRefPrefix: "sha1-0",
|
|
},
|
|
B: &Constraint{
|
|
BlobRefPrefix: "sha1-08ef767ba2c93f8f40",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, foo)
|
|
})
|
|
}
|
|
|
|
func TestQueryLogicalNot(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
// foo is 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
|
|
_, foo := id.UploadFile("file.txt", "foo", time.Unix(1382073153, 0))
|
|
// "bar.." is 08ef767ba2c93f8f40902118fa5260a65a2a4975
|
|
_, bar := id.UploadFile("file.txt", "bar..", time.Unix(1382073153, 0))
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Logical: &LogicalConstraint{
|
|
Op: "not",
|
|
A: &Constraint{
|
|
CamliType: "file",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, foo, bar)
|
|
})
|
|
}
|
|
|
|
func TestQueryPermanodeAttrExact(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
p1 := id.NewPlannedPermanode("1")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
id.SetAttribute(p1, "someAttr", "value1")
|
|
id.SetAttribute(p2, "someAttr", "value2")
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "someAttr",
|
|
Value: "value1",
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p1)
|
|
})
|
|
}
|
|
|
|
func TestQueryPermanodeAttrMatches(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
p1 := id.NewPlannedPermanode("1")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
p3 := id.NewPlannedPermanode("3")
|
|
id.SetAttribute(p1, "someAttr", "value1")
|
|
id.SetAttribute(p2, "someAttr", "value2")
|
|
id.SetAttribute(p3, "someAttr", "NOT starting with value")
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "someAttr",
|
|
ValueMatches: &StringConstraint{
|
|
HasPrefix: "value",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p1, p2)
|
|
})
|
|
}
|
|
|
|
func TestQueryPermanodeAttrNumValue(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
// TODO(bradfitz): if we set an empty attribute value here and try to search
|
|
// by NumValue IntConstraint Min = 1, it fails only in classic (no corpus) mode.
|
|
// Something there must be skipping empty values.
|
|
p1 := id.NewPlannedPermanode("1")
|
|
id.AddAttribute(p1, "x", "1")
|
|
id.AddAttribute(p1, "x", "2")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
id.AddAttribute(p2, "x", "1")
|
|
id.AddAttribute(p2, "x", "2")
|
|
id.AddAttribute(p2, "x", "3")
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "x",
|
|
NumValue: &IntConstraint{
|
|
Min: 3,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p2)
|
|
})
|
|
}
|
|
|
|
// Tests that NumValue queries with ZeroMax return permanodes without any values.
|
|
func TestQueryPermanodeAttrNumValueZeroMax(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
p1 := id.NewPlannedPermanode("1")
|
|
id.AddAttribute(p1, "x", "1")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
id.AddAttribute(p2, "y", "1") // Permanodes without any attributes are ignored.
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "x",
|
|
NumValue: &IntConstraint{
|
|
ZeroMax: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p2)
|
|
})
|
|
}
|
|
|
|
// find a permanode (p2) that has a property being a blobref pointing
|
|
// to a sub-query
|
|
func TestQueryPermanodeAttrValueInSet(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
p1 := id.NewPlannedPermanode("1")
|
|
id.SetAttribute(p1, "bar", "baz")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
id.SetAttribute(p2, "foo", p1.String())
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "foo",
|
|
ValueInSet: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "bar",
|
|
Value: "baz",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p2)
|
|
})
|
|
}
|
|
|
|
// Tests PermanodeConstraint.ValueMatchesInt.
|
|
func TestQueryPermanodeValueMatchesInt(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
p1 := id.NewPlannedPermanode("1")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
p3 := id.NewPlannedPermanode("3")
|
|
p4 := id.NewPlannedPermanode("4")
|
|
p5 := id.NewPlannedPermanode("5")
|
|
id.SetAttribute(p1, "x", "-5")
|
|
id.SetAttribute(p2, "x", "0")
|
|
id.SetAttribute(p3, "x", "2")
|
|
id.SetAttribute(p4, "x", "10.0")
|
|
id.SetAttribute(p5, "x", "abc")
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "x",
|
|
ValueMatchesInt: &IntConstraint{
|
|
Min: -2,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p2, p3)
|
|
})
|
|
}
|
|
|
|
// Tests PermanodeConstraint.ValueMatchesFloat.
|
|
func TestQueryPermanodeValueMatchesFloat(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
p1 := id.NewPlannedPermanode("1")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
p3 := id.NewPlannedPermanode("3")
|
|
p4 := id.NewPlannedPermanode("4")
|
|
id.SetAttribute(p1, "x", "2.5")
|
|
id.SetAttribute(p2, "x", "5.7")
|
|
id.SetAttribute(p3, "x", "10")
|
|
id.SetAttribute(p4, "x", "abc")
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "x",
|
|
ValueMatchesFloat: &FloatConstraint{
|
|
Max: 6.0,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p1, p2)
|
|
})
|
|
}
|
|
|
|
// find permanodes matching a certain file query
|
|
func TestQueryFileConstraint(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
fileRef, _ := id.UploadFile("some-stuff.txt", "hello", time.Unix(123, 0))
|
|
qt.t.Logf("fileRef = %q", fileRef)
|
|
p1 := id.NewPlannedPermanode("1")
|
|
id.SetAttribute(p1, "camliContent", fileRef.String())
|
|
|
|
fileRef2, _ := id.UploadFile("other-file", "hellooooo", time.Unix(456, 0))
|
|
qt.t.Logf("fileRef2 = %q", fileRef2)
|
|
p2 := id.NewPlannedPermanode("2")
|
|
id.SetAttribute(p2, "camliContent", fileRef2.String())
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "camliContent",
|
|
ValueInSet: &Constraint{
|
|
File: &FileConstraint{
|
|
FileName: &StringConstraint{
|
|
Contains: "-stuff",
|
|
},
|
|
FileSize: &IntConstraint{
|
|
Max: 5,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p1)
|
|
})
|
|
}
|
|
|
|
func TestQueryPermanodeModtime(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
// indextest advances time one second per operation:
|
|
p1 := id.NewPlannedPermanode("1")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
p3 := id.NewPlannedPermanode("3")
|
|
id.SetAttribute(p1, "someAttr", "value1") // 2011-11-28 01:32:37.000123456 +0000 UTC 1322443957
|
|
id.SetAttribute(p2, "someAttr", "value2") // 2011-11-28 01:32:38.000123456 +0000 UTC 1322443958
|
|
id.SetAttribute(p3, "someAttr", "value3") // 2011-11-28 01:32:39.000123456 +0000 UTC 1322443959
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
ModTime: &TimeConstraint{
|
|
After: types.Time3339(time.Unix(1322443957, 456789)),
|
|
Before: types.Time3339(time.Unix(1322443959, 0)),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p2)
|
|
})
|
|
}
|
|
|
|
// This really belongs in pkg/index for the index-vs-corpus tests, but
|
|
// it's easier here for now.
|
|
// TODO: make all the indextest/tests.go
|
|
// also test the three memory build modes that testQuery does.
|
|
func TestDecodeFileInfo(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
fileRef, _ := id.UploadFile("file.gif", "GIF87afoo", time.Unix(456, 0))
|
|
res, err := qt.Handler().Describe(&DescribeRequest{
|
|
BlobRef: fileRef,
|
|
})
|
|
if err != nil {
|
|
qt.t.Error(err)
|
|
return
|
|
}
|
|
db := res.Meta[fileRef.String()]
|
|
if db == nil {
|
|
qt.t.Error("DescribedBlob missing")
|
|
return
|
|
}
|
|
if db.File == nil {
|
|
qt.t.Error("DescribedBlob.File is nil")
|
|
return
|
|
}
|
|
if db.File.MIMEType != "image/gif" {
|
|
qt.t.Errorf("DescribedBlob.File = %+v; mime type is not image/gif", db.File)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestQueryRecentPermanodes(t *testing.T) {
|
|
// TODO: care about classic (allIndexTypes) too?
|
|
testQueryTypes(t, memIndexTypes, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
p1 := id.NewPlannedPermanode("1")
|
|
id.SetAttribute(p1, "foo", "p1")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
id.SetAttribute(p2, "foo", "p2")
|
|
p3 := id.NewPlannedPermanode("3")
|
|
id.SetAttribute(p3, "foo", "p3")
|
|
|
|
var usedSource string
|
|
ExportSetCandidateSourceHook(func(s string) {
|
|
usedSource = s
|
|
})
|
|
|
|
req := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{},
|
|
},
|
|
Limit: 2,
|
|
Sort: UnspecifiedSort,
|
|
Describe: &DescribeRequest{},
|
|
}
|
|
handler := qt.Handler()
|
|
res, err := handler.Query(req)
|
|
if err != nil {
|
|
qt.t.Fatal(err)
|
|
}
|
|
if usedSource != "corpus_permanode_lastmod" {
|
|
t.Errorf("used candidate source strategy %q; want corpus_permanode_desc", usedSource)
|
|
}
|
|
wantBlobs := []*SearchResultBlob{
|
|
{Blob: p3},
|
|
{Blob: p2},
|
|
}
|
|
if !reflect.DeepEqual(res.Blobs, wantBlobs) {
|
|
gotj, wantj := prettyJSON(res.Blobs), prettyJSON(wantBlobs)
|
|
t.Errorf("Got blobs:\n%s\nWant:\n%s\n", gotj, wantj)
|
|
}
|
|
if got := len(res.Describe.Meta); got != 2 {
|
|
t.Errorf("got %d described blobs; want 2", got)
|
|
}
|
|
|
|
// And test whether continue (for infinite scroll) works:
|
|
{
|
|
if got, want := res.Continue, "pn:1322443958000123456:sha1-fbb5be10fcb4c88d32cfdddb20a7b8d13e9ba284"; got != want {
|
|
t.Fatalf("Continue token = %q; want %q", got, want)
|
|
}
|
|
req := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{},
|
|
},
|
|
Limit: 2,
|
|
Sort: UnspecifiedSort,
|
|
Continue: res.Continue,
|
|
}
|
|
res, err := handler.Query(req)
|
|
if err != nil {
|
|
qt.t.Fatal(err)
|
|
}
|
|
wantBlobs := []*SearchResultBlob{{Blob: p1}}
|
|
if !reflect.DeepEqual(res.Blobs, wantBlobs) {
|
|
gotj, wantj := prettyJSON(res.Blobs), prettyJSON(wantBlobs)
|
|
t.Errorf("After scroll, got blobs:\n%s\nWant:\n%s\n", gotj, wantj)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Tests the continue token on recent permanodes, notably when the
|
|
// page limit truncates in the middle of a bunch of permanodes with the
|
|
// same modtime.
|
|
func TestQueryRecentPermanodes_Continue(t *testing.T) {
|
|
testQueryTypes(t, memIndexTypes, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
var blobs []blob.Ref
|
|
for i := 1; i <= 4; i++ {
|
|
pn := id.NewPlannedPermanode(fmt.Sprint(i))
|
|
blobs = append(blobs, pn)
|
|
t.Logf("permanode %d is %v", i, pn)
|
|
id.SetAttribute_NoTimeMove(pn, "foo", "bar")
|
|
}
|
|
sort.Sort(blob.ByRef(blobs))
|
|
for i, br := range blobs {
|
|
t.Logf("Sorted %d = %v", i, br)
|
|
}
|
|
handler := qt.Handler()
|
|
|
|
contToken := ""
|
|
tests := [][]blob.Ref{
|
|
[]blob.Ref{blobs[3], blobs[2]},
|
|
[]blob.Ref{blobs[1], blobs[0]},
|
|
[]blob.Ref{},
|
|
}
|
|
|
|
for i, wantBlobs := range tests {
|
|
req := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{},
|
|
},
|
|
Limit: 2,
|
|
Sort: UnspecifiedSort,
|
|
Continue: contToken,
|
|
}
|
|
res, err := handler.Query(req)
|
|
if err != nil {
|
|
qt.t.Fatalf("Error on query %d: %v", i+1, err)
|
|
}
|
|
t.Logf("Query %d/%d: continue = %q", i+1, len(tests), res.Continue)
|
|
for i, sb := range res.Blobs {
|
|
t.Logf(" res[%d]: %v", i, sb.Blob)
|
|
}
|
|
|
|
var want []*SearchResultBlob
|
|
for _, br := range wantBlobs {
|
|
want = append(want, &SearchResultBlob{Blob: br})
|
|
}
|
|
if !reflect.DeepEqual(res.Blobs, want) {
|
|
gotj, wantj := prettyJSON(res.Blobs), prettyJSON(want)
|
|
t.Fatalf("Query %d: Got blobs:\n%s\nWant:\n%s\n", i+1, gotj, wantj)
|
|
}
|
|
contToken = res.Continue
|
|
haveToken := contToken != ""
|
|
wantHaveToken := (i + 1) < len(tests)
|
|
if haveToken != wantHaveToken {
|
|
t.Fatalf("Query %d: token = %q; want token = %v", i+1, contToken, wantHaveToken)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Tests continue token hitting the end mid-page.
|
|
func TestQueryRecentPermanodes_ContinueEndMidPage(t *testing.T) {
|
|
testQueryTypes(t, memIndexTypes, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
var blobs []blob.Ref
|
|
for i := 1; i <= 3; i++ {
|
|
pn := id.NewPlannedPermanode(fmt.Sprint(i))
|
|
blobs = append(blobs, pn)
|
|
t.Logf("permanode %d is %v", i, pn)
|
|
id.SetAttribute_NoTimeMove(pn, "foo", "bar")
|
|
}
|
|
sort.Sort(blob.ByRef(blobs))
|
|
for i, br := range blobs {
|
|
t.Logf("Sorted %d = %v", i, br)
|
|
}
|
|
handler := qt.Handler()
|
|
|
|
contToken := ""
|
|
tests := [][]blob.Ref{
|
|
[]blob.Ref{blobs[2], blobs[1]},
|
|
[]blob.Ref{blobs[0]},
|
|
}
|
|
|
|
for i, wantBlobs := range tests {
|
|
req := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{},
|
|
},
|
|
Limit: 2,
|
|
Sort: UnspecifiedSort,
|
|
Continue: contToken,
|
|
}
|
|
res, err := handler.Query(req)
|
|
if err != nil {
|
|
qt.t.Fatalf("Error on query %d: %v", i+1, err)
|
|
}
|
|
t.Logf("Query %d/%d: continue = %q", i+1, len(tests), res.Continue)
|
|
for i, sb := range res.Blobs {
|
|
t.Logf(" res[%d]: %v", i, sb.Blob)
|
|
}
|
|
|
|
var want []*SearchResultBlob
|
|
for _, br := range wantBlobs {
|
|
want = append(want, &SearchResultBlob{Blob: br})
|
|
}
|
|
if !reflect.DeepEqual(res.Blobs, want) {
|
|
gotj, wantj := prettyJSON(res.Blobs), prettyJSON(want)
|
|
t.Fatalf("Query %d: Got blobs:\n%s\nWant:\n%s\n", i+1, gotj, wantj)
|
|
}
|
|
contToken = res.Continue
|
|
haveToken := contToken != ""
|
|
wantHaveToken := (i + 1) < len(tests)
|
|
if haveToken != wantHaveToken {
|
|
t.Fatalf("Query %d: token = %q; want token = %v", i+1, contToken, wantHaveToken)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Tests PermanodeConstraint.ValueAll
|
|
func TestQueryPermanodeValueAll(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
p1 := id.NewPlannedPermanode("1")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
id.SetAttribute(p1, "attr", "foo")
|
|
id.SetAttribute(p1, "attr", "barrrrr")
|
|
id.SetAttribute(p2, "attr", "foo")
|
|
id.SetAttribute(p2, "attr", "bar")
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "attr",
|
|
ValueAll: true,
|
|
ValueMatches: &StringConstraint{
|
|
ByteLength: &IntConstraint{
|
|
Min: 3,
|
|
Max: 3,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p2)
|
|
})
|
|
}
|
|
|
|
// Tests PermanodeConstraint.ValueMatches.CaseInsensitive.
|
|
func TestQueryPermanodeValueMatchesCaseInsensitive(t *testing.T) {
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
p1 := id.NewPlannedPermanode("1")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
|
|
id.SetAttribute(p1, "x", "Foo")
|
|
id.SetAttribute(p2, "x", "start")
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Logical: &LogicalConstraint{
|
|
Op: "or",
|
|
|
|
A: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "x",
|
|
ValueMatches: &StringConstraint{
|
|
Equals: "foo",
|
|
CaseInsensitive: true,
|
|
},
|
|
},
|
|
},
|
|
|
|
B: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "x",
|
|
ValueMatches: &StringConstraint{
|
|
Contains: "TAR",
|
|
CaseInsensitive: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, p1, p2)
|
|
})
|
|
}
|
|
|
|
// permanodes tagged "foo" or those in sets where the parent
|
|
// permanode set itself is tagged "foo".
|
|
func TestQueryPermanodeTaggedViaParent(t *testing.T) {
|
|
t.Skip("TODO: finish implementing")
|
|
|
|
testQuery(t, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
ptagged := id.NewPlannedPermanode("tagged_photo")
|
|
pindirect := id.NewPlannedPermanode("via_parent")
|
|
pset := id.NewPlannedPermanode("set")
|
|
pboth := id.NewPlannedPermanode("both") // funny directly and via its parent
|
|
pnotfunny := id.NewPlannedPermanode("not_funny")
|
|
|
|
id.SetAttribute(ptagged, "tag", "funny")
|
|
id.SetAttribute(pset, "tag", "funny")
|
|
id.SetAttribute(pboth, "tag", "funny")
|
|
id.AddAttribute(pset, "camliMember", pindirect.String())
|
|
id.AddAttribute(pset, "camliMember", pboth.String())
|
|
id.SetAttribute(pnotfunny, "tag", "boring")
|
|
|
|
sq := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Logical: &LogicalConstraint{
|
|
Op: "or",
|
|
|
|
// Those tagged funny directly:
|
|
A: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "tag",
|
|
Value: "funny",
|
|
},
|
|
},
|
|
|
|
// Those tagged funny indirectly:
|
|
B: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Relation: &RelationConstraint{
|
|
Relation: "ancestor",
|
|
Any: &Constraint{
|
|
Permanode: &PermanodeConstraint{
|
|
Attr: "tag",
|
|
Value: "funny",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
qt.wantRes(sq, ptagged, pset, pboth, pindirect)
|
|
})
|
|
}
|
|
|
|
func TestLimitDoesntDeadlock(t *testing.T) {
|
|
// TODO: care about classic (allIndexTypes) too?
|
|
testQueryTypes(t, memIndexTypes, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
const limit = 2
|
|
for i := 0; i < ExportBufferedConst()+limit+1; i++ {
|
|
pn := id.NewPlannedPermanode(fmt.Sprint(i))
|
|
id.SetAttribute(pn, "foo", "bar")
|
|
}
|
|
|
|
req := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{},
|
|
},
|
|
Limit: limit,
|
|
Sort: UnspecifiedSort,
|
|
Describe: &DescribeRequest{},
|
|
}
|
|
h := qt.Handler()
|
|
gotRes := make(chan bool, 1)
|
|
go func() {
|
|
_, err := h.Query(req)
|
|
if err != nil {
|
|
qt.t.Error(err)
|
|
}
|
|
gotRes <- true
|
|
}()
|
|
select {
|
|
case <-gotRes:
|
|
case <-time.After(5 * time.Second):
|
|
t.Error("timeout; deadlock?")
|
|
}
|
|
})
|
|
}
|
|
|
|
func prettyJSON(v interface{}) string {
|
|
b, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func TestPlannedQuery(t *testing.T) {
|
|
tests := []struct {
|
|
in, want *SearchQuery
|
|
}{
|
|
{
|
|
in: &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{},
|
|
},
|
|
},
|
|
want: &SearchQuery{
|
|
Sort: LastModifiedDesc,
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{},
|
|
},
|
|
Limit: 200,
|
|
},
|
|
},
|
|
}
|
|
for i, tt := range tests {
|
|
got := tt.in.ExportPlannedQuery()
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("%d. for input:\n%s\ngot:\n%s\nwant:\n%s\n", i,
|
|
prettyJSON(tt.in), prettyJSON(got), prettyJSON(tt.want))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDescribeMarshal(t *testing.T) {
|
|
// Empty Describe
|
|
q := &SearchQuery{
|
|
Describe: &DescribeRequest{},
|
|
}
|
|
enc, err := json.Marshal(q)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := string(enc), `{"describe":{"blobref":null,"at":null}}`; got != want {
|
|
t.Errorf("JSON: %s; want %s", got, want)
|
|
}
|
|
back := &SearchQuery{}
|
|
err = json.Unmarshal(enc, back)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(q, back) {
|
|
t.Errorf("Didn't round-trip. Got %#v; want %#v", back, q)
|
|
}
|
|
|
|
// DescribeRequest with multiple blobref
|
|
q = &SearchQuery{
|
|
Describe: &DescribeRequest{
|
|
BlobRefs: []blob.Ref{blob.MustParse("sha-1234"), blob.MustParse("sha-abcd")},
|
|
},
|
|
}
|
|
enc, err = json.Marshal(q)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := string(enc), `{"describe":{"blobrefs":["sha-1234","sha-abcd"],"blobref":null,"at":null}}`; got != want {
|
|
t.Errorf("JSON: %s; want %s", got, want)
|
|
}
|
|
back = &SearchQuery{}
|
|
err = json.Unmarshal(enc, back)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(q, back) {
|
|
t.Errorf("Didn't round-trip. Got %#v; want %#v", back, q)
|
|
}
|
|
|
|
// and the zero value
|
|
q = &SearchQuery{}
|
|
enc, err = json.Marshal(q)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(enc) != "{}" {
|
|
t.Errorf(`Zero value: %q; want null`, enc)
|
|
}
|
|
}
|
|
|
|
func TestSortMarshal(t *testing.T) {
|
|
q := &SearchQuery{
|
|
Sort: CreatedDesc,
|
|
}
|
|
enc, err := json.Marshal(q)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := string(enc), `{"sort":"-created"}`; got != want {
|
|
t.Errorf("JSON: %s; want %s", got, want)
|
|
}
|
|
back := &SearchQuery{}
|
|
err = json.Unmarshal(enc, back)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(q, back) {
|
|
t.Errorf("Didn't round-trip. Got %#v; want %#v", back, q)
|
|
}
|
|
|
|
// and the zero value
|
|
q = &SearchQuery{}
|
|
enc, err = json.Marshal(q)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(enc) != "{}" {
|
|
t.Errorf("Zero value: %s; want {}", enc)
|
|
}
|
|
}
|
|
|
|
func BenchmarkQueryRecentPermanodes(b *testing.B) {
|
|
b.ReportAllocs()
|
|
testQueryTypes(b, corpusTypeOnly, func(qt *queryTest) {
|
|
id := qt.id
|
|
|
|
p1 := id.NewPlannedPermanode("1")
|
|
id.SetAttribute(p1, "foo", "p1")
|
|
p2 := id.NewPlannedPermanode("2")
|
|
id.SetAttribute(p2, "foo", "p2")
|
|
p3 := id.NewPlannedPermanode("3")
|
|
id.SetAttribute(p3, "foo", "p3")
|
|
|
|
req := &SearchQuery{
|
|
Constraint: &Constraint{
|
|
Permanode: &PermanodeConstraint{},
|
|
},
|
|
Limit: 2,
|
|
Sort: UnspecifiedSort,
|
|
Describe: &DescribeRequest{},
|
|
}
|
|
|
|
h := qt.Handler()
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
*req.Describe = DescribeRequest{}
|
|
_, err := h.Query(req)
|
|
if err != nil {
|
|
qt.t.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
}
|