This is an automated email from the ASF dual-hosted git repository.

paulk 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 fc72bbc183 GROOVY-11887: Scripting support for JUnit Jupiter 
conditional execution
fc72bbc183 is described below

commit fc72bbc1830822c7e275930c37ed056b974c1be0
Author: Paul King <[email protected]>
AuthorDate: Sat Mar 28 22:20:59 2026 +1000

    GROOVY-11887: Scripting support for JUnit Jupiter conditional execution
---
 .../junit5/plugin/GroovyJUnitRunnerHelper.groovy   |  2 +-
 .../plugin/ConditionEvaluationContext.groovy       | 48 +++++++++++
 .../junit6/plugin/GroovyConditionExtension.groovy  | 85 +++++++++++++++++++
 .../groovy/junit6/plugin/GroovyDisabledIf.groovy   | 66 +++++++++++++++
 .../groovy/junit6/plugin/GroovyEnabledIf.groovy    | 66 +++++++++++++++
 .../src/test/groovy/GroovyConditionTest.groovy     | 94 ++++++++++++++++++++++
 6 files changed, 360 insertions(+), 1 deletion(-)

diff --git 
a/subprojects/groovy-test-junit5/src/main/groovy/groovy/junit5/plugin/GroovyJUnitRunnerHelper.groovy
 
