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