1/*
2 * Copyright 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.assertTrue
21import org.junit.Assert.fail
22import org.junit.Test
23import org.junit.runner.RunWith
24import org.junit.runners.JUnit4
25import org.mockito.Mockito.mock
26import org.mockito.Mockito.verify
27import org.mockito.Mockito.verifyNoMoreInteractions
28
29@RunWith(JUnit4::class)
30class PositionalDataSourceTest {
31    private fun computeInitialLoadPos(
32            requestedStartPosition: Int,
33            requestedLoadSize: Int,
34            pageSize: Int,
35            totalCount: Int): Int {
36        val params = PositionalDataSource.LoadInitialParams(
37                requestedStartPosition, requestedLoadSize, pageSize, true)
38        return PositionalDataSource.computeInitialLoadPosition(params, totalCount)
39    }
40
41    @Test
42    fun computeInitialLoadPositionZero() {
43        assertEquals(0, computeInitialLoadPos(
44                requestedStartPosition = 0,
45                requestedLoadSize = 30,
46                pageSize = 10,
47                totalCount = 100))
48    }
49
50    @Test
51    fun computeInitialLoadPositionRequestedPositionIncluded() {
52        assertEquals(10, computeInitialLoadPos(
53                requestedStartPosition = 10,
54                requestedLoadSize = 10,
55                pageSize = 10,
56                totalCount = 100))
57    }
58
59    @Test
60    fun computeInitialLoadPositionRound() {
61        assertEquals(10, computeInitialLoadPos(
62                requestedStartPosition = 13,
63                requestedLoadSize = 30,
64                pageSize = 10,
65                totalCount = 100))
66    }
67
68    @Test
69    fun computeInitialLoadPositionEndAdjusted() {
70        assertEquals(70, computeInitialLoadPos(
71                requestedStartPosition = 99,
72                requestedLoadSize = 30,
73                pageSize = 10,
74                totalCount = 100))
75    }
76
77    @Test
78    fun computeInitialLoadPositionEndAdjustedAndAligned() {
79        assertEquals(70, computeInitialLoadPos(
80                requestedStartPosition = 99,
81                requestedLoadSize = 35,
82                pageSize = 10,
83                totalCount = 100))
84    }
85
86    @Test
87    fun fullLoadWrappedAsContiguous() {
88        // verify that prepend / append work correctly with a PositionalDataSource, made contiguous
89        val config = PagedList.Config.Builder()
90                .setPageSize(10)
91                .setInitialLoadSizeHint(10)
92                .setEnablePlaceholders(true)
93                .build()
94        val dataSource: PositionalDataSource<Int> = ListDataSource((0..99).toList())
95        val testExecutor = TestExecutor()
96        val pagedList = ContiguousPagedList(dataSource.wrapAsContiguousWithoutPlaceholders(),
97                testExecutor, testExecutor, null, config, 15,
98                ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
99
100        assertEquals((10..19).toList(), pagedList)
101
102        // prepend + append work correctly
103        pagedList.loadAround(5)
104        testExecutor.executeAll()
105        assertEquals((0..29).toList(), pagedList)
106
107        // and load the rest of the data to be sure further appends work
108        for (i in (2..9)) {
109            pagedList.loadAround(i * 10 - 5)
110            testExecutor.executeAll()
111            assertEquals((0..i * 10 + 9).toList(), pagedList)
112        }
113    }
114
115    private fun performLoadInitial(
116            enablePlaceholders: Boolean = true,
117            invalidateDataSource: Boolean = false,
118            callbackInvoker: (callback: PositionalDataSource.LoadInitialCallback<String>) -> Unit) {
119        val dataSource = object : PositionalDataSource<String>() {
120            override fun loadInitial(
121                    params: LoadInitialParams,
122                    callback: LoadInitialCallback<String>) {
123                if (invalidateDataSource) {
124                    // invalidate data source so it's invalid when onResult() called
125                    invalidate()
126                }
127                callbackInvoker(callback)
128            }
129
130            override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<String>) {
131                fail("loadRange not expected")
132            }
133        }
134
135        val config = PagedList.Config.Builder()
136                .setPageSize(10)
137                .setEnablePlaceholders(enablePlaceholders)
138                .build()
139        if (enablePlaceholders) {
140            TiledPagedList(dataSource, FailExecutor(), FailExecutor(), null, config, 0)
141        } else {
142            ContiguousPagedList(dataSource.wrapAsContiguousWithoutPlaceholders(),
143                    FailExecutor(), FailExecutor(), null, config, null,
144                    ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
145        }
146    }
147
148    @Test
149    fun initialLoadCallbackSuccess() = performLoadInitial {
150        // LoadInitialCallback correct usage
151        it.onResult(listOf("a", "b"), 0, 2)
152    }
153
154    @Test(expected = IllegalArgumentException::class)
155    fun initialLoadCallbackNotPageSizeMultiple() = performLoadInitial {
156        // Positional LoadInitialCallback can't accept result that's not a multiple of page size
157        val elevenLetterList = List(11) { "" + 'a' + it }
158        it.onResult(elevenLetterList, 0, 12)
159    }
160
161    @Test(expected = IllegalArgumentException::class)
162    fun initialLoadCallbackListTooBig() = performLoadInitial {
163        // LoadInitialCallback can't accept pos + list > totalCount
164        it.onResult(listOf("a", "b", "c"), 0, 2)
165    }
166
167    @Test(expected = IllegalArgumentException::class)
168    fun initialLoadCallbackPositionTooLarge() = performLoadInitial {
169        // LoadInitialCallback can't accept pos + list > totalCount
170        it.onResult(listOf("a", "b"), 1, 2)
171    }
172
173    @Test(expected = IllegalArgumentException::class)
174    fun initialLoadCallbackPositionNegative() = performLoadInitial {
175        // LoadInitialCallback can't accept negative position
176        it.onResult(listOf("a", "b", "c"), -1, 2)
177    }
178
179    @Test(expected = IllegalArgumentException::class)
180    fun initialLoadCallbackEmptyCannotHavePlaceholders() = performLoadInitial {
181        // LoadInitialCallback can't accept empty result unless data set is empty
182        it.onResult(emptyList(), 0, 2)
183    }
184
185    @Test(expected = IllegalStateException::class)
186    fun initialLoadCallbackRequireTotalCount() = performLoadInitial(enablePlaceholders = true) {
187        // LoadInitialCallback requires 3 args when placeholders enabled
188        it.onResult(listOf("a", "b"), 0)
189    }
190
191    @Test
192    fun initialLoadCallbackSuccessTwoArg() = performLoadInitial(enablePlaceholders = false) {
193        // LoadInitialCallback correct 2 arg usage
194        it.onResult(listOf("a", "b"), 0)
195    }
196
197    @Test(expected = IllegalArgumentException::class)
198    fun initialLoadCallbackPosNegativeTwoArg() = performLoadInitial(enablePlaceholders = false) {
199        // LoadInitialCallback can't accept negative position
200        it.onResult(listOf("a", "b"), -1)
201    }
202
203    @Test(expected = IllegalArgumentException::class)
204    fun initialLoadCallbackEmptyWithOffset() = performLoadInitial(enablePlaceholders = false) {
205        // LoadInitialCallback can't accept empty result unless pos is 0
206        it.onResult(emptyList(), 1)
207    }
208
209    @Test
210    fun initialLoadCallbackInvalidTwoArg() = performLoadInitial(invalidateDataSource = true) {
211        // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
212        it.onResult(emptyList(), 1)
213    }
214
215    @Test
216    fun initialLoadCallbackInvalidThreeArg() = performLoadInitial(invalidateDataSource = true) {
217        // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
218        it.onResult(emptyList(), 0, 1)
219    }
220
221    private abstract class WrapperDataSource<in A, B>(private val source: PositionalDataSource<A>)
222            : PositionalDataSource<B>() {
223        override fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
224            source.addInvalidatedCallback(onInvalidatedCallback)
225        }
226
227        override fun removeInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
228            source.removeInvalidatedCallback(onInvalidatedCallback)
229        }
230
231        override fun invalidate() {
232            source.invalidate()
233        }
234
235        override fun isInvalid(): Boolean {
236            return source.isInvalid
237        }
238
239        override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<B>) {
240            source.loadInitial(params, object : LoadInitialCallback<A>() {
241                override fun onResult(data: List<A>, position: Int, totalCount: Int) {
242                    callback.onResult(convert(data), position, totalCount)
243                }
244
245                override fun onResult(data: List<A>, position: Int) {
246                    callback.onResult(convert(data), position)
247                }
248            })
249        }
250
251        override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<B>) {
252            source.loadRange(params, object : LoadRangeCallback<A>() {
253                override fun onResult(data: List<A>) {
254                    callback.onResult(convert(data))
255                }
256            })
257        }
258
259        protected abstract fun convert(source: List<A>): List<B>
260    }
261
262    private class StringWrapperDataSource<in A>(source: PositionalDataSource<A>)
263            : WrapperDataSource<A, String>(source) {
264        override fun convert(source: List<A>): List<String> {
265            return source.map { it.toString() }
266        }
267    }
268
269    private fun verifyWrappedDataSource(
270            createWrapper: (PositionalDataSource<Int>) -> PositionalDataSource<String>) {
271        val orig = ListDataSource(listOf(0, 5, 4, 8, 12))
272        val wrapper = createWrapper(orig)
273
274        // load initial
275        @Suppress("UNCHECKED_CAST")
276        val loadInitialCallback = mock(PositionalDataSource.LoadInitialCallback::class.java)
277                as PositionalDataSource.LoadInitialCallback<String>
278
279        wrapper.loadInitial(PositionalDataSource.LoadInitialParams(0, 2, 1, true),
280                loadInitialCallback)
281        verify(loadInitialCallback).onResult(listOf("0", "5"), 0, 5)
282        verifyNoMoreInteractions(loadInitialCallback)
283
284        // load range
285        @Suppress("UNCHECKED_CAST")
286        val loadRangeCallback = mock(PositionalDataSource.LoadRangeCallback::class.java)
287                as PositionalDataSource.LoadRangeCallback<String>
288
289        wrapper.loadRange(PositionalDataSource.LoadRangeParams(2, 3), loadRangeCallback)
290        verify(loadRangeCallback).onResult(listOf("4", "8", "12"))
291        verifyNoMoreInteractions(loadRangeCallback)
292
293        // check invalidation behavior
294        val invalCallback = mock(DataSource.InvalidatedCallback::class.java)
295        wrapper.addInvalidatedCallback(invalCallback)
296        orig.invalidate()
297        verify(invalCallback).onInvalidated()
298        verifyNoMoreInteractions(invalCallback)
299
300        // verify invalidation
301        orig.invalidate()
302        assertTrue(wrapper.isInvalid)
303    }
304
305    @Test
306    fun testManualWrappedDataSource() = verifyWrappedDataSource {
307        StringWrapperDataSource(it)
308    }
309
310    @Test
311    fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
312        it.mapByPage { it.map { it.toString() } }
313    }
314
315    @Test
316    fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
317        it.map { it.toString() }
318    }
319
320    @Test
321    fun testInvalidateToWrapper() {
322        val orig = ListDataSource(listOf(0, 1, 2))
323        val wrapper = orig.map { it.toString() }
324
325        orig.invalidate()
326        assertTrue(wrapper.isInvalid)
327    }
328
329    @Test
330    fun testInvalidateFromWrapper() {
331        val orig = ListDataSource(listOf(0, 1, 2))
332        val wrapper = orig.map { it.toString() }
333
334        wrapper.invalidate()
335        assertTrue(orig.isInvalid)
336    }
337
338    @Test
339    fun testInvalidateToWrapper_contiguous() {
340        val orig = ListDataSource(listOf(0, 1, 2))
341        val wrapper = orig.wrapAsContiguousWithoutPlaceholders()
342
343        orig.invalidate()
344        assertTrue(wrapper.isInvalid)
345    }
346
347    @Test
348    fun testInvalidateFromWrapper_contiguous() {
349        val orig = ListDataSource(listOf(0, 1, 2))
350        val wrapper = orig.wrapAsContiguousWithoutPlaceholders()
351
352        wrapper.invalidate()
353        assertTrue(orig.isInvalid)
354    }
355}
356