Home | History | Annotate | Download | only in paging
      1 /*
      2  * Copyright (C) 2017 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.arch.core.util.Function
     20 import org.junit.Assert.assertArrayEquals
     21 import org.junit.Assert.assertEquals
     22 import org.junit.Assert.assertFalse
     23 import org.junit.Assert.assertSame
     24 import org.junit.Assert.assertTrue
     25 import org.junit.Test
     26 import org.junit.runner.RunWith
     27 import org.junit.runners.Parameterized
     28 import org.mockito.Mockito.mock
     29 import org.mockito.Mockito.verify
     30 import org.mockito.Mockito.verifyNoMoreInteractions
     31 import org.mockito.Mockito.verifyZeroInteractions
     32 import java.util.concurrent.Executor
     33 
     34 @RunWith(Parameterized::class)
     35 class ContiguousPagedListTest(private val mCounted: Boolean) {
     36     private val mMainThread = TestExecutor()
     37     private val mBackgroundThread = TestExecutor()
     38 
     39     private class Item(position: Int) {
     40         val name: String = "Item $position"
     41 
     42         override fun toString(): String {
     43             return name
     44         }
     45     }
     46 
     47     private inner class TestSource(val listData: List<Item> = ITEMS)
     48             : ContiguousDataSource<Int, Item>() {
     49         override fun dispatchLoadInitial(
     50                 key: Int?,
     51                 initialLoadSize: Int,
     52                 pageSize: Int,
     53                 enablePlaceholders: Boolean,
     54                 mainThreadExecutor: Executor,
     55                 receiver: PageResult.Receiver<Item>) {
     56 
     57             val convertPosition = key ?: 0
     58             val position = Math.max(0, (convertPosition - initialLoadSize / 2))
     59             val data = getClampedRange(position, position + initialLoadSize)
     60             val trailingUnloadedCount = listData.size - position - data.size
     61 
     62             if (enablePlaceholders && mCounted) {
     63                 receiver.onPageResult(PageResult.INIT,
     64                         PageResult(data, position, trailingUnloadedCount, 0))
     65             } else {
     66                 // still must pass offset, even if not counted
     67                 receiver.onPageResult(PageResult.INIT,
     68                         PageResult(data, position))
     69             }
     70         }
     71 
     72         override fun dispatchLoadAfter(
     73                 currentEndIndex: Int,
     74                 currentEndItem: Item,
     75                 pageSize: Int,
     76                 mainThreadExecutor: Executor,
     77                 receiver: PageResult.Receiver<Item>) {
     78             val startIndex = currentEndIndex + 1
     79             val data = getClampedRange(startIndex, startIndex + pageSize)
     80 
     81             mainThreadExecutor.execute {
     82                 receiver.onPageResult(PageResult.APPEND, PageResult(data, 0, 0, 0))
     83             }
     84         }
     85 
     86         override fun dispatchLoadBefore(
     87                 currentBeginIndex: Int,
     88                 currentBeginItem: Item,
     89                 pageSize: Int,
     90                 mainThreadExecutor: Executor,
     91                 receiver: PageResult.Receiver<Item>) {
     92 
     93             val startIndex = currentBeginIndex - 1
     94             val data = getClampedRange(startIndex - pageSize + 1, startIndex + 1)
     95 
     96             mainThreadExecutor.execute {
     97                 receiver.onPageResult(PageResult.PREPEND, PageResult(data, 0, 0, 0))
     98             }
     99         }
    100 
    101         override fun getKey(position: Int, item: Item?): Int {
    102             return 0
    103         }
    104 
    105         private fun getClampedRange(startInc: Int, endExc: Int): List<Item> {
    106             return listData.subList(Math.max(0, startInc), Math.min(listData.size, endExc))
    107         }
    108 
    109         override fun <ToValue : Any?> mapByPage(function: Function<List<Item>, List<ToValue>>):
    110                 DataSource<Int, ToValue> {
    111             throw UnsupportedOperationException()
    112         }
    113 
    114         override fun <ToValue : Any?> map(function: Function<Item, ToValue>):
    115                 DataSource<Int, ToValue> {
    116             throw UnsupportedOperationException()
    117         }
    118     }
    119 
    120     private fun verifyRange(start: Int, count: Int, actual: PagedStorage<Item>) {
    121         if (mCounted) {
    122             // assert nulls + content
    123             val expected = arrayOfNulls<Item>(ITEMS.size)
    124             System.arraycopy(ITEMS.toTypedArray(), start, expected, start, count)
    125             assertArrayEquals(expected, actual.toTypedArray())
    126 
    127             val expectedTrailing = ITEMS.size - start - count
    128             assertEquals(ITEMS.size, actual.size)
    129             assertEquals((ITEMS.size - start - expectedTrailing),
    130                     actual.storageCount)
    131             assertEquals(start, actual.leadingNullCount)
    132             assertEquals(expectedTrailing, actual.trailingNullCount)
    133         } else {
    134             assertEquals(ITEMS.subList(start, start + count), actual)
    135 
    136             assertEquals(count, actual.size)
    137             assertEquals(actual.size, actual.storageCount)
    138             assertEquals(0, actual.leadingNullCount)
    139             assertEquals(0, actual.trailingNullCount)
    140         }
    141     }
    142 
    143     private fun verifyRange(start: Int, count: Int, actual: PagedList<Item>) {
    144         verifyRange(start, count, actual.mStorage)
    145     }
    146 
    147     private fun createCountedPagedList(
    148             initialPosition: Int,
    149             pageSize: Int = 20,
    150             initLoadSize: Int = 40,
    151             prefetchDistance: Int = 20,
    152             listData: List<Item> = ITEMS,
    153             boundaryCallback: PagedList.BoundaryCallback<Item>? = null,
    154             lastLoad: Int = ContiguousPagedList.LAST_LOAD_UNSPECIFIED
    155     ): ContiguousPagedList<Int, Item> {
    156         return ContiguousPagedList(
    157                 TestSource(listData), mMainThread, mBackgroundThread, boundaryCallback,
    158                 PagedList.Config.Builder()
    159                         .setInitialLoadSizeHint(initLoadSize)
    160                         .setPageSize(pageSize)
    161                         .setPrefetchDistance(prefetchDistance)
    162                         .build(),
    163                 initialPosition,
    164                 lastLoad)
    165     }
    166 
    167     @Test
    168     fun construct() {
    169         val pagedList = createCountedPagedList(0)
    170         verifyRange(0, 40, pagedList)
    171     }
    172 
    173     @Test
    174     fun getDataSource() {
    175         val pagedList = createCountedPagedList(0)
    176         assertTrue(pagedList.dataSource is TestSource)
    177 
    178         // snapshot keeps same DataSource
    179         assertSame(pagedList.dataSource,
    180                 (pagedList.snapshot() as SnapshotPagedList<Item>).dataSource)
    181     }
    182 
    183     private fun verifyCallback(callback: PagedList.Callback, countedPosition: Int,
    184             uncountedPosition: Int) {
    185         if (mCounted) {
    186             verify(callback).onChanged(countedPosition, 20)
    187         } else {
    188             verify(callback).onInserted(uncountedPosition, 20)
    189         }
    190     }
    191 
    192     @Test
    193     fun append() {
    194         val pagedList = createCountedPagedList(0)
    195         val callback = mock(PagedList.Callback::class.java)
    196         pagedList.addWeakCallback(null, callback)
    197         verifyRange(0, 40, pagedList)
    198         verifyZeroInteractions(callback)
    199 
    200         pagedList.loadAround(35)
    201         drain()
    202 
    203         verifyRange(0, 60, pagedList)
    204         verifyCallback(callback, 40, 40)
    205         verifyNoMoreInteractions(callback)
    206     }
    207 
    208     @Test
    209     fun prepend() {
    210         val pagedList = createCountedPagedList(80)
    211         val callback = mock(PagedList.Callback::class.java)
    212         pagedList.addWeakCallback(null, callback)
    213         verifyRange(60, 40, pagedList)
    214         verifyZeroInteractions(callback)
    215 
    216         pagedList.loadAround(if (mCounted) 65 else 5)
    217         drain()
    218 
    219         verifyRange(40, 60, pagedList)
    220         verifyCallback(callback, 40, 0)
    221         verifyNoMoreInteractions(callback)
    222     }
    223 
    224     @Test
    225     fun outwards() {
    226         val pagedList = createCountedPagedList(50)
    227         val callback = mock(PagedList.Callback::class.java)
    228         pagedList.addWeakCallback(null, callback)
    229         verifyRange(30, 40, pagedList)
    230         verifyZeroInteractions(callback)
    231 
    232         pagedList.loadAround(if (mCounted) 65 else 35)
    233         drain()
    234 
    235         verifyRange(30, 60, pagedList)
    236         verifyCallback(callback, 70, 40)
    237         verifyNoMoreInteractions(callback)
    238 
    239         pagedList.loadAround(if (mCounted) 35 else 5)
    240         drain()
    241 
    242         verifyRange(10, 80, pagedList)
    243         verifyCallback(callback, 10, 0)
    244         verifyNoMoreInteractions(callback)
    245     }
    246 
    247     @Test
    248     fun multiAppend() {
    249         val pagedList = createCountedPagedList(0)
    250         val callback = mock(PagedList.Callback::class.java)
    251         pagedList.addWeakCallback(null, callback)
    252         verifyRange(0, 40, pagedList)
    253         verifyZeroInteractions(callback)
    254 
    255         pagedList.loadAround(55)
    256         drain()
    257 
    258         verifyRange(0, 80, pagedList)
    259         verifyCallback(callback, 40, 40)
    260         verifyCallback(callback, 60, 60)
    261         verifyNoMoreInteractions(callback)
    262     }
    263 
    264     @Test
    265     fun distantPrefetch() {
    266         val pagedList = createCountedPagedList(0,
    267                 initLoadSize = 10, pageSize = 10, prefetchDistance = 30)
    268         val callback = mock(PagedList.Callback::class.java)
    269         pagedList.addWeakCallback(null, callback)
    270         verifyRange(0, 10, pagedList)
    271         verifyZeroInteractions(callback)
    272 
    273         pagedList.loadAround(5)
    274         drain()
    275 
    276         verifyRange(0, 40, pagedList)
    277 
    278         pagedList.loadAround(6)
    279         drain()
    280 
    281         // although our prefetch window moves forward, no new load triggered
    282         verifyRange(0, 40, pagedList)
    283     }
    284 
    285     @Test
    286     fun appendCallbackAddedLate() {
    287         val pagedList = createCountedPagedList(0)
    288         verifyRange(0, 40, pagedList)
    289 
    290         pagedList.loadAround(35)
    291         drain()
    292         verifyRange(0, 60, pagedList)
    293 
    294         // snapshot at 60 items
    295         val snapshot = pagedList.snapshot() as PagedList<Item>
    296         verifyRange(0, 60, snapshot)
    297 
    298         // load more items...
    299         pagedList.loadAround(55)
    300         drain()
    301         verifyRange(0, 80, pagedList)
    302         verifyRange(0, 60, snapshot)
    303 
    304         // and verify the snapshot hasn't received them
    305         val callback = mock(PagedList.Callback::class.java)
    306         pagedList.addWeakCallback(snapshot, callback)
    307         verifyCallback(callback, 60, 60)
    308         verifyNoMoreInteractions(callback)
    309     }
    310 
    311     @Test
    312     fun prependCallbackAddedLate() {
    313         val pagedList = createCountedPagedList(80)
    314         verifyRange(60, 40, pagedList)
    315 
    316         pagedList.loadAround(if (mCounted) 65 else 5)
    317         drain()
    318         verifyRange(40, 60, pagedList)
    319 
    320         // snapshot at 60 items
    321         val snapshot = pagedList.snapshot() as PagedList<Item>
    322         verifyRange(40, 60, snapshot)
    323 
    324         pagedList.loadAround(if (mCounted) 45 else 5)
    325         drain()
    326         verifyRange(20, 80, pagedList)
    327         verifyRange(40, 60, snapshot)
    328 
    329         val callback = mock(PagedList.Callback::class.java)
    330         pagedList.addWeakCallback(snapshot, callback)
    331         verifyCallback(callback, 40, 0)
    332         verifyNoMoreInteractions(callback)
    333     }
    334 
    335     @Test
    336     fun initialLoad_lastLoad() {
    337         val pagedList = createCountedPagedList(
    338                 initialPosition = 0,
    339                 initLoadSize = 20,
    340                 lastLoad = 4)
    341         // last load is param passed
    342         assertEquals(4, pagedList.mLastLoad)
    343         verifyRange(0, 20, pagedList)
    344     }
    345 
    346     @Test
    347     fun initialLoad_lastLoadComputed() {
    348         val pagedList = createCountedPagedList(
    349                 initialPosition = 0,
    350                 initLoadSize = 20,
    351                 lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
    352         // last load is middle of initial load
    353         assertEquals(10, pagedList.mLastLoad)
    354         verifyRange(0, 20, pagedList)
    355     }
    356 
    357     @Test
    358     fun initialLoadAsync() {
    359         // Note: ignores Parameterized param
    360         val asyncDataSource = AsyncListDataSource(ITEMS)
    361         val dataSource = asyncDataSource.wrapAsContiguousWithoutPlaceholders()
    362         val pagedList = ContiguousPagedList(
    363                 dataSource, mMainThread, mBackgroundThread, null,
    364                 PagedList.Config.Builder().setPageSize(10).build(), null,
    365                 ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
    366         val callback = mock(PagedList.Callback::class.java)
    367         pagedList.addWeakCallback(null, callback)
    368 
    369         assertTrue(pagedList.isEmpty())
    370         drain()
    371         assertTrue(pagedList.isEmpty())
    372         asyncDataSource.flush()
    373         assertTrue(pagedList.isEmpty())
    374         mBackgroundThread.executeAll()
    375         assertTrue(pagedList.isEmpty())
    376         verifyZeroInteractions(callback)
    377 
    378         // Data source defers callbacks until flush, which posts result to main thread
    379         mMainThread.executeAll()
    380         assertFalse(pagedList.isEmpty())
    381         // callback onInsert called once with initial size
    382         verify(callback).onInserted(0, pagedList.size)
    383         verifyNoMoreInteractions(callback)
    384     }
    385 
    386     @Test
    387     fun addWeakCallbackEmpty() {
    388         // Note: ignores Parameterized param
    389         val asyncDataSource = AsyncListDataSource(ITEMS)
    390         val dataSource = asyncDataSource.wrapAsContiguousWithoutPlaceholders()
    391         val pagedList = ContiguousPagedList(
    392                 dataSource, mMainThread, mBackgroundThread, null,
    393                 PagedList.Config.Builder().setPageSize(10).build(), null,
    394                 ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
    395         val callback = mock(PagedList.Callback::class.java)
    396 
    397         // capture empty snapshot
    398         val emptySnapshot = pagedList.snapshot()
    399         assertTrue(pagedList.isEmpty())
    400         assertTrue(emptySnapshot.isEmpty())
    401 
    402         // verify that adding callback notifies nothing going from empty -> empty
    403         pagedList.addWeakCallback(emptySnapshot, callback)
    404         verifyZeroInteractions(callback)
    405         pagedList.removeWeakCallback(callback)
    406 
    407         // data added in asynchronously
    408         asyncDataSource.flush()
    409         drain()
    410         assertFalse(pagedList.isEmpty())
    411 
    412         // verify that adding callback notifies insert going from empty -> content
    413         pagedList.addWeakCallback(emptySnapshot, callback)
    414         verify(callback).onInserted(0, pagedList.size)
    415         verifyNoMoreInteractions(callback)
    416     }
    417 
    418     @Test
    419     fun boundaryCallback_empty() {
    420         @Suppress("UNCHECKED_CAST")
    421         val boundaryCallback =
    422                 mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
    423         val pagedList = createCountedPagedList(0,
    424                 listData = ArrayList(), boundaryCallback = boundaryCallback)
    425         assertEquals(0, pagedList.size)
    426 
    427         // nothing yet
    428         verifyNoMoreInteractions(boundaryCallback)
    429 
    430         // onZeroItemsLoaded posted, since creation often happens on BG thread
    431         drain()
    432         verify(boundaryCallback).onZeroItemsLoaded()
    433         verifyNoMoreInteractions(boundaryCallback)
    434     }
    435 
    436     @Test
    437     fun boundaryCallback_singleInitialLoad() {
    438         val shortList = ITEMS.subList(0, 4)
    439         @Suppress("UNCHECKED_CAST")
    440         val boundaryCallback =
    441                 mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
    442         val pagedList = createCountedPagedList(0, listData = shortList,
    443                 initLoadSize = shortList.size, boundaryCallback = boundaryCallback)
    444         assertEquals(shortList.size, pagedList.size)
    445 
    446         // nothing yet
    447         verifyNoMoreInteractions(boundaryCallback)
    448 
    449         // onItemAtFrontLoaded / onItemAtEndLoaded posted, since creation often happens on BG thread
    450         drain()
    451         pagedList.loadAround(0)
    452         drain()
    453         verify(boundaryCallback).onItemAtFrontLoaded(shortList.first())
    454         verify(boundaryCallback).onItemAtEndLoaded(shortList.last())
    455         verifyNoMoreInteractions(boundaryCallback)
    456     }
    457 
    458     @Test
    459     fun boundaryCallback_delayed() {
    460         @Suppress("UNCHECKED_CAST")
    461         val boundaryCallback =
    462                 mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
    463         val pagedList = createCountedPagedList(90,
    464                 initLoadSize = 20, prefetchDistance = 5, boundaryCallback = boundaryCallback)
    465         verifyRange(80, 20, pagedList)
    466 
    467         // nothing yet
    468         verifyZeroInteractions(boundaryCallback)
    469         drain()
    470         verifyZeroInteractions(boundaryCallback)
    471 
    472         // loading around last item causes onItemAtEndLoaded
    473         pagedList.loadAround(if (mCounted) 99 else 19)
    474         drain()
    475         verifyRange(80, 20, pagedList)
    476         verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
    477         verifyNoMoreInteractions(boundaryCallback)
    478 
    479         // prepending doesn't trigger callback...
    480         pagedList.loadAround(if (mCounted) 80 else 0)
    481         drain()
    482         verifyRange(60, 40, pagedList)
    483         verifyZeroInteractions(boundaryCallback)
    484 
    485         // ...load rest of data, still no dispatch...
    486         pagedList.loadAround(if (mCounted) 60 else 0)
    487         drain()
    488         pagedList.loadAround(if (mCounted) 40 else 0)
    489         drain()
    490         pagedList.loadAround(if (mCounted) 20 else 0)
    491         drain()
    492         verifyRange(0, 100, pagedList)
    493         verifyZeroInteractions(boundaryCallback)
    494 
    495         // ... finally try prepend, see 0 items, which will dispatch front callback
    496         pagedList.loadAround(0)
    497         drain()
    498         verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
    499         verifyNoMoreInteractions(boundaryCallback)
    500     }
    501 
    502     private fun drain() {
    503         var executed: Boolean
    504         do {
    505             executed = mBackgroundThread.executeAll()
    506             executed = mMainThread.executeAll() || executed
    507         } while (executed)
    508     }
    509 
    510     companion object {
    511         @JvmStatic
    512         @Parameterized.Parameters(name = "counted:{0}")
    513         fun parameters(): Array<Array<Boolean>> {
    514             return arrayOf(arrayOf(true), arrayOf(false))
    515         }
    516 
    517         private val ITEMS = List(100) { Item(it) }
    518     }
    519 }
    520