github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/rpc/client/eventstream/eventstream_test.go (about)

     1  package eventstream_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/fortytw2/leaktest"
    11  	"github.com/google/go-cmp/cmp"
    12  
    13  	"github.com/ari-anchor/sei-tendermint/internal/eventlog"
    14  	"github.com/ari-anchor/sei-tendermint/internal/eventlog/cursor"
    15  	rpccore "github.com/ari-anchor/sei-tendermint/internal/rpc/core"
    16  	"github.com/ari-anchor/sei-tendermint/rpc/client/eventstream"
    17  	"github.com/ari-anchor/sei-tendermint/rpc/coretypes"
    18  	"github.com/ari-anchor/sei-tendermint/types"
    19  )
    20  
    21  func TestStream_filterOrder(t *testing.T) {
    22  	defer leaktest.Check(t)
    23  
    24  	s := newStreamTester(t, `tm.event = 'good'`, eventlog.LogSettings{
    25  		WindowSize: 30 * time.Second,
    26  	}, nil)
    27  
    28  	// Verify that events are delivered in forward time order (i.e., that the
    29  	// stream unpacks the pages correctly) and that events not matching the
    30  	// query (here, type="bad") are skipped.
    31  	//
    32  	// The minimum batch size is 16 and half the events we publish match, so we
    33  	// publish > 32 items (> 16 good) to ensure we exercise paging.
    34  	etype := [2]string{"good", "bad"}
    35  	var items []testItem
    36  	for i := 0; i < 40; i++ {
    37  		s.advance(100 * time.Millisecond)
    38  		text := fmt.Sprintf("item%d", i)
    39  		cur := s.publish(etype[i%2], text)
    40  
    41  		// Even-numbered items match the target type.
    42  		if i%2 == 0 {
    43  			items = append(items, makeTestItem(cur, text))
    44  		}
    45  	}
    46  
    47  	s.start()
    48  	for _, itm := range items {
    49  		s.mustItem(t, itm)
    50  	}
    51  	s.stopWait()
    52  }
    53  
    54  func TestStream_lostItem(t *testing.T) {
    55  	defer leaktest.Check(t)
    56  
    57  	s := newStreamTester(t, ``, eventlog.LogSettings{
    58  		WindowSize: 30 * time.Second,
    59  	}, nil)
    60  
    61  	// Publish an item and let the client observe it.
    62  	cur := s.publish("ok", "whatever")
    63  	s.start()
    64  	s.mustItem(t, makeTestItem(cur, "whatever"))
    65  	s.stopWait()
    66  
    67  	// Time passes, and cur expires out of the window.
    68  	s.advance(50 * time.Second)
    69  	next1 := s.publish("ok", "more stuff")
    70  	s.advance(15 * time.Second)
    71  	next2 := s.publish("ok", "still more stuff")
    72  
    73  	// At this point, the oldest item in the log is newer than the point at
    74  	// which we continued, we should get an error.
    75  	s.start()
    76  	var missed *eventstream.MissedItemsError
    77  	if err := s.mustError(t); !errors.As(err, &missed) {
    78  		t.Errorf("Wrong error: got %v, want %T", err, missed)
    79  	} else {
    80  		t.Logf("Correctly reported missed item: %v", missed)
    81  	}
    82  
    83  	// If we reset the stream and continue from head, we should catch up.
    84  	s.stopWait()
    85  	s.stream.Reset()
    86  	s.start()
    87  
    88  	s.mustItem(t, makeTestItem(next1, "more stuff"))
    89  	s.mustItem(t, makeTestItem(next2, "still more stuff"))
    90  	s.stopWait()
    91  }
    92  
    93  func TestMinPollTime(t *testing.T) {
    94  	defer leaktest.Check(t)
    95  
    96  	s := newStreamTester(t, ``, eventlog.LogSettings{
    97  		WindowSize: 30 * time.Second,
    98  	}, nil)
    99  
   100  	s.publish("bad", "whatever")
   101  
   102  	// Waiting for an item on a log with no matching events incurs a minimum
   103  	// wait time and reports no events.
   104  	ctx := context.Background()
   105  	filter := &coretypes.EventFilter{Query: `tm.event = 'good'`}
   106  
   107  	t.Run("NoneMatch", func(t *testing.T) {
   108  		start := time.Now()
   109  
   110  		// Request a very short delay, and affirm we got the server's minimum.
   111  		rsp, err := s.env.Events(ctx, &coretypes.RequestEvents{
   112  			Filter:   filter,
   113  			MaxItems: 1,
   114  			WaitTime: 10 * time.Millisecond,
   115  		})
   116  		if err != nil {
   117  			t.Fatalf("Events failed: %v", err)
   118  		} else if elapsed := time.Since(start); elapsed < time.Second {
   119  			t.Errorf("Events returned too quickly: got %v, wanted 1s", elapsed)
   120  		} else if len(rsp.Items) != 0 {
   121  			t.Errorf("Events returned %d items, expected none", len(rsp.Items))
   122  		}
   123  	})
   124  
   125  	s.publish("good", "whatever")
   126  
   127  	// Waiting for an available matching item incurs no delay.
   128  	t.Run("SomeMatch", func(t *testing.T) {
   129  		start := time.Now()
   130  
   131  		// Request a long-ish delay and affirm we don't block for it.
   132  		// Check for this by ensuring we return sooner than the minimum delay,
   133  		// since we don't know the exact timing.
   134  		rsp, err := s.env.Events(ctx, &coretypes.RequestEvents{
   135  			Filter:   filter,
   136  			MaxItems: 1,
   137  			WaitTime: 10 * time.Second,
   138  		})
   139  		if err != nil {
   140  			t.Fatalf("Events failed: %v", err)
   141  		} else if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
   142  			t.Errorf("Events returned too slowly: got %v, wanted immediate", elapsed)
   143  		} else if len(rsp.Items) == 0 {
   144  			t.Error("Events returned no items, wanted at least 1")
   145  		}
   146  	})
   147  }
   148  
   149  // testItem is a wrapper for comparing item results in a friendly output format
   150  // for the cmp package.
   151  type testItem struct {
   152  	Cursor string
   153  	Data   string
   154  
   155  	// N.B. Fields exported to simplify use in cmp.
   156  }
   157  
   158  func makeTestItem(cur, data string) testItem {
   159  	return testItem{
   160  		Cursor: cur,
   161  		Data:   fmt.Sprintf(`{"type":%q,"value":%q}`, types.EventDataString("").TypeTag(), data),
   162  	}
   163  }
   164  
   165  // streamTester is a simulation harness for an eventstream.Stream. It simulates
   166  // the production service by plumbing an event log into a stub RPC environment,
   167  // into which the test can publish events and advance the perceived time to
   168  // exercise various cases of the stream.
   169  type streamTester struct {
   170  	log    *eventlog.Log
   171  	env    *rpccore.Environment
   172  	clock  int64
   173  	index  int64
   174  	stream *eventstream.Stream
   175  	errc   chan error
   176  	recv   chan *coretypes.EventItem
   177  	stop   func()
   178  }
   179  
   180  func newStreamTester(t *testing.T, query string, logOpts eventlog.LogSettings, streamOpts *eventstream.StreamOptions) *streamTester {
   181  	t.Helper()
   182  	s := new(streamTester)
   183  
   184  	// Plumb a time source controlled by the tester into the event log.
   185  	logOpts.Source = cursor.Source{
   186  		TimeIndex: s.timeNow,
   187  	}
   188  	lg, err := eventlog.New(logOpts)
   189  	if err != nil {
   190  		t.Fatalf("Creating event log: %v", err)
   191  	}
   192  	s.log = lg
   193  	s.env = &rpccore.Environment{EventLog: lg}
   194  	s.stream = eventstream.New(s, query, streamOpts)
   195  	return s
   196  }
   197  
   198  // start starts the stream receiver, which runs until it it terminated by
   199  // calling stop.
   200  func (s *streamTester) start() {
   201  	ctx, cancel := context.WithCancel(context.Background())
   202  	s.errc = make(chan error, 1)
   203  	s.recv = make(chan *coretypes.EventItem)
   204  	s.stop = cancel
   205  	go func() {
   206  		defer close(s.errc)
   207  		s.errc <- s.stream.Run(ctx, func(itm *coretypes.EventItem) error {
   208  			select {
   209  			case <-ctx.Done():
   210  				return ctx.Err()
   211  			case s.recv <- itm:
   212  				return nil
   213  			}
   214  		})
   215  	}()
   216  }
   217  
   218  // publish adds a single event to the event log at the present moment.
   219  func (s *streamTester) publish(etype, payload string) string {
   220  	_ = s.log.Add(etype, types.EventDataString(payload))
   221  	s.index++
   222  	return fmt.Sprintf("%016x-%04x", s.clock, s.index)
   223  }
   224  
   225  // wait blocks until either an item is received or the runner stops.
   226  func (s *streamTester) wait() (*coretypes.EventItem, error) {
   227  	select {
   228  	case itm := <-s.recv:
   229  		return itm, nil
   230  	case err := <-s.errc:
   231  		return nil, err
   232  	}
   233  }
   234  
   235  // mustItem waits for an item and fails if either an error occurs or the item
   236  // does not match want.
   237  func (s *streamTester) mustItem(t *testing.T, want testItem) {
   238  	t.Helper()
   239  
   240  	itm, err := s.wait()
   241  	if err != nil {
   242  		t.Fatalf("Receive: got error %v, want item %v", err, want)
   243  	}
   244  	got := testItem{Cursor: itm.Cursor, Data: string(itm.Data)}
   245  	if diff := cmp.Diff(want, got); diff != "" {
   246  		t.Errorf("Item: (-want, +got)\n%s", diff)
   247  	}
   248  }
   249  
   250  // mustError waits for an error and fails if an item is returned.
   251  func (s *streamTester) mustError(t *testing.T) error {
   252  	t.Helper()
   253  	itm, err := s.wait()
   254  	if err == nil {
   255  		t.Fatalf("Receive: got item %v, want error", itm)
   256  	}
   257  	return err
   258  }
   259  
   260  // stopWait stops the runner and waits for it to terminate.
   261  func (s *streamTester) stopWait() { s.stop(); s.wait() } //nolint:errcheck
   262  
   263  // timeNow reports the current simulated time index.
   264  func (s *streamTester) timeNow() int64 { return s.clock }
   265  
   266  // advance moves the simulated time index.
   267  func (s *streamTester) advance(d time.Duration) { s.clock += int64(d) }
   268  
   269  // Events implements the eventstream.Client interface by delegating to a stub
   270  // environment as if it were a local RPC client.  This works because the Events
   271  // method only requires the event log, the other fields are unused.
   272  func (s *streamTester) Events(ctx context.Context, req *coretypes.RequestEvents) (*coretypes.ResultEvents, error) {
   273  	return s.env.Events(ctx, req)
   274  }