blob: f16718d49fb1568def20391856b8294744168d61 [file] [log] [blame]
/*
* Copyright 2018 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 androidx.recyclerview.widget;
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.test.filters.SmallTest;
import androidx.annotation.Nullable;
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<Item> mBefore = new ArrayList<>();
private List<Item> 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<Item> 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<Item> 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<Item> applied, List<Item> 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<Item> applyUpdates(List<Item> before, DiffUtil.DiffResult result) {
final List<Item> 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 + '\'' +
'}';
}
}
}