@ -1,10 +0,0 @@ | |||
gen_query_parser: | |||
go generate . | |||
fuzzy_test: | |||
go get -u -v github.com/dvyukov/go-fuzz/go-fuzz | |||
go get -u -v github.com/dvyukov/go-fuzz/go-fuzz-build | |||
go-fuzz-build github.com/tendermint/tendermint/libs/pubsub/query/fuzz_test | |||
go-fuzz -bin=./fuzz_test-fuzz.zip -workdir=./fuzz_test/output | |||
.PHONY: gen_query_parser fuzzy_test |
@ -1,18 +0,0 @@ | |||
package query | |||
import ( | |||
"github.com/tendermint/tendermint/abci/types" | |||
) | |||
// Empty query matches any set of events. | |||
type Empty struct { | |||
} | |||
// Matches always returns true. | |||
func (Empty) Matches(events []types.Event) (bool, error) { | |||
return true, nil | |||
} | |||
func (Empty) String() string { | |||
return "empty" | |||
} |
@ -1,55 +0,0 @@ | |||
package query_test | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/require" | |||
abci "github.com/tendermint/tendermint/abci/types" | |||
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery" | |||
) | |||
func TestEmptyQueryMatchesAnything(t *testing.T) { | |||
q := query.Empty{} | |||
testCases := []struct { | |||
events []abci.Event | |||
}{ | |||
{ | |||
[]abci.Event{}, | |||
}, | |||
{ | |||
[]abci.Event{ | |||
{ | |||
Type: "Asher", | |||
Attributes: []abci.EventAttribute{{Key: "Roth"}}, | |||
}, | |||
}, | |||
}, | |||
{ | |||
[]abci.Event{ | |||
{ | |||
Type: "Route", | |||
Attributes: []abci.EventAttribute{{Key: "66"}}, | |||
}, | |||
}, | |||
}, | |||
{ | |||
[]abci.Event{ | |||
{ | |||
Type: "Route", | |||
Attributes: []abci.EventAttribute{{Key: "66"}}, | |||
}, | |||
{ | |||
Type: "Billy", | |||
Attributes: []abci.EventAttribute{{Key: "Blue"}}, | |||
}, | |||
}, | |||
}, | |||
} | |||
for _, tc := range testCases { | |||
match, err := q.Matches(tc.events) | |||
require.Nil(t, err) | |||
require.True(t, match) | |||
} | |||
} |
@ -1,30 +0,0 @@ | |||
package fuzz_test | |||
import ( | |||
"fmt" | |||
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery" | |||
) | |||
func Fuzz(data []byte) int { | |||
sdata := string(data) | |||
q0, err := query.New(sdata) | |||
if err != nil { | |||
return 0 | |||
} | |||
sdata1 := q0.String() | |||
q1, err := query.New(sdata1) | |||
if err != nil { | |||
panic(err) | |||
} | |||
sdata2 := q1.String() | |||
if sdata1 != sdata2 { | |||
fmt.Printf("q0: %q\n", sdata1) | |||
fmt.Printf("q1: %q\n", sdata2) | |||
panic("query changed") | |||
} | |||
return 1 | |||
} |
@ -1,97 +0,0 @@ | |||
package query_test | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery" | |||
) | |||
// TODO: fuzzy testing? | |||
func TestParser(t *testing.T) { | |||
cases := []struct { | |||
query string | |||
valid bool | |||
}{ | |||
{"tm.events.type='NewBlock'", true}, | |||
{"tm.events.type = 'NewBlock'", true}, | |||
{"tm.events.name = ''", true}, | |||
{"tm.events.type='TIME'", true}, | |||
{"tm.events.type='DATE'", true}, | |||
{"tm.events.type='='", true}, | |||
{"tm.events.type='TIME", false}, | |||
{"tm.events.type=TIME'", false}, | |||
{"tm.events.type==", false}, | |||
{"tm.events.type=NewBlock", false}, | |||
{">==", false}, | |||
{"tm.events.type 'NewBlock' =", false}, | |||
{"tm.events.type>'NewBlock'", false}, | |||
{"", false}, | |||
{"=", false}, | |||
{"='NewBlock'", false}, | |||
{"tm.events.type=", false}, | |||
{"tm.events.typeNewBlock", false}, | |||
{"tm.events.type'NewBlock'", false}, | |||
{"'NewBlock'", false}, | |||
{"NewBlock", false}, | |||
{"", false}, | |||
{"tm.events.type='NewBlock' AND abci.account.name='Igor'", true}, | |||
{"tm.events.type='NewBlock' AND", false}, | |||
{"tm.events.type='NewBlock' AN", false}, | |||
{"tm.events.type='NewBlock' AN tm.events.type='NewBlockHeader'", false}, | |||
{"AND tm.events.type='NewBlock' ", false}, | |||
{"abci.account.name CONTAINS 'Igor'", true}, | |||
{"tx.date > DATE 2013-05-03", true}, | |||
{"tx.date < DATE 2013-05-03", true}, | |||
{"tx.date <= DATE 2013-05-03", true}, | |||
{"tx.date >= DATE 2013-05-03", true}, | |||
{"tx.date >= DAT 2013-05-03", false}, | |||
{"tx.date <= DATE2013-05-03", false}, | |||
{"tx.date <= DATE -05-03", false}, | |||
{"tx.date >= DATE 20130503", false}, | |||
{"tx.date >= DATE 2013+01-03", false}, | |||
// incorrect year, month, day | |||
{"tx.date >= DATE 0013-01-03", false}, | |||
{"tx.date >= DATE 2013-31-03", false}, | |||
{"tx.date >= DATE 2013-01-83", false}, | |||
{"tx.date > TIME 2013-05-03T14:45:00+07:00", true}, | |||
{"tx.date < TIME 2013-05-03T14:45:00-02:00", true}, | |||
{"tx.date <= TIME 2013-05-03T14:45:00Z", true}, | |||
{"tx.date >= TIME 2013-05-03T14:45:00Z", true}, | |||
{"tx.date >= TIME2013-05-03T14:45:00Z", false}, | |||
{"tx.date = IME 2013-05-03T14:45:00Z", false}, | |||
{"tx.date = TIME 2013-05-:45:00Z", false}, | |||
{"tx.date >= TIME 2013-05-03T14:45:00", false}, | |||
{"tx.date >= TIME 0013-00-00T14:45:00Z", false}, | |||
{"tx.date >= TIME 2013+05=03T14:45:00Z", false}, | |||
{"account.balance=100", true}, | |||
{"account.balance >= 200", true}, | |||
{"account.balance >= -300", false}, | |||
{"account.balance >>= 400", false}, | |||
{"account.balance=33.22.1", false}, | |||
{"slashing.amount EXISTS", true}, | |||
{"slashing.amount EXISTS AND account.balance=100", true}, | |||
{"account.balance=100 AND slashing.amount EXISTS", true}, | |||
{"slashing EXISTS", true}, | |||
{"hash='136E18F7E4C348B780CF873A0BF43922E5BAFA63'", true}, | |||
{"hash=136E18F7E4C348B780CF873A0BF43922E5BAFA63", false}, | |||
} | |||
for _, c := range cases { | |||
_, err := query.New(c.query) | |||
if c.valid { | |||
assert.NoErrorf(t, err, "Query was '%s'", c.query) | |||
} else { | |||
assert.Errorf(t, err, "Query was '%s'", c.query) | |||
} | |||
} | |||
} |
@ -1,3 +0,0 @@ | |||
package query | |||
//go:generate go run github.com/pointlander/peg@v1.0.0 -inline -switch query.peg |
@ -1,527 +0,0 @@ | |||
// Package query provides a parser for a custom query format: | |||
// | |||
// abci.invoice.number=22 AND abci.invoice.owner=Ivan | |||
// | |||
// See query.peg for the grammar, which is a https://en.wikipedia.org/wiki/Parsing_expression_grammar. | |||
// More: https://github.com/PhilippeSigaud/Pegged/wiki/PEG-Basics | |||
// | |||
// It has a support for numbers (integer and floating point), dates and times. | |||
package query | |||
import ( | |||
"fmt" | |||
"reflect" | |||
"regexp" | |||
"strconv" | |||
"strings" | |||
"time" | |||
"github.com/tendermint/tendermint/abci/types" | |||
) | |||
var ( | |||
numRegex = regexp.MustCompile(`([0-9\.]+)`) | |||
) | |||
// Query holds the query string and the query parser. | |||
type Query struct { | |||
str string | |||
parser *QueryParser | |||
} | |||
// Condition represents a single condition within a query and consists of composite key | |||
// (e.g. "tx.gas"), operator (e.g. "=") and operand (e.g. "7"). | |||
type Condition struct { | |||
CompositeKey string | |||
Op Operator | |||
Operand interface{} | |||
} | |||
// New parses the given string and returns a query or error if the string is | |||
// invalid. | |||
func New(s string) (*Query, error) { | |||
p := &QueryParser{Buffer: fmt.Sprintf(`"%s"`, s)} | |||
p.Init() | |||
if err := p.Parse(); err != nil { | |||
return nil, err | |||
} | |||
return &Query{str: s, parser: p}, nil | |||
} | |||
// MustParse turns the given string into a query or panics; for tests or others | |||
// cases where you know the string is valid. | |||
func MustParse(s string) *Query { | |||
q, err := New(s) | |||
if err != nil { | |||
panic(fmt.Sprintf("failed to parse %s: %v", s, err)) | |||
} | |||
return q | |||
} | |||
// String returns the original string. | |||
func (q *Query) String() string { | |||
return q.str | |||
} | |||
// Operator is an operator that defines some kind of relation between composite key and | |||
// operand (equality, etc.). | |||
type Operator uint8 | |||
const ( | |||
// "<=" | |||
OpLessEqual Operator = iota | |||
// ">=" | |||
OpGreaterEqual | |||
// "<" | |||
OpLess | |||
// ">" | |||
OpGreater | |||
// "=" | |||
OpEqual | |||
// "CONTAINS"; used to check if a string contains a certain sub string. | |||
OpContains | |||
// "EXISTS"; used to check if a certain event attribute is present. | |||
OpExists | |||
) | |||
const ( | |||
// DateLayout defines a layout for all dates (`DATE date`) | |||
DateLayout = "2006-01-02" | |||
// TimeLayout defines a layout for all times (`TIME time`) | |||
TimeLayout = time.RFC3339 | |||
) | |||
// Conditions returns a list of conditions. It returns an error if there is any | |||
// error with the provided grammar in the Query. | |||
func (q *Query) Conditions() ([]Condition, error) { | |||
var ( | |||
eventAttr string | |||
op Operator | |||
) | |||
conditions := make([]Condition, 0) | |||
buffer, begin, end := q.parser.Buffer, 0, 0 | |||
// tokens must be in the following order: tag ("tx.gas") -> operator ("=") -> operand ("7") | |||
for token := range q.parser.Tokens() { | |||
switch token.pegRule { | |||
case rulePegText: | |||
begin, end = int(token.begin), int(token.end) | |||
case ruletag: | |||
eventAttr = buffer[begin:end] | |||
case rulele: | |||
op = OpLessEqual | |||
case rulege: | |||
op = OpGreaterEqual | |||
case rulel: | |||
op = OpLess | |||
case ruleg: | |||
op = OpGreater | |||
case ruleequal: | |||
op = OpEqual | |||
case rulecontains: | |||
op = OpContains | |||
case ruleexists: | |||
op = OpExists | |||
conditions = append(conditions, Condition{eventAttr, op, nil}) | |||
case rulevalue: | |||
// strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock") | |||
valueWithoutSingleQuotes := buffer[begin+1 : end-1] | |||
conditions = append(conditions, Condition{eventAttr, op, valueWithoutSingleQuotes}) | |||
case rulenumber: | |||
number := buffer[begin:end] | |||
if strings.ContainsAny(number, ".") { // if it looks like a floating-point number | |||
value, err := strconv.ParseFloat(number, 64) | |||
if err != nil { | |||
err = fmt.Errorf( | |||
"got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", | |||
err, number, | |||
) | |||
return nil, err | |||
} | |||
conditions = append(conditions, Condition{eventAttr, op, value}) | |||
} else { | |||
value, err := strconv.ParseInt(number, 10, 64) | |||
if err != nil { | |||
err = fmt.Errorf( | |||
"got %v while trying to parse %s as int64 (should never happen if the grammar is correct)", | |||
err, number, | |||
) | |||
return nil, err | |||
} | |||
conditions = append(conditions, Condition{eventAttr, op, value}) | |||
} | |||
case ruletime: | |||
value, err := time.Parse(TimeLayout, buffer[begin:end]) | |||
if err != nil { | |||
err = fmt.Errorf( | |||
"got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", | |||
err, buffer[begin:end], | |||
) | |||
return nil, err | |||
} | |||
conditions = append(conditions, Condition{eventAttr, op, value}) | |||
case ruledate: | |||
value, err := time.Parse("2006-01-02", buffer[begin:end]) | |||
if err != nil { | |||
err = fmt.Errorf( | |||
"got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)", | |||
err, buffer[begin:end], | |||
) | |||
return nil, err | |||
} | |||
conditions = append(conditions, Condition{eventAttr, op, value}) | |||
} | |||
} | |||
return conditions, nil | |||
} | |||
// Matches returns true if the query matches against any event in the given set | |||
// of events, false otherwise. For each event, a match exists if the query is | |||
// matched against *any* value in a slice of values. An error is returned if | |||
// any attempted event match returns an error. | |||
// | |||
// For example, query "name=John" matches events = {"name": ["John", "Eric"]}. | |||
// More examples could be found in parser_test.go and query_test.go. | |||
func (q *Query) Matches(rawEvents []types.Event) (bool, error) { | |||
if len(rawEvents) == 0 { | |||
return false, nil | |||
} | |||
events := flattenEvents(rawEvents) | |||
var ( | |||
eventAttr string | |||
op Operator | |||
) | |||
buffer, begin, end := q.parser.Buffer, 0, 0 | |||
// tokens must be in the following order: | |||
// tag ("tx.gas") -> operator ("=") -> operand ("7") | |||
for token := range q.parser.Tokens() { | |||
switch token.pegRule { | |||
case rulePegText: | |||
begin, end = int(token.begin), int(token.end) | |||
case ruletag: | |||
eventAttr = buffer[begin:end] | |||
case rulele: | |||
op = OpLessEqual | |||
case rulege: | |||
op = OpGreaterEqual | |||
case rulel: | |||
op = OpLess | |||
case ruleg: | |||
op = OpGreater | |||
case ruleequal: | |||
op = OpEqual | |||
case rulecontains: | |||
op = OpContains | |||
case ruleexists: | |||
op = OpExists | |||
if strings.Contains(eventAttr, ".") { | |||
// Searching for a full "type.attribute" event. | |||
_, ok := events[eventAttr] | |||
if !ok { | |||
return false, nil | |||
} | |||
} else { | |||
foundEvent := false | |||
loop: | |||
for compositeKey := range events { | |||
if strings.Index(compositeKey, eventAttr) == 0 { | |||
foundEvent = true | |||
break loop | |||
} | |||
} | |||
if !foundEvent { | |||
return false, nil | |||
} | |||
} | |||
case rulevalue: | |||
// strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock") | |||
valueWithoutSingleQuotes := buffer[begin+1 : end-1] | |||
// see if the triplet (event attribute, operator, operand) matches any event | |||
// "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" } | |||
match, err := match(eventAttr, op, reflect.ValueOf(valueWithoutSingleQuotes), events) | |||
if err != nil { | |||
return false, err | |||
} | |||
if !match { | |||
return false, nil | |||
} | |||
case rulenumber: | |||
number := buffer[begin:end] | |||
if strings.ContainsAny(number, ".") { // if it looks like a floating-point number | |||
value, err := strconv.ParseFloat(number, 64) | |||
if err != nil { | |||
err = fmt.Errorf( | |||
"got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", | |||
err, number, | |||
) | |||
return false, err | |||
} | |||
match, err := match(eventAttr, op, reflect.ValueOf(value), events) | |||
if err != nil { | |||
return false, err | |||
} | |||
if !match { | |||
return false, nil | |||
} | |||
} else { | |||
value, err := strconv.ParseInt(number, 10, 64) | |||
if err != nil { | |||
err = fmt.Errorf( | |||
"got %v while trying to parse %s as int64 (should never happen if the grammar is correct)", | |||
err, number, | |||
) | |||
return false, err | |||
} | |||
match, err := match(eventAttr, op, reflect.ValueOf(value), events) | |||
if err != nil { | |||
return false, err | |||
} | |||
if !match { | |||
return false, nil | |||
} | |||
} | |||
case ruletime: | |||
value, err := time.Parse(TimeLayout, buffer[begin:end]) | |||
if err != nil { | |||
err = fmt.Errorf( | |||
"got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", | |||
err, buffer[begin:end], | |||
) | |||
return false, err | |||
} | |||
match, err := match(eventAttr, op, reflect.ValueOf(value), events) | |||
if err != nil { | |||
return false, err | |||
} | |||
if !match { | |||
return false, nil | |||
} | |||
case ruledate: | |||
value, err := time.Parse("2006-01-02", buffer[begin:end]) | |||
if err != nil { | |||
err = fmt.Errorf( | |||
"got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)", | |||
err, buffer[begin:end], | |||
) | |||
return false, err | |||
} | |||
match, err := match(eventAttr, op, reflect.ValueOf(value), events) | |||
if err != nil { | |||
return false, err | |||
} | |||
if !match { | |||
return false, nil | |||
} | |||
} | |||
} | |||
return true, nil | |||
} | |||
// match returns true if the given triplet (attribute, operator, operand) matches | |||
// any value in an event for that attribute. If any match fails with an error, | |||
// that error is returned. | |||
// | |||
// First, it looks up the key in the events and if it finds one, tries to compare | |||
// all the values from it to the operand using the operator. | |||
// | |||
// "tx.gas", "=", "7", {"tx": [{"gas": 7, "ID": "4AE393495334"}]} | |||
func match(attr string, op Operator, operand reflect.Value, events map[string][]string) (bool, error) { | |||
// look up the tag from the query in tags | |||
values, ok := events[attr] | |||
if !ok { | |||
return false, nil | |||
} | |||
for _, value := range values { | |||
// return true if any value in the set of the event's values matches | |||
match, err := matchValue(value, op, operand) | |||
if err != nil { | |||
return false, err | |||
} | |||
if match { | |||
return true, nil | |||
} | |||
} | |||
return false, nil | |||
} | |||
// matchValue will attempt to match a string value against an operator an | |||
// operand. A boolean is returned representing the match result. It will return | |||
// an error if the value cannot be parsed and matched against the operand type. | |||
func matchValue(value string, op Operator, operand reflect.Value) (bool, error) { | |||
switch operand.Kind() { | |||
case reflect.Struct: // time | |||
operandAsTime := operand.Interface().(time.Time) | |||
// try our best to convert value from events to time.Time | |||
var ( | |||
v time.Time | |||
err error | |||
) | |||
if strings.ContainsAny(value, "T") { | |||
v, err = time.Parse(TimeLayout, value) | |||
} else { | |||
v, err = time.Parse(DateLayout, value) | |||
} | |||
if err != nil { | |||
return false, fmt.Errorf("failed to convert value %v from event attribute to time.Time: %w", value, err) | |||
} | |||
switch op { | |||
case OpLessEqual: | |||
return (v.Before(operandAsTime) || v.Equal(operandAsTime)), nil | |||
case OpGreaterEqual: | |||
return (v.Equal(operandAsTime) || v.After(operandAsTime)), nil | |||
case OpLess: | |||
return v.Before(operandAsTime), nil | |||
case OpGreater: | |||
return v.After(operandAsTime), nil | |||
case OpEqual: | |||
return v.Equal(operandAsTime), nil | |||
} | |||
case reflect.Float64: | |||
var v float64 | |||
operandFloat64 := operand.Interface().(float64) | |||
filteredValue := numRegex.FindString(value) | |||
// try our best to convert value from tags to float64 | |||
v, err := strconv.ParseFloat(filteredValue, 64) | |||
if err != nil { | |||
return false, fmt.Errorf("failed to convert value %v from event attribute to float64: %w", filteredValue, err) | |||
} | |||
switch op { | |||
case OpLessEqual: | |||
return v <= operandFloat64, nil | |||
case OpGreaterEqual: | |||
return v >= operandFloat64, nil | |||
case OpLess: | |||
return v < operandFloat64, nil | |||
case OpGreater: | |||
return v > operandFloat64, nil | |||
case OpEqual: | |||
return v == operandFloat64, nil | |||
} | |||
case reflect.Int64: | |||
var v int64 | |||
operandInt := operand.Interface().(int64) | |||
filteredValue := numRegex.FindString(value) | |||
// if value looks like float, we try to parse it as float | |||
if strings.ContainsAny(filteredValue, ".") { | |||
v1, err := strconv.ParseFloat(filteredValue, 64) | |||
if err != nil { | |||
return false, fmt.Errorf("failed to convert value %v from event attribute to float64: %w", filteredValue, err) | |||
} | |||
v = int64(v1) | |||
} else { | |||
var err error | |||
// try our best to convert value from tags to int64 | |||
v, err = strconv.ParseInt(filteredValue, 10, 64) | |||
if err != nil { | |||
return false, fmt.Errorf("failed to convert value %v from event attribute to int64: %w", filteredValue, err) | |||
} | |||
} | |||
switch op { | |||
case OpLessEqual: | |||
return v <= operandInt, nil | |||
case OpGreaterEqual: | |||
return v >= operandInt, nil | |||
case OpLess: | |||
return v < operandInt, nil | |||
case OpGreater: | |||
return v > operandInt, nil | |||
case OpEqual: | |||
return v == operandInt, nil | |||
} | |||
case reflect.String: | |||
switch op { | |||
case OpEqual: | |||
return value == operand.String(), nil | |||
case OpContains: | |||
return strings.Contains(value, operand.String()), nil | |||
} | |||
default: | |||
return false, fmt.Errorf("unknown kind of operand %v", operand.Kind()) | |||
} | |||
return false, nil | |||
} | |||
func flattenEvents(events []types.Event) map[string][]string { | |||
flattened := make(map[string][]string) | |||
for _, event := range events { | |||
if len(event.Type) == 0 { | |||
continue | |||
} | |||
for _, attr := range event.Attributes { | |||
if len(attr.Key) == 0 { | |||
continue | |||
} | |||
compositeEvent := fmt.Sprintf("%s.%s", event.Type, attr.Key) | |||
flattened[compositeEvent] = append(flattened[compositeEvent], attr.Value) | |||
} | |||
} | |||
return flattened | |||
} |
@ -1,35 +0,0 @@ | |||
package query | |||
type QueryParser Peg { | |||
} | |||
e <- '\"' condition ( ' '+ and ' '+ condition )* '\"' !. | |||
condition <- tag ' '* (le ' '* (number / time / date) | |||
/ ge ' '* (number / time / date) | |||
/ l ' '* (number / time / date) | |||
/ g ' '* (number / time / date) | |||
/ equal ' '* (number / time / date / value) | |||
/ contains ' '* value | |||
/ exists | |||
) | |||
tag <- < (![ \t\n\r\\()"'=><] .)+ > | |||
value <- < '\'' (!["'] .)* '\''> | |||
number <- < ('0' | |||
/ [1-9] digit* ('.' digit*)?) > | |||
digit <- [0-9] | |||
time <- "TIME " < year '-' month '-' day 'T' digit digit ':' digit digit ':' digit digit (('-' / '+') digit digit ':' digit digit / 'Z') > | |||
date <- "DATE " < year '-' month '-' day > | |||
year <- ('1' / '2') digit digit digit | |||
month <- ('0' / '1') digit | |||
day <- ('0' / '1' / '2' / '3') digit | |||
and <- "AND" | |||
equal <- "=" | |||
contains <- "CONTAINS" | |||
exists <- "EXISTS" | |||
le <- "<=" | |||
ge <- ">=" | |||
l <- "<" | |||
g <- ">" |
@ -1,205 +0,0 @@ | |||
package query_test | |||
import ( | |||
"fmt" | |||
"strings" | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/require" | |||
abci "github.com/tendermint/tendermint/abci/types" | |||
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery" | |||
) | |||
func expandEvents(flattenedEvents map[string][]string) []abci.Event { | |||
events := make([]abci.Event, len(flattenedEvents)) | |||
for composite, values := range flattenedEvents { | |||
tokens := strings.Split(composite, ".") | |||
attrs := make([]abci.EventAttribute, len(values)) | |||
for i, v := range values { | |||
attrs[i] = abci.EventAttribute{ | |||
Key: tokens[len(tokens)-1], | |||
Value: v, | |||
} | |||
} | |||
events = append(events, abci.Event{ | |||
Type: strings.Join(tokens[:len(tokens)-1], "."), | |||
Attributes: attrs, | |||
}) | |||
} | |||
return events | |||
} | |||
func TestMatches(t *testing.T) { | |||
var ( | |||
txDate = "2017-01-01" | |||
txTime = "2018-05-03T14:45:00Z" | |||
) | |||
testCases := []struct { | |||
s string | |||
events map[string][]string | |||
matches bool | |||
}{ | |||
{"tm.events.type='NewBlock'", map[string][]string{"tm.events.type": {"NewBlock"}}, true}, | |||
{"tx.gas > 7", map[string][]string{"tx.gas": {"8"}}, true}, | |||
{"transfer.amount > 7", map[string][]string{"transfer.amount": {"8stake"}}, true}, | |||
{"transfer.amount > 7", map[string][]string{"transfer.amount": {"8.045stake"}}, true}, | |||
{"transfer.amount > 7.043", map[string][]string{"transfer.amount": {"8.045stake"}}, true}, | |||
{"transfer.amount > 8.045", map[string][]string{"transfer.amount": {"8.045stake"}}, false}, | |||
{"tx.gas > 7 AND tx.gas < 9", map[string][]string{"tx.gas": {"8"}}, true}, | |||
{"body.weight >= 3.5", map[string][]string{"body.weight": {"3.5"}}, true}, | |||
{"account.balance < 1000.0", map[string][]string{"account.balance": {"900"}}, true}, | |||
{"apples.kg <= 4", map[string][]string{"apples.kg": {"4.0"}}, true}, | |||
{"body.weight >= 4.5", map[string][]string{"body.weight": {fmt.Sprintf("%v", float32(4.5))}}, true}, | |||
{ | |||
"oranges.kg < 4 AND watermellons.kg > 10", | |||
map[string][]string{"oranges.kg": {"3"}, "watermellons.kg": {"12"}}, | |||
true, | |||
}, | |||
{"peaches.kg < 4", map[string][]string{"peaches.kg": {"5"}}, false}, | |||
{ | |||
"tx.date > DATE 2017-01-01", | |||
map[string][]string{"tx.date": {time.Now().Format(query.DateLayout)}}, | |||
true, | |||
}, | |||
{"tx.date = DATE 2017-01-01", map[string][]string{"tx.date": {txDate}}, true}, | |||
{"tx.date = DATE 2018-01-01", map[string][]string{"tx.date": {txDate}}, false}, | |||
{ | |||
"tx.time >= TIME 2013-05-03T14:45:00Z", | |||
map[string][]string{"tx.time": {time.Now().Format(query.TimeLayout)}}, | |||
true, | |||
}, | |||
{"tx.time = TIME 2013-05-03T14:45:00Z", map[string][]string{"tx.time": {txTime}}, false}, | |||
{"abci.owner.name CONTAINS 'Igor'", map[string][]string{"abci.owner.name": {"Igor,Ivan"}}, true}, | |||
{"abci.owner.name CONTAINS 'Igor'", map[string][]string{"abci.owner.name": {"Pavel,Ivan"}}, false}, | |||
{"abci.owner.name = 'Igor'", map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, true}, | |||
{ | |||
"abci.owner.name = 'Ivan'", | |||
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, | |||
true, | |||
}, | |||
{ | |||
"abci.owner.name = 'Ivan' AND abci.owner.name = 'Igor'", | |||
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, | |||
true, | |||
}, | |||
{ | |||
"abci.owner.name = 'Ivan' AND abci.owner.name = 'John'", | |||
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, | |||
false, | |||
}, | |||
{ | |||
"tm.events.type='NewBlock'", | |||
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}}, | |||
true, | |||
}, | |||
{ | |||
"app.name = 'fuzzed'", | |||
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}}, | |||
true, | |||
}, | |||
{ | |||
"tm.events.type='NewBlock' AND app.name = 'fuzzed'", | |||
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}}, | |||
true, | |||
}, | |||
{ | |||
"tm.events.type='NewHeader' AND app.name = 'fuzzed'", | |||
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}}, | |||
false, | |||
}, | |||
{"slash EXISTS", | |||
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}}, | |||
true, | |||
}, | |||
{"sl EXISTS", | |||
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}}, | |||
true, | |||
}, | |||
{"slash EXISTS", | |||
map[string][]string{"transfer.recipient": {"cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz"}, | |||
"transfer.sender": {"cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5"}}, | |||
false, | |||
}, | |||
{"slash.reason EXISTS AND slash.power > 1000", | |||
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}}, | |||
true, | |||
}, | |||
{"slash.reason EXISTS AND slash.power > 1000", | |||
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"500"}}, | |||
false, | |||
}, | |||
{"slash.reason EXISTS", | |||
map[string][]string{"transfer.recipient": {"cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz"}, | |||
"transfer.sender": {"cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5"}}, | |||
false, | |||
}, | |||
} | |||
for _, tc := range testCases { | |||
q, err := query.New(tc.s) | |||
require.Nil(t, err) | |||
require.NotNil(t, q, "Query '%s' should not be nil", tc.s) | |||
rawEvents := expandEvents(tc.events) | |||
match, err := q.Matches(rawEvents) | |||
require.Nil(t, err, "Query '%s' should not error on input %v", tc.s, tc.events) | |||
require.Equal(t, tc.matches, match, "Query '%s' on input %v: got %v, want %v", | |||
tc.s, tc.events, match, tc.matches) | |||
} | |||
} | |||
func TestMustParse(t *testing.T) { | |||
require.Panics(t, func() { query.MustParse("=") }) | |||
require.NotPanics(t, func() { query.MustParse("tm.events.type='NewBlock'") }) | |||
} | |||
func TestConditions(t *testing.T) { | |||
txTime, err := time.Parse(time.RFC3339, "2013-05-03T14:45:00Z") | |||
require.NoError(t, err) | |||
testCases := []struct { | |||
s string | |||
conditions []query.Condition | |||
}{ | |||
{ | |||
s: "tm.events.type='NewBlock'", | |||
conditions: []query.Condition{ | |||
{CompositeKey: "tm.events.type", Op: query.OpEqual, Operand: "NewBlock"}, | |||
}, | |||
}, | |||
{ | |||
s: "tx.gas > 7 AND tx.gas < 9", | |||
conditions: []query.Condition{ | |||
{CompositeKey: "tx.gas", Op: query.OpGreater, Operand: int64(7)}, | |||
{CompositeKey: "tx.gas", Op: query.OpLess, Operand: int64(9)}, | |||
}, | |||
}, | |||
{ | |||
s: "tx.time >= TIME 2013-05-03T14:45:00Z", | |||
conditions: []query.Condition{ | |||
{CompositeKey: "tx.time", Op: query.OpGreaterEqual, Operand: txTime}, | |||
}, | |||
}, | |||
{ | |||
s: "slashing EXISTS", | |||
conditions: []query.Condition{ | |||
{CompositeKey: "slashing", Op: query.OpExists}, | |||
}, | |||
}, | |||
} | |||
for _, tc := range testCases { | |||
q, err := query.New(tc.s) | |||
require.Nil(t, err) | |||
c, err := q.Conditions() | |||
require.NoError(t, err) | |||
require.Equal(t, tc.conditions, c) | |||
} | |||
} |