Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support arbitrary Java feature versions with JRE conditions #3931

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linking "feature version" to java.lang.Runtime.Version.feature() would help to connect some dots. Perhaps you can't given your Java baseline?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning on documenting that more thoroughly, though I'm glad you made the connection.

We're currently invoking the deprecated major() method defensively via reflection for support on Java 9+, and I picked the "feature" terminology because of the new feature() method in Java 10+.

So I plan to explain that to some extent.

As for linking to the newer API, I think that actually might not be an issue, because I believe we are generating Javadoc with a JDK version later than JDK 8 (perhaps JDK 11 or later -- I'll have to check).

* 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()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to @see JRE#featureVersion() here to connect the two in the reader's mind.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I'll improve the Javadoc once we've determined if we want to implement this feature.

*/
@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
Loading