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 PageKeyedDataSourceTest {
31    private val mMainThread = TestExecutor()
32    private val mBackgroundThread = TestExecutor()
33
34    internal data class Item(val name: String)
35
36    internal data class Page(val prev: String?, val data: List<Item>, val next: String?)
37
38    internal class ItemDataSource(val data: Map<String, Page> = PAGE_MAP)
39            : PageKeyedDataSource<String, Item>() {
40
41        private fun getPage(key: String): Page = data[key]!!
42
43        override fun loadInitial(
44                params: LoadInitialParams<String>,
45                callback: LoadInitialCallback<String, Item>) {
46            val page = getPage(INIT_KEY)
47            callback.onResult(page.data, page.prev, page.next)
48        }
49
50        override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String, Item>) {
51            val page = getPage(params.key)
52            callback.onResult(page.data, page.prev)
53        }
54
55        override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, Item>) {
56            val page = getPage(params.key)
57            callback.onResult(page.data, page.next)
58        }
59    }
60
61    @Test
62    fun loadFullVerify() {
63        // validate paging entire ItemDataSource results in full, correctly ordered data
64        val pagedList = ContiguousPagedList<String, Item>(ItemDataSource(),
65                mMainThread, mBackgroundThread,
66                null, PagedList.Config.Builder().setPageSize(100).build(), null,
67                ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
68
69        // validate initial load
70        assertEquals(PAGE_MAP[INIT_KEY]!!.data, pagedList)
71
72        // flush the remaining loads
73        for (i in 0..PAGE_MAP.keys.size) {
74            pagedList.loadAround(0)
75            pagedList.loadAround(pagedList.size - 1)
76            drain()
77        }
78
79        // validate full load
80        assertEquals(ITEM_LIST, pagedList)
81    }
82
83    private fun performLoadInitial(invalidateDataSource: Boolean = false,
84            callbackInvoker:
85                    (callback: PageKeyedDataSource.LoadInitialCallback<String, String>) -> Unit) {
86        val dataSource = object : PageKeyedDataSource<String, String>() {
87            override fun loadInitial(
88                    params: LoadInitialParams<String>,
89                    callback: LoadInitialCallback<String, String>) {
90                if (invalidateDataSource) {
91                    // invalidate data source so it's invalid when onResult() called
92                    invalidate()
93                }
94                callbackInvoker(callback)
95            }
96
97            override fun loadBefore(
98                    params: LoadParams<String>,
99                    callback: LoadCallback<String, String>) {
100                fail("loadBefore not expected")
101            }
102
103            override fun loadAfter(
104                    params: LoadParams<String>,
105                    callback: LoadCallback<String, String>) {
106                fail("loadAfter not expected")
107            }
108        }
109
110        ContiguousPagedList<String, String>(
111                dataSource, FailExecutor(), FailExecutor(), null,
112                PagedList.Config.Builder()
113                        .setPageSize(10)
114                        .build(),
115                "",
116                ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
117    }
118
119    @Test
120    fun loadInitialCallbackSuccess() = performLoadInitial {
121        // LoadInitialCallback correct usage
122        it.onResult(listOf("a", "b"), 0, 2, null, null)
123    }
124
125    @Test
126    fun loadInitialCallbackNotPageSizeMultiple() = performLoadInitial {
127        // Keyed LoadInitialCallback *can* accept result that's not a multiple of page size
128        val elevenLetterList = List(11) { "" + 'a' + it }
129        it.onResult(elevenLetterList, 0, 12, null, null)
130    }
131
132    @Test(expected = IllegalArgumentException::class)
133    fun loadInitialCallbackListTooBig() = performLoadInitial {
134        // LoadInitialCallback can't accept pos + list > totalCount
135        it.onResult(listOf("a", "b", "c"), 0, 2, null, null)
136    }
137
138    @Test(expected = IllegalArgumentException::class)
139    fun loadInitialCallbackPositionTooLarge() = performLoadInitial {
140        // LoadInitialCallback can't accept pos + list > totalCount
141        it.onResult(listOf("a", "b"), 1, 2, null, null)
142    }
143
144    @Test(expected = IllegalArgumentException::class)
145    fun loadInitialCallbackPositionNegative() = performLoadInitial {
146        // LoadInitialCallback can't accept negative position
147        it.onResult(listOf("a", "b", "c"), -1, 2, null, null)
148    }
149
150    @Test(expected = IllegalArgumentException::class)
151    fun loadInitialCallbackEmptyCannotHavePlaceholders() = performLoadInitial {
152        // LoadInitialCallback can't accept empty result unless data set is empty
153        it.onResult(emptyList(), 0, 2, null, null)
154    }
155
156    @Test
157    fun initialLoadCallbackInvalidThreeArg() = performLoadInitial(invalidateDataSource = true) {
158        // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
159        it.onResult(emptyList(), 0, 1, null, null)
160    }
161
162    private abstract class WrapperDataSource<K, A, B>(private val source: PageKeyedDataSource<K, A>)
163            : PageKeyedDataSource<K, B>() {
164        override fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
165            source.addInvalidatedCallback(onInvalidatedCallback)
166        }
167
168        override fun removeInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
169            source.removeInvalidatedCallback(onInvalidatedCallback)
170        }
171
172        override fun invalidate() {
173            source.invalidate()
174        }
175
176        override fun isInvalid(): Boolean {
177            return source.isInvalid
178        }
179
180        override fun loadInitial(params: LoadInitialParams<K>,
181                callback: LoadInitialCallback<K, B>) {
182            source.loadInitial(params, object : LoadInitialCallback<K, A>() {
183                override fun onResult(data: List<A>, position: Int, totalCount: Int,
184                        previousPageKey: K?, nextPageKey: K?) {
185                    callback.onResult(convert(data), position, totalCount,
186                            previousPageKey, nextPageKey)
187                }
188
189                override fun onResult(data: MutableList<A>, previousPageKey: K?, nextPageKey: K?) {
190                    callback.onResult(convert(data), previousPageKey, nextPageKey)
191                }
192            })
193        }
194
195        override fun loadBefore(params: LoadParams<K>, callback: LoadCallback<K, B>) {
196            source.loadBefore(params, object : LoadCallback<K, A>() {
197                override fun onResult(data: List<A>, adjacentPageKey: K?) {
198                    callback.onResult(convert(data), adjacentPageKey)
199                }
200            })
201        }
202
203        override fun loadAfter(params: LoadParams<K>, callback: LoadCallback<K, B>) {
204            source.loadAfter(params, object : LoadCallback<K, A>() {
205                override fun onResult(data: List<A>, adjacentPageKey: K?) {
206                    callback.onResult(convert(data), adjacentPageKey)
207                }
208            })
209        }
210
211        protected abstract fun convert(source: List<A>): List<B>
212    }
213
214    private class StringWrapperDataSource<K, V>(source: PageKeyedDataSource<K, V>)
215            : WrapperDataSource<K, V, String>(source) {
216        override fun convert(source: List<V>): List<String> {
217            return source.map { it.toString() }
218        }
219    }
220
221    private fun verifyWrappedDataSource(createWrapper:
222            (PageKeyedDataSource<String, Item>) -> PageKeyedDataSource<String, String>) {
223        // verify that it's possible to wrap a PageKeyedDataSource, and add info to its data
224        val orig = ItemDataSource(data = PAGE_MAP)
225        val wrapper = createWrapper(orig)
226
227        // load initial
228        @Suppress("UNCHECKED_CAST")
229        val loadInitialCallback = mock(PageKeyedDataSource.LoadInitialCallback::class.java)
230                as PageKeyedDataSource.LoadInitialCallback<String, String>
231
232        wrapper.loadInitial(PageKeyedDataSource.LoadInitialParams<String>(4, true),
233                loadInitialCallback)
234        val expectedInitial = PAGE_MAP.get(INIT_KEY)!!
235        verify(loadInitialCallback).onResult(expectedInitial.data.map { it.toString() },
236                expectedInitial.prev, expectedInitial.next)
237        verifyNoMoreInteractions(loadInitialCallback)
238
239        @Suppress("UNCHECKED_CAST")
240        val loadCallback = mock(PageKeyedDataSource.LoadCallback::class.java)
241                as PageKeyedDataSource.LoadCallback<String, String>
242        // load after
243        wrapper.loadAfter(PageKeyedDataSource.LoadParams(expectedInitial.next!!, 4), loadCallback)
244        val expectedAfter = PAGE_MAP.get(expectedInitial.next)!!
245        verify(loadCallback).onResult(expectedAfter.data.map { it.toString() },
246                expectedAfter.next)
247        verifyNoMoreInteractions(loadCallback)
248
249        // load before
250        wrapper.loadBefore(PageKeyedDataSource.LoadParams(expectedAfter.prev!!, 4), loadCallback)
251        verify(loadCallback).onResult(expectedInitial.data.map { it.toString() },
252                expectedInitial.prev)
253        verifyNoMoreInteractions(loadCallback)
254
255        // verify invalidation
256        orig.invalidate()
257        assertTrue(wrapper.isInvalid)
258    }
259
260    @Test
261    fun testManualWrappedDataSource() = verifyWrappedDataSource {
262        StringWrapperDataSource(it)
263    }
264
265    @Test
266    fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
267        it.mapByPage { it.map { it.toString() } }
268    }
269
270    @Test
271    fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
272        it.map { it.toString() }
273    }
274
275    @Test
276    fun testInvalidateToWrapper() {
277        val orig = ItemDataSource()
278        val wrapper = orig.map { it.toString() }
279
280        orig.invalidate()
281        assertTrue(wrapper.isInvalid)
282    }
283
284    @Test
285    fun testInvalidateFromWrapper() {
286        val orig = ItemDataSource()
287        val wrapper = orig.map { it.toString() }
288
289        wrapper.invalidate()
290        assertTrue(orig.isInvalid)
291    }
292
293    companion object {
294        // first load is 2nd page to ensure we test prepend as well as append behavior
295        private val INIT_KEY: String = "key 2"
296        private val PAGE_MAP: Map<String, Page>
297        private val ITEM_LIST: List<Item>
298
299        init {
300            val map = HashMap<String, Page>()
301            val list = ArrayList<Item>()
302            val pageCount = 5
303            for (i in 1..pageCount) {
304                val data = List(4) { Item("name $i $it") }
305                list.addAll(data)
306
307                val key = "key $i"
308                val prev = if (i > 1) ("key " + (i - 1)) else null
309                val next = if (i < pageCount) ("key " + (i + 1)) else null
310                map.put(key, Page(prev, data, next))
311            }
312            PAGE_MAP = map
313            ITEM_LIST = list
314        }
315    }
316
317    private fun drain() {
318        var executed: Boolean
319        do {
320            executed = mBackgroundThread.executeAll()
321            executed = mMainThread.executeAll() || executed
322        } while (executed)
323    }
324}
325