/*
* Copyright 2016, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc.internal;
import static org.junit.Assert.assertEquals;
import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.ConnectivityState;
import java.util.LinkedList;
import java.util.concurrent.Executor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for {@link ConnectivityStateManager}.
*/
@RunWith(JUnit4.class)
public class ConnectivityStateManagerTest {
private final FakeClock executor = new FakeClock();
private final ConnectivityStateManager state =
new ConnectivityStateManager(ConnectivityState.IDLE);
private final LinkedList<ConnectivityState> sink = new LinkedList<ConnectivityState>();
@Test
public void noCallback() {
state.gotoState(ConnectivityState.CONNECTING);
assertEquals(ConnectivityState.CONNECTING, state.getState());
state.gotoState(ConnectivityState.TRANSIENT_FAILURE);
assertEquals(ConnectivityState.TRANSIENT_FAILURE, state.getState());
}
@Test
public void registerCallbackBeforeStateChanged() {
state.gotoState(ConnectivityState.CONNECTING);
assertEquals(ConnectivityState.CONNECTING, state.getState());
state.notifyWhenStateChanged(new Runnable() {
@Override
public void run() {
sink.add(state.getState());
}
}, executor.getScheduledExecutorService(), ConnectivityState.CONNECTING);
assertEquals(0, executor.numPendingTasks());
state.gotoState(ConnectivityState.TRANSIENT_FAILURE);
// Make sure the callback is run in the executor
assertEquals(0, sink.size());
assertEquals(1, executor.runDueTasks());
assertEquals(0, executor.numPendingTasks());
assertEquals(1, sink.size());
assertEquals(ConnectivityState.TRANSIENT_FAILURE, sink.poll());
}
@Test
public void registerCallbackAfterStateChanged() {
state.gotoState(ConnectivityState.CONNECTING);
assertEquals(ConnectivityState.CONNECTING, state.getState());
state.notifyWhenStateChanged(new Runnable() {
@Override
public void run() {
sink.add(state.getState());
}
}, executor.getScheduledExecutorService(), ConnectivityState.IDLE);
// Make sure the callback is run in the executor
assertEquals(0, sink.size());
assertEquals(1, executor.runDueTasks());
assertEquals(0, executor.numPendingTasks());
assertEquals(1, sink.size());
assertEquals(ConnectivityState.CONNECTING, sink.poll());
}
@Test
public void callbackOnlyCalledOnTransition() {
state.notifyWhenStateChanged(new Runnable() {
@Override
public void run() {
sink.add(state.getState());
}
}, executor.getScheduledExecutorService(), ConnectivityState.IDLE);
state.gotoState(ConnectivityState.IDLE);
assertEquals(0, executor.numPendingTasks());
assertEquals(0, sink.size());
}
@Test
public void callbacksAreOneShot() {
Runnable callback = new Runnable() {
@Override
public void run() {
sink.add(state.getState());
}
};
state.notifyWhenStateChanged(callback, executor.getScheduledExecutorService(),
ConnectivityState.IDLE);
// First transition triggers the callback
state.gotoState(ConnectivityState.CONNECTING);
assertEquals(1, executor.runDueTasks());
assertEquals(1, sink.size());
assertEquals(ConnectivityState.CONNECTING, sink.poll());
assertEquals(0, executor.numPendingTasks());
// Second transition doesn't trigger the callback
state.gotoState(ConnectivityState.TRANSIENT_FAILURE);
assertEquals(0, sink.size());
assertEquals(0, executor.numPendingTasks());
// Register another callback
state.notifyWhenStateChanged(callback, executor.getScheduledExecutorService(),
ConnectivityState.TRANSIENT_FAILURE);
state.gotoState(ConnectivityState.READY);
state.gotoState(ConnectivityState.IDLE);
// Callback loses the race with the second stage change
assertEquals(1, executor.runDueTasks());
assertEquals(1, sink.size());
// It will see the second state
assertEquals(ConnectivityState.IDLE, sink.poll());
assertEquals(0, executor.numPendingTasks());
}
@Test
public void multipleCallbacks() {
final LinkedList<String> callbackRuns = new LinkedList<String>();
state.notifyWhenStateChanged(new Runnable() {
@Override
public void run() {
sink.add(state.getState());
callbackRuns.add("callback1");
}
}, executor.getScheduledExecutorService(), ConnectivityState.IDLE);
state.notifyWhenStateChanged(new Runnable() {
@Override
public void run() {
sink.add(state.getState());
callbackRuns.add("callback2");
}
}, executor.getScheduledExecutorService(), ConnectivityState.IDLE);
state.notifyWhenStateChanged(new Runnable() {
@Override
public void run() {
sink.add(state.getState());
callbackRuns.add("callback3");
}
}, executor.getScheduledExecutorService(), ConnectivityState.READY);
// callback3 is run immediately because the source state is already different from the current
// state.
assertEquals(1, executor.runDueTasks());
assertEquals(1, callbackRuns.size());
assertEquals("callback3", callbackRuns.poll());
assertEquals(1, sink.size());
assertEquals(ConnectivityState.IDLE, sink.poll());
// Now change the state.
state.gotoState(ConnectivityState.CONNECTING);
assertEquals(2, executor.runDueTasks());
assertEquals(2, callbackRuns.size());
assertEquals("callback1", callbackRuns.poll());
assertEquals("callback2", callbackRuns.poll());
assertEquals(2, sink.size());
assertEquals(ConnectivityState.CONNECTING, sink.poll());
assertEquals(ConnectivityState.CONNECTING, sink.poll());
assertEquals(0, executor.numPendingTasks());
}
private Runnable newRecursiveCallback(final Executor executor) {
return new Runnable() {
@Override
public void run() {
sink.add(state.getState());
state.notifyWhenStateChanged(newRecursiveCallback(executor), executor, state.getState());
}
};
}
@Test
public void registerCallbackFromCallback() {
state.notifyWhenStateChanged(newRecursiveCallback(executor.getScheduledExecutorService()),
executor.getScheduledExecutorService(), state.getState());
state.gotoState(ConnectivityState.CONNECTING);
assertEquals(1, executor.runDueTasks());
assertEquals(0, executor.numPendingTasks());
assertEquals(1, sink.size());
assertEquals(ConnectivityState.CONNECTING, sink.poll());
state.gotoState(ConnectivityState.READY);
assertEquals(1, executor.runDueTasks());
assertEquals(0, executor.numPendingTasks());
assertEquals(1, sink.size());
assertEquals(ConnectivityState.READY, sink.poll());
}
@Test
public void registerCallbackFromCallbackDirectExecutor() {
state.notifyWhenStateChanged(newRecursiveCallback(MoreExecutors.directExecutor()),
MoreExecutors.directExecutor(), state.getState());
state.gotoState(ConnectivityState.CONNECTING);
assertEquals(1, sink.size());
assertEquals(ConnectivityState.CONNECTING, sink.poll());
state.gotoState(ConnectivityState.READY);
assertEquals(1, sink.size());
assertEquals(ConnectivityState.READY, sink.poll());
}
}