/******************************************************************************* * Copyright (c) 2015 Obeo. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Obeo - initial API and implementation *******************************************************************************/ package org.eclipse.emf.compare.ide.ui.tests.logical.resolver; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import com.google.common.util.concurrent.FutureCallback; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.IComputation; import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler; import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler.CallStatus; import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler.ComputationState; import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler.ShutdownState; import org.eclipse.emf.compare.ide.ui.internal.logical.resolver.ResourceComputationScheduler.ShutdownStatus; import org.junit.Before; import org.junit.Test; /** * These tests are the same as in the extend class, except they use a {@link ResourceComputationScheduler} * initialized with a non-null {@link EventBus}. It also adds tests that can only be performed when an * EventBus is set on the scheduler. * * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> */ @SuppressWarnings("nls") public class ResourceComputationSchedulerWithEventBusTest extends ResourceComputationSchedulerTest { private EventBus bus; @Test public void testEventsLaunchedInStandardCase() throws Exception { scheduler.initialize(); final List<CallStatus> receivedEvents = new ArrayList<CallStatus>(); bus.register(new Object() { @Subscribe public void callStatusChanged(CallStatus cs) { receivedEvents.add(cs); } }); scheduler.call(new Callable<String>() { public String call() throws Exception { return ""; } }, null); assertEquals(4, receivedEvents.size()); assertEquals(ComputationState.SETTING_UP, receivedEvents.get(0).getState()); assertEquals(ComputationState.SCHEDULED, receivedEvents.get(1).getState()); assertEquals(ComputationState.FINISHING, receivedEvents.get(2).getState()); assertEquals(ComputationState.FINISHED, receivedEvents.get(3).getState()); } @Test public void testEventsLaunchedWhenCallThrowsException() throws Exception { scheduler.initialize(); final List<CallStatus> receivedEvents = new ArrayList<CallStatus>(); bus.register(new Object() { @Subscribe public void callStatusChanged(CallStatus cs) { receivedEvents.add(cs); } }); Exception exceptionReceived = null; try { scheduler.call(new Callable<String>() { public String call() throws Exception { throw new Exception("Test"); } }, null); } catch (Exception e) { exceptionReceived = e; } assertEquals(5, receivedEvents.size()); assertEquals(ComputationState.SETTING_UP, receivedEvents.get(0).getState()); assertEquals(ComputationState.SCHEDULED, receivedEvents.get(1).getState()); assertEquals(ComputationState.FAILED, receivedEvents.get(2).getState()); // Check the root exception has been well passed in the event too assertNotNull(exceptionReceived); assertSame(exceptionReceived.getCause(), receivedEvents.get(2).getCause()); assertEquals(ComputationState.FINISHING, receivedEvents.get(3).getState()); assertEquals(ComputationState.FINISHED, receivedEvents.get(4).getState()); } @Test public void testEventsLaunchedWhenPostTreamentThrowsException() throws Exception { scheduler.initialize(); final List<CallStatus> receivedEvents = new ArrayList<CallStatus>(); bus.register(new Object() { @Subscribe public void callStatusChanged(CallStatus cs) { receivedEvents.add(cs); } }); try { scheduler.call(new Callable<String>() { public String call() throws Exception { return ""; } }, new Runnable() { public void run() { throw new RuntimeException(); } }); fail("There should have been a RuntimeException"); } catch (RuntimeException e) { // As expected } // Flag will only be true if the CallStatus "FINISH event has been received assertEquals(4, receivedEvents.size()); assertEquals(ComputationState.SETTING_UP, receivedEvents.get(0).getState()); assertEquals(ComputationState.SCHEDULED, receivedEvents.get(1).getState()); assertEquals(ComputationState.FINISHING, receivedEvents.get(2).getState()); assertEquals(ComputationState.FINISHED, receivedEvents.get(3).getState()); } /** * This test checks that when a scheduler executes a long-runnning task that does not handle interrupts * "gracefully", that is by taking care to stop its treatment ASAP, then the scheduler can nevertheless * shutdown correctly. * * @throws Exception */ @Test public void testDemandShutdownWithLongRunningTaskThatInterruptsImproperly() throws Exception { scheduler.initialize(); final CompStatus cs = new CompStatus(); final AtomicBoolean readyForFinalChecks = new AtomicBoolean(false); // The following computation does not handle properly interruption // so we expect its treatment to terminate properly with succeeded instead of failed // The computation used here will set cs to "interrupted" when it is interrupted, but it will go on // until cs.trigger() is called, which is done after all the checks are made in the test. final TriggerableComputation tc = new TriggerableComputation(cs, "long1", false) { @Override protected void succeeded(Object r) { cs.success("As expected"); } @Override protected void failed(Throwable t) { cs.fail("failed() called, should have been succeeded()."); } }; bus.register(new Object() { @Subscribe public void check(ShutdownStatus status) { synchronized(scheduler) { switch (status.getState()) { case FINISH_SUCCESS: cs.setShutdownStatus(status); readyForFinalChecks.compareAndSet(false, true); // This will trigger the execution of the final tests scheduler.notifyAll(); break; case FINISH_FAILED: cs.setShutdownStatus(status); readyForFinalChecks.compareAndSet(false, true); scheduler.notifyAll(); } } } }); try { Integer result = scheduler.call(new Callable<Integer>() { public Integer call() throws Exception { scheduler.scheduleComputation(tc); // We ask for shutdown before the task can complete // The scheduler is configured to wait only 10ms scheduler.demandShutdown(); return Integer.valueOf(42); } }, null); assertEquals(Integer.valueOf(42), result); assertTrue(scheduler.getComputedElements().isEmpty()); // This will wait for the shutdown to be over // But the computation is still running synchronized(scheduler) { while (!readyForFinalChecks.get()) { scheduler.wait(); } assertEquals(1, cs.getCallCount()); assertTrue(cs.isInterrupted()); assertFalse(cs.isFailed()); // 2 next lines make sure the computation is still running assertFalse(cs.isSuccess()); assertNull(cs.getMessage()); // Make sure the shutdown is ok assertEquals(ShutdownState.FINISH_SUCCESS, cs.getShutdownStatus().getState()); } } finally { // We finally allow the tested computation, that is still running, to terminate tc.trigger(); } } @Test public void testDemandShutdownWithLongRunningTaskThatInterruptsGracefully() throws Exception { scheduler.initialize(); final CompStatus cs = new CompStatus(); final AtomicBoolean readyForFinalChecks = new AtomicBoolean(false); // The following computation handles interruption properly // so we expect its treatment to terminate with failed final TriggerableComputation tc = new TriggerableComputation(cs, "long1", true) { @Override protected void failed(Throwable t) { cs.fail("As expected"); } @Override protected void succeeded(Object r) { cs.success("Computation ends successfully when it should have failed."); } }; bus.register(new Object() { @Subscribe public void check(ShutdownStatus status) { synchronized(scheduler) { switch (status.getState()) { case FINISH_SUCCESS: cs.setShutdownStatus(status); readyForFinalChecks.compareAndSet(false, true); // This will trigger the execution of the final tests scheduler.notifyAll(); break; case FINISH_FAILED: cs.setShutdownStatus(status); readyForFinalChecks.compareAndSet(false, true); scheduler.notifyAll(); } } } }); Integer result = scheduler.call(new Callable<Integer>() { public Integer call() throws Exception { scheduler.scheduleComputation(tc); // We ask for shutdown before the task can complete // The scheduler is configured to wait only 100ms scheduler.demandShutdown(); return Integer.valueOf(42); } }, null); assertEquals(Integer.valueOf(42), result); assertTrue(scheduler.getComputedElements().isEmpty()); synchronized(scheduler) { // This will wait until the computation has finished to perform the final tests while (!readyForFinalChecks.get()) { scheduler.wait(); } assertEquals(1, cs.getCallCount()); assertTrue(cs.isInterrupted()); assertTrue(cs.isFailed()); assertFalse(cs.isSuccess()); assertEquals("As expected", cs.getMessage()); } } @Test public void testDemandShutdownWithRunningTaskThatTerminatesGracefully() throws Exception { scheduler = new ResourceComputationScheduler<String>(1, TimeUnit.SECONDS, bus); scheduler.initialize(); final CompStatus cs = new CompStatus(); final AtomicBoolean readyForFinalChecks = new AtomicBoolean(false); // The following computation does not handle interruption properly // but it has the time to terminate before the pools shut down // so we expect to have it succeed final TriggerableComputation tc = new TriggerableComputation(cs, "long1", false) { @Override protected void succeeded(Object r) { getStatus().success("As expected"); } @Override protected void failed(Throwable t) { getStatus().fail("failed() should not have been called."); } }; bus.register(new Object() { @Subscribe public void check(ShutdownStatus status) { synchronized(scheduler) { switch (status.getState()) { case FINISH_SUCCESS: cs.setShutdownStatus(status); readyForFinalChecks.compareAndSet(false, true); // This will trigger the execution of the final tests scheduler.notifyAll(); break; case FINISH_FAILED: cs.setShutdownStatus(status); readyForFinalChecks.compareAndSet(false, true); scheduler.notifyAll(); } } } }); Integer result = scheduler.call(new Callable<Integer>() { public Integer call() throws Exception { scheduler.scheduleComputation(tc); // We ask for shutdown before the task can complete // The scheduler is configured to wait 1s while the task only takes 500ms scheduler.demandShutdown(); // This allows the test to make sure that the shutdown has been demanded before // the treatment is over tc.trigger(); return Integer.valueOf(42); } }, null); assertEquals(Integer.valueOf(42), result); assertTrue(scheduler.getComputedElements().isEmpty()); synchronized(scheduler) { // This will wait until the computation has finished to perform the final tests while (!readyForFinalChecks.get()) { scheduler.wait(); } assertEquals(1, cs.getCallCount()); assertFalse(cs.isInterrupted()); assertTrue(cs.isSuccess()); assertFalse(cs.isFailed()); assertEquals("As expected", cs.getMessage()); assertEquals(ShutdownState.FINISH_SUCCESS, cs.getShutdownStatus().getState()); } } @Override @Before public void setUp() { bus = new EventBus(); scheduler = new ResourceComputationScheduler<String>(10, TimeUnit.MILLISECONDS, bus); } /** * A test computation that sleeps until it's either triggered by a call to {@link #trigger()} or * interrupted. * * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> */ private static class TriggerableComputation implements IComputation<String> { private volatile boolean start = false; private final CompStatus cs; private final String name; private final boolean throwOnInterrupt; public TriggerableComputation(CompStatus cs, String name, boolean throwOnInterrupt) { this.cs = cs; this.name = name; this.throwOnInterrupt = throwOnInterrupt; } public void trigger() { start = true; } public void run() { cs.addCall(); while (!start) { try { Thread.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); cs.interrupt(); if (throwOnInterrupt) { throw new RuntimeException("Interrupted"); } } } } public FutureCallback<Object> getPostTreatment() { return new FutureCallback<Object>() { public void onFailure(Throwable t) { failed(t); } public void onSuccess(Object r) { succeeded(r); } }; } public CompStatus getStatus() { return cs; } public String getKey() { return name; } /** * Override this method to specify specific expected behaviour in tests. Update variable cs to convey * information about succes or failure, since using junit methods (fail, assertTrue, etc.) here will * not cause a test to fail. * * @param t */ protected void failed(Throwable t) { cs.fail("As expected"); } /** * Override this method to specify specific expected behaviour in tests. Update variable cs to convey * information about succes or failure, since using junit methods (fail, assertTrue, etc.) here will * not cause a test to fail. * * @param t */ protected void succeeded(@SuppressWarnings("unused") Object r) { cs.success("As expected"); } } }