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"); } /**