blob: ec6da4351a81ebda2e20f0b896063e2e086b6972 [file]
/*
* 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.
*/
import org.junit.jupiter.api.Test
import static groovy.test.GroovyAssert.assertScript
final class AsyncAwaitSpecTest {
// === Getting started ===
@Test
void testBasicAsyncAwait() {
assertScript '''
// tag::basic_async_await[]
def task = async { 21 * 2 }
assert await(task) == 42
// end::basic_async_await[]
'''
}
@Test
void testDrawCard() {
assertScript '''
// tag::draw_card[]
def deck = ['2', '3', 'K', 'A']
def card = async { deck.shuffled()[0] }
println "You drew: ${await card}"
// end::draw_card[]
'''
}
@Test
void testExceptionHandling() {
assertScript '''
// tag::exception_handling[]
def drawFromEmpty = async {
throw new IllegalStateException('deck is empty')
}
try {
await drawFromEmpty
} catch (IllegalStateException e) {
// Original exception — no CompletionException wrapper
assert e.message == 'deck is empty'
}
// end::exception_handling[]
'''
}
@Test
void testCfInterop() {
assertScript '''
import java.util.concurrent.CompletableFuture
// tag::cf_interop[]
// await works with CompletableFuture from Java libraries
def future = CompletableFuture.supplyAsync { 'A' }
assert await(future) == 'A'
// end::cf_interop[]
'''
}
// === Parallel tasks and combinators ===
@Test
void testDealHands() {
assertScript '''
import groovy.concurrent.Awaitable
import java.util.concurrent.atomic.AtomicInteger
// tag::deal_hands[]
// Deal cards to three players concurrently
def deck = [*1..9,'J','Q','K','A'].collectMany { rank ->
['','','',''].collect { suit -> "$rank$suit" }
}.shuffled()
def index = new AtomicInteger()
def draw5 = { -> def i = index.getAndAdd(5); deck[i..i+4] }
def alice = async { draw5() }
def bob = async { draw5() }
def carol = async { draw5() }
def (a, b, c) = await Awaitable.all(alice, bob, carol)
assert a.size() == 5 && b.size() == 5 && c.size() == 5
// end::deal_hands[]
'''
}
@Test
void testMultiArgAwait() {
assertScript '''
// tag::multi_arg_await[]
def a = async { 1 }
def b = async { 2 }
def c = async { 3 }
// Parenthesized multi-arg await (sugar for Awaitable.all):
def results = await(a, b, c)
assert results == [1, 2, 3]
// end::multi_arg_await[]
'''
}
@Test
void testFastestServer() {
assertScript '''
import groovy.concurrent.Awaitable
// tag::fastest_server[]
// Race two servers — use whichever responds first
def primary = async { Thread.sleep(200); 'primary-response' }
def fallback = async { 'fallback-response' }
def response = await Awaitable.any(primary, fallback)
assert response == 'fallback-response'
// end::fastest_server[]
'''
}
@Test
void testFirstSuccess() {
assertScript '''
import groovy.concurrent.Awaitable
// tag::first_success[]
// Try multiple sources — use the first that succeeds
def failing = async { throw new RuntimeException('server down') }
def succeeding = async { 'card-data-from-cache' }
def result = await Awaitable.first(failing, succeeding)
assert result == 'card-data-from-cache'
// end::first_success[]
'''
}
@Test
void testAllSettled() {
assertScript '''
import groovy.concurrent.Awaitable
// tag::all_settled[]
def save1 = async { 42 }
def save2 = async { throw new RuntimeException('db error') }
def results = await Awaitable.allSettled(save1, save2)
assert results[0].success && results[0].value == 42
assert !results[1].success && results[1].error.message == 'db error'
// end::all_settled[]
'''
}
// === Generators and for await ===
@Test
void testDeckGenerator() {
assertScript '''
// tag::deck_generator[]
def dealCards = async {
def suits = ['', '', '', '']
def ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
for (suit in suits) {
for (rank in ranks) {
yield return "$rank$suit"
}
}
}
def cards = dealCards.collect()
assert cards.size() == 52
assert cards.first() == 'A'
assert cards.last() == 'K'
// end::deck_generator[]
'''
}
@Test
void testForAwaitGenerator() {
assertScript '''
// tag::for_await_generator[]
def topCards = async {
for (card in ['A', 'K', 'Q']) {
yield return card
}
}
def hand = []
for await (card in topCards) {
hand << card
}
assert hand == ['A', 'K', 'Q']
// end::for_await_generator[]
'''
}
@Test
void testForAwaitPlainCollection() {
assertScript '''
// tag::for_await_collection[]
def results = []
for await (card in ['A', 'K', 'Q']) {
results << card
}
assert results == ['A', 'K', 'Q']
// end::for_await_collection[]
'''
}
@Test
void testGeneratorRegularFor() {
assertScript '''
// tag::generator_regular_for[]
def scores = async {
for (s in [100, 250, 75]) { yield return s }
}
// Generators return Iterable — regular for and collect work
assert scores.collect { it * 2 } == [200, 500, 150]
// end::generator_regular_for[]
'''
}
// === Channels ===
@Test
void testChannel() {
assertScript '''
import groovy.concurrent.AsyncChannel
// tag::channel[]
def cardStream = AsyncChannel.create(3)
// Dealer — sends cards concurrently
async {
for (card in ['A', 'K', 'Q', 'J']) {
await cardStream.send(card)
}
cardStream.close()
}
// Player — receives cards as they arrive
def hand = []
for await (card in cardStream) {
hand << card
}
assert hand == ['A', 'K', 'Q', 'J']
// end::channel[]
'''
}
// === Defer ===
@Test
void testDeferBasic() {
assertScript '''
// tag::defer_basic[]
def log = []
def task = async {
log << 'open connection'
defer { log << 'close connection' }
log << 'open transaction'
defer { log << 'close transaction' }
log << 'save game state'
'saved'
}
assert await(task) == 'saved'
// Deferred actions run in LIFO order — last registered, first to run
assert log == ['open connection', 'open transaction', 'save game state',
'close transaction', 'close connection']
// end::defer_basic[]
'''
}
@Test
void testDeferOnException() {
assertScript '''
// tag::defer_exception[]
def cleaned = false
def task = async {
defer { cleaned = true }
throw new RuntimeException('save failed')
}
try {
await task
} catch (RuntimeException e) {
assert e.message == 'save failed'
}
// Deferred actions run even when an exception occurs
assert cleaned
// end::defer_exception[]
'''
}
// === Structured concurrency ===
@Test
void testTournamentScope() {
assertScript '''
import groovy.concurrent.AsyncScope
// tag::structured_concurrency[]
// Run a tournament round — all tables play concurrently
def results = AsyncScope.withScope { scope ->
def table1 = scope.async { [winner: 'Alice', score: 320] }
def table2 = scope.async { [winner: 'Bob', score: 280] }
def table3 = scope.async { [winner: 'Carol', score: 410] }
[await(table1), await(table2), await(table3)]
}
// All tables guaranteed complete when withScope returns
assert results.size() == 3
assert results.max { it.score }.winner == 'Carol'
// end::structured_concurrency[]
'''
}
@Test
void testScopeFailFast() {
assertScript '''
import groovy.concurrent.AsyncScope
// tag::scope_fail_fast[]
try {
AsyncScope.withScope { scope ->
scope.async { Thread.sleep(5000); 'still playing' }
scope.async { throw new RuntimeException('player disconnected') }
}
} catch (RuntimeException e) {
// First failure cancels all siblings and propagates
assert e.message == 'player disconnected'
}
// end::scope_fail_fast[]
'''
}
@Test
void testScopeWaitsForAll() {
assertScript '''
import groovy.concurrent.AsyncScope
import java.util.concurrent.atomic.AtomicInteger
// tag::scope_waits[]
def completed = new AtomicInteger(0)
AsyncScope.withScope { scope ->
3.times { scope.async { Thread.sleep(50); completed.incrementAndGet() } }
}
// All children have completed — even without explicit await
assert completed.get() == 3
// end::scope_waits[]
'''
}
// === Timeouts and delays ===
@Test
void testTimeout() {
assertScript '''
import groovy.concurrent.Awaitable
import java.util.concurrent.TimeoutException
// tag::timeout[]
def slowPlayer = async { Thread.sleep(5000); 'finally played' }
try {
await Awaitable.orTimeoutMillis(slowPlayer, 100)
} catch (TimeoutException e) {
// Player took too long — turn forfeited
assert true
}
// end::timeout[]
'''
}
@Test
void testTimeoutFallback() {
assertScript '''
import groovy.concurrent.Awaitable
// tag::timeout_fallback[]
def slowPlayer = async { Thread.sleep(5000); 'deliberate move' }
def move = await Awaitable.completeOnTimeoutMillis(slowPlayer, 'auto-pass', 100)
assert move == 'auto-pass'
// end::timeout_fallback[]
'''
}
@Test
void testDelay() {
assertScript '''
import groovy.concurrent.Awaitable
// tag::delay[]
long start = System.currentTimeMillis()
await Awaitable.delay(100) // pause without blocking a thread
assert System.currentTimeMillis() - start >= 90
// end::delay[]
'''
}
// === JDK Integration ===
@Test
void testJdkIntegration() {
assertScript '''
import com.sun.net.httpserver.HttpServer
import groovy.http.HttpBuilder
import java.nio.charset.StandardCharsets
import java.nio.file.Files
// setup: temp file containing a password
def etcPassword = Files.createTempFile('passwords', '.txt')
Files.writeString(etcPassword, 'admin:sEcrEt\\nroot:P4ssw0rd')
// setup: fake bank HTTP server
def server = HttpServer.create(new InetSocketAddress('127.0.0.1', 0), 0)
server.createContext('/') { exchange ->
def body = '<html>SUCCESS</html>'
byte[] bytes = body.getBytes(StandardCharsets.UTF_8)
exchange.responseHeaders.add('Content-Type', 'text/html')
exchange.sendResponseHeaders(200, bytes.length)
exchange.responseBody.withCloseable { it.write(bytes) }
}
server.start()
def bankUrl = "http://127.0.0.1:${server.address.port}"
// setup: portable echo command (Windows has no echo executable — it's a cmd built-in)
def echoCmd = System.getProperty('os.name').toLowerCase().startsWith('windows')
? ['cmd', '/c', 'echo Hello from Groovy']
: ['echo', 'Hello from Groovy']
try {
// tag::jdk_integration[]
// Task 1: Run a process and await its completion
def proc = async {
def p = echoCmd.execute() // echo Hello from Groovy
await p.onExit()
p.text.trim()
}
// Task 2: Read a file asynchronously
def fileContent = async {
await etcPassword.textAsync
}
// Task 3: Fetch a web page using Groovy's HttpBuilder
def webContent = async {
def http = HttpBuilder.http { baseUri bankUrl }
http.get('/withdraw/100').body
}
// All three tasks run concurrently — collect results
var (echo, file, html) = await proc, fileContent, webContent
assert echo == 'Hello from Groovy'
assert file =~ /sEcrEt/
assert html == '<html>SUCCESS</html>'
// end::jdk_integration[]
} finally {
server.stop(0)
Files.delete(etcPassword)
}
'''
}
// === Async closure factories ===
@Test
void testClosureFactory() {
assertScript '''
import groovy.concurrent.Awaitable
// tag::closure_factory[]
// A method that returns an async task — a simple factory
def dealCard(List deck) {
async { deck.shuffled()[0] }
}
def numPlayers = 3
// each player gets a card from their own deck
def cards = (1..numPlayers).collect { dealCard(['A♠', 'K♥', 'Q♦', 'J♣']) }
def hands = await Awaitable.all(*cards)
assert hands.size() == 3
assert hands.every { it in ['A♠', 'K♥', 'Q♦', 'J♣'] }
// end::closure_factory[]
'''
}
@Test
void testAwaitCompletableFutureNoAmbiguity() {
// Verifies that await works directly with HttpClient.sendAsync()
// which returns a CompletableFuture (implements both CompletionStage and Future)
assertScript '''
import com.sun.net.httpserver.HttpServer
import java.nio.charset.StandardCharsets
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
def server = HttpServer.create(new InetSocketAddress('127.0.0.1', 0), 0)
server.createContext('/') { exchange ->
byte[] bytes = 'OK'.getBytes(StandardCharsets.UTF_8)
exchange.sendResponseHeaders(200, bytes.length)
exchange.responseBody.withCloseable { it.write(bytes) }
}
server.start()
def url = "http://127.0.0.1:${server.address.port}"
try {
def result = async {
def client = HttpClient.newHttpClient()
def req = HttpRequest.newBuilder().uri(URI.create(url)).build()
def resp = await client.sendAsync(req, HttpResponse.BodyHandlers.ofString())
resp.body()
}
assert await(result) == 'OK'
} finally {
server.stop(0)
}
'''
}
}