/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v7.util; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import android.support.annotation.Nullable; import android.support.test.filters.SmallTest; import org.hamcrest.CoreMatchers; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.UUID; @RunWith(JUnit4.class) @SmallTest public class DiffUtilTest { private static Random sRand = new Random(System.nanoTime()); private List mBefore = new ArrayList<>(); private List mAfter = new ArrayList<>(); private StringBuilder mLog = new StringBuilder(); private DiffUtil.Callback mCallback = new DiffUtil.Callback() { @Override public int getOldListSize() { return mBefore.size(); } @Override public int getNewListSize() { return mAfter.size(); } @Override public boolean areItemsTheSame(int oldItemIndex, int newItemIndex) { return mBefore.get(oldItemIndex).id == mAfter.get(newItemIndex).id; } @Override public boolean areContentsTheSame(int oldItemIndex, int newItemIndex) { assertThat(mBefore.get(oldItemIndex).id, CoreMatchers.equalTo(mAfter.get(newItemIndex).id)); return mBefore.get(oldItemIndex).data.equals(mAfter.get(newItemIndex).data); } @Nullable @Override public Object getChangePayload(int oldItemIndex, int newItemIndex) { assertThat(mBefore.get(oldItemIndex).id, CoreMatchers.equalTo(mAfter.get(newItemIndex).id)); assertThat(mBefore.get(oldItemIndex).data, not(CoreMatchers.equalTo(mAfter.get(newItemIndex).data))); return mAfter.get(newItemIndex).payload; } }; @Rule public TestWatcher mLogOnExceptionWatcher = new TestWatcher() { @Override protected void failed(Throwable e, Description description) { System.err.println(mLog.toString()); } }; @Test public void testNoChange() { initWithSize(5); check(); } @Test public void testAddItems() { initWithSize(2); add(1); check(); } //@Test //@LargeTest // Used for development public void testRandom() { for (int x = 0; x < 100; x++) { for (int i = 0; i < 100; i++) { for (int j = 2; j < 40; j++) { testRandom(i, j); } } } } @Test public void testGen2() { initWithSize(5); add(5); delete(3); delete(1); check(); } @Test public void testGen3() { initWithSize(5); add(0); delete(1); delete(3); check(); } @Test public void testGen4() { initWithSize(5); add(5); add(1); add(4); add(4); check(); } @Test public void testGen5() { initWithSize(5); delete(0); delete(2); add(0); add(2); check(); } @Test public void testGen6() { initWithSize(2); delete(0); delete(0); check(); } @Test public void testGen7() { initWithSize(3); move(2, 0); delete(2); add(2); check(); } @Test public void testGen8() { initWithSize(3); delete(1); add(0); move(2, 0); check(); } @Test public void testGen9() { initWithSize(2); add(2); move(0, 2); check(); } @Test public void testGen10() { initWithSize(3); move(0, 1); move(1, 2); add(0); check(); } @Test public void testGen11() { initWithSize(4); move(2, 0); move(2, 3); check(); } @Test public void testGen12() { initWithSize(4); move(3, 0); move(2, 1); check(); } @Test public void testGen13() { initWithSize(4); move(3, 2); move(0, 3); check(); } @Test public void testGen14() { initWithSize(4); move(3, 2); add(4); move(0, 4); check(); } @Test public void testAdd1() { initWithSize(1); add(1); check(); } @Test public void testMove1() { initWithSize(3); move(0, 2); check(); } @Test public void tmp() { initWithSize(4); move(0, 2); check(); } @Test public void testUpdate1() { initWithSize(3); update(2); check(); } @Test public void testUpdate2() { initWithSize(2); add(1); update(1); update(2); check(); } @Test public void testDisableMoveDetection() { initWithSize(5); move(0, 4); List applied = applyUpdates(mBefore, DiffUtil.calculateDiff(mCallback, false)); assertThat(applied.size(), is(5)); assertThat(applied.get(4).newItem, is(true)); assertThat(applied.contains(mBefore.get(0)), is(false)); } private void testRandom(int initialSize, int operationCount) { mLog.setLength(0); initWithSize(initialSize); for (int i = 0; i < operationCount; i++) { int op = sRand.nextInt(5); switch (op) { case 0: add(sRand.nextInt(mAfter.size() + 1)); break; case 1: if (!mAfter.isEmpty()) { delete(sRand.nextInt(mAfter.size())); } break; case 2: // move if (mAfter.size() > 0) { move(sRand.nextInt(mAfter.size()), sRand.nextInt(mAfter.size())); } break; case 3: // update if (mAfter.size() > 0) { update(sRand.nextInt(mAfter.size())); } break; case 4: // update with payload if (mAfter.size() > 0) { updateWithPayload(sRand.nextInt(mAfter.size())); } break; } } check(); } private void check() { DiffUtil.DiffResult result = DiffUtil.calculateDiff(mCallback); log("before", mBefore); log("after", mAfter); log("snakes", result.getSnakes()); List applied = applyUpdates(mBefore, result); assertEquals(applied, mAfter); } private void initWithSize(int size) { mBefore.clear(); mAfter.clear(); for (int i = 0; i < size; i++) { mBefore.add(new Item(false)); } mAfter.addAll(mBefore); mLog.append("initWithSize(" + size + ");\n"); } private void log(String title, List items) { mLog.append(title).append(":").append(items.size()).append("\n"); for (Object item : items) { mLog.append(" ").append(item).append("\n"); } } private void assertEquals(List applied, List after) { log("applied", applied); String report = mLog.toString(); assertThat(report, applied.size(), is(after.size())); for (int i = 0; i < after.size(); i++) { Item item = applied.get(i); if (after.get(i).newItem) { assertThat(report, item.newItem, is(true)); } else if (after.get(i).changed) { assertThat(report, item.newItem, is(false)); assertThat(report, item.changed, is(true)); assertThat(report, item.id, is(after.get(i).id)); assertThat(report, item.payload, is(after.get(i).payload)); } else { assertThat(report, item, equalTo(after.get(i))); } } } private List applyUpdates(List before, DiffUtil.DiffResult result) { final List target = new ArrayList<>(); target.addAll(before); result.dispatchUpdatesTo(new ListUpdateCallback() { @Override public void onInserted(int position, int count) { for (int i = 0; i < count; i++) { target.add(i + position, new Item(true)); } } @Override public void onRemoved(int position, int count) { for (int i = 0; i < count; i++) { target.remove(position); } } @Override public void onMoved(int fromPosition, int toPosition) { Item item = target.remove(fromPosition); target.add(toPosition, item); } @Override public void onChanged(int position, int count, Object payload) { for (int i = 0; i < count; i++) { int positionInList = position + i; Item existing = target.get(positionInList); // make sure we don't update same item twice in callbacks assertThat(existing.changed, is(false)); assertThat(existing.newItem, is(false)); assertThat(existing.payload, is(nullValue())); Item replica = new Item(existing); replica.payload = (String) payload; replica.changed = true; target.remove(positionInList); target.add(positionInList, replica); } } }); return target; } private void add(int index) { mAfter.add(index, new Item(true)); mLog.append("add(").append(index).append(");\n"); } private void delete(int index) { mAfter.remove(index); mLog.append("delete(").append(index).append(");\n"); } private void update(int index) { Item existing = mAfter.get(index); if (existing.newItem) { return;//new item cannot be changed } Item replica = new Item(existing); replica.changed = true; // clean the payload since this might be after an updateWithPayload call replica.payload = null; replica.data = UUID.randomUUID().toString(); mAfter.remove(index); mAfter.add(index, replica); mLog.append("update(").append(index).append(");\n"); } private void updateWithPayload(int index) { Item existing = mAfter.get(index); if (existing.newItem) { return;//new item cannot be changed } Item replica = new Item(existing); replica.changed = true; replica.data = UUID.randomUUID().toString(); replica.payload = UUID.randomUUID().toString(); mAfter.remove(index); mAfter.add(index, replica); mLog.append("update(").append(index).append(");\n"); } private void move(int from, int to) { Item removed = mAfter.remove(from); mAfter.add(to, removed); mLog.append("move(").append(from).append(",").append(to).append(");\n"); } static class Item { static long idCounter = 0; final long id; final boolean newItem; boolean changed = false; String payload; String data = UUID.randomUUID().toString(); public Item(boolean newItem) { id = idCounter++; this.newItem = newItem; } public Item(Item other) { id = other.id; newItem = other.newItem; changed = other.changed; payload = other.payload; data = other.data; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Item item = (Item) o; if (id != item.id) return false; if (newItem != item.newItem) return false; if (changed != item.changed) return false; if (payload != null ? !payload.equals(item.payload) : item.payload != null) { return false; } return data.equals(item.data); } @Override public int hashCode() { int result = (int) (id ^ (id >>> 32)); result = 31 * result + (newItem ? 1 : 0); result = 31 * result + (changed ? 1 : 0); result = 31 * result + (payload != null ? payload.hashCode() : 0); result = 31 * result + data.hashCode(); return result; } @Override public String toString() { return "Item{" + "id=" + id + ", newItem=" + newItem + ", changed=" + changed + ", payload='" + payload + '\'' + ", data='" + data + '\'' + '}'; } } }