This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new db97f3f644 some smoke tests for groovy-concurrent-java
db97f3f644 is described below
commit db97f3f644bba700c8dd4183d2cc2d468c8e2e60
Author: Paul King <[email protected]>
AuthorDate: Mon Sep 7 08:56:03 2026 +1000
some smoke tests for groovy-concurrent-java
---
subprojects/groovy-concurrent-java/build.gradle | 67 +++++-
.../apache/groovy/concurrent/java/ActorTest.java | 92 ++++++++
.../apache/groovy/concurrent/java/AgentTest.java | 42 ++++
.../groovy/concurrent/java/ChannelSelectTest.java | 256 +++++++++++++++++++++
.../groovy/concurrent/java/DataflowTest.java | 54 +++++
.../concurrent/java/ModuleSelfContainmentTest.java | 173 ++++++++++++++
.../apache/groovy/concurrent/java/PoolTest.java | 63 +++++
.../groovy/concurrent/java/package-info.java | 23 ++
8 files changed, 762 insertions(+), 8 deletions(-)
diff --git a/subprojects/groovy-concurrent-java/build.gradle
b/subprojects/groovy-concurrent-java/build.gradle
index af5e22a213..bad80c9035 100644
--- a/subprojects/groovy-concurrent-java/build.gradle
+++ b/subprojects/groovy-concurrent-java/build.gradle
@@ -21,13 +21,12 @@ plugins {
id 'org.apache.groovy-library'
}
-// This module has no source code of its own. Its jar is extracted from
+// This module has no main source code of its own. Its jar is extracted from
// the Groovy core jar, containing only the pure-Java concurrent API.
// Java users can depend on this module without the Groovy runtime.
sourceSets {
main.java.srcDirs = []
main.groovy.srcDirs = []
- test.java.srcDirs = []
test.groovy.srcDirs = []
}
@@ -35,15 +34,32 @@ sourceSets {
configurations.groovyCompilerClasspath.dependencies.clear()
// The jar is built by filtering core's repackageJar (shadow) output
+def apiIncludes = [
+ 'groovy/concurrent/**',
+ 'org/apache/groovy/runtime/async/**',
+ 'org/apache/groovy/util/concurrent/ThreadHelper*',
+ // Types the API above references that live outside those packages.
+ // ModuleSelfContainmentTest fails the build if a groovy.* reference
+ // is left unresolved by this list.
+ 'groovy/transform/Internal*',
+ 'groovy/util/function/TriConsumer*',
+ 'org/apache/groovy/lang/annotation/GroovyABI*',
+]
+// Groovy-dependent classes
+def apiExcludes = [
+ '**/AsyncClosureUtils*',
+ '**/Dataflows*',
+]
+
tasks.named('jar') {
dependsOn rootProject.tasks.named('repackageJar')
+ // Copy-spec patterns are not tracked as task inputs on their own, so an
+ // edited list would otherwise leave a stale, up-to-date jar behind.
+ inputs.property('apiIncludes', apiIncludes)
+ inputs.property('apiExcludes', apiExcludes)
from(zipTree(rootProject.tasks.named('repackageJar').flatMap {
it.archiveFile })) {
- include 'groovy/concurrent/**'
- include 'org/apache/groovy/runtime/async/**'
- include 'org/apache/groovy/util/concurrent/ThreadHelper*'
- // Exclude Groovy-dependent classes
- exclude '**/AsyncClosureUtils*'
- exclude '**/Dataflows*'
+ include apiIncludes
+ exclude apiExcludes
}
manifest {
attributes 'Automatic-Module-Name': 'org.apache.groovy.concurrent.java'
@@ -55,6 +71,41 @@ groovyLibrary {
withoutBinaryCompatibilityChecks()
}
+// The tests are the pure-Java consumer's view of this module: they are
compiled
+// and run against the module jar plus JUnit only, exactly as a Java project
+// depending on org.apache.groovy:groovy-concurrent-java would be. Groovy core
+// (which the conventions put on testImplementation) is deliberately kept off
+// the classpath, so a groovy.* type leaking into a signature fails here rather
+// than at a user's site.
+configurations {
+ javaConsumerTest {
+ canBeConsumed = false
+ description = 'Test-only dependencies of the pure-Java consumer tests'
+ }
+}
+
+dependencies {
+ javaConsumerTest "org.junit.jupiter:junit-jupiter:${versions.junit6}"
+ // the JUnit jars carry @API annotations; keep javac quiet about them
+ javaConsumerTest "org.apiguardian:apiguardian-api:${versions.apiguardian}"
+ javaConsumerTest
"org.junit.platform:junit-platform-launcher:${versions.junit6}", {
+ exclude group: 'org.apiguardian', module: 'apiguardian-api'
+ }
+}
+
+def moduleJar = tasks.named('repackageJar') // the published artifact, not the
raw intermediate
+sourceSets {
+ test {
+ compileClasspath = files(moduleJar) + configurations.javaConsumerTest
+ runtimeClasspath = output + files(moduleJar) +
configurations.javaConsumerTest
+ }
+}
+
+tasks.named('test') {
+ classpath = sourceSets.test.runtimeClasspath
+ testClassesDirs = sourceSets.test.output.classesDirs
+}
+
// Declare capability for mutual exclusion with Groovy core on library
variants.
afterEvaluate {
def capabilityVersion = sharedConfiguration.groovyVersion.get()
diff --git
a/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/ActorTest.java
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/ActorTest.java
new file mode 100644
index 0000000000..35ff351289
--- /dev/null
+++
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/ActorTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.apache.groovy.concurrent.java;
+
+import groovy.concurrent.Actor;
+import groovy.concurrent.Awaitable;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.apache.groovy.runtime.async.AsyncSupport.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ActorTest {
+
+ @Test
+ void reactorProcessesMessages() {
+ Actor<Integer> doubler = Actor.<Integer, Integer>reactor(n -> n * 2);
+ try {
+ int result = await(doubler.<Integer>sendAndGet(21));
+ assertEquals(42, result);
+ } finally {
+ doubler.stop();
+ }
+ }
+
+ @Test
+ void statefulActorMaintainsState() {
+ Actor<String> counter = Actor.<String, Integer>stateful(0, (state,
msg) -> {
+ if ("increment".equals(msg)) return state + 1;
+ return state;
+ });
+ try {
+ counter.send("increment");
+ counter.send("increment");
+ int result = await(counter.<Integer>sendAndGet("increment"));
+ assertEquals(3, result);
+ } finally {
+ counter.stop();
+ }
+ }
+
+ @Test
+ void errorHandlerSeesTheFailure() {
+ AtomicReference<Throwable> captured = new AtomicReference<>();
+ Actor<String> actor = Actor.<String, String>reactor(msg -> {
+ if ("boom".equals(msg)) throw new IllegalStateException("boom");
+ return msg;
+ }).onError((Throwable t, String msg) -> captured.set(t));
+ try {
+ assertEquals("ok", await(actor.<String>sendAndGet("ok")));
+ assertThrows(IllegalStateException.class, () ->
await(actor.<String>sendAndGet("boom")));
+ for (int i = 0; i < 100 && captured.get() == null; i++) {
+ await(Awaitable.delay(10));
+ }
+ assertEquals("boom", captured.get().getMessage());
+ } finally {
+ actor.stop();
+ }
+ }
+
+ @Test
+ void contextAwareErrorHandlerCanStopTheActor() {
+ Actor<String> actor = Actor.<String, String>reactor(msg -> {
+ throw new IllegalStateException("always");
+ }).onError((ctx, t, msg) -> ctx.self().stop());
+
+ actor.send("trigger");
+ for (int i = 0; i < 100 && actor.isActive(); i++) {
+ await(Awaitable.delay(10));
+ }
+ assertFalse(actor.isActive());
+ }
+}
diff --git
a/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/AgentTest.java
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/AgentTest.java
new file mode 100644
index 0000000000..602bd273bc
--- /dev/null
+++
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/AgentTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.apache.groovy.concurrent.java;
+
+import groovy.concurrent.Agent;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.groovy.runtime.async.AsyncSupport.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class AgentTest {
+
+ @Test
+ void agentSerialisesUpdates() {
+ Agent<Integer> counter = Agent.create(0);
+ try {
+ counter.send(n -> n + 1);
+ counter.send(n -> n + 1);
+ counter.send(n -> n + 1);
+ int result = await(counter.getAsync());
+ assertEquals(3, result);
+ } finally {
+ counter.shutdown();
+ }
+ }
+}
diff --git
a/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/ChannelSelectTest.java
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/ChannelSelectTest.java
new file mode 100644
index 0000000000..7842f4dd89
--- /dev/null
+++
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/ChannelSelectTest.java
@@ -0,0 +1,256 @@
+/*
+ * 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.apache.groovy.concurrent.java;
+
+import groovy.concurrent.AsyncChannel;
+import groovy.concurrent.Awaitable;
+import groovy.concurrent.ChannelSelect;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static groovy.concurrent.ChannelSelect.after;
+import static groovy.concurrent.ChannelSelect.offers;
+import static groovy.concurrent.ChannelSelect.receive;
+import static groovy.concurrent.ChannelSelect.send;
+import static org.apache.groovy.runtime.async.AsyncSupport.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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;
+
+/**
+ * {@link ChannelSelect} driven from plain Java: choice policies, mixed
+ * send/receive offers, guards, preconditions and timer branches.
+ */
+class ChannelSelectTest {
+
+ @Test
+ void selectTakesTheFirstChannelToDeliver() {
+ AsyncChannel<String> ch1 = AsyncChannel.create(10);
+ AsyncChannel<String> ch2 = AsyncChannel.create(10);
+ ChannelSelect sel = ChannelSelect.from(ch1, ch2);
+
+ Awaitable.go(() -> {
+ await(Awaitable.delay(50));
+ return await(ch1.send("from-ch1"));
+ });
+
+ ChannelSelect.Result result = await(sel.select());
+ assertEquals(0, result.getIndex());
+ assertSame(ch1, result.getChannel());
+ String value = result.getValue();
+ assertEquals("from-ch1", value);
+ assertFalse(result.isSend());
+ assertFalse(result.isTimeout());
+ }
+
+ @Test
+ void losingBranchesAreNotConsumed() {
+ AsyncChannel<String> a = AsyncChannel.create(4);
+ AsyncChannel<String> b = AsyncChannel.create(4);
+ await(a.send("a1"));
+ await(b.send("b1"));
+
+ ChannelSelect.Result result = await(ChannelSelect.from(a, b).select());
+ assertEquals(0, result.getIndex());
+ assertEquals("a1", result.<String>getValue());
+
+ assertEquals(1, b.getBufferedSize());
+ assertEquals("b1", await(b.receive()));
+ }
+
+ @Test
+ void fairChoiceRotatesAmongReadyChannels() {
+ AsyncChannel<String> a = AsyncChannel.create(4);
+ AsyncChannel<String> b = AsyncChannel.create(4);
+ AsyncChannel<String> c = AsyncChannel.create(4);
+ for (int i = 0; i < 2; i++) {
+ await(a.send("a" + i));
+ await(b.send("b" + i));
+ await(c.send("c" + i));
+ }
+
+ ChannelSelect fair = ChannelSelect.from(a, b, c).fair();
+ List<Integer> order = new ArrayList<>();
+ for (int i = 0; i < 6; i++) {
+ order.add(await(fair.select()).getIndex());
+ }
+ assertEquals(List.of(0, 1, 2, 0, 1, 2), order);
+
+ // the default policy is priority by list order
+ AsyncChannel<String> p = AsyncChannel.create(4);
+ AsyncChannel<String> q = AsyncChannel.create(4);
+ await(p.send("p0"));
+ await(p.send("p1"));
+ await(q.send("q0"));
+ ChannelSelect priority = ChannelSelect.from(p, q);
+ assertEquals(0, await(priority.select()).getIndex());
+ assertEquals(0, await(priority.select()).getIndex());
+ }
+
+ @Test
+ void randomChoiceSpreadsAmongReadyChannelsAndKeepsEachInOrder() {
+ int rounds = 200;
+ AsyncChannel<Integer> a = AsyncChannel.create(rounds);
+ AsyncChannel<Integer> b = AsyncChannel.create(rounds);
+ for (int i = 0; i < rounds; i++) {
+ await(a.send(i));
+ await(b.send(i));
+ }
+
+ ChannelSelect random = ChannelSelect.from(a, b).random();
+ List<List<Integer>> taken = List.of(new ArrayList<>(), new
ArrayList<>());
+ for (int i = 0; i < rounds; i++) {
+ ChannelSelect.Result result = await(random.select());
+ taken.get(result.getIndex()).add(result.getValue());
+ }
+ // the chance of one channel never being chosen in 200 draws is 2^-199
+ assertFalse(taken.get(0).isEmpty());
+ assertFalse(taken.get(1).isEmpty());
+ for (List<Integer> values : taken) {
+ for (int i = 0; i < values.size(); i++) {
+ assertEquals(i, values.get(i));
+ }
+ }
+ }
+
+ @Test
+ void sendOfferCommitsWhenAReceiverIsWaiting() {
+ AsyncChannel<String> out = AsyncChannel.create(); // rendezvous
+ AsyncChannel<String> other = AsyncChannel.create(4);
+
+ Awaitable<String> taken = out.receive(); // a receiver is already
waiting
+ ChannelSelect.Result result = await(offers(send(out, "opener"),
receive(other)).select());
+
+ assertEquals(0, result.getIndex());
+ assertTrue(result.isSend());
+ assertEquals("opener", result.<String>getValue());
+ assertEquals("opener", await(taken));
+ }
+
+ @Test
+ void sendOfferCommitsIntoFreeBufferSpace() {
+ AsyncChannel<String> out = AsyncChannel.create(1);
+ AsyncChannel<String> other = AsyncChannel.create(4);
+
+ ChannelSelect.Result result = await(offers(send(out, "buffered"),
receive(other)).select());
+ assertEquals(0, result.getIndex());
+ assertTrue(result.isSend());
+ assertEquals(1, out.getBufferedSize());
+ assertEquals("buffered", await(out.receive()));
+ }
+
+ @Test
+ void guardMasksAnOfferWithoutRenumberingTheOthers() {
+ AsyncChannel<String> first = AsyncChannel.create(4);
+ AsyncChannel<String> second = AsyncChannel.create(4);
+ await(first.send("f1"));
+ await(second.send("s1"));
+
+ AtomicBoolean gate = new AtomicBoolean(false);
+ ChannelSelect sel = offers(receive(first).when(gate::get),
receive(second));
+
+ // both are ready, but the first branch is guarded off: the second
+ // wins and still calls itself index 1
+ ChannelSelect.Result result = await(sel.select());
+ assertEquals(1, result.getIndex());
+ assertEquals("s1", result.<String>getValue());
+ assertEquals(1, first.getBufferedSize());
+
+ // the guard is consulted afresh on every select
+ gate.set(true);
+ result = await(sel.select());
+ assertEquals(0, result.getIndex());
+ assertEquals("f1", result.<String>getValue());
+ }
+
+ @Test
+ void positionalPreconditionsMaskOffers() {
+ AsyncChannel<String> a = AsyncChannel.create(4);
+ AsyncChannel<String> b = AsyncChannel.create(4);
+ await(a.send("a1"));
+ await(b.send("b1"));
+ ChannelSelect sel = ChannelSelect.from(a, b);
+
+ ChannelSelect.Result result = await(sel.select(false, true));
+ assertEquals(1, result.getIndex());
+ assertEquals("b1", result.<String>getValue());
+ assertEquals(1, a.getBufferedSize());
+
+ assertThrows(IllegalArgumentException.class, () -> sel.select(true));
+ assertThrows(IllegalStateException.class, () ->
await(sel.select(false, false)));
+ assertEquals(1, a.getBufferedSize());
+ }
+
+ @Test
+ void timerOfferWinsAQuietSelect() {
+ AsyncChannel<String> work = AsyncChannel.create(4);
+ Instant before = Instant.now();
+
+ ChannelSelect.Result result = await(offers(receive(work),
after(50)).select());
+ assertEquals(1, result.getIndex());
+ assertTrue(result.isTimeout());
+ assertFalse(result.isSend());
+ assertNull(result.getChannel());
+ Instant firedAt = result.getValue();
+ assertFalse(firedAt.isBefore(before));
+ }
+
+ @Test
+ void dataBeatsTheTimer() {
+ AsyncChannel<String> work = AsyncChannel.create(4);
+ await(work.send("job"));
+
+ ChannelSelect.Result result = await(offers(receive(work),
after(Duration.ofSeconds(5))).select());
+ assertEquals(0, result.getIndex());
+ assertFalse(result.isTimeout());
+ assertEquals("job", result.<String>getValue());
+ }
+
+ @Test
+ void timerChannelIsAFixedDeadlineAcrossSelects() {
+ AsyncChannel<Integer> work = AsyncChannel.create(16);
+ AsyncChannel<Instant> deadline = AsyncChannel.after(200);
+ ChannelSelect sel = ChannelSelect.from(work, deadline);
+
+ Awaitable.go(() -> {
+ for (int i = 0; i < 3; i++) {
+ await(work.send(i));
+ await(Awaitable.delay(10));
+ }
+ return null;
+ });
+
+ List<Integer> received = new ArrayList<>();
+ while (true) {
+ ChannelSelect.Result result = await(sel.select());
+ if (result.getIndex() == 1) break;
+ received.add(result.getValue());
+ }
+ assertEquals(List.of(0, 1, 2), received);
+ assertTrue(deadline.isClosed());
+ }
+}
diff --git
a/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/DataflowTest.java
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/DataflowTest.java
new file mode 100644
index 0000000000..00eb7690a1
--- /dev/null
+++
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/DataflowTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.apache.groovy.concurrent.java;
+
+import groovy.concurrent.AsyncScope;
+import groovy.concurrent.Awaitable;
+import groovy.concurrent.DataflowVariable;
+import org.apache.groovy.runtime.async.AsyncSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.groovy.runtime.async.AsyncSupport.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class DataflowTest {
+
+ @Test
+ void dataflowVariablesBindInAnyOrder() {
+ DataflowVariable<Integer> x = new DataflowVariable<>();
+ DataflowVariable<Integer> y = new DataflowVariable<>();
+
+ Awaitable<Integer> z = Awaitable.go(() -> await(x) + await(y));
+
+ AsyncSupport.getExecutor().execute(() -> x.bind(10));
+ AsyncSupport.getExecutor().execute(() -> y.bind(5));
+
+ assertEquals(15, await(z));
+ }
+
+ @Test
+ void structuredConcurrencyWithScope() {
+ int result = AsyncScope.withScope(scope -> {
+ Awaitable<Integer> a = scope.async(() -> 10);
+ Awaitable<Integer> b = scope.async(() -> 20);
+ return await(a) + await(b);
+ });
+ assertEquals(30, result);
+ }
+}
diff --git
a/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/ModuleSelfContainmentTest.java
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/ModuleSelfContainmentTest.java
new file mode 100644
index 0000000000..98f0607261
--- /dev/null
+++
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/ModuleSelfContainmentTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.apache.groovy.concurrent.java;
+
+import groovy.concurrent.ChannelSelect;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The module jar must stand alone: a Java project depending on it has no
+ * Groovy runtime, so every {@code groovy.*} type a class in the jar
+ * mentions, in a supertype, a signature, or an annotation, has to ship in
+ * the jar too. The jar's include list lives in this module's build script;
+ * these checks are what keep it honest.
+ */
+class ModuleSelfContainmentTest {
+
+ /** A class name in one of Groovy's own packages, as written in a class
file. */
+ private static final String GROOVY_CLASS =
"(?:groovy|org/apache/groovy)(?:/[a-z_]\\w*)*/[A-Z][\\w$]*";
+ private static final Pattern BARE_NAME = Pattern.compile("^" +
GROOVY_CLASS + "$");
+ private static final Pattern DESCRIPTOR = Pattern.compile("L(" +
GROOVY_CLASS + ")[<;]");
+
+ @Test
+ void groovyCoreIsNotOnTheTestClasspath() {
+ // the tests only prove anything if they are compiled and run the way
+ // a Java consumer is: against the module jar alone
+ assertThrows(ClassNotFoundException.class, () ->
Class.forName("groovy.lang.GroovyObject"));
+ }
+
+ @Test
+ void everyGroovyReferenceResolvesInsideTheJar() throws IOException {
+ Map<String, Set<String>> unresolved = new TreeMap<>();
+ try (JarFile jar = new JarFile(moduleJar().toFile())) {
+ Set<String> present = jar.stream()
+ .map(JarEntry::getName)
+ .filter(name -> name.endsWith(".class"))
+ .collect(Collectors.toSet());
+ for (String name : present) {
+ Set<String> missing = new TreeSet<>();
+ try (InputStream in =
jar.getInputStream(jar.getJarEntry(name))) {
+ for (String ref : groovyReferences(in.readAllBytes())) {
+ if (!present.contains(ref + ".class"))
missing.add(ref);
+ }
+ }
+ if (!missing.isEmpty()) unresolved.put(name, missing);
+ }
+ }
+ assertTrue(unresolved.isEmpty(),
+ () -> "groovy types referenced but not shipped in the jar (add
them to the jar's include list): " + unresolved);
+ }
+
+ @Test
+ void everyClassReflectsWithoutGroovyCore() throws Exception {
+ Path jar = moduleJar();
+ List<String> failures = new ArrayList<>();
+ try (URLClassLoader loader = new URLClassLoader(new
URL[]{jar.toUri().toURL()}, ClassLoader.getPlatformClassLoader());
+ JarFile jarFile = new JarFile(jar.toFile())) {
+ List<String> classNames = jarFile.stream()
+ .map(JarEntry::getName)
+ .filter(name -> name.endsWith(".class"))
+ .map(name -> name.substring(0, name.length() -
".class".length()).replace('/', '.'))
+ .collect(Collectors.toList());
+ int members = 0;
+ for (String className : classNames) {
+ try {
+ Class<?> c = Class.forName(className, false, loader);
+ // each of these resolves the types it mentions
+ if (c.getSuperclass() != null) members++;
+ members += c.getInterfaces().length
+ + c.getDeclaredFields().length
+ + c.getDeclaredConstructors().length
+ + c.getDeclaredMethods().length;
+ } catch (Throwable t) {
+ failures.add(className + ": " + t);
+ }
+ }
+ assertTrue(members > 0, "nothing was reflected over");
+ }
+ assertTrue(failures.isEmpty(), () -> "classes that cannot be reflected
over without Groovy core: " + failures);
+ }
+
+ private static Path moduleJar() {
+ URL location =
ChannelSelect.class.getProtectionDomain().getCodeSource().getLocation();
+ Path path = Paths.get(java.net.URI.create(location.toString()));
+ assertTrue(path.getFileName().toString().endsWith(".jar"),
+ () -> "the API should be loaded from the module jar, not from
" + path);
+ return path;
+ }
+
+ private static final int CONSTANT_UTF8 = 1;
+ private static final int CONSTANT_LONG = 5;
+ private static final int CONSTANT_DOUBLE = 6;
+ /** Byte size of each non-Utf8 constant pool entry, by tag (JVMS ยง4.4). */
+ private static final int[] CONSTANT_SIZE = new int[21];
+
+ static {
+ for (int tag : new int[]{7, 8, 16, 19, 20}) CONSTANT_SIZE[tag] = 2; //
Class, String, MethodType, Module, Package
+ CONSTANT_SIZE[15] = 3; // MethodHandle
+ for (int tag : new int[]{3, 4, 9, 10, 11, 12, 17, 18})
CONSTANT_SIZE[tag] = 4; // Integer, Float, refs, NameAndType, Dynamic,
InvokeDynamic
+ CONSTANT_SIZE[CONSTANT_LONG] = 8;
+ CONSTANT_SIZE[CONSTANT_DOUBLE] = 8;
+ }
+
+ /**
+ * Every Groovy-package class name mentioned in the constant pool: class
+ * references are stored as bare internal names, while field, method,
+ * generic-signature and annotation references embed them in descriptors.
+ */
+ static Set<String> groovyReferences(byte[] classFile) throws IOException {
+ DataInputStream in = new DataInputStream(new
ByteArrayInputStream(classFile));
+ in.readInt(); // magic
+ in.readUnsignedShort(); // minor version
+ in.readUnsignedShort(); // major version
+ int count = in.readUnsignedShort();
+ Set<String> refs = new TreeSet<>();
+ for (int i = 1; i < count; i++) {
+ int tag = in.readUnsignedByte();
+ if (tag == CONSTANT_UTF8) {
+ collectGroovyNames(in.readUTF(), refs);
+ continue;
+ }
+ int size = tag < CONSTANT_SIZE.length ? CONSTANT_SIZE[tag] : 0;
+ if (size == 0) throw new IllegalStateException("unknown constant
pool tag " + tag);
+ if (in.skipBytes(size) != size) throw new
IllegalStateException("truncated constant pool");
+ if (tag == CONSTANT_LONG || tag == CONSTANT_DOUBLE) i++; // these
take two slots
+ }
+ return refs;
+ }
+
+ private static void collectGroovyNames(String text, Set<String> refs) {
+ if (BARE_NAME.matcher(text).matches()) refs.add(text);
+ Matcher m = DESCRIPTOR.matcher(text);
+ while (m.find()) refs.add(m.group(1));
+ }
+}
diff --git
a/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/PoolTest.java
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/PoolTest.java
new file mode 100644
index 0000000000..d518da63f2
--- /dev/null
+++
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/PoolTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.apache.groovy.concurrent.java;
+
+import groovy.concurrent.AsyncScope;
+import groovy.concurrent.Awaitable;
+import groovy.concurrent.ParallelScope;
+import groovy.concurrent.Pool;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.groovy.runtime.async.AsyncSupport.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class PoolTest {
+
+ @Test
+ void scopeRunsOnAPool() {
+ try (Pool pool = Pool.cpu()) {
+ String result = AsyncScope.withScope(pool, scope -> {
+ Awaitable<String> a = scope.async(() -> "hello");
+ Awaitable<String> b = scope.async(() -> "world");
+ return await(a) + " " + await(b);
+ });
+ assertEquals("hello world", result);
+ }
+ }
+
+ @Test
+ void parallelScopeWithPool() {
+ int result = ParallelScope.withPool(4, scope -> {
+ List<Awaitable<Integer>> tasks = new ArrayList<>();
+ for (int i = 1; i <= 4; i++) {
+ int n = i;
+ tasks.add(scope.async(() -> n * 10));
+ }
+ int sum = 0;
+ for (Awaitable<Integer> task : tasks) {
+ sum += await(task);
+ }
+ return sum;
+ });
+ assertEquals(100, result);
+ }
+}
diff --git
a/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/package-info.java
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/package-info.java
new file mode 100644
index 0000000000..a63a9178eb
--- /dev/null
+++
b/subprojects/groovy-concurrent-java/src/test/java/org/apache/groovy/concurrent/java/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+/**
+ * Consumer tests for the {@code groovy-concurrent-java} module, written in
+ * plain Java and run against the module jar without the Groovy runtime.
+ */
+package org.apache.groovy.concurrent.java;