|
|
- package eventstream_test
-
- import (
- "context"
- "errors"
- "fmt"
- "testing"
- "time"
-
- "github.com/fortytw2/leaktest"
- "github.com/google/go-cmp/cmp"
-
- "github.com/tendermint/tendermint/internal/eventlog"
- "github.com/tendermint/tendermint/internal/eventlog/cursor"
- rpccore "github.com/tendermint/tendermint/internal/rpc/core"
- "github.com/tendermint/tendermint/rpc/client/eventstream"
- "github.com/tendermint/tendermint/rpc/coretypes"
- "github.com/tendermint/tendermint/types"
- )
-
- func TestStream_filterOrder(t *testing.T) {
- defer leaktest.Check(t)
-
- s := newStreamTester(t, `tm.event = 'good'`, eventlog.LogSettings{
- WindowSize: 30 * time.Second,
- }, nil)
-
- // Verify that events are delivered in forward time order (i.e., that the
- // stream unpacks the pages correctly) and that events not matching the
- // query (here, type="bad") are skipped.
- //
- // The minimum batch size is 16 and half the events we publish match, so we
- // publish > 32 items (> 16 good) to ensure we exercise paging.
- etype := [2]string{"good", "bad"}
- var items []testItem
- for i := 0; i < 40; i++ {
- s.advance(100 * time.Millisecond)
- text := fmt.Sprintf("item%d", i)
- cur := s.publish(etype[i%2], text)
-
- // Even-numbered items match the target type.
- if i%2 == 0 {
- items = append(items, makeTestItem(cur, text))
- }
- }
-
- s.start()
- for _, itm := range items {
- s.mustItem(t, itm)
- }
- s.stopWait()
- }
-
- func TestStream_lostItem(t *testing.T) {
- defer leaktest.Check(t)
-
- s := newStreamTester(t, ``, eventlog.LogSettings{
- WindowSize: 30 * time.Second,
- }, nil)
-
- // Publish an item and let the client observe it.
- cur := s.publish("ok", "whatever")
- s.start()
- s.mustItem(t, makeTestItem(cur, "whatever"))
- s.stopWait()
-
- // Time passes, and cur expires out of the window.
- s.advance(50 * time.Second)
- next1 := s.publish("ok", "more stuff")
- s.advance(15 * time.Second)
- next2 := s.publish("ok", "still more stuff")
-
- // At this point, the oldest item in the log is newer than the point at
- // which we continued, we should get an error.
- s.start()
- var missed *eventstream.MissedItemsError
- if err := s.mustError(t); !errors.As(err, &missed) {
- t.Errorf("Wrong error: got %v, want %T", err, missed)
- } else {
- t.Logf("Correctly reported missed item: %v", missed)
- }
-
- // If we reset the stream and continue from head, we should catch up.
- s.stopWait()
- s.stream.Reset()
- s.start()
-
- s.mustItem(t, makeTestItem(next1, "more stuff"))
- s.mustItem(t, makeTestItem(next2, "still more stuff"))
- s.stopWait()
- }
-
- // testItem is a wrapper for comparing item results in a friendly output format
- // for the cmp package.
- type testItem struct {
- Cursor string
- Data string
-
- // N.B. Fields exported to simplify use in cmp.
- }
-
- func makeTestItem(cur, data string) testItem {
- return testItem{
- Cursor: cur,
- Data: fmt.Sprintf(`{"type":%q,"value":%q}`, types.EventDataString("").TypeTag(), data),
- }
- }
-
- // streamTester is a simulation harness for an eventstream.Stream. It simulates
- // the production service by plumbing an event log into a stub RPC environment,
- // into which the test can publish events and advance the perceived time to
- // exercise various cases of the stream.
- type streamTester struct {
- log *eventlog.Log
- env *rpccore.Environment
- clock int64
- index int64
- stream *eventstream.Stream
- errc chan error
- recv chan *coretypes.EventItem
- stop func()
- }
-
- func newStreamTester(t *testing.T, query string, logOpts eventlog.LogSettings, streamOpts *eventstream.StreamOptions) *streamTester {
- t.Helper()
- s := new(streamTester)
-
- // Plumb a time source controlled by the tester into the event log.
- logOpts.Source = cursor.Source{
- TimeIndex: s.timeNow,
- }
- lg, err := eventlog.New(logOpts)
- if err != nil {
- t.Fatalf("Creating event log: %v", err)
- }
- s.log = lg
- s.env = &rpccore.Environment{EventLog: lg}
- s.stream = eventstream.New(s, query, streamOpts)
- return s
- }
-
- // start starts the stream receiver, which runs until it it terminated by
- // calling stop.
- func (s *streamTester) start() {
- ctx, cancel := context.WithCancel(context.Background())
- s.errc = make(chan error, 1)
- s.recv = make(chan *coretypes.EventItem)
- s.stop = cancel
- go func() {
- defer close(s.errc)
- s.errc <- s.stream.Run(ctx, func(itm *coretypes.EventItem) error {
- select {
- case <-ctx.Done():
- return ctx.Err()
- case s.recv <- itm:
- return nil
- }
- })
- }()
- }
-
- // publish adds a single event to the event log at the present moment.
- func (s *streamTester) publish(etype, payload string) string {
- _ = s.log.Add(etype, types.EventDataString(payload))
- s.index++
- return fmt.Sprintf("%016x-%04x", s.clock, s.index)
- }
-
- // wait blocks until either an item is received or the runner stops.
- func (s *streamTester) wait() (*coretypes.EventItem, error) {
- select {
- case itm := <-s.recv:
- return itm, nil
- case err := <-s.errc:
- return nil, err
- }
- }
-
- // mustItem waits for an item and fails if either an error occurs or the item
- // does not match want.
- func (s *streamTester) mustItem(t *testing.T, want testItem) {
- t.Helper()
-
- itm, err := s.wait()
- if err != nil {
- t.Fatalf("Receive: got error %v, want item %v", err, want)
- }
- got := testItem{Cursor: itm.Cursor, Data: string(itm.Data)}
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("Item: (-want, +got)\n%s", diff)
- }
- }
-
- // mustError waits for an error and fails if an item is returned.
- func (s *streamTester) mustError(t *testing.T) error {
- t.Helper()
- itm, err := s.wait()
- if err == nil {
- t.Fatalf("Receive: got item %v, want error", itm)
- }
- return err
- }
-
- // stopWait stops the runner and waits for it to terminate.
- func (s *streamTester) stopWait() { s.stop(); s.wait() } //nolint:errcheck
-
- // timeNow reports the current simulated time index.
- func (s *streamTester) timeNow() int64 { return s.clock }
-
- // advance moves the simulated time index.
- func (s *streamTester) advance(d time.Duration) { s.clock += int64(d) }
-
- // Events implements the eventstream.Client interface by delegating to a stub
- // environment as if it were a local RPC client. This works because the Events
- // method only requires the event log, the other fields are unused.
- func (s *streamTester) Events(ctx context.Context, req *coretypes.RequestEvents) (*coretypes.ResultEvents, error) {
- var before, after cursor.Cursor
- if err := before.UnmarshalText([]byte(req.Before)); err != nil {
- return nil, err
- }
- if err := after.UnmarshalText([]byte(req.After)); err != nil {
- return nil, err
- }
- return s.env.Events(ctx, req.Filter, req.MaxItems, before, after, req.WaitTime)
- }
|