Skip to content

Commit

Permalink
Support arbitrary Java feature numbers with JRE conditions
Browse files Browse the repository at this point in the history
This commit serves as a proof of concept for supporting arbitrary Java
feature numbers in @⁠EnabledOnJre, @⁠EnabledForJreRange, and the JRE enum.

See junit-team#3930
  • Loading branch information
sbrannen committed Aug 20, 2024
1 parent d85dd9b commit 1f2af32
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -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)
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,6 +91,7 @@
* supported JRE version.
*
* @see JRE
* @see #minFeatureVersion()
*/
JRE min() default JRE.JAVA_8;

Expand All @@ -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.
*
* <p>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.
*
* <p>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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,25 @@ class EnabledForJreRangeCondition extends BooleanExecutionCondition<EnabledForJr

@Override
boolean isEnabled(EnabledForJreRange annotation) {
JRE min = annotation.min();
JRE max = annotation.max();

Preconditions.condition((min != JRE.JAVA_8 || max != JRE.OTHER),
"You must declare a non-default value for min or max in @EnabledForJreRange");
Preconditions.condition(max.compareTo(min) >= 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ class EnabledOnJreCondition extends BooleanExecutionCondition<EnabledOnJre> {

@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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <%--
Expand All @@ -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);

Expand All @@ -77,7 +82,7 @@ public enum JRE {
}

if (!javaVersionIsBlank && javaVersion.startsWith("1.8")) {
return JAVA_8;
return 8;
}

try {
Expand All @@ -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;
}

/**
Expand All @@ -116,6 +130,19 @@ public enum JRE {
return this == CURRENT_VERSION;
}

/**
* Get the feature version of <em>this</em> {@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}
Expand All @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

/**
Expand Down

0 comments on commit 1f2af32

Please sign in to comment.