/* * 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.util; import junit.framework.TestCase; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import android.test.suitebuilder.annotation.SmallTest; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Random; @RunWith(JUnit4.class) @SmallTest public class SortedListTest extends TestCase { SortedList mList; List mAdditions = new ArrayList(); List mRemovals = new ArrayList(); List mMoves = new ArrayList(); List mUpdates = new ArrayList(); private SortedList.Callback mCallback; InsertedCallback mInsertedCallback; ChangedCallback mChangedCallback; private Comparator sItemComparator = new Comparator() { @Override public int compare(Item o1, Item o2) { return mCallback.compare(o1, o2); } }; private abstract class InsertedCallback { public abstract void onInserted(int position, int count); } private abstract class ChangedCallback { public abstract void onChanged(int position, int count); } @Override @Before public void setUp() throws Exception { super.setUp(); mCallback = new SortedList.Callback() { @Override public int compare(Item o1, Item o2) { return o1.cmpField < o2.cmpField ? -1 : (o1.cmpField == o2.cmpField ? 0 : 1); } @Override public void onInserted(int position, int count) { mAdditions.add(new Pair(position, count)); if (mInsertedCallback != null) { mInsertedCallback.onInserted(position, count); } } @Override public void onRemoved(int position, int count) { mRemovals.add(new Pair(position, count)); } @Override public void onMoved(int fromPosition, int toPosition) { mMoves.add(new Pair(fromPosition, toPosition)); } @Override public void onChanged(int position, int count) { mUpdates.add(new Pair(position, count)); if (mChangedCallback != null) { mChangedCallback.onChanged(position, count); } } @Override public boolean areContentsTheSame(Item oldItem, Item newItem) { return oldItem.cmpField == newItem.cmpField && oldItem.data == newItem.data; } @Override public boolean areItemsTheSame(Item item1, Item item2) { return item1.id == item2.id; } }; mInsertedCallback = null; mChangedCallback = null; mList = new SortedList(Item.class, mCallback); } @Test public void testEmpty() { assertEquals("empty", mList.size(), 0); } @Test public void testAdd() { Item item = new Item(); assertEquals(insert(item), 0); assertEquals(size(), 1); assertTrue(mAdditions.contains(new Pair(0, 1))); Item item2 = new Item(); item2.cmpField = item.cmpField + 1; assertEquals(insert(item2), 1); assertEquals(size(), 2); assertTrue(mAdditions.contains(new Pair(1, 1))); Item item3 = new Item(); item3.cmpField = item.cmpField - 1; mAdditions.clear(); assertEquals(insert(item3), 0); assertEquals(size(), 3); assertTrue(mAdditions.contains(new Pair(0, 1))); } @Test public void testAddDuplicate() { Item item = new Item(); Item item2 = new Item(item.id, item.cmpField); item2.data = item.data; insert(item); assertEquals(0, insert(item2)); assertEquals(1, size()); assertEquals(1, mAdditions.size()); assertEquals(0, mUpdates.size()); } @Test public void testRemove() { Item item = new Item(); assertFalse(remove(item)); assertEquals(0, mRemovals.size()); insert(item); assertTrue(remove(item)); assertEquals(1, mRemovals.size()); assertTrue(mRemovals.contains(new Pair(0, 1))); assertEquals(0, size()); assertFalse(remove(item)); assertEquals(1, mRemovals.size()); } @Test public void testRemove2() { Item item = new Item(); Item item2 = new Item(item.cmpField); insert(item); assertFalse(remove(item2)); assertEquals(0, mRemovals.size()); } @Test public void clearTest() { insert(new Item(1)); insert(new Item(2)); assertEquals(2, mList.size()); mList.clear(); assertEquals(0, mList.size()); insert(new Item(3)); assertEquals(1, mList.size()); } @Test public void testBatch() { mList.beginBatchedUpdates(); for (int i = 0; i < 5; i++) { mList.add(new Item(i)); } assertEquals(0, mAdditions.size()); mList.endBatchedUpdates(); assertTrue(mAdditions.contains(new Pair(0, 5))); } @Test public void testRandom() throws Throwable { Random random = new Random(System.nanoTime()); List copy = new ArrayList(); StringBuilder log = new StringBuilder(); try { for (int i = 0; i < 10000; i++) { switch (random.nextInt(3)) { case 0://ADD Item item = new Item(); copy.add(item); insert(item); log.append("add ").append(item).append("\n"); break; case 1://REMOVE if (copy.size() > 0) { int index = random.nextInt(mList.size()); item = mList.get(index); log.append("remove ").append(item).append("\n"); assertTrue(copy.remove(item)); assertTrue(mList.remove(item)); } break; case 2://UPDATE if (copy.size() > 0) { int index = random.nextInt(mList.size()); item = mList.get(index); // TODO this cannot work Item newItem = new Item(item.id, item.cmpField); log.append("update ").append(item).append(" to ").append(newItem) .append("\n"); while (newItem.data == item.data) { newItem.data = random.nextInt(1000); } int itemIndex = mList.add(newItem); copy.remove(item); copy.add(newItem); assertSame(mList.get(itemIndex), newItem); assertNotSame(mList.get(index), item); } break; case 3:// UPDATE AT if (copy.size() > 0) { int index = random.nextInt(mList.size()); item = mList.get(index); Item newItem = new Item(item.id, random.nextInt()); mList.updateItemAt(index, newItem); copy.remove(item); copy.add(newItem); } } int lastCmp = Integer.MIN_VALUE; for (int index = 0; index < copy.size(); index++) { assertFalse(mList.indexOf(copy.get(index)) == SortedList.INVALID_POSITION); assertTrue(mList.get(index).cmpField >= lastCmp); lastCmp = mList.get(index).cmpField; assertTrue(copy.contains(mList.get(index))); } for (int index = 0; index < mList.size(); index++) { assertNotNull(mList.mData[index]); } for (int index = mList.size(); index < mList.mData.length; index++) { assertNull(mList.mData[index]); } } } catch (Throwable t) { Collections.sort(copy, sItemComparator); log.append("Items:\n"); for (Item item : copy) { log.append(item).append("\n"); } log.append("SortedList:\n"); for (int i = 0; i < mList.size(); i++) { log.append(mList.get(i)).append("\n"); } throw new Throwable(" \nlog:\n" + log.toString(), t); } } private static Item[] createItems(int idFrom, int idTo, int idStep) { final int count = (idTo - idFrom) / idStep + 1; Item[] items = new Item[count]; int id = idFrom; for (int i = 0; i < count; i++) { Item item = new Item(id, id); item.data = id; items[i] = item; id += idStep; } return items; } private static Item[] shuffle(Item[] items) { Random random = new Random(System.nanoTime()); final int count = items.length; for (int i = 0; i < count; i++) { int pos1 = random.nextInt(count); int pos2 = random.nextInt(count); if (pos1 != pos2) { Item temp = items[pos1]; items[pos1] = items[pos2]; items[pos2] = temp; } } return items; } private void assertIntegrity(int size, String context) { assertEquals(context + ": incorrect size", size, size()); int rangeStart = 0; for (int i = 0; i < size(); i++) { Item item = mList.get(i); assertNotNull(context + ": get returned null @" + i, item); assertEquals(context + ": incorrect indexOf result @" + i, i, mList.indexOf(item)); if (i == 0) { continue; } final int compare = mCallback.compare(mList.get(i - 1), item); assertTrue(context + ": incorrect sorting order @" + i, compare <= 0); if (compare == 0) { for (int j = rangeStart; j < i; j++) { assertFalse(context + ": duplicates found @" + j + " and " + i, mCallback.areItemsTheSame(mList.get(j), item)); } } else { rangeStart = i; } } } private void assertSequentialOrder() { for (int i = 0; i < size(); i++) { assertEquals(i, mList.get(i).cmpField); } } @Test public void testAddAllMerge() throws Throwable { mList.addAll(new Item[0]); assertIntegrity(0, "addAll, empty list, empty input"); assertEquals(0, mAdditions.size()); // Add first 5 even numbers. Test adding to an empty list. mList.addAll(createItems(0, 8, 2)); assertIntegrity(5, "addAll, empty list, non-empty input"); assertEquals(1, mAdditions.size()); assertTrue(mAdditions.contains(new Pair(0, 5))); mList.addAll(new Item[0]); assertIntegrity(5, "addAll, non-empty list, empty input"); assertEquals(1, mAdditions.size()); // Add 5 more even numbers, shuffled (test pre-sorting). mList.addAll(shuffle(createItems(10, 18, 2))); assertIntegrity(10, "addAll, shuffled input"); assertEquals(2, mAdditions.size()); assertTrue(mAdditions.contains(new Pair(5, 5))); // Add 5 more even numbers, reversed (test pre-sorting). mList.addAll(shuffle(createItems(28, 20, -2))); assertIntegrity(15, "addAll, reversed input"); assertEquals(3, mAdditions.size()); assertTrue(mAdditions.contains(new Pair(10, 5))); // Add first 10 odd numbers. // Test the merge when the new items run out first. mList.addAll(createItems(1, 19, 2)); assertIntegrity(25, "addAll, merging in the middle"); assertEquals(13, mAdditions.size()); for (int i = 1; i <= 19; i += 2) { assertTrue(mAdditions.contains(new Pair(i, 1))); } // Add 10 more odd numbers. // Test the merge when the old items run out first. mList.addAll(createItems(21, 39, 2)); assertIntegrity(35, "addAll, merging at the end"); assertEquals(18, mAdditions.size()); for (int i = 21; i <= 27; i += 2) { assertTrue(mAdditions.contains(new Pair(i, 1))); } assertTrue(mAdditions.contains(new Pair(29, 6))); // Add 5 more even numbers. mList.addAll(createItems(30, 38, 2)); assertIntegrity(40, "addAll, merging more"); assertEquals(23, mAdditions.size()); for (int i = 30; i <= 38; i += 2) { assertTrue(mAdditions.contains(new Pair(i, 1))); } assertEquals(0, mMoves.size()); assertEquals(0, mUpdates.size()); assertEquals(0, mRemovals.size()); assertSequentialOrder(); } @Test public void testAddAllUpdates() throws Throwable { // Add first 5 even numbers. Item[] evenItems = createItems(0, 8, 2); for (Item item : evenItems) { item.data = 1; } mList.addAll(evenItems); assertEquals(5, size()); assertEquals(1, mAdditions.size()); assertTrue(mAdditions.contains(new Pair(0, 5))); assertEquals(0, mUpdates.size()); Item[] sameEvenItems = createItems(0, 8, 2); for (Item item : sameEvenItems) { item.data = 1; } mList.addAll(sameEvenItems); assertEquals(1, mAdditions.size()); assertEquals(0, mUpdates.size()); Item[] newEvenItems = createItems(0, 8, 2); for (Item item : newEvenItems) { item.data = 2; } mList.addAll(newEvenItems); assertEquals(5, size()); assertEquals(1, mAdditions.size()); assertEquals(1, mUpdates.size()); assertTrue(mUpdates.contains(new Pair(0, 5))); for (int i = 0; i < 5; i++) { assertEquals(2, mList.get(i).data); } // Add all numbers from 0 to 9 Item[] sequentialItems = createItems(0, 9, 1); for (Item item : sequentialItems) { item.data = 3; } mList.addAll(sequentialItems); // Odd numbers should have been added. assertEquals(6, mAdditions.size()); for (int i = 0; i < 5; i++) { assertTrue(mAdditions.contains(new Pair(i * 2 + 1, 1))); } // All even items should have been updated. assertEquals(6, mUpdates.size()); for (int i = 0; i < 5; i++) { assertTrue(mUpdates.contains(new Pair(i * 2, 1))); } assertEquals(10, size()); // All items should have the latest data value. for (int i = 0; i < 10; i++) { assertEquals(3, mList.get(i).data); } assertEquals(0, mMoves.size()); assertEquals(0, mRemovals.size()); assertSequentialOrder(); } @Test public void testAddAllWithDuplicates() throws Throwable { final int maxCmpField = 5; final int idsPerCmpField = 10; final int maxUniqueId = maxCmpField * idsPerCmpField; final int maxGeneration = 5; Item[] items = new Item[maxUniqueId * maxGeneration]; int index = 0; for (int generation = 0; generation < maxGeneration; generation++) { int uniqueId = 0; for (int cmpField = 0; cmpField < maxCmpField; cmpField++) { for (int id = 0; id < idsPerCmpField; id++) { Item item = new Item(uniqueId++, cmpField); item.data = generation; items[index++] = item; } } } mList.addAll(items); assertIntegrity(maxUniqueId, "addAll with duplicates"); // Check that the most recent items have made it to the list. for (int i = 0; i != size(); i++) { Item item = mList.get(i); assertEquals(maxGeneration - 1, item.data); } } @Test public void testAddAllFast() throws Throwable { mList.addAll(new Item[0], true); assertIntegrity(0, "addAll(T[],boolean), empty list, with empty input"); assertEquals(0, mAdditions.size()); mList.addAll(createItems(0, 9, 1), true); assertIntegrity(10, "addAll(T[],boolean), empty list, non-empty input"); assertEquals(1, mAdditions.size()); assertTrue(mAdditions.contains(new Pair(0, 10))); mList.addAll(new Item[0], true); assertEquals(1, mAdditions.size()); assertIntegrity(10, "addAll(T[],boolean), non-empty list, empty input"); mList.addAll(createItems(10, 19, 1), true); assertEquals(2, mAdditions.size()); assertTrue(mAdditions.contains(new Pair(10, 10))); assertIntegrity(20, "addAll(T[],boolean), non-empty list, non-empty input"); } @Test public void testAddAllCollection() throws Throwable { Collection itemList = new ArrayList(); for (int i = 0; i < 5; i++) { itemList.add(new Item(i)); } mList.addAll(itemList); assertEquals(1, mAdditions.size()); assertTrue(mAdditions.contains(new Pair(0, itemList.size()))); assertIntegrity(itemList.size(), "addAll on collection"); } @Test public void testAddAllStableSort() { int id = 0; Item item = new Item(id++, 0); mList.add(item); // Create a few items with the same sort order. Item[] items = new Item[3]; for (int i = 0; i < 3; i++) { items[i] = new Item(id++, item.cmpField); assertEquals(0, mCallback.compare(item, items[i])); } mList.addAll(items); assertEquals(1 + items.length, size()); // Check that the order has been preserved. for (int i = 0; i < size(); i++) { assertEquals(i, mList.get(i).id); } } @Test public void testAddAllAccessFromCallbacks() { // Add first 5 even numbers. Item[] evenItems = createItems(0, 8, 2); for (Item item : evenItems) { item.data = 1; } mInsertedCallback = new InsertedCallback() { @Override public void onInserted(int position, int count) { assertEquals(0, position); assertEquals(5, count); for (int i = 0; i < count; i++) { assertEquals(i * 2, mList.get(i).id); } assertIntegrity(5, "onInserted(" + position + ", " + count + ")"); } }; mList.addAll(evenItems); assertEquals(1, mAdditions.size()); assertEquals(0, mUpdates.size()); // Add all numbers from 0 to 9. This should trigger 5 change and 5 insert notifications. Item[] sequentialItems = createItems(0, 9, 1); for (Item item : sequentialItems) { item.data = 2; } mChangedCallback = new ChangedCallback() { int expectedSize = 5; @Override public void onChanged(int position, int count) { assertEquals(1, count); assertEquals(position, mList.get(position).id); assertIntegrity(++expectedSize, "onChanged(" + position + ")"); } }; mInsertedCallback = new InsertedCallback() { int expectedSize = 5; @Override public void onInserted(int position, int count) { assertEquals(1, count); assertEquals(position, mList.get(position).id); assertIntegrity(++expectedSize, "onInserted(" + position + ")"); } }; mList.addAll(sequentialItems); assertEquals(6, mAdditions.size()); assertEquals(5, mUpdates.size()); } @Test public void testModificationFromCallbackThrows() { final Item extraItem = new Item(0); Item[] items = createItems(1, 5, 2); for (Item item : items) { item.data = 1; } mList.addAll(items); mInsertedCallback = new InsertedCallback() { @Override public void onInserted(int position, int count) { try { mList.add(new Item()); fail("add must throw from within a callback"); } catch (IllegalStateException e) { } try { mList.addAll(createItems(0, 0, 1)); fail("addAll must throw from within a callback"); } catch (IllegalStateException e) { } try { mList.addAll(createItems(0, 0, 1), true); fail("addAll(T[],boolean) must throw from within a callback"); } catch (IllegalStateException e) { } try { mList.remove(extraItem); fail("remove must throw from within a callback"); } catch (IllegalStateException e) { } try { mList.removeItemAt(0); fail("removeItemAt must throw from within a callback"); } catch (IllegalStateException e) { } try { mList.updateItemAt(0, extraItem); fail("updateItemAt must throw from within a callback"); } catch (IllegalStateException e) { } try { mList.recalculatePositionOfItemAt(0); fail("recalculatePositionOfItemAt must throw from within a callback"); } catch (IllegalStateException e) { } try { mList.clear(); fail("recalculatePositionOfItemAt must throw from within a callback"); } catch (IllegalStateException e) { } } }; // Make sure that the last one notification is change, so that the above callback is // not called from endBatchUpdates when the nested alls are actually OK. items = createItems(1, 5, 1); for (Item item : items) { item.data = 2; } mList.addAll(items); assertIntegrity(5, "Modification from callback"); } @Test public void testAddAllOutsideBatchedUpdates() { mList.add(new Item(1)); assertEquals(1, mAdditions.size()); mList.add(new Item(2)); assertEquals(2, mAdditions.size()); mList.addAll(new Item(3), new Item(4)); assertEquals(3, mAdditions.size()); mList.add(new Item(5)); assertEquals(4, mAdditions.size()); mList.add(new Item(6)); assertEquals(5, mAdditions.size()); } @Test public void testAddAllInsideBatchedUpdates() { mList.beginBatchedUpdates(); mList.add(new Item(1)); assertEquals(0, mAdditions.size()); mList.add(new Item(2)); assertEquals(0, mAdditions.size()); mList.addAll(new Item(3), new Item(4)); assertEquals(0, mAdditions.size()); mList.add(new Item(5)); assertEquals(0, mAdditions.size()); mList.add(new Item(6)); assertEquals(0, mAdditions.size()); mList.endBatchedUpdates(); assertEquals(1, mAdditions.size()); assertTrue(mAdditions.contains(new Pair(0, 6))); } private int size() { return mList.size(); } private int insert(Item item) { return mList.add(item); } private boolean remove(Item item) { return mList.remove(item); } static class Item { static int idCounter = 0; final int id; int cmpField; int data = (int) (Math.random() * 1000);//used for comparison public Item() { id = idCounter++; cmpField = (int) (Math.random() * 1000); } public Item(int cmpField) { id = idCounter++; this.cmpField = cmpField; } public Item(int id, int cmpField) { this.id = id; this.cmpField = cmpField; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Item item = (Item) o; if (cmpField != item.cmpField) { return false; } if (id != item.id) { return false; } return true; } @Override public int hashCode() { int result = id; result = 31 * result + cmpField; return result; } @Override public String toString() { return "Item{" + "id=" + id + ", cmpField=" + cmpField + ", data=" + data + '}'; } } private static final class Pair { final int first, second; public Pair(int first) { this.first = first; this.second = Integer.MIN_VALUE; } public Pair(int first, int second) { this.first = first; this.second = second; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Pair pair = (Pair) o; if (first != pair.first) { return false; } if (second != pair.second) { return false; } return true; } @Override public int hashCode() { int result = first; result = 31 * result + second; return result; } } }