blob: a53bc0b22e8188eff6a92f4d2ff24f34a1fef896 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@file:Suppress("DEPRECATION")
package androidx.paging
import androidx.paging.ItemKeyedDataSourceTest.ItemDataSource
import androidx.paging.PagedList.BoundaryCallback
import androidx.paging.PagedList.Callback
import androidx.paging.PagedList.Config
import androidx.paging.PagingSource.LoadResult.Page
import androidx.testutils.TestDispatcher
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.reset
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import kotlin.test.assertFailsWith
import kotlin.test.assertNotSame
@RunWith(Parameterized::class)
class ContiguousPagedListTest(private val placeholdersEnabled: Boolean) {
private val mainThread = TestDispatcher()
private val backgroundThread = TestDispatcher()
private class Item(position: Int) {
val pos: Int = position
val name: String = "Item $position"
override fun toString(): String = name
}
/**
* Note: we use a non-positional dataSource here because we want to avoid the initial load size
* and alignment restrictions. These tests were written before positional+contiguous enforced
* these behaviors.
*/
private inner class TestPagingSource(val listData: List<Item> = ITEMS) :
PagingSource<Int, Item>() {
override fun getRefreshKey(state: PagingState<Int, Item>): Int? {
return state.anchorPosition
?.let { anchorPosition -> state.closestItemToPosition(anchorPosition)?.pos }
?: 0
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
return when (params.loadType) {
LoadType.REFRESH -> loadInitial(params)
LoadType.START -> loadBefore(params)
LoadType.END -> loadAfter(params)
}
}
fun enqueueErrorForIndex(index: Int) {
errorIndices.add(index)
}
val errorIndices = mutableListOf<Int>()
private fun loadInitial(params: LoadParams<Int>): LoadResult<Int, Item> {
val initPos = params.key ?: 0
val start = maxOf(initPos - params.loadSize / 2, 0)
val result = getClampedRange(start, start + params.loadSize)
return when {
result == null -> LoadResult.Error(EXCEPTION)
placeholdersEnabled -> Page(
data = result,
prevKey = result.firstOrNull()?.pos,
nextKey = result.lastOrNull()?.pos,
itemsBefore = start,
itemsAfter = listData.size - result.size - start
)
else -> Page(
data = result,
prevKey = result.firstOrNull()?.pos,
nextKey = result.lastOrNull()?.pos
)
}
}
private fun loadAfter(params: LoadParams<Int>): LoadResult<Int, Item> {
val result = getClampedRange(params.key!! + 1, params.key!! + 1 + params.loadSize)
?: return LoadResult.Error(EXCEPTION)
return Page(
data = result,
prevKey = if (result.isNotEmpty()) result.first().pos else null,
nextKey = if (result.isNotEmpty()) result.last().pos else null
)
}
private fun loadBefore(params: LoadParams<Int>): LoadResult<Int, Item> {
val result = getClampedRange(params.key!! - params.loadSize, params.key!!)
?: return LoadResult.Error(EXCEPTION)
return Page(
data = result,
prevKey = result.firstOrNull()?.pos,
nextKey = result.lastOrNull()?.pos
)
}
private fun getClampedRange(startInc: Int, endExc: Int): List<Item>? {
val matching = errorIndices.filter { it in startInc until endExc }
if (matching.isNotEmpty()) {
// found indices with errors enqueued - fail to load them
errorIndices.removeAll(matching)
return null
}
return listData.subList(maxOf(0, startInc), minOf(listData.size, endExc))
}
}
private fun PagingSource<*, Item>.enqueueErrorForIndex(index: Int) {
(this as TestPagingSource).enqueueErrorForIndex(index)
}
private fun <E> MutableList<E>.getAllAndClear(): List<E> {
val data = this.toList()
assertNotSame(data, this)
this.clear()
return data
}
private fun <E : Any> PagedList<E>.addLoadStateCapture(desiredType: LoadType):
Pair<Any, MutableList<StateChange>> {
val list = mutableListOf<StateChange>()
val listener = { type: LoadType, state: LoadState ->
if (type == desiredType) {
list.add(StateChange(type, state))
}
}
addWeakLoadStateListener(listener)
return Pair(listener, list)
}
private fun verifyRange(start: Int, count: Int, actual: PagedStorage<Item>) {
if (placeholdersEnabled) {
// assert nulls + content
val expected = arrayOfNulls<Item>(ITEMS.size)
System.arraycopy(ITEMS.toTypedArray(), start, expected, start, count)
assertEquals(expected.toList(), actual)
val expectedTrailing = ITEMS.size - start - count
assertEquals(ITEMS.size, actual.size)
assertEquals(start, actual.placeholdersStart)
assertEquals(expectedTrailing, actual.placeholdersEnd)
} else {
assertEquals(ITEMS.subList(start, start + count), actual)
assertEquals(count, actual.size)
assertEquals(0, actual.placeholdersStart)
assertEquals(0, actual.placeholdersEnd)
}
assertEquals(count, actual.storageCount)
}
private fun verifyRange(start: Int, count: Int, actual: PagedList<Item>) {
verifyRange(start, count, actual.storage)
assertEquals(count, actual.loadedCount)
}
private fun PagingSource<Int, Item>.getInitialPage(
initialKey: Int,
loadSize: Int,
pageSize:
Int
): Page<Int, Item> = runBlocking {
val result = load(
PagingSource.LoadParams(
LoadType.REFRESH,
initialKey,
loadSize,
placeholdersEnabled,
pageSize
)
)
result as? Page ?: throw RuntimeException("Unexpected load failure")
}
private fun createCountedPagedList(
initialPosition: Int?,
pageSize: Int = 20,
initLoadSize: Int = 40,
prefetchDistance: Int = 20,
listData: List<Item> = ITEMS,
boundaryCallback: BoundaryCallback<Item>? = null,
maxSize: Int = Config.MAX_SIZE_UNBOUNDED,
pagingSource: PagingSource<Int, Item> = TestPagingSource(listData)
): PagedList<Item> {
val initialPage = pagingSource.getInitialPage(
initialPosition ?: 0,
initLoadSize,
pageSize
)
val config = Config.Builder()
.setPageSize(pageSize)
.setInitialLoadSizeHint(initLoadSize)
.setPrefetchDistance(prefetchDistance)
.setMaxSize(maxSize)
.setEnablePlaceholders(placeholdersEnabled)
.build()
return PagedList.Builder(pagingSource, initialPage, config)
.setBoundaryCallback(boundaryCallback)
.setFetchDispatcher(backgroundThread)
.setNotifyDispatcher(mainThread)
.setInitialKey(initialPosition)
.build()
}
@Test
fun construct() {
val pagedList = createCountedPagedList(0)
verifyRange(0, 40, pagedList)
}
@Test
fun getDataSource() {
// Create a pagedList with a pagingSource directly.
val pagedListWithPagingSource = createCountedPagedList(0)
@Suppress("DEPRECATION")
assertFailsWith<IllegalStateException> { pagedListWithPagingSource.dataSource }
@Suppress("DEPRECATION")
val pagedListWithDataSource = PagedList.Builder(ItemDataSource(), 10).build()
@Suppress("DEPRECATION")
assertTrue(pagedListWithDataSource.dataSource is ItemDataSource)
// snapshot keeps same DataSource
@Suppress("DEPRECATION")
assertSame(
pagedListWithDataSource.dataSource,
(pagedListWithDataSource.snapshot() as SnapshotPagedList<*>).dataSource
)
}
@Test
fun getPagingSource() {
val pagedList = createCountedPagedList(0)
assertTrue(pagedList.pagingSource is TestPagingSource)
// snapshot keeps same DataSource
@Suppress("DEPRECATION")
assertSame(
pagedList.pagingSource,
(pagedList.snapshot() as SnapshotPagedList<Item>).pagingSource
)
}
@Test(expected = IndexOutOfBoundsException::class)
fun loadAroundNegative() {
val pagedList = createCountedPagedList(0)
pagedList.loadAround(-1)
}
@Test(expected = IndexOutOfBoundsException::class)
fun loadAroundTooLarge() {
val pagedList = createCountedPagedList(0)
pagedList.loadAround(pagedList.size)
}
private fun verifyCallback(
callback: Callback,
countedPosition: Int,
uncountedPosition: Int
) {
if (placeholdersEnabled) {
verify(callback).onChanged(countedPosition, 20)
} else {
verify(callback).onInserted(uncountedPosition, 20)
}
}
private fun verifyCallback(callback: Callback, position: Int) {
verifyCallback(callback, position, position)
}
private fun verifyDropCallback(
callback: Callback,
countedPosition: Int,
uncountedPosition: Int
) {
if (placeholdersEnabled) {
verify(callback).onChanged(countedPosition, 20)
} else {
verify(callback).onRemoved(uncountedPosition, 20)
}
}
private fun verifyDropCallback(callback: Callback, position: Int) {
verifyDropCallback(callback, position, position)
}
@Test
fun append() {
val pagedList = createCountedPagedList(0)
val callback = mock<Callback>()
pagedList.addWeakCallback(callback)
verifyRange(0, 40, pagedList)
verifyZeroInteractions(callback)
pagedList.loadAround(35)
drain()
verifyRange(0, 60, pagedList)
verifyCallback(callback, 40)
verifyNoMoreInteractions(callback)
}
@Test
fun prepend() {
val pagedList = createCountedPagedList(80)
val callback = mock<Callback>()
pagedList.addWeakCallback(callback)
verifyRange(60, 40, pagedList)
verifyZeroInteractions(callback)
pagedList.loadAround(if (placeholdersEnabled) 65 else 5)
drain()
verifyRange(40, 60, pagedList)
verifyCallback(callback, 40, 0)
verifyNoMoreInteractions(callback)
}
@Test
fun outwards() {
val pagedList = createCountedPagedList(40)
val callback = mock<Callback>()
pagedList.addWeakCallback(callback)
verifyRange(20, 40, pagedList)
verifyZeroInteractions(callback)
pagedList.loadAround(if (placeholdersEnabled) 55 else 35)
drain()
verifyRange(20, 60, pagedList)
verifyCallback(callback, 60, 40)
verifyNoMoreInteractions(callback)
pagedList.loadAround(if (placeholdersEnabled) 25 else 5)
drain()
verifyRange(0, 80, pagedList)
verifyCallback(callback, 0, 0)
verifyNoMoreInteractions(callback)
}
@Test
fun prefetchRequestedPrepend() {
assertEquals(10, ContiguousPagedList.getPrependItemsRequested(10, 0, 0))
assertEquals(15, ContiguousPagedList.getPrependItemsRequested(10, 0, 5))
assertEquals(0, ContiguousPagedList.getPrependItemsRequested(1, 41, 40))
assertEquals(1, ContiguousPagedList.getPrependItemsRequested(1, 40, 40))
}
@Test
fun prefetchRequestedAppend() {
assertEquals(10, ContiguousPagedList.getAppendItemsRequested(10, 9, 10))
assertEquals(15, ContiguousPagedList.getAppendItemsRequested(10, 9, 5))
assertEquals(0, ContiguousPagedList.getAppendItemsRequested(1, 8, 10))
assertEquals(1, ContiguousPagedList.getAppendItemsRequested(1, 9, 10))
}
@Test
fun prefetchFront() {
val pagedList = createCountedPagedList(
initialPosition = 50,
pageSize = 20,
initLoadSize = 20,
prefetchDistance = 1
)
verifyRange(40, 20, pagedList)
// access adjacent to front, shouldn't trigger prefetch
pagedList.loadAround(if (placeholdersEnabled) 41 else 1)
drain()
verifyRange(40, 20, pagedList)
// access front item, should trigger prefetch
pagedList.loadAround(if (placeholdersEnabled) 40 else 0)
drain()
verifyRange(20, 40, pagedList)
}
@Test
fun prefetchEnd() {
val pagedList = createCountedPagedList(
initialPosition = 50,
pageSize = 20,
initLoadSize = 20,
prefetchDistance = 1
)
verifyRange(40, 20, pagedList)
// access adjacent from end, shouldn't trigger prefetch
pagedList.loadAround(if (placeholdersEnabled) 58 else 18)
drain()
verifyRange(40, 20, pagedList)
// access end item, should trigger prefetch
pagedList.loadAround(if (placeholdersEnabled) 59 else 19)
drain()
verifyRange(40, 40, pagedList)
}
@Test
fun pageDropEnd() {
val pagedList = createCountedPagedList(
initialPosition = 0,
pageSize = 20,
initLoadSize = 20,
prefetchDistance = 1,
maxSize = 70
)
val callback = mock<Callback>()
pagedList.addWeakCallback(callback)
verifyRange(0, 20, pagedList)
verifyZeroInteractions(callback)
// load 2nd page
pagedList.loadAround(19)
drain()
verifyRange(0, 40, pagedList)
verifyCallback(callback, 20)
verifyNoMoreInteractions(callback)
// load 3rd page
pagedList.loadAround(39)
drain()
verifyRange(0, 60, pagedList)
verifyCallback(callback, 40)
verifyNoMoreInteractions(callback)
// load 4th page, drop 1st
pagedList.loadAround(59)
drain()
verifyRange(20, 60, pagedList)
verifyCallback(callback, 60)
verifyDropCallback(callback, 0)
verifyNoMoreInteractions(callback)
}
@Test
fun pageDropFront() {
val pagedList = createCountedPagedList(
initialPosition = 90,
pageSize = 20,
initLoadSize = 20,
prefetchDistance = 1,
maxSize = 70
)
val callback = mock<Callback>()
pagedList.addWeakCallback(callback)
verifyRange(80, 20, pagedList)
verifyZeroInteractions(callback)
// load 4th page
pagedList.loadAround(if (placeholdersEnabled) 80 else 0)
drain()
verifyRange(60, 40, pagedList)
verifyCallback(callback, 60, 0)
verifyNoMoreInteractions(callback)
reset(callback)
// load 3rd page
pagedList.loadAround(if (placeholdersEnabled) 60 else 0)
drain()
verifyRange(40, 60, pagedList)
verifyCallback(callback, 40, 0)
verifyNoMoreInteractions(callback)
reset(callback)
// load 2nd page, drop 5th
pagedList.loadAround(if (placeholdersEnabled) 40 else 0)
drain()
verifyRange(20, 60, pagedList)
verifyCallback(callback, 20, 0)
verifyDropCallback(callback, 80, 60)
verifyNoMoreInteractions(callback)
}
@Test
fun pageDropCancelPrepend() {
// verify that, based on most recent load position, a prepend can be dropped as it arrives
val pagedList = createCountedPagedList(
initialPosition = 2,
pageSize = 1,
initLoadSize = 1,
prefetchDistance = 1,
maxSize = 3
)
// load 3 pages - 2nd, 3rd, 4th
pagedList.loadAround(if (placeholdersEnabled) 2 else 0)
drain()
verifyRange(1, 3, pagedList)
val callback = mock<Callback>()
pagedList.addWeakCallback(callback)
// start a load at the beginning...
pagedList.loadAround(if (placeholdersEnabled) 1 else 0)
backgroundThread.executeAll()
// but before page received, access near end of list
pagedList.loadAround(if (placeholdersEnabled) 3 else 2)
verifyZeroInteractions(callback)
mainThread.executeAll()
// and the load at the beginning is dropped without signaling callback
verifyNoMoreInteractions(callback)
verifyRange(1, 3, pagedList)
drain()
if (placeholdersEnabled) {
verify(callback).onChanged(4, 1)
verify(callback).onChanged(1, 1)
} else {
verify(callback).onInserted(3, 1)
verify(callback).onRemoved(0, 1)
}
verifyRange(2, 3, pagedList)
}
@Test
fun pageDropCancelAppend() {
// verify that, based on most recent load position, an append can be dropped as it arrives
val pagedList = createCountedPagedList(
initialPosition = 2,
pageSize = 1,
initLoadSize = 1,
prefetchDistance = 1,
maxSize = 3
)
// load 3 pages - 2nd, 3rd, 4th
pagedList.loadAround(if (placeholdersEnabled) 2 else 0)
drain()
val callback = mock<Callback>()
pagedList.addWeakCallback(callback)
// start a load at the end...
pagedList.loadAround(if (placeholdersEnabled) 3 else 2)
backgroundThread.executeAll()
// but before page received, access near front of list
pagedList.loadAround(if (placeholdersEnabled) 1 else 0)
verifyZeroInteractions(callback)
mainThread.executeAll()
// and the load at the end is dropped without signaling callback
verifyNoMoreInteractions(callback)
verifyRange(1, 3, pagedList)
drain()
if (placeholdersEnabled) {
verify(callback).onChanged(0, 1)
verify(callback).onChanged(3, 1)
} else {
verify(callback).onInserted(0, 1)
verify(callback).onRemoved(3, 1)
}
verifyRange(0, 3, pagedList)
}
@Test
fun loadingListenerAppend() {
val pagedList = createCountedPagedList(0)
val capture = pagedList.addLoadStateCapture(LoadType.END)
val states = capture.second
// No loading going on currently
assertEquals(
listOf(StateChange(LoadType.END, LoadState.Idle)),
states.getAllAndClear()
)
verifyRange(0, 40, pagedList)
// trigger load
pagedList.loadAround(35)
mainThread.executeAll()
assertEquals(
listOf(StateChange(LoadType.END, LoadState.Loading)),
states.getAllAndClear()
)
verifyRange(0, 40, pagedList)
// load finishes
drain()
assertEquals(
listOf(StateChange(LoadType.END, LoadState.Idle)),
states.getAllAndClear()
)
verifyRange(0, 60, pagedList)
pagedList.pagingSource.enqueueErrorForIndex(65)
// trigger load which will error
pagedList.loadAround(55)
mainThread.executeAll()
assertEquals(
listOf(StateChange(LoadType.END, LoadState.Loading)),
states.getAllAndClear()
)
verifyRange(0, 60, pagedList)
// load now in error state
drain()
assertEquals(
listOf(StateChange(LoadType.END, LoadState.Error(EXCEPTION))),
states.getAllAndClear()
)
verifyRange(0, 60, pagedList)
// retry
pagedList.retry()
mainThread.executeAll()
assertEquals(
listOf(StateChange(LoadType.END, LoadState.Loading)),
states.getAllAndClear()
)
// load finishes
drain()
assertEquals(
listOf(StateChange(LoadType.END, LoadState.Idle)),
states.getAllAndClear()
)
verifyRange(0, 80, pagedList)
}
@Test
fun pageDropCancelPrependError() {
// verify a prepend in error state can be dropped
val pagedList = createCountedPagedList(
initialPosition = 2,
pageSize = 1,
initLoadSize = 1,
prefetchDistance = 1,
maxSize = 3
)
val capture = pagedList.addLoadStateCapture(LoadType.START)
val states = capture.second
// load 3 pages - 2nd, 3rd, 4th
pagedList.loadAround(if (placeholdersEnabled) 2 else 0)
drain()
verifyRange(1, 3, pagedList)
assertEquals(
listOf(
StateChange(LoadType.START, LoadState.Idle),
StateChange(LoadType.START, LoadState.Loading),
StateChange(LoadType.START, LoadState.Idle)
),
states.getAllAndClear()
)
// start a load at the beginning, which will fail
pagedList.pagingSource.enqueueErrorForIndex(0)
pagedList.loadAround(if (placeholdersEnabled) 1 else 0)
drain()
verifyRange(1, 3, pagedList)
assertEquals(
listOf(
StateChange(LoadType.START, LoadState.Loading),
StateChange(LoadType.START, LoadState.Error(EXCEPTION))
),
states.getAllAndClear()
)
// but without that failure being retried, access near end of list, which drops the error
pagedList.loadAround(if (placeholdersEnabled) 3 else 2)
drain()
assertEquals(
listOf(StateChange(LoadType.START, LoadState.Idle)),
states.getAllAndClear()
)
verifyRange(2, 3, pagedList)
}
@Test
fun pageDropCancelAppendError() {
// verify an append in error state can be dropped
val pagedList = createCountedPagedList(
initialPosition = 2,
pageSize = 1,
initLoadSize = 1,
prefetchDistance = 1,
maxSize = 3
)
val capture = pagedList.addLoadStateCapture(LoadType.END)
val states = capture.second
// load 3 pages - 2nd, 3rd, 4th
pagedList.loadAround(if (placeholdersEnabled) 2 else 0)
drain()
verifyRange(1, 3, pagedList)
assertEquals(
listOf(
StateChange(LoadType.END, LoadState.Idle),
StateChange(LoadType.END, LoadState.Loading),
StateChange(LoadType.END, LoadState.Idle)
),
states.getAllAndClear()
)
// start a load at the end, which will fail
pagedList.pagingSource.enqueueErrorForIndex(4)
pagedList.loadAround(if (placeholdersEnabled) 3 else 2)
drain()
verifyRange(1, 3, pagedList)
assertEquals(
listOf(
StateChange(LoadType.END, LoadState.Loading),
StateChange(LoadType.END, LoadState.Error(EXCEPTION))
),
states.getAllAndClear()
)
// but without that failure being retried, access near start of list, which drops the error
pagedList.loadAround(if (placeholdersEnabled) 1 else 0)
drain()
assertEquals(
listOf(StateChange(LoadType.END, LoadState.Idle)),
states.getAllAndClear()
)
verifyRange(0, 3, pagedList)
}
@Test
fun errorIntoDrop() {
// have an error, move loading range, error goes away
val pagedList = createCountedPagedList(0)
val capture = pagedList.addLoadStateCapture(LoadType.END)
val states = capture.second
pagedList.pagingSource.enqueueErrorForIndex(45)
pagedList.loadAround(35)
drain()
assertEquals(
listOf(
StateChange(LoadType.END, LoadState.Idle),
StateChange(LoadType.END, LoadState.Loading),
StateChange(LoadType.END, LoadState.Error(EXCEPTION))
),
states.getAllAndClear()
)
verifyRange(0, 40, pagedList)
}
@Test
fun distantPrefetch() {
val pagedList = createCountedPagedList(
0,
initLoadSize = 10,
pageSize = 10,
prefetchDistance = 30
)
val callback = mock<Callback>()
pagedList.addWeakCallback(callback)
verifyRange(0, 10, pagedList)
verifyZeroInteractions(callback)
pagedList.loadAround(5)
drain()
verifyRange(0, 40, pagedList)
pagedList.loadAround(6)
drain()
// although our prefetch window moves forward, no new load triggered
verifyRange(0, 40, pagedList)
}
@Test
fun appendCallbackAddedLate() {
val pagedList = createCountedPagedList(0)
verifyRange(0, 40, pagedList)
pagedList.loadAround(35)
drain()
verifyRange(0, 60, pagedList)
// snapshot at 60 items
val snapshot = pagedList.snapshot() as PagedList<Item>
val snapshotCopy = snapshot.toList()
verifyRange(0, 60, snapshot)
// load more items...
pagedList.loadAround(55)
drain()
verifyRange(0, 80, pagedList)
verifyRange(0, 60, snapshot)
// and verify the snapshot hasn't received them
assertEquals(snapshotCopy, snapshot)
val callback = mock<Callback>()
@Suppress("DEPRECATION")
pagedList.addWeakCallback(snapshot, callback)
verify(callback).onChanged(0, snapshot.size)
if (!placeholdersEnabled) {
verify(callback).onInserted(60, 20)
}
verifyNoMoreInteractions(callback)
}
@Test
fun prependCallbackAddedLate() {
val pagedList = createCountedPagedList(80)
verifyRange(60, 40, pagedList)
pagedList.loadAround(if (placeholdersEnabled) 65 else 5)
drain()
verifyRange(40, 60, pagedList)
// snapshot at 60 items
val snapshot = pagedList.snapshot() as PagedList<Item>
val snapshotCopy = snapshot.toList()
verifyRange(40, 60, snapshot)
pagedList.loadAround(if (placeholdersEnabled) 45 else 5)
drain()
verifyRange(20, 80, pagedList)
verifyRange(40, 60, snapshot)
assertEquals(snapshotCopy, snapshot)
val callback = mock<Callback>()
@Suppress("DEPRECATION")
pagedList.addWeakCallback(snapshot, callback)
verify(callback).onChanged(0, snapshot.size)
if (!placeholdersEnabled) {
// deprecated snapshot compare dispatches as if inserts occur at the end
verify(callback).onInserted(60, 20)
}
verifyNoMoreInteractions(callback)
}
@Test
fun initialLoad_lastKey() {
val pagedList = createCountedPagedList(
initialPosition = 4,
initLoadSize = 20,
pageSize = 10
)
verifyRange(0, 20, pagedList)
// lastKey should return result of PagingSource.getRefreshKey after loading 20 items.
assertEquals(10, pagedList.lastKey)
// but in practice will be immediately overridden by quick loadAround call
// (e.g. in latching list after diffing, we loadAround immediately, since previous pos of
// viewport should win overall)
pagedList.loadAround(4)
assertEquals(4, pagedList.lastKey)
}
@Test
fun addWeakCallbackLegacyEmpty() {
val pagedList = createCountedPagedList(0)
verifyRange(0, 40, pagedList)
// capture empty snapshot
val initSnapshot = pagedList.snapshot()
assertEquals(pagedList, initSnapshot)
// verify that adding callback notifies naive "everything changed" when snapshot passed
var callback = mock<Callback>()
@Suppress("DEPRECATION")
pagedList.addWeakCallback(initSnapshot, callback)
verify(callback).onChanged(0, pagedList.size)
verifyNoMoreInteractions(callback)
pagedList.removeWeakCallback(callback)
pagedList.loadAround(35)
drain()
verifyRange(0, 60, pagedList)
// verify that adding callback notifies insert going from empty -> content
callback = mock()
@Suppress("DEPRECATION")
pagedList.addWeakCallback(initSnapshot, callback)
verify(callback).onChanged(0, initSnapshot.size)
if (!placeholdersEnabled) {
verify(callback).onInserted(40, 20)
}
verifyNoMoreInteractions(callback)
}
@Test
fun boundaryCallback_empty() {
@Suppress("UNCHECKED_CAST")
val boundaryCallback = mock<BoundaryCallback<Item>>()
val pagedList = createCountedPagedList(
0,
listData = ArrayList(), boundaryCallback = boundaryCallback
)
assertEquals(0, pagedList.size)
// nothing yet
verifyNoMoreInteractions(boundaryCallback)
// onZeroItemsLoaded posted, since creation often happens on BG thread
drain()
verify(boundaryCallback).onZeroItemsLoaded()
verifyNoMoreInteractions(boundaryCallback)
}
@Test
fun boundaryCallback_singleInitialLoad() {
val shortList = ITEMS.subList(0, 4)
@Suppress("UNCHECKED_CAST")
val boundaryCallback = mock<BoundaryCallback<Item>>()
val pagedList = createCountedPagedList(
0, listData = shortList,
initLoadSize = shortList.size, boundaryCallback = boundaryCallback
)
assertEquals(shortList.size, pagedList.size)
// nothing yet
verifyNoMoreInteractions(boundaryCallback)
// onItemAtFrontLoaded / onItemAtEndLoaded posted, since creation often happens on BG thread
drain()
pagedList.loadAround(0)
drain()
verify(boundaryCallback).onItemAtFrontLoaded(shortList.first())
verify(boundaryCallback).onItemAtEndLoaded(shortList.last())
verifyNoMoreInteractions(boundaryCallback)
}
@Test
fun boundaryCallback_delayed() {
@Suppress("UNCHECKED_CAST")
val boundaryCallback = mock<BoundaryCallback<Item>>()
val pagedList = createCountedPagedList(
90,
initLoadSize = 20, prefetchDistance = 5, boundaryCallback = boundaryCallback
)
verifyRange(80, 20, pagedList)
// nothing yet
verifyZeroInteractions(boundaryCallback)
drain()
verifyZeroInteractions(boundaryCallback)
// loading around last item causes onItemAtEndLoaded
pagedList.loadAround(if (placeholdersEnabled) 99 else 19)
drain()
verifyRange(80, 20, pagedList)
verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
verifyNoMoreInteractions(boundaryCallback)
// prepending doesn't trigger callback...
pagedList.loadAround(if (placeholdersEnabled) 80 else 0)
drain()
verifyRange(60, 40, pagedList)
verifyZeroInteractions(boundaryCallback)
// ...load rest of data, still no dispatch...
pagedList.loadAround(if (placeholdersEnabled) 60 else 0)
drain()
pagedList.loadAround(if (placeholdersEnabled) 40 else 0)
drain()
pagedList.loadAround(if (placeholdersEnabled) 20 else 0)
drain()
verifyRange(0, 100, pagedList)
verifyZeroInteractions(boundaryCallback)
// ... finally try prepend, see 0 items, which will dispatch front callback
pagedList.loadAround(0)
drain()
verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
verifyNoMoreInteractions(boundaryCallback)
}
private fun drain() {
while (backgroundThread.queue.isNotEmpty() || mainThread.queue.isNotEmpty()) {
backgroundThread.executeAll()
mainThread.executeAll()
}
}
companion object {
@JvmStatic
@Parameterized.Parameters(name = "counted:{0}")
fun parameters(): Array<Array<Boolean>> {
return arrayOf(arrayOf(true), arrayOf(false))
}
val EXCEPTION = Exception()
private val ITEMS = List(100) { Item(it) }
}
}