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