diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09ed237ade58..0ba681c68077 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,17 +119,30 @@ jobs: with: result-encoding: string script: | - const milestones = await github.rest.issues.listMilestones({ + const query = ` + query ($owner: String!, $repo: String!, $title: String!) { + repository(owner: $owner, name: $repo) { + milestones(first: 100, query: $title) { + nodes { + title + number + openIssueCount + } + } + } + } + `; + const {repository} = await github.graphql(query, { owner: context.repo.owner, repo: context.repo.repo, - state: 'all' + title: "${{ inputs.releaseVersion }}" }); - const [milestone] = milestones.data.filter(x => x.title === "${{ inputs.releaseVersion }}") + const [milestone] = repository.milestones.nodes.filter(it => it.title === "${{ inputs.releaseVersion }}") if (!milestone) { throw new Error('Milestone "${{ inputs.releaseVersion }}" not found'); } - if (milestone.open_issues > 0) { - throw new Error(`Milestone "${{ inputs.releaseVersion }}" has ${milestone.open_issues} open issue(s)`); + if (milestone.openIssueCount > 0) { + throw new Error(`Milestone "${{ inputs.releaseVersion }}" has ${milestone.openIssueCount} open issue(s)`); } const requestBody = { owner: context.repo.owner, diff --git a/README.md b/README.md index 4f6249075d16..c1a30d55b379 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This repository is the home of _JUnit 5_. ## Latest Releases -- General Availability (GA): [JUnit 5.13.0](https://github.com/junit-team/junit5/releases/tag/r5.13.0) (May 30, 2025) +- General Availability (GA): [JUnit 5.13.1](https://github.com/junit-team/junit5/releases/tag/r5.13.1) (June 7, 2025) - Preview (Milestone/Release Candidate): [JUnit 5.13.0-RC1](https://github.com/junit-team/junit5/releases/tag/r5.13.0-RC1) (May 16, 2025) ## Documentation diff --git a/SECURITY.md b/SECURITY.md index 4d4bad5f6641..90102e191264 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,8 +11,8 @@ You'll find more information about the key here: [KEYS](./KEYS) | Version | Supported | |---------| ------------------ | -| 5.12.x | :white_check_mark: | -| < 5.12 | :x: | +| 5.13.x | :white_check_mark: | +| < 5.13 | :x: | ## Reporting a Vulnerability diff --git a/documentation/src/docs/asciidoc/release-notes/index.adoc b/documentation/src/docs/asciidoc/release-notes/index.adoc index 9fd3e2b75ae9..62d53b05257e 100644 --- a/documentation/src/docs/asciidoc/release-notes/index.adoc +++ b/documentation/src/docs/asciidoc/release-notes/index.adoc @@ -17,6 +17,8 @@ authors as well as build tool and IDE vendors. include::{includedir}/link-attributes.adoc[] +include::{basedir}/release-notes-5.13.1.adoc[] + include::{basedir}/release-notes-5.13.0.adoc[] include::{basedir}/release-notes-5.12.2.adoc[] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.1.adoc new file mode 100644 index 000000000000..cfd56ea3e397 --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.1.adoc @@ -0,0 +1,39 @@ +[[release-notes-5.13.1]] +== 5.13.1 + +*Date of Release:* June 7, 2025 + +*Scope:* Bug fixes and enhancements since 5.13.0 + +For a complete list of all _closed_ issues and pull requests for this release, consult the +link:{junit5-repo}+/milestone/97?closed=1+[5.13.1] milestone page in the JUnit repository +on GitHub. + + +[[release-notes-5.13.1-junit-platform]] +=== JUnit Platform + +No changes. + + +[[release-notes-5.13.1-junit-jupiter]] +=== JUnit Jupiter + +[[release-notes-5.13.1-junit-jupiter-bug-fixes]] +==== Bug Fixes + +* The 5.13.0 release introduced a regression regarding the execution order in test classes + containing both test methods and `@Nested` test classes. When classpath scanning was + used during test discovery -- for example, when resolving a package selector for a + `@Suite` class -- test methods in `@Nested` classes were executed _before_ test methods + in their enclosing class. This undesired change in behavior has now been reverted so + that tests in `@Nested` test classes are always executed _after_ tests in enclosing test + classes again. +* Fix support for `AnnotationBasedArgumentsProvider` implementations that override the + deprecated `provideArguments(ExtensionContext, Annotation)` method. + + +[[release-notes-5.13.1-junit-vintage]] +=== JUnit Vintage + +No changes diff --git a/gradle.properties b/gradle.properties index 82f60e414f01..0e69e1385cc5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,13 @@ group = org.junit -version = 5.13.0 +version = 5.13.1 jupiterGroup = org.junit.jupiter platformGroup = org.junit.platform -platformVersion = 1.13.0 +platformVersion = 1.13.1 vintageGroup = org.junit.vintage -vintageVersion = 5.13.0 +vintageVersion = 5.13.1 # We need more metaspace due to apparent memory leak in Asciidoctor/JRuby org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodOrderingVisitor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodOrderingVisitor.java index 471b9ecf8368..d1aeb152cc30 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodOrderingVisitor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodOrderingVisitor.java @@ -10,12 +10,15 @@ package org.junit.jupiter.engine.discovery; +import static java.util.Comparator.comparing; import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.Consumer; +import java.util.function.UnaryOperator; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; @@ -40,6 +43,9 @@ class MethodOrderingVisitor extends AbstractOrderingVisitor { private final JupiterConfiguration configuration; private final Condition noOrderAnnotation; + // Not a static field to avoid initialization at build time for GraalVM + private final UnaryOperator> methodsBeforeNestedClassesOrderer; + MethodOrderingVisitor(JupiterConfiguration configuration, DiscoveryIssueReporter issueReporter) { super(issueReporter); this.configuration = configuration; @@ -52,6 +58,7 @@ class MethodOrderingVisitor extends AbstractOrderingVisitor { .source(MethodSource.from(testDescriptor.getTestMethod())) // .build(); }); + this.methodsBeforeNestedClassesOrderer = createMethodsBeforeNestedClassesOrderer(); } @Override @@ -82,6 +89,7 @@ private void orderContainedMethods(ClassBasedTestDescriptor classBasedTestDescri private void orderContainedMethods(ClassBasedTestDescriptor classBasedTestDescriptor, Class testClass, Optional methodOrderer) { + DescriptorWrapperOrderer descriptorWrapperOrderer = createDescriptorWrapperOrderer( testClass, methodOrderer); @@ -91,6 +99,11 @@ private void orderContainedMethods(ClassBasedTestDescriptor classBasedTestDescri DefaultMethodDescriptor::new, // descriptorWrapperOrderer); + if (!methodOrderer.isPresent()) { + // If there is an orderer, this is ensured by the call above + classBasedTestDescriptor.orderChildren(methodsBeforeNestedClassesOrderer); + } + // Note: MethodOrderer#getDefaultExecutionMode() is guaranteed // to be invoked after MethodOrderer#orderMethods(). methodOrderer // @@ -130,4 +143,12 @@ private Optional> toValidationAction(Optiona return Optional.of(noOrderAnnotation::check); } + private static UnaryOperator> createMethodsBeforeNestedClassesOrderer() { + Comparator methodsFirst = comparing(MethodBasedTestDescriptor.class::isInstance).reversed(); + return children -> { + children.sort(methodsFirst); + return children; + }; + } + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java index 785c9e571fc4..c05c323b4fbc 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java @@ -41,6 +41,7 @@ public final class AnnotationConsumerInitializer { private static final List annotationConsumingMethodSignatures = asList( // new AnnotationConsumingMethodSignature("accept", 1, 0), // new AnnotationConsumingMethodSignature("provideArguments", 3, 2), // + new AnnotationConsumingMethodSignature("provideArguments", 2, 1), // new AnnotationConsumingMethodSignature("convert", 3, 2)); private AnnotationConsumerInitializer() { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/NestedTestClassesTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/NestedTestClassesTests.java index 2fe8352f41b8..ca36a15b1183 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/NestedTestClassesTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/NestedTestClassesTests.java @@ -13,8 +13,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; import static org.junit.platform.launcher.LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; @@ -22,19 +24,25 @@ import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; import java.util.List; +import java.util.function.Consumer; +import java.util.regex.Pattern; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.engine.NestedTestClassesTests.OuterClass.NestedClass; import org.junit.jupiter.engine.NestedTestClassesTests.OuterClass.NestedClass.RecursiveNestedClass; import org.junit.jupiter.engine.NestedTestClassesTests.OuterClass.NestedClass.RecursiveNestedSiblingClass; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; import org.junit.platform.testkit.engine.EngineExecutionResults; import org.junit.platform.testkit.engine.Events; @@ -53,20 +61,36 @@ void nestedTestsAreCorrectlyDiscovered() { assertEquals(5, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } - @Test - void nestedTestsAreExecuted() { - EngineExecutionResults executionResults = executeTestsForClass(TestCaseWithNesting.class); - Events containers = executionResults.containerEvents(); - Events tests = executionResults.testEvents(); + @ParameterizedTest(name = "{0}") + @MethodSource + void nestedTestsAreExecutedInTheRightOrder(Consumer configurer) { + EngineExecutionResults executionResults = executeTests(configurer); + Events tests = executionResults.testEvents(); assertEquals(3, tests.started().count(), "# tests started"); assertEquals(2, tests.succeeded().count(), "# tests succeeded"); assertEquals(1, tests.failed().count(), "# tests failed"); + assertThat(tests.started().map(it -> it.getTestDescriptor().getDisplayName())) // + .containsExactlyInAnyOrder("someTest()", "successful()", "failing()") // + .containsSubsequence("someTest()", "successful()") // + .containsSubsequence("someTest()", "failing()"); + + Events containers = executionResults.containerEvents(); assertEquals(3, containers.started().count(), "# containers started"); assertEquals(3, containers.finished().count(), "# containers finished"); } + static List>> nestedTestsAreExecutedInTheRightOrder() { + return List.of( // + Named.of("class selector", request -> request // + .selectors(selectClass(TestCaseWithNesting.class))), + Named.of("package selector", request -> request // + .selectors(selectPackage(TestCaseWithNesting.class.getPackageName())) // + .filters(includeClassNamePatterns(Pattern.quote(TestCaseWithNesting.class.getName()) + ".*"))) // + ); + } + @Test void doublyNestedTestsAreCorrectlyDiscovered() { LauncherDiscoveryRequest request = request().selectors(selectClass(TestCaseWithDoubleNesting.class)).build(); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java index 16306451bca4..0e14dd36283a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java @@ -23,17 +23,21 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.converter.AnnotationBasedArgumentConverter; import org.junit.jupiter.params.converter.JavaTimeConversionPattern; import org.junit.jupiter.params.provider.AnnotationBasedArgumentsProvider; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.FieldSource; import org.junit.platform.commons.JUnitException; @DisplayName("AnnotationConsumerInitializer") @@ -51,10 +55,11 @@ void shouldInitializeAnnotationConsumer() throws NoSuchMethodException { source -> assertThat(source.value()).containsExactly("a", "b")); } - @Test + @ParameterizedTest + @FieldSource("argumentsProviders") @DisplayName("should initialize annotation-based ArgumentsProvider") - void shouldInitializeAnnotationBasedArgumentsProvider() throws NoSuchMethodException { - var instance = new SomeAnnotationBasedArgumentsProvider(); + void shouldInitializeAnnotationBasedArgumentsProvider(AbstractAnnotationBasedArgumentsProvider instance) + throws NoSuchMethodException { var method = SubjectClass.class.getDeclaredMethod("foo"); var initialisedAnnotationConsumer = initialize(method, instance); @@ -101,9 +106,11 @@ void shouldThrowExceptionWhenParameterIsNotAnnotated() throws NoSuchMethodExcept assertThatThrownBy(() -> initialize(parameter, instance)).isInstanceOf(JUnitException.class); } - @Test - void shouldInitializeForEachAnnotations() throws NoSuchMethodException { - var instance = spy(new SomeAnnotationBasedArgumentsProvider()); + @ParameterizedTest + @FieldSource("argumentsProviders") + void shouldInitializeForEachAnnotations(AbstractAnnotationBasedArgumentsProvider provider) + throws NoSuchMethodException { + var instance = spy(provider); var method = SubjectClass.class.getDeclaredMethod("repeatableAnnotation", String.class); initialize(method, instance); @@ -111,10 +118,20 @@ void shouldInitializeForEachAnnotations() throws NoSuchMethodException { verify(instance, times(2)).accept(any(CsvSource.class)); } - private static class SomeAnnotationBasedArgumentsProvider extends AnnotationBasedArgumentsProvider { + static Supplier>> argumentsProviders = () -> List.of( // + Named.of("current", new SomeAnnotationBasedArgumentsProvider()), // + Named.of("deprecated", new DeprecatedAnnotationBasedArgumentsProvider()) // + ); + + private static abstract class AbstractAnnotationBasedArgumentsProvider + extends AnnotationBasedArgumentsProvider { List annotations = new ArrayList<>(); + } + + private static class SomeAnnotationBasedArgumentsProvider extends AbstractAnnotationBasedArgumentsProvider { + @Override protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, CsvSource annotation) { @@ -123,6 +140,16 @@ protected Stream provideArguments(ParameterDeclarations par } } + private static class DeprecatedAnnotationBasedArgumentsProvider extends AbstractAnnotationBasedArgumentsProvider { + + @Override + @SuppressWarnings("deprecation") + protected Stream provideArguments(ExtensionContext context, CsvSource annotation) { + annotations.add(annotation); + return Stream.empty(); + } + } + private static class SomeAnnotationBasedArgumentConverter extends AnnotationBasedArgumentConverter {