Merge "Fix LimitOffsetPagingSourceTest flaky test" into androidx-main
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
index d3f0f18..aec2c6e 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
@@ -200,7 +200,103 @@
     }
 
     @Test
-    fun dataChangesWithDelayedInvalidation() {
+    fun prependWithDelayedInvalidation() {
+        val items = createItems(startId = 0, count = 90)
+        db.dao.insert(items)
+
+        val pager = Pager(
+            config = CONFIG,
+            initialKey = 20,
+            pagingSourceFactory = { db.dao.loadItems().also { pagingSources.add(it) } }
+        )
+
+        runTest(pager) {
+            itemStore.awaitInitialLoad()
+            assertThat(
+                itemStore.peekItems()
+            ).containsExactlyElementsIn(
+                // should load starting from initial Key = 30
+                items.createExpected(
+                    fromIndex = 20,
+                    toIndex = 20 + CONFIG.initialLoadSize
+                )
+            )
+            assertThat(db.invalidationTracker.pendingRefresh).isFalse()
+            // now do some changes in the database but don't let change notifications go through
+            // to the data source. it should not crash :)
+            queryExecutor.filterFunction = { runnable ->
+                runnable !== db.invalidationTracker.refreshRunnable
+            }
+            db.dao.deleteItems(
+                items.subList(0, 60).map { it.id }
+            )
+            // make sure invalidation requests a refresh
+            db.invalidationTracker.awaitPendingRefresh()
+
+            // make sure we blocked the refresh runnable until after the exception generates a
+            // new paging source
+            queryExecutor.awaitDeferredSizeAtLeast(1)
+
+            // Now get more items. The pagingSource's load() will check for invalidation and then
+            // return LoadResult.Invalid, causing a second generation paging source to be generated.
+            itemStore.get(2)
+
+            itemStore.awaitGeneration(2)
+            assertTrue(pagingSources[0].invalid)
+            itemStore.awaitInitialLoad()
+
+            // the initial load triggers a call to refreshVersionsAsync which calls
+            // mRefreshRunnable. The runnable is getting filtered out but we need this one to
+            // complete, so we executed the latest queued mRefreshRunnable.
+            assertThat(queryExecutor.deferredSize()).isEqualTo(2)
+            queryExecutor.executeLatestDeferred()
+            assertThat(queryExecutor.deferredSize()).isEqualTo(1)
+
+            // it might be reloaded in any range so just make sure everything is there
+            // expects 30 items because items 60 - 89 left in database, so presenter should have
+            // items 60-68 from initialLoad + 21 null placeholders
+            assertThat(itemStore.peekItems()).hasSize(30)
+            withContext(Dispatchers.Main) {
+                (0 until 10).forEach {
+                    itemStore.get(it)
+                }
+            }
+            // now ensure all of them are loaded
+            // only waiting for 9 items because because the 10th item and onwards are nulls from
+            // placeholders
+            (0 until 9).forEach {
+                assertThat(
+                    itemStore.awaitItem(it)
+                ).isEqualTo(
+                    items[60 + it]
+                )
+            }
+
+            // Runs the original invalidationTracker.refreshRunnable.
+            // Note that the second initial load's call to mRefreshRunnable resets the flag to
+            // false, so this mRefreshRunnable will not detect changes in the table anymore.
+            assertThat(db.invalidationTracker.pendingRefresh).isFalse()
+            queryExecutor.executeAll()
+
+            itemStore.awaitInitialLoad()
+
+            // make sure only two generations of paging sources have been created
+            assertTrue(!pagingSources[1].invalid)
+
+            // if a third generation is created, awaitGeneration(3) will return instead of timing
+            // out.
+            val expectError = assertFailsWith<AssertionError> {
+                itemStore.awaitGeneration(3)
+            }
+            assertThat(expectError.message).isEqualTo("didn't complete in expected time")
+
+            assertThat(itemStore.currentGenerationId).isEqualTo(2)
+            assertThat(pagingSources.size).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun appendWithDelayedInvalidation() {
         val items = createItems(startId = 0, count = 90)
         db.dao.insert(items)
         runTest {
@@ -234,7 +330,19 @@
 
             itemStore.awaitGeneration(2)
             assertTrue(pagingSources[0].invalid)
+            // initial load is executed but refreshVersionsAsync's call to mRefreshRunnable is
+            // actually queued up here
             itemStore.awaitInitialLoad()
+            // the initial load triggers a call to refreshVersionsAsync which calls
+            // mRefreshRunnable. The runnable is getting filtered out but we need this one to
+            // complete, so we executed the latest queued mRefreshRunnable.
+            assertThat(queryExecutor.deferredSize()).isEqualTo(2)
+            queryExecutor.executeLatestDeferred()
+            assertThat(queryExecutor.deferredSize()).isEqualTo(1)
+
+            // second paging source should be generated
+            assertThat(pagingSources.size).isEqualTo(2)
+
             // it might be reloaded in any range so just make sure everything is there
             assertThat(itemStore.peekItems()).hasSize(10)
             withContext(Dispatchers.Main) {
@@ -250,11 +358,10 @@
                     items[80 + it]
                 )
             }
-            // Runs deferred invalidationTracker.refreshRunnable. Note that the step in
-            // itemStore.get(70) includes checking the invalidation tables & resetting the tracker's
-            // pendingRefresh flag to false.
-            // Therefore, the mRefreshRunnable executed by executeAll() will not detect changes
-            // in the table anymore.
+
+            // Runs the original invalidationTracker.refreshRunnable.
+            // Note that the second initial load's call to mRefreshRunnable resets the flag to
+            // false, so this mRefreshRunnable will not detect changes in the table anymore.
             assertThat(db.invalidationTracker.pendingRefresh).isFalse()
             queryExecutor.executeAll()
 
@@ -546,12 +653,20 @@
             copy.forEach(this::execute)
         }
 
+        fun deferredSize(): Int {
+            return deferred.size
+        }
+
         fun executeAll() {
             while (deferred.isNotEmpty()) {
                 deferred.removeFirst().run()
             }
         }
 
+        fun executeLatestDeferred() {
+            deferred.removeLast().run()
+        }
+
         override fun execute(command: Runnable) {
             lock.withLock {
                 if (filterFunction(command)) {
diff --git a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
index e0e120c..46e50e8 100644
--- a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
+++ b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
@@ -419,11 +419,11 @@
                 itemsList.subList(30, 35)
             )
 
-            // make changes to database
-            dao.deleteTestItem(itemsList[42])
+            // invalidate pagingSource to imitate invalidation from running refreshVersionSync
+            pagingSource.invalidate()
 
-            // this append should check invalidation tables, realize it has been updated,
-            // and return a LoadResult.Invalid
+            // this append should check pagingSource's invalid status, realize it is invalid, and
+            // return a LoadResult.Invalid
             val result2 = pagingSource.append(key = result.nextKey)
 
             assertThat(result2).isInstanceOf(LoadResult.Invalid::class.java)
@@ -505,14 +505,13 @@
                 itemsList.subList(15, 20)
             )
 
-            // now write into database
-            dao.deleteTestItem(itemsList[30])
+            // invalidate pagingSource to imitate invalidation from running refreshVersionSync
+            pagingSource.invalidate()
 
-            // second prepend using prevKey from previous load
+            // this prepend should check pagingSource's invalid status, realize it is invalid, and
+            // return LoadResult.Invalid
             val result2 = pagingSource.prepend(key = result.prevKey)
 
-            // this prepend should check invalidation tables, realize it has been updated,
-            // and return a LoadResult.Invalid
             assertThat(result2).isInstanceOf(LoadResult.Invalid::class.java)
         }
     }