diff --git a/src/antlr/GroovyLexer.g4 b/src/antlr/GroovyLexer.g4 index 79ddf78dbd5..0887920f862 100644 --- a/src/antlr/GroovyLexer.g4 +++ b/src/antlr/GroovyLexer.g4 @@ -406,7 +406,9 @@ BuiltInPrimitiveType ; ABSTRACT : 'abstract'; +ASYNC : 'async'; ASSERT : 'assert'; +AWAIT : 'await'; fragment BOOLEAN : 'boolean'; diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4 index b41faad3333..befbe95a5c6 100644 --- a/src/antlr/GroovyParser.g4 +++ b/src/antlr/GroovyParser.g4 @@ -130,6 +130,7 @@ modifier : classOrInterfaceModifier | m=( NATIVE | SYNCHRONIZED + | ASYNC | TRANSIENT | VOLATILE | DEF @@ -776,6 +777,12 @@ expression // must come before postfixExpression to resolve the ambiguities between casting and call on parentheses expression, e.g. (int)(1 / 2) : castParExpression castOperandExpression #castExprAlt + // async expression + | ASYNC nls closureOrLambdaExpression #asyncExprAlt + + // await expression + | AWAIT nls expression #awaitExprAlt + // qualified names, array expressions, method invocation, post inc/dec | postfixExpression #postfixExprAlt @@ -1226,6 +1233,8 @@ identifier : Identifier | CapitalizedIdentifier | AS + | ASYNC + | AWAIT | IN | PERMITS | RECORD @@ -1243,7 +1252,9 @@ builtInType keywords : ABSTRACT | AS + | ASYNC | ASSERT + | AWAIT | BREAK | CASE | CATCH diff --git a/src/main/java/groovy/transform/Async.java b/src/main/java/groovy/transform/Async.java new file mode 100644 index 00000000000..0b9cd5809dd --- /dev/null +++ b/src/main/java/groovy/transform/Async.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform; + +import org.codehaus.groovy.transform.GroovyASTTransformationClass; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to mark async methods, closures and lambda expressions. + * + * @since 6.0.0 + */ +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.LOCAL_VARIABLE, ElementType.FIELD}) +@GroovyASTTransformationClass({"org.codehaus.groovy.transform.AsyncASTTransformation"}) +public @interface Async { +} diff --git a/src/main/java/groovy/util/concurrent/async/AsyncHelper.java b/src/main/java/groovy/util/concurrent/async/AsyncHelper.java new file mode 100644 index 00000000000..870f5d02368 --- /dev/null +++ b/src/main/java/groovy/util/concurrent/async/AsyncHelper.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.util.concurrent.async; + +import org.apache.groovy.util.SystemUtil; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +/** + * Helper class for async/await operations + * + * @since 6.0.0 + */ +public class AsyncHelper { + private static final int PARALLELISM = SystemUtil.getIntegerSafe("groovy.async.parallelism", Runtime.getRuntime().availableProcessors() + 1); + private static final Executor DEFAULT_EXECUTOR; + private static int seq; + + static { + Executor tmpExecutor; + try { + MethodHandle mh = MethodHandles.lookup().findStatic( + Executors.class, + "newVirtualThreadPerTaskExecutor", + MethodType.methodType(ExecutorService.class) + ); + tmpExecutor = (Executor) mh.invoke(); + } catch (Throwable throwable) { + // Fallback to default thread pool if virtual threads are not available + tmpExecutor = Executors.newFixedThreadPool(PARALLELISM, r -> { + Thread t = new Thread(r); + t.setName("async-thread-" + seq++); + return t; + }); + } + DEFAULT_EXECUTOR = tmpExecutor; + } + + /** + * Submits a supplier for asynchronous execution using the default executor + * + * @param supplier the supplier + * @param the result type + * @return the promise + */ + public static Promise async(Supplier supplier) { + return SimplePromise.of(supplier, DEFAULT_EXECUTOR); + } + + /** + * Submits a supplier for asynchronous execution using the provided executor + * + * @param supplier the supplier + * @param executor the executor + * @param the result type + * @return the promise + */ + public static Promise async(Supplier supplier, Executor executor) { + return SimplePromise.of(supplier, executor); + } + + /** + * Awaits the result of an awaitable + * + * @param awaitable the awaitable + * @param the result type + * @return the result + */ + public static T await(Awaitable awaitable) { + if (null == awaitable) return null; + + return awaitable.await(); + } + + /** + * Awaits the result of an object implementing Awaitable + * + * @param obj the object + * @param the result type + * @return the result + * @throws IllegalArgumentException if the object does not implement Awaitable + */ + @SuppressWarnings("unchecked") + public static T await(Object obj) { + if (null == obj) return null; + + if (!(obj instanceof Awaitable)) { + throw new IllegalArgumentException("type " + obj.getClass().getName() + " does not implement " + Awaitable.class.getName()); + } + return await((Awaitable) obj); + } + + private AsyncHelper() {} +} diff --git a/src/main/java/groovy/util/concurrent/async/AwaitException.java b/src/main/java/groovy/util/concurrent/async/AwaitException.java new file mode 100644 index 00000000000..20997581813 --- /dev/null +++ b/src/main/java/groovy/util/concurrent/async/AwaitException.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.util.concurrent.async; + +/** + * Exception thrown when attempting to wait for the result of a promise + * that aborted by throwing an exception. This exception can be + * inspected using the {@link #getCause()} method. + * + * @see Promise + * @since 6.0.0 + */ +public class AwaitException extends RuntimeException { + public AwaitException() { + } + + public AwaitException(String message) { + super(message); + } + + public AwaitException(Throwable cause) { + super(cause); + } + + public AwaitException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/groovy/util/concurrent/async/Awaitable.java b/src/main/java/groovy/util/concurrent/async/Awaitable.java new file mode 100644 index 00000000000..1cab022d8a7 --- /dev/null +++ b/src/main/java/groovy/util/concurrent/async/Awaitable.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.util.concurrent.async; + +/** + * Represents a result of an asynchronous computation + * + * @since 6.0.0 + */ +public interface Awaitable { + /** + * Waits if necessary for the computation to complete, and then retrieves its + * result. + * + * @return the computed result + * @throws AwaitException if the computation was cancelled or completed exceptionally + */ + T await(); +} diff --git a/src/main/java/groovy/util/concurrent/async/Promise.java b/src/main/java/groovy/util/concurrent/async/Promise.java new file mode 100644 index 00000000000..ee0964b7fa6 --- /dev/null +++ b/src/main/java/groovy/util/concurrent/async/Promise.java @@ -0,0 +1,340 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.util.concurrent.async; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Represents the result of an asynchronous computation that may be explicitly completed (setting its + * value and status), and may be used as a {@link StageAwaitable}, + * supporting dependent functions and actions that trigger upon its + * completion. + * + * @since 6.0.0 + */ +public interface Promise extends StageAwaitable, Future { + /** + * Causes invocations of {@link #get()} and related methods to throw the provided + * exception if not already completed. + * + * @param ex the exception + * @return {@code true} if this invocation caused this Promise to transition to a + * completed state, else {@code false} + */ + boolean completeExceptionally(Throwable ex); + + /** + * Forcibly causes subsequent invocations of {@link #get()} and related methods + * to throw the provided exception, regardless of whether already completed. + * This method is intended for use only in error recovery scenarios and may cause + * ongoing dependent completions to use established outcomes instead of the + * overwritten outcome even in such situations. + * + * @param ex the exception + * @throws NullPointerException if the exception is null + */ + void obtrudeException(Throwable ex); + + /** + * Completes this Promise exceptionally with a {@link TimeoutException} if not + * otherwise completed before the specified timeout. + * + * @param timeout the duration to wait before completing exceptionally with a + * TimeoutException, measured in units of {@code unit} + * @param unit the {@code TimeUnit} that determines how to interpret the + * {@code timeout} parameter + * @return this Promise + */ + Promise orTimeout(long timeout, TimeUnit unit); + + /** + * Returns the result value when complete, or throws an unchecked exception if + * completed exceptionally. To better conform with the use of common functional + * forms, if a computation involved in the completion of this Promise threw an + * exception, this method throws an unchecked {@link CompletionException} with + * the underlying exception as its cause. + * + * @return the result value + * @throws CancellationException if the computation was cancelled + * @throws CompletionException if this future completed exceptionally or a + * completion computation threw an exception + */ + T join(); + + /** + * Returns the default Executor used for async methods that do not specify an + * Executor. + * + * @return the executor + */ + Executor defaultExecutor(); + + /** + * Completes this Promise with the provided value if not otherwise completed + * before the specified timeout. + * + * @param value the value to use upon timeout + * @param timeout the duration to wait before completing normally with the + * provided value, measured in units of {@code unit} + * @param unit the {@code TimeUnit} that determines how to interpret the + * {@code timeout} parameter + * @return this Promise + */ + Promise completeOnTimeout(T value, long timeout, TimeUnit unit); + + /** + * If not already completed, sets the value returned by {@link #get()} and + * related methods to the provided value. + * + * @param value the result value + * @return {@code true} if this invocation caused this Promise to transition + * to a completed state, else {@code false} + */ + boolean complete(T value); + + /** + * Returns the estimated number of Promises whose completions are awaiting + * completion of this Promise. This method is designed for use in monitoring + * system state, not for synchronization control. + * + * @return the number of dependent Promises + */ + int getNumberOfDependents(); + + /** + * Returns {@code true} if this Promise completed exceptionally, in any way. + * Possible causes include cancellation, explicit invocation of + * {@code completeExceptionally}, and abrupt termination of a CompletionStage + * action. + * + * @return {@code true} if this Promise completed exceptionally + */ + boolean isCompletedExceptionally(); + + /** + * Completes this Promise with the result of the provided Supplier function + * invoked from an asynchronous task using the specified executor. + * + * @param supplier a function returning the value to be used to complete this + * Promise + * @param executor the executor to use for asynchronous execution + * @return this Promise + */ + Promise completeAsync(Supplier supplier, Executor executor); + + /** + * Forcibly sets or resets the value subsequently returned by method + * {@link #get()} and related methods, regardless of whether already + * completed. This method is designed for use only in error recovery actions, + * and even in such situations may result in ongoing dependent completions + * using established versus overwritten outcomes. + * + * @param value the completion value + */ + void obtrudeValue(T value); + + /** + * Returns a new Promise that is completed normally with the same value as + * this Promise when it completes normally. If this Promise completes + * exceptionally, then the returned Promise completes exceptionally with a + * CompletionException with this exception as cause. The behavior is + * equivalent to {@code thenApply(x -> x)}. This method may be useful as a + * form of "defensive copying", to prevent clients from completing, while + * still being able to arrange dependent actions. + * + * @return the new Promise + */ + Promise copy(); + + /** + * Completes this Promise with the result of the provided Supplier function + * invoked from an asynchronous task using the default executor. + * + * @param supplier a function returning the value to be used to complete this + * Promise + * @return this Promise + */ + Promise completeAsync(Supplier supplier); + + /** + * Returns the result value (or throws any encountered exception) if + * completed, else returns the provided valueIfAbsent. + * + * @param valueIfAbsent the value to return if not completed + * @return the result value, if completed, else the provided valueIfAbsent + * @throws CancellationException if the computation was cancelled + * @throws CompletionException if this future completed exceptionally or a + * completion computation threw an exception + */ + T getNow(T valueIfAbsent); + + /** + * Returns a {@link CompletableFuture} representation of the object. + * + * @return the CompletableFuture + */ + CompletableFuture toCompletableFuture(); + + @Override + Promise thenApply(Function fn); + + @Override + Promise thenApplyAsync(Function fn); + + @Override + Promise thenApplyAsync(Function fn, Executor executor); + + @Override + Promise thenAccept(Consumer action); + + @Override + Promise thenAcceptAsync(Consumer action); + + @Override + Promise thenAcceptAsync(Consumer action, Executor executor); + + @Override + Promise thenRun(Runnable action); + + @Override + Promise thenRunAsync(Runnable action); + + @Override + Promise thenRunAsync(Runnable action, Executor executor); + + @Override + Promise thenCombine(StageAwaitable other, BiFunction fn); + + @Override + Promise thenCombineAsync(StageAwaitable other, BiFunction fn); + + @Override + Promise thenCombineAsync(StageAwaitable other, BiFunction fn, Executor executor); + + @Override + Promise thenAcceptBoth(StageAwaitable other, BiConsumer action); + + @Override + Promise thenAcceptBothAsync(StageAwaitable other, BiConsumer action); + + @Override + Promise thenAcceptBothAsync(StageAwaitable other, BiConsumer action, Executor executor); + + @Override + Promise runAfterBoth(StageAwaitable other, Runnable action); + + @Override + Promise runAfterBothAsync(StageAwaitable other, Runnable action); + + @Override + Promise runAfterBothAsync(StageAwaitable other, Runnable action, Executor executor); + + @Override + Promise applyToEither(StageAwaitable other, Function fn); + + @Override + Promise applyToEitherAsync(StageAwaitable other, Function fn); + + @Override + Promise applyToEitherAsync(StageAwaitable other, Function fn, Executor executor); + + @Override + Promise acceptEither(StageAwaitable other, Consumer action); + + @Override + Promise acceptEitherAsync(StageAwaitable other, Consumer action); + + @Override + Promise acceptEitherAsync(StageAwaitable other, Consumer action, Executor executor); + + @Override + Promise runAfterEither(StageAwaitable other, Runnable action); + + @Override + Promise runAfterEitherAsync(StageAwaitable other, Runnable action); + + @Override + Promise runAfterEitherAsync(StageAwaitable other, Runnable action, Executor executor); + + @Override + Promise thenCompose(Function> fn); + + @Override + Promise thenComposeAsync(Function> fn); + + @Override + Promise thenComposeAsync(Function> fn, Executor executor); + + @Override + Promise handle(BiFunction fn); + + @Override + Promise handleAsync(BiFunction fn); + + @Override + Promise handleAsync(BiFunction fn, Executor executor); + + @Override + Promise whenComplete(BiConsumer action); + + @Override + Promise whenCompleteAsync(BiConsumer action); + + @Override + Promise whenCompleteAsync(BiConsumer action, Executor executor); + + @Override + Promise exceptionally(Function fn); + + @Override + default Promise exceptionallyAsync(Function fn) { + return StageAwaitable.super.exceptionallyAsync(fn).toPromise(); + } + + @Override + default Promise exceptionallyAsync(Function fn, Executor executor) { + return StageAwaitable.super.exceptionallyAsync(fn, executor).toPromise(); + } + + @Override + default Promise exceptionallyCompose(Function> fn) { + return StageAwaitable.super.exceptionallyCompose(fn).toPromise(); + } + + @Override + default Promise exceptionallyComposeAsync(Function> fn) { + return StageAwaitable.super.exceptionallyComposeAsync(fn).toPromise(); + } + + @Override + default Promise exceptionallyComposeAsync(Function> fn, Executor executor) { + return StageAwaitable.super.exceptionallyComposeAsync(fn, executor).toPromise(); + } +} diff --git a/src/main/java/groovy/util/concurrent/async/SimplePromise.java b/src/main/java/groovy/util/concurrent/async/SimplePromise.java new file mode 100644 index 00000000000..427322aa841 --- /dev/null +++ b/src/main/java/groovy/util/concurrent/async/SimplePromise.java @@ -0,0 +1,516 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.util.concurrent.async; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A simple implementation of {@link Promise} based on {@link CompletableFuture}. + * + * @since 6.0.0 + */ +public class SimplePromise implements Promise { + private final CompletableFuture future; + + private SimplePromise(CompletableFuture future) { + this.future = future; + } + + /** + * Creates a new Promise backed by the given CompletableFuture. + * + * @param future the CompletableFuture to back the Promise + * @param the type of the Promise's result + * @return the new Promise + */ + public static SimplePromise of(CompletableFuture future) { + return new SimplePromise<>(future); + } + + /** + * Returns a new Promise that is not yet completed. + * + * @param the type of the Promise's result + * @return the new Promise + */ + public static SimplePromise of() { + return of(new CompletableFuture<>()); + } + + /** + * Returns a new Promise that is asynchronously completed by a task running in + * the {@link ForkJoinPool#commonPool()} with the value obtained by calling the + * provided Supplier. + * + * @param supplier a function returning the value to be used to complete the + * returned Promise + * @param the function's return type + * @return the new Promise + */ + public static SimplePromise of(Supplier supplier) { + return of(CompletableFuture.supplyAsync(supplier)); + } + + /** + * Returns a new Promise that is asynchronously completed by a task running in + * the provided executor with the value obtained by calling the provided Supplier. + * + * @param supplier a function returning the value to be used to complete the + * returned Promise + * @param executor the executor to use for asynchronous execution + * @param the function's return type + * @return the new Promise + */ + public static SimplePromise of(Supplier supplier, Executor executor) { + return of(CompletableFuture.supplyAsync(supplier, executor)); + } + +// /** +// * Returns a new Promise that is asynchronously completed by a task running in +// * the {@link ForkJoinPool#commonPool()} after it runs the provided action. +// * +// * @param runnable the action to run before completing the returned Promise +// * @return the new Promise +// */ +// public static SimplePromise of(Runnable runnable) { +// return of(CompletableFuture.runAsync(runnable)); +// } +// +// /** +// * Returns a new Promise that is asynchronously completed by a task running in +// * the provided executor after it runs the provided action. +// * +// * @param runnable the action to run before completing the returned Promise +// * @param executor the executor to use for asynchronous execution +// * @return the new Promise +// */ +// public static SimplePromise of(Runnable runnable, Executor executor) { +// return of(CompletableFuture.runAsync(runnable, executor)); +// } + + /** + * Returns a new Promise that is already completed with the provided value. + * + * @param value the value + * @param the type of the value + * @return the completed Promise + */ + public static SimplePromise completed(U value) { + return of(CompletableFuture.completedFuture(value)); + } + + /** + * Returns a new Promise that is completed when all the provided Promises + * complete. If any of the provided Promises complete exceptionally, then the + * returned Promise also does so, with a CompletionException holding this + * exception as its cause. Otherwise, the results, if any, of the provided + * Promises are not reflected in the returned Promise, but may be obtained by + * inspecting them individually. If no Promises are provided, returns a Promise + * completed with the value {@code null}. + * + *

Among the applications of this method is to await completion of a set of + * independent Promises before continuing a program, as in: + * {@code SimplePromise.allOf(p1, p2, p3).join();}. + * + * @param ps the Promises + * @return a new Promise that is completed when all the provided Promises complete + * @throws NullPointerException if the array or any of its elements are {@code null} + */ + public static SimplePromise allOf(Promise... ps) { + return of(CompletableFuture.allOf( + Arrays.stream(ps) + .map(Promise::toCompletableFuture) + .toArray(CompletableFuture[]::new) + )); + } + + /** + * Returns a new Promise that is completed when any of the provided Promises + * complete, with the same result. Otherwise, if it completed exceptionally, + * the returned Promise also does so, with a CompletionException holding this + * exception as its cause. If no Promises are provided, returns an incomplete + * Promise. + * + * @param ps the Promises + * @return a new Promise that is completed with the result or exception to any + * of the provided Promises when one completes + * @throws NullPointerException if the array or any of its elements are + * {@code null} + */ + public static SimplePromise anyOf(Promise... ps) { + return of(CompletableFuture.anyOf( + Arrays.stream(ps) + .map(Promise::toCompletableFuture) + .toArray(CompletableFuture[]::new) + )); + } + + @Override + public T await() { + try { + return this.join(); + } catch (Throwable t) { + throw new AwaitException(t); + } + } + + @Override + public Promise whenCompleteAsync(BiConsumer action, Executor executor) { + return of(future.whenCompleteAsync(action, executor)); + } + + @Override + public boolean completeExceptionally(Throwable ex) { + return future.completeExceptionally(ex); + } + + @Override + public Promise thenRun(Runnable action) { + return of(future.thenRun(action)); + } + + @Override + public Promise applyToEither(StageAwaitable other, Function fn) { + return of(future.applyToEither(other.toPromise().toCompletableFuture(), fn)); + } + + @Override + public void obtrudeException(Throwable ex) { + future.obtrudeException(ex); + } + + @Override + public Promise exceptionallyComposeAsync(Function> fn) { + return of(future.exceptionallyComposeAsync(t -> { + final StageAwaitable p = fn.apply(t); + return p.toPromise().toCompletableFuture(); + })); + } + + @Override + public Promise whenComplete(BiConsumer action) { + return of(future.whenComplete(action)); + } + + @Override + public Promise applyToEitherAsync(StageAwaitable other, Function fn, Executor executor) { + return of(future.applyToEitherAsync(other.toPromise().toCompletableFuture(), fn, executor)); + } + + @Override + public Promise thenApplyAsync(Function fn) { + return of(future.thenApplyAsync(fn)); + } + + @Override + public Promise thenAcceptBothAsync(StageAwaitable other, BiConsumer action) { + return of(future.thenAcceptBothAsync(other.toPromise().toCompletableFuture(), action)); + } + + @Override + public Promise exceptionallyAsync(Function fn, Executor executor) { + return of(future.exceptionallyAsync(fn, executor)); + } + + @Override + public Promise thenRunAsync(Runnable action, Executor executor) { + return of(future.thenRunAsync(action, executor)); + } + + @Override + public Promise runAfterEitherAsync(StageAwaitable other, Runnable action) { + return of(future.runAfterEitherAsync(other.toPromise().toCompletableFuture(), action)); + } + + @Override + public Promise orTimeout(long timeout, TimeUnit unit) { + return of(future.orTimeout(timeout, unit)); + } + + @Override + public Promise thenCombineAsync(StageAwaitable other, BiFunction fn) { + return of(future.thenCombineAsync(other.toPromise().toCompletableFuture(), fn)); + } + + @Override + public Promise runAfterBoth(StageAwaitable other, Runnable action) { + return of(future.runAfterBoth(other.toPromise().toCompletableFuture(), action)); + } + + @Override + public Promise thenCompose(Function> fn) { + return of(future.thenCompose(t -> { + final StageAwaitable p = fn.apply(t); + return p.toPromise().toCompletableFuture(); + })); + } + + @Override + public Promise runAfterBothAsync(StageAwaitable other, Runnable action, Executor executor) { + return of(future.runAfterBothAsync(other.toPromise().toCompletableFuture(), action, executor)); + } + + @Override + public Promise handleAsync(BiFunction fn) { + return of(future.handleAsync(fn)); + } + + @Override + public Promise thenComposeAsync(Function> fn, Executor executor) { + return of(future.thenComposeAsync(t -> { + final StageAwaitable p = fn.apply(t); + return p.toPromise().toCompletableFuture(); + }, executor)); + } + + @Override + public Promise thenAccept(Consumer action) { + return of(future.thenAccept(action)); + } + + @Override + public T join() { + return future.join(); + } + + @Override + public Promise acceptEitherAsync(StageAwaitable other, Consumer action) { + return of(future.acceptEitherAsync(other.toPromise().toCompletableFuture(), action)); + } + + @Override + public Executor defaultExecutor() { + return future.defaultExecutor(); + } + + @Override + public Promise exceptionallyCompose(Function> fn) { + return of(future.exceptionallyCompose(t -> { + final StageAwaitable p = fn.apply(t); + return p.toPromise().toCompletableFuture(); + })); + } + + @Override + public Promise thenAcceptBoth(StageAwaitable other, BiConsumer action) { + return of(future.thenAcceptBoth(other.toPromise().toCompletableFuture(), action)); + } + + @Override + public Promise runAfterEither(StageAwaitable other, Runnable action) { + return of(future.runAfterEither(other.toPromise().toCompletableFuture(), action)); + } + + @Override + public Promise completeOnTimeout(T value, long timeout, TimeUnit unit) { + return of(future.completeOnTimeout(value, timeout, unit)); + } + + @Override + public Promise handle(BiFunction fn) { + return of(future.handle(fn)); + } + + @Override + public boolean complete(T value) { + return future.complete(value); + } + + @Override + public Promise thenAcceptAsync(Consumer action, Executor executor) { + return of(future.thenAcceptAsync(action, executor)); + } + + @Override + public int getNumberOfDependents() { + return future.getNumberOfDependents(); + } + + @Override + public Promise thenAcceptBothAsync(StageAwaitable other, BiConsumer action, Executor executor) { + return of(future.thenAcceptBothAsync(other.toPromise().toCompletableFuture(), action, executor)); + } + + @Override + public Promise exceptionallyAsync(Function fn) { + return of(future.exceptionallyAsync(fn)); + } + + @Override + public Promise runAfterEitherAsync(StageAwaitable other, Runnable action, Executor executor) { + return of(future.runAfterEitherAsync(other.toPromise().toCompletableFuture(), action, executor)); + } + + @Override + public boolean isCompletedExceptionally() { + return future.isCompletedExceptionally(); + } + + @Override + public Promise completeAsync(Supplier supplier) { + return of(future.completeAsync(supplier)); + } + + @Override + public Promise applyToEitherAsync(StageAwaitable other, Function fn) { + return of(future.applyToEitherAsync(other.toPromise().toCompletableFuture(), fn)); + } + + @Override + public Promise whenCompleteAsync(BiConsumer action) { + return of(future.whenCompleteAsync(action)); + } + + @Override + public Promise thenRunAsync(Runnable action) { + return of(future.thenRunAsync(action)); + } + + @Override + public Promise thenApply(Function fn) { + return of(future.thenApply(fn)); + } + + @Override + public void obtrudeValue(T value) { + future.obtrudeValue(value); + } + + @Override + public Promise thenComposeAsync(Function> fn) { + return of(future.thenComposeAsync(t -> { + final StageAwaitable p = fn.apply(t); + return p.toPromise().toCompletableFuture(); + })); + } + + @Override + public Promise copy() { + return of(future.copy()); + } + + @Override + public Promise acceptEither(StageAwaitable other, Consumer action) { + return of(future.acceptEither(other.toPromise().toCompletableFuture(), action)); + } + + @Override + public Promise thenApplyAsync(Function fn, Executor executor) { + return of(future.thenApplyAsync(fn, executor)); + } + + @Override + public Promise thenCombine(StageAwaitable other, BiFunction fn) { + return of(future.thenCombine(other.toPromise().toCompletableFuture(), fn)); + } + + @Override + public Promise exceptionally(Function fn) { + return of(future.exceptionally(fn)); + } + + @Override + public Promise completeAsync(Supplier supplier, Executor executor) { + return of(future.completeAsync(supplier, executor)); + } + + @Override + public Promise acceptEitherAsync(StageAwaitable other, Consumer action, Executor executor) { + return of(future.acceptEitherAsync(other.toPromise().toCompletableFuture(), action, executor)); + } + + @Override + public Promise exceptionallyComposeAsync(Function> fn, Executor executor) { + return of(future.exceptionallyComposeAsync(t -> { + final StageAwaitable p = fn.apply(t); + return p.toPromise().toCompletableFuture(); + }, executor)); + } + + @Override + public Promise toPromise() { + return this; + } + + @Override + public Promise thenAcceptAsync(Consumer action) { + return of(future.thenAcceptAsync(action)); + } + + @Override + public Promise thenCombineAsync(StageAwaitable other, BiFunction fn, Executor executor) { + return of(future.thenCombineAsync(other.toPromise().toCompletableFuture(), fn, executor)); + } + + @Override + public Promise runAfterBothAsync(StageAwaitable other, Runnable action) { + return of(future.runAfterBothAsync(other.toPromise().toCompletableFuture(), action)); + } + + @Override + public Promise handleAsync(BiFunction fn, Executor executor) { + return of(future.handleAsync(fn, executor)); + } + + @Override + public T getNow(T valueIfAbsent) { + return future.getNow(valueIfAbsent); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return future.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public T get() throws InterruptedException, ExecutionException { + return future.get(); + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return future.get(timeout, unit); + } + + @Override + public CompletableFuture toCompletableFuture() { + return future.toCompletableFuture(); + } +} diff --git a/src/main/java/groovy/util/concurrent/async/StageAwaitable.java b/src/main/java/groovy/util/concurrent/async/StageAwaitable.java new file mode 100644 index 00000000000..59da4e1b0d3 --- /dev/null +++ b/src/main/java/groovy/util/concurrent/async/StageAwaitable.java @@ -0,0 +1,721 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.util.concurrent.async; + +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Represents a computation stage in a potentially asynchronous execution chain that + * executes an action or computes a value upon completion of another StageAwaitable. + * A stage completes when its computation finishes, which may subsequently trigger + * the execution of dependent stages in the chain. + * + * @since 6.0.0 + */ +public interface StageAwaitable extends Awaitable { + /** + * Creates a new StageAwaitable that executes the provided function with this stage's + * result as input when this stage completes successfully. + * + *

This method follows the same pattern as {@link java.util.Optional#map Optional.map} + * and {@link java.util.stream.Stream#map Stream.map}. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param fn the function used to compute the resulting StageAwaitable's value + * @param the return type of the function + * @return the newly created StageAwaitable + */ + StageAwaitable thenApply(Function fn); + + /** + * Creates a new StageAwaitable that executes the provided function asynchronously + * using this stage's default asynchronous execution facility when this stage + * completes successfully. The function receives this stage's result as input. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param fn the function used to compute the resulting StageAwaitable's value + * @param the return type of the function + * @return the newly created StageAwaitable + */ + StageAwaitable thenApplyAsync(Function fn); + + /** + * Creates a new StageAwaitable that executes the provided function asynchronously + * using the specified Executor when this stage completes successfully. The function + * receives this stage's result as input. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param fn the function used to compute the resulting StageAwaitable's value + * @param executor the executor used for asynchronous execution + * @param the return type of the function + * @return the newly created StageAwaitable + */ + StageAwaitable thenApplyAsync(Function fn, Executor executor); + + /** + * Creates a new StageAwaitable that executes the provided action with this stage's + * result as input when this stage completes successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param action the action to perform before completing the resulting StageAwaitable + * @return the newly created StageAwaitable + */ + StageAwaitable thenAccept(Consumer action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using this stage's default asynchronous execution facility when this stage + * completes successfully. The action receives this stage's result as input. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param action the action to perform before completing the resulting StageAwaitable + * @return the newly created StageAwaitable + */ + StageAwaitable thenAcceptAsync(Consumer action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using the specified Executor when this stage completes successfully. The action + * receives this stage's result as input. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param action the action to perform before completing the resulting StageAwaitable + * @param executor the executor used for asynchronous execution + * @return the newly created StageAwaitable + */ + StageAwaitable thenAcceptAsync(Consumer action, Executor executor); + + /** + * Creates a new StageAwaitable that executes the provided action when this stage + * completes successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param action the action to perform before completing the resulting StageAwaitable + * @return the newly created StageAwaitable + */ + StageAwaitable thenRun(Runnable action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using this stage's default asynchronous execution facility when this stage + * completes successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param action the action to perform before completing the resulting StageAwaitable + * @return the newly created StageAwaitable + */ + StageAwaitable thenRunAsync(Runnable action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using the specified Executor when this stage completes successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param action the action to perform before completing the resulting StageAwaitable + * @param executor the executor used for asynchronous execution + * @return the newly created StageAwaitable + */ + StageAwaitable thenRunAsync(Runnable action, Executor executor); + + /** + * Creates a new StageAwaitable that executes the provided function with the results + * of both this stage and the other stage when they both complete successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param fn the function used to compute the value of the resulting StageAwaitable + * @param the type of the other StageAwaitable's result + * @param the function's return type + * @return the newly created StageAwaitable + */ + StageAwaitable thenCombine(StageAwaitable other, BiFunction fn); + + /** + * Creates a new StageAwaitable that executes the provided function asynchronously + * using this stage's default asynchronous execution facility when both this stage + * and the other stage complete successfully. The function receives both results as + * arguments. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param fn the function used to compute the value of the resulting StageAwaitable + * @param the type of the other StageAwaitable's result + * @param the function's return type + * @return the newly created StageAwaitable + */ + StageAwaitable thenCombineAsync(StageAwaitable other, BiFunction fn); + + /** + * Creates a new StageAwaitable that executes the provided function asynchronously + * using the specified executor when both this stage and the other stage complete + * successfully. The function receives both results as arguments. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param fn the function used to compute the value of the resulting StageAwaitable + * @param executor the executor used for asynchronous execution + * @param the type of the other StageAwaitable's result + * @param the function's return type + * @return the newly created StageAwaitable + */ + StageAwaitable thenCombineAsync(StageAwaitable other, BiFunction fn, Executor executor); + + /** + * Creates a new StageAwaitable that executes the provided action with the results + * of both this stage and the other stage when they both complete successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @param the type of the other StageAwaitable's result + * @return the newly created StageAwaitable + */ + StageAwaitable thenAcceptBoth(StageAwaitable other, BiConsumer action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using this stage's default asynchronous execution facility when both this stage + * and the other stage complete successfully. The action receives both results as + * arguments. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @param the type of the other StageAwaitable's result + * @return the newly created StageAwaitable + */ + StageAwaitable thenAcceptBothAsync(StageAwaitable other, BiConsumer action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using the specified executor when both this stage and the other stage complete + * successfully. The action receives both results as arguments. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @param executor the executor used for asynchronous execution + * @param the type of the other StageAwaitable's result + * @return the newly created StageAwaitable + */ + StageAwaitable thenAcceptBothAsync(StageAwaitable other, BiConsumer action, Executor executor); + + /** + * Creates a new StageAwaitable that executes the provided action when both this + * stage and the other stage complete successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @return the newly created StageAwaitable + */ + StageAwaitable runAfterBoth(StageAwaitable other, Runnable action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using this stage's default asynchronous execution facility when both this stage + * and the other stage complete successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @return the newly created StageAwaitable + */ + StageAwaitable runAfterBothAsync(StageAwaitable other, Runnable action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using the specified executor when both this stage and the other stage complete + * successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @param executor the executor used for asynchronous execution + * @return the newly created StageAwaitable + */ + StageAwaitable runAfterBothAsync(StageAwaitable other, Runnable action, Executor executor); + + /** + * Creates a new StageAwaitable that executes the provided function with the result + * from whichever stage completes successfully first (either this stage or the other + * stage). + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param fn the function used to compute the value of the resulting StageAwaitable + * @param the function's return type + * @return the newly created StageAwaitable + */ + StageAwaitable applyToEither(StageAwaitable other, Function fn); + + /** + * Creates a new StageAwaitable that executes the provided function asynchronously + * using this stage's default asynchronous execution facility with the result from + * whichever stage completes successfully first (either this stage or the other stage). + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param fn the function used to compute the value of the resulting StageAwaitable + * @param the function's return type + * @return the newly created StageAwaitable + */ + StageAwaitable applyToEitherAsync(StageAwaitable other, Function fn); + + /** + * Creates a new StageAwaitable that executes the provided function asynchronously + * using the specified executor with the result from whichever stage completes + * successfully first (either this stage or the other stage). + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param fn the function used to compute the value of the resulting StageAwaitable + * @param executor the executor used for asynchronous execution + * @param the function's return type + * @return the newly created StageAwaitable + */ + StageAwaitable applyToEitherAsync(StageAwaitable other, Function fn, Executor executor); + + /** + * Creates a new StageAwaitable that executes the provided action with the result + * from whichever stage completes successfully first (either this stage or the other + * stage). + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @return the newly created StageAwaitable + */ + StageAwaitable acceptEither(StageAwaitable other, Consumer action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using this stage's default asynchronous execution facility with the result from + * whichever stage completes successfully first (either this stage or the other stage). + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @return the newly created StageAwaitable + */ + StageAwaitable acceptEitherAsync(StageAwaitable other, Consumer action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using the specified executor with the result from whichever stage completes + * successfully first (either this stage or the other stage). + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @param executor the executor used for asynchronous execution + * @return the newly created StageAwaitable + */ + StageAwaitable acceptEitherAsync(StageAwaitable other, Consumer action, Executor executor); + + /** + * Creates a new StageAwaitable that executes the provided action when either + * this stage or the other stage completes successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @return the newly created StageAwaitable + */ + StageAwaitable runAfterEither(StageAwaitable other, Runnable action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using this stage's default asynchronous execution facility when either this + * stage or the other stage completes successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @return the newly created StageAwaitable + */ + StageAwaitable runAfterEitherAsync(StageAwaitable other, Runnable action); + + /** + * Creates a new StageAwaitable that executes the provided action asynchronously + * using the specified executor when either this stage or the other stage completes + * successfully. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param other the other StageAwaitable + * @param action the action to perform before completing the resulting StageAwaitable + * @param executor the executor used for asynchronous execution + * @return the newly created StageAwaitable + */ + StageAwaitable runAfterEitherAsync(StageAwaitable other, Runnable action, Executor executor); + + /** + * Creates a new StageAwaitable that is completed with the same value as the + * StageAwaitable returned by the provided function. + * + *

When this stage completes successfully, the provided function is invoked + * with this stage's result as the argument, returning another StageAwaitable. + * When that stage completes successfully, the StageAwaitable returned by this + * method is completed with the same value. + * + *

To ensure progress, the supplied function must arrange eventual completion + * of its result. + * + *

This method is analogous to {@link java.util.Optional#flatMap Optional.flatMap} + * and {@link java.util.stream.Stream#flatMap Stream.flatMap}. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param fn the function used to compute another StageAwaitable + * @param the type of the resulting StageAwaitable's result + * @return the newly created StageAwaitable + */ + StageAwaitable thenCompose(Function> fn); + + /** + * Creates a new StageAwaitable that is completed with the same value as the + * StageAwaitable returned by the provided function, executed asynchronously + * using this stage's default asynchronous execution facility. + * + *

When this stage completes successfully, the provided function is invoked + * with this stage's result as the argument, returning another StageAwaitable. + * When that stage completes successfully, the StageAwaitable returned by this + * method is completed with the same value. + * + *

To ensure progress, the supplied function must arrange eventual completion + * of its result. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param fn the function used to compute another StageAwaitable + * @param the type of the resulting StageAwaitable's result + * @return the newly created StageAwaitable + */ + StageAwaitable thenComposeAsync(Function> fn); + + /** + * Creates a new StageAwaitable that is completed with the same value as the + * StageAwaitable returned by the provided function, executed asynchronously + * using the specified Executor. + * + *

When this stage completes successfully, the provided function is invoked + * with this stage's result as the argument, returning another StageAwaitable. + * When that stage completes successfully, the StageAwaitable returned by this + * method is completed with the same value. + * + *

To ensure progress, the supplied function must arrange eventual completion + * of its result. + * + *

Refer to the {@link StageAwaitable} documentation for behavior regarding + * exceptional completion scenarios. + * + * @param fn the function used to compute another StageAwaitable + * @param executor the executor used for asynchronous execution + * @param the type of the resulting StageAwaitable's result + * @return the newly created StageAwaitable + */ + StageAwaitable thenComposeAsync(Function> fn, Executor executor); + + /** + * Creates a new StageAwaitable that is executed with this stage's result and + * exception as arguments to the provided function when this stage completes + * either successfully or exceptionally. + * + *

When this stage is complete, the provided function is invoked with the + * result (or {@code null} if none) and the exception (or {@code null} if none) + * of this stage as arguments, and the function's result is used to complete the + * resulting stage. + * + * @param fn the function used to compute the value of the resulting StageAwaitable + * @param the function's return type + * @return the newly created StageAwaitable + */ + StageAwaitable handle(BiFunction fn); + + /** + * Creates a new StageAwaitable that is executed asynchronously using this stage's + * default asynchronous execution facility with this stage's result and exception + * as arguments to the provided function when this stage completes either + * successfully or exceptionally. + * + *

When this stage is complete, the provided function is invoked with the + * result (or {@code null} if none) and the exception (or {@code null} if none) + * of this stage as arguments, and the function's result is used to complete the + * resulting stage. + * + * @param fn the function used to compute the value of the resulting StageAwaitable + * @param the function's return type + * @return the newly created StageAwaitable + */ + StageAwaitable handleAsync(BiFunction fn); + + /** + * Creates a new StageAwaitable that is executed asynchronously using the + * specified executor with this stage's result and exception as arguments to + * the provided function when this stage completes either successfully or + * exceptionally. + * + *

When this stage is complete, the provided function is invoked with the + * result (or {@code null} if none) and the exception (or {@code null} if none) + * of this stage as arguments, and the function's result is used to complete the + * resulting stage. + * + * @param fn the function used to compute the value of the resulting StageAwaitable + * @param executor the executor used for asynchronous execution + * @param the function's return type + * @return the newly created StageAwaitable + */ + StageAwaitable handleAsync(BiFunction fn, Executor executor); + + /** + * Creates a new StageAwaitable with the same result or exception as this stage, + * that executes the provided action when this stage completes. + * + *

When this stage is complete, the provided action is invoked with the result + * (or {@code null} if none) and the exception (or {@code null} if none) of this + * stage as arguments. The resulting stage is completed when the action returns. + * + *

Unlike method {@link #handle handle}, this method is not designed to + * translate completion outcomes, so the supplied action should not throw an + * exception. However, if it does, the following rules apply: if this stage + * completed successfully but the supplied action throws an exception, then the + * resulting stage completes exceptionally with the supplied action's exception. + * Or, if this stage completed exceptionally and the supplied action throws an + * exception, then the resulting stage completes exceptionally with this stage's + * exception. + * + * @param action the action to perform + * @return the newly created StageAwaitable + */ + StageAwaitable whenComplete(BiConsumer action); + + /** + * Creates a new StageAwaitable with the same result or exception as this stage, + * that executes the provided action asynchronously using this stage's default + * asynchronous execution facility when this stage completes. + * + *

When this stage is complete, the provided action is invoked with the result + * (or {@code null} if none) and the exception (or {@code null} if none) of this + * stage as arguments. The resulting stage is completed when the action returns. + * + *

Unlike method {@link #handleAsync(BiFunction) handleAsync}, this method is + * not designed to translate completion outcomes, so the supplied action should + * not throw an exception. However, if it does, the following rules apply: If + * this stage completed successfully but the supplied action throws an exception, + * then the resulting stage completes exceptionally with the supplied action's + * exception. Or, if this stage completed exceptionally and the supplied action + * throws an exception, then the resulting stage completes exceptionally with + * this stage's exception. + * + * @param action the action to perform + * @return the newly created StageAwaitable + */ + StageAwaitable whenCompleteAsync(BiConsumer action); + + /** + * Creates a new StageAwaitable with the same result or exception as this stage, + * that executes the provided action asynchronously using the specified Executor + * when this stage completes. + * + *

When this stage is complete, the provided action is invoked with the result + * (or {@code null} if none) and the exception (or {@code null} if none) of this + * stage as arguments. The resulting stage is completed when the action returns. + * + *

Unlike method {@link #handleAsync(BiFunction,Executor) handleAsync}, this + * method is not designed to translate completion outcomes, so the supplied action + * should not throw an exception. However, if it does, the following rules apply: + * If this stage completed successfully but the supplied action throws an + * exception, then the resulting stage completes exceptionally with the supplied + * action's exception. Or, if this stage completed exceptionally and the supplied + * action throws an exception, then the resulting stage completes exceptionally + * with this stage's exception. + * + * @param action the action to perform + * @param executor the executor used for asynchronous execution + * @return the newly created StageAwaitable + */ + StageAwaitable whenCompleteAsync(BiConsumer action, Executor executor); + + /** + * Creates a new StageAwaitable that is executed with this stage's exception as + * the argument to the provided function when this stage completes exceptionally. + * Otherwise, if this stage completes successfully, then the resulting stage also + * completes successfully with the same value. + * + * @param fn the function used to compute the value of the resulting StageAwaitable + * if this StageAwaitable completed exceptionally + * @return the newly created StageAwaitable + */ + StageAwaitable exceptionally(Function fn); + + /** + * Creates a new StageAwaitable that is executed asynchronously with this stage's + * exception as the argument to the provided function using this stage's default + * asynchronous execution facility when this stage completes exceptionally. + * Otherwise, if this stage completes successfully, then the resulting stage also + * completes successfully with the same value. + * + * @implSpec The default implementation invokes {@link #handle}, relaying to + * {@link #handleAsync} on exception, then {@link #thenCompose} for result. + * + * @param fn the function used to compute the value of the resulting StageAwaitable + * if this StageAwaitable completed exceptionally + * @return the newly created StageAwaitable + */ + default StageAwaitable exceptionallyAsync(Function fn) { + return handle((r, ex) -> (ex == null) ? this : this.handleAsync((r1, ex1) -> fn.apply(ex1))).thenCompose(Function.identity()); + } + + /** + * Creates a new StageAwaitable that is executed asynchronously with this stage's + * exception as the argument to the provided function using the specified Executor + * when this stage completes exceptionally. Otherwise, if this stage completes + * successfully, then the resulting stage also completes successfully with the + * same value. + * + * @implSpec The default implementation invokes {@link #handle}, relaying to + * {@link #handleAsync} on exception, then {@link #thenCompose} for result. + * + * @param fn the function used to compute the value of the resulting StageAwaitable + * if this StageAwaitable completed exceptionally + * @param executor the executor used for asynchronous execution + * @return the newly created StageAwaitable + */ + default StageAwaitable exceptionallyAsync(Function fn, Executor executor) { + return handle((r, ex) -> (ex == null) ? this : this.handleAsync((r1, ex1) -> fn.apply(ex1), executor)).thenCompose(Function.identity()); + } + + /** + * Creates a new StageAwaitable that is composed using the result of the provided + * function applied to this stage's exception when this stage completes exceptionally. + * + * @implSpec The default implementation invokes {@link #handle}, invoking the + * provided function on exception, then {@link #thenCompose} for result. + * + * @param fn the function used to compute the resulting StageAwaitable if this + * StageAwaitable completed exceptionally + * @return the newly created StageAwaitable + */ + default StageAwaitable exceptionallyCompose(Function> fn) { + return handle((r, ex) -> (ex == null) ? this : fn.apply(ex)).thenCompose(Function.identity()); + } + + /** + * Creates a new StageAwaitable that is composed asynchronously using the result + * of the provided function applied to this stage's exception using this stage's + * default asynchronous execution facility when this stage completes exceptionally. + * + * @implSpec The default implementation invokes {@link #handle}, relaying to + * {@link #handleAsync} on exception, then {@link #thenCompose} for result. + * + * @param fn the function used to compute the resulting StageAwaitable if this + * StageAwaitable completed exceptionally + * @return the newly created StageAwaitable + */ + default StageAwaitable exceptionallyComposeAsync(Function> fn) { + return handle((r, ex) -> (ex == null) ? this : this.handleAsync((r1, ex1) -> fn.apply(ex1)).thenCompose(Function.identity())).thenCompose(Function.identity()); + } + + /** + * Creates a new StageAwaitable that is composed asynchronously using the result + * of the provided function applied to this stage's exception using the specified + * Executor when this stage completes exceptionally. + * + * @implSpec The default implementation invokes {@link #handle}, relaying to + * {@link #handleAsync} on exception, then {@link #thenCompose} for result. + * + * @param fn the function used to compute the resulting StageAwaitable if this + * StageAwaitable completed exceptionally + * @param executor the executor used for asynchronous execution + * @return the newly created StageAwaitable + */ + default StageAwaitable exceptionallyComposeAsync(Function> fn, Executor executor) { + return handle((r, ex) -> (ex == null) ? this : this.handleAsync((r1, ex1) -> fn.apply(ex1), executor).thenCompose(Function.identity())).thenCompose(Function.identity()); + } + + /** + * Creates a {@link Promise} that maintains the same completion properties + * as this stage. If this stage is already a Promise, this method may return + * this stage itself. Otherwise, invoking this method may have the same effect as + * {@code thenApply(x -> x)}, but returns an instance of type {@code Promise}. + * + * @return the Promise + */ + Promise toPromise(); +} diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java index 74d8c062e32..fe5ad6c2d4f 100644 --- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java +++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java @@ -20,11 +20,13 @@ import groovy.lang.Tuple2; import groovy.lang.Tuple3; +import groovy.transform.Async; import groovy.transform.CompileStatic; import groovy.transform.NonSealed; import groovy.transform.Sealed; import groovy.transform.Trait; import groovy.transform.TupleConstructor; +import groovy.util.concurrent.async.AsyncHelper; import org.antlr.v4.runtime.ANTLRErrorListener; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; @@ -39,6 +41,7 @@ import org.antlr.v4.runtime.misc.ParseCancellationException; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; +import org.apache.groovy.parser.antlr4.GroovyParser.AwaitExprAltContext; import org.apache.groovy.parser.antlr4.internal.DescriptiveErrorStrategy; import org.apache.groovy.parser.antlr4.internal.atnmanager.AtnManager; import org.apache.groovy.parser.antlr4.util.StringUtils; @@ -97,6 +100,7 @@ import org.codehaus.groovy.ast.expr.RangeExpression; import org.codehaus.groovy.ast.expr.SpreadExpression; import org.codehaus.groovy.ast.expr.SpreadMapExpression; +import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; import org.codehaus.groovy.ast.expr.TernaryExpression; import org.codehaus.groovy.ast.expr.TupleExpression; import org.codehaus.groovy.ast.expr.UnaryMinusExpression; @@ -151,10 +155,217 @@ import java.util.stream.Collectors; import static groovy.lang.Tuple.tuple; -import static org.apache.groovy.parser.antlr4.GroovyParser.*; +import static org.apache.groovy.parser.antlr4.GroovyParser.ADD; +import static org.apache.groovy.parser.antlr4.GroovyParser.ARROW; +import static org.apache.groovy.parser.antlr4.GroovyParser.AS; +import static org.apache.groovy.parser.antlr4.GroovyParser.ASYNC; +import static org.apache.groovy.parser.antlr4.GroovyParser.AdditiveExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.AndExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.AnnotatedQualifiedClassNameContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.AnnotationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.AnnotationNameContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.AnnotationsOptContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.AnonymousInnerClassDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ArgumentsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ArrayInitializerContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.AssertStatementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.AssignmentExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.AsyncExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.BlockContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.BlockStatementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.BlockStatementsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.BlockStatementsOptContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.BooleanLiteralAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.BreakStatementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.BuiltInTypeContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CASE; +import static org.apache.groovy.parser.antlr4.GroovyParser.CastExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CastParExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CatchClauseContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CatchTypeContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ClassBodyContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ClassBodyDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ClassDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ClassNameContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ClassOrInterfaceModifierContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ClassOrInterfaceModifiersContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ClassOrInterfaceModifiersOptContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ClosureContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ClosureOrLambdaExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CommandArgumentContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CommandExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CommandExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CompactConstructorDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CompilationUnitContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ConditionalExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ConditionalStatementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ContinueStatementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CreatedNameContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.CreatorContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.DEC; +import static org.apache.groovy.parser.antlr4.GroovyParser.DEF; +import static org.apache.groovy.parser.antlr4.GroovyParser.DEFAULT; +import static org.apache.groovy.parser.antlr4.GroovyParser.DoWhileStmtAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.DynamicMemberNameContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ElementValueArrayInitializerContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ElementValueContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ElementValuePairContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ElementValuePairsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ElementValuesContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.EnhancedArgumentListElementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.EnhancedArgumentListInParContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.EnhancedExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.EnhancedForControlContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.EnhancedStatementExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.EnumConstantContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.EnumConstantsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.EqualityExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ExclusiveOrExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ExpressionInParContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ExpressionListContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ExpressionListElementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.FINAL; +import static org.apache.groovy.parser.antlr4.GroovyParser.FieldDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.FinallyBlockContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.FloatingPointLiteralAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ForControlContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ForInitContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ForStmtAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ForUpdateContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.FormalParameterContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.FormalParameterListContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.FormalParametersContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.GE; +import static org.apache.groovy.parser.antlr4.GroovyParser.GT; +import static org.apache.groovy.parser.antlr4.GroovyParser.GroovyParserRuleContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.GstringContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.GstringPathContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.GstringValueContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.IN; +import static org.apache.groovy.parser.antlr4.GroovyParser.INC; +import static org.apache.groovy.parser.antlr4.GroovyParser.INSTANCEOF; +import static org.apache.groovy.parser.antlr4.GroovyParser.IdentifierContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.IdentifierPrmrAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.IfElseStatementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ImplicationExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ImportDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.InclusiveOrExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.IndexPropertyArgsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.IndexVariableContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.IntegerLiteralAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.KeywordsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.LE; +import static org.apache.groovy.parser.antlr4.GroovyParser.LT; +import static org.apache.groovy.parser.antlr4.GroovyParser.LabeledStmtAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.LambdaBodyContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ListContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.LocalVariableDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.LogicalAndExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.LogicalOrExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.LoopStmtAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MapContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MapEntryContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MapEntryLabelContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MapEntryListContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MatchingTypeContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MemberDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MethodBodyContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MethodDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MethodNameContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ModifierContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ModifiersContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ModifiersOptContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MultipleAssignmentExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.MultiplicativeExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.NON_SEALED; +import static org.apache.groovy.parser.antlr4.GroovyParser.NOT_IN; +import static org.apache.groovy.parser.antlr4.GroovyParser.NOT_INSTANCEOF; +import static org.apache.groovy.parser.antlr4.GroovyParser.NamePartContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.NamedPropertyArgsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.NewPrmrAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.NonWildcardTypeArgumentsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.NullLiteralAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.OriginalForControlContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.PRIVATE; +import static org.apache.groovy.parser.antlr4.GroovyParser.PackageDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ParExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.PathElementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.PathExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.PostfixExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.PowerExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.PrimitiveTypeContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.QualifiedClassNameContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.QualifiedClassNameListContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.QualifiedNameContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.QualifiedNameElementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.QualifiedStandardClassNameContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.RANGE_EXCLUSIVE_FULL; +import static org.apache.groovy.parser.antlr4.GroovyParser.RANGE_EXCLUSIVE_LEFT; +import static org.apache.groovy.parser.antlr4.GroovyParser.RANGE_EXCLUSIVE_RIGHT; +import static org.apache.groovy.parser.antlr4.GroovyParser.RANGE_INCLUSIVE; +import static org.apache.groovy.parser.antlr4.GroovyParser.ReferenceTypeContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.RegexExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.RelationalExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ResourceContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ResourceListContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ResourcesContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ReturnStmtAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ReturnTypeContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.SAFE_INDEX; +import static org.apache.groovy.parser.antlr4.GroovyParser.SEALED; +import static org.apache.groovy.parser.antlr4.GroovyParser.STATIC; +import static org.apache.groovy.parser.antlr4.GroovyParser.SUB; +import static org.apache.groovy.parser.antlr4.GroovyParser.ScriptStatementsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ShiftExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.StandardLambdaExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.StandardLambdaParametersContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.StatementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.StringLiteralContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.SuperPrmrAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.SwitchBlockStatementExpressionGroupContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.SwitchBlockStatementGroupContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.SwitchExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.SwitchExpressionContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.SwitchExpressionLabelContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.SwitchLabelContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.SwitchStatementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.SynchronizedStmtAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ThisFormalParameterContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ThisPrmrAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.ThrowStmtAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TryCatchStatementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeArgumentContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeArgumentsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeArgumentsOrDiamondContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeBoundContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeListContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeNamePairContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeNamePairsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeParameterContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.TypeParametersContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.UnaryAddExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.UnaryNotExprAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.VAR; +import static org.apache.groovy.parser.antlr4.GroovyParser.VariableDeclarationContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.VariableDeclaratorContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.VariableDeclaratorIdContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.VariableDeclaratorsContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.VariableInitializerContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.VariableModifierContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.VariableModifiersContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.VariableModifiersOptContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.VariableNamesContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.WhileStmtAltContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.YieldStatementContext; +import static org.apache.groovy.parser.antlr4.GroovyParser.YieldStmtAltContext; import static org.apache.groovy.parser.antlr4.util.PositionConfigureUtils.configureAST; import static org.apache.groovy.parser.antlr4.util.PositionConfigureUtils.configureEndPosition; +import static org.codehaus.groovy.ast.ClassHelper.make; import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.asyncS; import static org.codehaus.groovy.ast.tools.GeneralUtils.block; import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX; import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; @@ -1758,6 +1969,11 @@ public MethodNode visitMethodDeclaration(final MethodDeclarationContext ctx) { methodNode.getVariableScope().setInStaticContext(true); } + if (modifierManager.containsAny(ASYNC)) { + AnnotationNode asyncAnnotationNode = makeAnnotationNode(Async.class); + methodNode.addAnnotation(asyncAnnotationNode); + } + configureAST(methodNode, ctx); validateMethodDeclaration(ctx, methodNode, modifierManager, classNode); @@ -2944,6 +3160,22 @@ public CastExpression visitCastExprAlt(final CastExprAltContext ctx) { return configureAST(cast, ctx); } + @Override + public Expression visitAsyncExprAlt(AsyncExprAltContext ctx) { + ClosureExpression closureExpression = this.visitClosureOrLambdaExpression(ctx.closureOrLambdaExpression()); + Statement origCode = closureExpression.getCode(); + BlockStatement newCode = asyncS(origCode); + closureExpression.setCode(newCode); + return closureExpression; + } + + @Override + public Expression visitAwaitExprAlt(AwaitExprAltContext ctx) { + Expression expr = (Expression) this.visit(ctx.expression()); + StaticMethodCallExpression awaitMethodCallExpression = callX(ASYNC_HELPER_TYPE, "await", expr); + return configureAST(awaitMethodCallExpression, ctx); + } + @Override public BinaryExpression visitPowerExprAlt(final PowerExprAltContext ctx) { return this.createBinaryExpression(ctx.left, ctx.op, ctx.right, ctx); @@ -4128,15 +4360,21 @@ public TupleExpression visitVariableNames(final VariableNamesContext ctx) { @Override public ClosureExpression visitClosureOrLambdaExpression(final ClosureOrLambdaExpressionContext ctx) { + ClosureExpression expression = null; + // GROOVY-8991: Difference in behaviour with closure and lambda if (asBoolean(ctx.closure())) { - return configureAST(this.visitClosure(ctx.closure()), ctx); + expression = this.visitClosure(ctx.closure()); } else if (asBoolean(ctx.standardLambdaExpression())) { - return configureAST(this.visitStandardLambdaExpression(ctx.standardLambdaExpression()), ctx); + expression = this.visitStandardLambdaExpression(ctx.standardLambdaExpression()); } - // should never reach here - throw createParsingFailedException("The node is not expected here" + ctx.getText(), ctx); + if (null == expression) { + // should never reach here + throw createParsingFailedException("The node is not expected here" + ctx.getText(), ctx); + } + + return configureAST(expression, ctx); } @Override @@ -4795,6 +5033,8 @@ public List getDeclarationExpressions() { private Tuple2 numberFormatError; + private static final ClassNode ASYNC_HELPER_TYPE = make(AsyncHelper.class); + private int visitingClosureCount; private int visitingLoopStatementCount; private int visitingSwitchStatementCount; diff --git a/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java b/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java index 4d86449b8d1..c4a903a55fa 100644 --- a/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java +++ b/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java @@ -41,13 +41,14 @@ import static org.apache.groovy.parser.antlr4.GroovyLangParser.NATIVE; import static org.apache.groovy.parser.antlr4.GroovyLangParser.STATIC; import static org.apache.groovy.parser.antlr4.GroovyLangParser.VOLATILE; +import static org.apache.groovy.parser.antlr4.GroovyLangParser.ASYNC; /** * Process modifiers for AST nodes */ class ModifierManager { private static final Map> INVALID_MODIFIERS_MAP = Maps.of( - ConstructorNode.class, Arrays.asList(STATIC, FINAL, ABSTRACT, NATIVE), + ConstructorNode.class, Arrays.asList(STATIC, FINAL, ABSTRACT, NATIVE, ASYNC), MethodNode.class, Arrays.asList(VOLATILE/*, TRANSIENT*/) // Transient is left open for properties for legacy reasons but should be removed before ClassCompletionVerifier runs (CLASSGEN) ); private AstBuilder astBuilder; diff --git a/src/main/java/org/codehaus/groovy/ast/ModifierNode.java b/src/main/java/org/codehaus/groovy/ast/ModifierNode.java index e76e1b48499..83f86e98fbd 100644 --- a/src/main/java/org/codehaus/groovy/ast/ModifierNode.java +++ b/src/main/java/org/codehaus/groovy/ast/ModifierNode.java @@ -27,6 +27,7 @@ import static org.apache.groovy.parser.antlr4.GroovyParser.ABSTRACT; import static org.apache.groovy.parser.antlr4.GroovyParser.DEF; import static org.apache.groovy.parser.antlr4.GroovyParser.DEFAULT; +import static org.apache.groovy.parser.antlr4.GroovyParser.ASYNC; import static org.apache.groovy.parser.antlr4.GroovyParser.FINAL; import static org.apache.groovy.parser.antlr4.GroovyParser.NATIVE; import static org.apache.groovy.parser.antlr4.GroovyParser.NON_SEALED; @@ -72,7 +73,8 @@ public class ModifierNode extends ASTNode { NON_SEALED, 0, FINAL, Opcodes.ACC_FINAL, STRICTFP, Opcodes.ACC_STRICT, - DEFAULT, 0 // no flag for specifying a default method in the JVM spec, hence no ACC_DEFAULT flag in ASM + DEFAULT, 0, // no flag for specifying a default method in the JVM spec, hence no ACC_DEFAULT flag in ASM + ASYNC, 0 // a virtual modifier with no corresponding JVM flag ); public ModifierNode(Integer type) { diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java index 9d11192ba20..e7409051f96 100644 --- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java +++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java @@ -19,6 +19,7 @@ package org.codehaus.groovy.ast.tools; import groovy.lang.MetaProperty; +import groovy.util.concurrent.async.AsyncHelper; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.AnnotationNode; @@ -111,6 +112,7 @@ public class GeneralUtils { public static final Token MINUS = Token.newSymbol(Types.MINUS , -1, -1); public static final Token PLUS = Token.newSymbol(Types.PLUS , -1, -1); + public static BinaryExpression andX(final Expression lhv, final Expression rhv) { return binX(lhv, AND, rhv); } @@ -311,6 +313,21 @@ public static LambdaExpression lambdaX(final Statement code) { return lambdaX(Parameter.EMPTY_ARRAY, code); } + /** + * Wraps the original code inside an asynchronous execution using {@link AsyncHelper#async(java.util.function.Supplier)} + * + * @param origCode the original code + * @return the new block statement with asynchronous execution + * @since 6.0.0 + */ + public static BlockStatement asyncS(final Statement origCode) { + LambdaExpression supplierLambdaExpression = lambdaX(origCode); + Expression resultExpression = callX(ASYNC_HELPER_TYPE, "async", supplierLambdaExpression); + BlockStatement newCode = block(stmt(resultExpression)); + newCode.setSourcePosition(origCode); + return newCode; + } + /** * Builds a binary expression that compares two values. * @@ -1208,4 +1225,6 @@ public static boolean maybeFallsThrough(final Statement statement) { return true; } + + private static final ClassNode ASYNC_HELPER_TYPE = ClassHelper.make(AsyncHelper.class); } diff --git a/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java new file mode 100644 index 00000000000..cf334307b8f --- /dev/null +++ b/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.codehaus.groovy.transform; + +import groovy.transform.Async; +import groovy.util.concurrent.async.AsyncHelper; +import groovy.util.concurrent.async.Promise; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.GenericsType; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.ast.tools.GeneralUtils; +import org.codehaus.groovy.ast.tools.GenericsUtils; +import org.codehaus.groovy.classgen.VariableScopeVisitor; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; + +import static org.codehaus.groovy.ast.ClassHelper.make; +import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching; + +/** + * Handles generation of code for the {@link Async} annotation. + */ +@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) +public class AsyncASTTransformation extends AbstractASTTransformation { + private static final ClassNode MY_TYPE = make(Async.class); + private static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); + private static final ClassNode ASYNC_HELPER_TYPE = make(AsyncHelper.class); + + @Override + public void visit(ASTNode[] nodes, SourceUnit source) { + init(nodes, source); + AnnotationNode annotationNode = (AnnotationNode) nodes[0]; + AnnotatedNode annotatedNode = (AnnotatedNode) nodes[1]; + + if (MY_TYPE.equals(annotationNode.getClassNode()) && annotatedNode instanceof MethodNode) { + MethodNode methodNode = (MethodNode) annotatedNode; + if (methodNode.isAbstract()) { + addError("Annotation " + MY_TYPE_NAME + " cannot be used for abstract methods.", methodNode); + return; + } + + Statement origCode = methodNode.getCode(); + BlockStatement newCode = GeneralUtils.asyncS(origCode); + methodNode.setCode(newCode); + ClassNode wrappedOrigReturnType = ClassHelper.getWrapper(methodNode.getReturnType()); + ClassNode promiseType = GenericsUtils.makeClassSafeWithGenerics(makeWithoutCaching(Promise.class), new GenericsType(wrappedOrigReturnType)); + methodNode.setReturnType(promiseType); + VariableScopeVisitor variableScopeVisitor = new VariableScopeVisitor(sourceUnit); + variableScopeVisitor.visitClass(methodNode.getDeclaringClass()); + } + } +} diff --git a/src/test-resources/core/AsyncAwait_01x.groovy b/src/test-resources/core/AsyncAwait_01x.groovy new file mode 100644 index 00000000000..1c8f41ae3c4 --- /dev/null +++ b/src/test-resources/core/AsyncAwait_01x.groovy @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +async fetchData() { + return 1 +} + +async processData() { + def data = await fetchData() + return data + 2 +} + +def result = await processData() +assert result == 3 diff --git a/src/test-resources/core/AsyncAwait_02x.groovy b/src/test-resources/core/AsyncAwait_02x.groovy new file mode 100644 index 00000000000..8bd6e9ddcde --- /dev/null +++ b/src/test-resources/core/AsyncAwait_02x.groovy @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +async processData() { + def c1 = async { + return 3 + } + def c2 = async { n -> + return n + } + def data1 = await c1() + def data2 = await c2(1) + return data1 + data2 + 2 +} + +def result = await processData() +assert result == 6 diff --git a/src/test-resources/core/AsyncAwait_03x.groovy b/src/test-resources/core/AsyncAwait_03x.groovy new file mode 100644 index 00000000000..04b6c0a9e0e --- /dev/null +++ b/src/test-resources/core/AsyncAwait_03x.groovy @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +async processData() { + def c1 = async () -> { + return 3 + } + def c2 = async (n) -> { + return n + } + def data1 = await c1() + def data2 = await c2(1) + return data1 + data2 + 2 +} + +def result = await processData() +assert result == 6 diff --git a/src/test/groovy/groovy/util/concurrent/async/AsyncHelperTest.groovy b/src/test/groovy/groovy/util/concurrent/async/AsyncHelperTest.groovy new file mode 100644 index 00000000000..fcc5079f7cf --- /dev/null +++ b/src/test/groovy/groovy/util/concurrent/async/AsyncHelperTest.groovy @@ -0,0 +1,677 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.util.concurrent.async + +import groovy.transform.CompileStatic +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference + +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertFalse +import static org.junit.jupiter.api.Assertions.assertNotNull +import static org.junit.jupiter.api.Assertions.assertNull +import static org.junit.jupiter.api.Assertions.assertThrows +import static org.junit.jupiter.api.Assertions.assertTrue + +@CompileStatic +@DisplayName("AsyncHelper Tests") +class AsyncHelperTest { + + @Nested + @DisplayName("Basic async/await operations") + class BasicOperationsTest { + + @Test + @DisplayName("should execute simple async operation") + void testSimpleAsync() { + Promise promise = AsyncHelper.async(() -> 42) + Integer result = AsyncHelper.await(promise) + assertEquals(42, result) + } + + @Test + @DisplayName("should execute async operation with string") + void testAsyncWithString() { + Promise promise = AsyncHelper.async(() -> "Hello, Async!") + String result = AsyncHelper.await(promise) + assertEquals("Hello, Async!", result) + } + + @Test + @DisplayName("should handle async operation returning null") + void testAsyncReturningNull() { + Promise promise = AsyncHelper.async(() -> null) + Object result = AsyncHelper.await(promise) + assertNull(result) + } + + @Test + @DisplayName("should execute multiple async operations") + void testMultipleAsyncOperations() { + Promise p1 = AsyncHelper.async(() -> 10) + Promise p2 = AsyncHelper.async(() -> 20) + Promise p3 = AsyncHelper.async(() -> 30) + + Assertions.assertEquals(10, AsyncHelper.await(p1)) + Assertions.assertEquals(20, AsyncHelper.await(p2)) + Assertions.assertEquals(30, AsyncHelper.await(p3)) + } + + @Test + @DisplayName("should handle computation in async") + void testAsyncComputation() { + Promise promise = AsyncHelper.async(() -> { + int sum = 0 + for (int i = 1; i <= 100; i++) { + sum += i + } + return sum + }) + Assertions.assertEquals(5050, AsyncHelper.await(promise)) + } + } + + @Nested + @DisplayName("Custom executor operations") + class CustomExecutorTest { + private ExecutorService customExecutor + + @AfterEach + void cleanup() { + if (customExecutor != null) { + customExecutor.shutdown() + } + } + + @Test + @DisplayName("should use custom executor for async operation") + void testAsyncWithCustomExecutor() { + customExecutor = Executors.newSingleThreadExecutor() + AtomicReference threadName = new AtomicReference<>() + + Promise promise = AsyncHelper.async(() -> { + threadName.set(Thread.currentThread().getName()) + return 100 + }, customExecutor) + + Assertions.assertEquals(100, AsyncHelper.await(promise)) + assertNotNull(threadName.get()) + } + + @Test + @DisplayName("should handle multiple operations with custom executor") + void testMultipleOperationsWithCustomExecutor() { + customExecutor = Executors.newFixedThreadPool(2) + + Promise p1 = AsyncHelper.async(() -> 1, customExecutor) + Promise p2 = AsyncHelper.async(() -> 2, customExecutor) + Promise p3 = AsyncHelper.async(() -> 3, customExecutor) + + Assertions.assertEquals(6, AsyncHelper.await(p1) + AsyncHelper.await(p2) + AsyncHelper.await(p3)) + } + + @Test + @DisplayName("should work with cached thread pool") + void testWithCachedThreadPool() { + customExecutor = Executors.newCachedThreadPool() + List> promises = new ArrayList<>() + + for (int i = 0; i < 10; i++) { + final int value = i + promises.add(AsyncHelper.async(() -> value * 2, customExecutor)) + } + + int sum = 0 + for (Promise p : promises) { + sum += AsyncHelper.await(p) + } + assertEquals(90, sum) + } + } + + @Nested + @DisplayName("JavaScript-like async/await patterns") + class JavaScriptPatternsTest { + + @Test + @DisplayName("should chain async operations like Promise.then()") + void testChainedAsync() { + Promise result = AsyncHelper.async(() -> 10) + .thenApply(n -> n * 2) + .thenApply(n -> n + 5) + + Assertions.assertEquals(25, AsyncHelper.await(result)) + } + + @Test + @DisplayName("should handle sequential async operations") + void testSequentialAsync() { + Promise step1 = AsyncHelper.async(() -> "Step1") + String result1 = AsyncHelper.await(step1) + + Promise step2 = AsyncHelper.async(() -> result1 + "-Step2") + String result2 = AsyncHelper.await(step2) + + Promise step3 = AsyncHelper.async(() -> result2 + "-Step3") + String result3 = AsyncHelper.await(step3) + + assertEquals("Step1-Step2-Step3", result3) + } + + @Test + @DisplayName("should handle parallel async operations like Promise.all()") + @Timeout(2) + void testParallelAsync() { + Promise p1 = AsyncHelper.async(() -> { + sleep(100) + return 1 + }) + + Promise p2 = AsyncHelper.async(() -> { + sleep(100) + return 2 + }) + + Promise p3 = AsyncHelper.async(() -> { + sleep(100) + return 3 + }) + + Promise all = SimplePromise.allOf(p1, p2, p3) + AsyncHelper.await(all) + + Assertions.assertEquals(1, AsyncHelper.await(p1)) + Assertions.assertEquals(2, AsyncHelper.await(p2)) + Assertions.assertEquals(3, AsyncHelper.await(p3)) + } + + @Test + @DisplayName("should handle race conditions like Promise.race()") + @Timeout(1) + void testAsyncRace() { + Promise slow = AsyncHelper.async(() -> { + sleep(500) + return "slow" + }) + + Promise fast = AsyncHelper.async(() -> { + sleep(50) + return "fast" + }) + + Promise winner = SimplePromise.anyOf(fast, slow) + String result = AsyncHelper.await(winner) + + assertEquals("fast", result) + } + + @Test + @DisplayName("should handle async/await with exception handling") + void testAsyncWithExceptionHandling() { + Promise promise = AsyncHelper.async(() -> { + throw new RuntimeException("Simulated error") + }) + + Promise recovered = promise.exceptionally(ex -> { + assertTrue(ex.getCause() instanceof RuntimeException) + return -1 + }) + + Assertions.assertEquals(-1, AsyncHelper.await(recovered)) + } + + @Test + @DisplayName("should handle async/await with data transformation pipeline") + void testAsyncDataPipeline() { + Promise> result = AsyncHelper.async(() -> List.of(1, 2, 3, 4, 5)) + .thenApply(list -> { + List doubled = new ArrayList<>() + for (Integer n : list) { + doubled.add(n * 2) + } + return doubled + }) + .thenApply(list -> { + List filtered = new ArrayList<>() + for (Integer n : list) { + if (n > 5) { + filtered.add(n) + } + } + return filtered + }) + + List expected = List.of(6, 8, 10) + assertEquals(expected, AsyncHelper.await(result)) + } + + @Test + @DisplayName("should handle nested async operations") + void testNestedAsync() { + Promise outer = AsyncHelper.async(() -> { + Promise inner = AsyncHelper.async(() -> 5) + return AsyncHelper.await(inner) * 2 + }) + + Assertions.assertEquals(10, AsyncHelper.await(outer)) + } + } + + @Nested + @DisplayName("Advanced async patterns") + class AdvancedPatternsTest { + + @Test + @DisplayName("should handle async retry pattern") + void testAsyncRetryPattern() { + AtomicInteger attempts = new AtomicInteger(0) + + Promise result = AsyncHelper.async(() -> { + int count = attempts.incrementAndGet() + if (count < 3) { + throw new RuntimeException("Not ready yet") + } + return "Success after " + count + " attempts" + }).exceptionallyCompose(ex -> { + sleep(50) + return AsyncHelper.async(() -> { + int count = attempts.incrementAndGet() + return "Success after " + count + " attempts" + }) + }) + + String finalResult = AsyncHelper.await(result) + assertTrue(finalResult.contains("Success")) + assertTrue(attempts.get() >= 2) + } + + @Test + @DisplayName("should handle async timeout pattern") + @Timeout(1) + void testAsyncTimeoutPattern() throws Exception { + Promise slowTask = AsyncHelper.async(() -> { + sleep(5000) + return "completed" + }) + + Promise timeoutFuture = slowTask + .orTimeout(200, TimeUnit.MILLISECONDS) + .exceptionally(ex -> "timeout") + + assertEquals("timeout", timeoutFuture.get()) + } + + @Test + @DisplayName("should handle async map-reduce pattern") + void testAsyncMapReducePattern() { + List numbers = List.of(1, 2, 3, 4, 5) + + List> squarePromises = new ArrayList<>() + for (Integer n : numbers) { + def tmpN = n + squarePromises.add(AsyncHelper.async(() -> tmpN * tmpN)) + } + + int total = 0 + for (Promise p : squarePromises) { + total += AsyncHelper.await(p) + } + + assertEquals(55, total) + } + + @Test + @DisplayName("should handle async combine operations") + void testAsyncCombine() { + Promise p1 = AsyncHelper.async(() -> 10) + Promise p2 = AsyncHelper.async(() -> 20) + + Promise combined = p1.thenCombine(p2, Integer::sum) + + Assertions.assertEquals(30, AsyncHelper.await(combined)) + } + + @Test + @DisplayName("should handle async compose operations") + void testAsyncCompose() { + Promise initial = AsyncHelper.async(() -> 5) + + Promise composed = initial.thenCompose(n -> + AsyncHelper.async(() -> n * 3) + ) + + Assertions.assertEquals(15, AsyncHelper.await(composed)) + } + } + + @Nested + @DisplayName("Real-world scenarios") + class RealWorldScenariosTest { + + @Test + @DisplayName("should simulate API call with data transformation") + void testSimulateApiCall() { + Promise apiCall = AsyncHelper.async(() -> { + sleep(50) + return "{\"userId\":1,\"name\":\"John\"}" + }) + + Promise transformed = apiCall.thenApply(json -> + json.replace("John", "Jane") + ) + + String result = AsyncHelper.await(transformed) + assertTrue(result.contains("Jane")) + assertFalse(result.contains("John")) + } + + @Test + @DisplayName("should handle multiple parallel API calls") + @Timeout(1) + void testMultipleParallelApiCalls() { + Promise userApi = AsyncHelper.async(() -> { + sleep(100) + return "User data" + }) + + Promise orderApi = AsyncHelper.async(() -> { + sleep(100) + return "Order data" + }) + + Promise productApi = AsyncHelper.async(() -> { + sleep(100) + return "Product data" + }) + + Promise combined = userApi.thenCombine(orderApi, (u, o) -> u + ", " + o) + .thenCombine(productApi, (uo, p) -> uo + ", " + p) + + String result = AsyncHelper.await(combined) + assertEquals("User data, Order data, Product data", result) + } + + @Test + @DisplayName("should handle async cache pattern") + void testAsyncCachePattern() { + AtomicReference cache = new AtomicReference<>() + AtomicInteger fetchCount = new AtomicInteger(0) + + Promise getCachedData = AsyncHelper.async(() -> { + if (cache.get() != null) { + return cache.get() + } + fetchCount.incrementAndGet() + sleep(50) + String data = "Fresh data" + cache.set(data) + return data + }) + + String firstCall = AsyncHelper.await(getCachedData) + assertEquals("Fresh data", firstCall) + assertEquals(1, fetchCount.get()) + + Promise secondCall = AsyncHelper.async(() -> cache.get()) + assertEquals("Fresh data", AsyncHelper.await(secondCall)) + assertEquals(1, fetchCount.get()) + } + + @Test + @DisplayName("should handle async queue processing") + @Timeout(2) + void testAsyncQueueProcessing() { + List queue = List.of(10, 20, 30, 40, 50) + AtomicInteger processed = new AtomicInteger(0) + + List> tasks = new ArrayList<>() + for (Integer item : queue) { + def tmp = item + tasks.add(AsyncHelper.async(() -> { + sleep(50) + processed.addAndGet(tmp) + return + })) + } + + Promise allProcessed = SimplePromise.allOf(tasks as Promise[]) + AsyncHelper.await(allProcessed) + + assertEquals(150, processed.get()) + } + + @Test + @DisplayName("should handle async batch processing") + void testAsyncBatchProcessing() { + List batch = List.of(1, 2, 3, 4, 5) + + Promise batchSum = AsyncHelper.async(() -> { + int sum = 0 + for (Integer item : batch) { + sum += AsyncHelper.await(AsyncHelper.async(() -> item * 2)) + } + return sum + }) + + Assertions.assertEquals(30, AsyncHelper.await(batchSum)) + } + } + + @Nested + @DisplayName("Error handling scenarios") + class ErrorHandlingTest { + + @Test + @DisplayName("should propagate exceptions in async chain") + void testExceptionPropagation() { + Promise promise = AsyncHelper.async(() -> 10) + .thenApply(n -> { + throw new RuntimeException("Chain error") + }) + + assertThrows(AwaitException.class, () -> AsyncHelper.await(promise)) + } + + @Test + @DisplayName("should handle exception in async operation") + void testAsyncException() { + Promise promise = AsyncHelper.async(() -> { + throw new IllegalStateException("Async error") + }) + + assertThrows(AwaitException.class, () -> AsyncHelper.await(promise)) + } + + @Test + @DisplayName("should recover from exception with fallback") + void testExceptionRecovery() { + Promise promise = AsyncHelper.async(() -> { + throw new RuntimeException("Error") + }).handle((result, ex) -> { + if (ex != null) { + return "Fallback value" + } + return result + }) + + assertEquals("Fallback value", AsyncHelper.await(promise)) + } + + @Test + @DisplayName("should handle exception with exceptionally") + void testExceptionallyHandler() { + Promise promise = AsyncHelper.async(() -> { + throw new RuntimeException("Error") + }).exceptionally(ex -> { + assertTrue(ex.getCause() instanceof RuntimeException) + return 999 + }) + + Assertions.assertEquals(999, AsyncHelper.await(promise)) + } + + @Test + @DisplayName("should handle null pointer exception") + void testNullPointerException() { + Promise promise = AsyncHelper.async(() -> { + String s = null + return s.length() + "" + }) + + assertThrows(AwaitException.class, () -> AsyncHelper.await(promise)) + } + } + + @Nested + @DisplayName("Thread safety and concurrency") + class ConcurrencyTest { + + @Test + @DisplayName("should handle concurrent async operations") + @Timeout(2) + void testConcurrentOperations() throws InterruptedException { + AtomicInteger counter = new AtomicInteger(0) + CountDownLatch latch = new CountDownLatch(100) + + for (int i = 0; i < 100; i++) { + AsyncHelper.async(() -> { + counter.incrementAndGet() + latch.countDown() + return null + }) + } + + latch.await(1, TimeUnit.SECONDS) + assertEquals(100, counter.get()) + } + + @Test + @DisplayName("should handle shared state correctly") + void testSharedState() { + AtomicInteger shared = new AtomicInteger(0) + + List> promises = new ArrayList<>() + for (int i = 0; i < 10; i++) { + promises.add(AsyncHelper.async(() -> { + shared.incrementAndGet() + return + })) + } + + for (Promise p : promises) { + AsyncHelper.await(p) + } + + assertEquals(10, shared.get()) + } + } + + @Nested + @DisplayName("Edge cases") + class EdgeCasesTest { + + @Test + @DisplayName("should handle empty result") + void testEmptyResult() { + Promise promise = AsyncHelper.async(() -> null) + assertNull(AsyncHelper.await(promise)) + } + + @Test + @DisplayName("should handle boolean results") + void testBooleanResults() { + Promise truePromise = AsyncHelper.async(() -> true) + Promise falsePromise = AsyncHelper.async(() -> false) + + Assertions.assertTrue(AsyncHelper.await(truePromise)) + Assertions.assertFalse(AsyncHelper.await(falsePromise)) + } + + @Test + @DisplayName("should handle long running tasks") + @Timeout(2) + void testLongRunningTask() { + Promise promise = AsyncHelper.async(() -> { + sleep(500) + return "Long task completed" + }) + + assertEquals("Long task completed", AsyncHelper.await(promise)) + } + + @Test + @DisplayName("should handle immediate completion") + void testImmediateCompletion() { + long start = System.currentTimeMillis() + Promise promise = AsyncHelper.async(() -> "Immediate") + String result = AsyncHelper.await(promise) + long duration = System.currentTimeMillis() - start + + assertEquals("Immediate", result) + assertTrue(duration < 100) + } + } + + + @Test + void testAwaitWithAwaitableReturnsValue() { + def awaitable = new SimpleAwaitable(42) + // ensure we call the Object overload at runtime + def result = AsyncHelper.await((Object) awaitable) + assertEquals(42, result) + assertTrue(awaitable.awaited) + } + + @Test + void testAwaitThrowsOnNonAwaitable() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, { + AsyncHelper.await(new Object()) + }) + assertTrue(ex.message.contains("does not implement groovy.util.concurrent.async.Awaitable")) + } + + @Test + void testAwaitReturnNullOnNull() { + assertNull(AsyncHelper.await((Object) null)) + } + + static class SimpleAwaitable implements Awaitable { + private final T value + boolean awaited = false + + SimpleAwaitable(T value) { this.value = value } + + @Override + T await() { + awaited = true + return value + } + } +} diff --git a/src/test/groovy/groovy/util/concurrent/async/SimplePromiseTest.groovy b/src/test/groovy/groovy/util/concurrent/async/SimplePromiseTest.groovy new file mode 100644 index 00000000000..eecbab907a8 --- /dev/null +++ b/src/test/groovy/groovy/util/concurrent/async/SimplePromiseTest.groovy @@ -0,0 +1,1192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.util.concurrent.async + +import groovy.transform.CompileStatic +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertFalse +import static org.junit.jupiter.api.Assertions.assertNotNull +import static org.junit.jupiter.api.Assertions.assertNotSame +import static org.junit.jupiter.api.Assertions.assertNull +import static org.junit.jupiter.api.Assertions.assertSame +import static org.junit.jupiter.api.Assertions.assertThrows +import static org.junit.jupiter.api.Assertions.assertTrue + +@CompileStatic +class SimplePromiseTest { + + @Nested + @DisplayName("Factory Methods") + class FactoryMethodsTest { + @Test + @DisplayName("should create promise from CompletableFuture") + void testOfWithCompletableFuture() { + Promise promise = SimplePromise.of(() -> "test") + + assertNotNull(promise) + assertEquals("test", promise.await()) + } + + @Test + @DisplayName("should create empty promise") + void testOfEmpty() { + Promise promise = SimplePromise.of() + + assertNotNull(promise) + assertFalse(promise.isDone()) + } + } + + @Nested + @DisplayName("CompletedPromise") + class SimplePromiseCompletedPromiseTest { + @Test + void testCompletedWithValue() throws Exception { + Promise p = SimplePromise.completed(42) + + assertTrue(p.isDone(), "promise should be done") + assertFalse(p.isCancelled(), "should not be cancelled") + assertFalse(p.isCompletedExceptionally(), "should not be completed exceptionally") + + // blocking and non-blocking retrievals + assertEquals(42, p.await()) + assertEquals(42, p.await()) + assertEquals(42, p.get()) + assertEquals(42, p.getNow(0)) + assertEquals(42, p.toCompletableFuture().get()) + assertEquals(42, p.get(1, TimeUnit.SECONDS)) + + // attempting to cancel or complete an already-completed promise returns false + assertFalse(p.cancel(true)) + assertFalse(p.complete(99)) + } + + @Test + void testCompletedWithNull() throws Exception { + Promise p = SimplePromise.completed(null) + + assertTrue(p.isDone()) + assertFalse(p.isCompletedExceptionally()) + assertNull(p.await()) + assertNull(p.await()) + assertNull(p.getNow("absent")) + assertNull(p.toCompletableFuture().get()) + } + + @Test + void testThenApplyOnCompleted() { + Promise p = SimplePromise.completed(5) + + Promise mapped = p.thenApply(x -> x * 11) + + // mapping should produce the expected result immediately + assertEquals(55, mapped.await()) + assertFalse(mapped.isCompletedExceptionally()) + } + + @Test + void testCompleteReturnsFalseAndObtrudeValueOverrides() { + Promise p = SimplePromise.completed(1) + + // cannot complete again + assertFalse(p.complete(2)) + assertEquals(1, p.await()) + + // obtrudeValue forcibly changes the stored value + p.obtrudeValue(2) + assertEquals(2, p.await()) + } + + @Test + void testObtrudeExceptionMakesPromiseExceptional() { + Promise p = SimplePromise.completed(3) + + p.obtrudeException(new IllegalStateException("boom")) + + assertTrue(p.isCompletedExceptionally()) + + // join throws CompletionException + assertThrows(CompletionException.class, p::join) + + // await wraps thrown exception in AwaitException + assertThrows(AwaitException.class, p::await) + } + } + + @Nested + @DisplayName("allOf and anyOf Methods") + class SimplePromiseAllAnyTest { + + private ExecutorService executor = Executors.newSingleThreadExecutor() + + @AfterEach + void tearDown() { + executor.shutdownNow() + } + + @Test + void testAllOfCompletesWhenAllComplete() throws Exception { + Promise p1 = SimplePromise.completed(1) + Promise p2 = SimplePromise.completed(2) + Promise p3 = SimplePromise.of() + + // complete p3 asynchronously after a short delay + executor.submit({ + Thread.sleep(50) + p3.complete(3) + } ) + + Promise all = SimplePromise.allOf(p1, p2, p3) + + // should complete when the last promise completes + assertDoesNotThrow((Executable) () -> { all.await() }) + assertTrue(all.isDone()) + // allOf semantics mirror CompletableFuture.allOf (no aggregated result), expect null from join + assertNull(all.await()) + } + + @Test + void testAllOfPropagatesExceptionIfAnyFail() { + // make the successful promise already completed so allOf can evaluate immediately + Promise good = SimplePromise.completed(7) + Promise bad = SimplePromise.of() + bad.completeExceptionally(new RuntimeException("fail")) + + Promise all = SimplePromise.allOf(good, bad) + + assertTrue(all.isDone()) + assertThrows(AwaitException.class, { all.await() }) + } + + @Test + void testAnyOfCompletesWithFirstValue() throws Exception { + Promise slow = SimplePromise.of() + Promise fast = SimplePromise.of() + + // fast completes earlier + executor.submit({ + Thread.sleep(30) + fast.complete("fast") + } ) + + executor.submit({ + Thread.sleep(80) + slow.complete("slow") + } ) + + Promise any = SimplePromise.anyOf(slow, fast) + + assertEquals("fast", any.await()) + assertTrue(any.isDone()) + } + + @Test + void testAnyOfCompletesImmediatelyIfOneAlreadyCompleted() { + Promise completed = SimplePromise.completed(10) + Promise pending = SimplePromise.of() + + Promise any = SimplePromise.anyOf(completed, pending) + + assertEquals(10, any.await()) + assertTrue(any.isDone()) + } + + @Test + void testAnyOfPropagatesFirstExceptionIfItOccursFirst() throws Exception { + Promise exc = SimplePromise.of() + Promise value = SimplePromise.of() + + // exception happens first + executor.submit({ + Thread.sleep(20) + exc.completeExceptionally(new IllegalStateException("boom")) + } ) + + // value completes slightly later + executor.submit({ + Thread.sleep(60) + value.complete("ok") + } ) + + Promise any = SimplePromise.anyOf(exc, value) + + // first completion is exceptional -> any should be exceptional + assertThrows(AwaitException.class, { any.await() }) + } + } + + @Nested + @DisplayName("Completion Methods") + class CompletionMethodsTest { + @Test + @DisplayName("should complete with value") + void testComplete() { + Promise promise = SimplePromise.of() + boolean result = promise.complete("value") + + assertTrue(result) + assertTrue(promise.isDone()) + assertEquals("value", promise.await()) + } + + @Test + @DisplayName("should complete exceptionally") + void testCompleteExceptionally() { + Promise promise = SimplePromise.of() + Exception ex = new RuntimeException("error") + boolean result = promise.completeExceptionally(ex) + + assertTrue(result) + assertTrue(promise.isCompletedExceptionally()) + assertThrows(CompletionException.class, promise::join) + } + + @Test + @DisplayName("should complete async with supplier") + void testCompleteAsync() throws Exception { + Promise promise = SimplePromise.of() + promise.completeAsync(() -> "async-value") + + Thread.sleep(100) + assertEquals("async-value", promise.get()) + } + + @Test + @DisplayName("should complete async with supplier and executor") + void testCompleteAsyncWithExecutor() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor() + try { + Promise promise = SimplePromise.of() + promise.completeAsync(() -> "executor-value", executor) + + assertEquals("executor-value", promise.get(1, TimeUnit.SECONDS)) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should obtrude value") + void testObtrudeValue() { + Promise promise = SimplePromise.of() + promise.complete("first") + promise.obtrudeValue("second") + + assertEquals("second", promise.await()) + } + + @Test + @DisplayName("should obtrude exception") + void testObtrudeException() { + Promise promise = SimplePromise.of() + promise.complete("value") + RuntimeException ex = new RuntimeException("obtruded") + promise.obtrudeException(ex) + + CompletionException thrown = assertThrows(CompletionException.class, promise::join) + assertEquals(ex, thrown.getCause()) + } + } + + @Nested + @DisplayName("Transformation Methods") + class TransformationMethodsTest { + @Test + @DisplayName("should apply function with thenApply") + void testThenApply() { + Promise promise = SimplePromise.completed(5) + Promise result = promise.thenApply(n -> "Number: " + n) + + assertEquals("Number: 5", result.await()) + } + + @Test + @DisplayName("should apply function async with thenApplyAsync") + void testThenApplyAsync() { + Promise promise = SimplePromise.completed(10) + Promise result = promise.thenApplyAsync(n -> n * 2) + + assertEquals(20, result.await()) + } + + @Test + @DisplayName("should compose with thenCompose") + void testThenCompose() { + Promise promise = SimplePromise.completed(3) + Promise result = promise.thenCompose(n -> SimplePromise.completed(n * 3)) + + assertEquals(9, result.await()) + } + + @Test + @DisplayName("should handle with bifunction") + void testHandle() { + Promise promise = SimplePromise.completed(5) + Promise result = promise.handle((val, ex) -> ex == null ? "Success: " + val : "Error") + + assertEquals("Success: 5", result.await()) + } + + @Test + @DisplayName("should handle exception with handle") + void testHandleWithException() { + Promise promise = SimplePromise.of() + promise.completeExceptionally(new RuntimeException("error")) + Promise result = promise.handle((val, ex) -> ex != null ? "Handled: " + ex.getMessage() : "OK") + + assertEquals("Handled: error", result.await()) + } + } + + @Nested + @DisplayName("Consumer Methods") + class ConsumerMethodsTest { + @Test + @DisplayName("should accept value with thenAccept") + void testThenAccept() { + AtomicReference ref = new AtomicReference<>() + Promise promise = SimplePromise.completed("test") + Promise result = promise.thenAccept(ref::set) + + result.await() + assertEquals("test", ref.get()) + } + + @Test + @DisplayName("should run action with thenRun") + void testThenRun() { + AtomicBoolean executed = new AtomicBoolean(false) + Promise promise = SimplePromise.completed("test") + Promise result = promise.thenRun(() -> executed.set(true)) + + result.await() + assertTrue(executed.get()) + } + + @Test + @DisplayName("should execute whenComplete") + void testWhenComplete() { + AtomicReference ref = new AtomicReference<>() + Promise promise = SimplePromise.completed("value") + Promise result = promise.whenComplete((val, ex) -> ref.set(val)) + + assertEquals("value", result.await()) + assertEquals("value", ref.get()) + } + } + + @Nested + @DisplayName("Combination Methods") + class CombinationMethodsTest { + @Test + @DisplayName("should combine two promises with thenCombine") + void testThenCombine() { + Promise p1 = SimplePromise.completed(5) + Promise p2 = SimplePromise.completed(3) + Promise result = p1.thenCombine(p2, Integer::sum) + + assertEquals(8, result.await()) + } + + @Test + @DisplayName("should accept both with thenAcceptBoth") + void testThenAcceptBoth() { + AtomicInteger sum = new AtomicInteger(0) + Promise p1 = SimplePromise.completed(5) + Promise p2 = SimplePromise.completed(7) + Promise result = p1.thenAcceptBoth(p2, (a, b) -> sum.set(a + b)) + + result.await() + assertEquals(12, sum.get()) + } + + @Test + @DisplayName("should run after both complete") + void testRunAfterBoth() { + AtomicBoolean executed = new AtomicBoolean(false) + Promise p1 = SimplePromise.completed("a") + Promise p2 = SimplePromise.completed("b") + Promise result = p1.runAfterBoth(p2, () -> executed.set(true)) + + result.await() + assertTrue(executed.get()) + } + + @Test + @DisplayName("should apply to either") + void testApplyToEither() { + Promise p1 = SimplePromise.completed(1) + Promise p2 = SimplePromise.of() + Promise result = p1.applyToEither(p2, n -> n * 10) + + assertEquals(10, result.await()) + } + + @Test + @DisplayName("should accept either") + void testAcceptEither() { + AtomicInteger ref = new AtomicInteger(0) + Promise p1 = SimplePromise.completed(42) + Promise p2 = SimplePromise.of() + Promise result = p1.acceptEither(p2, ref::set) + + result.await() + assertEquals(42, ref.get()) + } + + @Test + @DisplayName("should run after either completes") + void testRunAfterEither() { + AtomicBoolean executed = new AtomicBoolean(false) + Promise p1 = SimplePromise.completed("fast") + Promise p2 = SimplePromise.of() + Promise result = p1.runAfterEither(p2, () -> executed.set(true)) + + result.await() + assertTrue(executed.get()) + } + } + + @Nested + @DisplayName("Exception Handling") + class ExceptionHandlingTest { + @Test + @DisplayName("should handle exception with exceptionally") + void testExceptionally() { + Promise promise = SimplePromise.of() + promise.completeExceptionally(new RuntimeException("error")) + Promise result = promise.exceptionally(ex -> -1) + + assertEquals(-1, result.await()) + } + + @Test + @DisplayName("should compose exception with exceptionallyCompose") + void testExceptionallyCompose() { + Promise promise = SimplePromise.of() + promise.completeExceptionally(new RuntimeException("error")) + Promise result = promise.exceptionallyCompose(ex -> SimplePromise.completed(99)) + + assertEquals(99, result.await()) + } + + @Test + @DisplayName("should handle exception async with exceptionallyAsync") + void testExceptionallyAsync() { + Promise promise = SimplePromise.of() + promise.completeExceptionally(new RuntimeException("async-error")) + Promise result = promise.exceptionallyAsync(ex -> "recovered") + + assertEquals("recovered", result.await()) + } + } + + @Nested + @DisplayName("Timeout and Cancellation") + class TimeoutAndCancellationTest { + @Test + @DisplayName("should timeout with orTimeout") + void testOrTimeout() { + Promise promise = SimplePromise.of() + Promise result = promise.orTimeout(100, TimeUnit.MILLISECONDS) + + assertThrows(CompletionException.class, result::join) + } + + @Test + @DisplayName("should complete on timeout with completeOnTimeout") + void testCompleteOnTimeout() { + Promise promise = SimplePromise.of() + Promise result = promise.completeOnTimeout("default", 100, TimeUnit.MILLISECONDS) + + assertEquals("default", result.await()) + } + + @Test + @DisplayName("should cancel promise") + void testCancel() { + Promise promise = SimplePromise.of() + boolean cancelled = promise.cancel(true) + + assertTrue(cancelled) + assertTrue(promise.isCancelled()) + assertTrue(promise.isDone()) + } + } + + @Nested + @DisplayName("Retrieval Methods") + class RetrievalMethodsTest { + @Test + @DisplayName("should get value with blocking get") + void testGet() throws Exception { + Promise promise = SimplePromise.completed("value") + assertEquals("value", promise.get()) + } + + @Test + @DisplayName("should get value with timeout") + void testGetWithTimeout() throws Exception { + Promise promise = SimplePromise.completed("value") + assertEquals("value", promise.get(1, TimeUnit.SECONDS)) + } + + @Test + @DisplayName("should join and return value") + void testJoin() { + Promise promise = SimplePromise.completed("joined") + assertEquals("joined", promise.join()) + } + + @Test + @DisplayName("should get now or default value") + void testGetNow() { + Promise promise = SimplePromise.of() + assertEquals("default", promise.getNow("default")) + + promise.complete("actual") + assertEquals("actual", promise.getNow("default")) + } + + @Test + @DisplayName("should await and return value") + void testAwait() { + Promise promise = SimplePromise.of(() -> "awaited") + assertEquals("awaited", promise.await()) + } + + @Test + @DisplayName("should throw AwaitException on await failure") + void testAwaitException() { + Promise promise = SimplePromise.of() + promise.completeExceptionally(new RuntimeException("error")) + + assertThrows(AwaitException.class, promise::await) + } + } + + @Nested + @DisplayName("Status and Conversion") + class StatusAndConversionTest { + @Test + @DisplayName("should check if done") + void testIsDone() { + Promise promise = SimplePromise.of() + assertFalse(promise.isDone()) + + promise.complete("value") + assertTrue(promise.isDone()) + } + + @Test + @DisplayName("should check if cancelled") + void testIsCancelled() { + Promise promise = SimplePromise.of() + assertFalse(promise.isCancelled()) + + promise.cancel(false) + assertTrue(promise.isCancelled()) + } + + @Test + @DisplayName("should check if completed exceptionally") + void testIsCompletedExceptionally() { + Promise promise = SimplePromise.of() + assertFalse(promise.isCompletedExceptionally()) + + promise.completeExceptionally(new RuntimeException()) + assertTrue(promise.isCompletedExceptionally()) + } + + @Test + @DisplayName("should convert to CompletableFuture") + void testToCompletableFuture() { + CompletableFuture cf = CompletableFuture.completedFuture("test") + Promise promise = SimplePromise.of(cf) + + assertSame(cf, promise.toCompletableFuture()) + } + + @Test + @DisplayName("should convert to Promise") + void testToPromise() { + Promise promise = SimplePromise.of() + assertSame(promise, promise.toPromise()) + } + + @Test + @DisplayName("should copy promise") + void testCopy() { + Promise original = SimplePromise.completed("original") + Promise copy = original.copy() + + assertNotSame(original, copy) + assertEquals("original", copy.await()) + } + + @Test + @DisplayName("should get number of dependents") + void testGetNumberOfDependents() { + Promise promise = SimplePromise.of() + assertEquals(0, promise.getNumberOfDependents()) + + promise.thenApply(String::toUpperCase) + assertTrue(promise.getNumberOfDependents() > 0) + } + + @Test + @DisplayName("should get default executor") + void testDefaultExecutor() { + Promise promise = SimplePromise.of() + assertNotNull(promise.defaultExecutor()) + } + } + + @Nested + @DisplayName("Additional Coverage Tests") + class AdditionalCoverageTest { + + @Test + @DisplayName("should apply function async with executor") + void testThenApplyAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + try { + Promise promise = SimplePromise.completed(5) + Promise result = promise.thenApplyAsync(n -> "Value: " + n, executor) + + assertEquals("Value: 5", result.await()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should accept value async with executor") + void testThenAcceptAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + AtomicReference ref = new AtomicReference<>() + try { + Promise promise = SimplePromise.completed("test") + Promise result = promise.thenAcceptAsync(ref::set, executor) + + result.await() + assertEquals("test", ref.get()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should run action async with executor") + void testThenRunAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + AtomicBoolean executed = new AtomicBoolean(false) + try { + Promise promise = SimplePromise.completed("test") + Promise result = promise.thenRunAsync(() -> executed.set(true), executor) + + result.await() + assertTrue(executed.get()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should compose async with executor") + void testThenComposeAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + try { + Promise promise = SimplePromise.completed(3) + Promise result = promise.thenComposeAsync(n -> SimplePromise.completed((n * 4)), executor) + + assertEquals(12, result.await()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should handle async with executor") + void testHandleAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + try { + Promise promise = SimplePromise.completed(7) + Promise result = promise.handleAsync((val, ex) -> ex == null ? "Result: " + val : "Error", executor) + + assertEquals("Result: 7", result.await()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should execute whenCompleteAsync with executor") + void testWhenCompleteAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + AtomicReference ref = new AtomicReference<>() + try { + Promise promise = SimplePromise.completed("async") + Promise result = promise.whenCompleteAsync((val, ex) -> ref.set(val), executor) + + assertEquals("async", result.await()) + assertEquals("async", ref.get()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should combine async with executor") + void testThenCombineAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + try { + Promise p1 = SimplePromise.completed(10) + Promise p2 = SimplePromise.completed(20) + Promise result = p1.thenCombineAsync(p2, Integer::sum, executor) + + assertEquals(30, result.await()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should accept both async with executor") + void testThenAcceptBothAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + AtomicInteger sum = new AtomicInteger(0) + try { + Promise p1 = SimplePromise.completed(15) + Promise p2 = SimplePromise.completed(25) + Promise result = p1.thenAcceptBothAsync(p2, (a, b) -> sum.set(a + b), executor) + + result.await() + assertEquals(40, sum.get()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should run after both async with executor") + void testRunAfterBothAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + AtomicBoolean executed = new AtomicBoolean(false) + try { + Promise p1 = SimplePromise.completed("x") + Promise p2 = SimplePromise.completed("y") + Promise result = p1.runAfterBothAsync(p2, () -> executed.set(true), executor) + + result.await() + assertTrue(executed.get()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should apply to either async with executor") + void testApplyToEitherAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + try { + Promise p1 = SimplePromise.completed(100) + Promise p2 = SimplePromise.of() + Promise result = p1.applyToEitherAsync(p2, n -> n * 2, executor) + + assertEquals(200, result.await()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should accept either async with executor") + void testAcceptEitherAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + AtomicInteger ref = new AtomicInteger(0) + try { + Promise p1 = SimplePromise.completed(88) + Promise p2 = SimplePromise.of() + Promise result = p1.acceptEitherAsync(p2, ref::set, executor) + + result.await() + assertEquals(88, ref.get()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should run after either async with executor") + void testRunAfterEitherAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + AtomicBoolean executed = new AtomicBoolean(false) + try { + Promise p1 = SimplePromise.completed("first") + Promise p2 = SimplePromise.of() + Promise result = p1.runAfterEitherAsync(p2, () -> executed.set(true), executor) + + result.await() + assertTrue(executed.get()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should handle exception async with exceptionallyComposeAsync with executor") + void testExceptionallyComposeAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + try { + Promise promise = SimplePromise.of() + promise.completeExceptionally(new RuntimeException("error")) + Promise result = promise.exceptionallyComposeAsync(ex -> SimplePromise.completed(777), executor) + + assertEquals(777, result.await()) + } finally { + executor.shutdown() + } + } + + @Test + @DisplayName("should handle exception with exceptionallyAsync with executor") + void testExceptionallyAsyncWithExecutor() { + ExecutorService executor = Executors.newSingleThreadExecutor() + try { + Promise promise = SimplePromise.of() + promise.completeExceptionally(new RuntimeException("error")) + Promise result = promise.exceptionallyAsync(ex -> "handled", executor) + + assertEquals("handled", result.await()) + } finally { + executor.shutdown() + } + } + } + + @Nested + @DisplayName("Advanced Async Ops") + class SimplePromiseAdvancedAsyncOpsTest { + + private final ExecutorService executor = Executors.newFixedThreadPool(4) + + @AfterEach + void tearDown() { + executor.shutdownNow() + } + + @Test + void testExceptionallyComposeAsync_recoversFromException() { + Promise p = SimplePromise.of() + p.completeExceptionally(new RuntimeException("boom")) + + Promise recovered = p.exceptionallyComposeAsync({ Throwable ex -> SimplePromise.completed(99) }, executor) + + assertEquals(99, recovered.await()) + assertTrue(recovered.isDone()) + assertFalse(recovered.isCompletedExceptionally()) + } + + @Test + void testExceptionallyComposeAsync_noExceptionPassesThrough() { + Promise p = SimplePromise.completed("ok") + + Promise result = p.exceptionallyComposeAsync({ Throwable ex -> SimplePromise.completed("fallback") }, + executor) + + assertEquals("ok", result.await()) + } + + @Test + void testThenAcceptBothAsync_consumerRunsWithBothValues() { + Promise left = SimplePromise.completed(2) + Promise right = SimplePromise.of({ 3 }, executor) + + AtomicInteger sum = new AtomicInteger() + CountDownLatch latch = new CountDownLatch(1) + + Promise done = left.thenAcceptBothAsync(right, + { Integer a, Integer b -> + sum.set(a + b) + latch.countDown() + }, + executor) + + assertTrue(latch.await(2, TimeUnit.SECONDS)) + done.await() + assertEquals(5, sum.get()) + } + + @Test + void testRunAfterEitherAsync_runsWhenEitherCompletes() { + Promise fast = SimplePromise.of() + Promise slow = SimplePromise.of() + + AtomicBoolean ran = new AtomicBoolean(false) + CountDownLatch latch = new CountDownLatch(1) + + Promise r = fast.runAfterEitherAsync(slow, + { + ran.set(true) + latch.countDown() + }, + executor) + + fast.complete("first") + + assertTrue(latch.await(2, TimeUnit.SECONDS)) + r.await() + assertTrue(ran.get()) + } + + @Test + void testThenCombineAsync_combinesResultsToNewValue() { + Promise a = SimplePromise.completed(6) + Promise b = SimplePromise.of({ 7 }, executor) + + Promise combined = a.thenCombineAsync(b, + { Integer x, Integer y -> x * y }, + executor) + + assertEquals(42, combined.await()) + } + + @Test + void testHandleAsync_handlesSuccessCase() { + Promise ok = SimplePromise.completed(10) + + Promise handled = ok.handleAsync({ Integer v, Throwable ex -> + if (ex != null) return "err" + return "v:$v" as String + }, executor) + + assertEquals("v:10", handled.await()) + } + + @Test + void testHandleAsync_handlesExceptionCase() { + Promise exc = SimplePromise.of() + exc.completeExceptionally(new IllegalStateException("fail")) + + Promise recovered = exc.handleAsync({ Integer v, Throwable ex -> + if (ex != null) return "recovered" + return "v:$v" + }, executor) + + assertEquals("recovered", recovered.await()) + } + + @Test + void testAcceptEitherAsync_usesFirstCompleted() { + Promise slow = SimplePromise.of() + Promise fast = SimplePromise.of() + + AtomicReference accepted = new AtomicReference<>() + CountDownLatch latch = new CountDownLatch(1) + + Promise acceptPromise = fast.acceptEitherAsync(slow, + { String val -> + accepted.set(val) + latch.countDown() + }, + executor) + + executor.submit({ + Thread.sleep(30) + fast.complete("fast") + } ) + + executor.submit({ + Thread.sleep(80) + slow.complete("slow") + } ) + + assertTrue(latch.await(2, TimeUnit.SECONDS)) + acceptPromise.await() + assertEquals("fast", accepted.get()) + } + + @Test + void testApplyToEitherAsync_transformsFirstCompleted() { + Promise already = SimplePromise.completed("ready") + Promise pending = SimplePromise.of() + + Promise applied = already.applyToEitherAsync(pending, + { String val -> "applied:$val" as String }, executor) + + String result = applied.await() + assertTrue(result.startsWith("applied:")) + assertTrue(result.contains("ready")) + } + + @Test + void testWhenCompleteAsync_actionSeesValueAndNoThrowable() { + Promise p = SimplePromise.completed(7) + AtomicReference valueRef = new AtomicReference<>() + AtomicReference exRef = new AtomicReference<>() + CountDownLatch latch = new CountDownLatch(1) + + Promise after = p.whenCompleteAsync({ Integer v, Throwable ex -> + valueRef.set(v) + exRef.set(ex) + latch.countDown() + }, executor) + + assertTrue(latch.await(2, TimeUnit.SECONDS)) + assertEquals(7, after.await()) + assertEquals(7, valueRef.get()) + assertNull(exRef.get()) + } + + @Test + void testWhenCompleteAsync_actionSeesException() { + Promise p = SimplePromise.of() + p.completeExceptionally(new IllegalArgumentException("bad")) + + AtomicReference exRef = new AtomicReference<>() + CountDownLatch latch = new CountDownLatch(1) + + Promise after = p.whenCompleteAsync({ Integer v, Throwable ex -> + exRef.set(ex) + latch.countDown() + }, executor) + + assertTrue(latch.await(2, TimeUnit.SECONDS)) + assertNotNull(exRef.get()) + assertThrows(AwaitException, { after.await() }) + } + + @Test + void testThenRunAsync_runnableExecutesAfterCompletion() { + Promise p = SimplePromise.of() + AtomicBoolean ran = new AtomicBoolean(false) + CountDownLatch latch = new CountDownLatch(1) + + Promise r = p.thenRunAsync({ + ran.set(true) + latch.countDown() + }, executor) + + p.complete(100) + + assertTrue(latch.await(2, TimeUnit.SECONDS)) + r.await() + assertTrue(ran.get()) + } + + @Test + void testThenComposeAsync_chainsToAnotherPromise() { + Promise source = SimplePromise.completed(4) + + Promise composed = source.thenComposeAsync({ Integer v -> SimplePromise.completed(v * 3) }, executor) + + assertEquals(12, composed.await()) + } + + @Test + void testThenComposeAsync_chainsWithAsyncExecution() { + Promise source = SimplePromise.of({ 5 }, executor) + + Promise composed = source.thenComposeAsync({ Integer v -> SimplePromise.of({ "result:$v" as String }, executor)}, executor) + + assertEquals("result:5", composed.await()) + } + + @Test + void testThenAcceptAsync_consumerRunsWithValue() { + Promise p = SimplePromise.completed("hello") + AtomicReference seen = new AtomicReference<>() + CountDownLatch latch = new CountDownLatch(1) + + Promise r = p.thenAcceptAsync({ String val -> + seen.set(val) + latch.countDown() + }, + executor) + + assertTrue(latch.await(2, TimeUnit.SECONDS)) + r.await() + assertEquals("hello", seen.get()) + } + + @Test + void testRunAfterBothAsync_runsWhenBothComplete() { + Promise a = SimplePromise.of() + Promise b = SimplePromise.of() + + AtomicBoolean ran = new AtomicBoolean(false) + CountDownLatch latch = new CountDownLatch(1) + + Promise after = a.runAfterBothAsync(b, + { + ran.set(true) + latch.countDown() + }, executor) + + a.complete(null) + b.complete(null) + + assertTrue(latch.await(2, TimeUnit.SECONDS)) + after.await() + assertTrue(ran.get()) + } + + @Test + void testRunAfterBothAsync_waitsForBothToComplete() { + Promise slow = SimplePromise.of() + Promise fast = SimplePromise.completed(1) + + AtomicBoolean ran = new AtomicBoolean(false) + CountDownLatch latch = new CountDownLatch(1) + + Promise after = fast.runAfterBothAsync(slow, + { + ran.set(true) + latch.countDown() + }, executor) + + assertFalse(ran.get()) + + slow.complete(2) + + assertTrue(latch.await(2, TimeUnit.SECONDS)) + after.await() + assertTrue(ran.get()) + } + } + +} diff --git a/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy index 60973387db4..b49cd474412 100644 --- a/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy +++ b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy @@ -345,6 +345,13 @@ final class GroovyParserTest { doTest('core/Label_01.groovy') } + @Test + void 'groovy core - async-await'() { + doRunAndTestAntlr4('core/AsyncAwait_01x.groovy') + doRunAndTestAntlr4('core/AsyncAwait_02x.groovy') + doRunAndTestAntlr4('core/AsyncAwait_03x.groovy') + } + @Test void 'groovy core - LocalVariableDeclaration'() { doTest('core/LocalVariableDeclaration_01.groovy', [Token]) // [class org.codehaus.groovy.syntax.Token][startLine]:: 9 != 8 diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncASTTransformationTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncASTTransformationTest.groovy new file mode 100644 index 00000000000..2fd9c94501a --- /dev/null +++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncASTTransformationTest.groovy @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.codehaus.groovy.transform + + +import org.junit.jupiter.api.Test + +import static groovy.test.GroovyAssert.assertScript + +/** + * Unit tests for {@link AsyncASTTransformation}. + */ +class AsyncASTTransformationTest { + + @Test + void testAsyncAnnotation() { + ['@groovy.transform.CompileStatic', '@groovy.transform.CompileDynamic', ''].each { compileAnnotation -> + assertScript """ + $compileAnnotation + class A { + @groovy.transform.Async + String fetchName() { + return 'Daniel' + } + } + def result = new A().fetchName() + assert result instanceof groovy.util.concurrent.async.Promise + assert result.await() == 'Daniel' + assert result.done + """ + } + } + + @Test + void testAsyncAnnotationOnVoidMethod() { + ['@groovy.transform.CompileStatic', '@groovy.transform.CompileDynamic', ''].each { compileAnnotation -> + assertScript """ + $compileAnnotation + class A { + @groovy.transform.Async + String fetchName() { + return 'Daniel' + } + + @groovy.transform.Async + void processName() { + groovy.util.concurrent.async.Promise p = fetchName() + String name = await p + assert name == 'Daniel' + return + } + } + def result = new A().processName() + assert result instanceof groovy.util.concurrent.async.Promise + await result + assert result.done + """ + } + + } +}