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