From 1f2af32c093c4d9a10b76f44eb811610a9d00232 Mon Sep 17 00:00:00 2001
From: Sam Brannen <104798+sbrannen@users.noreply.github.com>
Date: Tue, 20 Aug 2024 11:24:13 +0200
Subject: [PATCH] Support arbitrary Java feature numbers with JRE conditions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This commit serves as a proof of concept for supporting arbitrary Java
feature numbers in @EnabledOnJre, @EnabledForJreRange, and the JRE enum.
See #3930
---
.../example/ConditionalTestExecutionDemo.java | 53 +++++++++++
.../api/condition/EnabledForJreRange.java | 29 ++++++
.../EnabledForJreRangeCondition.java | 26 ++++--
.../jupiter/api/condition/EnabledOnJre.java | 14 ++-
.../api/condition/EnabledOnJreCondition.java | 9 +-
.../junit/jupiter/api/condition/JRE.java.jte | 91 +++++++++++++++----
.../EnabledForJreRangeConditionTests.java | 3 +-
7 files changed, 195 insertions(+), 30 deletions(-)
diff --git a/documentation/src/test/java/example/ConditionalTestExecutionDemo.java b/documentation/src/test/java/example/ConditionalTestExecutionDemo.java
index c2011653287a..7d7ed85d1f39 100644
--- a/documentation/src/test/java/example/ConditionalTestExecutionDemo.java
+++ b/documentation/src/test/java/example/ConditionalTestExecutionDemo.java
@@ -12,6 +12,8 @@
import static org.junit.jupiter.api.condition.JRE.JAVA_10;
import static org.junit.jupiter.api.condition.JRE.JAVA_11;
+import static org.junit.jupiter.api.condition.JRE.JAVA_17;
+import static org.junit.jupiter.api.condition.JRE.JAVA_18;
import static org.junit.jupiter.api.condition.JRE.JAVA_8;
import static org.junit.jupiter.api.condition.JRE.JAVA_9;
import static org.junit.jupiter.api.condition.OS.LINUX;
@@ -23,6 +25,7 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.DisabledIf;
@@ -38,6 +41,7 @@
import org.junit.jupiter.api.condition.EnabledInNativeImage;
import org.junit.jupiter.api.condition.EnabledOnJre;
import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.JRE;
class ConditionalTestExecutionDemo {
@@ -99,6 +103,16 @@ void notOnNewMacs() {
}
// end::user_guide_architecture[]
+ @Test
+ @EnabledOnJre(value = { JAVA_17, JAVA_18 }, featureVersions = { 20, 21 })
+ void onJava17or18or20or21() {
+ }
+
+ @Test
+ @EnabledOnJre(featureVersions = 21)
+ void onlyOnJava21() {
+ }
+
// tag::user_guide_jre[]
@Test
@EnabledOnJre(JAVA_8)
@@ -124,6 +138,45 @@ void fromJava9toCurrentJavaFeatureNumber() {
// ...
}
+ @Test
+ @EnabledForJreRange(minFeatureVersion = 10)
+ void fromJava10toCurrentJavaFeatureNumber() {
+ // ...
+ }
+
+ @Test
+ @EnabledForJreRange(minFeatureVersion = 25)
+ void fromJava25toCurrentJavaFeatureNumber() {
+ // ...
+ }
+
+ @Disabled("DEMO: intended to fail")
+ @Test
+ @EnabledForJreRange(minFeatureVersion = 99, max = JRE.JAVA_17)
+ void fromJava99toJava17() {
+ // ...
+ }
+
+ @Disabled("DEMO: intended to fail")
+ @Test
+ @EnabledForJreRange(min = JAVA_11, minFeatureVersion = 10)
+ void competingJreAndMinFeatureVersions() {
+ // ...
+ }
+
+ @Disabled("DEMO: intended to fail")
+ @Test
+ @EnabledForJreRange(max = JAVA_11, maxFeatureVersion = 10)
+ void competingJreAndMaxFeatureVersions() {
+ // ...
+ }
+
+ @Test
+ @EnabledForJreRange(minFeatureVersion = 10, maxFeatureVersion = 25)
+ void fromJava17to25() {
+ // ...
+ }
+
@Test
@EnabledForJreRange(max = JAVA_11)
void fromJava8To11() {
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledForJreRange.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledForJreRange.java
index 1ccd390c08e8..136b6bbd5abf 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledForJreRange.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledForJreRange.java
@@ -10,6 +10,7 @@
package org.junit.jupiter.api.condition;
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;
import java.lang.annotation.Documented;
@@ -90,6 +91,7 @@
* supported JRE version.
*
* @see JRE
+ * @see #minFeatureVersion()
*/
JRE min() default JRE.JAVA_8;
@@ -102,9 +104,36 @@
* possible version.
*
* @see JRE
+ * @see #maxFeatureVersion()
*/
JRE max() default JRE.OTHER;
+ /**
+ * Java Runtime Environment feature version which should be used as the lower
+ * boundary for the version range that determines if the annotated class or
+ * method should be enabled.
+ *
+ *
Defaults to {@code -1} to signal that {@link #min()} should be used instead.
+ *
+ * @since 5.12
+ * @see #min()
+ */
+ @API(status = EXPERIMENTAL, since = "5.12")
+ int minFeatureVersion() default -1;
+
+ /**
+ * Java Runtime Environment feature version which should be used as the upper
+ * boundary for the version range that determines if the annotated class or
+ * method should be enabled.
+ *
+ *
Defaults to {@code -1} to signal that {@link #max()} should be used instead.
+ *
+ * @since 5.12
+ * @see #max()
+ */
+ @API(status = EXPERIMENTAL, since = "5.12")
+ int maxFeatureVersion() default -1;
+
/**
* Custom reason to provide if the test or container is disabled.
*
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledForJreRangeCondition.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledForJreRangeCondition.java
index 0c7c0187778e..f6cd8a2149d1 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledForJreRangeCondition.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledForJreRangeCondition.java
@@ -31,13 +31,25 @@ class EnabledForJreRangeCondition extends BooleanExecutionCondition= 0,
- "@EnabledForJreRange.min must be less than or equal to @EnabledForJreRange.max");
+ JRE minJre = annotation.min();
+ JRE maxJre = annotation.max();
+ int minFeatureVersion = annotation.minFeatureVersion();
+ int maxFeatureVersion = annotation.maxFeatureVersion();
+
+ Preconditions.condition(!(minJre != JRE.JAVA_8 && minFeatureVersion != -1),
+ "@EnabledForJreRange's minimum value must be configured with either a JRE or feature version, but not both");
+ Preconditions.condition(!(maxJre != JRE.OTHER && maxFeatureVersion != -1),
+ "@EnabledForJreRange's maximum value must be configured with either a JRE or feature version, but not both");
+
+ boolean minValueConfigured = minJre != JRE.JAVA_8 || minFeatureVersion != -1;
+ boolean maxValueConfigured = maxJre != JRE.OTHER || maxFeatureVersion != -1;
+ Preconditions.condition(minValueConfigured || maxValueConfigured,
+ "You must declare a non-default value for the minimum or maximum value in @EnabledForJreRange");
+
+ int min = (minFeatureVersion != -1 ? minFeatureVersion : minJre.featureVersion());
+ int max = (maxFeatureVersion != -1 ? maxFeatureVersion : maxJre.featureVersion());
+ Preconditions.condition(min <= max,
+ "@EnabledForJreRange's minimum value must be less than or equal to its maximum value");
return JRE.isCurrentVersionWithinRange(min, max);
}
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnJre.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnJre.java
index b37776e31ee6..be0cc0019ca8 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnJre.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnJre.java
@@ -10,6 +10,7 @@
package org.junit.jupiter.api.condition;
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;
import java.lang.annotation.Documented;
@@ -86,8 +87,19 @@
* method should be enabled.
*
* @see JRE
+ * @see #featureVersions()
*/
- JRE[] value();
+ JRE[] value() default {};
+
+ /**
+ * Java Runtime Environment feature versions on which the annotated class or
+ * method should be enabled.
+ *
+ * @since 5.12
+ * @see #value()
+ */
+ @API(status = EXPERIMENTAL, since = "5.12")
+ int[] featureVersions() default {};
/**
* Custom reason to provide if the test or container is disabled.
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnJreCondition.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnJreCondition.java
index 70c0b3867a66..1650c1eae9c2 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnJreCondition.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnJreCondition.java
@@ -35,9 +35,12 @@ class EnabledOnJreCondition extends BooleanExecutionCondition {
@Override
boolean isEnabled(EnabledOnJre annotation) {
- JRE[] versions = annotation.value();
- Preconditions.condition(versions.length > 0, "You must declare at least one JRE in @EnabledOnJre");
- return Arrays.stream(versions).anyMatch(JRE::isCurrentVersion);
+ JRE[] jres = annotation.value();
+ int[] featureVersions = annotation.featureVersions();
+ Preconditions.condition(jres.length > 0 || featureVersions.length > 0,
+ "You must declare at least one JRE or feature version in @EnabledOnJre");
+ return Arrays.stream(jres).anyMatch(JRE::isCurrentVersion)
+ || Arrays.stream(featureVersions).anyMatch(JRE::isCurrentFeatureVersion);
}
}
diff --git a/junit-jupiter-api/src/templates/resources/main/org/junit/jupiter/api/condition/JRE.java.jte b/junit-jupiter-api/src/templates/resources/main/org/junit/jupiter/api/condition/JRE.java.jte
index 93479da1378f..61b52ca44f1f 100644
--- a/junit-jupiter-api/src/templates/resources/main/org/junit/jupiter/api/condition/JRE.java.jte
+++ b/junit-jupiter-api/src/templates/resources/main/org/junit/jupiter/api/condition/JRE.java.jte
@@ -7,6 +7,7 @@
${licenseHeader}
package org.junit.jupiter.api.condition;
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;
import java.lang.reflect.Method;
@@ -49,7 +50,7 @@ public enum JRE {
@if(jre.getSince() != null)<%--
--%>@API(status = STABLE, since = "${jre.getSince()}")
@endif<%--
---%>JAVA_${jre.getVersion()},
+--%>JAVA_${jre.getVersion()}(${jre.getVersion()}),
@endfor
/**
* A JRE version other than <%--
@@ -61,13 +62,17 @@ public enum JRE {
* @elseif(!jre.isLast()) @endif<%--
--%>@endfor
*/
- OTHER;
+ OTHER(Integer.MAX_VALUE);
private static final Logger logger = LoggerFactory.getLogger(JRE.class);
- private static final JRE CURRENT_VERSION = determineCurrentVersion();
+ private static final int UNKNOWN_FEATURE_VERSION = -1;
- private static JRE determineCurrentVersion() {
+ private static final int CURRENT_FEATURE_VERSION = determineCurrentFeatureVersion();
+
+ private static final JRE CURRENT_VERSION = determineCurrentVersion(CURRENT_FEATURE_VERSION);
+
+ private static int determineCurrentFeatureVersion() {
String javaVersion = System.getProperty("java.version");
boolean javaVersionIsBlank = StringUtils.isBlank(javaVersion);
@@ -77,7 +82,7 @@ public enum JRE {
}
if (!javaVersionIsBlank && javaVersion.startsWith("1.8")) {
- return JAVA_8;
+ return 8;
}
try {
@@ -87,24 +92,33 @@ public enum JRE {
Method versionMethod = Runtime.class.getMethod("version");
Object version = ReflectionSupport.invokeMethod(versionMethod, null);
Method majorMethod = version.getClass().getMethod("major");
- int major = (int) ReflectionSupport.invokeMethod(majorMethod, version);
- switch (major) {<%--
- --%>@for(var jre : jres)<%--
- --%>@if(jre.getVersion() != 8)
- case ${jre.getVersion()}:
- return JAVA_${jre.getVersion()};<%--
- --%>@endif<%--
- --%>@endfor
- default:
- return OTHER;
- }
+ return (int) ReflectionSupport.invokeMethod(majorMethod, version);
}
catch (Exception ex) {
logger.debug(ex, () -> "Failed to determine the current JRE version via java.lang.Runtime.Version.");
}
- // null signals that the current JRE version is "unknown"
- return null;
+ return UNKNOWN_FEATURE_VERSION;
+ }
+
+ private static JRE determineCurrentVersion(int currentFeatureVersion) {
+ switch (currentFeatureVersion) {
+ case UNKNOWN_FEATURE_VERSION:
+ // null signals that the current JRE version is "unknown"
+ return null;<%--
+ --%>@for(var jre : jres)
+ case ${jre.getVersion()}:
+ return JAVA_${jre.getVersion()};<%--
+ --%>@endfor
+ default:
+ return OTHER;
+ }
+ }
+
+ private final int featureVersion;
+
+ private JRE(int featureVersion) {
+ this.featureVersion = featureVersion;
}
/**
@@ -116,6 +130,19 @@ public enum JRE {
return this == CURRENT_VERSION;
}
+ /**
+ * Get the feature version of this {@code JRE}.
+ *
+ * @return the feature version of this {@code JRE}, or
+ * {@link Integer#MAX_VALUE} if this {@code JRE} is {@link #OTHER}
+ *
+ * @since 5.12
+ */
+ @API(status = EXPERIMENTAL, since = "5.12")
+ public int featureVersion() {
+ return this.featureVersion;
+ }
+
/**
* @return the {@link JRE} for the currently executing JVM, potentially
* {@link #OTHER}
@@ -127,8 +154,36 @@ public enum JRE {
return CURRENT_VERSION;
}
+ /**
+ * @return the feature version for the currently executing JVM, or
+ * {@code -1} to signal that the feature version is unknown
+ *
+ * @since 5.12
+ */
+ @API(status = EXPERIMENTAL, since = "5.12")
+ public static int currentFeatureVersion() {
+ return CURRENT_FEATURE_VERSION;
+ }
+
+ /**
+ * @return {@code true} if the supplied feature version is known to be
+ * the Java Runtime Environment version for the currently executing JVM
+ * or if the supplied feature version is {@code -1} and the feature
+ * version of the current JVM is unknown
+ *
+ * @since 5.12
+ */
+ @API(status = EXPERIMENTAL, since = "5.12")
+ public static boolean isCurrentFeatureVersion(int featureVersion) {
+ return featureVersion == CURRENT_FEATURE_VERSION;
+ }
+
static boolean isCurrentVersionWithinRange(JRE min, JRE max) {
return EnumSet.range(min, max).contains(CURRENT_VERSION);
}
+ static boolean isCurrentVersionWithinRange(int min, int max) {
+ return CURRENT_FEATURE_VERSION >= min && CURRENT_FEATURE_VERSION <= max;
+ }
+
}
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/condition/EnabledForJreRangeConditionTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/condition/EnabledForJreRangeConditionTests.java
index 03b870b462ea..881a68119cca 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/api/condition/EnabledForJreRangeConditionTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/condition/EnabledForJreRangeConditionTests.java
@@ -66,7 +66,8 @@ void enabledBecauseAnnotationIsNotPresent() {
void defaultValues() {
assertThatExceptionOfType(PreconditionViolationException.class)//
.isThrownBy(this::evaluateCondition)//
- .withMessageContaining("You must declare a non-default value for min or max in @EnabledForJreRange");
+ .withMessageContaining(
+ "You must declare a non-default value for the minimum or maximum value in @EnabledForJreRange");
}
/**