Unverified Commit d0ca9bc3 authored by Steven Allen's avatar Steven Allen Committed by GitHub

Merge pull request #136 from ipfs/feat/test

Add a large test suite
parents 04313878 113fe4cb
......@@ -4,7 +4,7 @@ os:
language: go
go:
- 1.11.x
- 1.12.x
env:
global:
......
......@@ -9,3 +9,5 @@ require (
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)
go 1.12
......@@ -224,7 +224,6 @@ func (d *Datastore) Delete(key ds.Key) error {
func (d *Datastore) Query(master query.Query) (query.Results, error) {
childQuery := query.Query{
Prefix: master.Prefix,
Limit: master.Limit,
Orders: master.Orders,
KeysOnly: master.KeysOnly,
ReturnExpirations: master.ReturnExpirations,
......@@ -254,7 +253,7 @@ func (d *Datastore) Query(master query.Query) (query.Results, error) {
queries.addResults(mount, results)
}
qr := query.ResultsFromIterator(childQuery, query.Iterator{
qr := query.ResultsFromIterator(master, query.Iterator{
Next: queries.next,
Close: queries.close,
})
......@@ -269,8 +268,8 @@ func (d *Datastore) Query(master query.Query) (query.Results, error) {
qr = query.NaiveOffset(qr, master.Offset)
}
if childQuery.Limit > 0 {
qr = query.NaiveLimit(qr, childQuery.Limit)
if master.Limit > 0 {
qr = query.NaiveLimit(qr, master.Limit)
}
return qr, nil
......
......@@ -57,6 +57,10 @@ func (f FilterValueCompare) Filter(e Entry) bool {
}
}
func (f FilterValueCompare) String() string {
return fmt.Sprintf("VALUE %s %q", f.Op, string(f.Value))
}
type FilterKeyCompare struct {
Op Op
Key string
......@@ -81,6 +85,10 @@ func (f FilterKeyCompare) Filter(e Entry) bool {
}
}
func (f FilterKeyCompare) String() string {
return fmt.Sprintf("KEY %s %q", f.Op, f.Key)
}
type FilterKeyPrefix struct {
Prefix string
}
......@@ -88,3 +96,7 @@ type FilterKeyPrefix struct {
func (f FilterKeyPrefix) Filter(e Entry) bool {
return strings.HasPrefix(e.Key, f.Prefix)
}
func (f FilterKeyPrefix) String() string {
return fmt.Sprintf("PREFIX(%q)", f.Prefix)
}
......@@ -18,6 +18,10 @@ func (o OrderByFunction) Compare(a, b Entry) int {
return o(a, b)
}
func (OrderByFunction) String() string {
return "FN"
}
// OrderByValue is used to signal to datastores they should apply internal
// orderings.
type OrderByValue struct{}
......@@ -26,6 +30,10 @@ func (o OrderByValue) Compare(a, b Entry) int {
return bytes.Compare(a.Value, b.Value)
}
func (OrderByValue) String() string {
return "VALUE"
}
// OrderByValueDescending is used to signal to datastores they
// should apply internal orderings.
type OrderByValueDescending struct{}
......@@ -34,6 +42,10 @@ func (o OrderByValueDescending) Compare(a, b Entry) int {
return -bytes.Compare(a.Value, b.Value)
}
func (OrderByValueDescending) String() string {
return "desc(VALUE)"
}
// OrderByKey
type OrderByKey struct{}
......@@ -41,6 +53,10 @@ func (o OrderByKey) Compare(a, b Entry) int {
return strings.Compare(a.Key, b.Key)
}
func (OrderByKey) String() string {
return "KEY"
}
// OrderByKeyDescending
type OrderByKeyDescending struct{}
......@@ -48,6 +64,10 @@ func (o OrderByKeyDescending) Compare(a, b Entry) int {
return -strings.Compare(a.Key, b.Key)
}
func (OrderByKeyDescending) String() string {
return "desc(KEY)"
}
// Less returns true if a comes before b with the requested orderings.
func Less(orders []Order, a, b Entry) bool {
for _, cmp := range orders {
......
package query
import (
"fmt"
"time"
goprocess "github.com/jbenet/goprocess"
......@@ -67,6 +68,48 @@ type Query struct {
ReturnExpirations bool // return expirations (see TTLDatastore)
}
func (q Query) String() string {
s := "SELECT keys"
if !q.KeysOnly {
s += ",vals"
}
if q.ReturnExpirations {
s += ",exps"
}
s += " "
if q.Prefix != "" {
s += fmt.Sprintf("FROM %q ", q.Prefix)
}
if len(q.Filters) > 0 {
s += fmt.Sprintf("FILTER [%s", q.Filters[0])
for _, f := range q.Filters[1:] {
s += fmt.Sprintf(", %s", f)
}
s += "] "
}
if len(q.Orders) > 0 {
s += fmt.Sprintf("ORDER [%s", q.Orders[0])
for _, f := range q.Orders[1:] {
s += fmt.Sprintf(", %s", f)
}
s += "] "
}
if q.Offset > 0 {
s += fmt.Sprintf("OFFSET %d ", q.Offset)
}
if q.Limit > 0 {
s += fmt.Sprintf("LIMIT %d ", q.Limit)
}
// Will always end with a space, strip it.
return s[:len(s)-1]
}
// Entry is a query result entry.
type Entry struct {
Key string // cant be ds.Key because circular imports ...!!!
......
......@@ -248,3 +248,65 @@ func getKeysViaChan(rs Results) []string {
}
return ret
}
func TestStringer(t *testing.T) {
q := Query{}
expected := `SELECT keys,vals`
actual := q.String()
if actual != expected {
t.Fatalf("expected\n\t%s\ngot\n\t%s", expected, actual)
}
q.Offset = 10
q.Limit = 10
expected = `SELECT keys,vals OFFSET 10 LIMIT 10`
actual = q.String()
if actual != expected {
t.Fatalf("expected\n\t%s\ngot\n\t%s", expected, actual)
}
q.Orders = []Order{OrderByValue{}, OrderByKey{}}
expected = `SELECT keys,vals ORDER [VALUE, KEY] OFFSET 10 LIMIT 10`
actual = q.String()
if actual != expected {
t.Fatalf("expected\n\t%s\ngot\n\t%s", expected, actual)
}
q.Filters = []Filter{
FilterKeyCompare{Op: GreaterThan, Key: "/foo/bar"},
FilterKeyCompare{Op: LessThan, Key: "/foo/bar"},
}
expected = `SELECT keys,vals FILTER [KEY > "/foo/bar", KEY < "/foo/bar"] ORDER [VALUE, KEY] OFFSET 10 LIMIT 10`
actual = q.String()
if actual != expected {
t.Fatalf("expected\n\t%s\ngot\n\t%s", expected, actual)
}
q.Prefix = "/foo"
expected = `SELECT keys,vals FROM "/foo" FILTER [KEY > "/foo/bar", KEY < "/foo/bar"] ORDER [VALUE, KEY] OFFSET 10 LIMIT 10`
actual = q.String()
if actual != expected {
t.Fatalf("expected\n\t%s\ngot\n\t%s", expected, actual)
}
q.ReturnExpirations = true
expected = `SELECT keys,vals,exps FROM "/foo" FILTER [KEY > "/foo/bar", KEY < "/foo/bar"] ORDER [VALUE, KEY] OFFSET 10 LIMIT 10`
actual = q.String()
if actual != expected {
t.Fatalf("expected\n\t%s\ngot\n\t%s", expected, actual)
}
q.KeysOnly = true
expected = `SELECT keys,exps FROM "/foo" FILTER [KEY > "/foo/bar", KEY < "/foo/bar"] ORDER [VALUE, KEY] OFFSET 10 LIMIT 10`
actual = q.String()
if actual != expected {
t.Fatalf("expected\n\t%s\ngot\n\t%s", expected, actual)
}
q.ReturnExpirations = false
expected = `SELECT keys FROM "/foo" FILTER [KEY > "/foo/bar", KEY < "/foo/bar"] ORDER [VALUE, KEY] OFFSET 10 LIMIT 10`
actual = q.String()
if actual != expected {
t.Fatalf("expected\n\t%s\ngot\n\t%s", expected, actual)
}
}
......@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"math/rand"
"reflect"
"strings"
"testing"
......@@ -123,6 +124,30 @@ func SubtestNotFounds(t *testing.T, ds dstore.Datastore) {
}
}
func SubtestLimit(t *testing.T, ds dstore.Datastore) {
test := func(offset, limit int) {
t.Run(fmt.Sprintf("Slice/%d/%d", offset, limit), func(t *testing.T) {
subtestQuery(t, ds, dsq.Query{
Orders: []dsq.Order{dsq.OrderByKey{}},
Offset: offset,
Limit: limit,
KeysOnly: true,
}, 100)
})
}
test(0, 10)
test(0, 0)
test(10, 0)
test(10, 10)
test(10, 20)
test(50, 20)
test(99, 20)
test(200, 20)
test(200, 0)
test(99, 0)
test(95, 0)
}
func SubtestOrder(t *testing.T, ds dstore.Datastore) {
test := func(orders ...dsq.Order) {
var types []string
......@@ -133,19 +158,7 @@ func SubtestOrder(t *testing.T, ds dstore.Datastore) {
t.Run(name, func(t *testing.T) {
subtestQuery(t, ds, dsq.Query{
Orders: orders,
}, func(t *testing.T, input, output []dsq.Entry) {
if len(input) != len(output) {
t.Fatal("got wrong number of keys back")
}
dsq.Sort(orders, input)
for i, e := range output {
if input[i].Key != e.Key {
t.Fatalf("in key output, got %s but expected %s", e.Key, input[i].Key)
}
}
})
}, 100)
})
}
test(dsq.OrderByKey{})
......@@ -160,20 +173,7 @@ func SubtestOrder(t *testing.T, ds dstore.Datastore) {
}
func SubtestManyKeysAndQuery(t *testing.T, ds dstore.Datastore) {
subtestQuery(t, ds, dsq.Query{KeysOnly: true}, func(t *testing.T, input, output []dsq.Entry) {
if len(input) != len(output) {
t.Fatal("got wrong number of keys back")
}
dsq.Sort([]dsq.Order{dsq.OrderByKey{}}, input)
dsq.Sort([]dsq.Order{dsq.OrderByKey{}}, output)
for i, e := range output {
if input[i].Key != e.Key {
t.Fatalf("in key output, got %s but expected %s", e.Key, input[i].Key)
}
}
})
subtestQuery(t, ds, dsq.Query{KeysOnly: true}, 100)
}
// need a custom test filter to test the "fallback" filter case for unknown
......@@ -184,6 +184,79 @@ func (testFilter) Filter(e dsq.Entry) bool {
return len(e.Key)%2 == 0
}
func SubtestCombinations(t *testing.T, ds dstore.Datastore) {
offsets := []int{
0,
10,
95,
100,
}
limits := []int{
0,
1,
10,
100,
}
filters := [][]dsq.Filter{
{dsq.FilterKeyCompare{
Op: dsq.Equal,
Key: "/0key0",
}},
{dsq.FilterKeyCompare{
Op: dsq.LessThan,
Key: "/2",
}},
}
orders := [][]dsq.Order{
{dsq.OrderByKey{}},
{dsq.OrderByKeyDescending{}},
{dsq.OrderByValue{}, dsq.OrderByKey{}},
{dsq.OrderByFunction(func(a, b dsq.Entry) int { return bytes.Compare(a.Value, b.Value) })},
}
lengths := []int{
0,
1,
100,
}
perms(
func(perm []int) {
q := dsq.Query{
Offset: offsets[perm[0]],
Limit: limits[perm[1]],
Filters: filters[perm[2]],
Orders: orders[perm[3]],
}
length := lengths[perm[4]]
t.Run(strings.ReplaceAll(fmt.Sprintf("%d/{%s}", length, q), " ", "·"), func(t *testing.T) {
subtestQuery(t, ds, q, length)
})
},
len(offsets),
len(limits),
len(filters),
len(orders),
len(lengths),
)
}
func perms(cb func([]int), ops ...int) {
current := make([]int, len(ops))
outer:
for {
for i := range current {
if current[i] < (ops[i] - 1) {
current[i]++
cb(current)
continue outer
}
current[i] = 0
}
// out of permutations
return
}
}
func SubtestFilter(t *testing.T, ds dstore.Datastore) {
test := func(filters ...dsq.Filter) {
var types []string
......@@ -194,31 +267,7 @@ func SubtestFilter(t *testing.T, ds dstore.Datastore) {
t.Run(name, func(t *testing.T) {
subtestQuery(t, ds, dsq.Query{
Filters: filters,
}, func(t *testing.T, input, output []dsq.Entry) {
var exp []dsq.Entry
input:
for _, e := range input {
for _, f := range filters {
if !f.Filter(e) {
continue input
}
}
exp = append(exp, e)
}
if len(exp) != len(output) {
t.Fatalf("got wrong number of keys back: expected %d, got %d", len(exp), len(output))
}
dsq.Sort([]dsq.Order{dsq.OrderByKey{}}, exp)
dsq.Sort([]dsq.Order{dsq.OrderByKey{}}, output)
for i, e := range output {
if exp[i].Key != e.Key {
t.Fatalf("in key output, got %s but expected %s", e.Key, exp[i].Key)
}
}
})
}, 100)
})
}
test(dsq.FilterKeyCompare{
......@@ -258,9 +307,8 @@ func randValue() []byte {
return value
}
func subtestQuery(t *testing.T, ds dstore.Datastore, q dsq.Query, check func(t *testing.T, input, output []dsq.Entry)) {
func subtestQuery(t *testing.T, ds dstore.Datastore, q dsq.Query, count int) {
var input []dsq.Entry
count := 100
for i := 0; i < count; i++ {
s := fmt.Sprintf("%dkey%d", i, i)
key := dstore.NewKey(s).String()
......@@ -297,14 +345,38 @@ func subtestQuery(t *testing.T, ds dstore.Datastore, q dsq.Query, check func(t *
t.Fatal("calling query: ", err)
}
if rq := resp.Query(); !reflect.DeepEqual(rq, q) {
t.Errorf("returned query\n %s\nexpected query\n %s", &rq, q)
}
t.Log("aggregating query results")
output, err := resp.Rest()
actual, err := resp.Rest()
if err != nil {
t.Fatal("query result error: ", err)
}
t.Log("verifying query output")
check(t, input, output)
expected, err := dsq.NaiveQueryApply(q, dsq.ResultsWithEntries(q, input)).Rest()
if err != nil {
t.Fatal("naive query error: ", err)
}
if len(actual) != len(expected) {
t.Fatalf("expected %d results, got %d", len(expected), len(actual))
}
if len(q.Orders) == 0 {
dsq.Sort([]dsq.Order{dsq.OrderByKey{}}, actual)
dsq.Sort([]dsq.Order{dsq.OrderByKey{}}, expected)
}
for i := range actual {
if actual[i].Key != expected[i].Key {
t.Errorf("for result %d, expected key %q, got %q", i, expected[i].Key, actual[i].Key)
continue
}
if !q.KeysOnly && !bytes.Equal(actual[i].Value, expected[i].Value) {
t.Errorf("value mismatch for result %d (key=%q)", i, expected[i].Key)
}
}
t.Log("deleting all keys")
for _, e := range input {
......
......@@ -13,7 +13,9 @@ import (
var BasicSubtests = []func(t *testing.T, ds dstore.Datastore){
SubtestBasicPutGet,
SubtestNotFounds,
SubtestCombinations,
SubtestOrder,
SubtestLimit,
SubtestFilter,
SubtestManyKeysAndQuery,
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment