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;
  * &#64;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>
+ * &#64;Test
+ * &#64;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>
+ * &#64;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

Reply via email to