diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 43754d118b7c..f7a7ef835937 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,13 @@ jobs: run: | sudo apt-get update sudo apt-get install graphviz + - name: Install GraalVM + uses: graalvm/setup-graalvm@v1 + with: + version: 'latest' + java-version: '17' + components: 'native-image' + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Build uses: ./.github/actions/main-build with: diff --git a/README.md b/README.md index aa04df17a909..ecf74ef5e4d5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This repository is the home of the next generation of JUnit, _JUnit 5_. ## Latest Releases -- General Availability (GA): [JUnit 5.9.0](https://github.com/junit-team/junit5/releases/tag/r5.9.0) (July 26, 2022) +- General Availability (GA): [JUnit 5.9.1](https://github.com/junit-team/junit5/releases/tag/r5.9.1) (September 20, 2022) - Preview (Milestone/Release Candidate): n/a ## Documentation diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index ef4f4f161e5e..9fd96fb5833b 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,8 +15,8 @@ dependencies { implementation("com.diffplug.spotless:spotless-plugin-gradle:6.0.0") implementation("com.github.ben-manes:gradle-versions-plugin:0.39.0") implementation("gradle.plugin.com.github.johnrengelman:shadow:7.1.2") - implementation("org.gradle:test-retry-gradle-plugin:1.3.1") - compileOnly("com.gradle.enterprise:test-distribution-gradle-plugin:2.3.5") // keep in sync with root settings.gradle.kts + implementation("org.gradle:test-retry-gradle-plugin:1.4.1") + compileOnly("com.gradle:gradle-enterprise-gradle-plugin:3.11.1") // keep in sync with root settings.gradle.kts } tasks { diff --git a/buildSrc/src/main/kotlin/java-toolchain-conventions.gradle.kts b/buildSrc/src/main/kotlin/java-toolchain-conventions.gradle.kts index e72c2244497b..c85e77e260f2 100644 --- a/buildSrc/src/main/kotlin/java-toolchain-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/java-toolchain-conventions.gradle.kts @@ -11,6 +11,13 @@ project.pluginManager.withPlugin("java") { } tasks.withType().configureEach { outputs.cacheIf { javaLanguageVersion == defaultLanguageVersion } + doFirst { + if (options.release.orNull == 8 && javaLanguageVersion.asInt() >= 20) { + options.compilerArgs.add( + "-Xlint:-options" // see https://github.com/junit-team/junit5/issues/3029 + ) + } + } } tasks.withType().configureEach { javaLauncher.set(javaToolchainService.launcherFor { diff --git a/buildSrc/src/main/kotlin/testing-conventions.gradle.kts b/buildSrc/src/main/kotlin/testing-conventions.gradle.kts index cb70596eb53d..a1ee801a05b7 100644 --- a/buildSrc/src/main/kotlin/testing-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/testing-conventions.gradle.kts @@ -52,6 +52,12 @@ tasks.withType().configureEach { // Track OS as input so that tests are executed on all configured operating systems on CI trackOperationSystemAsInput() + // Avoid passing unnecessary environment variables to the JVM (from GitHub Actions) + if (isCiServer) { + environment.remove("RUNNER_TEMP") + environment.remove("GITHUB_ACTION") + } + jvmArgumentProviders += CommandLineArgumentProvider { listOf( "-Djunit.platform.reporting.open.xml.enabled=true", diff --git a/documentation/documentation.gradle.kts b/documentation/documentation.gradle.kts index a8a96bd98f1c..721a35e1b06d 100644 --- a/documentation/documentation.gradle.kts +++ b/documentation/documentation.gradle.kts @@ -48,6 +48,9 @@ dependencies { testRuntimeOnly(libs.apiguardian) { because("it's required to generate API tables") } + testRuntimeOnly(libs.openTestReporting.events) { + because("it's required to run tests via IntelliJ which does not consumed the shadowed jar of junit-platform-reporting") + } testImplementation(libs.classgraph) { because("ApiReportGenerator needs it") diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 820c808abb47..025ab6fc86d4 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -119,17 +119,19 @@ endif::[] :TestWatcher: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestWatcher.html[TestWatcher] // Jupiter Conditions :DisabledForJreRange: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledForJreRange.html[@DisabledForJreRange] +:DisabledIf: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledIf.html[@DisabledIf] :DisabledIfEnvironmentVariable: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledIfEnvironmentVariable.html[@DisabledIfEnvironmentVariable] :DisabledIfSystemProperty: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledIfSystemProperty.html[@DisabledIfSystemProperty] +:DisabledInNativeImage: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledInNativeImage.html[@DisabledInNativeImage] :DisabledOnJre: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledOnJre.html[@DisabledOnJre] :DisabledOnOs: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledOnOs.html[@DisabledOnOs] -:DisabledIf: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledIf.html[@DisabledIf] :EnabledForJreRange: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/EnabledForJreRange.html[@EnabledForJreRange] +:EnabledIf: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/EnabledIf.html[@EnabledIf] :EnabledIfEnvironmentVariable: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/EnabledIfEnvironmentVariable.html[@EnabledIfEnvironmentVariable] :EnabledIfSystemProperty: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/EnabledIfSystemProperty.html[@EnabledIfSystemProperty] +:EnabledInNativeImage: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/EnabledInNativeImage.html[@EnabledInNativeImage] :EnabledOnJre: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/EnabledOnJre.html[@EnabledOnJre] :EnabledOnOs: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/EnabledOnOs.html[@EnabledOnOs] -:EnabledIf: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/EnabledIf.html[@EnabledIf] :JRE: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/JRE.html[JRE] // Jupiter I/O :TempDir: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/io/TempDir.html[@TempDir] diff --git a/documentation/src/docs/asciidoc/release-notes/index.adoc b/documentation/src/docs/asciidoc/release-notes/index.adoc index 6cca6088e477..e4be51b9779a 100644 --- a/documentation/src/docs/asciidoc/release-notes/index.adoc +++ b/documentation/src/docs/asciidoc/release-notes/index.adoc @@ -16,11 +16,9 @@ authors as well as build tool and IDE vendors. include::{includedir}/link-attributes.adoc[] -include::{basedir}/release-notes-5.9.0.adoc[] - -include::{basedir}/release-notes-5.9.0-RC1.adoc[] +include::{basedir}/release-notes-5.9.1.adoc[] -include::{basedir}/release-notes-5.9.0-M1.adoc[] +include::{basedir}/release-notes-5.9.0.adoc[] include::{basedir}/release-notes-5.8.2.adoc[] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.0.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.0.adoc index 06c77728fa2b..aa292b95a634 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.0.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.0.adoc @@ -17,8 +17,10 @@ format * Various improvements to `ConsoleLauncher` For a complete list of all _closed_ issues and pull requests for this release, consult the -link:{junit5-repo}+/milestone/62?closed=1+[5.9.0] milestone page in the JUnit repository on -GitHub. +link:{junit5-repo}+/milestone/58?closed=1+[5.9 M1], +link:{junit5-repo}+/milestone/61?closed=1+[5.9 RC1], and +link:{junit5-repo}+/milestone/62?closed=1+[5.9 GA] milestone pages in the JUnit repository +on GitHub. [[release-notes-5.9.0-junit-platform]] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.1.adoc new file mode 100644 index 000000000000..3c82815368e1 --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.1.adoc @@ -0,0 +1,53 @@ +[[release-notes-5.9.1]] +== 5.9.1 + +*Date of Release:* September 20, 2022 + +*Scope:* + +* New `@EnabledInNativeImage` and `@DisabledInNativeImage` annotations for testing in + GraalVM native images. +* Minor bug fixes and enhancements since 5.9.0 + +For a complete list of all _closed_ issues and pull requests for this release, consult the +link:{junit5-repo}+/milestone/63?closed=1+[5.9.1] milestone page in the JUnit repository +on GitHub. + + +[[release-notes-5.9.1-junit-platform]] +=== JUnit Platform + +==== Bug Fixes + +* `ReflectionSupport.findMethods(...)` now returns a distinct set of methods. +* Execution in GraalVM native images no longer requires `--initialize-at-build-time` for + `OpenTestReportGeneratingListener`. + + +[[release-notes-5.9.1-junit-jupiter]] +=== JUnit Jupiter + +==== Bug Fixes + +* Headers provided via the `value` attribute in `@CsvSource` for a `@ParameterizedTest` + are now properly parsed when the `useHeadersInDisplayName` attribute is set to `true`. +* A `@ParameterizedTest` method configured with a `@MethodSource` annotation that + references a factory method inherited from multiple interfaces no longer fails with an + exception stating that multiple factory methods with the same name were found. +* A `@ParameterizedTest` method configured with a `@MethodSource` annotation that + references a factory method whose name is the same as other non-factory methods in the + same class no longer fails with an exception stating that multiple factory methods with + the same name were found. +* Assertion failures thrown from methods with applied timeouts using `ThreadMode.SEPARATE` + are now properly reported. + +==== New Features and Improvements + +* New `@EnabledInNativeImage` and `@DisabledInNativeImage` annotations for enabling and + disabling tests within a GraalVM native image. + + +[[release-notes-5.9.1-junit-vintage]] +=== JUnit Vintage + +No changes. diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc index c6d89a0edba8..655d6bc7903b 100644 --- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc +++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc @@ -114,7 +114,7 @@ XML reports as follows: junit.platform.reporting.open.xml.enabled = true - junit.platform.reporting.output.dir = ${project.build.directory}/surefire-reports + junit.platform.reporting.output.dir = target/surefire-reports diff --git a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc index 3fcb484f7193..4a5d1bd57e6b 100644 --- a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc @@ -527,7 +527,7 @@ link:https://ant.apache.org/manual/Tasks/junitlauncher.html[`junitlauncher` task link:https://spring.io/projects/spring-boot[Spring Boot] provides automatic support for managing the version of JUnit used in your project. In addition, the `spring-boot-starter-test` artifact automatically includes testing libraries such as JUnit -Jupiter, AspectJ, Mockito, etc. +Jupiter, AssertJ, Mockito, etc. If your build relies on dependency management support from Spring Boot, you should not import the <> in your build script since that diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 92432cc8a935..6fb1838f63ec 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -270,7 +270,7 @@ include::{testDir}/example/AssertionsDemo.java[tags=user_guide] [WARNING] .Preemptive Timeouts with `assertTimeoutPreemptively()` ==== -The various`assertTimeoutPreemptively()` methods in the `Assertions` class execute +The various `assertTimeoutPreemptively()` methods in the `Assertions` class execute the provided `executable` or `supplier` in a different thread than that of the calling code. This behavior can lead to undesirable side effects if the code that is executed within the `executable` or `supplier` relies on `java.lang.ThreadLocal` storage. @@ -453,6 +453,21 @@ half open ranges. include::{testDir}/example/ConditionalTestExecutionDemo.java[tags=user_guide_jre] ---- +[[writing-tests-conditional-execution-native]] +==== Native Image Conditions + +A container or test may be enabled or disabled within a +https://www.graalvm.org/reference-manual/native-image/[GraalVM native image] via the +`{EnabledInNativeImage}` and `{DisabledInNativeImage}` annotations. These annotations are +typically used when running tests within a native image using the Gradle and Maven +plug-ins from the GraalVM https://graalvm.github.io/native-build-tools/latest/[Native +Build Tools] project. + +[source,java,indent=0] +---- +include::{testDir}/example/ConditionalTestExecutionDemo.java[tags=user_guide_native] +---- + [[writing-tests-conditional-execution-system-properties]] ==== System Property Conditions diff --git a/documentation/src/test/java/example/ConditionalTestExecutionDemo.java b/documentation/src/test/java/example/ConditionalTestExecutionDemo.java index d4dea7b4f31f..a85031a1117c 100644 --- a/documentation/src/test/java/example/ConditionalTestExecutionDemo.java +++ b/documentation/src/test/java/example/ConditionalTestExecutionDemo.java @@ -28,12 +28,14 @@ import org.junit.jupiter.api.condition.DisabledIf; import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; +import org.junit.jupiter.api.condition.DisabledInNativeImage; import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.condition.EnabledInNativeImage; import org.junit.jupiter.api.condition.EnabledOnJre; import org.junit.jupiter.api.condition.EnabledOnOs; @@ -153,6 +155,20 @@ void notFromJava8to11() { } // end::user_guide_jre[] + // tag::user_guide_native[] + @Test + @EnabledInNativeImage + void onlyWithinNativeImage() { + // ... + } + + @Test + @DisabledInNativeImage + void neverWithinNativeImage() { + // ... + } + // end::user_guide_native[] + // tag::user_guide_system_property[] @Test @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*") diff --git a/gradle.properties b/gradle.properties index 05e31a8013af..6ada11024510 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,13 @@ group = org.junit -version = 5.9.0 +version = 5.9.1 jupiterGroup = org.junit.jupiter platformGroup = org.junit.platform -platformVersion = 1.9.0 +platformVersion = 1.9.1 vintageGroup = org.junit.vintage -vintageVersion = 5.9.0 +vintageVersion = 5.9.1 defaultBuiltBy = JUnit Team diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 012d6d90445b..b916c04dbb24 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=cb87f222c5585bd46838ad4db78463a5c5f3d336e5e2b98dc7c0c586527351c2 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeout.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeout.java index 81268f7eafec..93c7c11f8a71 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeout.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeout.java @@ -11,23 +11,13 @@ package org.junit.jupiter.api; import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; +import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException; import java.time.Duration; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.api.function.ThrowingSupplier; -import org.junit.platform.commons.JUnitException; -import org.junit.platform.commons.util.ExceptionUtils; /** * {@code AssertTimeout} is a collection of utility methods that support asserting @@ -79,7 +69,7 @@ private static T assertTimeout(Duration timeout, ThrowingSupplier supplie result = supplier.get(); } catch (Throwable ex) { - ExceptionUtils.throwAsUncheckedException(ex); + throwAsUncheckedException(ex); } long timeElapsed = System.currentTimeMillis() - start; @@ -93,105 +83,4 @@ private static T assertTimeout(Duration timeout, ThrowingSupplier supplie return result; } - static void assertTimeoutPreemptively(Duration timeout, Executable executable) { - assertTimeoutPreemptively(timeout, executable, (String) null); - } - - static void assertTimeoutPreemptively(Duration timeout, Executable executable, String message) { - assertTimeoutPreemptively(timeout, () -> { - executable.execute(); - return null; - }, message); - } - - static void assertTimeoutPreemptively(Duration timeout, Executable executable, Supplier messageSupplier) { - assertTimeoutPreemptively(timeout, () -> { - executable.execute(); - return null; - }, messageSupplier); - } - - static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier) { - return assertTimeoutPreemptively(timeout, supplier, (Object) null); - } - - static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, String message) { - return assertTimeoutPreemptively(timeout, supplier, (Object) message); - } - - static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, - Supplier messageSupplier) { - - return assertTimeoutPreemptively(timeout, supplier, (Object) messageSupplier); - } - - private static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, - Object messageOrSupplier) { - - AtomicReference threadReference = new AtomicReference<>(); - ExecutorService executorService = Executors.newSingleThreadExecutor(new TimeoutThreadFactory()); - - try { - Future future = executorService.submit(() -> { - try { - threadReference.set(Thread.currentThread()); - return supplier.get(); - } - catch (Throwable throwable) { - throw ExceptionUtils.throwAsUncheckedException(throwable); - } - }); - - long timeoutInMillis = timeout.toMillis(); - try { - return future.get(timeoutInMillis, TimeUnit.MILLISECONDS); - } - catch (TimeoutException ex) { - AssertionFailureBuilder failure = assertionFailure() // - .message(messageOrSupplier) // - .reason("execution timed out after " + timeoutInMillis + " ms"); - - Thread thread = threadReference.get(); - if (thread != null) { - ExecutionTimeoutException exception = new ExecutionTimeoutException( - "Execution timed out in thread " + thread.getName()); - exception.setStackTrace(thread.getStackTrace()); - failure.cause(exception); - } - throw failure.build(); - } - catch (ExecutionException ex) { - throw ExceptionUtils.throwAsUncheckedException(ex.getCause()); - } - catch (Throwable ex) { - throw ExceptionUtils.throwAsUncheckedException(ex); - } - } - finally { - executorService.shutdownNow(); - } - } - - private static class ExecutionTimeoutException extends JUnitException { - - private static final long serialVersionUID = 1L; - - ExecutionTimeoutException(String message) { - super(message); - } - } - - /** - * The thread factory used for preemptive timeout. - * - * The factory creates threads with meaningful names, helpful for debugging purposes. - */ - private static class TimeoutThreadFactory implements ThreadFactory { - private static final AtomicInteger threadNumber = new AtomicInteger(1); - - public Thread newThread(Runnable r) { - return new Thread(r, "junit-timeout-thread-" + threadNumber.getAndIncrement()); - } - } - } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java new file mode 100644 index 000000000000..94534171dd01 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api; + +import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; +import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException; + +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.function.ThrowingSupplier; +import org.junit.platform.commons.JUnitException; +import org.opentest4j.AssertionFailedError; + +/** + * {@code AssertTimeout} is a collection of utility methods that support asserting + * the execution of the code under test did not take longer than the timeout duration + * using a preemptive approach. + * + * @since 5.9.1 + */ +class AssertTimeoutPreemptively { + + static void assertTimeoutPreemptively(Duration timeout, Executable executable) { + assertTimeoutPreemptively(timeout, executable, (String) null); + } + + static void assertTimeoutPreemptively(Duration timeout, Executable executable, String message) { + assertTimeoutPreemptively(timeout, () -> { + executable.execute(); + return null; + }, message); + } + + static void assertTimeoutPreemptively(Duration timeout, Executable executable, Supplier messageSupplier) { + assertTimeoutPreemptively(timeout, () -> { + executable.execute(); + return null; + }, messageSupplier); + } + + static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier) { + return assertTimeoutPreemptively(timeout, supplier, null, AssertTimeoutPreemptively::createAssertionFailure); + } + + static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, String message) { + return assertTimeoutPreemptively(timeout, supplier, message == null ? null : () -> message, + AssertTimeoutPreemptively::createAssertionFailure); + } + + static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, + Supplier messageSupplier) { + return assertTimeoutPreemptively(timeout, supplier, messageSupplier, + AssertTimeoutPreemptively::createAssertionFailure); + } + + static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, + Supplier messageSupplier, Assertions.TimeoutFailureFactory failureFactory) throws E { + AtomicReference threadReference = new AtomicReference<>(); + ExecutorService executorService = Executors.newSingleThreadExecutor(new TimeoutThreadFactory()); + + try { + Future future = submitTask(supplier, threadReference, executorService); + return resolveFutureAndHandleException(future, timeout, messageSupplier, threadReference::get, + failureFactory); + } + finally { + executorService.shutdownNow(); + } + } + + private static Future submitTask(ThrowingSupplier supplier, AtomicReference threadReference, + ExecutorService executorService) { + return executorService.submit(() -> { + try { + threadReference.set(Thread.currentThread()); + return supplier.get(); + } + catch (Throwable throwable) { + throw throwAsUncheckedException(throwable); + } + }); + } + + private static T resolveFutureAndHandleException(Future future, Duration timeout, + Supplier messageSupplier, Supplier threadSupplier, + Assertions.TimeoutFailureFactory failureFactory) throws E { + try { + return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } + catch (TimeoutException ex) { + Thread thread = threadSupplier.get(); + ExecutionTimeoutException cause = null; + if (thread != null) { + cause = new ExecutionTimeoutException("Execution timed out in thread " + thread.getName()); + cause.setStackTrace(thread.getStackTrace()); + } + throw failureFactory.createTimeoutFailure(timeout, messageSupplier, cause); + } + catch (ExecutionException ex) { + throw throwAsUncheckedException(ex.getCause()); + } + catch (Throwable ex) { + throw throwAsUncheckedException(ex); + } + } + + private static AssertionFailedError createAssertionFailure(Duration timeout, Supplier messageSupplier, + Throwable cause) { + return assertionFailure() // + .message(messageSupplier) // + .reason("execution timed out after " + timeout.toMillis() + " ms") // + .cause(cause) // + .build(); + } + + private static class ExecutionTimeoutException extends JUnitException { + + private static final long serialVersionUID = 1L; + + ExecutionTimeoutException(String message) { + super(message); + } + } + + /** + * The thread factory used for preemptive timeout. + *

