--- jsr166/src/test/tck/CompletableFutureTest.java 2015/09/06 22:21:07 1.122 +++ jsr166/src/test/tck/CompletableFutureTest.java 2015/11/15 19:55:38 1.134 @@ -7,10 +7,20 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.CompletableFuture.failedFuture; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -28,8 +38,10 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; +import junit.framework.AssertionFailedError; import junit.framework.Test; import junit.framework.TestSuite; @@ -828,9 +840,9 @@ public class CompletableFutureTest exten if (!createIncomplete) assertTrue(f.complete(v1)); final CompletableFuture g = f.exceptionally ((Throwable t) -> { - // Should not be called a.getAndIncrement(); - throw new AssertionError(); + threadFail("should not be called"); + return null; // unreached }); if (createIncomplete) assertTrue(f.complete(v1)); @@ -889,7 +901,7 @@ public class CompletableFutureTest exten * whenComplete action executes on normal completion, propagating * source result. */ - public void testWhenComplete_normalCompletion1() { + public void testWhenComplete_normalCompletion() { for (ExecutionMode m : ExecutionMode.values()) for (boolean createIncomplete : new boolean[] { true, false }) for (Integer v1 : new Integer[] { 1, null }) @@ -970,7 +982,7 @@ public class CompletableFutureTest exten * If a whenComplete action throws an exception when triggered by * a normal completion, it completes exceptionally */ - public void testWhenComplete_actionFailed() { + public void testWhenComplete_sourceCompletedNormallyActionFailed() { for (boolean createIncomplete : new boolean[] { true, false }) for (ExecutionMode m : ExecutionMode.values()) for (Integer v1 : new Integer[] { 1, null }) @@ -998,9 +1010,9 @@ public class CompletableFutureTest exten /** * If a whenComplete action throws an exception when triggered by * a source completion that also throws an exception, the source - * exception takes precedence. + * exception takes precedence (unlike handle) */ - public void testWhenComplete_actionFailedSourceFailed() { + public void testWhenComplete_sourceFailedActionFailed() { for (boolean createIncomplete : new boolean[] { true, false }) for (ExecutionMode m : ExecutionMode.values()) { @@ -1113,55 +1125,62 @@ public class CompletableFutureTest exten }} /** - * handle result completes exceptionally if action does + * If a "handle action" throws an exception when triggered by + * a normal completion, it completes exceptionally */ - public void testHandle_sourceFailedActionFailed() { + public void testHandle_sourceCompletedNormallyActionFailed() { for (ExecutionMode m : ExecutionMode.values()) for (boolean createIncomplete : new boolean[] { true, false }) + for (Integer v1 : new Integer[] { 1, null }) { final CompletableFuture f = new CompletableFuture<>(); final AtomicInteger a = new AtomicInteger(0); - final CFException ex1 = new CFException(); - final CFException ex2 = new CFException(); - if (!createIncomplete) f.completeExceptionally(ex1); + final CFException ex = new CFException(); + if (!createIncomplete) assertTrue(f.complete(v1)); final CompletableFuture g = m.handle (f, (Integer x, Throwable t) -> { m.checkExecutionMode(); - threadAssertNull(x); - threadAssertSame(ex1, t); + threadAssertSame(x, v1); + threadAssertNull(t); a.getAndIncrement(); - throw ex2; + throw ex; }); - if (createIncomplete) f.completeExceptionally(ex1); + if (createIncomplete) assertTrue(f.complete(v1)); - checkCompletedWithWrappedException(g, ex2); - checkCompletedExceptionally(f, ex1); + checkCompletedWithWrappedException(g, ex); + checkCompletedNormally(f, v1); assertEquals(1, a.get()); }} - public void testHandle_sourceCompletedNormallyActionFailed() { - for (ExecutionMode m : ExecutionMode.values()) + /** + * If a "handle action" throws an exception when triggered by + * a source completion that also throws an exception, the action + * exception takes precedence (unlike whenComplete) + */ + public void testHandle_sourceFailedActionFailed() { for (boolean createIncomplete : new boolean[] { true, false }) - for (Integer v1 : new Integer[] { 1, null }) + for (ExecutionMode m : ExecutionMode.values()) { - final CompletableFuture f = new CompletableFuture<>(); final AtomicInteger a = new AtomicInteger(0); - final CFException ex = new CFException(); - if (!createIncomplete) assertTrue(f.complete(v1)); + final CFException ex1 = new CFException(); + final CFException ex2 = new CFException(); + final CompletableFuture f = new CompletableFuture<>(); + + if (!createIncomplete) f.completeExceptionally(ex1); final CompletableFuture g = m.handle (f, (Integer x, Throwable t) -> { m.checkExecutionMode(); - threadAssertSame(x, v1); - threadAssertNull(t); + threadAssertNull(x); + threadAssertSame(ex1, t); a.getAndIncrement(); - throw ex; + throw ex2; }); - if (createIncomplete) assertTrue(f.complete(v1)); + if (createIncomplete) f.completeExceptionally(ex1); - checkCompletedWithWrappedException(g, ex); - checkCompletedNormally(f, v1); + checkCompletedWithWrappedException(g, ex2); + checkCompletedExceptionally(f, ex1); assertEquals(1, a.get()); }} @@ -3688,4 +3707,233 @@ public class CompletableFutureTest exten } }} + /** + * Minimal completion stages throw UOE for all non-CompletionStage methods + */ + public void testMinimalCompletionStage_minimality() { + if (!testImplementationDetails) return; + Function toSignature = + (method) -> method.getName() + Arrays.toString(method.getParameterTypes()); + Predicate isNotStatic = + (method) -> (method.getModifiers() & Modifier.STATIC) == 0; + List minimalMethods = + Stream.of(Object.class, CompletionStage.class) + .flatMap((klazz) -> Stream.of(klazz.getMethods())) + .filter(isNotStatic) + .collect(Collectors.toList()); + // Methods from CompletableFuture permitted NOT to throw UOE + String[] signatureWhitelist = { + "newIncompleteFuture[]", + "defaultExecutor[]", + "minimalCompletionStage[]", + "copy[]", + }; + Set permittedMethodSignatures = + Stream.concat(minimalMethods.stream().map(toSignature), + Stream.of(signatureWhitelist)) + .collect(Collectors.toSet()); + List allMethods = Stream.of(CompletableFuture.class.getMethods()) + .filter(isNotStatic) + .filter((method) -> !permittedMethodSignatures.contains(toSignature.apply(method))) + .collect(Collectors.toList()); + + CompletionStage minimalStage = + new CompletableFuture().minimalCompletionStage(); + + List bugs = new ArrayList<>(); + for (Method method : allMethods) { + Class[] parameterTypes = method.getParameterTypes(); + Object[] args = new Object[parameterTypes.length]; + // Manufacture boxed primitives for primitive params + for (int i = 0; i < args.length; i++) { + Class type = parameterTypes[i]; + if (parameterTypes[i] == boolean.class) + args[i] = false; + else if (parameterTypes[i] == int.class) + args[i] = 0; + else if (parameterTypes[i] == long.class) + args[i] = 0L; + } + try { + method.invoke(minimalStage, args); + bugs.add(method); + } + catch (java.lang.reflect.InvocationTargetException expected) { + if (! (expected.getCause() instanceof UnsupportedOperationException)) { + bugs.add(method); + // expected.getCause().printStackTrace(); + } + } + catch (ReflectiveOperationException bad) { throw new Error(bad); } + } + if (!bugs.isEmpty()) + throw new Error("Methods did not throw UOE: " + bugs.toString()); + } + + static class Monad { + static class ZeroException extends RuntimeException { + public ZeroException() { super("monadic zero"); } + } + // "return", "unit" + static CompletableFuture unit(T value) { + return completedFuture(value); + } + // monadic zero ? + static CompletableFuture zero() { + return failedFuture(new ZeroException()); + } + // >=> + static Function> compose + (Function> f, + Function> g) { + return (x) -> f.apply(x).thenCompose(g); + } + + static void assertZero(CompletableFuture f) { + try { + f.getNow(null); + throw new AssertionFailedError("should throw"); + } catch (CompletionException success) { + assertTrue(success.getCause() instanceof ZeroException); + } + } + + static void assertFutureEquals(CompletableFuture f, + CompletableFuture g) { + T fval = null, gval = null; + Throwable fex = null, gex = null; + + try { fval = f.get(); } + catch (ExecutionException ex) { fex = ex.getCause(); } + catch (Throwable ex) { fex = ex; } + + try { gval = g.get(); } + catch (ExecutionException ex) { gex = ex.getCause(); } + catch (Throwable ex) { gex = ex; } + + if (fex != null || gex != null) + assertSame(fex.getClass(), gex.getClass()); + else + assertEquals(fval, gval); + } + + static class PlusFuture extends CompletableFuture { + AtomicReference firstFailure = new AtomicReference<>(null); + } + + // Monadic "plus" + static CompletableFuture plus(CompletableFuture f, + CompletableFuture g) { + PlusFuture plus = new PlusFuture(); + BiConsumer action = (T result, Throwable ex) -> { + if (ex == null) { + if (plus.complete(result)) + if (plus.firstFailure.get() != null) + plus.firstFailure.set(null); + } + else if (plus.firstFailure.compareAndSet(null, ex)) { + if (plus.isDone()) + plus.firstFailure.set(null); + } + else { + // first failure has precedence + Throwable first = plus.firstFailure.getAndSet(null); + + // may fail with "Self-suppression not permitted" + try { first.addSuppressed(ex); } + catch (Exception ignored) {} + + plus.completeExceptionally(first); + } + }; + f.whenComplete(action); + g.whenComplete(action); + return plus; + } + } + + /** + * CompletableFuture is an additive monad - sort of. + * https://en.wikipedia.org/wiki/Monad_(functional_programming)#Additive_monads + */ + public void testAdditiveMonad() throws Throwable { + Function> unit = Monad::unit; + CompletableFuture zero = Monad.zero(); + + // Some mutually non-commutative functions + Function> triple + = (x) -> Monad.unit(3 * x); + Function> inc + = (x) -> Monad.unit(x + 1); + + // unit is a right identity: m >>= unit === m + Monad.assertFutureEquals(inc.apply(5L).thenCompose(unit), + inc.apply(5L)); + // unit is a left identity: (unit x) >>= f === f x + Monad.assertFutureEquals(unit.apply(5L).thenCompose(inc), + inc.apply(5L)); + + // associativity: (m >>= f) >>= g === m >>= ( \x -> (f x >>= g) ) + Monad.assertFutureEquals( + unit.apply(5L).thenCompose(inc).thenCompose(triple), + unit.apply(5L).thenCompose((x) -> inc.apply(x).thenCompose(triple))); + + // The case for CompletableFuture as an additive monad is weaker... + + // zero is a monadic zero + Monad.assertZero(zero); + + // left zero: zero >>= f === zero + Monad.assertZero(zero.thenCompose(inc)); + // right zero: f >>= (\x -> zero) === zero + Monad.assertZero(inc.apply(5L).thenCompose((x) -> zero)); + + // f plus zero === f + Monad.assertFutureEquals(Monad.unit(5L), + Monad.plus(Monad.unit(5L), zero)); + // zero plus f === f + Monad.assertFutureEquals(Monad.unit(5L), + Monad.plus(zero, Monad.unit(5L))); + // zero plus zero === zero + Monad.assertZero(Monad.plus(zero, zero)); + { + CompletableFuture f = Monad.plus(Monad.unit(5L), + Monad.unit(8L)); + // non-determinism + assertTrue(f.get() == 5L || f.get() == 8L); + } + + CompletableFuture godot = new CompletableFuture<>(); + // f plus godot === f (doesn't wait for godot) + Monad.assertFutureEquals(Monad.unit(5L), + Monad.plus(Monad.unit(5L), godot)); + // godot plus f === f (doesn't wait for godot) + Monad.assertFutureEquals(Monad.unit(5L), + Monad.plus(godot, Monad.unit(5L))); + } + +// static U join(CompletionStage stage) { +// CompletableFuture f = new CompletableFuture<>(); +// stage.whenComplete((v, ex) -> { +// if (ex != null) f.completeExceptionally(ex); else f.complete(v); +// }); +// return f.join(); +// } + +// static boolean isDone(CompletionStage stage) { +// CompletableFuture f = new CompletableFuture<>(); +// stage.whenComplete((v, ex) -> { +// if (ex != null) f.completeExceptionally(ex); else f.complete(v); +// }); +// return f.isDone(); +// } + +// static U join2(CompletionStage stage) { +// return stage.toCompletableFuture().copy().join(); +// } + +// static boolean isDone2(CompletionStage stage) { +// return stage.toCompletableFuture().copy().isDone(); +// } + }