/* * Copyright (C) 2014 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.widget; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import android.support.v7.widget.AdapterHelper.UpdateOp; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; import static android.support.v7.widget.AdapterHelper.UpdateOp.ADD; import static android.support.v7.widget.AdapterHelper.UpdateOp.MOVE; import static android.support.v7.widget.AdapterHelper.UpdateOp.REMOVE; import static android.support.v7.widget.AdapterHelper.UpdateOp.UPDATE; import static org.junit.Assert.*; @RunWith(JUnit4.class) @SmallTest public class OpReorderTest { private static final String TAG = "OpReorderTest"; List mUpdateOps = new ArrayList(); List mAddedItems = new ArrayList(); List mRemovedItems = new ArrayList(); Set mRecycledOps = new HashSet(); static Random random = new Random(System.nanoTime()); OpReorderer mOpReorderer = new OpReorderer(new OpReorderer.Callback() { @Override public UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload) { return new UpdateOp(cmd, startPosition, itemCount, payload); } @Override public void recycleUpdateOp(UpdateOp op) { mRecycledOps.add(op); } }); int itemCount = 10; int updatedItemCount = 0; public void setup(int count) { itemCount = count; updatedItemCount = itemCount; } @Before public void setUp() throws Exception { cleanState(); } void cleanState() { mUpdateOps = new ArrayList(); mAddedItems = new ArrayList(); mRemovedItems = new ArrayList(); mRecycledOps = new HashSet(); Item.idCounter = 0; } @Test public void testMoveRemoved() throws Exception { setup(10); mv(3, 8); rm(7, 3); process(); } @Test public void testMoveRemove() throws Exception { setup(10); mv(3, 8); rm(3, 5); process(); } @Test public void test1() { setup(10); mv(3, 5); rm(3, 4); process(); } @Test public void test2() { setup(5); mv(1, 3); rm(1, 1); process(); } @Test public void test3() { setup(5); mv(0, 4); rm(2, 1); process(); } @Test public void test4() { setup(5); mv(3, 0); rm(3, 1); process(); } @Test public void test5() { setup(10); mv(8, 1); rm(6, 3); process(); } @Test public void test6() { setup(5); mv(1, 3); rm(0, 3); process(); } @Test public void test7() { setup(5); mv(3, 4); rm(3, 1); process(); } @Test public void test8() { setup(5); mv(4, 3); rm(3, 1); process(); } @Test public void test9() { setup(5); mv(2, 0); rm(2, 2); process(); } @Test public void testRandom() throws Exception { for (int i = 0; i < 150; i++) { try { cleanState(); setup(50); for (int j = 0; j < 50; j++) { randOp(nextInt(random, nextInt(random, 4))); } Log.d(TAG, "running random test " + i); process(); } catch (Throwable t) { throw new Exception(t.getMessage() + "\n" + opsToString(mUpdateOps)); } } } @Test public void testRandomMoveRemove() throws Exception { for (int i = 0; i < 1000; i++) { try { cleanState(); setup(5); orderedRandom(MOVE, REMOVE); process(); } catch (Throwable t) { throw new Exception(t.getMessage() + "\n" + opsToString(mUpdateOps)); } } } @Test public void testRandomMoveAdd() throws Exception { for (int i = 0; i < 1000; i++) { try { cleanState(); setup(5); orderedRandom(MOVE, ADD); process(); } catch (Throwable t) { throw new Exception(t.getMessage() + "\n" + opsToString(mUpdateOps)); } } } @Test public void testRandomMoveUpdate() throws Exception { for (int i = 0; i < 1000; i++) { try { cleanState(); setup(5); orderedRandom(MOVE, UPDATE); process(); } catch (Throwable t) { throw new Exception(t.getMessage() + "\n" + opsToString(mUpdateOps)); } } } private String opsToString(List updateOps) { StringBuilder sb = new StringBuilder(); for (UpdateOp op : updateOps) { sb.append("\n").append(op.toString()); } return sb.append("\n").toString(); } public void orderedRandom(int... ops) { for (int op : ops) { randOp(op); } } void randOp(int cmd) { switch (cmd) { case REMOVE: if (updatedItemCount > 1) { int s = nextInt(random, updatedItemCount - 1); int len = Math.max(1, nextInt(random, updatedItemCount - s)); rm(s, len); } break; case ADD: int s = updatedItemCount == 0 ? 0 : nextInt(random, updatedItemCount); add(s, nextInt(random, 50)); break; case MOVE: if (updatedItemCount >= 2) { int from = nextInt(random, updatedItemCount); int to; do { to = nextInt(random, updatedItemCount); } while (to == from); mv(from, to); } break; case UPDATE: if (updatedItemCount > 1) { s = nextInt(random, updatedItemCount - 1); int len = Math.max(1, nextInt(random, updatedItemCount - s)); up(s, len); } break; } } int nextInt(Random random, int n) { if (n == 0) { return 0; } return random.nextInt(n); } UpdateOp rm(int start, int count) { updatedItemCount -= count; return record(new UpdateOp(REMOVE, start, count, null)); } UpdateOp mv(int from, int to) { return record(new UpdateOp(MOVE, from, to, null)); } UpdateOp add(int start, int count) { updatedItemCount += count; return record(new UpdateOp(ADD, start, count, null)); } UpdateOp up(int start, int count) { return record(new UpdateOp(UPDATE, start, count, null)); } UpdateOp record(UpdateOp op) { mUpdateOps.add(op); return op; } void process() { List items = new ArrayList(itemCount); for (int i = 0; i < itemCount; i++) { items.add(Item.create()); } List clones = new ArrayList(itemCount); for (int i = 0; i < itemCount; i++) { clones.add(Item.clone(items.get(i))); } List rewritten = rewriteOps(mUpdateOps); assertAllMovesAtTheEnd(rewritten); apply(items, mUpdateOps); List originalAdded = mAddedItems; List originalRemoved = mRemovedItems; if (originalAdded.size() > 0) { Item.idCounter = originalAdded.get(0).id; } mAddedItems = new ArrayList(); mRemovedItems = new ArrayList(); apply(clones, rewritten); // now check equality assertListsIdentical(items, clones); assertHasTheSameItems(originalAdded, mAddedItems); assertHasTheSameItems(originalRemoved, mRemovedItems); assertRecycledOpsAreNotReused(items); assertRecycledOpsAreNotReused(clones); } private void assertRecycledOpsAreNotReused(List items) { for (Item item : items) { assertFalse(mRecycledOps.contains(item)); } } private void assertAllMovesAtTheEnd(List ops) { boolean foundMove = false; for (UpdateOp op : ops) { if (op.cmd == MOVE) { foundMove = true; } else { assertFalse(foundMove); } } } private void assertHasTheSameItems(List items, List clones) { String log = "has the same items\n" + toString(items) + "--\n" + toString(clones); assertEquals(log, items.size(), clones.size()); for (Item item : items) { for (Item clone : clones) { if (item.id == clone.id && item.version == clone.version) { clones.remove(clone); break; } } } assertEquals(log, 0, clones.size()); } private void assertListsIdentical(List items, List clones) { String log = "is identical\n" + toString(items) + "--\n" + toString(clones); assertEquals(items.size(), clones.size()); for (int i = 0; i < items.size(); i++) { Item.assertIdentical(log, items.get(i), clones.get(i)); } } private void apply(List items, List updateOps) { for (UpdateOp op : updateOps) { switch (op.cmd) { case UpdateOp.ADD: for (int i = 0; i < op.itemCount; i++) { final Item newItem = Item.create(); mAddedItems.add(newItem); items.add(op.positionStart + i, newItem); } break; case UpdateOp.REMOVE: for (int i = 0; i < op.itemCount; i++) { mRemovedItems.add(items.remove(op.positionStart)); } break; case UpdateOp.MOVE: items.add(op.itemCount, items.remove(op.positionStart)); break; case UpdateOp.UPDATE: for (int i = 0; i < op.itemCount; i++) { final int index = op.positionStart + i; items.get(index).version = items.get(index).version + 1; } break; } } } private List rewriteOps(List updateOps) { List copy = new ArrayList(); for (UpdateOp op : updateOps) { copy.add(new UpdateOp(op.cmd, op.positionStart, op.itemCount, null)); } mOpReorderer.reorderOps(copy); return copy; } @Test public void testSwapMoveRemove_1() { mv(10, 15); rm(2, 3); swapMoveRemove(mUpdateOps, 0); assertEquals(2, mUpdateOps.size()); assertEquals(mv(7, 12), mUpdateOps.get(1)); assertEquals(rm(2, 3), mUpdateOps.get(0)); } @Test public void testSwapMoveRemove_2() { mv(3, 8); rm(4, 2); swapMoveRemove(mUpdateOps, 0); assertEquals(2, mUpdateOps.size()); assertEquals(rm(5, 2), mUpdateOps.get(0)); assertEquals(mv(3, 6), mUpdateOps.get(1)); } @Test public void testSwapMoveRemove_3() { mv(3, 8); rm(3, 2); swapMoveRemove(mUpdateOps, 0); assertEquals(2, mUpdateOps.size()); assertEquals(rm(4, 2), mUpdateOps.get(0)); assertEquals(mv(3, 6), mUpdateOps.get(1)); } @Test public void testSwapMoveRemove_4() { mv(3, 8); rm(2, 3); swapMoveRemove(mUpdateOps, 0); assertEquals(3, mUpdateOps.size()); assertEquals(rm(4, 2), mUpdateOps.get(0)); assertEquals(rm(2, 1), mUpdateOps.get(1)); assertEquals(mv(2, 5), mUpdateOps.get(2)); } @Test public void testSwapMoveRemove_5() { mv(3, 0); rm(2, 3); swapMoveRemove(mUpdateOps, 0); assertEquals(3, mUpdateOps.size()); assertEquals(rm(4, 1), mUpdateOps.get(0)); assertEquals(rm(1, 2), mUpdateOps.get(1)); assertEquals(mv(1, 0), mUpdateOps.get(2)); } @Test public void testSwapMoveRemove_6() { mv(3, 10); rm(2, 3); swapMoveRemove(mUpdateOps, 0); assertEquals(3, mUpdateOps.size()); assertEquals(rm(4, 2), mUpdateOps.get(0)); assertEquals(rm(2, 1), mUpdateOps.get(1)); } @Test public void testSwapMoveRemove_7() { mv(3, 2); rm(6, 2); swapMoveRemove(mUpdateOps, 0); assertEquals(2, mUpdateOps.size()); assertEquals(rm(6, 2), mUpdateOps.get(0)); assertEquals(mv(3, 2), mUpdateOps.get(1)); } @Test public void testSwapMoveRemove_8() { mv(3, 4); rm(3, 1); swapMoveRemove(mUpdateOps, 0); assertEquals(1, mUpdateOps.size()); assertEquals(rm(4, 1), mUpdateOps.get(0)); } @Test public void testSwapMoveRemove_9() { mv(3, 4); rm(4, 1); swapMoveRemove(mUpdateOps, 0); assertEquals(1, mUpdateOps.size()); assertEquals(rm(3, 1), mUpdateOps.get(0)); } @Test public void testSwapMoveRemove_10() { mv(1, 3); rm(0, 3); swapMoveRemove(mUpdateOps, 0); assertEquals(2, mUpdateOps.size()); assertEquals(rm(2, 2), mUpdateOps.get(0)); assertEquals(rm(0, 1), mUpdateOps.get(1)); } @Test public void testSwapMoveRemove_11() { mv(3, 8); rm(7, 3); swapMoveRemove(mUpdateOps, 0); assertEquals(2, mUpdateOps.size()); assertEquals(rm(3, 1), mUpdateOps.get(0)); assertEquals(rm(7, 2), mUpdateOps.get(1)); } @Test public void testSwapMoveRemove_12() { mv(1, 3); rm(2, 1); swapMoveRemove(mUpdateOps, 0); assertEquals(2, mUpdateOps.size()); assertEquals(rm(3, 1), mUpdateOps.get(0)); assertEquals(mv(1, 2), mUpdateOps.get(1)); } @Test public void testSwapMoveRemove_13() { mv(1, 3); rm(1, 2); swapMoveRemove(mUpdateOps, 0); assertEquals(1, mUpdateOps.size()); assertEquals(rm(2, 2), mUpdateOps.get(1)); } @Test public void testSwapMoveRemove_14() { mv(4, 2); rm(3, 1); swapMoveRemove(mUpdateOps, 0); assertEquals(2, mUpdateOps.size()); assertEquals(rm(2, 1), mUpdateOps.get(0)); assertEquals(mv(2, 3), mUpdateOps.get(1)); } @Test public void testSwapMoveRemove_15() { mv(4, 2); rm(3, 2); swapMoveRemove(mUpdateOps, 0); assertEquals(1, mUpdateOps.size()); assertEquals(rm(2, 2), mUpdateOps.get(0)); } @Test public void testSwapMoveRemove_16() { mv(2, 3); rm(1, 2); swapMoveRemove(mUpdateOps, 0); assertEquals(2, mUpdateOps.size()); assertEquals(rm(3, 1), mUpdateOps.get(0)); assertEquals(rm(1, 1), mUpdateOps.get(1)); } @Test public void testSwapMoveUpdate_0() { mv(1, 3); up(1, 2); swapMoveUpdate(mUpdateOps, 0); assertEquals(2, mUpdateOps.size()); assertEquals(up(2, 2), mUpdateOps.get(0)); assertEquals(mv(1, 3), mUpdateOps.get(1)); } @Test public void testSwapMoveUpdate_1() { mv(0, 2); up(0, 4); swapMoveUpdate(mUpdateOps, 0); assertEquals(3, mUpdateOps.size()); assertEquals(up(0, 1), mUpdateOps.get(0)); assertEquals(up(1, 3), mUpdateOps.get(1)); assertEquals(mv(0, 2), mUpdateOps.get(2)); } @Test public void testSwapMoveUpdate_2() { mv(2, 0); up(1, 3); swapMoveUpdate(mUpdateOps, 0); assertEquals(3, mUpdateOps.size()); assertEquals(up(3, 1), mUpdateOps.get(0)); assertEquals(up(0, 2), mUpdateOps.get(1)); assertEquals(mv(2, 0), mUpdateOps.get(2)); } private void swapMoveUpdate(List list, int move) { mOpReorderer.swapMoveUpdate(list, move, list.get(move), move + 1, list.get(move + 1)); } private void swapMoveRemove(List list, int move) { mOpReorderer.swapMoveRemove(list, move, list.get(move), move + 1, list.get(move + 1)); } private String toString(List items) { StringBuilder sb = new StringBuilder(); for (Item item : items) { sb.append(item.toString()).append("\n"); } return sb.toString(); } static class Item { static int idCounter = 0; int id; int version; Item(int id, int version) { this.id = id; this.version = version; } static Item create() { return new Item(idCounter++, 1); } static Item clone(Item other) { return new Item(other.id, other.version); } public static void assertIdentical(String logPrefix, Item item1, Item item2) { assertEquals(logPrefix + "\n" + item1 + " vs " + item2, item1.id, item2.id); assertEquals(logPrefix + "\n" + item1 + " vs " + item2, item1.version, item2.version); } @Override public String toString() { return "Item{" + "id=" + id + ", version=" + version + '}'; } } }