blob: dca6a56a0b7cf6e3e60d38c342c88ae6da00ea77 [file] [log] [blame]
/*
* Copyright (C) 2017 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 com.android.tools.idea.observable;
import com.google.common.base.Objects;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.TestCase.fail;
public final class BatchInvokerTest {
@Test
public void invokingImmediatelyWorks() throws Exception {
BatchInvoker invoker = new BatchInvoker(BatchInvoker.INVOKE_IMMEDIATELY_STRATEGY);
IntWrapper intWrapper = new IntWrapper();
AddToValue addToValue = new AddToValue(0, intWrapper, 10);
assertThat(intWrapper.value).isEqualTo(0);
invoker.enqueue(addToValue);
assertThat(intWrapper.value).isEqualTo(10);
invoker.enqueue(addToValue);
assertThat(intWrapper.value).isEqualTo(20);
}
@Test
public void batchedInvokingWorks() throws Exception {
TestInvokeStrategy testStrategy = new TestInvokeStrategy();
BatchInvoker invoker = new BatchInvoker(testStrategy);
IntWrapper intWrapper = new IntWrapper();
AddToValue addToValue10 = new AddToValue(0, intWrapper, 10);
AddToValue addToValue100 = new AddToValue(1, intWrapper, 100);
assertThat(intWrapper.value).isEqualTo(0);
invoker.enqueue(addToValue10);
invoker.enqueue(addToValue100);
assertThat(intWrapper.value).isEqualTo(0);
testStrategy.updateOneStep();
assertThat(intWrapper.value).isEqualTo(110);
}
@Test
public void allDeferredInvocationsRunAtOnce() throws Exception {
TestInvokeStrategy testStrategy = new TestInvokeStrategy();
BatchInvoker invoker = new BatchInvoker(testStrategy);
IntWrapper intWrapper = new IntWrapper();
AddToValue addToValue1 = new AddToValue(0, intWrapper, 1);
AddToValue addToValue10 = new AddToValue(1, intWrapper, 10);
DeferRunnable deferRunnable = new DeferRunnable(invoker);
deferRunnable.setRunnable(addToValue10);
invoker.enqueue(addToValue1);
invoker.enqueue(deferRunnable);
testStrategy.updateOneStep();
assertThat(intWrapper.value).isEqualTo(11);
}
@Test
public void batchedInvokingDropsRedundantUpdates() throws Exception {
TestInvokeStrategy testStrategy = new TestInvokeStrategy();
BatchInvoker invoker = new BatchInvoker(testStrategy);
IntWrapper intWrapper = new IntWrapper();
// For add events with the same ID, all but the first will be dropped
AddToValue addToValue1 = new AddToValue(0, intWrapper, 1);
AddToValue addToValue10 = new AddToValue(0, intWrapper, 10);
AddToValue addToValue100 = new AddToValue(0, intWrapper, 100);
AddToValue addToValue2 = new AddToValue(1, intWrapper, 2);
AddToValue addToValue20 = new AddToValue(1, intWrapper, 20);
AddToValue addToValue200 = new AddToValue(1, intWrapper, 200);
invoker.enqueue(addToValue1);
invoker.enqueue(addToValue10); // dropped
invoker.enqueue(addToValue100); // dropped
invoker.enqueue(addToValue2);
invoker.enqueue(addToValue20); // dropped
invoker.enqueue(addToValue200); // dropped
testStrategy.updateOneStep();
assertThat(intWrapper.value).isEqualTo(3);
}
@Test
public void infiniteCycleThrowsException() throws Exception {
BatchInvoker invoker = new BatchInvoker(BatchInvoker.INVOKE_IMMEDIATELY_STRATEGY);
DeferRunnable runnableA = new DeferRunnable(invoker);
DeferRunnable runnableB = new DeferRunnable(invoker);
runnableA.setRunnable(runnableB);
runnableB.setRunnable(runnableA);
try {
invoker.enqueue(runnableA); // A -> B -> A -> B -> ...
fail();
}
catch (BatchInvoker.InfiniteCycleException ignored) {
}
// Ensure invoker continues to work after throwing an exception
IntWrapper intWrapper = new IntWrapper();
invoker.enqueue(new AddToValue(0, intWrapper, 123));
assertThat(intWrapper.value).isEqualTo(123);
}
@Test
public void batchedInvokingRecoversFromException() throws Exception {
AtomicBoolean invokeResult = new AtomicBoolean();
BatchInvoker invoker = new BatchInvoker(BatchInvoker.INVOKE_IMMEDIATELY_STRATEGY);
assertThat(invokeResult.get()).isFalse();
invoker.enqueue(() -> invokeResult.set(true));
assertThat(invokeResult.get()).isTrue();
try {
invoker.enqueue(() -> { // Simulate a task that throws an exception
throw new RuntimeException("Some Exception");
});
}
catch (RuntimeException ignored) {
}
// After an exception, the invoker should not get jammed
invoker.enqueue(() -> invokeResult.set(false));
assertThat(invokeResult.get()).isFalse();
}
private static final class IntWrapper {
int value;
}
/**
* Simple runnable with ID (and runnables with the same ID should be collapsed)
*/
private static final class AddToValue implements Runnable {
@NotNull private final IntWrapper myTarget;
private final int myId;
private final int myAmount;
public AddToValue(int id, @NotNull IntWrapper target, int amount) {
myTarget = target;
myId = id;
myAmount = amount;
}
@Override
public void run() {
myTarget.value += myAmount;
}
/**
* Equality purely based on ID, not anything else. This allows multiple AddToValue runnables
* with the same ID to collapse.
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AddToValue that = (AddToValue)o;
return myId == that.myId;
}
@Override
public int hashCode() {
return Objects.hashCode(myId);
}
}
/**
* A runnable which assigns another runnable to a target invoker. This will let us unit test
* deferred behavior and infinite loop scenarios.
*/
private static final class DeferRunnable implements Runnable {
@NotNull private final BatchInvoker myOwningInvoker;
@Nullable private Runnable myOther;
public DeferRunnable(@NotNull BatchInvoker owningInvoker) {
myOwningInvoker = owningInvoker;
}
public void setRunnable(@NotNull Runnable other) {
myOther = other;
}
@Override
public void run() {
assert myOther != null;
myOwningInvoker.enqueue(myOther);
}
}
}