[go: nahoru, domu]

blob: dd4dcaa05c6a2fdaf0bd214e05a05b1998ea9786 [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.
*/
package android.arch.paging
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
@RunWith(JUnit4::class)
class ItemKeyedDataSourceTest {
// ----- STANDARD -----
private fun loadInitial(dataSource: ItemDataSource, key: Key?, initialLoadSize: Int,
enablePlaceholders: Boolean): PageResult<Item> {
@Suppress("UNCHECKED_CAST")
val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<Item>
@Suppress("UNCHECKED_CAST")
val captor = ArgumentCaptor.forClass(PageResult::class.java)
as ArgumentCaptor<PageResult<Item>>
dataSource.dispatchLoadInitial(key, initialLoadSize,
/* ignored pageSize */ 10, enablePlaceholders, FailExecutor(), receiver)
verify(receiver).onPageResult(anyInt(), captor.capture())
verifyNoMoreInteractions(receiver)
assertNotNull(captor.value)
return captor.value
}
@Test
fun loadInitial() {
val dataSource = ItemDataSource()
val result = loadInitial(dataSource, dataSource.getKey(ITEMS_BY_NAME_ID[49]), 10, true)
assertEquals(45, result.leadingNulls)
assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
assertEquals(45, result.trailingNulls)
}
@Test
fun loadInitial_keyMatchesSingleItem() {
val dataSource = ItemDataSource(items = ITEMS_BY_NAME_ID.subList(0, 1))
// this is tricky, since load after and load before with the passed key will fail
val result = loadInitial(dataSource, dataSource.getKey(ITEMS_BY_NAME_ID[0]), 20, true)
assertEquals(0, result.leadingNulls)
assertEquals(ITEMS_BY_NAME_ID.subList(0, 1), result.page)
assertEquals(0, result.trailingNulls)
}
@Test
fun loadInitial_keyMatchesLastItem() {
val dataSource = ItemDataSource()
// tricky, because load after key is empty, so another load before and load after required
val key = dataSource.getKey(ITEMS_BY_NAME_ID.last())
val result = loadInitial(dataSource, key, 20, true)
assertEquals(90, result.leadingNulls)
assertEquals(ITEMS_BY_NAME_ID.subList(90, 100), result.page)
assertEquals(0, result.trailingNulls)
}
@Test
fun loadInitial_nullKey() {
val dataSource = ItemDataSource()
// dispatchLoadInitial(null, count) == dispatchLoadInitial(count)
val result = loadInitial(dataSource, null, 10, true)
assertEquals(0, result.leadingNulls)
assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), result.page)
assertEquals(90, result.trailingNulls)
}
@Test
fun loadInitial_keyPastEndOfList() {
val dataSource = ItemDataSource()
// if key is past entire data set, should return last items in data set
val key = Key("fz", 0)
val result = loadInitial(dataSource, key, 10, true)
// NOTE: ideally we'd load 10 items here, but it adds complexity and unpredictability to
// do: load after was empty, so pass full size to load before, since this can incur larger
// loads than requested (see keyMatchesLastItem test)
assertEquals(95, result.leadingNulls)
assertEquals(ITEMS_BY_NAME_ID.subList(95, 100), result.page)
assertEquals(0, result.trailingNulls)
}
// ----- UNCOUNTED -----
@Test
fun loadInitial_disablePlaceholders() {
val dataSource = ItemDataSource()
// dispatchLoadInitial(key, count) == null padding, loadAfter(key, count), null padding
val key = dataSource.getKey(ITEMS_BY_NAME_ID[49])
val result = loadInitial(dataSource, key, 10, false)
assertEquals(0, result.leadingNulls)
assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
assertEquals(0, result.trailingNulls)
}
@Test
fun loadInitial_uncounted() {
val dataSource = ItemDataSource(counted = false)
// dispatchLoadInitial(key, count) == null padding, loadAfter(key, count), null padding
val key = dataSource.getKey(ITEMS_BY_NAME_ID[49])
val result = loadInitial(dataSource, key, 10, true)
assertEquals(0, result.leadingNulls)
assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
assertEquals(0, result.trailingNulls)
}
@Test
fun loadInitial_nullKey_uncounted() {
val dataSource = ItemDataSource(counted = false)
// dispatchLoadInitial(null, count) == dispatchLoadInitial(count)
val result = loadInitial(dataSource, null, 10, true)
assertEquals(0, result.leadingNulls)
assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), result.page)
assertEquals(0, result.trailingNulls)
}
// ----- EMPTY -----
@Test
fun loadInitial_empty() {
val dataSource = ItemDataSource(items = ArrayList())
// dispatchLoadInitial(key, count) == null padding, loadAfter(key, count), null padding
val key = dataSource.getKey(ITEMS_BY_NAME_ID[49])
val result = loadInitial(dataSource, key, 10, true)
assertEquals(0, result.leadingNulls)
assertTrue(result.page.isEmpty())
assertEquals(0, result.trailingNulls)
}
@Test
fun loadInitial_nullKey_empty() {
val dataSource = ItemDataSource(items = ArrayList())
val result = loadInitial(dataSource, null, 10, true)
assertEquals(0, result.leadingNulls)
assertTrue(result.page.isEmpty())
assertEquals(0, result.trailingNulls)
}
// ----- Other behavior -----
@Test
fun loadBefore() {
val dataSource = ItemDataSource()
@Suppress("UNCHECKED_CAST")
val callback = mock(ItemKeyedDataSource.LoadCallback::class.java)
as ItemKeyedDataSource.LoadCallback<Item>
dataSource.loadBefore(
ItemKeyedDataSource.LoadParams(dataSource.getKey(ITEMS_BY_NAME_ID[5]), 5), callback)
@Suppress("UNCHECKED_CAST")
val argument = ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<Item>>
verify(callback).onResult(argument.capture())
verifyNoMoreInteractions(callback)
val observed = argument.value
assertEquals(ITEMS_BY_NAME_ID.subList(0, 5), observed)
}
internal data class Key(val name: String, val id: Int)
internal data class Item(
val name: String, val id: Int, val balance: Double, val address: String)
internal class ItemDataSource(private val counted: Boolean = true,
private val items: List<Item> = ITEMS_BY_NAME_ID)
: ItemKeyedDataSource<Key, Item>() {
override fun loadInitial(
params: LoadInitialParams<Key>,
callback: LoadInitialCallback<Item>) {
val key = params.requestedInitialKey ?: Key("", Integer.MAX_VALUE)
val start = Math.max(0, findFirstIndexAfter(key) - params.requestedLoadSize / 2)
val endExclusive = Math.min(start + params.requestedLoadSize, items.size)
if (params.placeholdersEnabled && counted) {
callback.onResult(items.subList(start, endExclusive), start, items.size)
} else {
callback.onResult(items.subList(start, endExclusive))
}
}
override fun loadAfter(params: LoadParams<Key>, callback: LoadCallback<Item>) {
val start = findFirstIndexAfter(params.key)
val endExclusive = Math.min(start + params.requestedLoadSize, items.size)
callback.onResult(items.subList(start, endExclusive))
}
override fun loadBefore(params: LoadParams<Key>, callback: LoadCallback<Item>) {
val firstIndexBefore = findFirstIndexBefore(params.key)
val endExclusive = Math.max(0, firstIndexBefore + 1)
val start = Math.max(0, firstIndexBefore - params.requestedLoadSize + 1)
callback.onResult(items.subList(start, endExclusive))
}
override fun getKey(item: Item): Key {
return Key(item.name, item.id)
}
private fun findFirstIndexAfter(key: Key): Int {
return items.indices.firstOrNull {
KEY_COMPARATOR.compare(key, getKey(items[it])) < 0
} ?: items.size
}
private fun findFirstIndexBefore(key: Key): Int {
return items.indices.reversed().firstOrNull {
KEY_COMPARATOR.compare(key, getKey(items[it])) > 0
} ?: -1
}
}
private fun performLoadInitial(
invalidateDataSource: Boolean = false,
callbackInvoker: (callback: ItemKeyedDataSource.LoadInitialCallback<String>) -> Unit) {
val dataSource = object : ItemKeyedDataSource<String, String>() {
override fun getKey(item: String): String {
return ""
}
override fun loadInitial(
params: LoadInitialParams<String>,
callback: LoadInitialCallback<String>) {
if (invalidateDataSource) {
// invalidate data source so it's invalid when onResult() called
invalidate()
}
callbackInvoker(callback)
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String>) {
fail("loadAfter not expected")
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String>) {
fail("loadBefore not expected")
}
}
ContiguousPagedList<String, String>(
dataSource, FailExecutor(), FailExecutor(), null,
PagedList.Config.Builder()
.setPageSize(10)
.build(),
"",
ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
}
@Test
fun loadInitialCallbackSuccess() = performLoadInitial {
// LoadInitialCallback correct usage
it.onResult(listOf("a", "b"), 0, 2)
}
@Test
fun loadInitialCallbackNotPageSizeMultiple() = performLoadInitial {
// Keyed LoadInitialCallback *can* accept result that's not a multiple of page size
val elevenLetterList = List(11) { "" + 'a' + it }
it.onResult(elevenLetterList, 0, 12)
}
@Test(expected = IllegalArgumentException::class)
fun loadInitialCallbackListTooBig() = performLoadInitial {
// LoadInitialCallback can't accept pos + list > totalCount
it.onResult(listOf("a", "b", "c"), 0, 2)
}
@Test(expected = IllegalArgumentException::class)
fun loadInitialCallbackPositionTooLarge() = performLoadInitial {
// LoadInitialCallback can't accept pos + list > totalCount
it.onResult(listOf("a", "b"), 1, 2)
}
@Test(expected = IllegalArgumentException::class)
fun loadInitialCallbackPositionNegative() = performLoadInitial {
// LoadInitialCallback can't accept negative position
it.onResult(listOf("a", "b", "c"), -1, 2)
}
@Test(expected = IllegalArgumentException::class)
fun loadInitialCallbackEmptyCannotHavePlaceholders() = performLoadInitial {
// LoadInitialCallback can't accept empty result unless data set is empty
it.onResult(emptyList(), 0, 2)
}
@Test
fun initialLoadCallbackInvalidThreeArg() = performLoadInitial(invalidateDataSource = true) {
// LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
it.onResult(emptyList(), 0, 1)
}
private abstract class WrapperDataSource<K, A, B>(private val source: ItemKeyedDataSource<K, A>)
: ItemKeyedDataSource<K, B>() {
private val invalidatedCallback = DataSource.InvalidatedCallback {
invalidate()
removeCallback()
}
init {
source.addInvalidatedCallback(invalidatedCallback)
}
private fun removeCallback() {
removeInvalidatedCallback(invalidatedCallback)
}
override fun loadInitial(params: LoadInitialParams<K>, callback: LoadInitialCallback<B>) {
source.loadInitial(params, object : LoadInitialCallback<A>() {
override fun onResult(data: List<A>, position: Int, totalCount: Int) {
callback.onResult(convert(data), position, totalCount)
}
override fun onResult(data: MutableList<A>) {
callback.onResult(convert(data))
}
})
}
override fun loadAfter(params: LoadParams<K>, callback: LoadCallback<B>) {
source.loadAfter(params, object : LoadCallback<A>() {
override fun onResult(data: MutableList<A>) {
callback.onResult(convert(data))
}
})
}
override fun loadBefore(params: LoadParams<K>, callback: LoadCallback<B>) {
source.loadBefore(params, object : LoadCallback<A>() {
override fun onResult(data: MutableList<A>) {
callback.onResult(convert(data))
}
})
}
protected abstract fun convert(source: List<A>): List<B>
}
private data class DecoratedItem(val item: Item)
private class DecoratedWrapperDataSource(private val source: ItemKeyedDataSource<Key, Item>)
: WrapperDataSource<Key, Item, DecoratedItem>(source) {
override fun convert(source: List<Item>): List<DecoratedItem> {
return source.map { DecoratedItem(it) }
}
override fun getKey(item: DecoratedItem): Key {
return source.getKey(item.item)
}
}
@Test
fun simpleWrappedDataSource() {
// verify that it's possible to wrap an ItemKeyedDataSource, and add info to its data
val orig = ItemDataSource(items = ITEMS_BY_NAME_ID)
val wrapper = DecoratedWrapperDataSource(orig)
// load initial
@Suppress("UNCHECKED_CAST")
val loadInitialCallback = mock(ItemKeyedDataSource.LoadInitialCallback::class.java)
as ItemKeyedDataSource.LoadInitialCallback<DecoratedItem>
val initKey = orig.getKey(ITEMS_BY_NAME_ID.first())
wrapper.loadInitial(ItemKeyedDataSource.LoadInitialParams(initKey, 10, false),
loadInitialCallback)
verify(loadInitialCallback).onResult(
ITEMS_BY_NAME_ID.subList(0, 10).map { DecoratedItem(it) })
verifyNoMoreInteractions(loadInitialCallback)
@Suppress("UNCHECKED_CAST")
val loadCallback = mock(ItemKeyedDataSource.LoadCallback::class.java)
as ItemKeyedDataSource.LoadCallback<DecoratedItem>
val key = orig.getKey(ITEMS_BY_NAME_ID[20])
// load after
wrapper.loadAfter(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
verify(loadCallback).onResult(ITEMS_BY_NAME_ID.subList(21, 31).map { DecoratedItem(it) })
verifyNoMoreInteractions(loadCallback)
// load before
wrapper.loadBefore(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
verify(loadCallback).onResult(ITEMS_BY_NAME_ID.subList(10, 20).map { DecoratedItem(it) })
verifyNoMoreInteractions(loadCallback)
}
companion object {
private val ITEM_COMPARATOR = compareBy<Item>({ it.name }).thenByDescending({ it.id })
private val KEY_COMPARATOR = compareBy<Key>({ it.name }).thenByDescending({ it.id })
private val ITEMS_BY_NAME_ID = List(100) {
val names = Array(10) { "f" + ('a' + it) }
Item(names[it % 10],
it,
Math.random() * 1000,
(Math.random() * 200).toInt().toString() + " fake st.")
}.sortedWith(ITEM_COMPARATOR)
}
}