- package query_test
-
- import (
- "fmt"
- "strings"
- "testing"
- "time"
-
- "github.com/tendermint/tendermint/abci/types"
- "github.com/tendermint/tendermint/internal/pubsub"
- "github.com/tendermint/tendermint/internal/pubsub/query"
- "github.com/tendermint/tendermint/internal/pubsub/query/syntax"
- )
-
- var _ pubsub.Query = (*query.Query)(nil)
-
- // Example events from the OpenAPI documentation:
- // https://github.com/tendermint/tendermint/blob/master/rpc/openapi/openapi.yaml
- //
- // Redactions:
- //
- // - Add an explicit "tm" event for the built-in attributes.
- // - Remove Index fields (not relevant to tests).
- // - Add explicit balance values (to use in tests).
- //
- var apiEvents = []types.Event{
- {
- Type: "tm",
- Attributes: []types.EventAttribute{
- {Key: "event", Value: "Tx"},
- {Key: "hash", Value: "XYZ"},
- {Key: "height", Value: "5"},
- },
- },
- {
- Type: "rewards.withdraw",
- Attributes: []types.EventAttribute{
- {Key: "address", Value: "AddrA"},
- {Key: "source", Value: "SrcX"},
- {Key: "amount", Value: "100"},
- {Key: "balance", Value: "1500"},
- },
- },
- {
- Type: "rewards.withdraw",
- Attributes: []types.EventAttribute{
- {Key: "address", Value: "AddrB"},
- {Key: "source", Value: "SrcY"},
- {Key: "amount", Value: "45"},
- {Key: "balance", Value: "999"},
- },
- },
- {
- Type: "transfer",
- Attributes: []types.EventAttribute{
- {Key: "sender", Value: "AddrC"},
- {Key: "recipient", Value: "AddrD"},
- {Key: "amount", Value: "160"},
- },
- },
- }
-
- func TestCompiledMatches(t *testing.T) {
- var (
- txDate = "2017-01-01"
- txTime = "2018-05-03T14:45:00Z"
- )
-
- testCases := []struct {
- s string
- events []types.Event
- matches bool
- }{
- {`tm.events.type='NewBlock'`,
- newTestEvents(`tm|events.type=NewBlock`),
- true},
- {`tx.gas > 7`,
- newTestEvents(`tx|gas=8`),
- true},
- {`transfer.amount > 7`,
- newTestEvents(`transfer|amount=8stake`),
- true},
- {`transfer.amount > 7`,
- newTestEvents(`transfer|amount=8.045`),
- true},
- {`transfer.amount > 7.043`,
- newTestEvents(`transfer|amount=8.045stake`),
- true},
- {`transfer.amount > 8.045`,
- newTestEvents(`transfer|amount=8.045stake`),
- false},
- {`tx.gas > 7 AND tx.gas < 9`,
- newTestEvents(`tx|gas=8`),
- true},
- {`body.weight >= 3.5`,
- newTestEvents(`body|weight=3.5`),
- true},
- {`account.balance < 1000.0`,
- newTestEvents(`account|balance=900`),
- true},
- {`apples.kg <= 4`,
- newTestEvents(`apples|kg=4.0`),
- true},
- {`body.weight >= 4.5`,
- newTestEvents(`body|weight=4.5`),
- true},
- {`oranges.kg < 4 AND watermellons.kg > 10`,
- newTestEvents(`oranges|kg=3`, `watermellons|kg=12`),
- true},
- {`peaches.kg < 4`,
- newTestEvents(`peaches|kg=5`),
- false},
- {`tx.date > DATE 2017-01-01`,
- newTestEvents(`tx|date=` + time.Now().Format(syntax.DateFormat)),
- true},
- {`tx.date = DATE 2017-01-01`,
- newTestEvents(`tx|date=` + txDate),
- true},
- {`tx.date = DATE 2018-01-01`,
- newTestEvents(`tx|date=` + txDate),
- false},
- {`tx.time >= TIME 2013-05-03T14:45:00Z`,
- newTestEvents(`tx|time=` + time.Now().Format(syntax.TimeFormat)),
- true},
- {`tx.time = TIME 2013-05-03T14:45:00Z`,
- newTestEvents(`tx|time=` + txTime),
- false},
- {`abci.owner.name CONTAINS 'Igor'`,
- newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
- true},
- {`abci.owner.name CONTAINS 'Igor'`,
- newTestEvents(`abci|owner.name=Pavel|owner.name=Ivan`),
- false},
- {`abci.owner.name = 'Igor'`,
- newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
- true},
- {`abci.owner.name = 'Ivan'`,
- newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
- true},
- {`abci.owner.name = 'Ivan' AND abci.owner.name = 'Igor'`,
- newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
- true},
- {`abci.owner.name = 'Ivan' AND abci.owner.name = 'John'`,
- newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
- false},
- {`tm.events.type='NewBlock'`,
- newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
- true},
- {`app.name = 'fuzzed'`,
- newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
- true},
- {`tm.events.type='NewBlock' AND app.name = 'fuzzed'`,
- newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
- true},
- {`tm.events.type='NewHeader' AND app.name = 'fuzzed'`,
- newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
- false},
- {`slash EXISTS`,
- newTestEvents(`slash|reason=missing_signature|power=6000`),
- true},
- {`slash EXISTS`,
- newTestEvents(`transfer|recipient=cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz|sender=cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5`),
- false},
- {`slash.reason EXISTS AND slash.power > 1000`,
- newTestEvents(`slash|reason=missing_signature|power=6000`),
- true},
- {`slash.reason EXISTS AND slash.power > 1000`,
- newTestEvents(`slash|reason=missing_signature|power=500`),
- false},
- {`slash.reason EXISTS`,
- newTestEvents(`transfer|recipient=cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz|sender=cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5`),
- false},
-
- // Test cases based on the OpenAPI examples.
- {`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA'`,
- apiEvents, true},
- {`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA' AND rewards.withdraw.source = 'SrcY'`,
- apiEvents, true},
- {`tm.event = 'Tx' AND transfer.sender = 'AddrA'`,
- apiEvents, false},
- {`tm.event = 'Tx' AND transfer.sender = 'AddrC'`,
- apiEvents, true},
- {`tm.event = 'Tx' AND transfer.sender = 'AddrZ'`,
- apiEvents, false},
- {`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrZ'`,
- apiEvents, false},
- {`tm.event = 'Tx' AND rewards.withdraw.source = 'W'`,
- apiEvents, false},
- }
-
- // NOTE: The original implementation allowed arbitrary prefix matches on
- // attribute tags, e.g., "sl" would match "slash".
- //
- // That is weird and probably wrong: "foo.ba" should not match "foo.bar",
- // or there is no way to distinguish the case where there were two values
- // for "foo.bar" or one value each for "foo.ba" and "foo.bar".
- //
- // Apart from a single test case, I could not find any attested usage of
- // this implementation detail. It isn't documented in the OpenAPI docs and
- // is not shown in any of the example inputs.
- //
- // On that basis, I removed that test case. This implementation still does
- // correctly handle variable type/attribute splits ("x", "y.z" / "x.y", "z")
- // since that was required by the original "flattened" event representation.
-
- for i, tc := range testCases {
- t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
- c, err := query.New(tc.s)
- if err != nil {
- t.Fatalf("NewCompiled %#q: unexpected error: %v", tc.s, err)
- }
-
- got, err := c.Matches(tc.events)
- if err != nil {
- t.Errorf("Query: %#q\nInput: %+v\nMatches: got error %v",
- tc.s, tc.events, err)
- }
- if got != tc.matches {
- t.Errorf("Query: %#q\nInput: %+v\nMatches: got %v, want %v",
- tc.s, tc.events, got, tc.matches)
- }
- })
- }
- }
-
- func TestAllMatchesAll(t *testing.T) {
- events := newTestEvents(
- ``,
- `Asher|Roth=`,
- `Route|66=`,
- `Rilly|Blue=`,
- )
- for i := 0; i < len(events); i++ {
- match, err := query.All.Matches(events[:i])
- if err != nil {
- t.Errorf("Matches failed: %w", err)
- } else if !match {
- t.Errorf("Did not match on %+v ", events[:i])
- }
- }
- }
-
- // newTestEvent constructs an Event message from a template string.
- // The format is "type|attr1=val1|attr2=val2|...".
- func newTestEvent(s string) types.Event {
- var event types.Event
- parts := strings.Split(s, "|")
- event.Type = parts[0]
- if len(parts) == 1 {
- return event // type only, no attributes
- }
- for _, kv := range parts[1:] {
- key, val := splitKV(kv)
- event.Attributes = append(event.Attributes, types.EventAttribute{
- Key: key,
- Value: val,
- })
- }
- return event
- }
-
- // newTestEvents constructs a slice of Event messages by applying newTestEvent
- // to each element of ss.
- func newTestEvents(ss ...string) []types.Event {
- events := make([]types.Event, len(ss))
- for i, s := range ss {
- events[i] = newTestEvent(s)
- }
- return events
- }
-
- func splitKV(s string) (key, value string) {
- kv := strings.SplitN(s, "=", 2)
- return kv[0], kv[1]
- }
|