This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch GROOVY-9381
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY-9381 by this push:
new 57cf83c9e0 GROOVY-9381: Support async/await like ES7(backend)
57cf83c9e0 is described below
commit 57cf83c9e03cf040c3cb5b75ced0627fef16c289
Author: Daniel Sun <[email protected]>
AuthorDate: Sun Oct 26 15:49:49 2025 +0900
GROOVY-9381: Support async/await like ES7(backend)
---
.../util/concurrent/async/SimplePromiseTest.groovy | 181 +++++++++++++++++++++
1 file changed, 181 insertions(+)
diff --git
a/src/test/groovy/groovy/util/concurrent/async/SimplePromiseTest.groovy
b/src/test/groovy/groovy/util/concurrent/async/SimplePromiseTest.groovy
index 02f2e0fcf0..1e43f53079 100644
--- a/src/test/groovy/groovy/util/concurrent/async/SimplePromiseTest.groovy
+++ b/src/test/groovy/groovy/util/concurrent/async/SimplePromiseTest.groovy
@@ -19,9 +19,11 @@
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
@@ -32,10 +34,12 @@ 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
@@ -66,6 +70,183 @@ class SimplePromiseTest {
}
}
+ @Nested
+ @DisplayName("CompletedPromise")
+ class SimplePromiseCompletedPromiseTest {
+ @Test
+ void testCompletedPromiseWithValue() throws Exception {
+ Promise<Integer> p = SimplePromise.completedPromise(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.join());
+ 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 testCompletedPromiseWithNull() throws Exception {
+ Promise<Object> p = SimplePromise.completedPromise(null);
+
+ assertTrue(p.isDone());
+ assertFalse(p.isCompletedExceptionally());
+ assertNull(p.join());
+ assertNull(p.await());
+ assertNull(p.getNow("absent"));
+ assertNull(p.toCompletableFuture().get());
+ }
+
+ @Test
+ void testThenApplyOnCompletedPromise() {
+ Promise<Integer> p = SimplePromise.completedPromise(5);
+
+ Promise<Integer> mapped = p.thenApply(x -> x * 11);
+
+ // mapping should produce the expected result immediately
+ assertEquals(55, mapped.join());
+ assertFalse(mapped.isCompletedExceptionally());
+ }
+
+ @Test
+ void testCompleteReturnsFalseAndObtrudeValueOverrides() {
+ Promise<Integer> p = SimplePromise.completedPromise(1);
+
+ // cannot complete again
+ assertFalse(p.complete(2));
+ assertEquals(1, p.join());
+
+ // obtrudeValue forcibly changes the stored value
+ p.obtrudeValue(2);
+ assertEquals(2, p.join());
+ }
+
+ @Test
+ void testObtrudeExceptionMakesPromiseExceptional() {
+ Promise<Integer> p = SimplePromise.completedPromise(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("Completion Methods")
+ class SimplePromiseAllAnyTest {
+
+ private ExecutorService executor = Executors.newSingleThreadExecutor()
+
+ @AfterEach
+ void tearDown() {
+ executor.shutdownNow()
+ }
+
+ @Test
+ void testAllOfCompletesWhenAllComplete() throws Exception {
+ Promise<Integer> p1 =
SimplePromise.of(CompletableFuture.completedFuture(1))
+ Promise<Integer> p2 =
SimplePromise.of(CompletableFuture.completedFuture(2))
+ Promise<Integer> p3 = SimplePromise.of()
+
+ // complete p3 asynchronously after a short delay
+ executor.submit({
+ Thread.sleep(50)
+ p3.complete(3)
+ } as Runnable)
+
+ Promise<Void> all = SimplePromise.allOf(p1, p2, p3)
+
+ // should complete when the last promise completes
+ assertDoesNotThrow((Executable) () -> { all.join() })
+ assertTrue(all.isDone())
+ // allOf semantics mirror CompletableFuture.allOf (no aggregated
result), expect null from join
+ assertNull(all.join())
+ }
+
+ @Test
+ void testAllOfPropagatesExceptionIfAnyFail() {
+ Promise<Integer> good =
SimplePromise.of(CompletableFuture.completedFuture(7))
+ Promise<Integer> bad = SimplePromise.of()
+ bad.completeExceptionally(new RuntimeException("fail"))
+
+ Promise<Void> all = SimplePromise.allOf(good, bad)
+
+ assertTrue(all.isDone())
+ assertThrows(CompletionException.class, { all.join() })
+ }
+
+ @Test
+ void testAnyOfCompletesWithFirstValue() throws Exception {
+ Promise<String> slow = SimplePromise.of()
+ Promise<String> fast = SimplePromise.of()
+
+ // fast completes earlier
+ executor.submit({
+ Thread.sleep(30)
+ fast.complete("fast")
+ } as Runnable)
+
+ executor.submit({
+ Thread.sleep(80)
+ slow.complete("slow")
+ } as Runnable)
+
+ Promise<Object> any = SimplePromise.anyOf(slow, fast)
+
+ assertEquals("fast", any.join())
+ assertTrue(any.isDone())
+ }
+
+ @Test
+ void testAnyOfCompletesImmediatelyIfOneAlreadyCompleted() {
+ Promise<Integer> completed =
SimplePromise.of(CompletableFuture.completedFuture(10))
+ Promise<Integer> pending = SimplePromise.of()
+
+ Promise<Object> any = SimplePromise.anyOf(completed, pending)
+
+ assertEquals(10, any.join())
+ assertTrue(any.isDone())
+ }
+
+ @Test
+ void testAnyOfPropagatesFirstExceptionIfItOccursFirst() throws
Exception {
+ Promise<String> exc = SimplePromise.of()
+ Promise<String> value = SimplePromise.of()
+
+ // exception happens first
+ executor.submit({
+ Thread.sleep(20)
+ exc.completeExceptionally(new IllegalStateException("boom"))
+ } as Runnable)
+
+ // value completes slightly later
+ executor.submit({
+ Thread.sleep(60)
+ value.complete("ok")
+ } as Runnable)
+
+ Promise<Object> any = SimplePromise.anyOf(exc, value)
+
+ // first completion is exceptional -> any should be exceptional
+ assertThrows(CompletionException.class, { any.join() })
+ }
+ }
+
@Nested
@DisplayName("Completion Methods")
class CompletionMethodsTest {