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 49a8738033 GROOVY-11997: Add @ForkedJvm and @ExpectedToFail JUnit
extensions to groovy-test-junit6 (minor tweaks and an additional test converted)
49a8738033 is described below
commit 49a8738033c46d50a76e2779c74084025488aa0c
Author: Paul King <[email protected]>
AuthorDate: Wed May 6 14:02:44 2026 +1000
GROOVY-11997: Add @ForkedJvm and @ExpectedToFail JUnit extensions to
groovy-test-junit6 (minor tweaks and an additional test converted)
---
subprojects/groovy-templates/build.gradle | 1 +
.../groovy/text/StreamingTemplateEngineTest.groovy | 79 ++++++----------------
.../main/java/groovy/junit6/plugin/ForkedJvm.java | 41 +++++++++++
.../groovy/junit6/plugin/ForkedJvmExtension.java | 24 +++++++
.../src/test/groovy/ForkedJvmTest.groovy | 63 +++++++++++++++++
5 files changed, 151 insertions(+), 57 deletions(-)
diff --git a/subprojects/groovy-templates/build.gradle
b/subprojects/groovy-templates/build.gradle
index e3064f1d55..db0091a594 100644
--- a/subprojects/groovy-templates/build.gradle
+++ b/subprojects/groovy-templates/build.gradle
@@ -24,6 +24,7 @@ dependencies {
api rootProject // Template uses Writable...
implementation projects.groovyXml
testImplementation projects.groovyTest
+ testImplementation projects.groovyTestJunit6
testImplementation ("org.spockframework:spock-core:${versions.spock}") {
exclude group: 'org.apache.groovy'
}
diff --git
a/subprojects/groovy-templates/src/test/groovy/groovy/text/StreamingTemplateEngineTest.groovy
b/subprojects/groovy-templates/src/test/groovy/groovy/text/StreamingTemplateEngineTest.groovy
index ddc6c20081..cae01ce055 100644
---
a/subprojects/groovy-templates/src/test/groovy/groovy/text/StreamingTemplateEngineTest.groovy
+++
b/subprojects/groovy-templates/src/test/groovy/groovy/text/StreamingTemplateEngineTest.groovy
@@ -18,9 +18,9 @@
*/
package groovy.text
+import groovy.junit6.plugin.ForkedJvm
import org.junit.jupiter.api.Test
-import static groovy.test.GroovyAssert.assertScript
import static org.junit.jupiter.api.Assertions.assertThrows
final class StreamingTemplateEngineTest {
@@ -452,66 +452,31 @@ final class StreamingTemplateEngineTest {
}
@Test
+ @ForkedJvm(
+ systemProperties =
['groovy.StreamingTemplateEngine.reuseClassLoader=true'],
+ inheritProperties = ['spock.*'])
void reuseClassLoader1() {
- assertScript '''
- final reuseOption =
'groovy.StreamingTemplateEngine.reuseClassLoader'
- System.setProperty(reuseOption, 'true')
- try {
- // reload class to initialize static field from the beginning
- def steClass =
groovy.text.StreamingTemplateEngineTest.reloadClass('groovy.text.StreamingTemplateEngine')
-
- GroovyClassLoader gcl = new GroovyClassLoader()
- def engine = steClass.newInstance(gcl)
- assert 'Hello, Daniel' == engine.createTemplate('Hello,
${name}').make([name: 'Daniel']).toString()
- assert gcl.loadedClasses.length > 0
- def cloned = gcl.loadedClasses.clone()
- assert 'Hello, Paul' == engine.createTemplate('Hello,
${name}').make([name: 'Paul']).toString()
- assert cloned == gcl.loadedClasses
- } finally {
- System.clearProperty(reuseOption)
- }
- '''
+ GroovyClassLoader gcl = new GroovyClassLoader()
+ def engine = new StreamingTemplateEngine(gcl)
+ assert 'Hello, Daniel' == engine.createTemplate('Hello,
${name}').make([name: 'Daniel']).toString()
+ assert gcl.loadedClasses.length > 0
+ def cloned = gcl.loadedClasses.clone()
+ assert 'Hello, Paul' == engine.createTemplate('Hello,
${name}').make([name: 'Paul']).toString()
+ assert cloned == gcl.loadedClasses
}
@Test
+ @ForkedJvm(
+ systemProperties =
['groovy.StreamingTemplateEngine.reuseClassLoader=true'],
+ inheritProperties = ['spock.*'])
void reuseClassLoader2() {
- assertScript '''
- final reuseOption =
'groovy.StreamingTemplateEngine.reuseClassLoader'
- System.setProperty(reuseOption, 'true')
- try {
- // reload class to initialize static field from the beginning
- def steClass =
groovy.text.StreamingTemplateEngineTest.reloadClass('groovy.text.StreamingTemplateEngine')
-
- GroovyClassLoader gcl = new GroovyClassLoader()
- def engine = steClass.newInstance(gcl)
- assert 'Hello, Daniel' == engine.createTemplate('Hello,
${name}').make([name: 'Daniel']).toString()
- assert gcl.loadedClasses.length > 0
- def cloned = gcl.loadedClasses.clone()
- engine = steClass.newInstance(gcl)
- assert 'Hello, Paul' == engine.createTemplate('Hello,
${name}').make([name: 'Paul']).toString()
- assert cloned == gcl.loadedClasses
- } finally {
- System.clearProperty(reuseOption)
- }
- '''
- }
-
- static Class reloadClass(String className) {
- def loader = new GroovyClassLoader() {
- private final Map<String, Class> loadedClasses = new
java.util.concurrent.ConcurrentHashMap<>()
-
- @Override
- Class loadClass(String name) {
- if (name ==~ ('^' + className + '([$].+)?$')) {
- return loadedClasses.computeIfAbsent(name, n -> {
- def clazz = defineClass(n,
GroovyClassLoader.class.getResourceAsStream('/' + n.replace('.', '/') +
'.class').bytes)
- return clazz
- })
- }
- return super.loadClass(name)
- }
- }
- def loaded = loader.loadClass(className)
- return loaded
+ GroovyClassLoader gcl = new GroovyClassLoader()
+ def engine = new StreamingTemplateEngine(gcl)
+ assert 'Hello, Daniel' == engine.createTemplate('Hello,
${name}').make([name: 'Daniel']).toString()
+ assert gcl.loadedClasses.length > 0
+ def cloned = gcl.loadedClasses.clone()
+ engine = new StreamingTemplateEngine(gcl)
+ assert 'Hello, Paul' == engine.createTemplate('Hello,
${name}').make([name: 'Paul']).toString()
+ assert cloned == gcl.loadedClasses
}
}
diff --git
a/subprojects/groovy-test-junit6/src/main/java/groovy/junit6/plugin/ForkedJvm.java
b/subprojects/groovy-test-junit6/src/main/java/groovy/junit6/plugin/ForkedJvm.java
index 80138b3cbb..dda239df1a 100644
---
a/subprojects/groovy-test-junit6/src/main/java/groovy/junit6/plugin/ForkedJvm.java
+++
b/subprojects/groovy-test-junit6/src/main/java/groovy/junit6/plugin/ForkedJvm.java
@@ -52,6 +52,34 @@ import java.lang.annotation.Target;
* @ForkedJvm(jvmArgs = {"--add-opens=java.base/java.lang=ALL-UNNAMED"})
* void withModuleAccess() { ... }
* </pre>
+ * <p>
+ * Properties from the parent JVM can be propagated to the child via
+ * {@link #inheritProperties()}. Each entry is either an exact property
+ * name or a prefix pattern ending in {@code *}. This is useful when the
+ * parent test JVM is configured by the build (e.g. Gradle) with a
+ * property the child also needs, such as Spock's Groovy-version-check
+ * override:
+ * <pre>
+ * @Test
+ * @ForkedJvm(inheritProperties = {"spock.*"})
+ * void seesSpockOverride() { ... }
+ * </pre>
+ * <p>
+ * <b>Lifecycle gotcha:</b> JUnit lifecycle hooks
+ * ({@code @BeforeAll}, {@code @BeforeEach}, {@code @AfterAll},
+ * {@code @AfterEach}) fire in <em>both</em> the parent and the forked
+ * child JVM, because the child re-runs the class lifecycle for the
+ * targeted method. Setup that mutates JVM-global state — typically
+ * {@code System.setProperty(...)} — therefore replays in the child and
+ * can defeat tests that assert on what was (or was not) propagated
+ * across the fork. Guard parent-only setup with the forked-flag check:
+ * <pre>
+ * @BeforeAll
+ * static void setUp() {
+ * if (Boolean.parseBoolean(System.getProperty("groovy.junit6.forked")))
return;
+ * // ...parent-only setup here...
+ * }
+ * </pre>
*
* @since 6.0.0
*/
@@ -71,4 +99,17 @@ public @interface ForkedJvm {
* {@code "-Xmx128m"} or {@code
"--add-opens=java.base/java.lang=ALL-UNNAMED"}.
*/
String[] jvmArgs() default {};
+
+ /**
+ * Names or prefix patterns of system properties from the parent JVM to
+ * propagate to the forked child. Each entry is either an exact property
+ * name (e.g. {@code "spock.iKnowWhatImDoing.disableGroovyVersionCheck"})
+ * or a prefix pattern ending in {@code *} (e.g. {@code "spock.*"} or
+ * {@code "groovy.compiler.*"}). Patterns that match no parent property
+ * are silently ignored.
+ * <p>
+ * Properties supplied via {@link #systemProperties()} override inherited
+ * values for the same key.
+ */
+ String[] inheritProperties() default {};
}
diff --git
a/subprojects/groovy-test-junit6/src/main/java/groovy/junit6/plugin/ForkedJvmExtension.java
b/subprojects/groovy-test-junit6/src/main/java/groovy/junit6/plugin/ForkedJvmExtension.java
index 197305d49e..a142af591d 100644
---
a/subprojects/groovy-test-junit6/src/main/java/groovy/junit6/plugin/ForkedJvmExtension.java
+++
b/subprojects/groovy-test-junit6/src/main/java/groovy/junit6/plugin/ForkedJvmExtension.java
@@ -32,7 +32,10 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Properties;
+import java.util.Set;
/**
* JUnit 5 {@link InvocationInterceptor} backing the {@link ForkedJvm}
@@ -109,6 +112,10 @@ public class ForkedJvmExtension implements
InvocationInterceptor {
if (parentInverting) {
cmd.add("-D" + ExpectedToFailExtension.DEFERRED_TO_PARENT_PROP +
"=true");
}
+ // Inherited properties first, then explicit systemProperties — later
-D wins on the JVM command line.
+ for (String name : resolveInherited(config.inheritProperties())) {
+ cmd.add("-D" + name + "=" + System.getProperty(name));
+ }
for (String sp : config.systemProperties()) {
int eq = sp.indexOf('=');
if (eq < 0) {
@@ -128,6 +135,23 @@ public class ForkedJvmExtension implements
InvocationInterceptor {
return cmd;
}
+ private static Set<String> resolveInherited(String[] patterns) {
+ if (patterns.length == 0) return java.util.Collections.emptySet();
+ Properties parent = System.getProperties();
+ Set<String> matched = new LinkedHashSet<>();
+ for (String pattern : patterns) {
+ if (pattern.endsWith("*")) {
+ String prefix = pattern.substring(0, pattern.length() - 1);
+ for (String name : parent.stringPropertyNames()) {
+ if (name.startsWith(prefix)) matched.add(name);
+ }
+ } else if (parent.getProperty(pattern) != null) {
+ matched.add(pattern);
+ }
+ }
+ return matched;
+ }
+
private static void propagateOutcome(int exit, Path resultFile,
Class<?> testClass, Method
testMethod) throws Throwable {
String location = testClass.getName() + "#" + testMethod.getName();
diff --git
a/subprojects/groovy-test-junit6/src/test/groovy/ForkedJvmTest.groovy
b/subprojects/groovy-test-junit6/src/test/groovy/ForkedJvmTest.groovy
index 34ff890ec4..778ed9e560 100644
--- a/subprojects/groovy-test-junit6/src/test/groovy/ForkedJvmTest.groovy
+++ b/subprojects/groovy-test-junit6/src/test/groovy/ForkedJvmTest.groovy
@@ -19,7 +19,9 @@
import groovy.junit6.plugin.ExpectedToFail
import groovy.junit6.plugin.ForkedJvm
+import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assumptions
+import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.platform.engine.discovery.DiscoverySelectors
@@ -33,6 +35,31 @@ import static org.junit.jupiter.api.Assertions.assertTrue
class ForkedJvmTest {
+ private static final String INHERIT_EXACT =
'groovy.junit6.test.inherit.exact'
+ private static final String INHERIT_GLOB_A =
'groovy.junit6.test.inherit.glob.a'
+ private static final String INHERIT_GLOB_B =
'groovy.junit6.test.inherit.glob.b'
+
+ @BeforeAll
+ static void setParentProperties() {
+ // Lifecycle hooks fire in BOTH parent and forked-child JVMs (the child
+ // re-runs the class lifecycle for the single targeted method). Only
+ // seed these properties in the parent — in the child they must arrive
+ // exclusively via @ForkedJvm propagation, otherwise the negative
+ // assertions below would be self-defeating.
+ if (Boolean.parseBoolean(System.getProperty('groovy.junit6.forked')))
return
+ System.setProperty(INHERIT_EXACT, 'exact-value')
+ System.setProperty(INHERIT_GLOB_A, 'a-value')
+ System.setProperty(INHERIT_GLOB_B, 'b-value')
+ }
+
+ @AfterAll
+ static void clearParentProperties() {
+ if (Boolean.parseBoolean(System.getProperty('groovy.junit6.forked')))
return
+ System.clearProperty(INHERIT_EXACT)
+ System.clearProperty(INHERIT_GLOB_A)
+ System.clearProperty(INHERIT_GLOB_B)
+ }
+
@Test
@ForkedJvm(systemProperties = ['groovy.junit6.test.example=hello'])
void runsInChildJvmWithSystemProperty() {
@@ -78,6 +105,42 @@ class ForkedJvmTest {
assertEquals('true', System.getProperty('groovy.junit6.forked'))
}
+ @Test
+ @ForkedJvm(inheritProperties = ['groovy.junit6.test.inherit.exact'])
+ void inheritsExactProperty() {
+ assertEquals('exact-value', System.getProperty(INHERIT_EXACT))
+ // Glob-prefixed siblings are NOT pulled in by an exact-match entry.
+ assertNull(System.getProperty(INHERIT_GLOB_A))
+ assertNull(System.getProperty(INHERIT_GLOB_B))
+ }
+
+ @Test
+ @ForkedJvm(inheritProperties = ['groovy.junit6.test.inherit.glob.*'])
+ void inheritsByPrefixPattern() {
+ assertEquals('a-value', System.getProperty(INHERIT_GLOB_A))
+ assertEquals('b-value', System.getProperty(INHERIT_GLOB_B))
+ // The exact-only sibling must not be matched by the glob prefix.
+ assertNull(System.getProperty(INHERIT_EXACT))
+ }
+
+ @Test
+ @ForkedJvm(
+ inheritProperties = ['groovy.junit6.test.inherit.exact'],
+ systemProperties = ['groovy.junit6.test.inherit.exact=overridden'])
+ void explicitSystemPropertyOverridesInherited() {
+ // Both supplied; explicit value wins because it is emitted last on the
+ // command line, and the JVM honours the last -D for a given key.
+ assertEquals('overridden', System.getProperty(INHERIT_EXACT))
+ }
+
+ @Test
+ @ForkedJvm(inheritProperties = ['does.not.exist.in.parent'])
+ void unmatchedInheritPatternIsSilentNoOp() {
+ // An inheritProperties entry that matches nothing in the parent JVM
+ // must not throw or pollute the child — it is a quiet no-op.
+ assertNull(System.getProperty('does.not.exist.in.parent'))
+ }
+
@Test
@ExpectedToFail(value = AssertionError, messageContains = 'expected
failure from forked child')
@ForkedJvm