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 }