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