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 17 package androidx.paging 18 19 import androidx.arch.core.util.Function 20 import org.junit.Assert.assertArrayEquals 21 import org.junit.Assert.assertEquals 22 import org.junit.Assert.assertFalse 23 import org.junit.Assert.assertSame 24 import org.junit.Assert.assertTrue 25 import org.junit.Test 26 import org.junit.runner.RunWith 27 import org.junit.runners.Parameterized 28 import org.mockito.Mockito.mock 29 import org.mockito.Mockito.verify 30 import org.mockito.Mockito.verifyNoMoreInteractions 31 import org.mockito.Mockito.verifyZeroInteractions 32 import java.util.concurrent.Executor 33 34 @RunWith(Parameterized::class) 35 class ContiguousPagedListTest(private val mCounted: Boolean) { 36 private val mMainThread = TestExecutor() 37 private val mBackgroundThread = TestExecutor() 38 39 private class Item(position: Int) { 40 val name: String = "Item $position" 41 42 override fun toString(): String { 43 return name 44 } 45 } 46 47 private inner class TestSource(val listData: List<Item> = ITEMS) 48 : ContiguousDataSource<Int, Item>() { 49 override fun dispatchLoadInitial( 50 key: Int?, 51 initialLoadSize: Int, 52 pageSize: Int, 53 enablePlaceholders: Boolean, 54 mainThreadExecutor: Executor, 55 receiver: PageResult.Receiver<Item>) { 56 57 val convertPosition = key ?: 0 58 val position = Math.max(0, (convertPosition - initialLoadSize / 2)) 59 val data = getClampedRange(position, position + initialLoadSize) 60 val trailingUnloadedCount = listData.size - position - data.size 61 62 if (enablePlaceholders && mCounted) { 63 receiver.onPageResult(PageResult.INIT, 64 PageResult(data, position, trailingUnloadedCount, 0)) 65 } else { 66 // still must pass offset, even if not counted 67 receiver.onPageResult(PageResult.INIT, 68 PageResult(data, position)) 69 } 70 } 71 72 override fun dispatchLoadAfter( 73 currentEndIndex: Int, 74 currentEndItem: Item, 75 pageSize: Int, 76 mainThreadExecutor: Executor, 77 receiver: PageResult.Receiver<Item>) { 78 val startIndex = currentEndIndex + 1 79 val data = getClampedRange(startIndex, startIndex + pageSize) 80 81 mainThreadExecutor.execute { 82 receiver.onPageResult(PageResult.APPEND, PageResult(data, 0, 0, 0)) 83 } 84 } 85 86 override fun dispatchLoadBefore( 87 currentBeginIndex: Int, 88 currentBeginItem: Item, 89 pageSize: Int, 90 mainThreadExecutor: Executor, 91 receiver: PageResult.Receiver<Item>) { 92 93 val startIndex = currentBeginIndex - 1 94 val data = getClampedRange(startIndex - pageSize + 1, startIndex + 1) 95 96 mainThreadExecutor.execute { 97 receiver.onPageResult(PageResult.PREPEND, PageResult(data, 0, 0, 0)) 98 } 99 } 100 101 override fun getKey(position: Int, item: Item?): Int { 102 return 0 103 } 104 105 private fun getClampedRange(startInc: Int, endExc: Int): List<Item> { 106 return listData.subList(Math.max(0, startInc), Math.min(listData.size, endExc)) 107 } 108 109 override fun <ToValue : Any?> mapByPage(function: Function<List<Item>, List<ToValue>>): 110 DataSource<Int, ToValue> { 111 throw UnsupportedOperationException() 112 } 113 114 override fun <ToValue : Any?> map(function: Function<Item, ToValue>): 115 DataSource<Int, ToValue> { 116 throw UnsupportedOperationException() 117 } 118 } 119 120 private fun verifyRange(start: Int, count: Int, actual: PagedStorage<Item>) { 121 if (mCounted) { 122 // assert nulls + content 123 val expected = arrayOfNulls<Item>(ITEMS.size) 124 System.arraycopy(ITEMS.toTypedArray(), start, expected, start, count) 125 assertArrayEquals(expected, actual.toTypedArray()) 126 127 val expectedTrailing = ITEMS.size - start - count 128 assertEquals(ITEMS.size, actual.size) 129 assertEquals((ITEMS.size - start - expectedTrailing), 130 actual.storageCount) 131 assertEquals(start, actual.leadingNullCount) 132 assertEquals(expectedTrailing, actual.trailingNullCount) 133 } else { 134 assertEquals(ITEMS.subList(start, start + count), actual) 135 136 assertEquals(count, actual.size) 137 assertEquals(actual.size, actual.storageCount) 138 assertEquals(0, actual.leadingNullCount) 139 assertEquals(0, actual.trailingNullCount) 140 } 141 } 142 143 private fun verifyRange(start: Int, count: Int, actual: PagedList<Item>) { 144 verifyRange(start, count, actual.mStorage) 145 } 146 147 private fun createCountedPagedList( 148 initialPosition: Int, 149 pageSize: Int = 20, 150 initLoadSize: Int = 40, 151 prefetchDistance: Int = 20, 152 listData: List<Item> = ITEMS, 153 boundaryCallback: PagedList.BoundaryCallback<Item>? = null, 154 lastLoad: Int = ContiguousPagedList.LAST_LOAD_UNSPECIFIED 155 ): ContiguousPagedList<Int, Item> { 156 return ContiguousPagedList( 157 TestSource(listData), mMainThread, mBackgroundThread, boundaryCallback, 158 PagedList.Config.Builder() 159 .setInitialLoadSizeHint(initLoadSize) 160 .setPageSize(pageSize) 161 .setPrefetchDistance(prefetchDistance) 162 .build(), 163 initialPosition, 164 lastLoad) 165 } 166 167 @Test 168 fun construct() { 169 val pagedList = createCountedPagedList(0) 170 verifyRange(0, 40, pagedList) 171 } 172 173 @Test 174 fun getDataSource() { 175 val pagedList = createCountedPagedList(0) 176 assertTrue(pagedList.dataSource is TestSource) 177 178 // snapshot keeps same DataSource 179 assertSame(pagedList.dataSource, 180 (pagedList.snapshot() as SnapshotPagedList<Item>).dataSource) 181 } 182 183 private fun verifyCallback(callback: PagedList.Callback, countedPosition: Int, 184 uncountedPosition: Int) { 185 if (mCounted) { 186 verify(callback).onChanged(countedPosition, 20) 187 } else { 188 verify(callback).onInserted(uncountedPosition, 20) 189 } 190 } 191 192 @Test 193 fun append() { 194 val pagedList = createCountedPagedList(0) 195 val callback = mock(PagedList.Callback::class.java) 196 pagedList.addWeakCallback(null, callback) 197 verifyRange(0, 40, pagedList) 198 verifyZeroInteractions(callback) 199 200 pagedList.loadAround(35) 201 drain() 202 203 verifyRange(0, 60, pagedList) 204 verifyCallback(callback, 40, 40) 205 verifyNoMoreInteractions(callback) 206 } 207 208 @Test 209 fun prepend() { 210 val pagedList = createCountedPagedList(80) 211 val callback = mock(PagedList.Callback::class.java) 212 pagedList.addWeakCallback(null, callback) 213 verifyRange(60, 40, pagedList) 214 verifyZeroInteractions(callback) 215 216 pagedList.loadAround(if (mCounted) 65 else 5) 217 drain() 218 219 verifyRange(40, 60, pagedList) 220 verifyCallback(callback, 40, 0) 221 verifyNoMoreInteractions(callback) 222 } 223 224 @Test 225 fun outwards() { 226 val pagedList = createCountedPagedList(50) 227 val callback = mock(PagedList.Callback::class.java) 228 pagedList.addWeakCallback(null, callback) 229 verifyRange(30, 40, pagedList) 230 verifyZeroInteractions(callback) 231 232 pagedList.loadAround(if (mCounted) 65 else 35) 233 drain() 234 235 verifyRange(30, 60, pagedList) 236 verifyCallback(callback, 70, 40) 237 verifyNoMoreInteractions(callback) 238 239 pagedList.loadAround(if (mCounted) 35 else 5) 240 drain() 241 242 verifyRange(10, 80, pagedList) 243 verifyCallback(callback, 10, 0) 244 verifyNoMoreInteractions(callback) 245 } 246 247 @Test 248 fun multiAppend() { 249 val pagedList = createCountedPagedList(0) 250 val callback = mock(PagedList.Callback::class.java) 251 pagedList.addWeakCallback(null, callback) 252 verifyRange(0, 40, pagedList) 253 verifyZeroInteractions(callback) 254 255 pagedList.loadAround(55) 256 drain() 257 258 verifyRange(0, 80, pagedList) 259 verifyCallback(callback, 40, 40) 260 verifyCallback(callback, 60, 60) 261 verifyNoMoreInteractions(callback) 262 } 263 264 @Test 265 fun distantPrefetch() { 266 val pagedList = createCountedPagedList(0, 267 initLoadSize = 10, pageSize = 10, prefetchDistance = 30) 268 val callback = mock(PagedList.Callback::class.java) 269 pagedList.addWeakCallback(null, callback) 270 verifyRange(0, 10, pagedList) 271 verifyZeroInteractions(callback) 272 273 pagedList.loadAround(5) 274 drain() 275 276 verifyRange(0, 40, pagedList) 277 278 pagedList.loadAround(6) 279 drain() 280 281 // although our prefetch window moves forward, no new load triggered 282 verifyRange(0, 40, pagedList) 283 } 284 285 @Test 286 fun appendCallbackAddedLate() { 287 val pagedList = createCountedPagedList(0) 288 verifyRange(0, 40, pagedList) 289 290 pagedList.loadAround(35) 291 drain() 292 verifyRange(0, 60, pagedList) 293 294 // snapshot at 60 items 295 val snapshot = pagedList.snapshot() as PagedList<Item> 296 verifyRange(0, 60, snapshot) 297 298 // load more items... 299 pagedList.loadAround(55) 300 drain() 301 verifyRange(0, 80, pagedList) 302 verifyRange(0, 60, snapshot) 303 304 // and verify the snapshot hasn't received them 305 val callback = mock(PagedList.Callback::class.java) 306 pagedList.addWeakCallback(snapshot, callback) 307 verifyCallback(callback, 60, 60) 308 verifyNoMoreInteractions(callback) 309 } 310 311 @Test 312 fun prependCallbackAddedLate() { 313 val pagedList = createCountedPagedList(80) 314 verifyRange(60, 40, pagedList) 315 316 pagedList.loadAround(if (mCounted) 65 else 5) 317 drain() 318 verifyRange(40, 60, pagedList) 319 320 // snapshot at 60 items 321 val snapshot = pagedList.snapshot() as PagedList<Item> 322 verifyRange(40, 60, snapshot) 323 324 pagedList.loadAround(if (mCounted) 45 else 5) 325 drain() 326 verifyRange(20, 80, pagedList) 327 verifyRange(40, 60, snapshot) 328 329 val callback = mock(PagedList.Callback::class.java) 330 pagedList.addWeakCallback(snapshot, callback) 331 verifyCallback(callback, 40, 0) 332 verifyNoMoreInteractions(callback) 333 } 334 335 @Test 336 fun initialLoad_lastLoad() { 337 val pagedList = createCountedPagedList( 338 initialPosition = 0, 339 initLoadSize = 20, 340 lastLoad = 4) 341 // last load is param passed 342 assertEquals(4, pagedList.mLastLoad) 343 verifyRange(0, 20, pagedList) 344 } 345 346 @Test 347 fun initialLoad_lastLoadComputed() { 348 val pagedList = createCountedPagedList( 349 initialPosition = 0, 350 initLoadSize = 20, 351 lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED) 352 // last load is middle of initial load 353 assertEquals(10, pagedList.mLastLoad) 354 verifyRange(0, 20, pagedList) 355 } 356 357 @Test 358 fun initialLoadAsync() { 359 // Note: ignores Parameterized param 360 val asyncDataSource = AsyncListDataSource(ITEMS) 361 val dataSource = asyncDataSource.wrapAsContiguousWithoutPlaceholders() 362 val pagedList = ContiguousPagedList( 363 dataSource, mMainThread, mBackgroundThread, null, 364 PagedList.Config.Builder().setPageSize(10).build(), null, 365 ContiguousPagedList.LAST_LOAD_UNSPECIFIED) 366 val callback = mock(PagedList.Callback::class.java) 367 pagedList.addWeakCallback(null, callback) 368 369 assertTrue(pagedList.isEmpty()) 370 drain() 371 assertTrue(pagedList.isEmpty()) 372 asyncDataSource.flush() 373 assertTrue(pagedList.isEmpty()) 374 mBackgroundThread.executeAll() 375 assertTrue(pagedList.isEmpty()) 376 verifyZeroInteractions(callback) 377 378 // Data source defers callbacks until flush, which posts result to main thread 379 mMainThread.executeAll() 380 assertFalse(pagedList.isEmpty()) 381 // callback onInsert called once with initial size 382 verify(callback).onInserted(0, pagedList.size) 383 verifyNoMoreInteractions(callback) 384 } 385 386 @Test 387 fun addWeakCallbackEmpty() { 388 // Note: ignores Parameterized param 389 val asyncDataSource = AsyncListDataSource(ITEMS) 390 val dataSource = asyncDataSource.wrapAsContiguousWithoutPlaceholders() 391 val pagedList = ContiguousPagedList( 392 dataSource, mMainThread, mBackgroundThread, null, 393 PagedList.Config.Builder().setPageSize(10).build(), null, 394 ContiguousPagedList.LAST_LOAD_UNSPECIFIED) 395 val callback = mock(PagedList.Callback::class.java) 396 397 // capture empty snapshot 398 val emptySnapshot = pagedList.snapshot() 399 assertTrue(pagedList.isEmpty()) 400 assertTrue(emptySnapshot.isEmpty()) 401 402 // verify that adding callback notifies nothing going from empty -> empty 403 pagedList.addWeakCallback(emptySnapshot, callback) 404 verifyZeroInteractions(callback) 405 pagedList.removeWeakCallback(callback) 406 407 // data added in asynchronously 408 asyncDataSource.flush() 409 drain() 410 assertFalse(pagedList.isEmpty()) 411 412 // verify that adding callback notifies insert going from empty -> content 413 pagedList.addWeakCallback(emptySnapshot, callback) 414 verify(callback).onInserted(0, pagedList.size) 415 verifyNoMoreInteractions(callback) 416 } 417 418 @Test 419 fun boundaryCallback_empty() { 420 @Suppress("UNCHECKED_CAST") 421 val boundaryCallback = 422 mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item> 423 val pagedList = createCountedPagedList(0, 424 listData = ArrayList(), boundaryCallback = boundaryCallback) 425 assertEquals(0, pagedList.size) 426 427 // nothing yet 428 verifyNoMoreInteractions(boundaryCallback) 429 430 // onZeroItemsLoaded posted, since creation often happens on BG thread 431 drain() 432 verify(boundaryCallback).onZeroItemsLoaded() 433 verifyNoMoreInteractions(boundaryCallback) 434 } 435 436 @Test 437 fun boundaryCallback_singleInitialLoad() { 438 val shortList = ITEMS.subList(0, 4) 439 @Suppress("UNCHECKED_CAST") 440 val boundaryCallback = 441 mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item> 442 val pagedList = createCountedPagedList(0, listData = shortList, 443 initLoadSize = shortList.size, boundaryCallback = boundaryCallback) 444 assertEquals(shortList.size, pagedList.size) 445 446 // nothing yet 447 verifyNoMoreInteractions(boundaryCallback) 448 449 // onItemAtFrontLoaded / onItemAtEndLoaded posted, since creation often happens on BG thread 450 drain() 451 pagedList.loadAround(0) 452 drain() 453 verify(boundaryCallback).onItemAtFrontLoaded(shortList.first()) 454 verify(boundaryCallback).onItemAtEndLoaded(shortList.last()) 455 verifyNoMoreInteractions(boundaryCallback) 456 } 457 458 @Test 459 fun boundaryCallback_delayed() { 460 @Suppress("UNCHECKED_CAST") 461 val boundaryCallback = 462 mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item> 463 val pagedList = createCountedPagedList(90, 464 initLoadSize = 20, prefetchDistance = 5, boundaryCallback = boundaryCallback) 465 verifyRange(80, 20, pagedList) 466 467 // nothing yet 468 verifyZeroInteractions(boundaryCallback) 469 drain() 470 verifyZeroInteractions(boundaryCallback) 471 472 // loading around last item causes onItemAtEndLoaded 473 pagedList.loadAround(if (mCounted) 99 else 19) 474 drain() 475 verifyRange(80, 20, pagedList) 476 verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last()) 477 verifyNoMoreInteractions(boundaryCallback) 478 479 // prepending doesn't trigger callback... 480 pagedList.loadAround(if (mCounted) 80 else 0) 481 drain() 482 verifyRange(60, 40, pagedList) 483 verifyZeroInteractions(boundaryCallback) 484 485 // ...load rest of data, still no dispatch... 486 pagedList.loadAround(if (mCounted) 60 else 0) 487 drain() 488 pagedList.loadAround(if (mCounted) 40 else 0) 489 drain() 490 pagedList.loadAround(if (mCounted) 20 else 0) 491 drain() 492 verifyRange(0, 100, pagedList) 493 verifyZeroInteractions(boundaryCallback) 494 495 // ... finally try prepend, see 0 items, which will dispatch front callback 496 pagedList.loadAround(0) 497 drain() 498 verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first()) 499 verifyNoMoreInteractions(boundaryCallback) 500 } 501 502 private fun drain() { 503 var executed: Boolean 504 do { 505 executed = mBackgroundThread.executeAll() 506 executed = mMainThread.executeAll() || executed 507 } while (executed) 508 } 509 510 companion object { 511 @JvmStatic 512 @Parameterized.Parameters(name = "counted:{0}") 513 fun parameters(): Array<Array<Boolean>> { 514 return arrayOf(arrayOf(true), arrayOf(false)) 515 } 516 517 private val ITEMS = List(100) { Item(it) } 518 } 519 } 520