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
17package androidx.paging
18
19import androidx.arch.core.util.Function
20import org.junit.Assert.assertArrayEquals
21import org.junit.Assert.assertEquals
22import org.junit.Assert.assertFalse
23import org.junit.Assert.assertSame
24import org.junit.Assert.assertTrue
25import org.junit.Test
26import org.junit.runner.RunWith
27import org.junit.runners.Parameterized
28import org.mockito.Mockito.mock
29import org.mockito.Mockito.verify
30import org.mockito.Mockito.verifyNoMoreInteractions
31import org.mockito.Mockito.verifyZeroInteractions
32import java.util.concurrent.Executor
33
34@RunWith(Parameterized::class)
35class 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