1/* 2 * Copyright 2018 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.recyclerview.widget 18 19import android.support.test.filters.SmallTest 20import org.junit.Assert.assertEquals 21import org.junit.Assert.assertNotSame 22import org.junit.Assert.assertSame 23import org.junit.Assert.fail 24import org.junit.Test 25import org.junit.runner.RunWith 26import org.junit.runners.JUnit4 27import org.mockito.Mockito.mock 28import org.mockito.Mockito.verify 29import org.mockito.Mockito.verifyNoMoreInteractions 30import org.mockito.Mockito.verifyZeroInteractions 31import java.lang.UnsupportedOperationException 32import java.util.Collections.emptyList 33import java.util.LinkedList 34import java.util.concurrent.Executor 35 36class TestExecutor : Executor { 37 private val mTasks = LinkedList<Runnable>() 38 39 override fun execute(command: Runnable) { 40 mTasks.add(command) 41 } 42 43 fun executeAll(): Boolean { 44 val consumed = !mTasks.isEmpty() 45 46 var task = mTasks.poll() 47 while (task != null) { 48 task.run() 49 task = mTasks.poll() 50 } 51 return consumed 52 } 53} 54 55@SmallTest 56@RunWith(JUnit4::class) 57class AsyncListDifferTest { 58 private val mMainThread = TestExecutor() 59 private val mBackgroundThread = TestExecutor() 60 61 private fun <T> createDiffer(listUpdateCallback: ListUpdateCallback, 62 diffCallback: DiffUtil.ItemCallback<T>): AsyncListDiffer<T> { 63 return AsyncListDiffer(listUpdateCallback, 64 AsyncDifferConfig.Builder<T>(diffCallback) 65 .setMainThreadExecutor(mMainThread) 66 .setBackgroundThreadExecutor(mBackgroundThread) 67 .build()) 68 } 69 70 @Test 71 fun initialState() { 72 val callback = mock(ListUpdateCallback::class.java) 73 val differ = createDiffer(callback, STRING_DIFF_CALLBACK) 74 assertEquals(0, differ.currentList.size) 75 verifyZeroInteractions(callback) 76 } 77 78 @Test(expected = IndexOutOfBoundsException::class) 79 fun getEmpty() { 80 val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK) 81 differ.currentList[0] 82 } 83 84 @Test(expected = IndexOutOfBoundsException::class) 85 fun getNegative() { 86 val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK) 87 differ.submitList(listOf("a", "b")) 88 differ.currentList[-1] 89 } 90 91 @Test(expected = IndexOutOfBoundsException::class) 92 fun getPastEnd() { 93 val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK) 94 differ.submitList(listOf("a", "b")) 95 differ.currentList[2] 96 } 97 98 @Test 99 fun getCurrentList() { 100 val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK) 101 102 // null is emptyList 103 assertSame(emptyList<String>(), differ.currentList) 104 105 // other list is wrapped 106 val list = listOf("a", "b") 107 differ.submitList(list) 108 assertEquals(list, differ.currentList) 109 assertNotSame(list, differ.currentList) 110 111 // null again, empty again 112 differ.submitList(null) 113 assertSame(emptyList<String>(), differ.currentList) 114 } 115 116 @Test(expected = UnsupportedOperationException::class) 117 fun mutateCurrentListEmpty() { 118 val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK) 119 differ.currentList[0] = "" 120 } 121 122 @Test(expected = UnsupportedOperationException::class) 123 fun mutateCurrentListNonEmpty() { 124 val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK) 125 differ.submitList(listOf("a")) 126 differ.currentList[0] = "" 127 } 128 129 @Test 130 fun submitListSimple() { 131 val callback = mock(ListUpdateCallback::class.java) 132 val differ = createDiffer(callback, STRING_DIFF_CALLBACK) 133 134 differ.submitList(listOf("a", "b")) 135 136 assertEquals(2, differ.currentList.size) 137 assertEquals("a", differ.currentList[0]) 138 assertEquals("b", differ.currentList[1]) 139 140 verify(callback).onInserted(0, 2) 141 verifyNoMoreInteractions(callback) 142 drain() 143 verifyNoMoreInteractions(callback) 144 } 145 146 @Test 147 fun nullsSkipCallback() { 148 val callback = mock(ListUpdateCallback::class.java) 149 // Note: by virtue of being written in Kotlin, the item callback includes explicit null 150 // checks on its parameters which assert that it is not invoked with a null value. 151 val helper = createDiffer(callback, STRING_DIFF_CALLBACK) 152 153 helper.submitList(listOf("a", "b")) 154 drain() 155 verify(callback).onInserted(0, 2) 156 157 helper.submitList(listOf("a", null)) 158 drain() 159 verify(callback).onRemoved(1, 1) 160 verify(callback).onInserted(1, 1) 161 162 helper.submitList(listOf("b", null)) 163 drain() 164 verify(callback).onRemoved(0, 1) 165 verify(callback).onInserted(0, 1) 166 167 verifyNoMoreInteractions(callback) 168 } 169 170 @Test 171 fun submitListUpdate() { 172 val callback = mock(ListUpdateCallback::class.java) 173 val differ = createDiffer(callback, STRING_DIFF_CALLBACK) 174 175 // initial list (immediate) 176 differ.submitList(listOf("a", "b")) 177 verify(callback).onInserted(0, 2) 178 verifyNoMoreInteractions(callback) 179 drain() 180 verifyNoMoreInteractions(callback) 181 182 // update (deferred) 183 differ.submitList(listOf("a", "b", "c")) 184 verifyNoMoreInteractions(callback) 185 drain() 186 verify(callback).onInserted(2, 1) 187 verifyNoMoreInteractions(callback) 188 189 // clear (immediate) 190 differ.submitList(null) 191 verify(callback).onRemoved(0, 3) 192 verifyNoMoreInteractions(callback) 193 drain() 194 verifyNoMoreInteractions(callback) 195 } 196 197 @Test 198 fun singleChangePayload() { 199 val callback = mock(ListUpdateCallback::class.java) 200 val differ = createDiffer(callback, STRING_DIFF_CALLBACK) 201 202 differ.submitList(listOf("a", "b")) 203 verify(callback).onInserted(0, 2) 204 verifyNoMoreInteractions(callback) 205 drain() 206 verifyNoMoreInteractions(callback) 207 208 differ.submitList(listOf("a", "beta")) 209 verifyNoMoreInteractions(callback) 210 drain() 211 verify(callback).onChanged(1, 1, "eta") 212 verifyNoMoreInteractions(callback) 213 } 214 215 @Test 216 fun multiChangePayload() { 217 val callback = mock(ListUpdateCallback::class.java) 218 val differ = createDiffer(callback, STRING_DIFF_CALLBACK) 219 220 differ.submitList(listOf("a", "b")) 221 verify(callback).onInserted(0, 2) 222 verifyNoMoreInteractions(callback) 223 drain() 224 verifyNoMoreInteractions(callback) 225 226 differ.submitList(listOf("alpha", "beta")) 227 verifyNoMoreInteractions(callback) 228 drain() 229 verify(callback).onChanged(1, 1, "eta") 230 verify(callback).onChanged(0, 1, "lpha") 231 verifyNoMoreInteractions(callback) 232 } 233 234 @Test 235 fun listUpdatedBeforeListUpdateCallbacks() { 236 // verify that itemCount is updated in the differ before dispatching ListUpdateCallbacks 237 238 val expectedCount = intArrayOf(0) 239 // provides access to differ, which must be constructed after callback 240 val differAccessor = arrayOf<AsyncListDiffer<*>?>(null) 241 242 val callback = object : ListUpdateCallback { 243 244 override fun onInserted(position: Int, count: Int) { 245 assertEquals(expectedCount[0], differAccessor[0]!!.currentList.size) 246 } 247 248 override fun onRemoved(position: Int, count: Int) { 249 assertEquals(expectedCount[0], differAccessor[0]!!.currentList.size) 250 } 251 252 override fun onMoved(fromPosition: Int, toPosition: Int) { 253 fail("not expected") 254 } 255 256 override fun onChanged(position: Int, count: Int, payload: Any?) { 257 fail("not expected") 258 } 259 } 260 261 val differ = createDiffer(callback, STRING_DIFF_CALLBACK) 262 differAccessor[0] = differ 263 264 // in the fast-add case... 265 expectedCount[0] = 3 266 assertEquals(0, differ.currentList.size) 267 differ.submitList(listOf("a", "b", "c")) 268 assertEquals(3, differ.currentList.size) 269 270 // in the slow, diff on BG thread case... 271 expectedCount[0] = 6 272 assertEquals(3, differ.currentList.size) 273 differ.submitList(listOf("a", "b", "c", "d", "e", "f")) 274 drain() 275 assertEquals(6, differ.currentList.size) 276 277 // and in the fast-remove case 278 expectedCount[0] = 0 279 assertEquals(6, differ.currentList.size) 280 differ.submitList(null) 281 assertEquals(0, differ.currentList.size) 282 } 283 284 private fun drain() { 285 var executed: Boolean 286 do { 287 executed = mBackgroundThread.executeAll() 288 executed = mMainThread.executeAll() or executed 289 } while (executed) 290 } 291 292 companion object { 293 private val STRING_DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() { 294 override fun areItemsTheSame(oldItem: String, newItem: String): Boolean { 295 // items are the same if first char is the same 296 return oldItem[0] == newItem[0] 297 } 298 299 override fun areContentsTheSame(oldItem: String, newItem: String): Boolean { 300 return oldItem == newItem 301 } 302 303 override fun getChangePayload(oldItem: String, newItem: String): Any? { 304 if (newItem.startsWith(oldItem)) { 305 // new string is appended, return added portion on the end 306 return newItem.subSequence(oldItem.length, newItem.length) 307 } 308 return null 309 } 310 } 311 312 private val IGNORE_CALLBACK = object : ListUpdateCallback { 313 override fun onInserted(position: Int, count: Int) {} 314 315 override fun onRemoved(position: Int, count: Int) {} 316 317 override fun onMoved(fromPosition: Int, toPosition: Int) {} 318 319 override fun onChanged(position: Int, count: Int, payload: Any?) {} 320 } 321 } 322} 323