github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/convloader_test.go (about) 1 package chat 2 3 import ( 4 "context" 5 "testing" 6 "time" 7 8 "github.com/keybase/client/go/chat/types" 9 "github.com/keybase/client/go/kbtest" 10 "github.com/keybase/client/go/protocol/chat1" 11 "github.com/keybase/client/go/protocol/gregor1" 12 "github.com/keybase/client/go/protocol/keybase1" 13 "github.com/keybase/clockwork" 14 "github.com/stretchr/testify/require" 15 ) 16 17 func setupLoaderTest(t *testing.T) (context.Context, *kbtest.ChatTestContext, *kbtest.ChatMockWorld, 18 func() chat1.RemoteInterface, types.Sender, *chatListener, chat1.NewConversationRemoteRes) { 19 ctx, world, ri, _, baseSender, listener := setupTest(t, 1) 20 21 u := world.GetUsers()[0] 22 tc := world.Tcs[u.Username] 23 trip := newConvTriple(ctx, t, tc, u.Username) 24 firstMessagePlaintext := chat1.MessagePlaintext{ 25 ClientHeader: chat1.MessageClientHeader{ 26 Conv: trip, 27 TlfName: u.Username, 28 TlfPublic: false, 29 MessageType: chat1.MessageType_TLFNAME, 30 }, 31 MessageBody: chat1.MessageBody{}, 32 } 33 prepareRes, err := baseSender.Prepare(ctx, firstMessagePlaintext, 34 chat1.ConversationMembersType_IMPTEAMNATIVE, nil, nil) 35 firstMessageBoxed := prepareRes.Boxed 36 require.NoError(t, err) 37 res, err := ri.NewConversationRemote2(ctx, chat1.NewConversationRemote2Arg{ 38 IdTriple: trip, 39 TLFMessage: firstMessageBoxed, 40 }) 41 require.NoError(t, err) 42 return ctx, tc, world, func() chat1.RemoteInterface { return ri }, baseSender, listener, res 43 } 44 45 func TestConvLoader(t *testing.T) { 46 ctx, tc, world, _, _, listener, res := setupLoaderTest(t) 47 defer world.Cleanup() 48 49 require.NoError(t, tc.Context().ConvLoader.Queue(ctx, 50 types.NewConvLoaderJob(res.ConvID, nil, types.ConvLoaderPriorityHigh, types.ConvLoaderUnique, nil))) 51 select { 52 case convID := <-listener.bgConvLoads: 53 if !convID.Eq(res.ConvID) { 54 t.Errorf("loaded conv id: %s, expected %s", convID, res.ConvID) 55 } 56 case <-time.After(20 * time.Second): 57 t.Fatal("timeout waiting for conversation load") 58 } 59 } 60 61 type slowestRemote struct { 62 chat1.RemoteInterface 63 callCh chan struct{} 64 } 65 66 func makeSlowestRemote() slowestRemote { 67 return slowestRemote{ 68 callCh: make(chan struct{}, 5), 69 } 70 } 71 72 func (s slowestRemote) delay(ctx context.Context) { 73 s.callCh <- struct{}{} 74 select { 75 case <-ctx.Done(): 76 case <-time.After(24 * time.Hour): 77 } 78 } 79 80 func (s slowestRemote) GetThreadRemote(ctx context.Context, arg chat1.GetThreadRemoteArg) (res chat1.GetThreadRemoteRes, err error) { 81 s.delay(ctx) 82 return res, context.Canceled 83 } 84 85 func (s slowestRemote) GetMessagesRemote(ctx context.Context, arg chat1.GetMessagesRemoteArg) (res chat1.GetMessagesRemoteRes, err error) { 86 s.delay(ctx) 87 return res, context.Canceled 88 } 89 90 func TestConvLoaderSuspend(t *testing.T) { 91 _, tc, world, _, _, listener, res := setupLoaderTest(t) 92 defer world.Cleanup() 93 94 ri := tc.ChatG.ConvSource.(*HybridConversationSource).ri 95 slowRi := makeSlowestRemote() 96 tc.ChatG.ConvSource.(*HybridConversationSource).ri = func() chat1.RemoteInterface { 97 return slowRi 98 } 99 require.NoError(t, tc.Context().ConvLoader.Queue(context.TODO(), 100 types.NewConvLoaderJob(res.ConvID, nil, types.ConvLoaderPriorityHigh, types.ConvLoaderUnique, nil))) 101 select { 102 case <-slowRi.callCh: 103 case <-time.After(20 * time.Second): 104 require.Fail(t, "no remote call") 105 } 106 require.True(t, tc.Context().ConvLoader.Suspend(context.TODO())) 107 select { 108 case <-listener.bgConvLoads: 109 require.Fail(t, "no load yet") 110 default: 111 } 112 require.False(t, tc.Context().ConvLoader.Suspend(context.TODO())) 113 114 tc.ChatG.ConvSource.(*HybridConversationSource).ri = ri 115 require.False(t, tc.Context().ConvLoader.Resume(context.TODO())) 116 select { 117 case <-listener.bgConvLoads: 118 require.Fail(t, "no load yet") 119 default: 120 } 121 require.True(t, tc.Context().ConvLoader.Resume(context.TODO())) 122 select { 123 case convID := <-listener.bgConvLoads: 124 require.Equal(t, res.ConvID, convID) 125 case <-time.After(20 * time.Second): 126 require.Fail(t, "no event") 127 } 128 } 129 130 func TestConvLoaderAppState(t *testing.T) { 131 _, tc, world, _, _, listener, res := setupLoaderTest(t) 132 defer world.Cleanup() 133 134 clock := clockwork.NewFakeClock() 135 appStateCh := make(chan struct{}) 136 tc.ChatG.ConvLoader.(*BackgroundConvLoader).loadWait = 0 137 tc.ChatG.ConvLoader.(*BackgroundConvLoader).clock = clock 138 tc.ChatG.ConvLoader.(*BackgroundConvLoader).appStateCh = appStateCh 139 ri := tc.ChatG.ConvSource.(*HybridConversationSource).ri 140 _ = ri 141 slowRi := makeSlowestRemote() 142 failDuration := 2 * time.Second 143 uid := gregor1.UID(tc.G.Env.GetUID().ToBytes()) 144 // Test that a foreground with no background doesnt do anything 145 tc.ChatG.ConvSource.(*HybridConversationSource).ri = func() chat1.RemoteInterface { 146 return slowRi 147 } 148 _ = tc.ChatG.ConvSource.(*HybridConversationSource).Clear(context.TODO(), res.ConvID, uid, nil) 149 require.NoError(t, tc.Context().ConvLoader.Queue(context.TODO(), 150 types.NewConvLoaderJob(res.ConvID, nil, types.ConvLoaderPriorityHigh, types.ConvLoaderUnique, nil))) 151 clock.BlockUntil(1) 152 clock.Advance(200 * time.Millisecond) // Get by small sleep 153 select { 154 case <-slowRi.callCh: 155 case <-time.After(failDuration): 156 require.Fail(t, "no remote call") 157 } 158 require.True(t, tc.Context().ConvLoader.Suspend(context.TODO())) 159 tc.G.MobileAppState.Update(keybase1.MobileAppState_FOREGROUND) 160 select { 161 case <-appStateCh: 162 require.Fail(t, "no app state") 163 default: 164 } 165 select { 166 case <-listener.bgConvLoads: 167 require.Fail(t, "no load yet") 168 default: 169 } 170 tc.ChatG.ConvSource.(*HybridConversationSource).ri = ri 171 require.True(t, tc.Context().ConvLoader.Resume(context.TODO())) 172 clock.BlockUntil(1) 173 clock.Advance(2 * time.Second) // Get by resume wait 174 clock.BlockUntil(1) 175 clock.Advance(time.Hour) // Get by small sleep 176 select { 177 case convID := <-listener.bgConvLoads: 178 require.Equal(t, res.ConvID, convID) 179 case <-time.After(failDuration): 180 require.Fail(t, "no event") 181 } 182 t.Logf("testing foreground/background") 183 // Test that background/foreground works 184 _ = tc.ChatG.ConvSource.(*HybridConversationSource).Clear(context.TODO(), res.ConvID, uid, nil) 185 tc.ChatG.ConvSource.(*HybridConversationSource).ri = func() chat1.RemoteInterface { 186 return slowRi 187 } 188 189 require.NoError(t, tc.Context().ConvLoader.Queue(context.TODO(), 190 types.NewConvLoaderJob(res.ConvID, nil, types.ConvLoaderPriorityHigh, types.ConvLoaderUnique, nil))) 191 192 clock.BlockUntil(1) 193 clock.Advance(200 * time.Millisecond) // Get by small sleep 194 select { 195 case <-slowRi.callCh: 196 case <-time.After(failDuration): 197 require.Fail(t, "no remote call") 198 } 199 tc.G.MobileAppState.Update(keybase1.MobileAppState_BACKGROUND) 200 select { 201 case <-appStateCh: 202 case <-time.After(failDuration): 203 require.Fail(t, "no app state") 204 } 205 tc.ChatG.ConvSource.(*HybridConversationSource).ri = ri 206 tc.G.MobileAppState.Update(keybase1.MobileAppState_FOREGROUND) 207 select { 208 case <-appStateCh: 209 case <-time.After(failDuration): 210 require.Fail(t, "no app state") 211 } 212 // Need to advance clock 213 select { 214 case <-listener.bgConvLoads: 215 require.Fail(t, "no load yet") 216 default: 217 } 218 219 clock.BlockUntil(1) 220 clock.Advance(10 * time.Second) 221 clock.BlockUntil(1) 222 clock.Advance(time.Hour) // Get by small sleep 223 select { 224 case convID := <-listener.bgConvLoads: 225 require.Equal(t, res.ConvID, convID) 226 case <-time.After(failDuration): 227 require.Fail(t, "no event") 228 } 229 } 230 231 func TestConvLoaderPageBack(t *testing.T) { 232 ctx, tc, world, ri, sender, listener, res := setupLoaderTest(t) 233 defer world.Cleanup() 234 235 ib, err := ri().GetInboxRemote(ctx, chat1.GetInboxRemoteArg{ 236 Query: &chat1.GetInboxQuery{ 237 ConvID: &res.ConvID, 238 }, 239 }) 240 require.NoError(t, err) 241 require.Equal(t, 1, len(ib.Inbox.Full().Conversations)) 242 conv := ib.Inbox.Full().Conversations[0] 243 244 u := world.GetUsers()[0] 245 skp, err := sender.(*BlockingSender).getSigningKeyPair(ctx) 246 require.NoError(t, err) 247 for i := 0; i < 2; i++ { 248 pt := chat1.MessagePlaintext{ 249 ClientHeader: chat1.MessageClientHeader{ 250 Conv: conv.Metadata.IdTriple, 251 Sender: u.User.GetUID().ToBytes(), 252 TlfName: u.Username, 253 MessageType: chat1.MessageType_TEXT, 254 }, 255 MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{Body: "foo"}), 256 } 257 boxed, err := NewBoxer(tc.Context()).BoxMessage(ctx, pt, conv.GetMembersType(), skp, nil) 258 require.NoError(t, err) 259 _, err = ri().PostRemote(ctx, chat1.PostRemoteArg{ 260 ConversationID: conv.GetConvID(), 261 MessageBoxed: boxed, 262 }) 263 require.NoError(t, err) 264 } 265 select { 266 case <-listener.bgConvLoads: 267 require.Fail(t, "no loads here") 268 default: 269 } 270 271 require.NoError(t, tc.Context().ConvLoader.Queue(context.TODO(), 272 types.NewConvLoaderJob(res.ConvID, &chat1.Pagination{Num: 1}, types.ConvLoaderPriorityHigh, 273 types.ConvLoaderGeneric, newConvLoaderPagebackHook(tc.Context(), 0, 1)))) 274 for i := 0; i < 2; i++ { 275 select { 276 case <-listener.bgConvLoads: 277 case <-time.After(20 * time.Second): 278 require.Fail(t, "no load") 279 } 280 } 281 } 282 283 func TestConvLoaderJobQueue(t *testing.T) { 284 j := newJobQueue(10) 285 convID1 := chat1.ConversationID([]byte{1, 2, 3}) 286 convID2 := chat1.ConversationID([]byte{1, 2, 3, 4}) 287 newTask := func(convID chat1.ConversationID, p types.ConvLoaderPriority, 288 u types.ConvLoaderUniqueness) clTask { 289 job := types.NewConvLoaderJob(convID, nil, p, u, nil) 290 return clTask{job: job} 291 } 292 293 t.Logf("test wait") 294 select { 295 case <-j.Wait(): 296 require.Fail(t, "queue empty") 297 default: 298 } 299 cb := make(chan bool, 1) 300 go func() { 301 ret := true 302 select { 303 case <-j.Wait(): 304 case <-time.After(20 * time.Second): 305 ret = false 306 } 307 cb <- ret 308 }() 309 time.Sleep(100 * time.Millisecond) 310 _, err := j.Push(newTask(convID1, types.ConvLoaderPriorityLow, types.ConvLoaderGeneric)) 311 require.NoError(t, err) 312 require.True(t, <-cb) 313 task, ok := j.PopFront() 314 require.True(t, ok) 315 require.Equal(t, types.ConvLoaderPriorityLow, task.job.Priority) 316 require.Zero(t, j.queue.Len()) 317 318 t.Logf("test priority") 319 order := []types.ConvLoaderPriority{types.ConvLoaderPriorityHigh, types.ConvLoaderPriorityMedium, 320 types.ConvLoaderPriorityLow, types.ConvLoaderPriorityLow} 321 for i := len(order) - 1; i >= 0; i-- { 322 _, err = j.Push(newTask(convID1, order[i], types.ConvLoaderUnique)) 323 require.NoError(t, err) 324 } 325 for i := 0; i < len(order); i++ { 326 task, ok := j.PopFront() 327 require.True(t, ok) 328 require.Equal(t, order[i], task.job.Priority) 329 330 } 331 require.Zero(t, j.queue.Len()) 332 333 t.Logf("test dupe checking") 334 queued, err := j.Push(clTask{job: types.NewConvLoaderJob(convID1, nil, types.ConvLoaderPriorityHigh, 335 types.ConvLoaderGeneric, nil)}) 336 require.NoError(t, err) 337 require.True(t, queued) 338 queued, err = j.Push(clTask{job: types.NewConvLoaderJob(convID2, nil, types.ConvLoaderPriorityHigh, 339 types.ConvLoaderGeneric, nil)}) 340 require.NoError(t, err) 341 require.True(t, queued) 342 queued, err = j.Push(clTask{job: types.NewConvLoaderJob(convID2, &chat1.Pagination{Num: 150}, 343 types.ConvLoaderPriorityHigh, types.ConvLoaderGeneric, nil)}) 344 require.NoError(t, err) 345 require.True(t, queued) 346 queued, err = j.Push(clTask{job: types.NewConvLoaderJob(convID2, &chat1.Pagination{Num: 150}, 347 types.ConvLoaderPriorityHigh, types.ConvLoaderGeneric, nil)}) 348 require.NoError(t, err) 349 require.False(t, queued) 350 queued, err = j.Push(clTask{job: types.NewConvLoaderJob(convID2, &chat1.Pagination{Num: 150}, 351 types.ConvLoaderPriorityHigh, types.ConvLoaderUnique, nil)}) 352 require.NoError(t, err) 353 require.True(t, queued) 354 for i := 0; i < 4; i++ { 355 _, ok := j.PopFront() 356 require.True(t, ok) 357 } 358 require.Zero(t, j.queue.Len()) 359 queued, err = j.Push(clTask{job: types.NewConvLoaderJob(convID2, &chat1.Pagination{Num: 150}, 360 types.ConvLoaderPriorityHigh, types.ConvLoaderGeneric, nil)}) 361 require.NoError(t, err) 362 require.True(t, queued) 363 _, ok = j.PopFront() 364 require.True(t, ok) 365 require.Zero(t, j.queue.Len()) 366 367 t.Logf("test maxsize") 368 j = newJobQueue(2) 369 _, err = j.Push(newTask(convID1, types.ConvLoaderPriorityLow, types.ConvLoaderUnique)) 370 require.NoError(t, err) 371 _, err = j.Push(newTask(convID1, types.ConvLoaderPriorityLow, types.ConvLoaderUnique)) 372 require.NoError(t, err) 373 _, err = j.Push(newTask(convID1, types.ConvLoaderPriorityLow, types.ConvLoaderUnique)) 374 require.Error(t, err) 375 }