1 /* <lambda>null2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.paging 18 19 import androidx.kruth.assertThat 20 import androidx.paging.LoadType.APPEND 21 import androidx.paging.LoadType.PREPEND 22 import androidx.paging.LoadType.REFRESH 23 import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED 24 import androidx.paging.RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH 25 import androidx.paging.RemoteMediator.InitializeAction.SKIP_INITIAL_REFRESH 26 import androidx.paging.RemoteMediatorMock.LoadEvent 27 import androidx.paging.TestPagingSource.Companion.LOAD_ERROR 28 import androidx.paging.internal.AtomicBoolean 29 import kotlin.test.Test 30 import kotlin.test.assertEquals 31 import kotlin.test.fail 32 import kotlinx.coroutines.ExperimentalCoroutinesApi 33 import kotlinx.coroutines.delay 34 import kotlinx.coroutines.launch 35 import kotlinx.coroutines.test.StandardTestDispatcher 36 import kotlinx.coroutines.test.TestScope 37 import kotlinx.coroutines.test.UnconfinedTestDispatcher 38 import kotlinx.coroutines.test.advanceTimeBy 39 import kotlinx.coroutines.test.advanceUntilIdle 40 import kotlinx.coroutines.test.runCurrent 41 import kotlinx.coroutines.test.runTest 42 43 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalPagingApi::class) 44 class RemoteMediatorAccessorTest { 45 private val testScope = TestScope(UnconfinedTestDispatcher()) 46 private var mockStateId = 0 47 48 // creates a unique state using the anchor position to be able to do equals check in assertions 49 private fun createMockState(anchorPosition: Int? = mockStateId++): PagingState<Int, Int> { 50 return PagingState( 51 pages = listOf(), 52 anchorPosition = anchorPosition, 53 config = PagingConfig(10), 54 leadingPlaceholderCount = COUNT_UNDEFINED 55 ) 56 } 57 58 @Test 59 fun requestLoadIfRefreshAllowed_noop() = 60 testScope.runTest { 61 val remoteMediator = 62 RemoteMediatorMock(loadDelay = 100).apply { 63 initializeResult = SKIP_INITIAL_REFRESH 64 } 65 val remoteMediatorAccessor = createAccessor(remoteMediator) 66 val pagingState = PagingState<Int, Int>(listOf(), null, PagingConfig(1), 0) 67 68 remoteMediatorAccessor.requestRefreshIfAllowed(pagingState) 69 advanceUntilIdle() 70 assertThat(remoteMediator.newLoadEvents).isEmpty() 71 } 72 73 @Test 74 fun requestLoadIfRefreshAllowed_simple() = 75 testScope.runTest { 76 val remoteMediator = 77 RemoteMediatorMock(loadDelay = 100).apply { 78 initializeResult = LAUNCH_INITIAL_REFRESH 79 } 80 val remoteMediatorAccessor = createAccessor(remoteMediator) 81 val pagingState = PagingState<Int, Int>(listOf(), null, PagingConfig(1), 0) 82 83 remoteMediatorAccessor.allowRefresh() 84 remoteMediatorAccessor.requestRefreshIfAllowed(pagingState) 85 advanceUntilIdle() 86 assertThat(remoteMediator.newLoadEvents) 87 .containsExactly(LoadEvent(loadType = REFRESH, state = pagingState)) 88 89 // allowRefresh should only allow one successful request to go through. 90 remoteMediatorAccessor.requestRefreshIfAllowed(pagingState) 91 advanceUntilIdle() 92 assertThat(remoteMediator.newLoadEvents).isEmpty() 93 } 94 95 @Test 96 fun requestLoadIfRefreshAllowed_retry() = 97 testScope.runTest { 98 val remoteMediator = 99 RemoteMediatorMock(loadDelay = 100).apply { 100 initializeResult = LAUNCH_INITIAL_REFRESH 101 } 102 val remoteMediatorAccessor = createAccessor(remoteMediator) 103 val pagingState = PagingState<Int, Int>(listOf(), null, PagingConfig(1), 0) 104 105 remoteMediator.loadCallback = { _, _ -> 106 RemoteMediator.MediatorResult.Error(Exception()) 107 } 108 109 remoteMediatorAccessor.allowRefresh() 110 remoteMediatorAccessor.requestRefreshIfAllowed(pagingState) 111 advanceUntilIdle() 112 assertThat(remoteMediator.newLoadEvents) 113 .containsExactly(LoadEvent(loadType = REFRESH, state = pagingState)) 114 115 remoteMediator.loadCallback = { _, _ -> 116 RemoteMediator.MediatorResult.Success(endOfPaginationReached = false) 117 } 118 119 remoteMediatorAccessor.retryFailed(pagingState) 120 advanceUntilIdle() 121 assertThat(remoteMediator.newLoadEvents) 122 .containsExactly(LoadEvent(loadType = REFRESH, state = pagingState)) 123 } 124 125 @Test 126 fun requestLoad_queuesBoundaryBehindRefresh() = 127 testScope.runTest { 128 val remoteMediator = RemoteMediatorMock(loadDelay = 100) 129 val remoteMediatorAccessor = createAccessor(remoteMediator) 130 val firstState = createMockState() 131 val secondState = createMockState() 132 133 remoteMediatorAccessor.requestLoad(REFRESH, firstState) 134 advanceTimeBy(50) // Start remote refresh, but do not let it finish. 135 assertThat(remoteMediator.newLoadEvents).containsExactly(LoadEvent(REFRESH, firstState)) 136 assertThat(remoteMediatorAccessor.state.value) 137 .isEqualTo( 138 LoadStates( 139 refresh = LoadState.Loading, 140 prepend = LoadState.NotLoading.Incomplete, 141 append = LoadState.NotLoading.Incomplete 142 ) 143 ) 144 145 // Queue a boundary requests, but it should not launch since refresh is running. 146 remoteMediatorAccessor.requestLoad(PREPEND, firstState) 147 remoteMediatorAccessor.requestLoad(APPEND, firstState) 148 assertThat(remoteMediator.newLoadEvents).isEmpty() 149 assertThat(remoteMediatorAccessor.state.value) 150 .isEqualTo( 151 LoadStates( 152 refresh = LoadState.Loading, 153 prepend = LoadState.Loading, 154 append = LoadState.Loading 155 ) 156 ) 157 158 // Queue more boundary requests, but with an updated PagingState. 159 remoteMediatorAccessor.requestLoad(PREPEND, secondState) 160 remoteMediatorAccessor.requestLoad(APPEND, secondState) 161 assertThat(remoteMediator.newLoadEvents).isEmpty() 162 assertThat(remoteMediatorAccessor.state.value) 163 .isEqualTo( 164 LoadStates( 165 refresh = LoadState.Loading, 166 prepend = LoadState.Loading, 167 append = LoadState.Loading 168 ) 169 ) 170 171 // Now wait until all queued requests finish running 172 advanceUntilIdle() 173 assertThat(remoteMediator.newLoadEvents) 174 .containsExactly( 175 LoadEvent(PREPEND, secondState), 176 LoadEvent(APPEND, secondState), 177 ) 178 assertThat(remoteMediatorAccessor.state.value) 179 .isEqualTo( 180 LoadStates( 181 refresh = LoadState.NotLoading.Incomplete, 182 prepend = LoadState.NotLoading.Incomplete, 183 append = LoadState.NotLoading.Incomplete 184 ) 185 ) 186 } 187 188 @Test 189 fun requestLoad_cancelledBoundaryRetriesAfterRefresh() = 190 testScope.runTest { 191 val remoteMediator = 192 RemoteMediatorMock(loadDelay = 100).apply { 193 initializeResult = SKIP_INITIAL_REFRESH 194 } 195 val remoteMediatorAccessor = createAccessor(remoteMediator) 196 val firstState = createMockState() 197 198 // Launch boundary calls, but do not let them finish. 199 remoteMediatorAccessor.requestLoad(PREPEND, firstState) 200 // Single runner should prevent append from triggering, but it should still be queued. 201 remoteMediatorAccessor.requestLoad(APPEND, firstState) 202 advanceTimeBy(50) 203 assertThat(remoteMediator.newLoadEvents) 204 .containsExactly( 205 LoadEvent(PREPEND, firstState), 206 ) 207 208 // Launch refresh, which should cancel running boundary calls 209 remoteMediatorAccessor.requestLoad(REFRESH, firstState) 210 advanceTimeBy(50) 211 assertThat(remoteMediator.newLoadEvents).containsExactly(LoadEvent(REFRESH, firstState)) 212 213 // Let refresh finish, retrying cancelled boundary calls 214 advanceUntilIdle() 215 assertThat(remoteMediator.newLoadEvents) 216 .containsExactly( 217 LoadEvent(PREPEND, firstState), 218 LoadEvent(APPEND, firstState), 219 ) 220 } 221 222 @Test 223 fun requestLoad_queuesBoundaryAfterRefreshFails() = 224 testScope.runTest { 225 val firstState = createMockState() 226 val secondState = createMockState() 227 val remoteMediator = 228 RemoteMediatorMock(loadDelay = 100).apply { 229 initializeResult = RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH 230 loadCallback = { loadType, state -> 231 // Only error out on first refresh. 232 if (loadType == REFRESH && state == firstState) { 233 RemoteMediator.MediatorResult.Error(throwable = LOAD_ERROR) 234 } else { 235 RemoteMediator.MediatorResult.Success(endOfPaginationReached = false) 236 } 237 } 238 } 239 val remoteMediatorAccessor = createAccessor(remoteMediator) 240 241 // Queue up some remote boundary calls, which will not run immediately because they 242 // depend on refresh. 243 remoteMediatorAccessor.requestLoad(PREPEND, firstState) 244 remoteMediatorAccessor.requestLoad(APPEND, firstState) 245 246 // Trigger refresh, letting it fail. 247 remoteMediatorAccessor.requestLoad(REFRESH, firstState) 248 advanceUntilIdle() 249 // Boundary calls should be queued, but not started. 250 assertThat(remoteMediator.newLoadEvents) 251 .containsExactly( 252 LoadEvent(REFRESH, firstState), 253 ) 254 // Although boundary calls are queued, they should not trigger or update LoadState since 255 // they are waiting for refresh to succeed. 256 assertThat(remoteMediatorAccessor.state.value) 257 .isEqualTo( 258 LoadStates( 259 refresh = LoadState.Error(LOAD_ERROR), 260 prepend = LoadState.NotLoading.Incomplete, 261 append = LoadState.NotLoading.Incomplete 262 ) 263 ) 264 265 // Let refresh finish, triggering queued boundary calls. 266 remoteMediatorAccessor.retryFailed(secondState) 267 advanceUntilIdle() 268 assertThat(remoteMediator.newLoadEvents) 269 .containsExactly( 270 LoadEvent(REFRESH, secondState), 271 LoadEvent(PREPEND, firstState), 272 LoadEvent(APPEND, firstState), 273 ) 274 assertThat(remoteMediatorAccessor.state.value) 275 .isEqualTo( 276 LoadStates( 277 refresh = LoadState.NotLoading.Incomplete, 278 prepend = LoadState.NotLoading.Incomplete, 279 append = LoadState.NotLoading.Incomplete 280 ) 281 ) 282 } 283 284 @Test 285 fun requestLoad_refreshEndOfPaginationReachedClearsBoundaryCalls() = 286 testScope.runTest { 287 val remoteMediator = 288 RemoteMediatorMock(loadDelay = 100).apply { 289 initializeResult = RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH 290 loadCallback = { _, _ -> 291 RemoteMediator.MediatorResult.Success(endOfPaginationReached = true) 292 } 293 } 294 val remoteMediatorAccessor = createAccessor(remoteMediator) 295 val firstState = createMockState() 296 297 // Queue up some remote boundary calls, which will not run immediately because they 298 // depend on refresh. 299 remoteMediatorAccessor.requestLoad(PREPEND, firstState) 300 remoteMediatorAccessor.requestLoad(APPEND, firstState) 301 302 // Trigger refresh and let it mark endOfPaginationReached 303 remoteMediatorAccessor.requestLoad(REFRESH, firstState) 304 advanceUntilIdle() 305 306 // Ensure boundary calls are not triggered since they should be cleared by 307 // endOfPaginationReached from refresh. 308 assertThat(remoteMediator.newLoadEvents).containsExactly(LoadEvent(REFRESH, firstState)) 309 // Although boundary calls are queued, they should not trigger or update LoadState since 310 // they are waiting for refresh. 311 assertThat(remoteMediatorAccessor.state.value) 312 .isEqualTo( 313 LoadStates( 314 refresh = LoadState.NotLoading.Incomplete, 315 prepend = LoadState.NotLoading.Complete, 316 append = LoadState.NotLoading.Complete 317 ) 318 ) 319 } 320 321 @Test 322 fun load_reportsPrependLoadState() = 323 testScope.runTest { 324 val emptyState = 325 PagingState<Int, Int>(listOf(), null, PagingConfig(10), COUNT_UNDEFINED) 326 val remoteMediator = RemoteMediatorMock(loadDelay = 1000) 327 val remoteMediatorAccessor = createAccessor(remoteMediator) 328 329 // Assert initial state is NotLoading.Incomplete. 330 assertEquals( 331 LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Incomplete), 332 remoteMediatorAccessor.state.value, 333 ) 334 335 // Start a PREPEND load. 336 remoteMediatorAccessor.requestLoad( 337 loadType = PREPEND, 338 pagingState = emptyState, 339 ) 340 341 // Assert state is immediately set to Loading. 342 assertEquals( 343 LoadStates.IDLE.copy(prepend = LoadState.Loading), 344 remoteMediatorAccessor.state.value, 345 ) 346 347 // Wait for load to finish. 348 advanceUntilIdle() 349 350 // Assert state is set to NotLoading.Incomplete. 351 assertEquals( 352 LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Incomplete), 353 remoteMediatorAccessor.state.value, 354 ) 355 356 // Start a PREPEND load which results in endOfPaginationReached = true. 357 remoteMediator.loadCallback = { _, _ -> 358 RemoteMediator.MediatorResult.Success(endOfPaginationReached = true) 359 } 360 remoteMediatorAccessor.requestLoad( 361 loadType = PREPEND, 362 pagingState = emptyState, 363 ) 364 365 // Wait for load to finish. 366 advanceUntilIdle() 367 368 // Assert state is set to NotLoading.Incomplete. 369 assertEquals( 370 LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Complete), 371 remoteMediatorAccessor.state.value, 372 ) 373 } 374 375 @Test 376 fun load_reportsAppendLoadState() = 377 testScope.runTest { 378 val emptyState = 379 PagingState<Int, Int>(listOf(), null, PagingConfig(10), COUNT_UNDEFINED) 380 val remoteMediator = RemoteMediatorMock(loadDelay = 1000) 381 val remoteMediatorAccessor = createAccessor(remoteMediator) 382 383 // Assert initial state is NotLoading.Incomplete. 384 assertEquals( 385 LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Incomplete), 386 remoteMediatorAccessor.state.value, 387 ) 388 389 // Start a APPEND load. 390 remoteMediatorAccessor.requestLoad( 391 loadType = APPEND, 392 pagingState = emptyState, 393 ) 394 395 // Assert state is immediately set to Loading. 396 assertEquals( 397 LoadStates.IDLE.copy(append = LoadState.Loading), 398 remoteMediatorAccessor.state.value, 399 ) 400 401 // Wait for load to finish. 402 advanceUntilIdle() 403 404 // Assert state is set to NotLoading.Incomplete. 405 assertEquals( 406 LoadStates.IDLE.copy(append = LoadState.NotLoading.Incomplete), 407 remoteMediatorAccessor.state.value, 408 ) 409 410 // Start a APPEND load which results in endOfPaginationReached = true. 411 remoteMediator.loadCallback = { _, _ -> 412 RemoteMediator.MediatorResult.Success(endOfPaginationReached = true) 413 } 414 remoteMediatorAccessor.requestLoad( 415 loadType = APPEND, 416 pagingState = emptyState, 417 ) 418 419 // Wait for load to finish. 420 advanceUntilIdle() 421 422 // Assert state is set to NotLoading.Incomplete. 423 assertEquals( 424 LoadStates.IDLE.copy(append = LoadState.NotLoading.Complete), 425 remoteMediatorAccessor.state.value, 426 ) 427 } 428 429 @Test 430 fun load_conflatesPrepend() = 431 testScope.runTest { 432 val remoteMediator = RemoteMediatorMock(loadDelay = 1000) 433 val remoteMediatorAccessor = createAccessor(remoteMediator) 434 435 remoteMediatorAccessor.requestLoad( 436 loadType = PREPEND, 437 pagingState = PagingState(listOf(), null, PagingConfig(10), COUNT_UNDEFINED) 438 ) 439 440 remoteMediatorAccessor.requestLoad( 441 loadType = PREPEND, 442 pagingState = PagingState(listOf(), null, PagingConfig(10), COUNT_UNDEFINED) 443 ) 444 445 // Assert that exactly one load request was started. 446 assertEquals(1, remoteMediator.newLoadEvents.size) 447 448 // Fast-forward time until both load requests jobs complete. 449 advanceUntilIdle() 450 451 // Assert that the second load request was skipped since it was launched while the first 452 // load request was still running. 453 assertEquals(0, remoteMediator.newLoadEvents.size) 454 } 455 456 @Test 457 fun load_conflatesAppend() = 458 testScope.runTest { 459 val remoteMediator = RemoteMediatorMock(loadDelay = 1000) 460 val remoteMediatorAccessor = createAccessor(remoteMediator) 461 462 remoteMediatorAccessor.requestLoad( 463 loadType = APPEND, 464 pagingState = PagingState(listOf(), null, PagingConfig(10), COUNT_UNDEFINED) 465 ) 466 467 remoteMediatorAccessor.requestLoad( 468 loadType = APPEND, 469 pagingState = PagingState(listOf(), null, PagingConfig(10), COUNT_UNDEFINED) 470 ) 471 472 // Assert that exactly one load request was started. 473 assertEquals(1, remoteMediator.newLoadEvents.size) 474 475 // Fast-forward time until both load requests jobs complete. 476 advanceUntilIdle() 477 478 // Assert that the second load request was skipped since it was launched while the first 479 // load request was still running. 480 assertEquals(0, remoteMediator.newLoadEvents.size) 481 } 482 483 @Test 484 fun load_conflatesRefresh() = 485 testScope.runTest { 486 val remoteMediator = RemoteMediatorMock(loadDelay = 1000) 487 val remoteMediatorAccessor = createAccessor(remoteMediator) 488 489 remoteMediatorAccessor.requestLoad( 490 loadType = REFRESH, 491 pagingState = PagingState(listOf(), null, PagingConfig(10), COUNT_UNDEFINED) 492 ) 493 494 remoteMediatorAccessor.requestLoad( 495 loadType = REFRESH, 496 pagingState = PagingState(listOf(), null, PagingConfig(10), COUNT_UNDEFINED) 497 ) 498 499 // Assert that exactly one load request was started. 500 assertEquals(1, remoteMediator.newLoadEvents.size) 501 502 // Fast-forward time until both load requests jobs complete. 503 advanceUntilIdle() 504 505 // Assert that the second load request was skipped since it was launched while the first 506 // load request was still running. 507 assertEquals(0, remoteMediator.newLoadEvents.size) 508 } 509 510 @Test 511 fun load_concurrentInitializeJobCancelsBoundaryJobs() = 512 testScope.runTest { 513 val emptyState = 514 PagingState<Int, Int>(listOf(), null, PagingConfig(10), COUNT_UNDEFINED) 515 val remoteMediator = 516 object : RemoteMediatorMock(loadDelay = 1000) { 517 var loading = AtomicBoolean(false) 518 519 override suspend fun load( 520 loadType: LoadType, 521 state: PagingState<Int, Int> 522 ): MediatorResult { 523 if (!loading.compareAndSet(false, true)) fail("Concurrent load") 524 525 return try { 526 super.load(loadType, state) 527 } finally { 528 loading.set(false) 529 } 530 } 531 } 532 val remoteMediatorAccessor = createAccessor(remoteMediator) 533 534 remoteMediatorAccessor.requestLoad(loadType = PREPEND, pagingState = emptyState) 535 536 remoteMediatorAccessor.requestLoad(loadType = APPEND, pagingState = emptyState) 537 538 // Start prependJob and appendJob, but do not let them finish. 539 advanceTimeBy(500) 540 541 // Assert that only the PREPEND RemoteMediator.load() call was made. 542 assertEquals(listOf(LoadEvent(PREPEND, emptyState)), remoteMediator.newLoadEvents) 543 544 // Start refreshJob 545 remoteMediatorAccessor.requestLoad(loadType = REFRESH, pagingState = emptyState) 546 547 // Give prependJob enough time to be cancelled and refresh started due to higher 548 // priority 549 advanceTimeBy(500) 550 551 assertEquals(listOf(LoadEvent(REFRESH, emptyState)), remoteMediator.newLoadEvents) 552 // assert that all of them are in loading state as we don't know if refresh will succeed 553 // if refresh fails, we would retry append / prepend 554 assertEquals( 555 LoadStates( 556 refresh = LoadState.Loading, 557 append = LoadState.Loading, 558 prepend = LoadState.Loading 559 ), 560 remoteMediatorAccessor.state.value 561 ) 562 563 // Wait for all outstanding / queued jobs to finish. 564 advanceUntilIdle() 565 566 // Assert all outstanding / queued jobs finished. 567 assertEquals( 568 LoadStates( 569 refresh = LoadState.NotLoading.Incomplete, 570 append = LoadState.NotLoading.Incomplete, 571 prepend = LoadState.NotLoading.Incomplete 572 ), 573 remoteMediatorAccessor.state.value 574 ) 575 576 // Queued boundary requests should be triggered, even though they are out-of-date. 577 assertThat(remoteMediator.newLoadEvents) 578 .containsExactly( 579 LoadEvent(PREPEND, emptyState), 580 LoadEvent(APPEND, emptyState), 581 ) 582 } 583 584 @Test 585 fun load_concurrentBoundaryJobsRunsSerially() = 586 testScope.runTest { 587 val emptyState = 588 PagingState<Int, Int>(listOf(), null, PagingConfig(10), COUNT_UNDEFINED) 589 val remoteMediator = 590 object : RemoteMediatorMock(loadDelay = 1000) { 591 var loading = AtomicBoolean(false) 592 593 override suspend fun load( 594 loadType: LoadType, 595 state: PagingState<Int, Int> 596 ): MediatorResult { 597 if (!loading.compareAndSet(false, true)) fail("Concurrent load") 598 599 return try { 600 super.load(loadType, state) 601 } finally { 602 loading.set(false) 603 } 604 } 605 } 606 607 val remoteMediatorAccessor = createAccessor(remoteMediator) 608 609 remoteMediatorAccessor.requestLoad(loadType = PREPEND, pagingState = emptyState) 610 611 remoteMediatorAccessor.requestLoad(loadType = APPEND, pagingState = emptyState) 612 613 // Assert that only one job runs due to second job joining the first before starting. 614 assertEquals(1, remoteMediator.newLoadEvents.size) 615 616 // Advance some time, but not enough to finish first load. 617 advanceTimeBy(500) 618 assertEquals(0, remoteMediator.newLoadEvents.size) 619 620 // Assert that second job starts after first finishes. 621 advanceTimeBy(500) 622 runCurrent() 623 assertEquals(1, remoteMediator.newLoadEvents.size) 624 625 // Allow second job to finish. 626 advanceTimeBy(1000) 627 } 628 629 @Test 630 fun ignoreAppendPrependWhenRefreshIsRequired() { 631 val remoteMediatorMock = 632 RemoteMediatorMock().apply { 633 initializeResult = RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH 634 } 635 val accessor = testScope.createAccessor(remoteMediatorMock) 636 accessor.requestLoad(APPEND, createMockState()) 637 accessor.requestLoad(PREPEND, createMockState()) 638 testScope.advanceUntilIdle() 639 assertThat(remoteMediatorMock.loadEvents).isEmpty() 640 } 641 642 @Test 643 fun allowAppendPrependWhenRefreshIsNotRequired() { 644 val remoteMediatorMock = 645 RemoteMediatorMock().apply { initializeResult = SKIP_INITIAL_REFRESH } 646 val accessor = testScope.createAccessor(remoteMediatorMock) 647 648 val appendState = createMockState(1) 649 val prependState = createMockState(2) 650 accessor.requestLoad(APPEND, appendState) 651 accessor.requestLoad(PREPEND, prependState) 652 testScope.advanceUntilIdle() 653 assertThat(remoteMediatorMock.loadEvents) 654 .containsExactly(LoadEvent(APPEND, appendState), LoadEvent(PREPEND, prependState)) 655 } 656 657 @Test 658 fun ignoreAppendPrependBeforeRefresh() { 659 val remoteMediatorMock = 660 RemoteMediatorMock(loadDelay = 100).apply { 661 initializeResult = RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH 662 } 663 val testScope = TestScope(StandardTestDispatcher()) 664 val accessor = testScope.createAccessor(remoteMediatorMock) 665 666 val refreshState = createMockState() 667 accessor.requestLoad(REFRESH, refreshState) 668 // no requests yet since scope is not triggered 669 assertThat(remoteMediatorMock.loadEvents).isEmpty() 670 671 // advance enough to trigger the request, not enough to complete 672 testScope.advanceTimeBy(50) 673 // these should be ignored 674 accessor.requestLoad(REFRESH, createMockState()) 675 676 testScope.advanceTimeBy(1) 677 val appendState = createMockState() 678 accessor.requestLoad(APPEND, appendState) 679 680 val prependState = createMockState() 681 testScope.advanceTimeBy(1) 682 accessor.requestLoad(PREPEND, prependState) 683 684 assertThat(remoteMediatorMock.newLoadEvents) 685 .containsExactly(LoadEvent(REFRESH, refreshState)) 686 687 // now advance enough that we can accept append prepend 688 testScope.advanceUntilIdle() 689 // queued append/prepend should be executed afterwards. 690 assertThat(remoteMediatorMock.newLoadEvents) 691 .containsExactly( 692 LoadEvent(APPEND, appendState), 693 LoadEvent(PREPEND, prependState), 694 ) 695 696 val otherPrependState = createMockState() 697 val otherAppendState = createMockState() 698 accessor.requestLoad(PREPEND, otherPrependState) 699 accessor.requestLoad(APPEND, otherAppendState) 700 701 testScope.advanceTimeBy(50) 702 assertThat(remoteMediatorMock.newLoadEvents) 703 .containsExactly(LoadEvent(PREPEND, otherPrependState)) 704 // while prepend running, any more requests should be ignored 705 accessor.requestLoad(PREPEND, createMockState()) 706 testScope.advanceTimeBy(10) 707 assertThat(remoteMediatorMock.newLoadEvents).isEmpty() 708 709 testScope.advanceTimeBy(41) 710 assertThat(remoteMediatorMock.newLoadEvents) 711 .containsExactly(LoadEvent(APPEND, otherAppendState)) 712 accessor.requestLoad(APPEND, createMockState()) 713 // while append running, any more requests should be ignored 714 accessor.requestLoad(APPEND, createMockState()) 715 testScope.advanceUntilIdle() 716 assertThat(remoteMediatorMock.newLoadEvents).isEmpty() 717 718 // now the work is done, we can add more 719 val newAppendState = createMockState() 720 accessor.requestLoad(APPEND, newAppendState) 721 testScope.advanceUntilIdle() 722 assertThat(remoteMediatorMock.newLoadEvents) 723 .containsExactly(LoadEvent(APPEND, newAppendState)) 724 } 725 726 @Test 727 fun dropAppendPrependIfRefreshIsTriggered() { 728 val remoteMediatorMock = 729 RemoteMediatorMock(loadDelay = 100).apply { initializeResult = SKIP_INITIAL_REFRESH } 730 val accessor = testScope.createAccessor(remoteMediatorMock) 731 val initialAppend = createMockState() 732 accessor.requestLoad(APPEND, initialAppend) 733 testScope.advanceTimeBy(50) 734 assertThat(remoteMediatorMock.newLoadEvents) 735 .containsExactly(LoadEvent(APPEND, initialAppend)) 736 // now before that append finishes, trigger a refresh 737 val newRefresh = createMockState() 738 accessor.requestLoad(REFRESH, newRefresh) 739 testScope.advanceTimeBy(10) 740 // check that we immediately get the new refresh because we'll cancel the append 741 assertThat(remoteMediatorMock.newLoadEvents).containsExactly(LoadEvent(REFRESH, newRefresh)) 742 assertThat(remoteMediatorMock.incompleteEvents) 743 .containsExactly(LoadEvent(APPEND, initialAppend)) 744 } 745 746 @Test 747 fun loadEvents() { 748 val remoteMediatorMock = 749 RemoteMediatorMock(loadDelay = 100).apply { initializeResult = SKIP_INITIAL_REFRESH } 750 val accessor = testScope.createAccessor(remoteMediatorMock) 751 752 // Initial state 753 assertThat(accessor.state.value).isEqualTo(LoadStates.IDLE) 754 755 // Append request should go through since it doesn't require refresh 756 val firstAppendState = createMockState() 757 accessor.requestLoad(APPEND, firstAppendState) 758 testScope.advanceTimeBy(40) 759 assertThat(accessor.state.value) 760 .isEqualTo( 761 LoadStates( 762 refresh = LoadState.NotLoading.Incomplete, 763 prepend = LoadState.NotLoading.Incomplete, 764 append = LoadState.Loading 765 ) 766 ) 767 assertThat(remoteMediatorMock.newLoadEvents) 768 .containsExactly(LoadEvent(APPEND, firstAppendState)) 769 770 // Trigger refresh, cancelling remote append 771 val firstRefreshState = createMockState() 772 accessor.requestLoad(REFRESH, firstRefreshState) 773 testScope.advanceTimeBy(1) 774 assertThat(accessor.state.value) 775 .isEqualTo( 776 LoadStates( 777 refresh = LoadState.Loading, 778 prepend = LoadState.NotLoading.Incomplete, 779 append = LoadState.Loading 780 ) 781 ) 782 // advance enough to complete refresh 783 testScope.advanceUntilIdle() 784 // assert that we receive refresh, and append is retried since it was cancelled 785 assertThat(remoteMediatorMock.newLoadEvents) 786 .containsExactly( 787 LoadEvent(REFRESH, firstRefreshState), 788 LoadEvent(APPEND, firstAppendState), 789 ) 790 assertThat(accessor.state.value) 791 .isEqualTo( 792 LoadStates( 793 refresh = LoadState.NotLoading.Incomplete, 794 prepend = LoadState.NotLoading.Incomplete, 795 append = LoadState.NotLoading.Incomplete 796 ) 797 ) 798 799 val appendState = createMockState() 800 accessor.requestLoad(APPEND, appendState) 801 val prependState = createMockState() 802 accessor.requestLoad(PREPEND, prependState) 803 testScope.advanceTimeBy(50) 804 // both states should be set to loading even though prepend is not really running 805 assertThat(accessor.state.value) 806 .isEqualTo( 807 LoadStates( 808 refresh = LoadState.NotLoading.Incomplete, 809 prepend = LoadState.Loading, 810 append = LoadState.Loading 811 ) 812 ) 813 assertThat(remoteMediatorMock.newLoadEvents).containsExactly(LoadEvent(APPEND, appendState)) 814 // advance enough to trigger prepend 815 testScope.advanceTimeBy(51) 816 assertThat(accessor.state.value) 817 .isEqualTo( 818 LoadStates( 819 refresh = LoadState.NotLoading.Incomplete, 820 prepend = LoadState.Loading, 821 append = LoadState.NotLoading.Incomplete 822 ) 823 ) 824 assertThat(remoteMediatorMock.newLoadEvents) 825 .containsExactly(LoadEvent(PREPEND, prependState)) 826 testScope.advanceUntilIdle() 827 val exception = Throwable() 828 remoteMediatorMock.loadCallback = { type, _ -> 829 if (type == PREPEND) { 830 RemoteMediator.MediatorResult.Error(exception) 831 } else { 832 null 833 } 834 } 835 accessor.requestLoad(APPEND, createMockState()) 836 accessor.requestLoad(PREPEND, createMockState()) 837 testScope.advanceUntilIdle() 838 assertThat(accessor.state.value) 839 .isEqualTo( 840 LoadStates( 841 refresh = LoadState.NotLoading.Incomplete, 842 prepend = LoadState.Error(exception), 843 append = LoadState.NotLoading.Incomplete 844 ) 845 ) 846 // now complete append, a.k.a. endOfPaginationReached 847 remoteMediatorMock.loadCallback = { type, _ -> 848 if (type == APPEND) { 849 RemoteMediator.MediatorResult.Success(endOfPaginationReached = true) 850 } else { 851 null 852 } 853 } 854 accessor.requestLoad(APPEND, createMockState()) 855 testScope.advanceUntilIdle() 856 assertThat(accessor.state.value) 857 .isEqualTo( 858 LoadStates( 859 refresh = LoadState.NotLoading.Incomplete, 860 prepend = LoadState.Error(exception), 861 append = LoadState.NotLoading.Complete 862 ) 863 ) 864 // clear events 865 remoteMediatorMock.newLoadEvents 866 // another append request should just be ignored 867 accessor.requestLoad(APPEND, createMockState()) 868 testScope.advanceUntilIdle() 869 assertThat(remoteMediatorMock.newLoadEvents).isEmpty() 870 assertThat(accessor.state.value) 871 .isEqualTo( 872 LoadStates( 873 refresh = LoadState.NotLoading.Incomplete, 874 prepend = LoadState.Error(exception), 875 append = LoadState.NotLoading.Complete 876 ) 877 ) 878 val refreshState = createMockState() 879 accessor.requestLoad(REFRESH, refreshState) 880 testScope.advanceTimeBy(50) 881 // prepend error state is still present 882 assertThat(accessor.state.value) 883 .isEqualTo( 884 LoadStates( 885 refresh = LoadState.Loading, 886 prepend = LoadState.Error(exception), 887 append = LoadState.NotLoading.Complete 888 ) 889 ) 890 testScope.advanceUntilIdle() 891 // if refresh succeeds, it will clear the error state for refresh 892 assertThat(accessor.state.value).isEqualTo(LoadStates.IDLE) 893 assertThat(remoteMediatorMock.newLoadEvents) 894 .containsExactly(LoadEvent(REFRESH, refreshState)) 895 } 896 897 @Test 898 fun retry_refresh() { 899 val remoteMediatorMock = 900 RemoteMediatorMock(loadDelay = 100).apply { initializeResult = SKIP_INITIAL_REFRESH } 901 val exception = Exception() 902 val accessor = testScope.createAccessor(remoteMediatorMock) 903 remoteMediatorMock.loadCallback = { loadType, _ -> 904 delay(100) 905 if (loadType == REFRESH) { 906 RemoteMediator.MediatorResult.Error(exception) 907 } else { 908 null 909 } 910 } 911 val firstRefreshState = createMockState() 912 accessor.requestLoad(REFRESH, firstRefreshState) 913 assertThat(remoteMediatorMock.newLoadEvents) 914 .containsExactly(LoadEvent(REFRESH, firstRefreshState)) 915 testScope.advanceUntilIdle() 916 assertThat(accessor.state.value.refresh).isEqualTo(LoadState.Error(exception)) 917 val retryState = createMockState() 918 accessor.retryFailed(retryState) 919 testScope.advanceUntilIdle() 920 assertThat(remoteMediatorMock.newLoadEvents).containsExactly(LoadEvent(REFRESH, retryState)) 921 } 922 923 @Test 924 fun failedRefreshShouldNotAllowAppendPrependIfRefreshIsRequired() { 925 val remoteMediatorMock = 926 RemoteMediatorMock(loadDelay = 100).apply { 927 initializeResult = RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH 928 } 929 val exception = Exception() 930 val accessor = testScope.createAccessor(remoteMediatorMock) 931 remoteMediatorMock.loadCallback = { _, _ -> 932 delay(100) 933 RemoteMediator.MediatorResult.Error(exception) 934 } 935 val initialState = createMockState() 936 accessor.requestLoad(REFRESH, initialState) 937 // even though we are sending append prepend, they won't really trigger since refresh will 938 // fail 939 940 // ensure that we didn't set append/prepend to loading when refresh is required 941 assertThat(accessor.state.value) 942 .isEqualTo(LoadStates.IDLE.modifyState(REFRESH, LoadState.Loading)) 943 testScope.advanceUntilIdle() 944 // make sure only refresh has happened 945 assertThat(remoteMediatorMock.newLoadEvents) 946 .containsExactly(LoadEvent(REFRESH, initialState)) 947 assertThat(accessor.state.value) 948 .isEqualTo(LoadStates.IDLE.modifyState(REFRESH, LoadState.Error(exception))) 949 } 950 951 @Test 952 fun failedRefreshShouldAllowAppendPrependIfRefreshIsNotRequired() { 953 val remoteMediatorMock = 954 RemoteMediatorMock(loadDelay = 100).apply { initializeResult = SKIP_INITIAL_REFRESH } 955 val exception = Exception() 956 val accessor = testScope.createAccessor(remoteMediatorMock) 957 remoteMediatorMock.loadCallback = { loadType, _ -> 958 delay(100) 959 if (loadType != APPEND) { 960 RemoteMediator.MediatorResult.Error(exception) 961 } else { 962 null // let append succeed 963 } 964 } 965 val initialState = createMockState() 966 accessor.requestLoad(REFRESH, initialState) 967 accessor.requestLoad(PREPEND, initialState) 968 accessor.requestLoad(APPEND, initialState) 969 // make sure we optimistically updated append prepend states 970 assertThat(accessor.state.value) 971 .isEqualTo( 972 LoadStates( 973 refresh = LoadState.Loading, 974 append = LoadState.Loading, 975 prepend = LoadState.Loading 976 ) 977 ) 978 testScope.advanceUntilIdle() 979 // make sure all requests did happen eventually 980 assertThat(remoteMediatorMock.newLoadEvents) 981 .containsExactly( 982 LoadEvent(REFRESH, initialState), 983 LoadEvent(PREPEND, initialState), 984 LoadEvent(APPEND, initialState) 985 ) 986 assertThat(accessor.state.value) 987 .isEqualTo( 988 LoadStates( 989 refresh = LoadState.Error(exception), 990 append = LoadState.NotLoading.Incomplete, 991 prepend = LoadState.Error(exception) 992 ) 993 ) 994 } 995 996 @Test 997 fun retry_retryBothAppendAndPrepend() { 998 val remoteMediatorMock = 999 RemoteMediatorMock(loadDelay = 100).apply { initializeResult = SKIP_INITIAL_REFRESH } 1000 val exception = Exception() 1001 val accessor = testScope.createAccessor(remoteMediatorMock) 1002 remoteMediatorMock.loadCallback = { _, _ -> 1003 delay(100) 1004 RemoteMediator.MediatorResult.Error(exception) 1005 } 1006 val appendState = createMockState() 1007 val prependState = createMockState() 1008 accessor.requestLoad(PREPEND, prependState) 1009 accessor.requestLoad(APPEND, appendState) 1010 testScope.advanceUntilIdle() 1011 assertThat(remoteMediatorMock.newLoadEvents) 1012 .containsExactly(LoadEvent(PREPEND, prependState), LoadEvent(APPEND, appendState)) 1013 // now retry, ensure both runs 1014 val retryState = createMockState() 1015 accessor.retryFailed(retryState) 1016 // make sure they both move to loading 1017 assertThat(accessor.state.value) 1018 .isEqualTo( 1019 LoadStates( 1020 refresh = LoadState.NotLoading.Incomplete, 1021 append = LoadState.Loading, 1022 prepend = LoadState.Loading 1023 ) 1024 ) 1025 testScope.advanceUntilIdle() 1026 // ensure they both got called 1027 assertThat(remoteMediatorMock.newLoadEvents) 1028 .containsExactly(LoadEvent(PREPEND, retryState), LoadEvent(APPEND, retryState)) 1029 // make sure new loading states are correct 1030 assertThat(accessor.state.value) 1031 .isEqualTo( 1032 LoadStates( 1033 refresh = LoadState.NotLoading.Incomplete, 1034 append = LoadState.Error(exception), 1035 prepend = LoadState.Error(exception) 1036 ) 1037 ) 1038 } 1039 1040 @Test 1041 fun retry_multipleTriggersOnlyRefresh() { 1042 val remoteMediator = 1043 object : RemoteMediatorMock(100) { 1044 override suspend fun initialize(): InitializeAction { 1045 return SKIP_INITIAL_REFRESH 1046 } 1047 } 1048 val exception = Exception() 1049 remoteMediator.loadCallback = { _, _ -> 1050 // fail all 1051 delay(60) 1052 RemoteMediator.MediatorResult.Error(exception) 1053 } 1054 val accessor = testScope.createAccessor(remoteMediator) 1055 accessor.requestLoad(REFRESH, createMockState()) 1056 accessor.requestLoad(APPEND, createMockState()) 1057 accessor.requestLoad(PREPEND, createMockState()) 1058 assertThat(accessor.state.value) 1059 .isEqualTo( 1060 LoadStates( 1061 refresh = LoadState.Loading, 1062 append = LoadState.Loading, 1063 prepend = LoadState.Loading 1064 ) 1065 ) 1066 // let refresh start but don't let it finish 1067 testScope.advanceUntilIdle() 1068 // get all errors 1069 assertThat(accessor.state.value) 1070 .isEqualTo( 1071 LoadStates( 1072 refresh = LoadState.Error(exception), 1073 append = LoadState.Error(exception), 1074 prepend = LoadState.Error(exception) 1075 ) 1076 ) 1077 // let requests succeed 1078 remoteMediator.loadCallback = null 1079 val retryState = createMockState() 1080 accessor.retryFailed(retryState) 1081 assertThat(accessor.state.value) 1082 .isEqualTo( 1083 LoadStates( 1084 refresh = LoadState.Loading, 1085 append = LoadState.NotLoading.Incomplete, 1086 prepend = LoadState.NotLoading.Incomplete 1087 ) 1088 ) 1089 } 1090 1091 @Test 1092 fun failingRefreshRetriesAppendPrepend_refreshNotRequired() { 1093 val remoteMediator = 1094 object : RemoteMediatorMock(100) { 1095 override suspend fun initialize(): InitializeAction { 1096 return SKIP_INITIAL_REFRESH 1097 } 1098 } 1099 val exception = Exception() 1100 remoteMediator.loadCallback = { type, _ -> 1101 // only fail for refresh 1102 if (type == REFRESH) { 1103 delay(60) 1104 RemoteMediator.MediatorResult.Error(exception) 1105 } else { 1106 null 1107 } 1108 } 1109 val accessor = testScope.createAccessor(remoteMediator) 1110 accessor.requestLoad(REFRESH, createMockState()) 1111 accessor.requestLoad(APPEND, createMockState()) 1112 accessor.requestLoad(PREPEND, createMockState()) 1113 assertThat(accessor.state.value) 1114 .isEqualTo( 1115 LoadStates( 1116 refresh = LoadState.Loading, 1117 append = LoadState.Loading, 1118 prepend = LoadState.Loading 1119 ) 1120 ) 1121 // let refresh start but don't let it finish 1122 testScope.advanceTimeBy(50) 1123 // make sure refresh does not revert the append / prepend states 1124 assertThat(accessor.state.value) 1125 .isEqualTo( 1126 LoadStates( 1127 refresh = LoadState.Loading, 1128 append = LoadState.Loading, 1129 prepend = LoadState.Loading 1130 ) 1131 ) 1132 // let refresh fail, it should retry append prepend 1133 testScope.advanceTimeBy(20) 1134 assertThat(accessor.state.value) 1135 .isEqualTo( 1136 LoadStates( 1137 refresh = LoadState.Error(exception), 1138 append = LoadState.Loading, 1139 prepend = LoadState.Loading 1140 ) 1141 ) 1142 // let the prepend retry start 1143 testScope.advanceTimeBy(100) 1144 assertThat(accessor.state.value) 1145 .isEqualTo( 1146 LoadStates( 1147 refresh = LoadState.Error(exception), 1148 append = LoadState.NotLoading.Incomplete, 1149 prepend = LoadState.Loading 1150 ) 1151 ) 1152 testScope.advanceUntilIdle() 1153 assertThat(accessor.state.value) 1154 .isEqualTo( 1155 LoadStates( 1156 refresh = LoadState.Error(exception), 1157 append = LoadState.NotLoading.Incomplete, 1158 prepend = LoadState.NotLoading.Incomplete 1159 ) 1160 ) 1161 } 1162 1163 @Test 1164 fun loadMoreRefreshShouldRetryRefresh() { 1165 // see: b/173438474 1166 val remoteMediator = RemoteMediatorMock(loadDelay = 100) 1167 val exception = Exception() 1168 remoteMediator.loadCallback = { _, _ -> 1169 delay(60) 1170 RemoteMediator.MediatorResult.Error(exception) 1171 } 1172 val accessor = testScope.createAccessor(remoteMediator) 1173 val state1 = createMockState() 1174 accessor.requestLoad(REFRESH, state1) 1175 assertThat(accessor.state.value.refresh).isEqualTo(LoadState.Loading) 1176 // run to get the error 1177 testScope.advanceUntilIdle() 1178 assertThat(accessor.state.value.refresh).isEqualTo(LoadState.Error(exception)) 1179 // now send another load type refresh, should trigger another load 1180 remoteMediator.loadCallback = null // let it succeed 1181 val state2 = createMockState() 1182 accessor.requestLoad(REFRESH, state2) 1183 assertThat(accessor.state.value.refresh).isEqualTo(LoadState.Loading) 1184 testScope.advanceUntilIdle() 1185 assertThat(accessor.state.value.refresh).isEqualTo(LoadState.NotLoading.Incomplete) 1186 } 1187 1188 @Test 1189 fun loadMoreRefreshShouldRetryRefresh_withAppendPrependErrors() { 1190 // see: b/173438474 1191 val remoteMediator = RemoteMediatorMock(loadDelay = 100) 1192 val exception = Exception() 1193 remoteMediator.loadCallback = { _, _ -> 1194 delay(60) 1195 RemoteMediator.MediatorResult.Error(exception) 1196 } 1197 val accessor = testScope.createAccessor(remoteMediator) 1198 val state1 = createMockState() 1199 accessor.requestLoad(REFRESH, state1) 1200 accessor.requestLoad(APPEND, state1) 1201 accessor.requestLoad(PREPEND, state1) 1202 // run to get the error 1203 testScope.advanceUntilIdle() 1204 assertThat(accessor.state.value) 1205 .isEqualTo( 1206 LoadStates( 1207 refresh = LoadState.Error(exception), 1208 prepend = LoadState.Error(exception), 1209 append = LoadState.Error(exception), 1210 ) 1211 ) 1212 // now send another load type refresh, should trigger another load 1213 remoteMediator.loadCallback = null // let it succeed 1214 val state2 = createMockState() 1215 accessor.requestLoad(REFRESH, state2) 1216 assertThat(accessor.state.value) 1217 .isEqualTo( 1218 LoadStates( 1219 refresh = LoadState.Loading, 1220 prepend = LoadState.Error(exception), // keep errors for these for now 1221 append = LoadState.Error(exception), 1222 ) 1223 ) 1224 testScope.advanceUntilIdle() 1225 assertThat(accessor.state.value) 1226 .isEqualTo( 1227 LoadStates( 1228 refresh = LoadState.NotLoading.Incomplete, 1229 prepend = LoadState.NotLoading.Incomplete, // clear errors 1230 append = LoadState.NotLoading.Incomplete, 1231 ) 1232 ) 1233 } 1234 1235 @Test 1236 fun loadMoreRefreshShouldRetryRefresh_withAppendPrependErrors_secondRefreshFails() { 1237 // see: b/173438474 1238 val remoteMediator = RemoteMediatorMock(loadDelay = 100) 1239 val exception1 = Exception("1") 1240 remoteMediator.loadCallback = { _, _ -> 1241 delay(60) 1242 RemoteMediator.MediatorResult.Error(exception1) 1243 } 1244 val accessor = testScope.createAccessor(remoteMediator) 1245 val state1 = createMockState() 1246 accessor.requestLoad(REFRESH, state1) 1247 accessor.requestLoad(APPEND, state1) 1248 accessor.requestLoad(PREPEND, state1) 1249 // run to get the error 1250 testScope.advanceUntilIdle() 1251 assertThat(accessor.state.value) 1252 .isEqualTo( 1253 LoadStates( 1254 refresh = LoadState.Error(exception1), 1255 prepend = LoadState.Error(exception1), 1256 append = LoadState.Error(exception1), 1257 ) 1258 ) 1259 // now send another load type refresh, should trigger another load 1260 val exception2 = Exception("2") 1261 remoteMediator.loadCallback = { _, _ -> 1262 delay(60) 1263 RemoteMediator.MediatorResult.Error(exception2) 1264 } 1265 val state2 = createMockState() 1266 accessor.requestLoad(REFRESH, state2) 1267 assertThat(accessor.state.value) 1268 .isEqualTo( 1269 LoadStates( 1270 refresh = LoadState.Loading, 1271 prepend = LoadState.Error(exception1), // keep errors for these for now 1272 append = LoadState.Error(exception1), 1273 ) 1274 ) 1275 testScope.advanceUntilIdle() 1276 assertThat(accessor.state.value) 1277 .isEqualTo( 1278 LoadStates( 1279 refresh = LoadState.Error(exception2), 1280 prepend = LoadState.Error(exception1), // these keep their original exceptions 1281 append = LoadState.Error(exception1), 1282 ) 1283 ) 1284 } 1285 1286 @Test 1287 fun requireRetry_append() { 1288 requireRetry(APPEND) 1289 } 1290 1291 @Test 1292 fun requireRetry_prepend() { 1293 requireRetry(PREPEND) 1294 } 1295 1296 private fun requireRetry(loadType: LoadType) { 1297 // ensure that we don't retry a failed request until a retry arrives. 1298 val remoteMediator = RemoteMediatorMock(100) 1299 val exception = Exception() 1300 remoteMediator.loadCallback = { _, _ -> 1301 delay(60) 1302 RemoteMediator.MediatorResult.Error(exception) 1303 } 1304 val accessor = testScope.createAccessor(remoteMediator) 1305 val state1 = createMockState() 1306 accessor.requestLoad(loadType, state1) 1307 assertThat(accessor.state.value.get(loadType)).isEqualTo(LoadState.Loading) 1308 testScope.advanceUntilIdle() 1309 assertThat(accessor.state.value.get(loadType)).isEqualTo(LoadState.Error(exception)) 1310 assertThat(remoteMediator.newLoadEvents).containsExactly(LoadEvent(loadType, state1)) 1311 // subsequent add calls shouldn't do anything 1312 accessor.requestLoad(loadType, createMockState()) 1313 assertThat(accessor.state.value.get(loadType)).isEqualTo(LoadState.Error(exception)) 1314 testScope.advanceUntilIdle() 1315 assertThat(accessor.state.value.get(loadType)).isEqualTo(LoadState.Error(exception)) 1316 assertThat(remoteMediator.newLoadEvents).isEmpty() 1317 1318 // if we send a retry, then it will work 1319 remoteMediator.loadCallback = null 1320 val retryState = createMockState() 1321 accessor.retryFailed(retryState) 1322 assertThat(accessor.state.value.get(loadType)).isEqualTo(LoadState.Loading) 1323 testScope.advanceUntilIdle() 1324 assertThat(remoteMediator.newLoadEvents).containsExactly(LoadEvent(loadType, retryState)) 1325 assertThat(accessor.state.value.get(loadType)).isEqualTo(LoadState.NotLoading.Incomplete) 1326 } 1327 1328 private fun TestScope.createAccessor( 1329 mediator: RemoteMediatorMock 1330 ): RemoteMediatorAccessor<Int, Int> { 1331 val accessor = RemoteMediatorAccessor(scope = this, delegate = mediator) 1332 TestScope().launch(coroutineContext) { accessor.initialize() } 1333 return accessor 1334 } 1335 } 1336