1/*
2 * Copyright (C) 2016 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 */
16package android.support.v7.util;
17
18import static org.hamcrest.CoreMatchers.equalTo;
19import static org.hamcrest.CoreMatchers.is;
20import static org.hamcrest.CoreMatchers.not;
21import static org.hamcrest.CoreMatchers.nullValue;
22import static org.hamcrest.MatcherAssert.assertThat;
23
24import android.support.annotation.Nullable;
25import android.support.test.filters.SmallTest;
26
27import org.hamcrest.CoreMatchers;
28import org.junit.Rule;
29import org.junit.Test;
30import org.junit.rules.TestWatcher;
31import org.junit.runner.Description;
32import org.junit.runner.RunWith;
33import org.junit.runners.JUnit4;
34
35import java.util.ArrayList;
36import java.util.List;
37import java.util.Random;
38import java.util.UUID;
39
40@RunWith(JUnit4.class)
41@SmallTest
42public class DiffUtilTest {
43    private static Random sRand = new Random(System.nanoTime());
44    private List<Item> mBefore = new ArrayList<>();
45    private List<Item> mAfter = new ArrayList<>();
46    private StringBuilder mLog = new StringBuilder();
47
48    private DiffUtil.Callback mCallback = new DiffUtil.Callback() {
49        @Override
50        public int getOldListSize() {
51            return mBefore.size();
52        }
53
54        @Override
55        public int getNewListSize() {
56            return mAfter.size();
57        }
58
59        @Override
60        public boolean areItemsTheSame(int oldItemIndex, int newItemIndex) {
61            return mBefore.get(oldItemIndex).id == mAfter.get(newItemIndex).id;
62        }
63
64        @Override
65        public boolean areContentsTheSame(int oldItemIndex, int newItemIndex) {
66            assertThat(mBefore.get(oldItemIndex).id,
67                    CoreMatchers.equalTo(mAfter.get(newItemIndex).id));
68            return mBefore.get(oldItemIndex).data.equals(mAfter.get(newItemIndex).data);
69        }
70
71        @Nullable
72        @Override
73        public Object getChangePayload(int oldItemIndex, int newItemIndex) {
74            assertThat(mBefore.get(oldItemIndex).id,
75                    CoreMatchers.equalTo(mAfter.get(newItemIndex).id));
76            assertThat(mBefore.get(oldItemIndex).data,
77                    not(CoreMatchers.equalTo(mAfter.get(newItemIndex).data)));
78            return mAfter.get(newItemIndex).payload;
79        }
80    };
81
82    @Rule
83    public TestWatcher mLogOnExceptionWatcher = new TestWatcher() {
84        @Override
85        protected void failed(Throwable e, Description description) {
86            System.err.println(mLog.toString());
87        }
88    };
89
90
91    @Test
92    public void testNoChange() {
93        initWithSize(5);
94        check();
95    }
96
97    @Test
98    public void testAddItems() {
99        initWithSize(2);
100        add(1);
101        check();
102    }
103
104    //@Test
105    //@LargeTest
106    // Used for development
107    public void testRandom() {
108        for (int x = 0; x < 100; x++) {
109            for (int i = 0; i < 100; i++) {
110                for (int j = 2; j < 40; j++) {
111                    testRandom(i, j);
112                }
113            }
114        }
115    }
116
117    @Test
118    public void testGen2() {
119        initWithSize(5);
120        add(5);
121        delete(3);
122        delete(1);
123        check();
124    }
125
126    @Test
127    public void testGen3() {
128        initWithSize(5);
129        add(0);
130        delete(1);
131        delete(3);
132        check();
133    }
134
135    @Test
136    public void testGen4() {
137        initWithSize(5);
138        add(5);
139        add(1);
140        add(4);
141        add(4);
142        check();
143    }
144
145    @Test
146    public void testGen5() {
147        initWithSize(5);
148        delete(0);
149        delete(2);
150        add(0);
151        add(2);
152        check();
153    }
154
155    @Test
156    public void testGen6() {
157        initWithSize(2);
158        delete(0);
159        delete(0);
160        check();
161    }
162
163    @Test
164    public void testGen7() {
165        initWithSize(3);
166        move(2, 0);
167        delete(2);
168        add(2);
169        check();
170    }
171
172    @Test
173    public void testGen8() {
174        initWithSize(3);
175        delete(1);
176        add(0);
177        move(2, 0);
178        check();
179    }
180
181    @Test
182    public void testGen9() {
183        initWithSize(2);
184        add(2);
185        move(0, 2);
186        check();
187    }
188
189    @Test
190    public void testGen10() {
191        initWithSize(3);
192        move(0, 1);
193        move(1, 2);
194        add(0);
195        check();
196    }
197
198    @Test
199    public void testGen11() {
200        initWithSize(4);
201        move(2, 0);
202        move(2, 3);
203        check();
204    }
205
206    @Test
207    public void testGen12() {
208        initWithSize(4);
209        move(3, 0);
210        move(2, 1);
211        check();
212    }
213
214    @Test
215    public void testGen13() {
216        initWithSize(4);
217        move(3, 2);
218        move(0, 3);
219        check();
220    }
221
222    @Test
223    public void testGen14() {
224        initWithSize(4);
225        move(3, 2);
226        add(4);
227        move(0, 4);
228        check();
229    }
230
231    @Test
232    public void testAdd1() {
233        initWithSize(1);
234        add(1);
235        check();
236    }
237
238    @Test
239    public void testMove1() {
240        initWithSize(3);
241        move(0, 2);
242        check();
243    }
244
245    @Test
246    public void tmp() {
247        initWithSize(4);
248        move(0, 2);
249        check();
250    }
251
252    @Test
253    public void testUpdate1() {
254        initWithSize(3);
255        update(2);
256        check();
257    }
258
259    @Test
260    public void testUpdate2() {
261        initWithSize(2);
262        add(1);
263        update(1);
264        update(2);
265        check();
266    }
267
268    @Test
269    public void testDisableMoveDetection() {
270        initWithSize(5);
271        move(0, 4);
272        List<Item> applied = applyUpdates(mBefore, DiffUtil.calculateDiff(mCallback, false));
273        assertThat(applied.size(), is(5));
274        assertThat(applied.get(4).newItem, is(true));
275        assertThat(applied.contains(mBefore.get(0)), is(false));
276    }
277
278    private void testRandom(int initialSize, int operationCount) {
279        mLog.setLength(0);
280        initWithSize(initialSize);
281        for (int i = 0; i < operationCount; i++) {
282            int op = sRand.nextInt(5);
283            switch (op) {
284                case 0:
285                    add(sRand.nextInt(mAfter.size() + 1));
286                    break;
287                case 1:
288                    if (!mAfter.isEmpty()) {
289                        delete(sRand.nextInt(mAfter.size()));
290                    }
291                    break;
292                case 2:
293                    // move
294                    if (mAfter.size() > 0) {
295                        move(sRand.nextInt(mAfter.size()), sRand.nextInt(mAfter.size()));
296                    }
297                    break;
298                case 3:
299                    // update
300                    if (mAfter.size() > 0) {
301                        update(sRand.nextInt(mAfter.size()));
302                    }
303                    break;
304                case 4:
305                    // update with payload
306                    if (mAfter.size() > 0) {
307                        updateWithPayload(sRand.nextInt(mAfter.size()));
308                    }
309                    break;
310            }
311        }
312        check();
313    }
314
315    private void check() {
316        DiffUtil.DiffResult result = DiffUtil.calculateDiff(mCallback);
317        log("before", mBefore);
318        log("after", mAfter);
319        log("snakes", result.getSnakes());
320
321        List<Item> applied = applyUpdates(mBefore, result);
322        assertEquals(applied, mAfter);
323    }
324
325    private void initWithSize(int size) {
326        mBefore.clear();
327        mAfter.clear();
328        for (int i = 0; i < size; i++) {
329            mBefore.add(new Item(false));
330        }
331        mAfter.addAll(mBefore);
332        mLog.append("initWithSize(" + size + ");\n");
333    }
334
335    private void log(String title, List<?> items) {
336        mLog.append(title).append(":").append(items.size()).append("\n");
337        for (Object item : items) {
338            mLog.append("  ").append(item).append("\n");
339        }
340    }
341
342    private void assertEquals(List<Item> applied, List<Item> after) {
343        log("applied", applied);
344
345        String report = mLog.toString();
346        assertThat(report, applied.size(), is(after.size()));
347        for (int i = 0; i < after.size(); i++) {
348            Item item = applied.get(i);
349            if (after.get(i).newItem) {
350                assertThat(report, item.newItem, is(true));
351            } else if (after.get(i).changed) {
352                assertThat(report, item.newItem, is(false));
353                assertThat(report, item.changed, is(true));
354                assertThat(report, item.id, is(after.get(i).id));
355                assertThat(report, item.payload, is(after.get(i).payload));
356            } else {
357                assertThat(report, item, equalTo(after.get(i)));
358            }
359        }
360    }
361
362    private List<Item> applyUpdates(List<Item> before, DiffUtil.DiffResult result) {
363        final List<Item> target = new ArrayList<>();
364        target.addAll(before);
365        result.dispatchUpdatesTo(new ListUpdateCallback() {
366            @Override
367            public void onInserted(int position, int count) {
368                for (int i = 0; i < count; i++) {
369                    target.add(i + position, new Item(true));
370                }
371            }
372
373            @Override
374            public void onRemoved(int position, int count) {
375                for (int i = 0; i < count; i++) {
376                    target.remove(position);
377                }
378            }
379
380            @Override
381            public void onMoved(int fromPosition, int toPosition) {
382                Item item = target.remove(fromPosition);
383                target.add(toPosition, item);
384            }
385
386            @Override
387            public void onChanged(int position, int count, Object payload) {
388                for (int i = 0; i < count; i++) {
389                    int positionInList = position + i;
390                    Item existing = target.get(positionInList);
391                    // make sure we don't update same item twice in callbacks
392                    assertThat(existing.changed, is(false));
393                    assertThat(existing.newItem, is(false));
394                    assertThat(existing.payload, is(nullValue()));
395                    Item replica = new Item(existing);
396                    replica.payload = (String) payload;
397                    replica.changed = true;
398                    target.remove(positionInList);
399                    target.add(positionInList, replica);
400                }
401            }
402        });
403        return target;
404    }
405
406    private void add(int index) {
407        mAfter.add(index, new Item(true));
408        mLog.append("add(").append(index).append(");\n");
409    }
410
411    private void delete(int index) {
412        mAfter.remove(index);
413        mLog.append("delete(").append(index).append(");\n");
414    }
415
416    private void update(int index) {
417        Item existing = mAfter.get(index);
418        if (existing.newItem) {
419            return;//new item cannot be changed
420        }
421        Item replica = new Item(existing);
422        replica.changed = true;
423        // clean the payload since this might be after an updateWithPayload call
424        replica.payload = null;
425        replica.data = UUID.randomUUID().toString();
426        mAfter.remove(index);
427        mAfter.add(index, replica);
428        mLog.append("update(").append(index).append(");\n");
429    }
430
431    private void updateWithPayload(int index) {
432        Item existing = mAfter.get(index);
433        if (existing.newItem) {
434            return;//new item cannot be changed
435        }
436        Item replica = new Item(existing);
437        replica.changed = true;
438        replica.data = UUID.randomUUID().toString();
439        replica.payload = UUID.randomUUID().toString();
440        mAfter.remove(index);
441        mAfter.add(index, replica);
442        mLog.append("update(").append(index).append(");\n");
443    }
444
445    private void move(int from, int to) {
446        Item removed = mAfter.remove(from);
447        mAfter.add(to, removed);
448        mLog.append("move(").append(from).append(",").append(to).append(");\n");
449    }
450
451    static class Item {
452        static long idCounter = 0;
453        final long id;
454        final boolean newItem;
455        boolean changed = false;
456        String payload;
457
458        String data = UUID.randomUUID().toString();
459
460        public Item(boolean newItem) {
461            id = idCounter++;
462            this.newItem = newItem;
463        }
464
465        public Item(Item other) {
466            id = other.id;
467            newItem = other.newItem;
468            changed = other.changed;
469            payload = other.payload;
470            data = other.data;
471        }
472
473        @Override
474        public boolean equals(Object o) {
475            if (this == o) return true;
476            if (o == null || getClass() != o.getClass()) return false;
477
478            Item item = (Item) o;
479
480            if (id != item.id) return false;
481            if (newItem != item.newItem) return false;
482            if (changed != item.changed) return false;
483            if (payload != null ? !payload.equals(item.payload) : item.payload != null) {
484                return false;
485            }
486            return data.equals(item.data);
487
488        }
489
490        @Override
491        public int hashCode() {
492            int result = (int) (id ^ (id >>> 32));
493            result = 31 * result + (newItem ? 1 : 0);
494            result = 31 * result + (changed ? 1 : 0);
495            result = 31 * result + (payload != null ? payload.hashCode() : 0);
496            result = 31 * result + data.hashCode();
497            return result;
498        }
499
500        @Override
501        public String toString() {
502            return "Item{" +
503                    "id=" + id +
504                    ", newItem=" + newItem +
505                    ", changed=" + changed +
506                    ", payload='" + payload + '\'' +
507                    ", data='" + data + '\'' +
508                    '}';
509        }
510    }
511}
512