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 org.junit.Assert.assertEquals
20import org.junit.Assert.assertFalse
21import org.junit.Assert.assertNotNull
22import org.junit.Assert.assertNull
23import org.junit.Assert.assertSame
24import org.junit.Assert.assertTrue
25import org.junit.Test
26import org.junit.runner.RunWith
27import org.junit.runners.JUnit4
28import org.mockito.Mockito.mock
29import org.mockito.Mockito.verify
30import org.mockito.Mockito.verifyNoMoreInteractions
31import org.mockito.Mockito.verifyZeroInteractions
32
33@RunWith(JUnit4::class)
34class TiledPagedListTest {
35    private val mMainThread = TestExecutor()
36    private val mBackgroundThread = TestExecutor()
37
38    private class Item(position: Int) {
39        val name: String = "Item $position"
40
41        override fun toString(): String {
42            return name
43        }
44    }
45
46    private fun verifyLoadedPages(list: List<Item>, vararg loadedPages: Int) {
47        val loadedPageList = loadedPages.asList()
48        assertEquals(ITEMS.size, list.size)
49        for (i in list.indices) {
50            if (loadedPageList.contains(i / PAGE_SIZE)) {
51                assertSame("Index $i", ITEMS[i], list[i])
52            } else {
53                assertNull("Index $i", list[i])
54            }
55        }
56    }
57
58    private fun createTiledPagedList(loadPosition: Int, initPageCount: Int,
59            prefetchDistance: Int = PAGE_SIZE,
60            listData: List<Item> = ITEMS,
61            boundaryCallback: PagedList.BoundaryCallback<Item>? = null): TiledPagedList<Item> {
62        return TiledPagedList(
63                ListDataSource(listData), mMainThread, mBackgroundThread, boundaryCallback,
64                PagedList.Config.Builder()
65                        .setPageSize(PAGE_SIZE)
66                        .setInitialLoadSizeHint(PAGE_SIZE * initPageCount)
67                        .setPrefetchDistance(prefetchDistance)
68                        .build(),
69                loadPosition)
70    }
71
72    @Test
73    fun getDataSource() {
74        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
75        assertTrue(pagedList.dataSource is ListDataSource<Item>)
76
77        // snapshot keeps same DataSource
78        assertSame(pagedList.dataSource,
79                (pagedList.snapshot() as SnapshotPagedList<Item>).dataSource)
80    }
81
82    @Test
83    fun initialLoad_onePage() {
84        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
85        verifyLoadedPages(pagedList, 0, 1)
86    }
87
88    @Test
89    fun initialLoad_onePageOffset() {
90        val pagedList = createTiledPagedList(loadPosition = 10, initPageCount = 1)
91        verifyLoadedPages(pagedList, 0, 1)
92    }
93
94    @Test
95    fun initialLoad_full() {
96        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 100)
97        verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
98    }
99
100    @Test
101    fun initialLoad_end() {
102        val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
103        verifyLoadedPages(pagedList, 3, 4)
104    }
105
106    @Test
107    fun initialLoad_multiple() {
108        val pagedList = createTiledPagedList(loadPosition = 9, initPageCount = 2)
109        verifyLoadedPages(pagedList, 0, 1)
110    }
111
112    @Test
113    fun initialLoad_offset() {
114        val pagedList = createTiledPagedList(loadPosition = 41, initPageCount = 2)
115        verifyLoadedPages(pagedList, 3, 4)
116    }
117
118    @Test
119    fun initialLoad_initializesLastKey() {
120        val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
121        assertEquals(44, pagedList.lastKey)
122    }
123
124    @Test
125    fun initialLoadAsync() {
126        val dataSource = AsyncListDataSource(ITEMS)
127        val pagedList = TiledPagedList(
128                dataSource, mMainThread, mBackgroundThread, null,
129                PagedList.Config.Builder().setPageSize(10).build(), 0)
130
131        assertTrue(pagedList.isEmpty())
132        drain()
133        assertTrue(pagedList.isEmpty())
134        dataSource.flush()
135        assertTrue(pagedList.isEmpty())
136        mBackgroundThread.executeAll()
137        assertTrue(pagedList.isEmpty())
138
139        // Data source defers callbacks until flush, which posts result to main thread
140        mMainThread.executeAll()
141        assertFalse(pagedList.isEmpty())
142    }
143
144    @Test
145    fun addWeakCallbackEmpty() {
146        val dataSource = AsyncListDataSource(ITEMS)
147        val pagedList = TiledPagedList(
148                dataSource, mMainThread, mBackgroundThread, null,
149                PagedList.Config.Builder().setPageSize(10).build(), 0)
150
151        // capture empty snapshot
152        val emptySnapshot = pagedList.snapshot()
153        assertTrue(pagedList.isEmpty())
154        assertTrue(emptySnapshot.isEmpty())
155
156        // data added in asynchronously
157        dataSource.flush()
158        drain()
159        assertFalse(pagedList.isEmpty())
160
161        // verify that adding callback works with empty start point
162        val callback = mock(PagedList.Callback::class.java)
163        pagedList.addWeakCallback(emptySnapshot, callback)
164        verify(callback).onInserted(0, pagedList.size)
165        verifyNoMoreInteractions(callback)
166    }
167
168    @Test
169    fun append() {
170        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
171        val callback = mock(PagedList.Callback::class.java)
172        pagedList.addWeakCallback(null, callback)
173        verifyLoadedPages(pagedList, 0, 1)
174        verifyZeroInteractions(callback)
175
176        pagedList.loadAround(15)
177
178        verifyLoadedPages(pagedList, 0, 1)
179
180        drain()
181
182        verifyLoadedPages(pagedList, 0, 1, 2)
183        verify(callback).onChanged(20, 10)
184        verifyNoMoreInteractions(callback)
185    }
186
187    @Test
188    fun prepend() {
189        val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
190        val callback = mock(PagedList.Callback::class.java)
191        pagedList.addWeakCallback(null, callback)
192        verifyLoadedPages(pagedList, 3, 4)
193        verifyZeroInteractions(callback)
194
195        pagedList.loadAround(35)
196        drain()
197
198        verifyLoadedPages(pagedList, 2, 3, 4)
199        verify<PagedList.Callback>(callback).onChanged(20, 10)
200        verifyNoMoreInteractions(callback)
201    }
202
203    @Test
204    fun loadWithGap() {
205        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
206        val callback = mock(PagedList.Callback::class.java)
207        pagedList.addWeakCallback(null, callback)
208        verifyLoadedPages(pagedList, 0, 1)
209        verifyZeroInteractions(callback)
210
211        pagedList.loadAround(44)
212        drain()
213
214        verifyLoadedPages(pagedList, 0, 1, 3, 4)
215        verify(callback).onChanged(30, 10)
216        verify(callback).onChanged(40, 5)
217        verifyNoMoreInteractions(callback)
218    }
219
220    @Test
221    fun tinyPrefetchTest() {
222        val pagedList = createTiledPagedList(
223                loadPosition = 0, initPageCount = 1, prefetchDistance = 1)
224        val callback = mock(PagedList.Callback::class.java)
225        pagedList.addWeakCallback(null, callback)
226        verifyLoadedPages(pagedList, 0, 1)
227        verifyZeroInteractions(callback)
228
229        pagedList.loadAround(33)
230        drain()
231
232        verifyLoadedPages(pagedList, 0, 1, 3)
233        verify(callback).onChanged(30, 10)
234        verifyNoMoreInteractions(callback)
235
236        pagedList.loadAround(44)
237        drain()
238
239        verifyLoadedPages(pagedList, 0, 1, 3, 4)
240        verify(callback).onChanged(40, 5)
241        verifyNoMoreInteractions(callback)
242    }
243
244    @Test
245    fun appendCallbackAddedLate() {
246        val pagedList = createTiledPagedList(
247                loadPosition = 0, initPageCount = 1, prefetchDistance = 0)
248        verifyLoadedPages(pagedList, 0, 1)
249
250        pagedList.loadAround(25)
251        drain()
252        verifyLoadedPages(pagedList, 0, 1, 2)
253
254        // snapshot at 30 items
255        val snapshot = pagedList.snapshot()
256        verifyLoadedPages(snapshot, 0, 1, 2)
257
258        pagedList.loadAround(35)
259        pagedList.loadAround(44)
260        drain()
261        verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
262        verifyLoadedPages(snapshot, 0, 1, 2)
263
264        val callback = mock(PagedList.Callback::class.java)
265        pagedList.addWeakCallback(snapshot, callback)
266        verify(callback).onChanged(30, 20)
267        verifyNoMoreInteractions(callback)
268    }
269
270    @Test
271    fun prependCallbackAddedLate() {
272        val pagedList = createTiledPagedList(
273                loadPosition = 44, initPageCount = 2, prefetchDistance = 0)
274        verifyLoadedPages(pagedList, 3, 4)
275
276        pagedList.loadAround(25)
277        drain()
278        verifyLoadedPages(pagedList, 2, 3, 4)
279
280        // snapshot at 30 items
281        val snapshot = pagedList.snapshot()
282        verifyLoadedPages(snapshot, 2, 3, 4)
283
284        pagedList.loadAround(15)
285        pagedList.loadAround(5)
286        drain()
287        verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
288        verifyLoadedPages(snapshot, 2, 3, 4)
289
290        val callback = mock(PagedList.Callback::class.java)
291        pagedList.addWeakCallback(snapshot, callback)
292        verify(callback).onChanged(0, 20)
293        verifyNoMoreInteractions(callback)
294    }
295
296    @Test
297    fun placeholdersDisabled() {
298        // disable placeholders with config, so we create a contiguous version of the pagedlist
299        val config = PagedList.Config.Builder()
300                .setPageSize(PAGE_SIZE)
301                .setPrefetchDistance(PAGE_SIZE)
302                .setInitialLoadSizeHint(PAGE_SIZE)
303                .setEnablePlaceholders(false)
304                .build()
305        val pagedList = PagedList.Builder<Int, Item>(ListDataSource(ITEMS), config)
306                .setNotifyExecutor(mMainThread)
307                .setFetchExecutor(mBackgroundThread)
308                .setInitialKey(20)
309                .build()
310
311        assertTrue(pagedList.isContiguous)
312
313        @Suppress("UNCHECKED_CAST")
314        val contiguousPagedList = pagedList as ContiguousPagedList<Int, Item>
315        assertEquals(0, contiguousPagedList.mStorage.leadingNullCount)
316        assertEquals(PAGE_SIZE, contiguousPagedList.mStorage.storageCount)
317        assertEquals(0, contiguousPagedList.mStorage.trailingNullCount)
318    }
319
320    @Test
321    fun boundaryCallback_empty() {
322        @Suppress("UNCHECKED_CAST")
323        val boundaryCallback =
324                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
325        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1,
326                listData = ArrayList(), boundaryCallback = boundaryCallback)
327        assertEquals(0, pagedList.size)
328
329        // nothing yet
330        verifyNoMoreInteractions(boundaryCallback)
331
332        // onZeroItemsLoaded posted, since creation often happens on BG thread
333        drain()
334        verify(boundaryCallback).onZeroItemsLoaded()
335        verifyNoMoreInteractions(boundaryCallback)
336    }
337
338    @Test
339    fun boundaryCallback_immediate() {
340        @Suppress("UNCHECKED_CAST")
341        val boundaryCallback =
342                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
343        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1,
344                listData = ITEMS.subList(0, 2), boundaryCallback = boundaryCallback)
345        assertEquals(2, pagedList.size)
346
347        // nothing yet
348        verifyZeroInteractions(boundaryCallback)
349
350        // callbacks posted, since creation often happens on BG thread
351        drain()
352        verify(boundaryCallback).onItemAtFrontLoaded(ITEMS[0])
353        verify(boundaryCallback).onItemAtEndLoaded(ITEMS[1])
354        verifyNoMoreInteractions(boundaryCallback)
355    }
356
357    @Test
358    fun boundaryCallback_delayedUntilLoaded() {
359        @Suppress("UNCHECKED_CAST")
360        val boundaryCallback =
361                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
362        val pagedList = createTiledPagedList(loadPosition = 20, initPageCount = 1,
363                boundaryCallback = boundaryCallback)
364        verifyLoadedPages(pagedList, 1, 2) // 0, 3, and 4 not loaded yet
365
366        // nothing yet, even after drain
367        verifyZeroInteractions(boundaryCallback)
368        drain()
369        verifyZeroInteractions(boundaryCallback)
370
371        pagedList.loadAround(0)
372        pagedList.loadAround(44)
373
374        // still nothing, since items aren't loaded...
375        verifyZeroInteractions(boundaryCallback)
376
377        drain()
378        // first/last items loaded now, so callbacks dispatched
379        verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
380        verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
381        verifyNoMoreInteractions(boundaryCallback)
382    }
383
384    @Test
385    fun boundaryCallback_delayedUntilNearbyAccess() {
386        @Suppress("UNCHECKED_CAST")
387        val boundaryCallback =
388                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
389        val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 5,
390                prefetchDistance = 2, boundaryCallback = boundaryCallback)
391        verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
392
393        // all items loaded, but no access near ends, so no callbacks
394        verifyZeroInteractions(boundaryCallback)
395        drain()
396        verifyZeroInteractions(boundaryCallback)
397
398        pagedList.loadAround(0)
399        pagedList.loadAround(44)
400
401        // callbacks not posted immediately
402        verifyZeroInteractions(boundaryCallback)
403
404        drain()
405
406        // items accessed, so now posted callbacks are run
407        verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
408        verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
409        verifyNoMoreInteractions(boundaryCallback)
410    }
411
412    private fun validateCallbackForSize(initPageCount: Int, itemCount: Int) {
413        @Suppress("UNCHECKED_CAST")
414        val boundaryCallback =
415                mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
416        val listData = ITEMS.subList(0, itemCount)
417        val pagedList = createTiledPagedList(
418                loadPosition = 0,
419                initPageCount = initPageCount,
420                prefetchDistance = 0,
421                boundaryCallback = boundaryCallback,
422                listData = listData)
423        assertNotNull(pagedList[pagedList.size - 1 - PAGE_SIZE])
424        assertNull(pagedList.last()) // not completed loading
425
426        // no access near list beginning, so no callbacks yet
427        verifyNoMoreInteractions(boundaryCallback)
428        drain()
429        verifyNoMoreInteractions(boundaryCallback)
430
431        // trigger front boundary callback (via access)
432        pagedList.loadAround(0)
433        drain()
434        verify(boundaryCallback).onItemAtFrontLoaded(listData.first())
435        verifyNoMoreInteractions(boundaryCallback)
436
437        // trigger end boundary callback (via load)
438        pagedList.loadAround(pagedList.size - 1)
439        drain()
440        verify(boundaryCallback).onItemAtEndLoaded(listData.last())
441        verifyNoMoreInteractions(boundaryCallback)
442    }
443
444    @Test
445    fun boundaryCallbackPageSize1() {
446        // verify different alignments of last page still trigger boundaryCallback correctly
447        validateCallbackForSize(2, 3 * PAGE_SIZE - 2)
448        validateCallbackForSize(2, 3 * PAGE_SIZE - 1)
449        validateCallbackForSize(2, 3 * PAGE_SIZE)
450        validateCallbackForSize(3, 3 * PAGE_SIZE + 1)
451        validateCallbackForSize(3, 3 * PAGE_SIZE + 2)
452    }
453
454    private fun drain() {
455        var executed: Boolean
456        do {
457            executed = mBackgroundThread.executeAll()
458            executed = mMainThread.executeAll() || executed
459        } while (executed)
460    }
461
462    companion object {
463        // use a page size that's not an even divisor of ITEMS.size() to test end conditions
464        private val PAGE_SIZE = 10
465
466        private val ITEMS = List(45) { Item(it) }
467    }
468}
469