package org.jbehave.eclipse.cache;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.jbehave.eclipse.editor.step.StepCandidate;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import fj.Effect;
@RunWith(MockitoJUnitRunner.class)
public class StepCandidateCacheLoaderTest {
/**
* A checkpoint is a thread notifier that holds its signaled state for later
* checks. It starts in the non-signaled state and can only be once in this
* state.
*/
private static class Checkpoint {
private final Object signal = new Object();
private final AtomicBoolean signalSet = new AtomicBoolean(false);
public void setSignal() {
synchronized (this.signal) {
this.signalSet.set(true);
this.signal.notifyAll();
}
}
public void waitForSignal() {
try {
waitForSignalUnsafe();
} catch (final InterruptedException e) {
Assert.fail("Interrupted Exception");
}
}
private void waitForSignalUnsafe() throws InterruptedException {
synchronized (this.signal) {
if (!this.signalSet.get()) {
this.signal.wait();
}
}
}
}
/**
* This executor either runs tasks in a dedicated thread or directly.
* Default is to run tasks directly.
*
* Tasks sent to the dedicated thread are blocked until signaled to run.
*/
private static class RiggedExecutor implements Executor {
private final AtomicBoolean directExecution = new AtomicBoolean(true);
private final Checkpoint completeThreadExecution = new Checkpoint();
private final Checkpoint threadExecutionStopped = new Checkpoint();
private final Executor executor = Executors.newSingleThreadExecutor();
/** {@inheritDoc} */
public void execute(Runnable command) {
if (directExecution.get()) {
command.run();
} else {
runInExecutor(command);
}
}
public void setDirectExecution(boolean value) {
directExecution.set(value);
}
public void signalBlockedThread() {
completeThreadExecution.setSignal();
threadExecutionStopped.waitForSignal();
}
private void runInExecutor(final Runnable command) {
executor.execute(new Runnable() {
public void run() {
completeThreadExecution.waitForSignal();
try {
command.run();
} finally {
threadExecutionStopped.setSignal();
}
}
});
}
}
private static final Executor DIRECT_EXECUTOR = new Executor() {
public void execute(Runnable command) {
command.run();
}
};
private Executor scanningExecutor;
private RiggedExecutor completionExecutor;
private StepCandidateCacheLoader loader;
private StepCandidateCacheListener listener;
private List<MethodCache<StepCandidate>> notifiedCaches;
@Mock
private IJavaProject javaProject;
private Effect<JavaScanner<?>> scanInitializer;
private List<IPackageFragmentRoot> rootPackageFragments;
@Test
public void testListenerCallbackCalled_WhenSimpleCase() {
MethodCache<StepCandidate> cache = createCache();
givenAnInitializedLoader();
whenRequestingLoad(cache);
thenCacheShouldHaveBeenNotified(cache);
}
@Test
public void testLoaderDoesNotUseScanningExecutorForCompletionWait() {
MethodCache<StepCandidate> cache = createCache();
givenAMockedScanningExecutor();
givenAnInitializedLoader();
whenRequestingLoad(cache);
thenScanningExecutorShouldNotHaveBeenUsed();
}
@Test
public void testSecondReloadWaitsUntilFirstIsNotified_WhenRequestedDuringProcess() {
MethodCache<StepCandidate> cacheA = createCache();
MethodCache<StepCandidate> cacheB = createCache();
givenAnInitializedLoader();
givenTheNextCacheReloadWouldBlock();
givenReloadWasRequested(cacheA);
givenTheNextCacheReloadWouldRunInstantly();
givenReloadWasRequested(cacheB);
whenTheBlockedCacheReloadCompletes();
thenCachesShouldHaveBeenNotifiedInOrder(cacheA, cacheB);
}
@Test
public void testSecondReloadIsSkipped_WhenThreeRequestedWhileFirstIsBlocking() {
MethodCache<StepCandidate> cacheA = createCache();
MethodCache<StepCandidate> cacheB = createCache();
MethodCache<StepCandidate> cacheC = createCache();
givenAnInitializedLoader();
givenTheNextCacheReloadWouldBlock();
givenReloadWasRequested(cacheA);
givenTheNextCacheReloadWouldRunInstantly();
givenReloadWasRequested(cacheB);
givenReloadWasRequested(cacheC);
whenTheBlockedCacheReloadCompletes();
thenCachesShouldHaveBeenNotifiedInOrder(cacheA, cacheC);
}
@Test
public void testAnotherReloadIsPossible_WhenFirstOneCompleted() {
MethodCache<StepCandidate> cacheA = createCache();
MethodCache<StepCandidate> cacheB = createCache();
givenAnInitializedLoader();
givenReloadWasRequested(cacheA);
whenRequestingLoad(cacheB);
thenCacheShouldHaveBeenNotified(cacheB);
}
@Before
public void setUp() throws Exception {
this.scanningExecutor = DIRECT_EXECUTOR;
this.completionExecutor = new RiggedExecutor();
this.notifiedCaches = new ArrayList<MethodCache<StepCandidate>>();
this.scanInitializer = new Effect<JavaScanner<?>>() {
@Override
public void e(JavaScanner<?> scanner) {
}
};
this.listener = new StepCandidateCacheListener() {
public void cacheLoaded(MethodCache<StepCandidate> cache) {
notifiedCaches.add(cache);
}
};
this.rootPackageFragments = new ArrayList<IPackageFragmentRoot>();
}
private MethodCache<StepCandidate> createCache() {
return new MethodCache<StepCandidate>(null);
}
public void givenAMockedScanningExecutor() {
this.scanningExecutor = Mockito.mock(Executor.class);
}
public void givenTheNextCacheReloadWouldRunInstantly() {
this.completionExecutor.setDirectExecution(true);
}
public void givenTheNextCacheReloadWouldBlock() {
// The tests are rigged through the completion executor; Trying
// this via a blocked scanningExecutor (as the real-life would) would
// require way too much mocking and control. For the sake of the tests,
// it is enough to directly block the completion executor.
this.completionExecutor.setDirectExecution(false);
}
public void whenTheBlockedCacheReloadCompletes() {
this.completionExecutor.signalBlockedThread();
}
public void givenAnInitializedLoader() {
try {
Mockito.when(this.javaProject.getAllPackageFragmentRoots())
.thenReturn(
this.rootPackageFragments
.toArray(new IPackageFragmentRoot[0]));
} catch (JavaModelException e) {
}
this.loader = new StepCandidateCacheLoader(this.listener,
this.scanningExecutor, this.completionExecutor);
}
public void givenReloadWasRequested(MethodCache<StepCandidate> cache) {
this.loader
.requestReload(cache, this.javaProject, this.scanInitializer);
}
public void whenRequestingLoad(MethodCache<StepCandidate> cache) {
this.loader
.requestReload(cache, this.javaProject, this.scanInitializer);
}
public void thenCacheShouldHaveBeenNotified(MethodCache<StepCandidate> cache) {
Assert.assertTrue(this.notifiedCaches.contains(cache));
}
public void thenScanningExecutorShouldNotHaveBeenUsed() {
Mockito.verify(this.scanningExecutor, Mockito.never()).execute(
Mockito.any(Runnable.class));
}
@SuppressWarnings("unchecked")
public void thenCachesShouldHaveBeenNotifiedInOrder(
MethodCache<StepCandidate> expectedA,
MethodCache<StepCandidate> expectedB) {
Assert.assertEquals(Arrays.asList(expectedA, expectedB),
this.notifiedCaches);
}
}