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