/** * The MIT License * Copyright (c) 2014-2016 Ilkka Seppälä * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.iluwatar.promise; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Function; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; /** * Tests Promise class. */ public class PromiseTest { private Executor executor; private Promise<Integer> promise; @Rule public ExpectedException exception = ExpectedException.none(); @Before public void setUp() { executor = Executors.newSingleThreadExecutor(); promise = new Promise<>(); } @Test public void promiseIsFulfilledWithTheResultantValueOfExecutingTheTask() throws InterruptedException, ExecutionException { promise.fulfillInAsync(new NumberCrunchingTask(), executor); assertEquals(NumberCrunchingTask.CRUNCHED_NUMBER, promise.get()); assertTrue(promise.isDone()); assertFalse(promise.isCancelled()); } @Test public void promiseIsFulfilledWithAnExceptionIfTaskThrowsAnException() throws InterruptedException, ExecutionException, TimeoutException { testWaitingForeverForPromiseToBeFulfilled(); testWaitingSomeTimeForPromiseToBeFulfilled(); } private void testWaitingForeverForPromiseToBeFulfilled() throws InterruptedException, TimeoutException { Promise<Integer> promise = new Promise<>(); promise.fulfillInAsync(new Callable<Integer>() { @Override public Integer call() throws Exception { throw new RuntimeException("Barf!"); } }, executor); try { promise.get(); fail("Fetching promise should result in exception if the task threw an exception"); } catch (ExecutionException ex) { assertTrue(promise.isDone()); assertFalse(promise.isCancelled()); } try { promise.get(1000, TimeUnit.SECONDS); fail("Fetching promise should result in exception if the task threw an exception"); } catch (ExecutionException ex) { assertTrue(promise.isDone()); assertFalse(promise.isCancelled()); } } private void testWaitingSomeTimeForPromiseToBeFulfilled() throws InterruptedException, TimeoutException { Promise<Integer> promise = new Promise<>(); promise.fulfillInAsync(new Callable<Integer>() { @Override public Integer call() throws Exception { throw new RuntimeException("Barf!"); } }, executor); try { promise.get(1000, TimeUnit.SECONDS); fail("Fetching promise should result in exception if the task threw an exception"); } catch (ExecutionException ex) { assertTrue(promise.isDone()); assertFalse(promise.isCancelled()); } try { promise.get(); fail("Fetching promise should result in exception if the task threw an exception"); } catch (ExecutionException ex) { assertTrue(promise.isDone()); assertFalse(promise.isCancelled()); } } @Test public void dependentPromiseIsFulfilledAfterTheConsumerConsumesTheResultOfThisPromise() throws InterruptedException, ExecutionException { Promise<Void> dependentPromise = promise .fulfillInAsync(new NumberCrunchingTask(), executor) .thenAccept(value -> { assertEquals(NumberCrunchingTask.CRUNCHED_NUMBER, value); }); dependentPromise.get(); assertTrue(dependentPromise.isDone()); assertFalse(dependentPromise.isCancelled()); } @Test public void dependentPromiseIsFulfilledWithAnExceptionIfConsumerThrowsAnException() throws InterruptedException, ExecutionException, TimeoutException { Promise<Void> dependentPromise = promise .fulfillInAsync(new NumberCrunchingTask(), executor) .thenAccept(new Consumer<Integer>() { @Override public void accept(Integer value) { throw new RuntimeException("Barf!"); } }); try { dependentPromise.get(); fail("Fetching dependent promise should result in exception " + "if the action threw an exception"); } catch (ExecutionException ex) { assertTrue(promise.isDone()); assertFalse(promise.isCancelled()); } try { dependentPromise.get(1000, TimeUnit.SECONDS); fail("Fetching dependent promise should result in exception " + "if the action threw an exception"); } catch (ExecutionException ex) { assertTrue(promise.isDone()); assertFalse(promise.isCancelled()); } } @Test public void dependentPromiseIsFulfilledAfterTheFunctionTransformsTheResultOfThisPromise() throws InterruptedException, ExecutionException { Promise<String> dependentPromise = promise .fulfillInAsync(new NumberCrunchingTask(), executor) .thenApply(value -> { assertEquals(NumberCrunchingTask.CRUNCHED_NUMBER, value); return String.valueOf(value); }); assertEquals(String.valueOf(NumberCrunchingTask.CRUNCHED_NUMBER), dependentPromise.get()); assertTrue(dependentPromise.isDone()); assertFalse(dependentPromise.isCancelled()); } @Test public void dependentPromiseIsFulfilledWithAnExceptionIfTheFunctionThrowsException() throws InterruptedException, ExecutionException, TimeoutException { Promise<String> dependentPromise = promise .fulfillInAsync(new NumberCrunchingTask(), executor) .thenApply(new Function<Integer, String>() { @Override public String apply(Integer value) { throw new RuntimeException("Barf!"); } }); try { dependentPromise.get(); fail("Fetching dependent promise should result in exception " + "if the function threw an exception"); } catch (ExecutionException ex) { assertTrue(promise.isDone()); assertFalse(promise.isCancelled()); } try { dependentPromise.get(1000, TimeUnit.SECONDS); fail("Fetching dependent promise should result in exception " + "if the function threw an exception"); } catch (ExecutionException ex) { assertTrue(promise.isDone()); assertFalse(promise.isCancelled()); } } @Test public void fetchingAnAlreadyFulfilledPromiseReturnsTheFulfilledValueImmediately() throws InterruptedException, ExecutionException, TimeoutException { Promise<Integer> promise = new Promise<>(); promise.fulfill(NumberCrunchingTask.CRUNCHED_NUMBER); promise.get(1000, TimeUnit.SECONDS); } @SuppressWarnings("unchecked") @Test public void exceptionHandlerIsCalledWhenPromiseIsFulfilledExceptionally() { Promise<Object> promise = new Promise<>(); Consumer<Throwable> exceptionHandler = mock(Consumer.class); promise.onError(exceptionHandler); Exception exception = new Exception("barf!"); promise.fulfillExceptionally(exception); verify(exceptionHandler).accept(eq(exception)); } private static class NumberCrunchingTask implements Callable<Integer> { private static final Integer CRUNCHED_NUMBER = Integer.MAX_VALUE; @Override public Integer call() throws Exception { // Do number crunching Thread.sleep(100); return CRUNCHED_NUMBER; } } }