package com.airbnb.epoxy;
import com.google.common.collect.Collections2;
import junit.framework.Assert;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import static com.airbnb.epoxy.ModelTestUtils.addModels;
import static com.airbnb.epoxy.ModelTestUtils.changeValues;
import static com.airbnb.epoxy.ModelTestUtils.convertToTestModels;
import static com.airbnb.epoxy.ModelTestUtils.remove;
import static com.airbnb.epoxy.ModelTestUtils.removeModelsAfterPosition;
import static junit.framework.Assert.assertEquals;
@Config(sdk = 21, manifest = TestRunner.MANIFEST_PATH)
@RunWith(TestRunner.class)
public class DifferCorrectnessTest {
private static final boolean SHOW_LOGS = false;
/**
* If true, will log the time taken on the diff and skip the validation since that takes a long
* time for big change sets.
*/
private static final boolean SPEED_RUN = false;
private final TestObserver testObserver = new TestObserver(SHOW_LOGS);
private final TestAdapter testAdapter = new TestAdapter();
private final List<EpoxyModel<?>> models = testAdapter.models;
private static long totalDiffMillis = 0;
private static long totalDiffOperations = 0;
private static long totalDiffs = 0;
@BeforeClass
public static void beforeClass() {
totalDiffMillis = 0;
totalDiffOperations = 0;
totalDiffs = 0;
}
@AfterClass
public static void afterClass() {
if (SPEED_RUN) {
System.out.println("Total time for all diffs (ms): " + totalDiffMillis);
} else {
System.out.println("Total operations for diffs: " + totalDiffOperations);
double avgOperations = ((double) totalDiffOperations / totalDiffs);
System.out.println("Average operations per diff: " + avgOperations);
}
}
@Before
public void setUp() {
if (!SPEED_RUN) {
testAdapter.registerAdapterDataObserver(testObserver);
}
}
@Test
public void noChange() {
diffAndValidateWithOpCount(0);
}
@Test
public void simpleUpdate() {
addModels(models);
diffAndValidate();
changeValues(models);
diffAndValidateWithOpCount(1);
}
@Test
public void updateStart() {
addModels(models);
diffAndValidate();
changeValues(models, 0, models.size() / 2);
diffAndValidateWithOpCount(1);
}
@Test
public void updateMiddle() {
addModels(models);
diffAndValidate();
changeValues(models, models.size() / 3, models.size() * 2 / 3);
diffAndValidateWithOpCount(1);
}
@Test
public void updateEnd() {
addModels(models);
diffAndValidate();
changeValues(models, models.size() / 2, models.size());
diffAndValidateWithOpCount(1);
}
@Test
public void shuffle() {
// Tries all permutations of item shuffles, with various list sizes. Also randomizes
// item values so that the diff must deal with both item updates and movements
for (int i = 0; i < 9; i++) {
List<EpoxyModel<?>> originalModels = new ArrayList<>();
addModels(i, originalModels);
int permutationNumber = 0;
for (List<EpoxyModel<?>> permutedModels : Collections2.permutations(originalModels)) {
permutationNumber++;
// Resetting to the original models each time, otherwise each subsequent permutation is
// only a small difference
models.clear();
models.addAll(originalModels);
diffAndValidate();
models.clear();
models.addAll(permutedModels);
changeValues(models);
log("\n\n***** Permutation " + permutationNumber + " - List Size: " + i + " ****** \n");
log("old models:\n" + models);
log("\n");
log("new models:\n" + models);
log("\n");
diffAndValidate();
}
}
}
@Test
public void swapEnds() {
addModels(models);
diffAndValidate();
EpoxyModel<?> firstModel = models.remove(0);
EpoxyModel<?> lastModel = models.remove(models.size() - 1);
models.add(0, lastModel);
models.add(firstModel);
diffAndValidateWithOpCount(2);
}
@Test
public void moveFrontToEnd() {
addModels(models);
diffAndValidate();
EpoxyModel<?> firstModel = models.remove(0);
models.add(firstModel);
diffAndValidateWithOpCount(1);
}
@Test
public void moveEndToFront() {
addModels(models);
diffAndValidate();
EpoxyModel<?> lastModel = models.remove(models.size() - 1);
models.add(0, lastModel);
diffAndValidateWithOpCount(1);
}
@Test
public void moveEndToFrontAndChangeValues() {
addModels(models);
diffAndValidate();
EpoxyModel<?> lastModel = models.remove(models.size() - 1);
models.add(0, lastModel);
changeValues(models);
diffAndValidateWithOpCount(2);
}
@Test
public void swapHalf() {
addModels(models);
diffAndValidate();
List<EpoxyModel<?>> firstHalf = models.subList(0, models.size() / 2);
ArrayList<EpoxyModel<?>> firstHalfCopy = new ArrayList<>(firstHalf);
firstHalf.clear();
models.addAll(firstHalfCopy);
diffAndValidateWithOpCount(firstHalfCopy.size());
}
@Test
public void reverse() {
addModels(models);
diffAndValidate();
Collections.reverse(models);
diffAndValidate();
}
@Test
public void removeAll() {
addModels(models);
diffAndValidate();
models.clear();
diffAndValidateWithOpCount(1);
}
@Test
public void removeEnd() {
addModels(models);
diffAndValidate();
int half = models.size() / 2;
ModelTestUtils.remove(models, half, half);
diffAndValidateWithOpCount(1);
}
@Test
public void removeMiddle() {
addModels(models);
diffAndValidate();
int third = models.size() / 3;
ModelTestUtils.remove(models, third, third);
diffAndValidateWithOpCount(1);
}
@Test
public void removeStart() {
addModels(models);
diffAndValidate();
int half = models.size() / 2;
ModelTestUtils.remove(models, 0, half);
diffAndValidateWithOpCount(1);
}
@Test
public void multipleRemovals() {
addModels(models);
diffAndValidate();
int size = models.size();
int tenth = size / 10;
// Remove a tenth of the models at the end, middle, and start
ModelTestUtils.removeModelsAfterPosition(models, size - tenth);
ModelTestUtils.remove(models, size / 2, tenth);
ModelTestUtils.remove(models, 0, tenth);
diffAndValidateWithOpCount(3);
}
@Test
public void simpleAdd() {
addModels(models);
diffAndValidateWithOpCount(1);
}
@Test
public void addToStart() {
addModels(models);
diffAndValidate();
addModels(models, 0);
diffAndValidateWithOpCount(1);
}
@Test
public void addToMiddle() {
addModels(models);
diffAndValidate();
addModels(models, models.size() / 2);
diffAndValidateWithOpCount(1);
}
@Test
public void addToEnd() {
addModels(models);
diffAndValidate();
addModels(models);
diffAndValidateWithOpCount(1);
}
@Test
public void multipleInsertions() {
addModels(models);
diffAndValidate();
addModels(models, 0);
addModels(models, models.size() * 2 / 3);
addModels(models);
diffAndValidateWithOpCount(3);
}
@Test
public void moveTwoInFrontOfInsertion() {
addModels(4, models);
diffAndValidate();
addModels(1, models, 0);
EpoxyModel<?> lastModel = models.remove(models.size() - 1);
models.add(0, lastModel);
lastModel = models.remove(models.size() - 1);
models.add(0, lastModel);
diffAndValidate();
}
@Test
public void randomCombinations() {
int maxBatchSize = 3;
int maxModelCount = 10;
int maxSeed = 100000;
// This modifies the models list in a random way many times, with different size lists.
for (int modelCount = 1; modelCount < maxModelCount; modelCount++) {
for (int randomSeed = 0; randomSeed < maxSeed; randomSeed++) {
log("\n\n*** Combination seed " + randomSeed + " Model Count: " + modelCount + " *** \n");
// We keep the list from the previous loop and keep modifying it. This allows us to test
// that state is maintained properly between diffs. We just make sure the list size
// says the same by adding or removing if necessary
int currentModelCount = models.size();
if (currentModelCount < modelCount) {
addModels(modelCount - currentModelCount, models);
} else if (currentModelCount > modelCount) {
removeModelsAfterPosition(models, modelCount);
}
diffAndValidate();
modifyModelsRandomly(models, maxBatchSize, new Random(randomSeed));
log("\nResulting diff: \n");
diffAndValidate();
}
}
}
private void modifyModelsRandomly(List<EpoxyModel<?>> models, int maxBatchSize, Random random) {
for (int i = 0; i < models.size(); i++) {
int batchSize = randInt(1, maxBatchSize, random);
switch (random.nextInt(4)) {
case 0:
// insert
log("Inserting " + batchSize + " at " + i);
addModels(batchSize, models, i);
i += batchSize;
break;
case 1:
// remove
int numAvailableToRemove = models.size() - i;
batchSize = numAvailableToRemove < batchSize ? numAvailableToRemove : batchSize;
log("Removing " + batchSize + " at " + i);
remove(models, i, batchSize);
break;
case 2:
// change
int numAvailableToChange = models.size() - i;
batchSize = numAvailableToChange < batchSize ? numAvailableToChange : batchSize;
log("Changing " + batchSize + " at " + i);
changeValues(models, i, batchSize);
break;
case 3:
// move
int targetPosition = random.nextInt(models.size());
EpoxyModel<?> currentItem = models.remove(i);
models.add(targetPosition, currentItem);
log("Moving " + i + " to " + targetPosition);
break;
default:
throw new IllegalStateException("unhandled)");
}
}
}
private void diffAndValidate() {
diffAndValidateWithOpCount(-1);
}
private void diffAndValidateWithOpCount(int expectedOperationCount) {
testObserver.operationCount = 0;
long start = System.currentTimeMillis();
testAdapter.notifyModelsChanged();
long end = System.currentTimeMillis();
totalDiffMillis += (end - start);
totalDiffOperations += testObserver.operationCount;
totalDiffs++;
if (!SPEED_RUN) {
if (expectedOperationCount != -1) {
assertEquals("Operation count is incorrect", expectedOperationCount,
testObserver.operationCount);
}
List<TestModel> newModels = convertToTestModels(models);
checkDiff(testObserver.initialModels, testObserver.modelsAfterDiffing, newModels);
testObserver.setUpForNextDiff(newModels);
}
}
private static int randInt(int min, int max, Random rand) {
// nextInt is normally exclusive of the top value,
// so add 1 to make it inclusive
return rand.nextInt((max - min) + 1) + min;
}
private void log(String text) {
log(text, false);
}
private void log(String text, boolean forceShow) {
if (forceShow || SHOW_LOGS) {
System.out.println(text);
}
}
private void checkDiff(List<TestModel> modelsBeforeDiff, List<TestModel> modelsAfterDiff,
List<TestModel> actualModels) {
assertEquals("Diff produces list of different size.", actualModels.size(),
modelsAfterDiff.size());
for (int i = 0; i < modelsAfterDiff.size(); i++) {
TestModel model = modelsAfterDiff.get(i);
final TestModel expected = actualModels.get(i);
if (model == InsertedModel.INSTANCE) {
// If the item at this index is new then it shouldn't exist in the original list
for (TestModel oldModel : modelsBeforeDiff) {
Assert.assertNotSame("The inserted model should not exist in the original list",
oldModel.id(), expected.id());
}
} else {
assertEquals("Models at same index should have same id", expected.id(), model.id());
if (model.updated) {
// If there was a change operation then the item hashcodes should be different
Assert
.assertNotSame("Incorrectly updated an item.", model.hashCode(), expected.hashCode());
} else {
assertEquals("Models should have same hashcode when not updated",
expected.hashCode(), model.hashCode());
}
// Clear state so the model can be used again in another diff
model.updated = false;
}
}
}
}