+ * The factory creates threads with meaningful names, helpful for debugging purposes. + */ + private static class TimeoutThreadFactory implements ThreadFactory { + private static final AtomicInteger threadNumber = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(r, "junit-timeout-thread-" + threadNumber.getAndIncrement()); + } + } +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java index fa6585771612..62fefbe630fb 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java @@ -11,6 +11,7 @@ package org.junit.jupiter.api; import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.time.Duration; @@ -3395,7 +3396,7 @@ public static T assertTimeout(Duration timeout, ThrowingSupplier supplier * @see #assertTimeout(Duration, Executable) */ public static void assertTimeoutPreemptively(Duration timeout, Executable executable) { - AssertTimeout.assertTimeoutPreemptively(timeout, executable); + AssertTimeoutPreemptively.assertTimeoutPreemptively(timeout, executable); } /** @@ -3418,7 +3419,7 @@ public static void assertTimeoutPreemptively(Duration timeout, Executable execut * @see #assertTimeout(Duration, Executable, String) */ public static void assertTimeoutPreemptively(Duration timeout, Executable executable, String message) { - AssertTimeout.assertTimeoutPreemptively(timeout, executable, message); + AssertTimeoutPreemptively.assertTimeoutPreemptively(timeout, executable, message); } /** @@ -3443,7 +3444,7 @@ public static void assertTimeoutPreemptively(Duration timeout, Executable execut */ public static void assertTimeoutPreemptively(Duration timeout, Executable executable, Supplier messageSupplier) { - AssertTimeout.assertTimeoutPreemptively(timeout, executable, messageSupplier); + AssertTimeoutPreemptively.assertTimeoutPreemptively(timeout, executable, messageSupplier); } // --- supplier - preemptively --- @@ -3468,7 +3469,7 @@ public static void assertTimeoutPreemptively(Duration timeout, Executable execut * @see #assertTimeout(Duration, Executable) */ public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier) { - return AssertTimeout.assertTimeoutPreemptively(timeout, supplier); + return AssertTimeoutPreemptively.assertTimeoutPreemptively(timeout, supplier); } /** @@ -3493,7 +3494,7 @@ public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier * @see #assertTimeout(Duration, Executable, String) */ public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, String message) { - return AssertTimeout.assertTimeoutPreemptively(timeout, supplier, message); + return AssertTimeoutPreemptively.assertTimeoutPreemptively(timeout, supplier, message); } /** @@ -3520,7 +3521,39 @@ public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier */ public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, Supplier messageSupplier) { - return AssertTimeout.assertTimeoutPreemptively(timeout, supplier, messageSupplier); + return AssertTimeoutPreemptively.assertTimeoutPreemptively(timeout, supplier, messageSupplier); + } + + /** + * Assert that execution of the supplied {@code supplier} + * completes before the given {@code timeout} is exceeded. + * + *

If the assertion passes then the {@code supplier}'s result is returned. + * + *

In the case the assertion does not pass, the supplied + * {@link TimeoutFailureFactory} is invoked to create an exception which is + * then thrown. + * + *

Note: the {@code supplier} will be executed in a different thread than + * that of the calling code. Furthermore, execution of the {@code supplier} will + * be preemptively aborted if the timeout is exceeded. See the + * {@linkplain Assertions Preemptive Timeouts} section of the class-level + * Javadoc for a discussion of possible undesirable side effects. + * + *

If necessary, the failure message will be retrieved lazily from the + * supplied {@code messageSupplier}. + * + * @see #assertTimeoutPreemptively(Duration, Executable) + * @see #assertTimeoutPreemptively(Duration, Executable, String) + * @see #assertTimeoutPreemptively(Duration, Executable, Supplier) + * @see #assertTimeoutPreemptively(Duration, ThrowingSupplier) + * @see #assertTimeoutPreemptively(Duration, ThrowingSupplier, String) + * @see #assertTimeout(Duration, Executable, Supplier) + */ + @API(status = INTERNAL, since = "5.9.1") + public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, + Supplier messageSupplier, TimeoutFailureFactory failureFactory) throws E { + return AssertTimeoutPreemptively.assertTimeoutPreemptively(timeout, supplier, messageSupplier, failureFactory); } // --- assertInstanceOf ---------------------------------------------------- @@ -3572,4 +3605,21 @@ public static T assertInstanceOf(Class expectedType, Object actualValue, return AssertInstanceOf.assertInstanceOf(expectedType, actualValue, messageSupplier); } + /** + * Factory for timeout failures. + * + * @param The type of error or exception created + * @since 5.9.1 + * @see Assertions#assertTimeoutPreemptively(Duration, ThrowingSupplier, Supplier, TimeoutFailureFactory) + */ + @API(status = INTERNAL, since = "5.9.1") + public interface TimeoutFailureFactory { + + /** + * Create a failure for the given timeout, message, and cause. + * + * @return timeout failure; never {@code null} + */ + T createTimeoutFailure(Duration timeout, Supplier messageSupplier, Throwable cause); + } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Disabled.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Disabled.java index 8961e0bdea7a..88b6f16d4b54 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Disabled.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Disabled.java @@ -48,6 +48,8 @@ * @see org.junit.jupiter.api.condition.DisabledForJreRange * @see org.junit.jupiter.api.condition.EnabledOnOs * @see org.junit.jupiter.api.condition.DisabledOnOs + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage * @see org.junit.jupiter.api.extension.ExecutionCondition */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledForJreRange.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledForJreRange.java index 5c8cf34c34a6..be2edd9642e6 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledForJreRange.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledForJreRange.java @@ -51,17 +51,19 @@ * * @since 5.6 * @see JRE - * @see org.junit.jupiter.api.condition.EnabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledOnJre - * @see org.junit.jupiter.api.condition.DisabledOnJre + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf * @see org.junit.jupiter.api.condition.EnabledOnOs * @see org.junit.jupiter.api.condition.DisabledOnOs - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.EnabledOnJre + * @see org.junit.jupiter.api.condition.DisabledOnJre + * @see org.junit.jupiter.api.condition.EnabledForJreRange + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty - * @see org.junit.jupiter.api.condition.EnabledIf - * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIf.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIf.java index 56ef7a7fb194..0eb3bc627911 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIf.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIf.java @@ -57,10 +57,12 @@ * @see org.junit.jupiter.api.condition.DisabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIfEnvironmentVariable.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIfEnvironmentVariable.java index e4583598c057..c32b693c5b03 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIfEnvironmentVariable.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIfEnvironmentVariable.java @@ -52,17 +52,19 @@ * given element. * * @since 5.1 - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty - * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledOnOs + * @see org.junit.jupiter.api.condition.DisabledOnOs * @see org.junit.jupiter.api.condition.EnabledOnJre * @see org.junit.jupiter.api.condition.DisabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledOnOs - * @see org.junit.jupiter.api.condition.DisabledOnOs - * @see org.junit.jupiter.api.condition.EnabledIf - * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage + * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty + * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIfSystemProperty.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIfSystemProperty.java index cda6efd2ce08..84f7c717f886 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIfSystemProperty.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledIfSystemProperty.java @@ -52,17 +52,19 @@ * given element. * * @since 5.1 - * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledOnOs + * @see org.junit.jupiter.api.condition.DisabledOnOs * @see org.junit.jupiter.api.condition.EnabledOnJre * @see org.junit.jupiter.api.condition.DisabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledOnOs - * @see org.junit.jupiter.api.condition.DisabledOnOs - * @see org.junit.jupiter.api.condition.EnabledIf - * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage + * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledInNativeImage.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledInNativeImage.java new file mode 100644 index 000000000000..a51fa19a81ec --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledInNativeImage.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.condition; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @DisabledInNativeImage} is used to signal that the annotated test class + * or test method is only disabled when executing within a GraalVM native + * image. + * + *

When applied at the class level, all test methods within that class will + * be disabled within a native image. + * + *

If a test method is disabled via this annotation, that does not prevent + * the test class from being instantiated. Rather, it prevents the execution of + * the test method and method-level lifecycle callbacks such as {@code @BeforeEach} + * methods, {@code @AfterEach} methods, and corresponding extension APIs. + * + *

This annotation may be used as a meta-annotation in order to create a + * custom composed annotation that inherits the semantics of this + * annotation. + * + *

Technical Details

+ * + *

JUnit detects whether tests are executing within a GraalVM native image by + * checking for the presence of the {@code org.graalvm.nativeimage.imagecode} + * system property (see + * org.graalvm.nativeimage.ImageInfo + * for details). The GraalVM compiler sets the property to {@code buildtime} while + * compiling a native image; the property is set to {@code runtime} while a native + * image is executing; and the Gradle and Maven plug-ins in the GraalVM + * Native Build Tools + * project set the property to {@code agent} while executing tests with the GraalVM + * tracing agent. + * + * @since 5.9.1 + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledOnOs + * @see org.junit.jupiter.api.condition.DisabledOnOs + * @see org.junit.jupiter.api.condition.EnabledOnJre + * @see org.junit.jupiter.api.condition.DisabledOnJre + * @see org.junit.jupiter.api.condition.EnabledForJreRange + * @see org.junit.jupiter.api.condition.DisabledForJreRange + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty + * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.Disabled + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@DisabledIfSystemProperty(named = "org.graalvm.nativeimage.imagecode", matches = ".+", // + disabledReason = "Currently executing within a GraalVM native image") +@API(status = STABLE, since = "5.9.1") +public @interface DisabledInNativeImage { +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledOnJre.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledOnJre.java index 3fdf227cfa02..dac3c3a46c4a 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledOnJre.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledOnJre.java @@ -51,17 +51,19 @@ * * @since 5.1 * @see JRE + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledOnOs + * @see org.junit.jupiter.api.condition.DisabledOnOs * @see org.junit.jupiter.api.condition.EnabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledOnOs - * @see org.junit.jupiter.api.condition.DisabledOnOs - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty - * @see org.junit.jupiter.api.condition.EnabledIf - * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledOnOs.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledOnOs.java index 529aee6b1fc5..52b60f486cff 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledOnOs.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/DisabledOnOs.java @@ -56,17 +56,19 @@ * * @since 5.1 * @see OS + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf * @see org.junit.jupiter.api.condition.EnabledOnOs * @see org.junit.jupiter.api.condition.EnabledOnJre * @see org.junit.jupiter.api.condition.DisabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty - * @see org.junit.jupiter.api.condition.EnabledIf - * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) 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 89f92c7ca758..e18d02c67bc9 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 @@ -51,17 +51,19 @@ * * @since 5.6 * @see JRE - * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledOnJre - * @see org.junit.jupiter.api.condition.DisabledOnJre + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf * @see org.junit.jupiter.api.condition.EnabledOnOs * @see org.junit.jupiter.api.condition.DisabledOnOs - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.EnabledOnJre + * @see org.junit.jupiter.api.condition.DisabledOnJre + * @see org.junit.jupiter.api.condition.DisabledForJreRange + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty - * @see org.junit.jupiter.api.condition.EnabledIf - * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIf.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIf.java index f4b9fa99249c..3005a8f0a460 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIf.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIf.java @@ -57,10 +57,12 @@ * @see org.junit.jupiter.api.condition.DisabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIfEnvironmentVariable.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIfEnvironmentVariable.java index 2898881c08ee..7eca6dffc9bd 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIfEnvironmentVariable.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIfEnvironmentVariable.java @@ -51,17 +51,19 @@ * given element. * * @since 5.1 - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty - * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledOnOs + * @see org.junit.jupiter.api.condition.DisabledOnOs * @see org.junit.jupiter.api.condition.EnabledOnJre * @see org.junit.jupiter.api.condition.DisabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledOnOs - * @see org.junit.jupiter.api.condition.DisabledOnOs - * @see org.junit.jupiter.api.condition.EnabledIf - * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage + * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty + * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIfSystemProperty.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIfSystemProperty.java index 428aa55676da..f26b972ce6e7 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIfSystemProperty.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledIfSystemProperty.java @@ -51,17 +51,19 @@ * given element. * * @since 5.1 - * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledOnOs + * @see org.junit.jupiter.api.condition.DisabledOnOs * @see org.junit.jupiter.api.condition.EnabledOnJre * @see org.junit.jupiter.api.condition.DisabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledOnOs - * @see org.junit.jupiter.api.condition.DisabledOnOs - * @see org.junit.jupiter.api.condition.EnabledIf - * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledInNativeImage.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledInNativeImage.java new file mode 100644 index 000000000000..81fd19cbf338 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledInNativeImage.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.condition; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @EnabledInNativeImage} is used to signal that the annotated test class + * or test method is only enabled when executing within a GraalVM native + * image. + * + *

When applied at the class level, all test methods within that class will + * be enabled within a native image. + * + *

If a test method is disabled via this annotation, that does not prevent + * the test class from being instantiated. Rather, it prevents the execution of + * the test method and method-level lifecycle callbacks such as {@code @BeforeEach} + * methods, {@code @AfterEach} methods, and corresponding extension APIs. + * + *

This annotation may be used as a meta-annotation in order to create a + * custom composed annotation that inherits the semantics of this + * annotation. + * + *

Technical Details

+ * + *

JUnit detects whether tests are executing within a GraalVM native image by + * checking for the presence of the {@code org.graalvm.nativeimage.imagecode} + * system property (see + * org.graalvm.nativeimage.ImageInfo + * for details). The GraalVM compiler sets the property to {@code buildtime} while + * compiling a native image; the property is set to {@code runtime} while a native + * image is executing; and the Gradle and Maven plug-ins in the GraalVM + * Native Build Tools + * project set the property to {@code agent} while executing tests with the GraalVM + * tracing agent. + * + * @since 5.9.1 + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledOnOs + * @see org.junit.jupiter.api.condition.DisabledOnOs + * @see org.junit.jupiter.api.condition.EnabledOnJre + * @see org.junit.jupiter.api.condition.DisabledOnJre + * @see org.junit.jupiter.api.condition.EnabledForJreRange + * @see org.junit.jupiter.api.condition.DisabledForJreRange + * @see org.junit.jupiter.api.condition.DisabledInNativeImage + * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty + * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.Disabled + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnabledIfSystemProperty(named = "org.graalvm.nativeimage.imagecode", matches = ".+", // + disabledReason = "Not currently executing within a GraalVM native image") +@API(status = STABLE, since = "5.9.1") +public @interface EnabledInNativeImage { +} 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 adca5ff552ac..d8210bf71f8b 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 @@ -51,17 +51,19 @@ * * @since 5.1 * @see JRE + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledOnOs + * @see org.junit.jupiter.api.condition.DisabledOnOs * @see org.junit.jupiter.api.condition.DisabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledOnOs - * @see org.junit.jupiter.api.condition.DisabledOnOs - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty - * @see org.junit.jupiter.api.condition.EnabledIf - * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnOs.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnOs.java index 7db330c896ca..e0c17deda556 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnOs.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/condition/EnabledOnOs.java @@ -56,17 +56,19 @@ * * @since 5.1 * @see OS + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf * @see org.junit.jupiter.api.condition.DisabledOnOs * @see org.junit.jupiter.api.condition.EnabledOnJre * @see org.junit.jupiter.api.condition.DisabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty - * @see org.junit.jupiter.api.condition.EnabledIf - * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable * @see org.junit.jupiter.api.Disabled */ @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExecutionCondition.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExecutionCondition.java index ca7d725c8068..4edc1dafb849 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExecutionCondition.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExecutionCondition.java @@ -36,16 +36,20 @@ * * @since 5.0 * @see org.junit.jupiter.api.Disabled - * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable - * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty - * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIf + * @see org.junit.jupiter.api.condition.DisabledIf + * @see org.junit.jupiter.api.condition.EnabledOnOs + * @see org.junit.jupiter.api.condition.DisabledOnOs * @see org.junit.jupiter.api.condition.EnabledOnJre * @see org.junit.jupiter.api.condition.DisabledOnJre * @see org.junit.jupiter.api.condition.EnabledForJreRange * @see org.junit.jupiter.api.condition.DisabledForJreRange - * @see org.junit.jupiter.api.condition.EnabledOnOs - * @see org.junit.jupiter.api.condition.DisabledOnOs + * @see org.junit.jupiter.api.condition.EnabledInNativeImage + * @see org.junit.jupiter.api.condition.DisabledInNativeImage + * @see org.junit.jupiter.api.condition.EnabledIfSystemProperty + * @see org.junit.jupiter.api.condition.DisabledIfSystemProperty + * @see org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + * @see org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocation.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocation.java index 1860f413455d..16f22d1b09f6 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocation.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocation.java @@ -16,7 +16,6 @@ import java.util.function.Supplier; import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; -import org.opentest4j.AssertionFailedError; /** * @since 5.9 @@ -36,13 +35,11 @@ class SeparateThreadTimeoutInvocation implements Invocation { @Override public T proceed() throws Throwable { - try { - return assertTimeoutPreemptively(timeout.toDuration(), delegate::proceed, descriptionSupplier); - } - catch (AssertionFailedError failure) { - TimeoutException exception = TimeoutExceptionFactory.create(descriptionSupplier.get(), timeout, null); - exception.initCause(failure.getCause()); - throw exception; - } + return assertTimeoutPreemptively(timeout.toDuration(), delegate::proceed, descriptionSupplier, + (__, messageSupplier, cause) -> { + TimeoutException exception = TimeoutExceptionFactory.create(messageSupplier.get(), timeout, null); + exception.initCause(cause); + return exception; + }); } } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertTimeoutAssertionsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertTimeoutAssertionsTests.java index 424317ed6b3b..0dfd4e7d6e13 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertTimeoutAssertionsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertTimeoutAssertionsTests.java @@ -11,37 +11,28 @@ package org.junit.jupiter.api; import static java.time.Duration.ofMillis; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEquals; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTimeout; -import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.api.condition.OS.WINDOWS; -import java.time.Duration; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.function.Executable; import org.junit.platform.commons.util.ExceptionUtils; import org.opentest4j.AssertionFailedError; /** - * Unit tests for JUnit Jupiter {@link Assertions}. + * Unit tests for {@link AssertTimeout}. * * @since 5.0 */ class AssertTimeoutAssertionsTests { - private static final Duration PREEMPTIVE_TIMEOUT = ofMillis(WINDOWS.isCurrentOs() ? 1000 : 100); - - private static ThreadLocal changed = ThreadLocal.withInitial(() -> new AtomicBoolean(false)); + private static final ThreadLocal changed = ThreadLocal.withInitial(() -> new AtomicBoolean(false)); private final Executable nix = () -> { }; @@ -163,157 +154,6 @@ void assertTimeoutWithMessageSupplierForSupplierThatCompletesAfterTheTimeout() { assertMessageStartsWith(error, "Tempus Fugit ==> execution exceeded timeout of 10 ms by"); } - // -- executable - preemptively --- - - @Test - void assertTimeoutPreemptivelyForExecutableThatCompletesBeforeTheTimeout() { - changed.get().set(false); - assertTimeoutPreemptively(ofMillis(500), () -> changed.get().set(true)); - assertFalse(changed.get().get(), "should have executed in a different thread"); - assertTimeoutPreemptively(ofMillis(500), nix, "message"); - assertTimeoutPreemptively(ofMillis(500), nix, () -> "message"); - } - - @Test - void assertTimeoutPreemptivelyForExecutableThatThrowsAnException() { - RuntimeException exception = assertThrows(RuntimeException.class, - () -> assertTimeoutPreemptively(ofMillis(500), () -> { - throw new RuntimeException("not this time"); - })); - assertMessageEquals(exception, "not this time"); - } - - @Test - void assertTimeoutPreemptivelyForExecutableThatThrowsAnAssertionFailedError() { - AssertionFailedError exception = assertThrows(AssertionFailedError.class, - () -> assertTimeoutPreemptively(ofMillis(500), () -> fail("enigma"))); - assertMessageEquals(exception, "enigma"); - } - - @Test - void assertTimeoutPreemptivelyForExecutableThatCompletesAfterTheTimeout() { - AssertionFailedError error = assertThrows(AssertionFailedError.class, - () -> assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, this::waitForInterrupt)); - assertMessageEquals(error, "execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); - assertMessageStartsWith(error.getCause(), "Execution timed out in "); - assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); - } - - @Test - void assertTimeoutPreemptivelyWithMessageForExecutableThatCompletesAfterTheTimeout() { - AssertionFailedError error = assertThrows(AssertionFailedError.class, - () -> assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, this::waitForInterrupt, "Tempus Fugit")); - assertMessageEquals(error, - "Tempus Fugit ==> execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); - assertMessageStartsWith(error.getCause(), "Execution timed out in "); - assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); - } - - @Test - void assertTimeoutPreemptivelyWithMessageSupplierForExecutableThatCompletesAfterTheTimeout() { - AssertionFailedError error = assertThrows(AssertionFailedError.class, - () -> assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, this::waitForInterrupt, - () -> "Tempus" + " " + "Fugit")); - assertMessageEquals(error, - "Tempus Fugit ==> execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); - assertMessageStartsWith(error.getCause(), "Execution timed out in "); - assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); - } - - @Test - void assertTimeoutPreemptivelyWithMessageSupplierForExecutableThatCompletesBeforeTheTimeout() { - assertTimeoutPreemptively(ofMillis(500), nix, () -> "Tempus" + " " + "Fugit"); - } - - // -- supplier - preemptively --- - - @Test - void assertTimeoutPreemptivelyForSupplierThatCompletesBeforeTheTimeout() { - changed.get().set(false); - String result = assertTimeoutPreemptively(ofMillis(500), () -> { - changed.get().set(true); - return "Tempus Fugit"; - }); - assertFalse(changed.get().get(), "should have executed in a different thread"); - assertEquals("Tempus Fugit", result); - assertEquals("Tempus Fugit", assertTimeoutPreemptively(ofMillis(500), () -> "Tempus Fugit", "message")); - assertEquals("Tempus Fugit", assertTimeoutPreemptively(ofMillis(500), () -> "Tempus Fugit", () -> "message")); - } - - @Test - void assertTimeoutPreemptivelyForSupplierThatThrowsAnException() { - RuntimeException exception = assertThrows(RuntimeException.class, () -> { - assertTimeoutPreemptively(ofMillis(500), () -> { - ExceptionUtils.throwAsUncheckedException(new RuntimeException("not this time")); - return "Tempus Fugit"; - }); - }); - assertMessageEquals(exception, "not this time"); - } - - @Test - void assertTimeoutPreemptivelyForSupplierThatThrowsAnAssertionFailedError() { - AssertionFailedError exception = assertThrows(AssertionFailedError.class, () -> { - assertTimeoutPreemptively(ofMillis(500), () -> { - fail("enigma"); - return "Tempus Fugit"; - }); - }); - assertMessageEquals(exception, "enigma"); - } - - @Test - void assertTimeoutPreemptivelyForSupplierThatCompletesAfterTheTimeout() { - AssertionFailedError error = assertThrows(AssertionFailedError.class, () -> { - assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, () -> { - waitForInterrupt(); - return "Tempus Fugit"; - }); - }); - - assertMessageEquals(error, "execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); - assertMessageStartsWith(error.getCause(), "Execution timed out in "); - assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); - } - - @Test - void assertTimeoutPreemptivelyWithMessageForSupplierThatCompletesAfterTheTimeout() { - AssertionFailedError error = assertThrows(AssertionFailedError.class, () -> { - assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, () -> { - waitForInterrupt(); - return "Tempus Fugit"; - }, "Tempus Fugit"); - }); - - assertMessageEquals(error, - "Tempus Fugit ==> execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); - assertMessageStartsWith(error.getCause(), "Execution timed out in "); - assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); - } - - @Test - void assertTimeoutPreemptivelyWithMessageSupplierForSupplierThatCompletesAfterTheTimeout() { - AssertionFailedError error = assertThrows(AssertionFailedError.class, () -> { - assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, () -> { - waitForInterrupt(); - return "Tempus Fugit"; - }, () -> "Tempus" + " " + "Fugit"); - }); - - assertMessageEquals(error, - "Tempus Fugit ==> execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); - assertMessageStartsWith(error.getCause(), "Execution timed out in "); - assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); - } - - @Test - void assertTimeoutPreemptivelyUsesThreadsWithSpecificNamePrefix() { - AtomicReference threadName = new AtomicReference<>(""); - assertTimeoutPreemptively(ofMillis(1000), () -> threadName.set(Thread.currentThread().getName())); - assertTrue(threadName.get().startsWith("junit-timeout-thread-"), - "Thread name does not match the expected prefix"); - } - /** * Take a nap for 100 milliseconds. */ @@ -325,24 +165,4 @@ private void nap() throws InterruptedException { } while (System.currentTimeMillis() - start < 100); } - private void waitForInterrupt() { - try { - assertFalse(Thread.interrupted(), "Already interrupted"); - new CountDownLatch(1).await(); - } - catch (InterruptedException ignore) { - // ignore - } - } - - /** - * Assert the given stack trace elements contain an element with the given class name and method name. - */ - private static void assertStackTraceContains(StackTraceElement[] stackTrace, String className, String methodName) { - assertThat(stackTrace).anySatisfy(element -> { - assertThat(element.getClassName()).endsWith(className); - assertThat(element.getMethodName()).isEqualTo(methodName); - }); - } - } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertTimeoutPreemptivelyAssertionsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertTimeoutPreemptivelyAssertionsTests.java new file mode 100644 index 000000000000..1f4d453cd698 --- /dev/null +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertTimeoutPreemptivelyAssertionsTests.java @@ -0,0 +1,255 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api; + +import static java.time.Duration.ofMillis; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEquals; +import static org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.condition.OS.WINDOWS; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.function.Executable; +import org.junit.platform.commons.util.ExceptionUtils; +import org.opentest4j.AssertionFailedError; + +/** + * Unit tests for {@link AssertTimeoutPreemptively}. + * + * @since 5.0 + */ +class AssertTimeoutPreemptivelyAssertionsTests { + + private static final Duration PREEMPTIVE_TIMEOUT = ofMillis(WINDOWS.isCurrentOs() ? 1000 : 100); + private static final Assertions.TimeoutFailureFactory TIMEOUT_EXCEPTION_FACTORY = (__, ___, + ____) -> new TimeoutException(); + + private static final ThreadLocal changed = ThreadLocal.withInitial(() -> new AtomicBoolean(false)); + + private final Executable nix = () -> { + }; + + // --- executable ---------------------------------------------------------- + + @Test + void assertTimeoutPreemptivelyForExecutableThatCompletesBeforeTheTimeout() { + changed.get().set(false); + assertTimeoutPreemptively(ofMillis(500), () -> changed.get().set(true)); + assertFalse(changed.get().get(), "should have executed in a different thread"); + assertTimeoutPreemptively(ofMillis(500), nix, "message"); + assertTimeoutPreemptively(ofMillis(500), nix, () -> "message"); + } + + @Test + void assertTimeoutPreemptivelyForExecutableThatThrowsAnException() { + RuntimeException exception = assertThrows(RuntimeException.class, + () -> assertTimeoutPreemptively(ofMillis(500), () -> { + throw new RuntimeException("not this time"); + })); + assertMessageEquals(exception, "not this time"); + } + + @Test + void assertTimeoutPreemptivelyForExecutableThatThrowsAnAssertionFailedError() { + AssertionFailedError exception = assertThrows(AssertionFailedError.class, + () -> assertTimeoutPreemptively(ofMillis(500), () -> fail("enigma"))); + assertMessageEquals(exception, "enigma"); + } + + @Test + void assertTimeoutPreemptivelyForExecutableThatCompletesAfterTheTimeout() { + AssertionFailedError error = assertThrows(AssertionFailedError.class, + () -> assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, this::waitForInterrupt)); + assertMessageEquals(error, "execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); + assertMessageStartsWith(error.getCause(), "Execution timed out in "); + assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); + } + + @Test + void assertTimeoutPreemptivelyWithMessageForExecutableThatCompletesAfterTheTimeout() { + AssertionFailedError error = assertThrows(AssertionFailedError.class, + () -> assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, this::waitForInterrupt, "Tempus Fugit")); + assertMessageEquals(error, + "Tempus Fugit ==> execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); + assertMessageStartsWith(error.getCause(), "Execution timed out in "); + assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); + } + + @Test + void assertTimeoutPreemptivelyWithMessageSupplierForExecutableThatCompletesAfterTheTimeout() { + AssertionFailedError error = assertThrows(AssertionFailedError.class, + () -> assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, this::waitForInterrupt, + () -> "Tempus" + " " + "Fugit")); + assertMessageEquals(error, + "Tempus Fugit ==> execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); + assertMessageStartsWith(error.getCause(), "Execution timed out in "); + assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); + } + + @Test + void assertTimeoutPreemptivelyWithMessageSupplierForExecutableThatCompletesBeforeTheTimeout() { + assertTimeoutPreemptively(ofMillis(500), nix, () -> "Tempus" + " " + "Fugit"); + } + + // --- supplier ------------------------------------------------------------ + + @Test + void assertTimeoutPreemptivelyForSupplierThatCompletesBeforeTheTimeout() { + changed.get().set(false); + String result = assertTimeoutPreemptively(ofMillis(500), () -> { + changed.get().set(true); + return "Tempus Fugit"; + }); + assertFalse(changed.get().get(), "should have executed in a different thread"); + assertEquals("Tempus Fugit", result); + assertEquals("Tempus Fugit", assertTimeoutPreemptively(ofMillis(500), () -> "Tempus Fugit", "message")); + assertEquals("Tempus Fugit", assertTimeoutPreemptively(ofMillis(500), () -> "Tempus Fugit", () -> "message")); + } + + @Test + void assertTimeoutPreemptivelyForSupplierThatThrowsAnException() { + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + assertTimeoutPreemptively(ofMillis(500), () -> { + ExceptionUtils.throwAsUncheckedException(new RuntimeException("not this time")); + return "Tempus Fugit"; + }); + }); + assertMessageEquals(exception, "not this time"); + } + + @Test + void assertTimeoutPreemptivelyForSupplierThatThrowsAnAssertionFailedError() { + AssertionFailedError exception = assertThrows(AssertionFailedError.class, () -> { + assertTimeoutPreemptively(ofMillis(500), () -> { + fail("enigma"); + return "Tempus Fugit"; + }); + }); + assertMessageEquals(exception, "enigma"); + } + + @Test + void assertTimeoutPreemptivelyForSupplierThatCompletesAfterTheTimeout() { + AssertionFailedError error = assertThrows(AssertionFailedError.class, () -> { + assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, () -> { + waitForInterrupt(); + return "Tempus Fugit"; + }); + }); + + assertMessageEquals(error, "execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); + assertMessageStartsWith(error.getCause(), "Execution timed out in "); + assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); + } + + @Test + void assertTimeoutPreemptivelyWithMessageForSupplierThatCompletesAfterTheTimeout() { + AssertionFailedError error = assertThrows(AssertionFailedError.class, () -> { + assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, () -> { + waitForInterrupt(); + return "Tempus Fugit"; + }, "Tempus Fugit"); + }); + + assertMessageEquals(error, + "Tempus Fugit ==> execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); + assertMessageStartsWith(error.getCause(), "Execution timed out in "); + assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); + } + + @Test + void assertTimeoutPreemptivelyWithMessageSupplierForSupplierThatCompletesAfterTheTimeout() { + AssertionFailedError error = assertThrows(AssertionFailedError.class, () -> { + assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, () -> { + waitForInterrupt(); + return "Tempus Fugit"; + }, () -> "Tempus" + " " + "Fugit"); + }); + + assertMessageEquals(error, + "Tempus Fugit ==> execution timed out after " + PREEMPTIVE_TIMEOUT.toMillis() + " ms"); + assertMessageStartsWith(error.getCause(), "Execution timed out in "); + assertStackTraceContains(error.getCause().getStackTrace(), "CountDownLatch", "await"); + } + + @Test + void assertTimeoutPreemptivelyUsesThreadsWithSpecificNamePrefix() { + AtomicReference threadName = new AtomicReference<>(""); + assertTimeoutPreemptively(ofMillis(1000), () -> threadName.set(Thread.currentThread().getName())); + assertTrue(threadName.get().startsWith("junit-timeout-thread-"), + "Thread name does not match the expected prefix"); + } + + @Test + void assertTimeoutPreemptivelyThrowingTimeoutExceptionWithMessageForSupplierThatCompletesAfterTheTimeout() { + assertThrows(TimeoutException.class, () -> Assertions.assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, () -> { + waitForInterrupt(); + return "Tempus Fugit"; + }, () -> "Tempus Fugit", TIMEOUT_EXCEPTION_FACTORY)); + } + + @Test + void assertTimeoutPreemptivelyThrowingTimeoutExceptionWithMessageForSupplierThatThrowsAnAssertionFailedError() { + AssertionFailedError exception = assertThrows(AssertionFailedError.class, + () -> Assertions.assertTimeoutPreemptively(ofMillis(500), () -> fail("enigma"), () -> "Tempus Fugit", + TIMEOUT_EXCEPTION_FACTORY)); + assertMessageEquals(exception, "enigma"); + } + + @Test + void assertTimeoutPreemptivelyThrowingTimeoutExceptionWithMessageForSupplierThatThrowsAnException() { + RuntimeException exception = assertThrows(RuntimeException.class, + () -> Assertions.assertTimeoutPreemptively(ofMillis(500), + () -> ExceptionUtils.throwAsUncheckedException(new RuntimeException(":(")), () -> "Tempus Fugit", + TIMEOUT_EXCEPTION_FACTORY)); + assertMessageEquals(exception, ":("); + } + + @Test + void assertTimeoutPreemptivelyThrowingTimeoutExceptionWithMessageForSupplierThatCompletesBeforeTimeout() + throws Exception { + var result = Assertions.assertTimeoutPreemptively(PREEMPTIVE_TIMEOUT, () -> "Tempus Fugit", + () -> "Tempus Fugit", TIMEOUT_EXCEPTION_FACTORY); + + assertThat(result).isEqualTo("Tempus Fugit"); + } + + private void waitForInterrupt() { + try { + assertFalse(Thread.interrupted(), "Already interrupted"); + new CountDownLatch(1).await(); + } + catch (InterruptedException ignore) { + // ignore + } + } + + /** + * Assert the given stack trace elements contain an element with the given class name and method name. + */ + private static void assertStackTraceContains(StackTraceElement[] stackTrace, String className, String methodName) { + assertThat(stackTrace).anySatisfy(element -> { + assertThat(element.getClassName()).endsWith(className); + assertThat(element.getMethodName()).isEqualTo(methodName); + }); + } +} diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TimeoutExtensionTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TimeoutExtensionTests.java index 38249d9b03ec..012074155dbe 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TimeoutExtensionTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TimeoutExtensionTests.java @@ -60,6 +60,7 @@ import org.junit.platform.testkit.engine.EngineExecutionResults; import org.junit.platform.testkit.engine.Events; import org.junit.platform.testkit.engine.Execution; +import org.opentest4j.AssertionFailedError; /** * @since 5.5 @@ -348,9 +349,13 @@ void timeoutExceededInSeparateThread() { EngineExecutionResults results = executeTestsForClass(TimeoutExceedingSeparateThreadTestCase.class); Execution execution = findExecution(results.testEvents(), "testMethod()"); - assertThat(execution.getTerminationInfo().getExecutionResult().getThrowable().orElseThrow()) // + Throwable failure = execution.getTerminationInfo().getExecutionResult().getThrowable().orElseThrow(); + assertThat(failure) // .isInstanceOf(TimeoutException.class) // - .hasMessage("testMethod() timed out after 10 milliseconds"); + .hasMessage("testMethod() timed out after 100 milliseconds"); + assertThat(failure.getCause()) // + .hasMessageStartingWith("Execution timed out in ") // + .hasStackTraceContaining(TimeoutExceedingSeparateThreadTestCase.class.getName() + ".testMethod"); } @Test @@ -376,19 +381,35 @@ void separateThreadHandlesInvocationExceptions() { Execution execution = findExecution(results.testEvents(), "test()"); assertThat(execution.getDuration()) // - .isLessThanOrEqualTo(Duration.ofMillis(10)); + .isLessThan(Duration.ofSeconds(5)); assertThat(execution.getTerminationInfo().getExecutionResult().getThrowable().orElseThrow()) // .isInstanceOf(RuntimeException.class) // .hasMessage("Oppps!"); } + @Test + @DisplayName("propagates assertion exceptions") + void separateThreadHandlesOpenTestFailedAssertion() { + EngineExecutionResults results = executeTestsForClass(FailedAssertionInSeparateThreadTestCase.class); + + Execution openTestFailure = findExecution(results.testEvents(), "testOpenTestAssertion()"); + assertThat(openTestFailure.getDuration()) // + .isLessThan(Duration.ofSeconds(5)); + assertThat(openTestFailure.getTerminationInfo().getExecutionResult().getThrowable().orElseThrow()) // + .isInstanceOf(AssertionFailedError.class); + + Execution javaLangFailure = findExecution(results.testEvents(), "testJavaLangAssertion()"); + assertThat(javaLangFailure.getDuration()) // + .isLessThan(Duration.ofSeconds(5)); + assertThat(javaLangFailure.getTerminationInfo().getExecutionResult().getThrowable().orElseThrow()) // + .isInstanceOf(AssertionError.class); + } + @Test @DisplayName("when one test is stuck \"forever\" the next tests should not get stuck") void oneThreadStuckForever() { EngineExecutionResults results = executeTestsForClass(OneTestStuckForeverAndTheOthersNotTestCase.class); - results.allEvents().debug(); - Execution stuckExecution = findExecution(results.testEvents(), "stuck()"); assertThat(stuckExecution.getTerminationInfo().getExecutionResult().getThrowable().orElseThrow()) // .isInstanceOf(TimeoutException.class) // @@ -700,7 +721,7 @@ void testMethod() throws InterruptedException { static class TimeoutExceedingSeparateThreadTestCase { @Test - @Timeout(value = 10, unit = MILLISECONDS, threadMode = SEPARATE_THREAD) + @Timeout(value = 100, unit = MILLISECONDS, threadMode = SEPARATE_THREAD) void testMethod() throws InterruptedException { Thread.sleep(1000); } @@ -723,12 +744,26 @@ void test() { static class ExceptionInSeparateThreadTestCase { @Test - @Timeout(value = 100, unit = MILLISECONDS, threadMode = SEPARATE_THREAD) + @Timeout(value = 5, unit = SECONDS, threadMode = SEPARATE_THREAD) void test() { throw new RuntimeException("Oppps!"); } } + static class FailedAssertionInSeparateThreadTestCase { + @Test + @Timeout(value = 5, unit = SECONDS, threadMode = SEPARATE_THREAD) + void testOpenTestAssertion() { + throw new AssertionFailedError(); + } + + @Test + @Timeout(value = 5, unit = SECONDS, threadMode = SEPARATE_THREAD) + void testJavaLangAssertion() { + throw new AssertionError(); + } + } + @Timeout(value = 100, unit = MILLISECONDS, threadMode = SEPARATE_THREAD) static class TimeoutExceededOnClassLevelTestCase { @Test diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java index 7f475a23081f..7a2999eaaf56 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java @@ -95,6 +95,7 @@ private Stream parseValueArray() { // Lazily retrieve headers if necessary. if (useHeadersInDisplayName && headers == null) { headers = getHeaders(this.csvParser); + continue; } Preconditions.notNull(csvRecord, () -> "Record at index " + index + " contains invalid CSV: \"" + input + "\""); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java index 4be203387d09..a4696a2739ec 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java @@ -11,13 +11,20 @@ package org.junit.jupiter.params.provider; import static java.lang.String.format; +import static java.util.stream.Collectors.toList; import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated; +import static org.junit.platform.commons.util.CollectionUtils.isConvertibleToStream; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.support.AnnotationConsumer; import org.junit.platform.commons.JUnitException; @@ -72,16 +79,40 @@ private Method getFactoryMethodByFullyQualifiedName(String fullyQualifiedMethodN methodParameters, className))); } + /** + * Find all methods in the given {@code testClass} with the desired {@code factoryMethodName} + * which have return types that can be converted to a {@link Stream}, ignoring the + * {@code testMethod} itself as well as any {@code @Test}, {@code @TestTemplate}, + * or {@code @TestFactory} methods with the same name. + */ private Method getFactoryMethodBySimpleName(Class testClass, Method testMethod, String factoryMethodName) { - // Find all methods with the desired factory method name, but ignore the test method itself. - List methods = ReflectionUtils.findMethods(testClass, - factoryMethod -> factoryMethodName.equals(factoryMethod.getName()) && !testMethod.equals(factoryMethod)); - Preconditions.condition(methods.size() > 0, - () -> format("Could not find factory method [%s] in class [%s]", factoryMethodName, testClass.getName())); - Preconditions.condition(methods.size() == 1, - () -> format("Several factory methods named [%s] were found in class [%s]", factoryMethodName, - testClass.getName())); - return methods.get(0); + Predicate isCandidate = candidate -> factoryMethodName.equals(candidate.getName()) + && !testMethod.equals(candidate); + List candidates = ReflectionUtils.findMethods(testClass, isCandidate); + Predicate isFactoryMethod = method -> isConvertibleToStream(method.getReturnType()) + && !isTestMethod(method); + List factoryMethods = candidates.stream().filter(isFactoryMethod).collect(toList()); + Preconditions.condition(factoryMethods.size() > 0, () -> { + // If we didn't find the factory method using the isFactoryMethod Predicate, perhaps + // the specified factory method has an invalid return type or is a test method. + // In that case, we report the invalid candidates that were found. + if (candidates.size() > 0) { + return format( + "Could not find valid factory method [%s] in class [%s] but found the following invalid candidates: %s", + factoryMethodName, testClass.getName(), candidates); + } + // Otherwise, report that we didn't find anything. + return format("Could not find factory method [%s] in class [%s]", factoryMethodName, testClass.getName()); + }); + Preconditions.condition(factoryMethods.size() == 1, + () -> format("%d factory methods named [%s] were found in class [%s]: %s", factoryMethods.size(), + factoryMethodName, testClass.getName(), factoryMethods)); + return factoryMethods.get(0); + } + + private boolean isTestMethod(Method candidate) { + return isAnnotated(candidate, Test.class) || isAnnotated(candidate, TestTemplate.class) + || isAnnotated(candidate, TestFactory.class); } private Class loadRequiredClass(String className) { diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java index 50aa14fc2732..56f35329262d 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java @@ -12,9 +12,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.junit.jupiter.api.Named.named; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.appendTestTemplateInvocationSegment; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestTemplateMethod; @@ -53,13 +56,17 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.TestReporter; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; @@ -722,11 +729,6 @@ void reportsContainerWithAssumptionFailureInMethodSourceAsAborted() { message("Assumption failed: nothing to test")))); } - private EngineExecutionResults execute(String methodName, Class... methodParameterTypes) { - return ParameterizedTestIntegrationTests.this.execute(MethodSourceTestCase.class, methodName, - methodParameterTypes); - } - @Test void namedParameters() { execute("namedParameters", String.class).allEvents().assertThatEvents() // @@ -745,6 +747,27 @@ void nameParametersAlias() { event(test(), displayName("default name"), finishedWithFailure(message("default name")))); } + /** + * @since 5.9.1 + * @see https://github.com/junit-team/junit5/issues/3001 + */ + @Test + void duplicateMethodNames() { + // It is sufficient to assert that 8 tests started and finished, because + // without the fix for #3001 the 4 parameterized tests would fail. In + // other words, we're not really testing the support for @RepeatedTest + // and @TestFactory, but their presence also contributes to the bug + // reported in #3001. + ParameterizedTestIntegrationTests.this.execute(selectClass(DuplicateMethodNamesMethodSourceTestCase.class))// + .testEvents()// + .assertStatistics(stats -> stats.started(8).failed(0).finished(8)); + } + + private EngineExecutionResults execute(String methodName, Class... methodParameterTypes) { + return ParameterizedTestIntegrationTests.this.execute(MethodSourceTestCase.class, methodName, + methodParameterTypes); + } + } @Nested @@ -1251,6 +1274,53 @@ static List assumptionFailureInMethodSourceFactoryMethod() { } + /** + * @since 5.9.1 + * @see https://github.com/junit-team/junit5/issues/3001 + */ + static class DuplicateMethodNamesMethodSourceTestCase { + + @ParameterizedTest + @MethodSource + void test(String value) { + test(1, value); + } + + @ParameterizedTest + @MethodSource("test") + void anotherTest(String value) { + assertTrue(test(value, 1)); + } + + @RepeatedTest(2) + void test(TestReporter testReporter) { + assertNotNull(testReporter); + } + + @TestFactory + Stream test(TestInfo testInfo) { + return test().map(value -> dynamicTest(value, () -> test(1, value))); + } + + // neither a test method nor a factory method. + // intentionally void. + private void test(int expectedLength, String value) { + assertEquals(expectedLength, value.length()); + } + + // neither a test method nor a factory method. + // intentionally non-void and also not convertible to a Stream. + private boolean test(String value, int expectedLength) { + return (value.length() == expectedLength); + } + + // legitimate factory method. + private static Stream test() { + return Stream.of("a", "b"); + } + + } + static class UnusedArgumentsTestCase { @ParameterizedTest diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java index 872ff28397e8..2fb55af504e6 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java @@ -334,13 +334,21 @@ void honorsCommentCharacterWhenUsingTextBlockAttribute() { @Test void supportsCsvHeadersWhenUsingTextBlockAttribute() { - var annotation = csvSource().useHeadersInDisplayName(true).textBlock(""" + supportsCsvHeaders(csvSource().useHeadersInDisplayName(true).textBlock(""" FRUIT, RANK apple, 1 banana, 2 - """).build(); + """).build()); + } - var arguments = provideArguments(annotation); + @Test + void supportsCsvHeadersWhenUsingValueAttribute() { + supportsCsvHeaders(csvSource().useHeadersInDisplayName(true)// + .lines("FRUIT, RANK", "apple, 1", "banana, 2").build()); + } + + private void supportsCsvHeaders(CsvSource csvSource) { + var arguments = provideArguments(csvSource); Stream argumentsAsStrings = arguments.map(array -> { String[] strings = new String[array.length]; for (int i = 0; i < array.length; i++) { diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java index 1a32fa1f35a7..21f14662ecc9 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java @@ -48,12 +48,24 @@ class MethodArgumentsProviderTests { private MutableExtensionRegistry extensionRegistry; + @Test + void throwsExceptionWhenFactoryMethodDoesNotExist() { + var exception = assertThrows(JUnitException.class, () -> provideArguments("unknownMethod").toArray()); + + assertThat(exception.getMessage()).isEqualTo( + "Could not find factory method [unknownMethod] in class [" + TestCase.class.getName() + "]"); + } + @Test void throwsExceptionForIllegalReturnType() { var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments("providerWithIllegalReturnType").toArray()); - assertThat(exception).hasMessageContaining("Cannot convert instance of java.lang.Integer into a Stream"); + assertThat(exception.getMessage())// + .containsSubsequence("Could not find valid factory method [providerWithIllegalReturnType] in class [", + TestCase.class.getName() + "]", // + "but found the following invalid candidates: ", // + "[static java.lang.Object org.junit.jupiter.params.provider.MethodArgumentsProviderTests$TestCase.providerWithIllegalReturnType()]"); } @Test @@ -194,14 +206,6 @@ void providesArgumentsFromNonStaticFactoryMethodWhenStaticIsNotRequired() { assertThat(arguments).containsExactly(array("foo"), array("bar")); } - @Test - void throwsExceptionWhenFactoryMethodDoesNotExist() { - var exception = assertThrows(JUnitException.class, () -> provideArguments("unknownMethod").toArray()); - - assertThat(exception.getMessage()).contains("Could not find factory method [unknownMethod] in class [", - TestCase.class.getName()); - } - @Test void providesArgumentsUsingDefaultFactoryMethodName() throws Exception { Class testClass = DefaultFactoryMethodNameTestCase.class; @@ -395,8 +399,11 @@ void throwsExceptionWhenSeveralFactoryMethodsWithSameNameAreAvailable() { var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments("stringStreamProviderWithOrWithoutParameter").toArray()); - assertThat(exception.getMessage()).isEqualTo( - "Several factory methods named [stringStreamProviderWithOrWithoutParameter] were found in class [org.junit.jupiter.params.provider.MethodArgumentsProviderTests$TestCase]"); + assertThat(exception.getMessage())// + .startsWith("2 factory methods named [stringStreamProviderWithOrWithoutParameter] were found in " + + "class [org.junit.jupiter.params.provider.MethodArgumentsProviderTests$TestCase]: ")// + .contains("stringStreamProviderWithOrWithoutParameter()", + "stringStreamProviderWithOrWithoutParameter(java.lang.String)"); } @Test diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ReflectionSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ReflectionSupport.java index f0741d0b5c9e..ae11ea789ed0 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ReflectionSupport.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ReflectionSupport.java @@ -282,8 +282,8 @@ public static Optional findMethod(Class clazz, String methodName, Cla } /** - * Find all {@linkplain Method methods} of the supplied class or interface - * that match the specified {@code predicate}. + * Find all distinct {@linkplain Method methods} of the supplied class or + * interface that match the specified {@code predicate}. * *

The results will not contain instance methods that are overridden * or {@code static} methods that are hidden. diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java index c8f677b1b325..a3f83b2f6656 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java @@ -112,6 +112,34 @@ public static Set toSet(T[] values) { return collectingAndThen(toList(), Collections::unmodifiableList); } + /** + * Determine if an instance of the supplied type can be converted into a + * {@code Stream}. + * + *

If this method returns {@code true}, {@link #toStream(Object)} can + * successfully convert an object of the specified type into a stream. See + * {@link #toStream(Object)} for supported types. + * + * @param type the type to check; may be {@code null} + * @return {@code true} if an instance of the type can be converted into a stream + * @since 1.9.1 + * @see #toStream(Object) + */ + @API(status = INTERNAL, since = "1.9.1") + public static boolean isConvertibleToStream(Class type) { + if (type == null || type == void.class) { + return false; + } + return (Stream.class.isAssignableFrom(type)// + || DoubleStream.class.isAssignableFrom(type)// + || IntStream.class.isAssignableFrom(type)// + || LongStream.class.isAssignableFrom(type)// + || Iterable.class.isAssignableFrom(type)// + || Iterator.class.isAssignableFrom(type)// + || Object[].class.isAssignableFrom(type)// + || (type.isArray() && type.getComponentType().isPrimitive())); + } + /** * Convert an object of one of the following supported types into a {@code Stream}. * @@ -131,6 +159,7 @@ public static Set toSet(T[] values) { * @return the resulting stream * @throws PreconditionViolationException if the supplied object is {@code null} * or not one of the supported types + * @see #isConvertibleToStream(Class) */ public static Stream toStream(Object object) { Preconditions.notNull(object, "Object must not be null"); diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java index 654bef9a68e0..31dd5cdc8572 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java @@ -1418,6 +1418,7 @@ public static List findMethods(Class clazz, Predicate predica // @formatter:off return findAllMethodsInHierarchy(clazz, traversalMode).stream() .filter(predicate) + .distinct() // unmodifiable since returned by public, non-internal method(s) .collect(toUnmodifiableList()); // @formatter:on diff --git a/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java b/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java index 73967cb55582..b826a63a5296 100644 --- a/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java +++ b/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java @@ -90,12 +90,6 @@ public class OpenTestReportGeneratingListener implements TestExecutionListener { static final String ENABLED_PROPERTY_NAME = "junit.platform.reporting.open.xml.enabled"; static final String OUTPUT_DIR_PROPERTY_NAME = "junit.platform.reporting.output.dir"; - private static final NamespaceRegistry NAMESPACE_REGISTRY = NamespaceRegistry.builder(Namespace.REPORTING_CORE) // - .add("e", Namespace.REPORTING_EVENTS) // - .add("java", Namespace.REPORTING_JAVA) // - .add("junit", JUnitFactory.NAMESPACE, "https://junit.org/junit5/schemas/open-test-reporting/junit-1.9.xsd") // - .build(); - private final AtomicInteger idCounter = new AtomicInteger(); private final Map inProgressIds = new ConcurrentHashMap<>(); private DocumentWriter eventsFileWriter = DocumentWriter.noop(); @@ -107,10 +101,16 @@ public OpenTestReportGeneratingListener() { public void testPlanExecutionStarted(TestPlan testPlan) { ConfigurationParameters config = testPlan.getConfigurationParameters(); if (isEnabled(config)) { + NamespaceRegistry namespaceRegistry = NamespaceRegistry.builder(Namespace.REPORTING_CORE) // + .add("e", Namespace.REPORTING_EVENTS) // + .add("java", Namespace.REPORTING_JAVA) // + .add("junit", JUnitFactory.NAMESPACE, + "https://junit.org/junit5/schemas/open-test-reporting/junit-1.9.xsd") // + .build(); Path eventsXml = OutputDir.create(config.get(OUTPUT_DIR_PROPERTY_NAME)) // .createFile("junit-platform-events", "xml"); try { - eventsFileWriter = Events.createDocumentWriter(NAMESPACE_REGISTRY, eventsXml); + eventsFileWriter = Events.createDocumentWriter(namespaceRegistry, eventsXml); reportInfrastructure(); } catch (Exception e) { diff --git a/platform-tests/platform-tests.gradle.kts b/platform-tests/platform-tests.gradle.kts index 8f67df13a8bf..cca78db6b588 100644 --- a/platform-tests/platform-tests.gradle.kts +++ b/platform-tests/platform-tests.gradle.kts @@ -1,5 +1,6 @@ import org.gradle.api.tasks.PathSensitivity.NONE import org.gradle.api.tasks.PathSensitivity.RELATIVE +import org.gradle.internal.os.OperatingSystem plugins { `java-library-conventions` @@ -61,6 +62,11 @@ tasks { excludeTags("exclude") } jvmArgs("-Xmx1g") + distribution { + // Retry in a new JVM on Windows to improve chances of successful retries when + // cached resources are used (e.g. in ClasspathScannerTests) + retryInSameJvm.set(!OperatingSystem.current().isWindows) + } } test { // Additional inputs for remote execution with Test Distribution diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java index e8093659ee2b..db8bdad2433f 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java @@ -22,6 +22,7 @@ import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -33,6 +34,10 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.PreconditionViolationException; /** @@ -75,6 +80,62 @@ void toUnmodifiableListThrowsOnMutation() { assertThrows(UnsupportedOperationException.class, numbers::clear); } + @ParameterizedTest + @ValueSource(classes = { // + Stream.class, // + DoubleStream.class, // + IntStream.class, // + LongStream.class, // + Collection.class, // + Iterable.class, // + Iterator.class, // + Object[].class, // + String[].class, // + int[].class, // + double[].class, // + char[].class // + }) + void isConvertibleToStreamForSupportedTypes(Class type) { + assertThat(CollectionUtils.isConvertibleToStream(type)).isTrue(); + } + + @ParameterizedTest + @MethodSource("objectsConvertibleToStreams") + void isConvertibleToStreamForSupportedTypesFromObjects(Object object) { + assertThat(CollectionUtils.isConvertibleToStream(object.getClass())).isTrue(); + } + + static Stream objectsConvertibleToStreams() { + return Stream.of(// + Stream.of("cat", "dog"), // + DoubleStream.of(42.3), // + IntStream.of(99), // + LongStream.of(100000000), // + Set.of(1, 2, 3), // + Arguments.of((Object) new Object[] { 9, 8, 7 }), // + new int[] { 5, 10, 15 }// + ); + } + + @ParameterizedTest + @ValueSource(classes = { // + void.class, // + Void.class, // + Object.class, // + Integer.class, // + String.class, // + int.class, // + boolean.class // + }) + void isConvertibleToStreamForUnsupportedTypes(Class type) { + assertThat(CollectionUtils.isConvertibleToStream(type)).isFalse(); + } + + @Test + void isConvertibleToStreamForNull() { + assertThat(CollectionUtils.isConvertibleToStream(null)).isFalse(); + } + @Test void toStreamWithNull() { Exception exception = assertThrows(PreconditionViolationException.class, () -> CollectionUtils.toStream(null)); diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java index 041a459c61ad..9744850a9da6 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java @@ -1074,6 +1074,16 @@ private static void assertOneFooMethodIn(Class clazz) { assertThat(findMethods(clazz, isFooMethod)).hasSize(1); } + /** + * @since 1.9.1 + * @see https://github.com/junit-team/junit5/issues/2993 + */ + @Test + void findMethodsFindsDistinctMethodsDeclaredInMultipleInterfaces() { + Predicate isStringsMethod = method -> method.getName().equals("strings"); + assertThat(findMethods(DoubleInheritedInterfaceMethodTestCase.class, isStringsMethod)).hasSize(1); + } + @Test void findMethodsInObject() { var methods = findMethods(Object.class, method -> true); @@ -1427,6 +1437,21 @@ void methodWithMultidimensionalObjectArray(Double[][][][][] data) { void methodWithParameterizedMap(Map map) { } + interface StringsInterface1 { + static Stream strings() { + return Stream.of("abc", "def"); + } + } + + interface StringsInterface2 extends StringsInterface1 { + } + + /** + * Inherits strings() from interfaces StringsInterface1 and StringsInterface2. + */ + static class DoubleInheritedInterfaceMethodTestCase implements StringsInterface1, StringsInterface2 { + } + interface Generic { X foo(); diff --git a/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts b/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts index 56dd772c6069..8a27b7b936b0 100644 --- a/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts +++ b/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts @@ -40,6 +40,7 @@ dependencies { testImplementation(libs.ant) { because("we reference Ant's main class") } + testImplementation(libs.bundles.xmlunit) thirdPartyJars(libs.junit4) thirdPartyJars(libs.assertj) @@ -51,6 +52,7 @@ dependencies { antJars(libs.bundles.ant) antJars(projects.junitPlatformConsoleStandalone) antJars(projects.junitPlatformLauncher) + antJars(projects.junitPlatformReporting) mavenDistribution(libs.maven) { artifact { @@ -107,6 +109,9 @@ tasks.test { distribution { requirements.add("jdk=8") + localOnly { + includeClasses.add("platform.tooling.support.tests.GraalVmStarterTests") // GraalVM is not installed on Test Distribution agents + } } jvmArgumentProviders += JavaHomeDir(project, 8) } diff --git a/platform-tooling-support-tests/projects/ant-starter/build.xml b/platform-tooling-support-tests/projects/ant-starter/build.xml index 953cd72f852f..6c805ceb1ad7 100644 --- a/platform-tooling-support-tests/projects/ant-starter/build.xml +++ b/platform-tooling-support-tests/projects/ant-starter/build.xml @@ -52,8 +52,11 @@ + + + diff --git a/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts b/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts new file mode 100644 index 000000000000..edd8bd7b7474 --- /dev/null +++ b/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + java + id("org.graalvm.buildtools.native") +} + +val jupiterVersion: String = System.getenv("JUNIT_JUPITER_VERSION") +val platformVersion: String = System.getenv("JUNIT_PLATFORM_VERSION") + +repositories { + maven { url = uri(file(System.getProperty("maven.repo"))) } + mavenCentral() +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter:$jupiterVersion") + testRuntimeOnly("org.junit.platform:junit-platform-reporting:$platformVersion") +} + +tasks.test { + useJUnitPlatform() + + val outputDir = reports.junitXml.outputLocation + jvmArgumentProviders += CommandLineArgumentProvider { + listOf( + "-Djunit.platform.reporting.open.xml.enabled=true", + "-Djunit.platform.reporting.output.dir=${outputDir.get().asFile.absolutePath}" + ) + } +} diff --git a/platform-tooling-support-tests/projects/graalvm-starter/gradle.properties b/platform-tooling-support-tests/projects/graalvm-starter/gradle.properties new file mode 100644 index 000000000000..83d0ce01114f --- /dev/null +++ b/platform-tooling-support-tests/projects/graalvm-starter/gradle.properties @@ -0,0 +1 @@ +org.gradle.java.installations.fromEnv=GRAALVM_HOME diff --git a/platform-tooling-support-tests/projects/graalvm-starter/settings.gradle.kts b/platform-tooling-support-tests/projects/graalvm-starter/settings.gradle.kts new file mode 100644 index 000000000000..c928064e495a --- /dev/null +++ b/platform-tooling-support-tests/projects/graalvm-starter/settings.gradle.kts @@ -0,0 +1,11 @@ +pluginManagement { + plugins { + id("org.graalvm.buildtools.native") version "0.9.13" + } + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "graalvm-starter" diff --git a/platform-tooling-support-tests/projects/graalvm-starter/src/main/java/com/example/project/Calculator.java b/platform-tooling-support-tests/projects/graalvm-starter/src/main/java/com/example/project/Calculator.java new file mode 100644 index 000000000000..b90710f2c68d --- /dev/null +++ b/platform-tooling-support-tests/projects/graalvm-starter/src/main/java/com/example/project/Calculator.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package com.example.project; + +public class Calculator { + + public int add(int a, int b) { + return a + b; + } + +} diff --git a/platform-tooling-support-tests/projects/graalvm-starter/src/test/java/com/example/project/CalculatorTests.java b/platform-tooling-support-tests/projects/graalvm-starter/src/test/java/com/example/project/CalculatorTests.java new file mode 100644 index 000000000000..7f043f0dd3ea --- /dev/null +++ b/platform-tooling-support-tests/projects/graalvm-starter/src/test/java/com/example/project/CalculatorTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package com.example.project; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class CalculatorTests { + + @Test + @DisplayName("1 + 1 = 2") + void addsTwoNumbers() { + Calculator calculator = new Calculator(); + assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2"); + } + + @ParameterizedTest(name = "{0} + {1} = {2}") + @CsvSource({ + "0, 1, 1", + "1, 2, 3", + "49, 51, 100", + "1, 100, 101" + }) + void add(int first, int second, int expectedResult) { + Calculator calculator = new Calculator(); + assertEquals(expectedResult, calculator.add(first, second), + () -> first + " + " + second + " should equal " + expectedResult); + } +} diff --git a/platform-tooling-support-tests/projects/gradle-kotlin-extensions/build.gradle.kts b/platform-tooling-support-tests/projects/gradle-kotlin-extensions/build.gradle.kts index c5245e8a531e..c79b5ff77427 100644 --- a/platform-tooling-support-tests/projects/gradle-kotlin-extensions/build.gradle.kts +++ b/platform-tooling-support-tests/projects/gradle-kotlin-extensions/build.gradle.kts @@ -9,9 +9,6 @@ repositories { mavenCentral() } -// don't use `build` as target to prevent Jenkins picking up -project.buildDir = file("bin") - // grab jupiter version from system environment val jupiterVersion = System.getenv("JUNIT_JUPITER_VERSION") diff --git a/platform-tooling-support-tests/projects/gradle-missing-engine/build.gradle.kts b/platform-tooling-support-tests/projects/gradle-missing-engine/build.gradle.kts index 29906a0122be..022b1d3a71eb 100644 --- a/platform-tooling-support-tests/projects/gradle-missing-engine/build.gradle.kts +++ b/platform-tooling-support-tests/projects/gradle-missing-engine/build.gradle.kts @@ -2,9 +2,6 @@ plugins { java } -// don't use `build` as target to prevent Jenkins picking up -buildDir = file("bin") - // grab jupiter version from system environment val jupiterVersion: String = System.getenv("JUNIT_JUPITER_VERSION") val vintageVersion: String = System.getenv("JUNIT_VINTAGE_VERSION") diff --git a/platform-tooling-support-tests/projects/gradle-starter/build.gradle.kts b/platform-tooling-support-tests/projects/gradle-starter/build.gradle.kts index fc89adfbd7eb..dfc4a31ad87e 100644 --- a/platform-tooling-support-tests/projects/gradle-starter/build.gradle.kts +++ b/platform-tooling-support-tests/projects/gradle-starter/build.gradle.kts @@ -2,22 +2,11 @@ plugins { java } -// don't use `build` as target to prevent Jenkins picking up -buildDir = file("bin") - // grab jupiter version from system environment val jupiterVersion: String = System.getenv("JUNIT_JUPITER_VERSION") val vintageVersion: String = System.getenv("JUNIT_VINTAGE_VERSION") val platformVersion: String = System.getenv("JUNIT_PLATFORM_VERSION") -// emit default file encoding to a file -file("file.encoding.txt").writeText(System.getProperty("file.encoding")) -file("junit.versions.txt").writeText(""" -jupiterVersion=$jupiterVersion -vintageVersion=$vintageVersion -platformVersion=$platformVersion -""") - repositories { maven { url = uri(file(System.getProperty("maven.repo"))) } mavenCentral() @@ -25,6 +14,7 @@ repositories { dependencies { testImplementation("org.junit.jupiter:junit-jupiter:$jupiterVersion") + testRuntimeOnly("org.junit.platform:junit-platform-reporting:$platformVersion") } tasks.test { @@ -38,6 +28,14 @@ tasks.test { html.isEnabled = true } + val outputDir = reports.junitXml.outputLocation + jvmArgumentProviders += CommandLineArgumentProvider { + listOf( + "-Djunit.platform.reporting.open.xml.enabled=true", + "-Djunit.platform.reporting.output.dir=${outputDir.get().asFile.absolutePath}" + ) + } + doFirst { println("Using Java version: ${JavaVersion.current()}") } diff --git a/platform-tooling-support-tests/projects/maven-starter/pom.xml b/platform-tooling-support-tests/projects/maven-starter/pom.xml index 21259e2fb99f..9ec72ae39829 100644 --- a/platform-tooling-support-tests/projects/maven-starter/pom.xml +++ b/platform-tooling-support-tests/projects/maven-starter/pom.xml @@ -12,6 +12,7 @@ 1.8 ${maven.compiler.source} ${env.JUNIT_JUPITER_VERSION} + ${env.JUNIT_PLATFORM_VERSION} @@ -21,6 +22,12 @@ ${junit.jupiter.version} test + + org.junit.platform + junit-platform-reporting + ${junit.platform.version} + test + @@ -32,6 +39,14 @@ maven-surefire-plugin 2.22.2 + + + + junit.platform.reporting.open.xml.enabled = true + junit.platform.reporting.output.dir = target/surefire-reports + + + org.codehaus.gmaven diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java index 900e3e8c8736..371dfe77d598 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java @@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertLinesMatch; +import static platform.tooling.support.tests.XmlAssertions.verifyContainsExpectedStartedOpenTestReport; import java.util.List; @@ -29,13 +30,14 @@ class AntStarterTests { @Test void ant_starter() { - var result = Request.builder() // + var request = Request.builder() // .setTool(new Java()) // .setProject("ant-starter") // .addArguments("-cp", System.getProperty("antJars"), Main.class.getName()) // .addArguments("-verbose") // - .build() // - .run(); + .build(); + + var result = request.run(); assertFalse(result.isTimedOut(), () -> "tool timed out: " + result); @@ -54,5 +56,8 @@ void ant_starter() { " \\[java\\] \\[ 0 tests failed \\]", // ">> TAIL >>"), // result.getOutputLines("out")); + + var testResultsDir = Request.WORKSPACE.resolve(request.getWorkspace()).resolve("build/test-report"); + verifyContainsExpectedStartedOpenTestReport(testResultsDir); } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GraalVmStarterTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GraalVmStarterTests.java new file mode 100644 index 000000000000..526393d13a47 --- /dev/null +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GraalVmStarterTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package platform.tooling.support.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static platform.tooling.support.tests.XmlAssertions.verifyContainsExpectedStartedOpenTestReport; + +import java.nio.file.Paths; +import java.time.Duration; + +import de.sormuras.bartholdy.tool.GradleWrapper; + +import org.junit.jupiter.api.Test; + +import platform.tooling.support.MavenRepo; +import platform.tooling.support.Request; + +/** + * @since 1.9.1 + */ +class GraalVmStarterTests { + + @Test + void runsTestsInNativeImage() { + var request = Request.builder() // + .setTool(new GradleWrapper(Paths.get(".."))) // + .setProject("graalvm-starter") // + .addArguments("-Dmaven.repo=" + MavenRepo.dir()) // + .addArguments("javaToolchains", "nativeTest", "--no-daemon", "--stacktrace") // + .setTimeout(Duration.ofMinutes(5)) // + .build(); + + var result = request.run(); + + assertFalse(result.isTimedOut(), () -> "tool timed out: " + result); + + assumeFalse( + result.getOutputLines("err").stream().anyMatch(line -> line.contains("No compatible toolchains found")), + "Abort test if GraalVM is not installed"); + + assertEquals(0, result.getExitCode()); + assertTrue(result.getOutputLines("out").stream().anyMatch(line -> line.contains("BUILD SUCCESSFUL"))); + + var testResultsDir = Request.WORKSPACE.resolve(request.getWorkspace()).resolve("build/test-results/test"); + verifyContainsExpectedStartedOpenTestReport(testResultsDir); + } +} diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleStarterTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleStarterTests.java index 975a6f3d0986..076c998ca40b 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleStarterTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleStarterTests.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static platform.tooling.support.Helper.TOOL_TIMEOUT; +import static platform.tooling.support.tests.XmlAssertions.verifyContainsExpectedStartedOpenTestReport; import java.nio.file.Paths; @@ -34,20 +35,24 @@ class GradleStarterTests { @Test void gradle_wrapper() { - var result = Request.builder() // + var request = Request.builder() // .setTool(new GradleWrapper(Paths.get(".."))) // .setProject("gradle-starter") // .addArguments("-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("build", "--no-daemon", "--stacktrace") // .setTimeout(TOOL_TIMEOUT) // .setJavaHome(Helper.getJavaHome("8").orElseThrow(TestAbortedException::new)) // - .build() // - .run(); + .build(); + + var result = request.run(); assertFalse(result.isTimedOut(), () -> "tool timed out: " + result); assertEquals(0, result.getExitCode()); assertTrue(result.getOutputLines("out").stream().anyMatch(line -> line.contains("BUILD SUCCESSFUL"))); assertThat(result.getOutput("out")).contains("Using Java version: 1.8"); + + var testResultsDir = Request.WORKSPACE.resolve(request.getWorkspace()).resolve("build/test-results/test"); + verifyContainsExpectedStartedOpenTestReport(testResultsDir); } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenStarterTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenStarterTests.java index e7c4b9b520cd..f4caa516af2e 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenStarterTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenStarterTests.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static platform.tooling.support.Helper.TOOL_TIMEOUT; +import static platform.tooling.support.tests.XmlAssertions.verifyContainsExpectedStartedOpenTestReport; import org.junit.jupiter.api.Test; import org.opentest4j.TestAbortedException; @@ -30,15 +31,16 @@ class MavenStarterTests { @Test void verifyMavenStarterProject() { - var result = Request.builder() // + var request = Request.builder() // .setTool(Request.maven()) // .setProject("maven-starter") // .addArguments("-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("--update-snapshots", "--batch-mode", "verify") // .setTimeout(TOOL_TIMEOUT) // .setJavaHome(Helper.getJavaHome("8").orElseThrow(TestAbortedException::new)) // - .build() // - .run(); + .build(); + + var result = request.run(); assertFalse(result.isTimedOut(), () -> "tool timed out: " + result); @@ -47,5 +49,8 @@ void verifyMavenStarterProject() { assertTrue(result.getOutputLines("out").contains("[INFO] BUILD SUCCESS")); assertTrue(result.getOutputLines("out").contains("[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0")); assertThat(result.getOutput("out")).contains("Using Java version: 1.8"); + + var testResultsDir = Request.WORKSPACE.resolve(request.getWorkspace()).resolve("target/surefire-reports"); + verifyContainsExpectedStartedOpenTestReport(testResultsDir); } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/XmlAssertions.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/XmlAssertions.java new file mode 100644 index 000000000000..976920ae03bd --- /dev/null +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/XmlAssertions.java @@ -0,0 +1,158 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package platform.tooling.support.tests; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.xmlunit.assertj3.XmlAssert; +import org.xmlunit.placeholder.PlaceholderDifferenceEvaluator; + +class XmlAssertions { + + static void verifyContainsExpectedStartedOpenTestReport(Path testResultsDir) { + try (var files = Files.list(testResultsDir)) { + Path xmlFile = files.filter(it -> it.getFileName().toString().startsWith("junit-platform-events-")) // + .findAny() // + .orElseThrow(() -> new AssertionError("Missing open-test-reporting XML file in " + testResultsDir)); + verifyContent(xmlFile); + } + catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static void verifyContent(Path xmlFile) { + var expected = """ + + + ${xmlunit.ignore} + ${xmlunit.ignore} + ${xmlunit.ignore} + ${xmlunit.ignore} + ${xmlunit.ignore} + ${xmlunit.ignore} + + + + + [engine:junit-jupiter] + JUnit Jupiter + CONTAINER + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests] + com.example.project.CalculatorTests + CONTAINER + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[method:addsTwoNumbers()] + addsTwoNumbers() + TEST + + + + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)] + add(int, int, int) + CONTAINER + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#1] + add(int, int, int)[1] + TEST + + + + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#2] + add(int, int, int)[2] + TEST + + + + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#3] + add(int, int, int)[3] + TEST + + + + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#4] + add(int, int, int)[4] + TEST + + + + + + + + + + + + + + + + + + + """; + + XmlAssert.assertThat(xmlFile).and(expected) // + .withDifferenceEvaluator(new PlaceholderDifferenceEvaluator()) // + .ignoreWhitespace() // + .areIdentical(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 1b3ced7b5dcf..6cc4aa4ef877 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,9 +5,8 @@ pluginManagement { gradlePluginPortal() } plugins { - id("com.gradle.enterprise") version "3.10.3" - id("com.gradle.enterprise.test-distribution") version "2.3.5" // keep in sync with buildSrc/build.gradle.kts - id("com.gradle.common-custom-user-data-gradle-plugin") version "1.7.2" + id("com.gradle.enterprise") version "3.11.1" // keep in sync with buildSrc/build.gradle.kts + id("com.gradle.common-custom-user-data-gradle-plugin") version "1.8.1" id("org.ajoberstar.git-publish") version "3.0.0" kotlin("jvm") version "1.5.31" // Check if workaround in documentation.gradle.kts can be removed when upgrading @@ -21,7 +20,6 @@ pluginManagement { plugins { id("com.gradle.enterprise") - id("com.gradle.enterprise.test-distribution") id("com.gradle.common-custom-user-data-gradle-plugin") } @@ -38,6 +36,7 @@ dependencyResolutionManagement { val gradleEnterpriseServer = "https://ge.junit.org" val isCiServer = System.getenv("CI") != null +val junitBuildCacheUrl: String? by extra val junitBuildCacheUsername: String? by extra val junitBuildCachePassword: String? by extra @@ -79,7 +78,7 @@ buildCache { isEnabled = !isCiServer } remote { - url = uri("$gradleEnterpriseServer/cache/") + url = uri(junitBuildCacheUrl ?: "$gradleEnterpriseServer/cache/") isPush = isCiServer && !junitBuildCacheUsername.isNullOrEmpty() && !junitBuildCachePassword.isNullOrEmpty() credentials { username = junitBuildCacheUsername?.ifEmpty { null } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy