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} — {@code System.getenv()}</li>
+ * <li>{@code systemProperties} — {@code System.getProperties()}</li>
+ * <li>{@code javaVersion} — Runtime Java feature version
(e.g. 17, 21)</li>
+ * <li>{@code junitTags} — tags assigned to the test</li>
+ * <li>{@code junitDisplayName} — display name of the test</li>
+ * <li>{@code junitUniqueId} — unique ID of the test</li>
+ * </ul>
+ * <p>
+ * Example usage:
+ * <pre>
+ * @Test
+ * @GroovyDisabledIf({ systemProperties['os.arch']?.contains('32') })
+ * void not32Bit() { ... }
+ *
+ * @Test
+ * @GroovyDisabledIf({ 'slow' in junitTags &&
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} — {@code System.getenv()}</li>
+ * <li>{@code systemProperties} — {@code System.getProperties()}</li>
+ * <li>{@code javaVersion} — Runtime Java feature version
(e.g. 17, 21)</li>
+ * <li>{@code junitTags} — tags assigned to the test</li>
+ * <li>{@code junitDisplayName} — display name of the test</li>
+ * <li>{@code junitUniqueId} — unique ID of the test</li>
+ * </ul>
+ * <p>
+ * Example usage:
+ * <pre>
+ * @Test
+ * @GroovyEnabledIf({ javaVersion >= 21 })
+ * void needsVirtualThreads() { ... }
+ *
+ * @Test
+ * @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)
+ }
+}