b/subprojects/groovy-test-junit5/src/main/groovy/groovy/junit5/plugin/GroovyJUnitRunnerHelper.groovy
index 9b599a9ef5..7493c9aa4a 100644
--- 
a/subprojects/groovy-test-junit5/src/main/groovy/groovy/junit5/plugin/GroovyJUnitRunnerHelper.groovy
+++ 
b/subprojects/groovy-test-junit5/src/main/groovy/groovy/junit5/plugin/GroovyJUnitRunnerHelper.groovy
@@ -39,7 +39,7 @@ class GroovyJUnitRunnerHelper {
         launcher.registerTestExecutionListeners(listener)
         
launcher.registerTestExecutionListeners(LoggingListener.forJavaUtilLogging())
         launcher.execute(request)
-        println listener.summary.with{ "JUnit5 launcher: 
passed=$testsSucceededCount, aborted=$testsAbortedCount, 
failed=$testsFailedCount, skipped=$testsSkippedCount, 
time=${timeFinished-timeStarted}ms" }
+        println listener.summary.with{ "JUnit Jupiter launcher: 
passed=$testsSucceededCount, aborted=$testsAbortedCount, 
failed=$testsFailedCount, skipped=$testsSkippedCount, 
time=${timeFinished-timeStarted}ms" }
         if (listener.summary.failures) {
             listener.summary.printFailuresTo(new PrintWriter(System.out, true))
             return listener.summary.failures[0].exception
diff --git 
a/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/ConditionEvaluationContext.groovy
 
b/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/ConditionEvaluationContext.groovy
new file mode 100644
index 0000000000..366480f157
--- /dev/null
+++ 
b/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/ConditionEvaluationContext.groovy
@@ -0,0 +1,48 @@
+/*
+ *  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 groovy.junit6.plugin
+
+import groovy.transform.CompileStatic
+import org.junit.jupiter.api.extension.ExtensionContext
+
+/**
+ * Delegate object for {@link GroovyEnabledIf} and {@link GroovyDisabledIf} 
closures,
+ * providing convenient access to environment, system properties, and JUnit 
context.
+ *
+ * @since 6.0.0
+ */
+@CompileStatic
+class ConditionEvaluationContext {
+
+    final Map<String, String> systemEnvironment
+    final Properties systemProperties
+    final int javaVersion
+    final Set<String> junitTags
+    final String junitDisplayName
+    final String junitUniqueId
+
+    ConditionEvaluationContext(ExtensionContext extensionContext) {
+        systemEnvironment = System.getenv()
+        systemProperties = System.properties
+        javaVersion = Runtime.version().feature()
+        junitTags = extensionContext.tags
+        junitDisplayName = extensionContext.displayName
+        junitUniqueId = extensionContext.uniqueId
+    }
+}
diff --git 
a/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/GroovyConditionExtension.groovy
 
b/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/GroovyConditionExtension.groovy
new file mode 100644
index 0000000000..04ffeaba73
--- /dev/null
+++ 
b/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/GroovyConditionExtension.groovy
@@ -0,0 +1,85 @@
+/*
+ *  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 groovy.junit6.plugin
+
+import groovy.transform.CompileStatic
+import org.junit.jupiter.api.extension.ConditionEvaluationResult
+import org.junit.jupiter.api.extension.ExecutionCondition
+import org.junit.jupiter.api.extension.ExtensionContext
+
+import java.lang.reflect.AnnotatedElement
+
+/**
+ * JUnit {@link ExecutionCondition} that evaluates Groovy closures from
+ * {@link GroovyEnabledIf} and {@link GroovyDisabledIf} annotations.
+ *
+ * @since 6.0.0
+ */
+@CompileStatic
+class GroovyConditionExtension implements ExecutionCondition {
+
+    @Override
+    ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext 
context) {
+        AnnotatedElement element = context.element.orElse(null)
+        if (element == null) {
+            return ConditionEvaluationResult.enabled('No element to evaluate')
+        }
+
+        GroovyEnabledIf enabledIf = element.getAnnotation(GroovyEnabledIf)
+        if (enabledIf != null) {
+            return evaluateEnabledIf(enabledIf, context)
+        }
+
+        GroovyDisabledIf disabledIf = element.getAnnotation(GroovyDisabledIf)
+        if (disabledIf != null) {
+            return evaluateDisabledIf(disabledIf, context)
+        }
+
+        ConditionEvaluationResult.enabled('No Groovy condition annotation 
found')
+    }
+
+    private static ConditionEvaluationResult evaluateEnabledIf(GroovyEnabledIf 
annotation, ExtensionContext context) {
+        boolean result = evaluateClosure(annotation.value(), context)
+        if (result) {
+            return ConditionEvaluationResult.enabled(
+                    annotation.reason() ?: 'Groovy condition evaluated to 
true')
+        }
+        ConditionEvaluationResult.disabled(
+                annotation.reason() ?: 'Groovy condition evaluated to false')
+    }
+
+    private static ConditionEvaluationResult 
evaluateDisabledIf(GroovyDisabledIf annotation, ExtensionContext context) {
+        boolean result = evaluateClosure(annotation.value(), context)
+        if (result) {
+            return ConditionEvaluationResult.disabled(
+                    annotation.reason() ?: 'Groovy condition evaluated to 
true')
+        }
+        ConditionEvaluationResult.enabled(
+                annotation.reason() ?: 'Groovy condition evaluated to false')
+    }
+
+    private static boolean evaluateClosure(Class closureClass, 
ExtensionContext context) {
+        def delegate = new ConditionEvaluationContext(context)
+        Closure closure = (Closure) closureClass.getConstructor(Object, Object)
+                .newInstance(null, null)
+        closure.delegate = delegate
+        closure.resolveStrategy = Closure.DELEGATE_FIRST
+        closure.call() as boolean
+    }
+}
diff --git 
a/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/GroovyDisabledIf.groovy
 
b/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/GroovyDisabledIf.groovy
new file mode 100644
index 0000000000..7be2b6bbde
--- /dev/null
+++ 
b/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/GroovyDisabledIf.groovy
@@ -0,0 +1,66 @@
+/*
+ *  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 groovy.junit6.plugin
+
+import org.apache.groovy.lang.annotation.Incubating
+import org.junit.jupiter.api.extension.ExtendWith
+
+import java.lang.annotation.Documented
+import java.lang.annotation.ElementType
+import java.lang.annotation.Retention
+import java.lang.annotation.RetentionPolicy
+import java.lang.annotation.Target
+
+/**
+ * Disables the annotated test class or method if the Groovy closure evaluates 
to {@code true}.
+ * <p>
+ * The closure is evaluated with a delegate providing the following bindings:
+ * <ul>
+ *   <li>{@code systemEnvironment} &mdash; {@code System.getenv()}</li>
+ *   <li>{@code systemProperties} &mdash; {@code System.getProperties()}</li>
+ *   <li>{@code javaVersion} &mdash; Runtime Java feature version 
(e.g.&nbsp;17, 21)</li>
+ *   <li>{@code junitTags} &mdash; tags assigned to the test</li>
+ *   <li>{@code junitDisplayName} &mdash; display name of the test</li>
+ *   <li>{@code junitUniqueId} &mdash; unique ID of the test</li>
+ * </ul>
+ * <p>
+ * Example usage:
+ * <pre>
+ * &#64;Test
+ * &#64;GroovyDisabledIf({ systemProperties['os.arch']?.contains('32') })
+ * void not32Bit() { ... }
+ *
+ * &#64;Test
+ * &#64;GroovyDisabledIf({ 'slow' in junitTags &amp;&amp; 
systemEnvironment['CI'] == 'true' })
+ * void skipSlowOnCI() { ... }
+ * </pre>
+ *
+ * @since 6.0.0
+ * @see GroovyEnabledIf
+ */
+@Documented
+@Incubating
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+@ExtendWith(GroovyConditionExtension.class)
+@interface GroovyDisabledIf {
+    Class value()
+
+    String reason() default ""
+}
diff --git 
a/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/GroovyEnabledIf.groovy
 
b/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/GroovyEnabledIf.groovy
new file mode 100644
index 0000000000..7c515e2b15
--- /dev/null
+++ 
b/subprojects/groovy-test-junit6/src/main/groovy/groovy/junit6/plugin/GroovyEnabledIf.groovy
@@ -0,0 +1,66 @@
+/*
+ *  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 groovy.junit6.plugin
+
+import org.apache.groovy.lang.annotation.Incubating
+import org.junit.jupiter.api.extension.ExtendWith
+
+import java.lang.annotation.Documented
+import java.lang.annotation.ElementType
+import java.lang.annotation.Retention
+import java.lang.annotation.RetentionPolicy
+import java.lang.annotation.Target
+
+/**
+ * Enables the annotated test class or method if the Groovy closure evaluates 
to {@code true}.
+ * <p>
+ * The closure is evaluated with a delegate providing the following bindings:
+ * <ul>
+ *   <li>{@code systemEnvironment} &mdash; {@code System.getenv()}</li>
+ *   <li>{@code systemProperties} &mdash; {@code System.getProperties()}</li>
+ *   <li>{@code javaVersion} &mdash; Runtime Java feature version 
(e.g.&nbsp;17, 21)</li>
+ *   <li>{@code junitTags} &mdash; tags assigned to the test</li>
+ *   <li>{@code junitDisplayName} &mdash; display name of the test</li>
+ *   <li>{@code junitUniqueId} &mdash; unique ID of the test</li>
+ * </ul>
+ * <p>
+ * Example usage:
+ * <pre>
+ * &#64;Test
+ * &#64;GroovyEnabledIf({ javaVersion >= 21 })
+ * void needsVirtualThreads() { ... }
+ *
+ * &#64;Test
+ * &#64;GroovyEnabledIf({ systemEnvironment['CI'] == 'true' })
+ * void onlyOnCI() { ... }
+ * </pre>
+ *
+ * @since 6.0.0
+ * @see GroovyDisabledIf
+ */
+@Documented
+@Incubating
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+@ExtendWith(GroovyConditionExtension)
+@interface GroovyEnabledIf {
+    Class value()
+
+    String reason() default ""
+}
diff --git 
a/subprojects/groovy-test-junit6/src/test/groovy/GroovyConditionTest.groovy 
b/subprojects/groovy-test-junit6/src/test/groovy/GroovyConditionTest.groovy
new file mode 100644
index 0000000000..40c803ff2f
--- /dev/null
+++ b/subprojects/groovy-test-junit6/src/test/groovy/GroovyConditionTest.groovy
@@ -0,0 +1,94 @@
+/*
+ *  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 groovy.junit6.plugin.GroovyDisabledIf
+import groovy.junit6.plugin.GroovyEnabledIf
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.assertTrue
+import static org.junit.jupiter.api.Assertions.fail
+
+class GroovyConditionTest {
+
+    @Test
+    @GroovyEnabledIf({ true })
+    void enabledIfTrue() {
+        assertTrue(true)
+    }
+
+    @Test
+    @GroovyEnabledIf({ false })
+    void enabledIfFalseSkipped() {
+        fail('This test should be skipped')
+    }
+
+    @Test
+    @GroovyDisabledIf({ true })
+    void disabledIfTrueSkipped() {
+        fail('This test should be skipped')
+    }
+
+    @Test
+    @GroovyDisabledIf({ false })
+    void disabledIfFalse() {
+        assertTrue(true)
+    }
+
+    @Test
+    @GroovyEnabledIf({ javaVersion >= 17 })
+    void enabledOnJava17Plus() {
+        assertTrue(Runtime.version().feature() >= 17)
+    }
+
+    @Test
+    @GroovyDisabledIf({ javaVersion < 10 })
+    void notDisabledOnModernJava() {
+        assertTrue(Runtime.version().feature() >= 10)
+    }
+
+    @Test
+    @GroovyEnabledIf({ systemProperties['os.name'] != null })
+    void enabledWithSystemProperty() {
+        assertTrue(System.getProperty('os.name') != null)
+    }
+
+    @Test
+    @GroovyEnabledIf({ systemEnvironment instanceof Map })
+    void enabledWithEnvironment() {
+        assertTrue(true)
+    }
+
+    @Test
+    @GroovyEnabledIf({ junitDisplayName != null })
+    void enabledWithJunitContext() {
+        assertTrue(true)
+    }
+
+    @Test
+    @GroovyEnabledIf({ 2 * 3 == 6 })
+    void enabledWithExpression() {
+        assertTrue(true)
+    }
+
+    @Test
+    @GroovyDisabledIf({ systemProperties['os.arch']?.contains('NONEXISTENT') })
+    void notDisabledForNonMatchingArch() {
+        assertTrue(true)
+    }
+}

Reply via email to