| /* |
| * 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); |
| } |
| } |
| } |