diff --git a/.github/actions/run-gradle/action.yml b/.github/actions/run-gradle/action.yml index ad901772a30c..7506f777a7ce 100644 --- a/.github/actions/run-gradle/action.yml +++ b/.github/actions/run-gradle/action.yml @@ -11,13 +11,13 @@ inputs: runs: using: "composite" steps: - - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 id: setup-gradle-jdk with: distribution: temurin java-version: 21 check-latest: true - - uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0 + - uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1 with: cache-encryption-key: ${{ inputs.encryptionKey }} - shell: bash diff --git a/.github/actions/setup-test-jdk/action.yml b/.github/actions/setup-test-jdk/action.yml index 3fd0f16bdc48..b2d6b1dbc46b 100644 --- a/.github/actions/setup-test-jdk/action.yml +++ b/.github/actions/setup-test-jdk/action.yml @@ -8,7 +8,7 @@ inputs: runs: using: "composite" steps: - - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: ${{ inputs.distribution }} java-version: 8 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 93dd4a2889de..31c9a5baa610 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: - name: Check out repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 with: languages: ${{ matrix.language }} tools: linked @@ -47,4 +47,4 @@ jobs: -Dscan.tag.CodeQL \ allMainClasses - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 diff --git a/.github/workflows/cross-version.yml b/.github/workflows/cross-version.yml index 1a69c3acb333..7c51412feaaf 100644 --- a/.github/workflows/cross-version.yml +++ b/.github/workflows/cross-version.yml @@ -24,7 +24,6 @@ jobs: jdk: - version: 24 type: ga - distribution: oracle - version: 25 type: ea name: "OpenJDK ${{ matrix.jdk.version }} (${{ matrix.jdk.release || matrix.jdk.type }})" @@ -45,7 +44,7 @@ jobs: version: latest - name: "Set up JDK ${{ matrix.jdk.version }} (${{ matrix.jdk.distribution || 'temurin' }})" if: matrix.jdk.type == 'ga' - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: ${{ matrix.jdk.distribution || 'temurin' }} java-version: ${{ matrix.jdk.version }} @@ -85,7 +84,7 @@ jobs: with: distribution: semeru - name: 'Set up JDK ${{ matrix.jdk }}' - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: semeru java-version: ${{ matrix.jdk }} diff --git a/.github/workflows/gradle-dependency-submission.yml b/.github/workflows/gradle-dependency-submission.yml index 1e1c037ef8af..d99e7d48ca52 100644 --- a/.github/workflows/gradle-dependency-submission.yml +++ b/.github/workflows/gradle-dependency-submission.yml @@ -19,10 +19,10 @@ jobs: with: fetch-depth: 1 - name: Setup Java - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: temurin java-version: 21 check-latest: true - name: Generate and submit dependency graph - uses: gradle/actions/dependency-submission@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0 + uses: gradle/actions/dependency-submission@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f8d924a763fa..e41fbe61130b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,7 +41,7 @@ jobs: jacocoRootReport \ --no-configuration-cache # Disable configuration cache due to https://github.com/diffplug/spotless/issues/2318 - name: Upload to Codecov.io - uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0 + uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -93,7 +93,7 @@ jobs: publish -x check \ prepareGitHubAttestation - name: Generate build provenance attestations - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 with: subject-path: documentation/build/attestation/*.jar diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 079c60cf265d..56874e9f9c9d 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -57,6 +57,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 with: sarif_file: results.sarif diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 96b65473e615..5b363033f4b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: :verifyArtifactsInStagingRepositoryAreReproducible \ --remote-repo-url=${{ env.STAGING_REPO_URL }} - name: Generate build provenance attestations - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 with: subject-path: build/repo/**/*.jar - name: Upload local repository for later jobs @@ -71,7 +71,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} fetch-depth: 1 - name: Set up JDK - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: java-version: 21 distribution: temurin @@ -191,7 +191,7 @@ jobs: fetch-depth: 1 ref: "refs/tags/${{ env.RELEASE_TAG }}" - name: Download local Maven repository - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: local-maven-repository path: build/repo @@ -212,7 +212,7 @@ jobs: token: ${{ secrets.GH_TOKEN }} fetch-depth: 1 - name: Set up JDK - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: java-version: 21 distribution: temurin diff --git a/README.md b/README.md index 2fc4dd7334d1..7268c5826da6 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ This repository is the home of _JUnit 5_. ## Latest Releases -- General Availability (GA): [JUnit 5.12.1](https://github.com/junit-team/junit5/releases/tag/r5.12.1) (March 14, 2025) -- Preview (Milestone/Release Candidate): [JUnit 5.13.0-M2](https://github.com/junit-team/junit5/releases/tag/r5.13.0-M2) (March 24, 2025) +- General Availability (GA): [JUnit 5.12.2](https://github.com/junit-team/junit5/releases/tag/r5.12.2) (April 11, 2025) +- Preview (Milestone/Release Candidate): [JUnit 5.13.0-M3](https://github.com/junit-team/junit5/releases/tag/r5.13.0-M3) (May 2, 2025) ## Documentation diff --git a/documentation/documentation.gradle.kts b/documentation/documentation.gradle.kts index 5b2ddbfd8df0..799c16ae3833 100644 --- a/documentation/documentation.gradle.kts +++ b/documentation/documentation.gradle.kts @@ -160,6 +160,7 @@ tasks { args.addAll("--config=junit.platform.reporting.open.xml.enabled=true") args.addAll("--config=junit.platform.output.capture.stdout=true") args.addAll("--config=junit.platform.output.capture.stderr=true") + args.addAll("--config=junit.platform.discovery.issue.severity.critical=info") outputs.dir(consoleLauncherTestReportsDir) argumentProviders.add(CommandLineArgumentProvider { listOf( diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 0fc1d24435cd..fbfc2021e938 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -25,6 +25,8 @@ endif::[] :ClasspathResourceSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ClasspathResourceSelector.html[ClasspathResourceSelector] :ClasspathRootSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ClasspathRootSelector.html[ClasspathRootSelector] :ClassSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ClassSelector.html[ClassSelector] +:DiscoveryIssue: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/DiscoveryIssue.html[DiscoveryIssue] +:DiscoveryIssueReporter: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/discovery/DiscoveryIssueReporter.html[DiscoveryIssueReporter] :DirectorySelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DirectorySelector.html[DirectorySelector] :DiscoverySelectors: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html[DiscoverySelectors] :DiscoverySelectors_selectClasspathResource: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectClasspathResource(java.lang.String)[selectClasspathResource] @@ -40,12 +42,14 @@ endif::[] :DiscoverySelectors_selectPackage: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectPackage(java.lang.String)[selectPackage] :DiscoverySelectors_selectUniqueId: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectUniqueId(java.lang.String)[selectUniqueId] :DiscoverySelectors_selectUri: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectUri(java.lang.String)[selectUri] +:EngineDiscoveryListener: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/EngineDiscoveryListener.html[EngineDiscoveryListener] :EngineDiscoveryRequest: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/EngineDiscoveryRequest.html[EngineDiscoveryRequest] :FileSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/FileSelector.html[FileSelector] :HierarchicalTestEngine: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/hierarchical/HierarchicalTestEngine.html[HierarchicalTestEngine] :IterationSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/IterationSelector.html[IterationSelector] :MethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/MethodSelector.html[MethodSelector] :ModuleSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ModuleSelector.html[ModuleSelector] +:NamespacedHierarchicalStore: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.html[NamespacedHierarchicalStore] :NestedClassSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedClassSelector.html[NestedClassSelector] :NestedMethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedMethodSelector.html[NestedMethodSelector] :OutputDirectoryProvider: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/reporting/OutputDirectoryProvider.html[OutputDirectoryProvider] @@ -56,6 +60,7 @@ endif::[] :TestEngine: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/TestEngine.html[TestEngine] // Platform Launcher API :junit-platform-launcher: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/package-summary.html[junit-platform-launcher] +:DiscoveryIssueException: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/core/DiscoveryIssueException.html[DiscoveryIssueException] :Launcher: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/Launcher.html[Launcher] :LauncherConfig: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/core/LauncherConfig.html[LauncherConfig] :LauncherDiscoveryListener: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/LauncherDiscoveryListener.html[LauncherDiscoveryListener] diff --git a/documentation/src/docs/asciidoc/release-notes/index.adoc b/documentation/src/docs/asciidoc/release-notes/index.adoc index bd88f8abbf76..af8a4feabdaf 100644 --- a/documentation/src/docs/asciidoc/release-notes/index.adoc +++ b/documentation/src/docs/asciidoc/release-notes/index.adoc @@ -17,10 +17,14 @@ authors as well as build tool and IDE vendors. include::{includedir}/link-attributes.adoc[] +include::{basedir}/release-notes-5.13.0-M3.adoc[] + include::{basedir}/release-notes-5.13.0-M2.adoc[] include::{basedir}/release-notes-5.13.0-M1.adoc[] +include::{basedir}/release-notes-5.12.2.adoc[] + include::{basedir}/release-notes-5.12.1.adoc[] include::{basedir}/release-notes-5.12.0.adoc[] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.2.adoc new file mode 100644 index 000000000000..224078ba645e --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.2.adoc @@ -0,0 +1,33 @@ +[[release-notes-5.12.2]] +== 5.12.2 + +*Date of Release:* April 11, 2025 + +*Scope:* Bug fixes and enhancements since 5.12.1 + +For a complete list of all _closed_ issues and pull requests for this release, consult the +link:{junit5-repo}+/milestone/95?closed=1+[5.12.2] milestone page in the JUnit repository +on GitHub. + + +[[release-notes-5.12.2-junit-platform]] +=== JUnit Platform + +No changes. + + +[[release-notes-5.12.2-junit-jupiter]] +=== JUnit Jupiter + +[[release-notes-5.12.2-junit-jupiter-bug-fixes]] +==== Bug Fixes + +* Fix handling of `CleanupMode.ON_SUCCESS` with `@TempDir` that caused no temporary + directories (using that mode) to be deleted after the first failure even if the + corresponding tests passed. + + +[[release-notes-5.12.2-junit-vintage]] +=== JUnit Vintage + +No changes. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc new file mode 100644 index 000000000000..403116a7fb51 --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc @@ -0,0 +1,115 @@ +[[release-notes-5.13.0-M3]] +== 5.13.0-M3 + +*Date of Release:* May 2, 2025 + +*Scope:* + +* Reporting of discovery issues for test engines +* Resource management for launcher sessions and execution requests +* GraalVM: removal of `native-image.properties` files from JARs + +For a complete list of all _closed_ issues and pull requests for this release, consult the +link:{junit5-repo}+/milestone/93?closed=1+[5.13.0-M3] milestone page in the JUnit +repository on GitHub. + + +[[release-notes-5.13.0-M3-overall-improvements]] +=== Overall Changes + +[[release-notes-5.13.0-M3-overall-new-features-and-improvements]] +==== Deprecations and Breaking Changes + +* The JUnit feature in GraalVM Native Build Tools has been rewritten to no longer require + JUnit classes to be initialized at build time. Therefore, JUnit's JARs no longer ship + with `native-image.properties` files that contain `--initialize-at-build-time` options + (introduced in 5.12.0). Please update to the most recent version of GraalVM Native Build + Tools prior to upgrading to this version of JUnit. + + +[[release-notes-5.13.0-M3-junit-platform]] +=== JUnit Platform + +[[release-notes-5.13.0-M3-junit-platform-bug-fixes]] +==== Bug Fixes + +* Reintroduce support for JVM shutdown hooks when using the `-cp`/`--classpath` option of + the `ConsoleLauncher`. Prior to this release, the created class loader was closed prior + to JVM shutdown hooks being invoked, which caused hooks to fail with a + `ClassNotFoundException` when loading classes during shutdown. + +[[release-notes-5.13.0-M3-junit-platform-new-features-and-improvements]] +==== New Features and Improvements + +* Introduce resource management mechanism that allows preparing and sharing state across + executions or test engines via stores that are scoped to a `LauncherSession` or + `ExecutionRequest`. The Jupiter API uses these stores as ancestors to the `Store` + instances accessible via `ExtensionContext` and provides a new method to access them + directly. Please refer to the User Guide for examples of managing + <<../user-guide/index.adoc#launcher-api-launcher-session-listeners-tool-example-usage, session-scoped>> + and + <<../user-guide/index.adoc#launcher-api-managing-state-across-test-engines, request-scoped>> + resources. +* Introduce a mechanism for `TestEngine` implementations to report issues encountered + during test discovery. If an engine reports a `DiscoveryIssue` with a `Severity` equal + to or higher than a configurable critical severity, its tests will not be executed. + Instead, the engine will be reported as failed during execution with a failure message + listing all critical issues. Non-critical issues will be logged but will not prevent the + engine from executing its tests. The critical severity can be configured via a new + configuration parameter and, currently, defaults to `ERROR`. Please refer to the + <<../user-guide/index.adoc#running-tests-discovery-issues, User Guide>> for details. ++ +If you're a test engine maintainer, please see the +<<../user-guide/index.adoc#test-engines-discovery-issues, User Guide>> for details on how +to start reporting discovery issues. +* Start reporting discovery issues for problematic `@Suite` classes: + - Invalid `@Suite` class declarations (for example, when `private`) + - Invalid `@BeforeSuite`/`@AfterSuite` method declarations (for example, when not + `static`) + - Cyclic dependencies between `@Suite` classes +* Make validation of including `EngineFilters` more strict to avoid misconfiguration, for + example, due to typos. Prior to this release, an exception was only thrown when _none_ + of a filter's included IDs matched any engine. Now, an exception is thrown if at least + one included ID across all filters did not match any engine. + + +[[release-notes-5.13.0-M3-junit-jupiter]] +=== JUnit Jupiter + +[[release-notes-5.13.0-M3-junit-jupiter-new-features-and-improvements]] +==== New Features and Improvements + +* Start reporting discovery issues for potentially problematic test classes: + - Invalid `@Test` and `@TestTemplate` method declarations (for example, when return + type is not `void`) + - Invalid `@TestFactory` methods (for example, when return type is invalid) + - Multiple method-level annotations (for example, `@Test` and `@TestTemplate`) + - Invalid test class and `@Nested` class declarations (for example, `static` `@Nested` + classes) + - Potentially missing `@Nested` annotations (for example, non-abstract inner classes + that contain test methods) + - Invalid lifecycle method declarations (for example, when `private`) + - Invalid `@Tag` syntax + - Blank `@DisplayName` declarations + - Blank `@SentenceFragment` declarations + - `@BeforeParameterizedClassInvocation` and `@AfterParameterizedClassInvocation` + methods declared in non-parameterized test classes +* By default, `AutoCloseable` objects put into `ExtensionContext.Store` are now treated + like instances of `CloseableResource` (which has been deprecated) and are closed + automatically when the store is closed at the end of the test lifecycle. It's possible + to <<../user-guide/index.adoc#extensions-keeping-state-autocloseable-support, revert to the old behavior>> + via a configuration parameter. Please also see the + <<../user-guide/index.adoc#extensions-keeping-state-autocloseable-migration, migration note>> + for third-party extensions wanting to support both JUnit 5.13 and earlier versions. +* `java.util.Locale` arguments are now converted according to the IETF BCP 47 language tag + format. See the + <<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit, User Guide>> + for details. +* Avoid reporting potentially misleading validation exception for `@ParameterizedClass` + test classes and `@ParameterizedTest` methods as suppressed exception for earlier + failures. + +[[release-notes-5.13.0-M3-junit-vintage]] +=== JUnit Vintage + +No changes. diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/engines.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/engines.adoc index 460a6fd4ba3c..89f9b127220a 100644 --- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/engines.adoc +++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/engines.adoc @@ -120,3 +120,22 @@ compatibility with build tools and IDEs: siblings or other nodes that are required for the execution of the selected tests. * `TestEngines` _should_ support <> tests and containers so that tag filters can be applied when discovering tests. + +[[test-engines-discovery-issues]] +==== Reporting Discovery Issues + +Test engines should report <> if they +encounter any problems or potential misconfigurations during test discovery. This is +especially important if the issue could lead to tests not being executed at all or only +partially. + +In order to report a `{DiscoveryIssue}`, a test engine should call the +`issueEncountered()` method on the `{EngineDiscoveryListener}` available via the +`{EngineDiscoveryRequest}` passed to its `discover()` method. Rather than passing the +listener around, the `{DiscoveryIssueReporter}` interface should be used. It also provides +a way to create a `Condition` that reports a discovery issue if its check fails and may +be used as a `Predicate` or `Consumer`. Please refer to the implementations of the +<> for examples. + +Moreover, <> provides a way to write tests for +reported discovery issues. diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc index e1fb5e37efca..9f8db34b21f1 100644 --- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc +++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc @@ -1,3 +1,6 @@ +:testDir: ../../../../../src/test/java +:testResourcesDir: ../../../../../src/test/resources + [[launcher-api]] === JUnit Platform Launcher API @@ -132,10 +135,22 @@ package example.session; include::{testDir}/example/session/GlobalSetupTeardownListener.java[tags=user_guide] ---- -<1> Start the HTTP server -<2> Export its host address as a system property for consumption by tests -<3> Export its port as a system property for consumption by tests -<4> Stop the HTTP server +<1> Get the store from the launcher session +<2> Lazily create the HTTP server and put it into the store +<3> Start the HTTP server + +It uses a wrapper class to ensure the server is stopped when the launcher session is +closed: + +[source,java] +.src/test/java/example/session/CloseableHttpServer.java +---- +package example.session; + +include::{testDir}/example/session/CloseableHttpServer.java[tags=user_guide] +---- +<1> The `close()` method is called when the launcher session is closed +<2> Stop the HTTP server This sample uses the HTTP server implementation from the jdk.httpserver module that comes with the JDK but would work similarly with any other server or resource. In order for the @@ -158,10 +173,11 @@ package example.session; include::{testDir}/example/session/HttpTests.java[tags=user_guide] ---- -<1> Read the host address of the server from the system property set by the listener -<2> Read the port of the server from the system property set by the listener -<3> Send a request to the server -<4> Check the status code of the response +<1> Retrieve the HTTP server instance from the store +<2> Get the host string directly from the injected HTTP server instance +<3> Get the port number directly from the injected HTTP server instance +<4> Send a request to the server +<5> Check the status code of the response [[launcher-api-launcher-interceptors-custom]] ==== Registering a LauncherInterceptor @@ -285,3 +301,55 @@ execute any tests but will notify registered `{TestExecutionListener}` instances tests had been skipped and their containers had been successful. This can be useful to test changes in the configuration of a build or to verify a listener is called as expected without having to wait for all tests to be executed. + +[[launcher-api-managing-state-across-test-engines]] +==== Managing State Across Test Engines + +When running tests on the JUnit Platform, multiple test engines may need to access shared +resources. Rather than initializing these resources multiple times, JUnit Platform +provides mechanisms to share state across test engines efficiently. Test engines can use +the Platform's `{NamespacedHierarchicalStore}` API to lazily initialize and share +resources, ensuring they are created only once regardless of execution order. Any resource +that is put into the store and implements `AutoCloseable` will be closed automatically when +the execution is finished. + +TIP: The Jupiter engine allows read and write access to such resources via its +`{ExtensionContext_Store}` API. + +The following example demonstrates two custom test engines sharing a `ServerSocket` +resource. `FirstCustomEngine` attempts to retrieve an existing `ServerSocket` from the +global store or creates a new one if it doesn't exist: + +[source,java] +---- +include::{testDir}/example/FirstCustomEngine.java[tags=user_guide] +---- + +`SecondCustomEngine` follows the same pattern, ensuring that regardless whether it runs +before or after `FirstCustomEngine`, it will use the same socket instance: + +[source,java] +---- +include::{testDir}/example/SecondCustomEngine.java[tags=user_guide] +---- + +TIP: In this case, the `ServerSocket` can be stored directly in the global store while +ensuring since it gets closed because it implements `AutoCloseable`. If you need to use a +type that does not do so, you can wrap it in a custom class that implements +`AutoCloseable` and delegates to the original type. This is important to ensure that the +resource is closed properly when the test run is finished. + +For illustration, the following test verifies that both engines are sharing the same +`ServerSocket` instance and that it's closed after `Launcher.execute()` returns: + +[source,java,indent=0] +---- +include::{testDir}/example/sharedresources/SharedResourceDemo.java[tags=user_guide] +---- + +By using the Platform's `{NamespacedHierarchicalStore}` API with shared namespaces in this +way, test engines can coordinate resource creation and sharing without direct dependencies +between them. + +Alternatively, it's possible to inject resources into test engines by +<>. diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index a89915269d18..e1a3b351aedb 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -863,17 +863,22 @@ surrounding `ExtensionContext`. Since `ExtensionContexts` may be nested, the sco inner contexts may also be limited. Consult the corresponding Javadoc for details on the methods available for storing and retrieving values via the `{ExtensionContext_Store}`. -.`ExtensionContext.Store.CloseableResource` +[[extensions-keeping-state-autocloseable-support]] +.Resource management via `_AutoCloseable_` NOTE: An extension context store is bound to its extension context lifecycle. When an -extension context lifecycle ends it closes its associated store. All stored values -that are instances of `CloseableResource` are notified by an invocation of their `close()` -method in the inverse order they were added in. - -An example implementation of `CloseableResource` is shown below, using an `HttpServer` +extension context lifecycle ends it closes its associated store. As of JUnit 5.13, +all stored values that are instances of `AutoCloseable` are notified by an invocation of +their `close()` method in the inverse order they were added in (unless the +`junit.jupiter.extensions.store.close.autocloseable.enabled` +<> is set to `false`). Older +versions only supported `CloseableResource`, which is deprecated but still available for +backward compatibility. + +An example implementation of `AutoCloseable` is shown below, using an `HttpServer` resource. [source,java,indent=0] -.`HttpServer` resource implementing `CloseableResource` +.`HttpServer` resource implementing `AutoCloseable` ---- include::{testDir}/example/extensions/HttpServerResource.java[tags=user_guide] ---- @@ -896,7 +901,32 @@ include::{testDir}/example/extensions/HttpServerExtension.java[tags=user_guide] include::{testDir}/example/HttpServerDemo.java[tags=user_guide] ---- -[[extensions-conditional-test-execution]] +[[extensions-keeping-state-autocloseable-migration]] +[TIP] +.Migration Note for Resource Cleanup +==== + +Starting with JUnit Jupiter 5.13, the framework automatically closes resources stored in +the `ExtensionContext.Store` that implement `AutoCloseable`. In earlier versions, only +resources implementing `Store.CloseableResource` were automatically closed. + +If you're developing an extension that needs to support both JUnit Jupiter 5.13+ and +earlier versions and your extension stores resources that need to be cleaned up, you +should implement both interfaces: + +[source,java,indent=0] +---- +public class MyResource implements Store.CloseableResource, AutoCloseable { + @Override + public void close() throws Exception { + // Resource cleanup code + } +} +---- + +This ensures that your resource will be properly closed regardless of which JUnit Jupiter +version is being used. +==== [[extensions-supported-utilities]] === Supported Utilities in Extensions diff --git a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc index a8c4bc0824fb..f3568ade9a31 100644 --- a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc @@ -1288,3 +1288,40 @@ never be excluded. In addition, all elements prior to and including the first call from the JUnit Platform Launcher will be removed. + +[[running-tests-discovery-issues]] +=== Discovery Issues + +Test engines may encounter issues during test discovery. For example, the declaration of a +test class or method may be invalid. To avoid such issues from going unnoticed, the JUnit +Platform provides a <> to +report them with different severity levels: + +INFO:: +Indicates that the engine encountered something that could be potentially problematic, but +could also happen due to a valid setup or configuration. + +WARNING:: +Indicates that the engine encountered something that is problematic and might lead to +unexpected behavior or will be removed or changed in a future release. + +ERROR:: +Indicates that the engine encountered something that is definitely problematic and will +lead to unexpected behavior. + +If an engine reports an issue with a severity equal to or higher than a configurable +_critical_ severity, its tests will not be executed. Instead, the engine will be reported +as failed during execution with a `{DiscoveryIssueException}` listing all critical issues. +Non-critical issues will be logged but will not prevent the engine from executing its +tests. The `junit.platform.discovery.issue.severity.critical` +<> can be used to set the critical +severity level. Currently, the default value is `ERROR` but it may be changed in a future +release. + +TIP: To surface all discovery issues in your project, it is recommended to set the +`junit.platform.discovery.issue.severity.critical` configuration parameter to `INFO`. + +In addition, registered `{LauncherDiscoveryListener}` implementations can receive +discovery issues via the `issueEncountered()` method. This allows IDEs and build tools to +report issues to the user in a more user-friendly way. For example, IDEs may choose to +display all issues in a list or table. diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 850c65b9d627..e84ed85c59be 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -1954,6 +1954,10 @@ an array of primitives. The "arguments" within the stream can be supplied as an of `Arguments`, an array of objects (e.g., `Object[]`), or a single value if the parameterized class or test method accepts a single argument. +If the return type is `Stream` or one of the primitive streams, +JUnit will properly close it by calling `BaseStream.close()`, +making it safe to use a resource such as `Files.lines()`. + If you only need a single parameter, you can return a `Stream` of instances of the parameter type as demonstrated in the following example. @@ -2049,6 +2053,10 @@ are _consumed_ the first time they are processed. However, if you wish to use on these types, you can wrap it in a `Supplier` — for example, `Supplier`. ==== +If the `Supplier` return type is `Stream` or one of the primitive streams, +JUnit will properly close it by calling `BaseStream.close()`, +making it safe to use a resource such as `Files.lines()`. + Please note that a one-dimensional array of objects supplied as a set of "arguments" will be handled differently than other types of arguments. Specifically, all the elements of a one-dimensional array of objects will be passed as individual physical arguments to the @@ -2475,10 +2483,16 @@ integral types: `byte`, `short`, `int`, `long`, and their boxed counterparts. | `java.time.ZoneId` | `"Europe/Berlin"` -> `ZoneId.of("Europe/Berlin")` | `java.time.ZoneOffset` | `"+02:30"` -> `ZoneOffset.ofHoursMinutes(2, 30)` | `java.util.Currency` | `"JPY"` -> `Currency.getInstance("JPY")` -| `java.util.Locale` | `"en"` -> `new Locale("en")` +| `java.util.Locale` | `"en-US"` -> `Locale.forLanguageTag("en-US")` | `java.util.UUID` | `"d043e930-7b3b-48e3-bdbe-5a3ccfb833db"` -> `UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db")` |=== +WARNING: To revert to the old `java.util.Locale` conversion behavior of version 5.12 and +earlier (which called the deprecated `Locale(String)` constructor), you can set the +`junit.jupiter.params.arguments.conversion.locale.format` +<> to `iso_639`. However, please +note that this parameter is deprecated and will be removed in a future release. + [[writing-tests-parameterized-tests-argument-conversion-implicit-fallback]] ====== Fallback String-to-Object Conversion @@ -2515,7 +2529,7 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=implicit_fallback_con [[writing-tests-parameterized-tests-argument-conversion-explicit]] ===== Explicit Conversion -Instead of relying on implicit argument conversion you may explicitly specify an +Instead of relying on implicit argument conversion, you may explicitly specify an `ArgumentConverter` to use for a certain parameter using the `@ConvertWith` annotation like in the following example. Note that an implementation of `ArgumentConverter` must be declared as either a top-level class or as a `static` nested class. @@ -2890,8 +2904,8 @@ or extensions between the execution of individual dynamic tests generated by the The following `DynamicTestsDemo` class demonstrates several examples of test factories and dynamic tests. -The first method returns an invalid return type. Since an invalid return type cannot be -detected at compile time, a `JUnitException` is thrown when it is detected at runtime. +The first method returns an invalid return type and will cause a warning to be reported by +JUnit during test discovery. Such methods are not executed. The next six methods demonstrate the generation of a `Collection`, `Iterable`, `Iterator`, array, or `Stream` of `DynamicTest` instances. Most of these examples do not really diff --git a/documentation/src/test/java/example/DynamicTestsDemo.java b/documentation/src/test/java/example/DynamicTestsDemo.java index 32388f62ed7b..c5643890b2d3 100644 --- a/documentation/src/test/java/example/DynamicTestsDemo.java +++ b/documentation/src/test/java/example/DynamicTestsDemo.java @@ -43,11 +43,12 @@ class DynamicTestsDemo { private final Calculator calculator = new Calculator(); + // This method will not be executed but produce a warning + @TestFactory // end::user_guide[] @Tag("exclude") + DynamicTest dummy() { return null; } // tag::user_guide[] - // This will result in a JUnitException! - @TestFactory List dynamicTestsWithInvalidReturnType() { return Arrays.asList("Hello"); } diff --git a/documentation/src/test/java/example/FirstCustomEngine.java b/documentation/src/test/java/example/FirstCustomEngine.java new file mode 100644 index 000000000000..efd9b14a7f0c --- /dev/null +++ b/documentation/src/test/java/example/FirstCustomEngine.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015-2025 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 example; + +//tag::user_guide[] +import static java.net.InetAddress.getLoopbackAddress; +import static org.junit.platform.engine.TestExecutionResult.successful; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.ServerSocket; + +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.EngineDescriptor; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; + +/** + * First custom test engine implementation. + */ +public class FirstCustomEngine implements TestEngine { + + public ServerSocket socket; + + @Override + public String getId() { + return "first-custom-test-engine"; + } + + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + return new EngineDescriptor(uniqueId, "First Custom Test Engine"); + } + + @Override + public void execute(ExecutionRequest request) { + request.getEngineExecutionListener() + // tag::custom_line_break[] + .executionStarted(request.getRootTestDescriptor()); + + NamespacedHierarchicalStore store = request.getStore(); + socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> { + try { + return new ServerSocket(0, 50, getLoopbackAddress()); + } + catch (IOException e) { + throw new UncheckedIOException("Failed to start ServerSocket", e); + } + }, ServerSocket.class); + + request.getEngineExecutionListener() + // tag::custom_line_break[] + .executionFinished(request.getRootTestDescriptor(), successful()); + } +} +//end::user_guide[] diff --git a/documentation/src/test/java/example/SecondCustomEngine.java b/documentation/src/test/java/example/SecondCustomEngine.java new file mode 100644 index 000000000000..3d11c13ac18d --- /dev/null +++ b/documentation/src/test/java/example/SecondCustomEngine.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015-2025 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 example; + +import static java.net.InetAddress.getLoopbackAddress; +import static org.junit.platform.engine.TestExecutionResult.successful; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.ServerSocket; + +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.EngineDescriptor; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; + +//tag::user_guide[] +/** + * Second custom test engine implementation. + */ +public class SecondCustomEngine implements TestEngine { + + public ServerSocket socket; + + @Override + public String getId() { + return "second-custom-test-engine"; + } + + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + return new EngineDescriptor(uniqueId, "Second Custom Test Engine"); + } + + @Override + public void execute(ExecutionRequest request) { + request.getEngineExecutionListener() + // tag::custom_line_break[] + .executionStarted(request.getRootTestDescriptor()); + + NamespacedHierarchicalStore store = request.getStore(); + socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> { + try { + return new ServerSocket(0, 50, getLoopbackAddress()); + } + catch (IOException e) { + throw new UncheckedIOException("Failed to start ServerSocket", e); + } + }, ServerSocket.class); + + request.getEngineExecutionListener() + // tag::custom_line_break[] + .executionFinished(request.getRootTestDescriptor(), successful()); + } +} +//end::user_guide[] diff --git a/documentation/src/test/java/example/extensions/HttpServerResource.java b/documentation/src/test/java/example/extensions/HttpServerResource.java index 845e88773fdc..24108f7a6484 100644 --- a/documentation/src/test/java/example/extensions/HttpServerResource.java +++ b/documentation/src/test/java/example/extensions/HttpServerResource.java @@ -19,13 +19,11 @@ import com.sun.net.httpserver.HttpServer; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; - /** - * Demonstrates an implementation of {@link CloseableResource} using an {@link HttpServer}. + * Demonstrates an implementation of {@link AutoCloseable} using an {@link HttpServer}. */ // tag::user_guide[] -class HttpServerResource implements CloseableResource { +class HttpServerResource implements AutoCloseable { private final HttpServer httpServer; diff --git a/documentation/src/test/java/example/session/CloseableHttpServer.java b/documentation/src/test/java/example/session/CloseableHttpServer.java new file mode 100644 index 000000000000..996fd85d8029 --- /dev/null +++ b/documentation/src/test/java/example/session/CloseableHttpServer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015-2025 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 example.session; + +//tag::user_guide[] +import java.util.concurrent.ExecutorService; + +import com.sun.net.httpserver.HttpServer; + +public class CloseableHttpServer implements AutoCloseable { + + private final HttpServer server; + private final ExecutorService executorService; + + CloseableHttpServer(HttpServer server, ExecutorService executorService) { + this.server = server; + this.executorService = executorService; + } + + public HttpServer getServer() { + return server; + } + + @Override + public void close() { // <1> + server.stop(0); // <2> + executorService.shutdownNow(); + } +} +//end::user_guide[] diff --git a/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java b/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java index 8db5232d5bcb..fdddad84ea4e 100644 --- a/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java +++ b/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java @@ -21,6 +21,8 @@ import com.sun.net.httpserver.HttpServer; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.LauncherSession; import org.junit.platform.launcher.LauncherSessionListener; import org.junit.platform.launcher.TestExecutionListener; @@ -28,8 +30,6 @@ public class GlobalSetupTeardownListener implements LauncherSessionListener { - private Fixture fixture; - @Override public void launcherSessionOpened(LauncherSession session) { // Avoid setup for test discovery by delaying it until tests are about to be executed @@ -42,50 +42,28 @@ public void testPlanExecutionStarted(TestPlan testPlan) { return; } //tag::user_guide[] - if (fixture == null) { - fixture = new Fixture(); - fixture.setUp(); - } - } - }); - } + NamespacedHierarchicalStore store = session.getStore(); // <1> + store.getOrComputeIfAbsent(Namespace.GLOBAL, "httpServer", key -> { // <2> + InetSocketAddress address = new InetSocketAddress(getLoopbackAddress(), 0); + HttpServer server; + try { + server = HttpServer.create(address, 0); + } + catch (IOException e) { + throw new UncheckedIOException("Failed to start HTTP server", e); + } + server.createContext("/test", exchange -> { + exchange.sendResponseHeaders(204, -1); + exchange.close(); + }); + ExecutorService executorService = Executors.newCachedThreadPool(); + server.setExecutor(executorService); + server.start(); // <3> - @Override - public void launcherSessionClosed(LauncherSession session) { - if (fixture != null) { - fixture.tearDown(); - fixture = null; - } - } - - static class Fixture { - - private HttpServer server; - private ExecutorService executorService; - - void setUp() { - try { - server = HttpServer.create(new InetSocketAddress(getLoopbackAddress(), 0), 0); + return new CloseableHttpServer(server, executorService); + }); } - catch (IOException e) { - throw new UncheckedIOException("Failed to start HTTP server", e); - } - server.createContext("/test", exchange -> { - exchange.sendResponseHeaders(204, -1); - exchange.close(); - }); - executorService = Executors.newCachedThreadPool(); - server.setExecutor(executorService); - server.start(); // <1> - int port = server.getAddress().getPort(); - System.setProperty("http.server.host", getLoopbackAddress().getHostAddress()); // <2> - System.setProperty("http.server.port", String.valueOf(port)); // <3> - } - - void tearDown() { - server.stop(0); // <4> - executorService.shutdownNow(); - } + }); } } diff --git a/documentation/src/test/java/example/session/HttpTests.java b/documentation/src/test/java/example/session/HttpTests.java index fdb560b66fa6..97a13439a3e4 100644 --- a/documentation/src/test/java/example/session/HttpTests.java +++ b/documentation/src/test/java/example/session/HttpTests.java @@ -13,25 +13,50 @@ //tag::user_guide[] import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.IOException; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; +import com.sun.net.httpserver.HttpServer; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; +@ExtendWith(HttpServerParameterResolver.class) class HttpTests { @Test - void respondsWith204() throws Exception { - String host = System.getProperty("http.server.host"); // <1> - String port = System.getProperty("http.server.port"); // <2> + void respondsWith204(HttpServer server) throws IOException { + String host = server.getAddress().getHostString(); // <2> + int port = server.getAddress().getPort(); // <3> URL url = URI.create("https://" + host + ":" + port + "/test").toURL(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); - int responseCode = connection.getResponseCode(); // <3> + int responseCode = connection.getResponseCode(); // <4> + + assertEquals(204, responseCode); // <5> + } +} + +class HttpServerParameterResolver implements ParameterResolver { + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return HttpServer.class.equals(parameterContext.getParameter().getType()); + } - assertEquals(204, responseCode); // <4> + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return extensionContext + // tag::custom_line_break[] + .getStore(ExtensionContext.Namespace.GLOBAL) + // tag::custom_line_break[] + .get("httpServer", CloseableHttpServer.class) // <1> + .getServer(); } } //end::user_guide[] diff --git a/documentation/src/test/java/example/sharedresources/SharedResourceDemo.java b/documentation/src/test/java/example/sharedresources/SharedResourceDemo.java new file mode 100644 index 000000000000..52b00c624471 --- /dev/null +++ b/documentation/src/test/java/example/sharedresources/SharedResourceDemo.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015-2025 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 example.sharedresources; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; + +import example.FirstCustomEngine; +import example.SecondCustomEngine; + +import org.junit.jupiter.api.Test; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.core.LauncherConfig; +import org.junit.platform.launcher.core.LauncherFactory; + +class SharedResourceDemo { + + //tag::user_guide[] + @Test + void runBothCustomEnginesTest() { + FirstCustomEngine firstCustomEngine = new FirstCustomEngine(); + SecondCustomEngine secondCustomEngine = new SecondCustomEngine(); + + Launcher launcher = LauncherFactory.create(LauncherConfig.builder() + // tag::custom_line_break[] + .addTestEngines(firstCustomEngine, secondCustomEngine) + // tag::custom_line_break[] + .enableTestEngineAutoRegistration(false) + // tag::custom_line_break[] + .build()); + + launcher.execute(request().build()); + + assertSame(firstCustomEngine.socket, secondCustomEngine.socket); + assertTrue(firstCustomEngine.socket.isClosed(), "socket should be closed"); + } + //end::user_guide[] +} diff --git a/gradle.properties b/gradle.properties index 250d65d1ba46..1921177d772d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,22 +1,21 @@ group = org.junit -version = 5.13.0-M2 +version = 5.13.0-M3 jupiterGroup = org.junit.jupiter platformGroup = org.junit.platform -platformVersion = 1.13.0-M2 +platformVersion = 1.13.0-M3 vintageGroup = org.junit.vintage -vintageVersion = 5.13.0-M2 +vintageVersion = 5.13.0-M3 # We need more metaspace due to apparent memory leak in Asciidoctor/JRuby org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError org.gradle.caching=true org.gradle.parallel=true org.gradle.configuration-cache.parallel=true -org.gradle.java.installations.fromEnv=JDK8,JDK18,JDK19,JDK20,JDK21,JDK22,JDK23,JDK24 +org.gradle.java.installations.fromEnv=GRAALVM_HOME,JDK8,JDK18,JDK19,JDK20,JDK21,JDK22,JDK23,JDK24 org.gradle.kotlin.dsl.allWarningsAsErrors=true -org.gradle.warning.mode=fail # Test Distribution develocity.internal.testdistribution.writeTraceFile=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 50df32541cac..95679e033664 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,10 +5,10 @@ asciidoctorj-pdf = "2.3.19" asciidoctor-plugins = "4.0.4" # Check if workaround in documentation.gradle.kts can be removed when upgrading assertj = "3.27.3" bnd = "7.1.0" -checkstyle = "10.21.4" +checkstyle = "10.23.1" eclipse = "4.35.0" -jackson = "2.18.3" -jacoco = "0.8.12" +jackson = "2.19.0" +jacoco = "0.8.13" jmh = "1.37" junit4 = "4.13.2" junit4Min = "4.12" @@ -16,9 +16,9 @@ ktlint = "1.5.0" log4j = "2.24.3" logback = "1.5.18" opentest4j = "1.3.0" -openTestReporting = "0.2.2" +openTestReporting = "0.2.3" snapshotTests = "1.11.0" -surefire = "3.5.2" +surefire = "3.5.3" xmlunit = "2.10.0" [libraries] @@ -34,7 +34,7 @@ assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } bndlib = { module = "biz.aQute.bnd:biz.aQute.bndlib", version.ref = "bnd" } checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" } classgraph = { module = "io.github.classgraph:classgraph", version = "4.8.179" } -commons-io = { module = "commons-io:commons-io", version = "2.18.0" } +commons-io = { module = "commons-io:commons-io", version = "2.19.0" } groovy4 = { module = "org.apache.groovy:groovy", version = "4.0.26" } groovy2-bom = { module = "org.codehaus.groovy:groovy-bom", version = "2.5.23" } hamcrest = { module = "org.hamcrest:hamcrest", version = "3.0" } @@ -46,15 +46,15 @@ jimfs = { module = "com.google.jimfs:jimfs", version = "1.3.0" } jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } joox = { module = "org.jooq:joox", version = "2.0.1" } -jte = { module = "gg.jte:jte", version = "3.1.16" } +jte = { module = "gg.jte:jte", version = "3.2.1" } junit4 = { module = "junit:junit", version = { require = "[4.12,)", prefer = "4.13.2" } } -kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.1" } +kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.2" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" } maven = { module = "org.apache.maven:apache-maven", version = "3.9.9" } mavenSurefirePlugin = { module = "org.apache.maven.plugins:maven-surefire-plugin", version.ref = "surefire" } memoryfilesystem = { module = "com.github.marschall:memoryfilesystem", version = "2.8.1" } -mockito-bom = { module = "org.mockito:mockito-bom", version = "5.16.1" } +mockito-bom = { module = "org.mockito:mockito-bom", version = "5.17.0" } mockito-core = { module = "org.mockito:mockito-core" } mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter" } nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version = "0.0.11" } @@ -63,7 +63,7 @@ openTestReporting-cli = { module = "org.opentest4j.reporting:open-test-reporting openTestReporting-events = { module = "org.opentest4j.reporting:open-test-reporting-events", version.ref = "openTestReporting" } openTestReporting-tooling-core = { module = "org.opentest4j.reporting:open-test-reporting-tooling-core", version.ref = "openTestReporting" } openTestReporting-tooling-spi = { module = "org.opentest4j.reporting:open-test-reporting-tooling-spi", version.ref = "openTestReporting" } -picocli = { module = "info.picocli:picocli", version = "4.7.6" } +picocli = { module = "info.picocli:picocli", version = "4.7.7" } slf4j-julBinding = { module = "org.slf4j:slf4j-jdk14", version = "2.0.17" } snapshotTests-junit5 = { module = "de.skuzzle.test:snapshot-tests-junit5", version.ref = "snapshotTests" } snapshotTests-xml = { module = "de.skuzzle.test:snapshot-tests-xml", version.ref = "snapshotTests" } @@ -93,15 +93,15 @@ asciidoctorConvert = { id = "org.asciidoctor.jvm.convert", version.ref = "asciid asciidoctorPdf = { id = "org.asciidoctor.jvm.pdf", version.ref = "asciidoctor-plugins" } bnd = { id = "biz.aQute.bnd", version.ref = "bnd" } buildParameters = { id = "org.gradlex.build-parameters", version = "1.4.4" } -commonCustomUserData = { id = "com.gradle.common-custom-user-data-gradle-plugin", version = "2.1" } -develocity = { id = "com.gradle.develocity", version = "3.19.2" } -foojayResolver = { id = "org.gradle.toolchains.foojay-resolver", version = "0.9.0" } +commonCustomUserData = { id = "com.gradle.common-custom-user-data-gradle-plugin", version = "2.2.1" } +develocity = { id = "com.gradle.develocity", version = "4.0.1" } +foojayResolver = { id = "org.gradle.toolchains.foojay-resolver", version = "0.10.0" } gitPublish = { id = "org.ajoberstar.git-publish", version = "5.1.1" } jmh = { id = "me.champeau.jmh", version = "0.7.3" } # check if workaround in gradle.properties can be removed when updating kotlin = { id = "org.jetbrains.kotlin.jvm", version = "2.1.20" } nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } -plantuml = { id = "io.freefair.plantuml", version = "8.13" } +plantuml = { id = "io.freefair.plantuml", version = "8.13.1" } shadow = { id = "com.gradleup.shadow", version = "8.3.6" } spotless = { id = "com.diffplug.spotless", version = "6.25.0" } versions = { id = "com.github.ben-manes.versions", version = "0.52.0" } diff --git a/gradle/plugins/build-parameters/build.gradle.kts b/gradle/plugins/build-parameters/build.gradle.kts index 5d647a81a7de..8a1276886d14 100644 --- a/gradle/plugins/build-parameters/build.gradle.kts +++ b/gradle/plugins/build-parameters/build.gradle.kts @@ -65,6 +65,10 @@ buildParameters { } group("testing") { description = "Testing related parameters" + bool("dryRun") { + description = "Enables dry run mode for tests" + defaultValue = false + } bool("enableJaCoCo") { description = "Enables JaCoCo test coverage reporting" defaultValue = true diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.native-image-properties.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.native-image-properties.gradle.kts deleted file mode 100644 index 262535f2083a..000000000000 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.native-image-properties.gradle.kts +++ /dev/null @@ -1,55 +0,0 @@ -import junitbuild.graalvm.NativeImagePropertiesExtension -import java.util.zip.ZipFile - -plugins { - `java-library` -} - -val extension = extensions.create("nativeImageProperties").apply { - val resourceFile: RegularFile = layout.projectDirectory.file("src/nativeImage/initialize-at-build-time") - if (resourceFile.asFile.exists()) { - initializeAtBuildTime.convention(providers.fileContents(resourceFile).asText.map { it.trim().lines() }) - } else { - initializeAtBuildTime.empty() - } - initializeAtBuildTime.finalizeValueOnRead() -} - -val outputDir = layout.buildDirectory.dir("resources/nativeImage") - -val propertyFileTask = tasks.register("nativeImageProperties") { - destinationFile = outputDir.map { it.file("META-INF/native-image/${project.group}/${project.name}/native-image.properties") } - // see https://www.graalvm.org/latest/reference-manual/native-image/overview/BuildConfiguration/#configuration-file-format - property("Args", extension.initializeAtBuildTime.map { - if (it.isEmpty()) { - "" - } else { - "--initialize-at-build-time=${it.joinToString(",")}" - } - }) -} - -val validationTask = tasks.register("validateNativeImageProperties") { - dependsOn(tasks.jar) - doLast { - val zipEntries = ZipFile(tasks.jar.get().archiveFile.get().asFile).use { zipFile -> - zipFile.entries().asSequence().map { it.name }.filter { it.endsWith(".class") }.toSet() - } - val missingClasses = extension.initializeAtBuildTime.get().filter { className -> - !zipEntries.contains("${className.replace('.', '/')}.class") - } - if (missingClasses.isNotEmpty()) { - throw GradleException("The following classes were specified as initialize-at-build-time but do not exist (you should probably remove them from nativeImageProperties.initializeAtBuildTime):\n${missingClasses.joinToString("\n- ", "- ")}") - } - } -} - -tasks.check { - dependsOn(validationTask) -} - -sourceSets { - main { - output.dir(propertyFileTask.map { outputDir }) - } -} diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts index 696548cf8e86..b5f63a5906a5 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.testing-conventions.gradle.kts @@ -114,6 +114,7 @@ tasks.withType().configureEach { "-XX:FlightRecorderOptions=stackdepth=1024" ) } + systemProperty("junit.platform.execution.dryRun.enabled", buildParameters.testing.dryRun) // Track OS as input so that tests are executed on all configured operating systems on CI trackOperationSystemAsInput() @@ -132,10 +133,12 @@ tasks.withType().configureEach { } systemProperty("junit.platform.output.capture.stdout", "true") systemProperty("junit.platform.output.capture.stderr", "true") + systemProperty("junit.platform.discovery.issue.severity.critical", "info") jvmArgumentProviders += objects.newInstance(JavaAgentArgumentProvider::class).apply { classpath.from(javaAgentClasspath) } + jvmArgs("-Xshare:off") // https://github.com/mockito/mockito/issues/3111 val reportDirTree = objects.fileTree().from(reports.junitXml.outputLocation) doFirst { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975c742b..1b33c55baabb 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 36e4933e1da7..247cf2a9f5ce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index faf93008b77e..23d15a936707 100755 --- a/gradlew +++ b/gradlew @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21834d5..db3a6ac207e5 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/junit-jupiter-api/junit-jupiter-api.gradle.kts b/junit-jupiter-api/junit-jupiter-api.gradle.kts index 378fc86a3d5d..402b5323eb57 100644 --- a/junit-jupiter-api/junit-jupiter-api.gradle.kts +++ b/junit-jupiter-api/junit-jupiter-api.gradle.kts @@ -1,7 +1,6 @@ plugins { id("junitbuild.kotlin-library-conventions") id("junitbuild.code-generator") - id("junitbuild.native-image-properties") `java-test-fixtures` } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java index 7ef4db7ed9b9..d7714236f56e 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java @@ -29,12 +29,9 @@ import java.util.function.Predicate; import org.apiguardian.api.API; -import org.junit.platform.commons.logging.Logger; -import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.commons.util.Preconditions; -import org.junit.platform.commons.util.StringUtils; /** * {@code DisplayNameGenerator} defines the SPI for generating display names @@ -350,8 +347,6 @@ class IndicativeSentences implements DisplayNameGenerator { static final DisplayNameGenerator INSTANCE = new IndicativeSentences(); - private static final Logger logger = LoggerFactory.getLogger(IndicativeSentences.class); - private static final Predicate> notIndicativeSentences = clazz -> clazz != IndicativeSentences.class; public IndicativeSentences() { @@ -502,22 +497,14 @@ private static Optional findIndicativeSentencesGe } private static String getSentenceFragment(AnnotatedElement element) { - Optional annotation = findAnnotation(element, SentenceFragment.class); - if (annotation.isPresent()) { - String sentenceFragment = annotation.get().value().trim(); - - // TODO [#242] Replace logging with precondition check once we have a proper mechanism for - // handling validation exceptions during the TestEngine discovery phase. - if (StringUtils.isBlank(sentenceFragment)) { - logger.warn(() -> String.format( - "Configuration error: @SentenceFragment on [%s] must be declared with a non-blank value.", - element)); - } - else { - return sentenceFragment; - } - } - return null; + return findAnnotation(element, SentenceFragment.class) // + .map(SentenceFragment::value) // + .map(sentenceFragment -> { + Preconditions.notBlank(sentenceFragment, String.format( + "@SentenceFragment on [%s] must be declared with a non-blank value.", element)); + return sentenceFragment.trim(); + }) // + .orElse(null); } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationContextProvider.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationContextProvider.java index 51a2edada45b..0af499b57f14 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationContextProvider.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationContextProvider.java @@ -90,6 +90,8 @@ public interface ClassTemplateInvocationContextProvider extends Extension { * invoked; never {@code null} * @return a {@code Stream} of {@code ClassTemplateInvocationContext} * instances for the invocation of the class template; never {@code null} + * @throws TemplateInvocationValidationException if a validation fails when + * while providing or closing the {@link java.util.stream.Stream}. * @see #supportsClassTemplate * @see ExtensionContext */ diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java index 6d047292a98d..ae6daf29e2c3 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java @@ -10,7 +10,9 @@ package org.junit.jupiter.api.extension; +import static java.util.Collections.unmodifiableList; 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.lang.reflect.AnnotatedElement; @@ -442,9 +444,28 @@ default void publishReportEntry(String value) { * @return the store in which to put and get objects for other invocations * working in the same namespace; never {@code null} * @see Namespace#GLOBAL + * @see #getStore(StoreScope, Namespace) */ Store getStore(Namespace namespace); + /** + * Returns the store for supplied scope and namespace. + * + *

If {@code scope} is + * {@link StoreScope#EXTENSION_CONTEXT EXTENSION_CONTEXT}, the store behaves + * exactly like the one returned by {@link #getStore(Namespace)}. If the + * {@code scope} is {@link StoreScope#LAUNCHER_SESSION LAUNCHER_SESSION} or + * {@link StoreScope#EXECUTION_REQUEST EXECUTION_REQUEST}, all stored values + * that are instances of {@link AutoCloseable} are notified by invoking + * their {@code close()} methods when the scope is closed. + * + * @since 5.13 + * @see StoreScope + * @see #getStore(Namespace) + */ + @API(status = EXPERIMENTAL, since = "5.13") + Store getStore(StoreScope scope, Namespace namespace); + /** * Get the {@link ExecutionMode} associated with the current test or container. * @@ -482,7 +503,9 @@ interface Store { * inverse order they were added in. * * @since 5.1 + * @deprecated Please extend {@code AutoCloseable} directly. */ + @Deprecated @API(status = STABLE, since = "5.1") interface CloseableResource { @@ -574,9 +597,11 @@ default V getOrDefault(Object key, Class requiredType, V defaultValue) { *

See {@link #getOrComputeIfAbsent(Object, Function, Class)} for * further details. * - *

If {@code type} implements {@link ExtensionContext.Store.CloseableResource} - * the {@code close()} method will be invoked on the stored object when - * the store is closed. + *

If {@code type} implements {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. * * @param type the type of object to retrieve; never {@code null} * @param the key and value type @@ -585,6 +610,7 @@ default V getOrDefault(Object key, Class requiredType, V defaultValue) { * @see #getOrComputeIfAbsent(Object, Function) * @see #getOrComputeIfAbsent(Object, Function, Class) * @see CloseableResource + * @see AutoCloseable */ @API(status = STABLE, since = "5.1") default V getOrComputeIfAbsent(Class type) { @@ -604,9 +630,11 @@ default V getOrComputeIfAbsent(Class type) { *

For greater type safety, consider using * {@link #getOrComputeIfAbsent(Object, Function, Class)} instead. * - *

If the created value is an instance of {@link ExtensionContext.Store.CloseableResource} - * the {@code close()} method will be invoked on the stored object when - * the store is closed. + *

If the created value is an instance of {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. * * @param key the key; never {@code null} * @param defaultCreator the function called with the supplied {@code key} @@ -617,6 +645,7 @@ default V getOrComputeIfAbsent(Class type) { * @see #getOrComputeIfAbsent(Class) * @see #getOrComputeIfAbsent(Object, Function, Class) * @see CloseableResource + * @see AutoCloseable */ Object getOrComputeIfAbsent(K key, Function defaultCreator); @@ -631,9 +660,11 @@ default V getOrComputeIfAbsent(Class type) { * a new value will be computed by the {@code defaultCreator} (given * the {@code key} as input), stored, and returned. * - *

If {@code requiredType} implements {@link ExtensionContext.Store.CloseableResource} - * the {@code close()} method will be invoked on the stored object when - * the store is closed. + *

If {@code requiredType} implements {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. * * @param key the key; never {@code null} * @param defaultCreator the function called with the supplied {@code key} @@ -645,6 +676,7 @@ default V getOrComputeIfAbsent(Class type) { * @see #getOrComputeIfAbsent(Class) * @see #getOrComputeIfAbsent(Object, Function) * @see CloseableResource + * @see AutoCloseable */ V getOrComputeIfAbsent(K key, Function defaultCreator, Class requiredType); @@ -655,14 +687,17 @@ default V getOrComputeIfAbsent(Class type) { * ExtensionContexts} for the store's {@code Namespace} unless they * overwrite it. * - *

If the {@code value} is an instance of {@link ExtensionContext.Store.CloseableResource} - * the {@code close()} method will be invoked on the stored object when - * the store is closed. + *

If the {@code value} is an instance of {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. * * @param key the key under which the value should be stored; never * {@code null} * @param value the value to store; may be {@code null} * @see CloseableResource + * @see AutoCloseable */ void put(Object key, Object value); @@ -670,8 +705,8 @@ default V getOrComputeIfAbsent(Class type) { * Remove the value that was previously stored under the supplied {@code key}. * *

The value will only be removed in the current {@link ExtensionContext}, - * not in ancestors. In addition, the {@link CloseableResource} API will not - * be honored for values that are manually removed via this method. + * not in ancestors. In addition, the {@link CloseableResource} and {@link AutoCloseable} + * API will not be honored for values that are manually removed via this method. * *

For greater type safety, consider using {@link #remove(Object, Class)} * instead. @@ -688,8 +723,8 @@ default V getOrComputeIfAbsent(Class type) { * under the supplied {@code key}. * *

The value will only be removed in the current {@link ExtensionContext}, - * not in ancestors. In addition, the {@link CloseableResource} API will not - * be honored for values that are manually removed via this method. + * not in ancestors. In addition, the {@link CloseableResource} and {@link AutoCloseable} + * API will not be honored for values that are manually removed via this method. * * @param key the key; never {@code null} * @param requiredType the required type of the value; never {@code null} @@ -771,6 +806,57 @@ public Namespace append(Object... parts) { Collections.addAll(newParts, parts); return new Namespace(newParts); } + + @API(status = INTERNAL, since = "5.13") + public List getParts() { + return unmodifiableList(parts); + } + } + + /** + * {@code StoreScope} is an enumeration of the different scopes for + * {@link Store} instances. + * + * @since 5.13 + * @see #getStore(StoreScope, Namespace) + */ + @API(status = EXPERIMENTAL, since = "5.13") + enum StoreScope { + + /** + * The store is scoped to the current {@code LauncherSession}. + * + *

Any data that is stored in a {@code Store} with this scope will be + * available throughout the entire launcher session. Therefore, it may + * be used to inject values from registered + * {@code LauncherSessionListener} implementations, to share data across + * multiple executions of the Jupiter engine within the same session, or + * even to share data across multiple engines. + * + * @see org.junit.platform.launcher.LauncherSession#getStore() + * @see org.junit.platform.launcher.LauncherSessionListener + */ + LAUNCHER_SESSION, + + /** + * The store is scoped to the current {@code ExecutionRequest} of the + * JUnit Platform {@code Launcher}. + * + *

Any data that is stored in a {@code Store} with this scope will be + * available for the duration of the current execution request. + * Therefore, it may be used to share data across multiple engines. + * + * @see org.junit.platform.engine.ExecutionRequest#getStore() + */ + EXECUTION_REQUEST, + + /** + * The store is scoped to the current {@code ExtensionContext}. + * + *

Any data that is stored in a {@code Store} with this scope will be + * bound to the current extension context lifecycle. + */ + EXTENSION_CONTEXT } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TemplateInvocationValidationException.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TemplateInvocationValidationException.java new file mode 100644 index 000000000000..94cad7ab8677 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TemplateInvocationValidationException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2025 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.extension; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; + +/** + * {@code TemplateInvocationValidationException} is an exception thrown by a + * {@link TestTemplateInvocationContextProvider} or + * {@link ClassTemplateInvocationContextProvider} if a validation fails when + * while providing or closing {@link java.util.stream.Stream} of invocation + * contexts. + * + * @since 5.13 + */ +@API(status = EXPERIMENTAL, since = "5.13") +public class TemplateInvocationValidationException extends JUnitException { + + private static final long serialVersionUID = 1L; + + public TemplateInvocationValidationException(String message) { + super(message); + } +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstantiationAwareExtension.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstantiationAwareExtension.java index 81bae81f853c..7f4847304c06 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstantiationAwareExtension.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstantiationAwareExtension.java @@ -16,7 +16,6 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtensionContext.Store; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; /** * Interface for {@link Extension Extensions} that are aware and can influence @@ -65,9 +64,11 @@ public interface TestInstantiationAwareExtension extends Extension { *

  • {@link ExtensionContext#getTestMethod() getTestMethod()} is no longer * empty, unless the {@link TestInstance.Lifecycle#PER_CLASS PER_CLASS} * lifecycle is used.
  • - *
  • If the callback adds a new {@link CloseableResource} to the - * {@link Store Store}, the resource is closed just after the instance is - * destroyed.
  • + *
  • If the callback adds a new {@link Store.CloseableResource} or + * {@link AutoCloseable} to the {@link Store Store} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then + * the resource is closed just after the instance is destroyed.
  • *
  • The callbacks can now access data previously stored by * {@link TestTemplateInvocationContext}, unless the * {@link TestInstance.Lifecycle#PER_CLASS PER_CLASS} lifecycle is used.
  • diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.java index 31076236ecae..f4a64479cde9 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.java @@ -86,6 +86,8 @@ public interface TestTemplateInvocationContextProvider extends Extension { * to be invoked; never {@code null} * @return a {@code Stream} of {@code TestTemplateInvocationContext} * instances for the invocation of the test template method; never {@code null} + * @throws TemplateInvocationValidationException if a validation fails when + * while providing or closing the {@link java.util.stream.Stream}. * @see #supportsTestTemplate * @see ExtensionContext */ diff --git a/junit-jupiter-api/src/nativeImage/initialize-at-build-time b/junit-jupiter-api/src/nativeImage/initialize-at-build-time deleted file mode 100644 index b8fb5c3d7514..000000000000 --- a/junit-jupiter-api/src/nativeImage/initialize-at-build-time +++ /dev/null @@ -1,4 +0,0 @@ -org.junit.jupiter.api.DisplayNameGenerator$Standard -org.junit.jupiter.api.TestInstance$Lifecycle -org.junit.jupiter.api.condition.OS -org.junit.jupiter.api.extension.ConditionEvaluationResult diff --git a/junit-jupiter-engine/junit-jupiter-engine.gradle.kts b/junit-jupiter-engine/junit-jupiter-engine.gradle.kts index 819993462c0e..04d86e5f0da7 100644 --- a/junit-jupiter-engine/junit-jupiter-engine.gradle.kts +++ b/junit-jupiter-engine/junit-jupiter-engine.gradle.kts @@ -1,6 +1,5 @@ plugins { id("junitbuild.kotlin-library-conventions") - id("junitbuild.native-image-properties") `java-test-fixtures` } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java index 51ca2c102ca2..321a707be394 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java @@ -210,6 +210,16 @@ public final class Constants { @API(status = STABLE, since = "5.10") public static final String PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME = JupiterConfiguration.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME; + /** + * Property name used to enable auto-closing of {@link AutoCloseable} instances + * + *

    By default, auto-closing is enabled. + * + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + public static final String CLOSING_STORED_AUTO_CLOSEABLE_ENABLED_PROPERTY_NAME = JupiterConfiguration.CLOSING_STORED_AUTO_CLOSEABLE_ENABLED_PROPERTY_NAME; + /** * Property name used to set the default test execution mode: {@value} * diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java index 0de9bbb308ec..c3878b9c84b7 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java @@ -19,6 +19,7 @@ import org.junit.jupiter.engine.config.DefaultJupiterConfiguration; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; +import org.junit.jupiter.engine.descriptor.LauncherStoreFacade; import org.junit.jupiter.engine.discovery.DiscoverySelectorResolver; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; import org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory; @@ -82,8 +83,8 @@ protected HierarchicalTestExecutorService createExecutorService(ExecutionRequest @Override protected JupiterEngineExecutionContext createExecutionContext(ExecutionRequest request) { - return new JupiterEngineExecutionContext(request.getEngineExecutionListener(), - getJupiterConfiguration(request)); + return new JupiterEngineExecutionContext(request.getEngineExecutionListener(), getJupiterConfiguration(request), + new LauncherStoreFacade(request.getStore())); } /** diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java index 170a8c2be817..ecab219838f3 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java @@ -69,6 +69,12 @@ public boolean isParallelExecutionEnabled() { __ -> delegate.isParallelExecutionEnabled()); } + @Override + public boolean isClosingStoredAutoCloseablesEnabled() { + return (boolean) cache.computeIfAbsent(CLOSING_STORED_AUTO_CLOSEABLE_ENABLED_PROPERTY_NAME, + __ -> delegate.isClosingStoredAutoCloseablesEnabled()); + } + @Override public boolean isExtensionAutoDetectionEnabled() { return (boolean) cache.computeIfAbsent(EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME, diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java index 7f24180acea7..c6ab8b0d5508 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java @@ -112,6 +112,11 @@ public boolean isParallelExecutionEnabled() { return configurationParameters.getBoolean(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME).orElse(false); } + @Override + public boolean isClosingStoredAutoCloseablesEnabled() { + return configurationParameters.getBoolean(CLOSING_STORED_AUTO_CLOSEABLE_ENABLED_PROPERTY_NAME).orElse(true); + } + @Override public boolean isExtensionAutoDetectionEnabled() { return configurationParameters.getBoolean(EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME).orElse(false); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java index c9b2781ea73e..ca4f8ff76a7d 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java @@ -42,6 +42,7 @@ public interface JupiterConfiguration { String EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.exclude"; String DEACTIVATE_CONDITIONS_PATTERN_PROPERTY_NAME = "junit.jupiter.conditions.deactivate"; String PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME = "junit.jupiter.execution.parallel.enabled"; + String CLOSING_STORED_AUTO_CLOSEABLE_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.store.close.autocloseable.enabled"; String DEFAULT_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_EXECUTION_MODE_PROPERTY_NAME; String DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME; String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.enabled"; @@ -49,7 +50,7 @@ public interface JupiterConfiguration { String DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME = TestInstance.Lifecycle.DEFAULT_LIFECYCLE_PROPERTY_NAME; String DEFAULT_DISPLAY_NAME_GENERATOR_PROPERTY_NAME = DisplayNameGenerator.DEFAULT_GENERATOR_PROPERTY_NAME; String DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME = MethodOrderer.DEFAULT_ORDER_PROPERTY_NAME; - String DEFAULT_TEST_CLASS_ORDER_PROPERTY_NAME = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME;; + String DEFAULT_TEST_CLASS_ORDER_PROPERTY_NAME = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME; String DEFAULT_TEST_INSTANTIATION_EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME = ExtensionContextScope.DEFAULT_SCOPE_PROPERTY_NAME; Predicate> getFilterForAutoDetectedExtensions(); @@ -60,6 +61,8 @@ public interface JupiterConfiguration { boolean isParallelExecutionEnabled(); + boolean isClosingStoredAutoCloseablesEnabled(); + boolean isExtensionAutoDetectionEnabled(); boolean isThreadDumpOnTimeoutEnabled(); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java index bf7cb5d9a059..219acbb474c7 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java @@ -27,16 +27,16 @@ import org.junit.jupiter.api.extension.ExecutableInvoker; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.MediaType; import org.junit.jupiter.api.function.ThrowingConsumer; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.DefaultExecutableInvoker; -import org.junit.jupiter.engine.execution.NamespaceAwareStore; import org.junit.jupiter.engine.extension.ExtensionContextInternal; import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.logging.Logger; +import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.UnrecoverableExceptions; import org.junit.platform.engine.EngineExecutionListener; @@ -52,23 +52,21 @@ */ abstract class AbstractExtensionContext implements ExtensionContextInternal, AutoCloseable { - private static final NamespacedHierarchicalStore.CloseAction CLOSE_RESOURCES = (__, ___, value) -> { - if (value instanceof CloseableResource) { - ((CloseableResource) value).close(); - } - }; + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExtensionContext.class); private final ExtensionContext parent; private final EngineExecutionListener engineExecutionListener; private final T testDescriptor; private final Set tags; private final JupiterConfiguration configuration; - private final NamespacedHierarchicalStore valuesStore; private final ExecutableInvoker executableInvoker; private final ExtensionRegistry extensionRegistry; + private final LauncherStoreFacade launcherStoreFacade; + private final NamespacedHierarchicalStore valuesStore; AbstractExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, T testDescriptor, - JupiterConfiguration configuration, ExtensionRegistry extensionRegistry) { + JupiterConfiguration configuration, ExtensionRegistry extensionRegistry, + LauncherStoreFacade launcherStoreFacade) { Preconditions.notNull(testDescriptor, "TestDescriptor must not be null"); Preconditions.notNull(configuration, "JupiterConfiguration must not be null"); @@ -78,22 +76,49 @@ abstract class AbstractExtensionContext implements Ext this.engineExecutionListener = engineExecutionListener; this.testDescriptor = testDescriptor; this.configuration = configuration; - this.valuesStore = createStore(parent); this.extensionRegistry = extensionRegistry; + this.launcherStoreFacade = launcherStoreFacade; // @formatter:off this.tags = testDescriptor.getTags().stream() .map(TestTag::getName) .collect(collectingAndThen(toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); // @formatter:on + + this.valuesStore = createStore(parent, launcherStoreFacade, createCloseAction()); + } + + @SuppressWarnings("deprecation") + private NamespacedHierarchicalStore.CloseAction createCloseAction() { + return (__, ___, value) -> { + boolean isAutoCloseEnabled = this.configuration.isClosingStoredAutoCloseablesEnabled(); + + if (value instanceof AutoCloseable && isAutoCloseEnabled) { + ((AutoCloseable) value).close(); + return; + } + + if (value instanceof Store.CloseableResource) { + if (isAutoCloseEnabled) { + LOGGER.warn( + () -> "Type implements CloseableResource but not AutoCloseable: " + value.getClass().getName()); + } + ((Store.CloseableResource) value).close(); + } + }; } - private static NamespacedHierarchicalStore createStore(ExtensionContext parent) { - NamespacedHierarchicalStore parentStore = null; - if (parent != null) { + private static NamespacedHierarchicalStore createStore( + ExtensionContext parent, LauncherStoreFacade launcherStoreFacade, + NamespacedHierarchicalStore.CloseAction closeAction) { + NamespacedHierarchicalStore parentStore; + if (parent == null) { + parentStore = launcherStoreFacade.getRequestLevelStore(); + } + else { parentStore = ((AbstractExtensionContext) parent).valuesStore; } - return new NamespacedHierarchicalStore<>(parentStore, CLOSE_RESOURCES); + return new NamespacedHierarchicalStore<>(parentStore, closeAction); } @Override @@ -188,8 +213,21 @@ protected T getTestDescriptor() { @Override public Store getStore(Namespace namespace) { - Preconditions.notNull(namespace, "Namespace must not be null"); - return new NamespaceAwareStore(this.valuesStore, namespace); + return launcherStoreFacade.getStoreAdapter(this.valuesStore, namespace); + } + + @Override + public Store getStore(StoreScope scope, Namespace namespace) { + // TODO [#4246] Use switch expression + switch (scope) { + case LAUNCHER_SESSION: + return launcherStoreFacade.getSessionLevelStore(namespace); + case EXECUTION_REQUEST: + return launcherStoreFacade.getRequestLevelStore(namespace); + case EXTENSION_CONTEXT: + return getStore(namespace); + } + throw new JUnitException("Unknown StoreScope: " + scope); } @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java index eb4a5141c667..d6837494403e 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java @@ -136,18 +136,29 @@ public final String getLegacyReportingName() { public final void validate(DiscoveryIssueReporter reporter) { validateCoreLifecycleMethods(reporter); validateClassTemplateInvocationLifecycleMethods(reporter); + validateTags(reporter); + validateDisplayNameAnnotation(reporter); + } + + private void validateDisplayNameAnnotation(DiscoveryIssueReporter reporter) { + DisplayNameUtils.validateAnnotation(getTestClass(), // + () -> String.format("class '%s'", getTestClass().getName()), // + () -> getSource().orElse(null), // + reporter); } protected void validateCoreLifecycleMethods(DiscoveryIssueReporter reporter) { - List discoveryIssues = this.lifecycleMethods.discoveryIssues; - discoveryIssues.forEach(reporter::reportIssue); - discoveryIssues.clear(); + Validatable.reportAndClear(this.lifecycleMethods.discoveryIssues, reporter); } protected void validateClassTemplateInvocationLifecycleMethods(DiscoveryIssueReporter reporter) { LifecycleMethodUtils.validateNoClassTemplateInvocationLifecycleMethodsAreDeclared(getTestClass(), reporter); } + private void validateTags(DiscoveryIssueReporter reporter) { + Validatable.reportAndClear(this.classInfo.discoveryIssues, reporter); + } + // --- Node ---------------------------------------------------------------- @Override @@ -201,7 +212,7 @@ public final JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext ThrowableCollector throwableCollector = createThrowableCollector(); ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(), context.getExecutionListener(), this, this.classInfo.lifecycle, context.getConfiguration(), registry, - throwableCollector); + context.getLauncherStoreFacade(), throwableCollector); // @formatter:off return context.extend() @@ -539,6 +550,8 @@ private void invokeMethodInExtensionContext(Method method, ExtensionContext cont protected static class ClassInfo { + private final List discoveryIssues = new ArrayList<>(); + final Class testClass; final Set tags; final Lifecycle lifecycle; @@ -547,7 +560,10 @@ protected static class ClassInfo { ClassInfo(Class testClass, JupiterConfiguration configuration) { this.testClass = testClass; - this.tags = getTags(testClass); + this.tags = getTags(testClass, // + () -> String.format("class '%s'", testClass.getName()), // + () -> ClassSource.from(testClass), // + discoveryIssues::add); this.lifecycle = getTestInstanceLifecycle(testClass, configuration); this.defaultChildExecutionMode = (this.lifecycle == Lifecycle.PER_CLASS ? ExecutionMode.SAME_THREAD : null); this.exclusiveResourceCollector = ExclusiveResourceCollector.from(testClass); @@ -566,10 +582,11 @@ private static class LifecycleMethods { LifecycleMethods(ClassInfo classInfo) { Class testClass = classInfo.testClass; boolean requireStatic = classInfo.lifecycle == Lifecycle.PER_METHOD; - this.beforeAll = findBeforeAllMethods(testClass, requireStatic, discoveryIssues::add); - this.afterAll = findAfterAllMethods(testClass, requireStatic, discoveryIssues::add); - this.beforeEach = findBeforeEachMethods(testClass, discoveryIssues::add); - this.afterEach = findAfterEachMethods(testClass, discoveryIssues::add); + DiscoveryIssueReporter issueReporter = DiscoveryIssueReporter.collecting(discoveryIssues); + this.beforeAll = findBeforeAllMethods(testClass, requireStatic, issueReporter); + this.afterAll = findAfterAllMethods(testClass, requireStatic, issueReporter); + this.beforeEach = findBeforeEachMethods(testClass, issueReporter); + this.afterEach = findAfterEachMethods(testClass, issueReporter); } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java index 9350bfe4ea2d..cc052d0a60f5 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java @@ -37,9 +37,10 @@ final class ClassExtensionContext extends AbstractExtensionContext this.invocationContext.prepareInvocation(extensionContext)); return context.extend() // diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateTestDescriptor.java index b3be1dfa5df1..77b540c6e2bb 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateTestDescriptor.java @@ -51,7 +51,7 @@ @API(status = INTERNAL, since = "5.13") public class ClassTemplateTestDescriptor extends ClassBasedTestDescriptor implements Filterable { - public static final String STATIC_CLASS_SEGMENT_TYPE = "class-template"; + public static final String STANDALONE_CLASS_SEGMENT_TYPE = "class-template"; public static final String NESTED_CLASS_SEGMENT_TYPE = "nested-class-template"; private final Map> childrenPrototypesByIndex = new HashMap<>(); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DisplayNameUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DisplayNameUtils.java index b61ba1586de1..619bbf81a60c 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DisplayNameUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DisplayNameUtils.java @@ -30,11 +30,13 @@ import org.junit.jupiter.api.DisplayNameGenerator.Simple; import org.junit.jupiter.api.DisplayNameGenerator.Standard; import org.junit.jupiter.engine.config.JupiterConfiguration; -import org.junit.platform.commons.logging.Logger; -import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.StringUtils; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; /** * Collection of utilities for working with display names. @@ -46,8 +48,6 @@ */ final class DisplayNameUtils { - private static final Logger logger = LoggerFactory.getLogger(DisplayNameUtils.class); - /** * Pre-defined standard display name generator instance. */ @@ -74,22 +74,24 @@ final class DisplayNameUtils { static String determineDisplayName(AnnotatedElement element, Supplier displayNameSupplier) { Preconditions.notNull(element, "Annotated element must not be null"); - Optional displayNameAnnotation = findAnnotation(element, DisplayName.class); - if (displayNameAnnotation.isPresent()) { - String displayName = displayNameAnnotation.get().value().trim(); - - // TODO [#242] Replace logging with precondition check once we have a proper mechanism for - // handling validation exceptions during the TestEngine discovery phase. - if (StringUtils.isBlank(displayName)) { - logger.warn(() -> String.format( - "Configuration error: @DisplayName on [%s] must be declared with a non-blank value.", element)); - } - else { - return displayName; - } - } - // else let a 'DisplayNameGenerator' generate a display name - return displayNameSupplier.get(); + return findAnnotation(element, DisplayName.class) // + .map(DisplayName::value) // + .filter(StringUtils::isNotBlank) // + .map(String::trim) // + .orElseGet(displayNameSupplier); + } + + static void validateAnnotation(AnnotatedElement element, Supplier elementDescription, + Supplier sourceProvider, DiscoveryIssueReporter reporter) { + findAnnotation(element, DisplayName.class) // + .map(DisplayName::value) // + .filter(StringUtils::isBlank) // + .ifPresent(__ -> { + String message = String.format("@DisplayName on %s must be declared with a non-blank value.", + elementDescription.get()); + reporter.reportIssue( + DiscoveryIssue.builder(Severity.WARNING, message).source(sourceProvider.get()).build()); + }); } static String determineDisplayNameForMethod(Supplier>> enclosingInstanceTypes, Class testClass, diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java index fc87ef3f3a68..8f5a0ad0f911 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java @@ -29,8 +29,8 @@ class DynamicExtensionContext extends AbstractExtensionContext { - private static final Logger logger = LoggerFactory.getLogger(JupiterTestDescriptor.class); - private static final ConditionEvaluator conditionEvaluator = new ConditionEvaluator(); final JupiterConfiguration configuration; @@ -77,27 +77,27 @@ public abstract class JupiterTestDescriptor extends AbstractTestDescriptor // --- TestDescriptor ------------------------------------------------------ - static Set getTags(AnnotatedElement element) { - // @formatter:off - return findRepeatableAnnotations(element, Tag.class).stream() - .map(Tag::value) + static Set getTags(AnnotatedElement element, Supplier elementDescription, + Supplier sourceProvider, Consumer issueCollector) { + AtomicReference source = new AtomicReference<>(); + return findRepeatableAnnotations(element, Tag.class).stream() // + .map(Tag::value) // .filter(tag -> { boolean isValid = TestTag.isValid(tag); if (!isValid) { - // TODO [#242] Replace logging with precondition check once we have a proper mechanism for - // handling validation exceptions during the TestEngine discovery phase. - // - // As an alternative to a precondition check here, we could catch any - // PreconditionViolationException thrown by TestTag::create. - logger.warn(() -> String.format( - "Configuration error: invalid tag syntax in @Tag(\"%s\") declaration on [%s]. Tag will be ignored.", - tag, element)); + String message = String.format( + "Invalid tag syntax in @Tag(\"%s\") declaration on %s. Tag will be ignored.", tag, + elementDescription.get()); + if (source.get() == null) { + source.set(sourceProvider.get()); + } + issueCollector.accept( + DiscoveryIssue.builder(Severity.WARNING, message).source(source.get()).build()); } return isValid; - }) - .map(TestTag::create) + }) // + .map(TestTag::create) // .collect(collectingAndThen(toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); - // @formatter:on } /** diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LauncherStoreFacade.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LauncherStoreFacade.java new file mode 100644 index 000000000000..212078d6f3e6 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LauncherStoreFacade.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015-2025 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.engine.descriptor; + +import static org.apiguardian.api.API.Status.INTERNAL; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.engine.execution.NamespaceAwareStore; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; + +@API(status = INTERNAL, since = "5.13") +public class LauncherStoreFacade { + + private final NamespacedHierarchicalStore requestLevelStore; + private final NamespacedHierarchicalStore sessionLevelStore; + + public LauncherStoreFacade(NamespacedHierarchicalStore requestLevelStore) { + this.requestLevelStore = requestLevelStore; + this.sessionLevelStore = requestLevelStore.getParent().orElseThrow( + () -> new JUnitException("Request-level store must have a parent")); + } + + NamespacedHierarchicalStore getRequestLevelStore() { + return this.requestLevelStore; + } + + ExtensionContext.Store getRequestLevelStore(ExtensionContext.Namespace namespace) { + return getStoreAdapter(this.requestLevelStore, namespace); + } + + ExtensionContext.Store getSessionLevelStore(ExtensionContext.Namespace namespace) { + return getStoreAdapter(this.sessionLevelStore, namespace); + } + + NamespaceAwareStore getStoreAdapter(NamespacedHierarchicalStore valuesStore, + ExtensionContext.Namespace namespace) { + Preconditions.notNull(namespace, "Namespace must not be null"); + return new NamespaceAwareStore(valuesStore, convert(namespace)); + } + + private Namespace convert(ExtensionContext.Namespace namespace) { + return namespace.equals(ExtensionContext.Namespace.GLOBAL) // + ? Namespace.GLOBAL // + : Namespace.create(namespace.getParts()); + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtils.java index 5103639abc17..8d629e82fbf4 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtils.java @@ -13,7 +13,7 @@ import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods; import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList; -import static org.junit.platform.engine.support.discovery.DiscoveryIssueReporter.Condition.allOf; +import static org.junit.platform.engine.support.discovery.DiscoveryIssueReporter.Condition.alwaysSatisfied; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -88,15 +88,14 @@ static void validateClassTemplateInvocationLifecycleMethodsAreDeclaredCorrectly( boolean requireStatic, DiscoveryIssueReporter issueReporter) { findAllClassTemplateInvocationLifecycleMethods(testClass) // - .forEach(allOf( // - isNotPrivateError(issueReporter), // - returnsPrimitiveVoid(issueReporter, - LifecycleMethodUtils::classTemplateInvocationLifecycleMethodAnnotationName), // - requireStatic - ? isStatic(issueReporter, - LifecycleMethodUtils::classTemplateInvocationLifecycleMethodAnnotationName) - : __ -> true // - )); + .forEach(isNotPrivateError(issueReporter) // + .and(returnsPrimitiveVoid(issueReporter, + LifecycleMethodUtils::classTemplateInvocationLifecycleMethodAnnotationName)) // + .and(requireStatic + ? isStatic(issueReporter, + LifecycleMethodUtils::classTemplateInvocationLifecycleMethodAnnotationName) + : alwaysSatisfied()) // + .toConsumer()); } private static Stream findAllClassTemplateInvocationLifecycleMethods(Class testClass) { @@ -115,7 +114,7 @@ private static List findMethodsAndCheckStatic(Class testClass, boolea Condition additionalCondition = requireStatic ? isStatic(issueReporter, __ -> annotationType.getSimpleName()) - : __ -> true; + : alwaysSatisfied(); return findMethodsAndCheckVoidReturnType(testClass, annotationType, traversalMode, issueReporter, additionalCondition); } @@ -133,9 +132,9 @@ private static List findMethodsAndCheckVoidReturnType(Class testClass DiscoveryIssueReporter issueReporter, Condition additionalCondition) { return findAnnotatedMethods(testClass, annotationType, traversalMode).stream() // - .peek(isNotPrivateDeprecation(issueReporter, annotationType::getSimpleName)) // - .filter(allOf(returnsPrimitiveVoid(issueReporter, __ -> annotationType.getSimpleName()), - additionalCondition)) // + .peek(isNotPrivateWarning(issueReporter, annotationType::getSimpleName).toConsumer()) // + .filter(returnsPrimitiveVoid(issueReporter, __ -> annotationType.getSimpleName()).and( + additionalCondition).toPredicate()) // .collect(toUnmodifiableList()); } @@ -166,13 +165,13 @@ private static Condition isNotPrivateError(DiscoveryIssueReporter issueR }); } - private static Condition isNotPrivateDeprecation(DiscoveryIssueReporter issueReporter, + private static Condition isNotPrivateWarning(DiscoveryIssueReporter issueReporter, Supplier annotationNameProvider) { return issueReporter.createReportingCondition(ModifierSupport::isNotPrivate, method -> { String message = String.format( "@%s method '%s' should not be private. This will be disallowed in a future release.", annotationNameProvider.get(), method.toGenericString()); - return createIssue(Severity.DEPRECATION, message, method); + return createIssue(Severity.WARNING, message, method); }); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java index 7fc96c4cf07d..4af8de8080e7 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java @@ -17,6 +17,7 @@ import static org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; @@ -39,10 +40,12 @@ import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.commons.util.UnrecoverableExceptions; +import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; /** * Base class for {@link TestDescriptor TestDescriptors} based on Java methods. @@ -51,17 +54,11 @@ */ @API(status = INTERNAL, since = "5.0") public abstract class MethodBasedTestDescriptor extends JupiterTestDescriptor - implements ResourceLockAware, TestClassAware { + implements ResourceLockAware, TestClassAware, Validatable { private static final Logger logger = LoggerFactory.getLogger(MethodBasedTestDescriptor.class); - private final Class testClass; - private final Method testMethod; - - /** - * Set of method-level tags; does not contain tags from parent. - */ - private final Set tags; + private final MethodInfo methodInfo; MethodBasedTestDescriptor(UniqueId uniqueId, Class testClass, Method testMethod, Supplier>> enclosingInstanceTypes, JupiterConfiguration configuration) { @@ -72,20 +69,59 @@ public abstract class MethodBasedTestDescriptor extends JupiterTestDescriptor MethodBasedTestDescriptor(UniqueId uniqueId, String displayName, Class testClass, Method testMethod, JupiterConfiguration configuration) { super(uniqueId, displayName, MethodSource.from(testClass, testMethod), configuration); + this.methodInfo = new MethodInfo(testClass, testMethod); + } - this.testClass = Preconditions.notNull(testClass, "Class must not be null"); - this.testMethod = testMethod; - this.tags = getTags(testMethod); + public final Method getTestMethod() { + return this.methodInfo.testMethod; } + // --- TestDescriptor ------------------------------------------------------ + @Override public final Set getTags() { // return modifiable copy - Set allTags = new LinkedHashSet<>(this.tags); + Set allTags = new LinkedHashSet<>(this.methodInfo.tags); getParent().ifPresent(parentDescriptor -> allTags.addAll(parentDescriptor.getTags())); return allTags; } + @Override + public String getLegacyReportingName() { + return String.format("%s(%s)", getTestMethod().getName(), + ClassUtils.nullSafeToString(Class::getSimpleName, getTestMethod().getParameterTypes())); + } + + // --- TestClassAware ------------------------------------------------------ + + @Override + public final Class getTestClass() { + return this.methodInfo.testClass; + } + + @Override + public List> getEnclosingTestClasses() { + return getParent() // + .filter(TestClassAware.class::isInstance) // + .map(TestClassAware.class::cast) // + .map(TestClassAware::getEnclosingTestClasses) // + .orElseGet(Collections::emptyList); + } + + // --- Validatable --------------------------------------------------------- + + @Override + public void validate(DiscoveryIssueReporter reporter) { + Validatable.reportAndClear(this.methodInfo.discoveryIssues, reporter); + DisplayNameUtils.validateAnnotation(getTestMethod(), // + () -> String.format("method '%s'", getTestMethod().toGenericString()), // + // Use _declaring_ class here because that's where the `@DisplayName` annotation is declared + () -> MethodSource.from(getTestMethod()), // + reporter); + } + + // --- Node ---------------------------------------------------------------- + @Override public ExclusiveResourceCollector getExclusiveResourceCollector() { // There's no need to cache this as this method should only be called once @@ -107,34 +143,11 @@ public Function> getResou getTestMethod())); } - @Override - public List> getEnclosingTestClasses() { - return getParent() // - .filter(TestClassAware.class::isInstance) // - .map(TestClassAware.class::cast) // - .map(TestClassAware::getEnclosingTestClasses) // - .orElseGet(Collections::emptyList); - } - @Override protected Optional getExplicitExecutionMode() { return getExecutionModeFromAnnotation(getTestMethod()); } - public final Class getTestClass() { - return this.testClass; - } - - public final Method getTestMethod() { - return this.testMethod; - } - - @Override - public String getLegacyReportingName() { - return String.format("%s(%s)", testMethod.getName(), - ClassUtils.nullSafeToString(Class::getSimpleName, testMethod.getParameterTypes())); - } - /** * Invoke {@link TestWatcher#testDisabled(ExtensionContext, Optional)} on each * registered {@link TestWatcher}, in registration order. @@ -180,4 +193,27 @@ protected void invokeTestWatchers(JupiterEngineExecutionContext context, boolean } } + private static class MethodInfo { + + private final List discoveryIssues = new ArrayList<>(); + + private final Class testClass; + private final Method testMethod; + + /** + * Set of method-level tags; does not contain tags from parent. + */ + private final Set tags; + + MethodInfo(Class testClass, Method testMethod) { + this.testClass = Preconditions.notNull(testClass, "Class must not be null"); + this.testMethod = testMethod; + this.tags = getTags(testMethod, // + () -> String.format("method '%s'", testMethod.toGenericString()), // + // Use _declaring_ class here because that's where the `@Tag` annotation is declared + () -> MethodSource.from(testMethod.getDeclaringClass(), testMethod), // + discoveryIssues::add); + } + } + } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java index 24cdd6914eb3..eaed0fe9418a 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java @@ -35,9 +35,10 @@ final class MethodExtensionContext extends AbstractExtensionContext stream = provideContexts(provider, extensionContext)) { + Stream stream = provideContexts(provider, extensionContext); + try { stream.forEach(invocationContext -> createInvocationTestDescriptor(invocationContext, invocationIndex.incrementAndGet()) // .ifPresent(testDescriptor -> execute(dynamicTestExecutor, testDescriptor))); } + catch (Throwable t) { + try { + stream.close(); + } + catch (TemplateInvocationValidationException ignore) { + // ignore exceptions from close() to avoid masking the original failure + } + throw ExceptionUtils.throwAsUncheckedException(t); + } + finally { + stream.close(); + } Preconditions.condition( invocationIndex.get() != initialValue || mayReturnZeroContexts(provider, extensionContext), diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java index f145e5531a7c..5b8183513ed1 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java @@ -35,7 +35,6 @@ import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.ReflectiveInterceptorCall; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; import org.junit.platform.commons.JUnitException; -import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.CollectionUtils; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.TestDescriptor; @@ -80,7 +79,6 @@ private TestFactoryTestDescriptor(UniqueId uniqueId, String displayName, Class uniqueIdTransformer) { - // TODO #871 Check that dynamic descendant filter is copied correctly return new TestFactoryTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getDisplayName(), getTestClass(), getTestMethod(), this.configuration, this.dynamicDescendantFilter.copy(uniqueIdTransformer)); } @@ -139,17 +137,11 @@ private Stream toDynamicNodeStream(Object testFactoryMethodResult) if (testFactoryMethodResult instanceof DynamicNode) { return Stream.of((DynamicNode) testFactoryMethodResult); } - try { - return (Stream) CollectionUtils.toStream(testFactoryMethodResult); - } - catch (PreconditionViolationException ex) { - throw invalidReturnTypeException(ex); - } + return (Stream) CollectionUtils.toStream(testFactoryMethodResult); } private JUnitException invalidReturnTypeException(Throwable cause) { - String message = String.format( - "@TestFactory method [%s] must return a single %2$s or a Stream, Collection, Iterable, Iterator, or array of %2$s.", + String message = String.format("Objects produced by @TestFactory method '%s' must be of type %s.", getTestMethod().toGenericString(), DynamicNode.class.getName()); return new JUnitException(message, cause); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java index 8d6f961e96fa..c6a416bd443a 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java @@ -116,7 +116,8 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte MutableExtensionRegistry registry = populateNewExtensionRegistry(context); ThrowableCollector throwableCollector = createThrowableCollector(); MethodExtensionContext extensionContext = new MethodExtensionContext(context.getExtensionContext(), - context.getExecutionListener(), this, context.getConfiguration(), registry, throwableCollector); + context.getExecutionListener(), this, context.getConfiguration(), registry, + context.getLauncherStoreFacade(), throwableCollector); // @formatter:off JupiterEngineExecutionContext newContext = context.extend() .withExtensionRegistry(registry) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java index 5fa74917714b..9a30e567ba2b 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java @@ -32,9 +32,9 @@ final class TestTemplateExtensionContext extends AbstractExtensionContext issues, DiscoveryIssueReporter reporter) { + issues.forEach(reporter::reportIssue); + issues.clear(); + } + } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/AbstractOrderingVisitor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/AbstractOrderingVisitor.java index 0645aa0abf64..8f104923bf5c 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/AbstractOrderingVisitor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/AbstractOrderingVisitor.java @@ -18,14 +18,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; -import org.junit.platform.commons.logging.Logger; -import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.UnrecoverableExceptions; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; /** * Abstract base class for {@linkplain TestDescriptor.Visitor visitors} that @@ -35,7 +37,11 @@ */ abstract class AbstractOrderingVisitor implements TestDescriptor.Visitor { - private static final Logger logger = LoggerFactory.getLogger(AbstractOrderingVisitor.class); + private final DiscoveryIssueReporter issueReporter; + + AbstractOrderingVisitor(DiscoveryIssueReporter issueReporter) { + this.issueReporter = issueReporter; + } /** * @param the parent container type to search in for matching children @@ -51,7 +57,10 @@ protected void doWithMatchingDescriptor(Class errorMessageBuilder.apply(parentTestDescriptor)); + String message = errorMessageBuilder.apply(parentTestDescriptor); + this.issueReporter.reportIssue(DiscoveryIssue.builder(Severity.ERROR, message) // + .source(parentTestDescriptor.getSource()) // + .cause(t)); } } } @@ -61,17 +70,24 @@ protected void doWithMatchingDescriptor(Class> void orderChildrenTestDescriptors( TestDescriptor parentTestDescriptor, Class matchingChildrenType, - Function descriptorWrapperFactory, - DescriptorWrapperOrderer descriptorWrapperOrderer) { + Optional> validationAction, Function descriptorWrapperFactory, + DescriptorWrapperOrderer descriptorWrapperOrderer) { + + Stream matchingChildren = parentTestDescriptor.getChildren()// + .stream()// + .filter(matchingChildrenType::isInstance)// + .map(matchingChildrenType::cast); if (!descriptorWrapperOrderer.canOrderWrappers()) { + validationAction.ifPresent(matchingChildren::forEach); return; } - List matchingDescriptorWrappers = parentTestDescriptor.getChildren()// - .stream()// - .filter(matchingChildrenType::isInstance)// - .map(matchingChildrenType::cast)// + if (validationAction.isPresent()) { + matchingChildren = matchingChildren.peek(validationAction.get()); + } + + List matchingDescriptorWrappers = matchingChildren// .map(descriptorWrapperFactory)// .collect(toCollection(ArrayList::new)); @@ -84,7 +100,8 @@ protected nonMatchingTestDescriptors = children.stream()// .filter(childTestDescriptor -> !matchingChildrenType.isInstance(childTestDescriptor)); - descriptorWrapperOrderer.orderWrappers(matchingDescriptorWrappers); + descriptorWrapperOrderer.orderWrappers(matchingDescriptorWrappers, + message -> reportWarning(parentTestDescriptor, message)); Stream orderedTestDescriptors = matchingDescriptorWrappers.stream()// .map(AbstractAnnotatedDescriptorWrapper::getTestDescriptor); @@ -100,39 +117,50 @@ protected the wrapper type for the children to order */ - protected static class DescriptorWrapperOrderer { + protected static class DescriptorWrapperOrderer { - private static final DescriptorWrapperOrderer NOOP = new DescriptorWrapperOrderer<>(null, __ -> "", + private static final DescriptorWrapperOrderer NOOP = new DescriptorWrapperOrderer<>(null, null, __ -> "", ___ -> ""); @SuppressWarnings("unchecked") - protected static DescriptorWrapperOrderer noop() { - return (DescriptorWrapperOrderer) NOOP; + protected static DescriptorWrapperOrderer noop() { + return (DescriptorWrapperOrderer) NOOP; } + private final ORDERER orderer; private final Consumer> orderingAction; private final MessageGenerator descriptorsAddedMessageGenerator; private final MessageGenerator descriptorsRemovedMessageGenerator; - DescriptorWrapperOrderer(Consumer> orderingAction, + DescriptorWrapperOrderer(ORDERER orderer, Consumer> orderingAction, MessageGenerator descriptorsAddedMessageGenerator, MessageGenerator descriptorsRemovedMessageGenerator) { + this.orderer = orderer; this.orderingAction = orderingAction; this.descriptorsAddedMessageGenerator = descriptorsAddedMessageGenerator; this.descriptorsRemovedMessageGenerator = descriptorsRemovedMessageGenerator; } + ORDERER getOrderer() { + return orderer; + } + private boolean canOrderWrappers() { return this.orderingAction != null; } - private void orderWrappers(List wrappers) { + private void orderWrappers(List wrappers, Consumer errorHandler) { List orderedWrappers = new ArrayList<>(wrappers); this.orderingAction.accept(orderedWrappers); Map distinctWrappersToIndex = distinctWrappersToIndex(orderedWrappers); @@ -140,10 +168,10 @@ private void orderWrappers(List wrappers) { int difference = orderedWrappers.size() - wrappers.size(); int distinctDifference = distinctWrappersToIndex.size() - wrappers.size(); if (difference > 0) { // difference >= distinctDifference - logDescriptorsAddedWarning(difference); + reportDescriptorsAddedWarning(difference, errorHandler); } if (distinctDifference < 0) { // distinctDifference <= difference - logDescriptorsRemovedWarning(distinctDifference); + reportDescriptorsRemovedWarning(distinctDifference, errorHandler); } wrappers.sort(comparing(wrapper -> distinctWrappersToIndex.getOrDefault(wrapper, -1))); @@ -161,12 +189,12 @@ private Map distinctWrappersToIndex(List wrappers) { return toIndex; } - private void logDescriptorsAddedWarning(int number) { - logger.warn(() -> this.descriptorsAddedMessageGenerator.generateMessage(number)); + private void reportDescriptorsAddedWarning(int number, Consumer errorHandler) { + errorHandler.accept(this.descriptorsAddedMessageGenerator.generateMessage(number)); } - private void logDescriptorsRemovedWarning(int number) { - logger.warn(() -> this.descriptorsRemovedMessageGenerator.generateMessage(Math.abs(number))); + private void reportDescriptorsRemovedWarning(int number, Consumer errorHandler) { + errorHandler.accept(this.descriptorsRemovedMessageGenerator.generateMessage(Math.abs(number))); } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassOrderingVisitor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassOrderingVisitor.java index fb886108363a..e64063088600 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassOrderingVisitor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassOrderingVisitor.java @@ -10,10 +10,14 @@ package org.junit.jupiter.engine.discovery; +import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; + import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.TestClassOrder; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor; @@ -21,21 +25,37 @@ import org.junit.platform.commons.support.AnnotationSupport; import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.commons.util.LruCache; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter.Condition; /** * @since 5.8 */ class ClassOrderingVisitor extends AbstractOrderingVisitor { - private final LruCache> ordererCache = new LruCache<>( + private final LruCache> ordererCache = new LruCache<>( 10); private final JupiterConfiguration configuration; - private final DescriptorWrapperOrderer globalOrderer; + private final DescriptorWrapperOrderer globalOrderer; + private final Condition noOrderAnnotation; - ClassOrderingVisitor(JupiterConfiguration configuration) { + ClassOrderingVisitor(JupiterConfiguration configuration, DiscoveryIssueReporter issueReporter) { + super(issueReporter); this.configuration = configuration; this.globalOrderer = createGlobalOrderer(configuration); + this.noOrderAnnotation = issueReporter.createReportingCondition( + testDescriptor -> !isAnnotated(testDescriptor.getTestClass(), Order.class), testDescriptor -> { + String message = String.format( + "Ineffective @Order annotation on class '%s'. It will not be applied because ClassOrderer.OrderAnnotation is not in use.", + testDescriptor.getTestClass().getName()); + return DiscoveryIssue.builder(Severity.INFO, message) // + .source(ClassSource.from(testDescriptor.getTestClass())) // + .build(); + }); } @Override @@ -59,31 +79,37 @@ private void orderTopLevelClasses(JupiterEngineDescriptor engineDescriptor) { orderChildrenTestDescriptors(// engineDescriptor, // ClassBasedTestDescriptor.class, // + toValidationAction(globalOrderer), // DefaultClassDescriptor::new, // globalOrderer); } private void orderNestedClasses(ClassBasedTestDescriptor descriptor) { + DescriptorWrapperOrderer wrapperOrderer = createAndCacheClassLevelOrderer( + descriptor); orderChildrenTestDescriptors(// descriptor, // ClassBasedTestDescriptor.class, // + toValidationAction(wrapperOrderer), // DefaultClassDescriptor::new, // - createAndCacheClassLevelOrderer(descriptor)); + wrapperOrderer); } - private DescriptorWrapperOrderer createGlobalOrderer(JupiterConfiguration configuration) { + private DescriptorWrapperOrderer createGlobalOrderer( + JupiterConfiguration configuration) { ClassOrderer classOrderer = configuration.getDefaultTestClassOrderer().orElse(null); return classOrderer == null ? DescriptorWrapperOrderer.noop() : createDescriptorWrapperOrderer(classOrderer); } - private DescriptorWrapperOrderer createAndCacheClassLevelOrderer( + private DescriptorWrapperOrderer createAndCacheClassLevelOrderer( ClassBasedTestDescriptor classBasedTestDescriptor) { - DescriptorWrapperOrderer orderer = createClassLevelOrderer(classBasedTestDescriptor); + DescriptorWrapperOrderer orderer = createClassLevelOrderer( + classBasedTestDescriptor); ordererCache.put(classBasedTestDescriptor, orderer); return orderer; } - private DescriptorWrapperOrderer createClassLevelOrderer( + private DescriptorWrapperOrderer createClassLevelOrderer( ClassBasedTestDescriptor classBasedTestDescriptor) { return AnnotationSupport.findAnnotation(classBasedTestDescriptor.getTestClass(), TestClassOrder.class)// .map(TestClassOrder::value)// @@ -93,7 +119,7 @@ private DescriptorWrapperOrderer createClassLevelOrderer Object parent = classBasedTestDescriptor.getParent().orElse(null); if (parent instanceof ClassBasedTestDescriptor) { ClassBasedTestDescriptor parentClassTestDescriptor = (ClassBasedTestDescriptor) parent; - DescriptorWrapperOrderer cacheEntry = ordererCache.get( + DescriptorWrapperOrderer cacheEntry = ordererCache.get( parentClassTestDescriptor); return cacheEntry != null ? cacheEntry : createClassLevelOrderer(parentClassTestDescriptor); } @@ -101,7 +127,8 @@ private DescriptorWrapperOrderer createClassLevelOrderer }); } - private DescriptorWrapperOrderer createDescriptorWrapperOrderer(ClassOrderer classOrderer) { + private DescriptorWrapperOrderer createDescriptorWrapperOrderer( + ClassOrderer classOrderer) { Consumer> orderingAction = classDescriptors -> classOrderer.orderClasses( new DefaultClassOrdererContext(classDescriptors, this.configuration)); @@ -112,8 +139,17 @@ private DescriptorWrapperOrderer createDescriptorWrapper "ClassOrderer [%s] removed %s ClassDescriptor(s) which will be retained with arbitrary ordering.", classOrderer.getClass().getName(), number); - return new DescriptorWrapperOrderer<>(orderingAction, descriptorsAddedMessageGenerator, + return new DescriptorWrapperOrderer<>(classOrderer, orderingAction, descriptorsAddedMessageGenerator, descriptorsRemovedMessageGenerator); } + private Optional> toValidationAction( + DescriptorWrapperOrderer wrapperOrderer) { + + if (wrapperOrderer.getOrderer() instanceof ClassOrderer.OrderAnnotation) { + return Optional.empty(); + } + return Optional.of(noOrderAnnotation::check); + } + } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassSelectorResolver.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassSelectorResolver.java index 3405dad0e7d7..19480129303e 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassSelectorResolver.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassSelectorResolver.java @@ -15,12 +15,11 @@ import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toSet; import static org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor.getEnclosingTestClasses; -import static org.junit.jupiter.engine.discovery.predicates.IsTestClassWithTests.isTestOrTestFactoryOrTestTemplateMethod; -import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; import static org.junit.platform.commons.support.HierarchyTraversalMode.TOP_DOWN; import static org.junit.platform.commons.support.ReflectionSupport.findMethods; import static org.junit.platform.commons.support.ReflectionSupport.streamNestedClasses; import static org.junit.platform.commons.util.FunctionUtils.where; +import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; import static org.junit.platform.engine.support.discovery.SelectorResolver.Resolution.unresolved; @@ -35,7 +34,6 @@ import java.util.function.Supplier; import java.util.stream.Stream; -import org.junit.jupiter.api.ClassTemplate; import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor; @@ -45,9 +43,11 @@ import org.junit.jupiter.engine.descriptor.Filterable; import org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor; import org.junit.jupiter.engine.descriptor.TestClassAware; -import org.junit.jupiter.engine.discovery.predicates.IsNestedTestClass; -import org.junit.jupiter.engine.discovery.predicates.IsTestClassWithTests; +import org.junit.jupiter.engine.discovery.predicates.TestClassPredicates; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.util.ReflectionUtils; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; @@ -56,6 +56,8 @@ import org.junit.platform.engine.discovery.IterationSelector; import org.junit.platform.engine.discovery.NestedClassSelector; import org.junit.platform.engine.discovery.UniqueIdSelector; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; import org.junit.platform.engine.support.discovery.SelectorResolver; /** @@ -63,41 +65,59 @@ */ class ClassSelectorResolver implements SelectorResolver { - private static final IsTestClassWithTests isTestClassWithTests = new IsTestClassWithTests(); - private static final IsNestedTestClass isNestedTestClass = new IsNestedTestClass(); - private static final Predicate> isAnnotatedWithClassTemplate = testClass -> isAnnotated(testClass, - ClassTemplate.class); - private final Predicate classNameFilter; private final JupiterConfiguration configuration; + private final TestClassPredicates predicates; + private final DiscoveryIssueReporter issueReporter; - ClassSelectorResolver(Predicate classNameFilter, JupiterConfiguration configuration) { + ClassSelectorResolver(Predicate classNameFilter, JupiterConfiguration configuration, + DiscoveryIssueReporter issueReporter) { this.classNameFilter = classNameFilter; this.configuration = configuration; + this.predicates = new TestClassPredicates(issueReporter); + this.issueReporter = issueReporter; } @Override public Resolution resolve(ClassSelector selector, Context context) { Class testClass = selector.getJavaClass(); - if (isTestClassWithTests.test(testClass)) { - // Nested tests are never filtered out - if (classNameFilter.test(testClass.getName())) { + + if (this.predicates.isAnnotatedWithNested.test(testClass)) { + // Class name filter is not applied to nested test classes + if (this.predicates.isValidNestedTestClass(testClass)) { return toResolution( - context.addToParent(parent -> Optional.of(newStaticClassTestDescriptor(parent, testClass)))); + context.addToParent(() -> DiscoverySelectors.selectClass(testClass.getEnclosingClass()), + parent -> Optional.of(newMemberClassTestDescriptor(parent, testClass)))); } } - else if (isNestedTestClass.test(testClass)) { - return toResolution(context.addToParent(() -> DiscoverySelectors.selectClass(testClass.getEnclosingClass()), - parent -> Optional.of(newMemberClassTestDescriptor(parent, testClass)))); + else if (isAcceptedStandaloneTestClass(testClass)) { + return toResolution( + context.addToParent(parent -> Optional.of(newStandaloneClassTestDescriptor(parent, testClass)))); } return unresolved(); } + private boolean isAcceptedStandaloneTestClass(Class testClass) { + return this.classNameFilter.test(testClass.getName()) // + && this.predicates.looksLikeIntendedTestClass(testClass) // + && this.predicates.isValidStandaloneTestClass(testClass); + } + @Override public Resolution resolve(NestedClassSelector selector, Context context) { - if (isNestedTestClass.test(selector.getNestedClass())) { - return toResolution(context.addToParent(() -> selectClass(selector.getEnclosingClasses()), - parent -> Optional.of(newMemberClassTestDescriptor(parent, selector.getNestedClass())))); + Class nestedClass = selector.getNestedClass(); + if (this.predicates.isAnnotatedWithNested.test(nestedClass)) { + if (this.predicates.isValidNestedTestClass(nestedClass)) { + return toResolution(context.addToParent(() -> selectClass(selector.getEnclosingClasses()), + parent -> Optional.of(newMemberClassTestDescriptor(parent, nestedClass)))); + } + } + else if (isInnerClass(nestedClass) && predicates.looksLikeIntendedTestClass(nestedClass)) { + String message = String.format( + "Inner class '%s' looks like it was intended to be a test class but will not be executed. It must be static or annotated with @Nested.", + nestedClass.getName()); + issueReporter.reportIssue(DiscoveryIssue.builder(Severity.WARNING, message) // + .source(ClassSource.from(nestedClass))); } return unresolved(); } @@ -107,17 +127,17 @@ public Resolution resolve(UniqueIdSelector selector, Context context) { UniqueId uniqueId = selector.getUniqueId(); UniqueId.Segment lastSegment = uniqueId.getLastSegment(); if (ClassTestDescriptor.SEGMENT_TYPE.equals(lastSegment.getType())) { - return resolveStaticClassUniqueId(context, lastSegment, __ -> true, this::newClassTestDescriptor); + return resolveStandaloneClassUniqueId(context, lastSegment, __ -> true, this::newClassTestDescriptor); } - if (ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE.equals(lastSegment.getType())) { - return resolveStaticClassUniqueId(context, lastSegment, isAnnotatedWithClassTemplate, - this::newStaticClassTemplateTestDescriptor); + if (ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE.equals(lastSegment.getType())) { + return resolveStandaloneClassUniqueId(context, lastSegment, this.predicates.isAnnotatedWithClassTemplate, + this::newClassTemplateTestDescriptor); } if (NestedClassTestDescriptor.SEGMENT_TYPE.equals(lastSegment.getType())) { return resolveNestedClassUniqueId(context, uniqueId, __ -> true, this::newNestedClassTestDescriptor); } if (ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE.equals(lastSegment.getType())) { - return resolveNestedClassUniqueId(context, uniqueId, isAnnotatedWithClassTemplate, + return resolveNestedClassUniqueId(context, uniqueId, this.predicates.isAnnotatedWithClassTemplate, this::newNestedClassTemplateTestDescriptor); } if (ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE.equals(lastSegment.getType())) { @@ -137,11 +157,11 @@ public Resolution resolve(UniqueIdSelector selector, Context context) { public Resolution resolve(IterationSelector selector, Context context) { DiscoverySelector parentSelector = selector.getParentSelector(); if (parentSelector instanceof ClassSelector - && isAnnotatedWithClassTemplate.test(((ClassSelector) parentSelector).getJavaClass())) { + && this.predicates.isAnnotatedWithClassTemplate.test(((ClassSelector) parentSelector).getJavaClass())) { return resolveIterations(selector, context); } - if (parentSelector instanceof NestedClassSelector - && isAnnotatedWithClassTemplate.test(((NestedClassSelector) parentSelector).getNestedClass())) { + if (parentSelector instanceof NestedClassSelector && this.predicates.isAnnotatedWithClassTemplate.test( + ((NestedClassSelector) parentSelector).getNestedClass())) { return resolveIterations(selector, context); } return unresolved(); @@ -159,13 +179,13 @@ private Resolution resolveIterations(IterationSelector selector, Context context return matches.isEmpty() ? unresolved() : Resolution.matches(matches); } - private Resolution resolveStaticClassUniqueId(Context context, UniqueId.Segment lastSegment, + private Resolution resolveStandaloneClassUniqueId(Context context, UniqueId.Segment lastSegment, Predicate> condition, BiFunction, ClassBasedTestDescriptor> factory) { String className = lastSegment.getValue(); return ReflectionSupport.tryToLoadClass(className).toOptional() // - .filter(isTestClassWithTests) // + .filter(this.predicates::isValidStandaloneTestClass) // .filter(condition) // .map(testClass -> toResolution( context.addToParent(parent -> Optional.of(factory.apply(parent, testClass))))) // @@ -180,7 +200,8 @@ private Resolution resolveNestedClassUniqueId(Context context, UniqueId uniqueId return toResolution(context.addToParent(() -> selectUniqueId(uniqueId.removeLastSegment()), parent -> { Class parentTestClass = ((TestClassAware) parent).getTestClass(); return ReflectionSupport.findNestedClasses(parentTestClass, - isNestedTestClass.and(where(Class::getSimpleName, isEqual(simpleClassName)))).stream() // + this.predicates.isAnnotatedWithNestedAndValid.and( + where(Class::getSimpleName, isEqual(simpleClassName)))).stream() // .findFirst() // .filter(condition) // .map(testClass -> factory.apply(parent, testClass)); @@ -195,15 +216,14 @@ private ClassTemplateInvocationTestDescriptor newDummyClassTemplateInvocationTes DummyClassTemplateInvocationContext.INSTANCE, index, parent.getSource().orElse(null), configuration); } - private ClassBasedTestDescriptor newStaticClassTestDescriptor(TestDescriptor parent, Class testClass) { - return isAnnotatedWithClassTemplate.test(testClass) // - ? newStaticClassTemplateTestDescriptor(parent, testClass) // + private ClassBasedTestDescriptor newStandaloneClassTestDescriptor(TestDescriptor parent, Class testClass) { + return this.predicates.isAnnotatedWithClassTemplate.test(testClass) // + ? newClassTemplateTestDescriptor(parent, testClass) // : newClassTestDescriptor(parent, testClass); } - private ClassTemplateTestDescriptor newStaticClassTemplateTestDescriptor(TestDescriptor parent, - Class testClass) { - return newClassTemplateTestDescriptor(parent, ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + private ClassTemplateTestDescriptor newClassTemplateTestDescriptor(TestDescriptor parent, Class testClass) { + return newClassTemplateTestDescriptor(parent, ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, newClassTestDescriptor(parent, testClass)); } @@ -214,7 +234,7 @@ private ClassTestDescriptor newClassTestDescriptor(TestDescriptor parent, Class< } private ClassBasedTestDescriptor newMemberClassTestDescriptor(TestDescriptor parent, Class testClass) { - return isAnnotatedWithClassTemplate.test(testClass) // + return this.predicates.isAnnotatedWithClassTemplate.test(testClass) // ? newNestedClassTemplateTestDescriptor(parent, testClass) // : newNestedClassTestDescriptor(parent, testClass); } @@ -271,10 +291,12 @@ private Supplier> expansionCallback(TestDescrip } List> testClasses = testClassesSupplier.get(); Class testClass = testClasses.get(testClasses.size() - 1); - Stream methods = findMethods(testClass, isTestOrTestFactoryOrTestTemplateMethod, - TOP_DOWN).stream().map(method -> selectMethod(testClasses, method)); - Stream nestedClasses = streamNestedClasses(testClass, isNestedTestClass).map( - nestedClass -> DiscoverySelectors.selectNestedClass(testClasses, nestedClass)); + Stream methods = findMethods(testClass, + this.predicates.isTestOrTestFactoryOrTestTemplateMethod, TOP_DOWN).stream() // + .map(method -> selectMethod(testClasses, method)); + Stream nestedClasses = streamNestedClasses(testClass, + this.predicates.isAnnotatedWithNested.or(ReflectionUtils::isInnerClass)) // + .map(nestedClass -> DiscoverySelectors.selectNestedClass(testClasses, nestedClass)); return Stream.concat(methods, nestedClasses).collect( toCollection((Supplier>) LinkedHashSet::new)); }; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolver.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolver.java index 974684d680cb..6fabb7546b73 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolver.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolver.java @@ -16,9 +16,10 @@ import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; import org.junit.jupiter.engine.descriptor.Validatable; -import org.junit.jupiter.engine.discovery.predicates.IsTestClassWithTests; +import org.junit.jupiter.engine.discovery.predicates.TestClassPredicates; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver; import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver.InitializationContext; @@ -37,12 +38,14 @@ public class DiscoverySelectorResolver { private static final EngineDiscoveryRequestResolver resolver = EngineDiscoveryRequestResolver. builder() // - .addClassContainerSelectorResolver(new IsTestClassWithTests()) // - .addSelectorResolver(ctx -> new ClassSelectorResolver(ctx.getClassNameFilter(), getConfiguration(ctx))) // + .addClassContainerSelectorResolverWithContext( + ctx -> new TestClassPredicates(ctx.getIssueReporter()).looksLikeNestedOrStandaloneTestClass) // + .addSelectorResolver(ctx -> new ClassSelectorResolver(ctx.getClassNameFilter(), getConfiguration(ctx), + ctx.getIssueReporter())) // .addSelectorResolver(ctx -> new MethodSelectorResolver(getConfiguration(ctx), ctx.getIssueReporter())) // .addTestDescriptorVisitor(ctx -> TestDescriptor.Visitor.composite( // - new ClassOrderingVisitor(getConfiguration(ctx)), // - new MethodOrderingVisitor(getConfiguration(ctx)), // + new ClassOrderingVisitor(getConfiguration(ctx), ctx.getIssueReporter()), // + new MethodOrderingVisitor(getConfiguration(ctx), ctx.getIssueReporter()), // descriptor -> { if (descriptor instanceof Validatable) { ((Validatable) descriptor).validate(ctx.getIssueReporter()); @@ -55,7 +58,9 @@ private static JupiterConfiguration getConfiguration(InitializationContext noOrderAnnotation; - MethodOrderingVisitor(JupiterConfiguration configuration) { + MethodOrderingVisitor(JupiterConfiguration configuration, DiscoveryIssueReporter issueReporter) { + super(issueReporter); this.configuration = configuration; + this.noOrderAnnotation = issueReporter.createReportingCondition( + testDescriptor -> !isAnnotated(testDescriptor.getTestMethod(), Order.class), testDescriptor -> { + String message = String.format( + "Ineffective @Order annotation on method '%s'. It will not be applied because MethodOrderer.OrderAnnotation is not in use.", + testDescriptor.getTestMethod().toGenericString()); + return DiscoveryIssue.builder(Severity.INFO, message) // + .source(MethodSource.from(testDescriptor.getTestMethod())) // + .build(); + }); } @Override @@ -54,37 +72,62 @@ protected boolean shouldNonMatchingDescriptorsComeBeforeOrderedOnes() { * @since 5.4 */ private void orderContainedMethods(ClassBasedTestDescriptor classBasedTestDescriptor, Class testClass) { - findAnnotation(testClass, TestMethodOrder.class)// + Optional methodOrderer = findAnnotation(testClass, TestMethodOrder.class)// .map(TestMethodOrder::value)// . map(ReflectionSupport::newInstance)// .map(Optional::of)// - .orElseGet(configuration::getDefaultTestMethodOrderer)// - .ifPresent(methodOrderer -> { - - Consumer> orderingAction = methodDescriptors -> methodOrderer.orderMethods( - new DefaultMethodOrdererContext(testClass, methodDescriptors, this.configuration)); - - MessageGenerator descriptorsAddedMessageGenerator = number -> String.format( - "MethodOrderer [%s] added %s MethodDescriptor(s) for test class [%s] which will be ignored.", - methodOrderer.getClass().getName(), number, testClass.getName()); - MessageGenerator descriptorsRemovedMessageGenerator = number -> String.format( - "MethodOrderer [%s] removed %s MethodDescriptor(s) for test class [%s] which will be retained with arbitrary ordering.", - methodOrderer.getClass().getName(), number, testClass.getName()); - - DescriptorWrapperOrderer descriptorWrapperOrderer = new DescriptorWrapperOrderer<>( - orderingAction, descriptorsAddedMessageGenerator, descriptorsRemovedMessageGenerator); - - orderChildrenTestDescriptors(classBasedTestDescriptor, // - MethodBasedTestDescriptor.class, // - DefaultMethodDescriptor::new, // - descriptorWrapperOrderer); - - // Note: MethodOrderer#getDefaultExecutionMode() is guaranteed - // to be invoked after MethodOrderer#orderMethods(). - methodOrderer.getDefaultExecutionMode()// - .map(JupiterTestDescriptor::toExecutionMode)// - .ifPresent(classBasedTestDescriptor::setDefaultChildExecutionMode); - }); + .orElseGet(configuration::getDefaultTestMethodOrderer); + orderContainedMethods(classBasedTestDescriptor, testClass, methodOrderer); + } + + private void orderContainedMethods(ClassBasedTestDescriptor classBasedTestDescriptor, Class testClass, + Optional methodOrderer) { + DescriptorWrapperOrderer descriptorWrapperOrderer = createDescriptorWrapperOrderer( + testClass, methodOrderer); + + orderChildrenTestDescriptors(classBasedTestDescriptor, // + MethodBasedTestDescriptor.class, // + toValidationAction(methodOrderer), // + DefaultMethodDescriptor::new, // + descriptorWrapperOrderer); + + // Note: MethodOrderer#getDefaultExecutionMode() is guaranteed + // to be invoked after MethodOrderer#orderMethods(). + methodOrderer // + .flatMap(it -> it.getDefaultExecutionMode().map(JupiterTestDescriptor::toExecutionMode)) // + .ifPresent(classBasedTestDescriptor::setDefaultChildExecutionMode); + } + + private DescriptorWrapperOrderer createDescriptorWrapperOrderer(Class testClass, + Optional methodOrderer) { + + return methodOrderer // + .map(it -> createDescriptorWrapperOrderer(testClass, it)) // + .orElseGet(DescriptorWrapperOrderer::noop); + + } + + private DescriptorWrapperOrderer createDescriptorWrapperOrderer(Class testClass, + MethodOrderer methodOrderer) { + Consumer> orderingAction = methodDescriptors -> methodOrderer.orderMethods( + new DefaultMethodOrdererContext(testClass, methodDescriptors, this.configuration)); + + MessageGenerator descriptorsAddedMessageGenerator = number -> String.format( + "MethodOrderer [%s] added %s MethodDescriptor(s) for test class [%s] which will be ignored.", + methodOrderer.getClass().getName(), number, testClass.getName()); + MessageGenerator descriptorsRemovedMessageGenerator = number -> String.format( + "MethodOrderer [%s] removed %s MethodDescriptor(s) for test class [%s] which will be retained with arbitrary ordering.", + methodOrderer.getClass().getName(), number, testClass.getName()); + + return new DescriptorWrapperOrderer<>(methodOrderer, orderingAction, descriptorsAddedMessageGenerator, + descriptorsRemovedMessageGenerator); + } + + private Optional> toValidationAction(Optional methodOrderer) { + if (methodOrderer.orElse(null) instanceof MethodOrderer.OrderAnnotation) { + return Optional.empty(); + } + return Optional.of(noOrderAnnotation::check); } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodSelectorResolver.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodSelectorResolver.java index f956edfd7905..09b021766f72 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodSelectorResolver.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodSelectorResolver.java @@ -36,11 +36,10 @@ import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateInvocationTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor; -import org.junit.jupiter.engine.discovery.predicates.IsNestedTestClass; -import org.junit.jupiter.engine.discovery.predicates.IsTestClassWithTests; import org.junit.jupiter.engine.discovery.predicates.IsTestFactoryMethod; import org.junit.jupiter.engine.discovery.predicates.IsTestMethod; import org.junit.jupiter.engine.discovery.predicates.IsTestTemplateMethod; +import org.junit.jupiter.engine.discovery.predicates.TestClassPredicates; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.DiscoveryIssue.Severity; @@ -62,15 +61,17 @@ class MethodSelectorResolver implements SelectorResolver { private static final MethodFinder methodFinder = new MethodFinder(); - private static final Predicate> testClassPredicate = new IsTestClassWithTests().or( - new IsNestedTestClass()); + private final Predicate> testClassPredicate; private final JupiterConfiguration configuration; private final DiscoveryIssueReporter issueReporter; + private final List methodTypes; MethodSelectorResolver(JupiterConfiguration configuration, DiscoveryIssueReporter issueReporter) { this.configuration = configuration; this.issueReporter = issueReporter; + this.methodTypes = MethodType.allPossibilities(issueReporter); + this.testClassPredicate = new TestClassPredicates(issueReporter).looksLikeNestedOrStandaloneTestClass; } @Override @@ -92,7 +93,7 @@ private Resolution resolve(Context context, List> enclosingClasses, Cla } Method method = methodSupplier.get(); // @formatter:off - Set matches = Arrays.stream(MethodType.values()) + Set matches = methodTypes.stream() .map(methodType -> methodType.resolve(enclosingClasses, testClass, method, context, configuration)) .filter(Optional::isPresent) .map(Optional::get) @@ -116,7 +117,7 @@ private Resolution resolve(Context context, List> enclosingClasses, Cla public Resolution resolve(UniqueIdSelector selector, Context context) { UniqueId uniqueId = selector.getUniqueId(); // @formatter:off - return Arrays.stream(MethodType.values()) + return methodTypes.stream() .map(methodType -> methodType.resolveUniqueIdIntoTestDescriptor(uniqueId, context, configuration)) .filter(Optional::isPresent) .map(Optional::get) @@ -164,48 +165,34 @@ private Supplier> expansionCallback(TestDescrip }; } - private enum MethodType { - - TEST(new IsTestMethod(), TestMethodTestDescriptor.SEGMENT_TYPE) { - @Override - protected TestDescriptor createTestDescriptor(UniqueId uniqueId, Class testClass, Method method, - Supplier>> enclosingInstanceTypes, JupiterConfiguration configuration) { - return new TestMethodTestDescriptor(uniqueId, testClass, method, enclosingInstanceTypes, configuration); - } - }, - - TEST_FACTORY(new IsTestFactoryMethod(), TestFactoryTestDescriptor.SEGMENT_TYPE, - TestFactoryTestDescriptor.DYNAMIC_CONTAINER_SEGMENT_TYPE, - TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE) { - @Override - protected TestDescriptor createTestDescriptor(UniqueId uniqueId, Class testClass, Method method, - Supplier>> enclosingInstanceTypes, JupiterConfiguration configuration) { - return new TestFactoryTestDescriptor(uniqueId, testClass, method, enclosingInstanceTypes, - configuration); - } - }, - - TEST_TEMPLATE(new IsTestTemplateMethod(), TestTemplateTestDescriptor.SEGMENT_TYPE, - TestTemplateInvocationTestDescriptor.SEGMENT_TYPE) { - @Override - protected TestDescriptor createTestDescriptor(UniqueId uniqueId, Class testClass, Method method, - Supplier>> enclosingInstanceTypes, JupiterConfiguration configuration) { - return new TestTemplateTestDescriptor(uniqueId, testClass, method, enclosingInstanceTypes, - configuration); - } - }; + private static class MethodType { + + static List allPossibilities(DiscoveryIssueReporter issueReporter) { + return Arrays.asList( // + new MethodType(new IsTestMethod(issueReporter), TestMethodTestDescriptor::new, + TestMethodTestDescriptor.SEGMENT_TYPE), // + new MethodType(new IsTestFactoryMethod(issueReporter), TestFactoryTestDescriptor::new, + TestFactoryTestDescriptor.SEGMENT_TYPE, TestFactoryTestDescriptor.DYNAMIC_CONTAINER_SEGMENT_TYPE, + TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE), // + new MethodType(new IsTestTemplateMethod(issueReporter), TestTemplateTestDescriptor::new, + TestTemplateTestDescriptor.SEGMENT_TYPE, TestTemplateInvocationTestDescriptor.SEGMENT_TYPE) // + ); + } private final Predicate methodPredicate; + private final TestDescriptorFactory testDescriptorFactory; private final String segmentType; private final Set dynamicDescendantSegmentTypes; - MethodType(Predicate methodPredicate, String segmentType, String... dynamicDescendantSegmentTypes) { + private MethodType(Predicate methodPredicate, TestDescriptorFactory testDescriptorFactory, + String segmentType, String... dynamicDescendantSegmentTypes) { this.methodPredicate = methodPredicate; + this.testDescriptorFactory = testDescriptorFactory; this.segmentType = segmentType; this.dynamicDescendantSegmentTypes = new LinkedHashSet<>(Arrays.asList(dynamicDescendantSegmentTypes)); } - private Optional resolve(List> enclosingClasses, Class testClass, Method method, + Optional resolve(List> enclosingClasses, Class testClass, Method method, Context context, JupiterConfiguration configuration) { if (!methodPredicate.test(method)) { return Optional.empty(); @@ -221,7 +208,7 @@ private DiscoverySelector selectClass(List> enclosingClasses, Class return DiscoverySelectors.selectNestedClass(enclosingClasses, testClass); } - private Optional resolveUniqueIdIntoTestDescriptor(UniqueId uniqueId, Context context, + Optional resolveUniqueIdIntoTestDescriptor(UniqueId uniqueId, Context context, JupiterConfiguration configuration) { UniqueId.Segment lastSegment = uniqueId.getLastSegment(); if (segmentType.equals(lastSegment.getType())) { @@ -244,8 +231,8 @@ private Optional resolveUniqueIdIntoTestDescriptor(UniqueId uniq private TestDescriptor createTestDescriptor(TestDescriptor parent, Class testClass, Method method, JupiterConfiguration configuration) { UniqueId uniqueId = createUniqueId(method, parent); - return createTestDescriptor(uniqueId, testClass, method, ((TestClassAware) parent)::getEnclosingTestClasses, - configuration); + return testDescriptorFactory.create(uniqueId, testClass, method, + ((TestClassAware) parent)::getEnclosingTestClasses, configuration); } private UniqueId createUniqueId(Method method, TestDescriptor parent) { @@ -254,8 +241,10 @@ private UniqueId createUniqueId(Method method, TestDescriptor parent) { return parent.getUniqueId().append(segmentType, methodId); } - protected abstract TestDescriptor createTestDescriptor(UniqueId uniqueId, Class testClass, Method method, - Supplier>> enclosingInstanceTypes, JupiterConfiguration configuration); + interface TestDescriptorFactory { + TestDescriptor create(UniqueId uniqueId, Class testClass, Method method, + Supplier>> enclosingInstanceTypes, JupiterConfiguration configuration); + } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsInnerClass.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsInnerClass.java deleted file mode 100644 index 1e30582d48ca..000000000000 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsInnerClass.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2015-2025 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.engine.discovery.predicates; - -import static org.apiguardian.api.API.Status.INTERNAL; -import static org.junit.platform.commons.support.ModifierSupport.isPrivate; -import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass; - -import java.util.function.Predicate; - -import org.apiguardian.api.API; - -/** - * Test if a class is a non-private inner class (i.e., a non-static nested class). - * - * @since 5.0 - */ -@API(status = INTERNAL, since = "5.0") -public class IsInnerClass implements Predicate> { - - @Override - public boolean test(Class candidate) { - // Do not collapse into a single return statement. - if (isPrivate(candidate)) { - return false; - } - if (!isInnerClass(candidate)) { - return false; - } - - return true; - } - -} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsNestedTestClass.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsNestedTestClass.java deleted file mode 100644 index 3474d8cf0240..000000000000 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsNestedTestClass.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2015-2025 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.engine.discovery.predicates; - -import static org.apiguardian.api.API.Status.INTERNAL; -import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; - -import java.util.function.Predicate; - -import org.apiguardian.api.API; -import org.junit.jupiter.api.Nested; - -/** - * Test if a class is a JUnit Jupiter {@link Nested @Nested} test class. - * - * @since 5.0 - */ -@API(status = INTERNAL, since = "5.0") -public class IsNestedTestClass implements Predicate> { - - private static final IsInnerClass isInnerClass = new IsInnerClass(); - - @Override - public boolean test(Class candidate) { - //please do not collapse into single return - if (!isInnerClass.test(candidate)) { - return false; - } - return isAnnotated(candidate, Nested.class); - } - -} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsPotentialTestContainer.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsPotentialTestContainer.java deleted file mode 100644 index 0534cd2481c0..000000000000 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsPotentialTestContainer.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2015-2025 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.engine.discovery.predicates; - -import static org.apiguardian.api.API.Status.INTERNAL; -import static org.junit.platform.commons.support.ModifierSupport.isAbstract; -import static org.junit.platform.commons.support.ModifierSupport.isPrivate; -import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass; - -import java.util.function.Predicate; - -import org.apiguardian.api.API; - -/** - * Test if a class is a potential top-level JUnit Jupiter test container, even if - * it does not contain tests. - * - * @since 5.0 - */ -@API(status = INTERNAL, since = "5.0") -public class IsPotentialTestContainer implements Predicate> { - - @Override - public boolean test(Class candidate) { - // Please do not collapse the following into a single statement. - if (isPrivate(candidate)) { - return false; - } - if (isAbstract(candidate)) { - return false; - } - if (candidate.isLocalClass()) { - return false; - } - if (candidate.isAnonymousClass()) { - return false; - } - return !isInnerClass(candidate); - } - -} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestClassWithTests.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestClassWithTests.java deleted file mode 100644 index 764ef8356067..000000000000 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestClassWithTests.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2015-2025 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.engine.discovery.predicates; - -import static org.apiguardian.api.API.Status.INTERNAL; - -import java.lang.reflect.Method; -import java.util.function.Predicate; - -import org.apiguardian.api.API; -import org.junit.platform.commons.support.ReflectionSupport; -import org.junit.platform.commons.util.ReflectionUtils; - -/** - * Test if a class is a JUnit Jupiter test class containing executable tests, - * test factories, test templates, or nested tests. - * - * @since 5.0 - */ -@API(status = INTERNAL, since = "5.1") -public class IsTestClassWithTests implements Predicate> { - - private static final IsTestMethod isTestMethod = new IsTestMethod(); - - private static final IsTestFactoryMethod isTestFactoryMethod = new IsTestFactoryMethod(); - - private static final IsTestTemplateMethod isTestTemplateMethod = new IsTestTemplateMethod(); - - public static final Predicate isTestOrTestFactoryOrTestTemplateMethod = isTestMethod.or( - isTestFactoryMethod).or(isTestTemplateMethod); - - private static final IsPotentialTestContainer isPotentialTestContainer = new IsPotentialTestContainer(); - - private static final IsNestedTestClass isNestedTestClass = new IsNestedTestClass(); - - @Override - public boolean test(Class candidate) { - return isPotentialTestContainer.test(candidate) - && (hasTestOrTestFactoryOrTestTemplateMethods(candidate) || hasNestedTests(candidate)); - } - - private boolean hasTestOrTestFactoryOrTestTemplateMethods(Class candidate) { - return ReflectionUtils.isMethodPresent(candidate, isTestOrTestFactoryOrTestTemplateMethod); - } - - private boolean hasNestedTests(Class candidate) { - return !ReflectionSupport.findNestedClasses(candidate, isNestedTestClass).isEmpty(); - } - -} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethod.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethod.java index 2932640add1a..aaf90d6db727 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethod.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethod.java @@ -12,8 +12,21 @@ import static org.apiguardian.api.API.Status.INTERNAL; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Iterator; +import java.util.stream.Stream; + import org.apiguardian.api.API; +import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.TestFactory; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; /** * Test if a method is a JUnit Jupiter {@link TestFactory @TestFactory} method. @@ -26,8 +39,76 @@ @API(status = INTERNAL, since = "5.0") public class IsTestFactoryMethod extends IsTestableMethod { - public IsTestFactoryMethod() { - super(TestFactory.class, false); + private static final String EXPECTED_RETURN_TYPE_MESSAGE = String.format( + "must return a single %1$s or a Stream, Collection, Iterable, Iterator, or array of %1$s", + DynamicNode.class.getName()); + + public IsTestFactoryMethod(DiscoveryIssueReporter issueReporter) { + super(TestFactory.class, IsTestFactoryMethod::hasCompatibleReturnType, issueReporter); + } + + private static DiscoveryIssueReporter.Condition hasCompatibleReturnType( + Class annotationType, DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(method -> isCompatible(method, issueReporter), + method -> createIssue(annotationType, method, EXPECTED_RETURN_TYPE_MESSAGE)); + } + + private static boolean isCompatible(Method method, DiscoveryIssueReporter issueReporter) { + Class returnType = method.getReturnType(); + if (DynamicNode.class.isAssignableFrom(returnType) || DynamicNode[].class.isAssignableFrom(returnType)) { + return true; + } + if (returnType == Object.class || returnType == Object[].class) { + issueReporter.reportIssue(createTooGenericReturnTypeIssue(method)); + return true; + } + boolean validContainerType = Stream.class.isAssignableFrom(returnType) // + || Iterable.class.isAssignableFrom(returnType) // + || Iterator.class.isAssignableFrom(returnType); + return validContainerType && isCompatibleContainerType(method, issueReporter); + } + + private static boolean isCompatibleContainerType(Method method, DiscoveryIssueReporter issueReporter) { + Type genericReturnType = method.getGenericReturnType(); + + if (genericReturnType instanceof ParameterizedType) { + Type[] typeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments(); + if (typeArguments.length == 1) { + Type typeArgument = typeArguments[0]; + if (typeArgument instanceof Class) { + // Stream etc. + return DynamicNode.class.isAssignableFrom((Class) typeArgument); + } + if (typeArgument instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) typeArgument; + Type[] upperBounds = wildcardType.getUpperBounds(); + Type[] lowerBounds = wildcardType.getLowerBounds(); + if (upperBounds.length == 1 && lowerBounds.length == 0 && upperBounds[0] instanceof Class) { + Class upperBound = (Class) upperBounds[0]; + if (Object.class.equals(upperBound)) { // Stream etc. + issueReporter.reportIssue(createTooGenericReturnTypeIssue(method)); + return true; + } + // Stream etc. + return DynamicNode.class.isAssignableFrom(upperBound); + } + } + } + return false; + } + + // Raw Stream etc. without type argument + issueReporter.reportIssue(createTooGenericReturnTypeIssue(method)); + return true; + } + + private static DiscoveryIssue.Builder createTooGenericReturnTypeIssue(Method method) { + String message = String.format( + "The declared return type of @TestFactory method '%s' does not support static validation. It " + + EXPECTED_RETURN_TYPE_MESSAGE + ".", + method.toGenericString()); + return DiscoveryIssue.builder(Severity.INFO, message) // + .source(MethodSource.from(method)); } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestMethod.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestMethod.java index 8cbc1b7b6cca..f703140bcdac 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestMethod.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestMethod.java @@ -14,6 +14,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.Test; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; /** * Test if a method is a JUnit Jupiter {@link Test @Test} method. @@ -23,8 +24,8 @@ @API(status = INTERNAL, since = "5.0") public class IsTestMethod extends IsTestableMethod { - public IsTestMethod() { - super(Test.class, true); + public IsTestMethod(DiscoveryIssueReporter issueReporter) { + super(Test.class, IsTestableMethod::hasVoidReturnType, issueReporter); } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestTemplateMethod.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestTemplateMethod.java index fded3a83eab9..61f8cb688679 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestTemplateMethod.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestTemplateMethod.java @@ -14,6 +14,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.TestTemplate; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; /** * Test if a method is a JUnit Jupiter {@link TestTemplate @TestTemplate} method. @@ -23,8 +24,8 @@ @API(status = INTERNAL, since = "5.0") public class IsTestTemplateMethod extends IsTestableMethod { - public IsTestTemplateMethod() { - super(TestTemplate.class, true); + public IsTestTemplateMethod(DiscoveryIssueReporter issueReporter) { + super(TestTemplate.class, IsTestableMethod::hasVoidReturnType, issueReporter); } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java index 7852d382c627..498d04e62992 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestableMethod.java @@ -11,45 +11,77 @@ package org.junit.jupiter.engine.discovery.predicates; import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; -import static org.junit.platform.commons.support.ModifierSupport.isAbstract; -import static org.junit.platform.commons.support.ModifierSupport.isPrivate; -import static org.junit.platform.commons.support.ModifierSupport.isStatic; -import static org.junit.platform.commons.util.ReflectionUtils.returnsPrimitiveVoid; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.function.BiFunction; import java.util.function.Predicate; +import org.junit.platform.commons.support.ModifierSupport; +import org.junit.platform.commons.util.ReflectionUtils; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter.Condition; + /** * @since 5.0 */ abstract class IsTestableMethod implements Predicate { private final Class annotationType; - private final boolean mustReturnPrimitiveVoid; + private final Condition condition; - IsTestableMethod(Class annotationType, boolean mustReturnPrimitiveVoid) { + IsTestableMethod(Class annotationType, + BiFunction, DiscoveryIssueReporter, Condition> returnTypeConditionFactory, + DiscoveryIssueReporter issueReporter) { this.annotationType = annotationType; - this.mustReturnPrimitiveVoid = mustReturnPrimitiveVoid; + this.condition = isNotStatic(annotationType, issueReporter) // + .and(isNotPrivate(annotationType, issueReporter)) // + .and(isNotAbstract(annotationType, issueReporter)) // + .and(returnTypeConditionFactory.apply(annotationType, issueReporter)); } @Override public boolean test(Method candidate) { - // Please do not collapse the following into a single statement. - if (isStatic(candidate)) { - return false; - } - if (isPrivate(candidate)) { - return false; - } - if (isAbstract(candidate)) { - return false; - } - if (returnsPrimitiveVoid(candidate) != this.mustReturnPrimitiveVoid) { - return false; + if (isAnnotated(candidate, this.annotationType)) { + return condition.check(candidate); } + return false; + } + + private static Condition isNotStatic(Class annotationType, + DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(ModifierSupport::isNotStatic, + method -> createIssue(annotationType, method, "must not be static")); + } + + private static Condition isNotPrivate(Class annotationType, + DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(ModifierSupport::isNotPrivate, + method -> createIssue(annotationType, method, "must not be private")); + } + + private static Condition isNotAbstract(Class annotationType, + DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(ModifierSupport::isNotAbstract, + method -> createIssue(annotationType, method, "must not be abstract")); + } + + protected static Condition hasVoidReturnType(Class annotationType, + DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(ReflectionUtils::returnsPrimitiveVoid, + method -> createIssue(annotationType, method, "must not return a value")); + } - return isAnnotated(candidate, this.annotationType); + protected static DiscoveryIssue createIssue(Class annotationType, Method method, + String condition) { + String message = String.format("@%s method '%s' %s. It will not be executed.", annotationType.getSimpleName(), + method.toGenericString(), condition); + return DiscoveryIssue.builder(Severity.WARNING, message) // + .source(MethodSource.from(method)) // + .build(); } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/TestClassPredicates.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/TestClassPredicates.java new file mode 100644 index 000000000000..6dceea4dfb98 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/TestClassPredicates.java @@ -0,0 +1,135 @@ +/* + * Copyright 2015-2025 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.engine.discovery.predicates; + +import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; +import static org.junit.platform.commons.support.ModifierSupport.isAbstract; +import static org.junit.platform.commons.support.ModifierSupport.isNotAbstract; +import static org.junit.platform.commons.support.ModifierSupport.isNotPrivate; +import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass; +import static org.junit.platform.commons.util.ReflectionUtils.isMethodPresent; +import static org.junit.platform.commons.util.ReflectionUtils.isNestedClassPresent; + +import java.lang.reflect.Method; +import java.util.function.Predicate; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.ClassTemplate; +import org.junit.jupiter.api.Nested; +import org.junit.platform.commons.util.ReflectionUtils; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter.Condition; + +/** + * Predicates for determining whether a class is a JUnit Jupiter test class. + * + * @since 5.13 + */ +@API(status = INTERNAL, since = "5.13") +public class TestClassPredicates { + + public final Predicate> isAnnotatedWithNested = testClass -> isAnnotated(testClass, Nested.class); + public final Predicate> isAnnotatedWithClassTemplate = testClass -> isAnnotated(testClass, + ClassTemplate.class); + + public final Predicate> isAnnotatedWithNestedAndValid = candidate -> this.isAnnotatedWithNested.test( + candidate) && isValidNestedTestClass(candidate); + public final Predicate> looksLikeNestedOrStandaloneTestClass = candidate -> this.isAnnotatedWithNested.test( + candidate) || looksLikeIntendedTestClass(candidate); + public final Predicate isTestOrTestFactoryOrTestTemplateMethod; + + private final Condition> isValidNestedTestClass; + private final Condition> isValidStandaloneTestClass; + + public TestClassPredicates(DiscoveryIssueReporter issueReporter) { + this.isTestOrTestFactoryOrTestTemplateMethod = new IsTestMethod(issueReporter) // + .or(new IsTestFactoryMethod(issueReporter)) // + .or(new IsTestTemplateMethod(issueReporter)); + this.isValidNestedTestClass = isNotPrivateUnlessAbstract("@Nested", issueReporter) // + .and(isInner(issueReporter)); + this.isValidStandaloneTestClass = isNotPrivateUnlessAbstract("Test", issueReporter) // + .and(isNotLocal(issueReporter)) // + .and(isNotInner(issueReporter)) // or should be annotated with @Nested! + .and(isNotAnonymous(issueReporter)); + } + + public boolean looksLikeIntendedTestClass(Class candidate) { + return this.isAnnotatedWithClassTemplate.test(candidate) // + || hasTestOrTestFactoryOrTestTemplateMethods(candidate) // + || hasNestedTests(candidate); + } + + public boolean isValidNestedTestClass(Class candidate) { + return this.isValidNestedTestClass.check(candidate) // + && isNotAbstract(candidate); + } + + public boolean isValidStandaloneTestClass(Class candidate) { + return this.isValidStandaloneTestClass.check(candidate) // + && isNotAbstract(candidate); + } + + private boolean hasTestOrTestFactoryOrTestTemplateMethods(Class candidate) { + return isMethodPresent(candidate, this.isTestOrTestFactoryOrTestTemplateMethod); + } + + private boolean hasNestedTests(Class candidate) { + return isNestedClassPresent( // + candidate, // + isNotSame(candidate).and( + this.isAnnotatedWithNested.or(it -> isInnerClass(it) && looksLikeIntendedTestClass(it)))); + } + + private static Predicate> isNotSame(Class candidate) { + return clazz -> candidate != clazz; + } + + private static Condition> isNotPrivateUnlessAbstract(String prefix, DiscoveryIssueReporter issueReporter) { + // Allow abstract test classes to be private because subclasses may widen access. + return issueReporter.createReportingCondition(testClass -> isNotPrivate(testClass) || isAbstract(testClass), + testClass -> createIssue(prefix, testClass, "must not be private")); + } + + private static Condition> isNotLocal(DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(testClass -> !testClass.isLocalClass(), + testClass -> createIssue("Test", testClass, "must not be a local class")); + } + + private static Condition> isInner(DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(ReflectionUtils::isInnerClass, testClass -> { + if (testClass.getEnclosingClass() == null) { + return createIssue("@Nested", testClass, "must not be a top-level class"); + } + return createIssue("@Nested", testClass, "must not be static"); + }); + } + + private static Condition> isNotInner(DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(testClass -> !isInnerClass(testClass), + testClass -> createIssue("Test", testClass, "must not be an inner class unless annotated with @Nested")); + } + + private static Condition> isNotAnonymous(DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(testClass -> !testClass.isAnonymousClass(), + testClass -> createIssue("Test", testClass, "must not be anonymous")); + } + + private static DiscoveryIssue createIssue(String prefix, Class testClass, String detailMessage) { + String message = String.format("%s class '%s' %s. It will not be executed.", prefix, testClass.getName(), + detailMessage); + return DiscoveryIssue.builder(DiscoveryIssue.Severity.WARNING, message) // + .source(ClassSource.from(testClass)) // + .build(); + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java index dc099bea9f44..fab10b76024e 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java @@ -15,6 +15,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.descriptor.LauncherStoreFacade; import org.junit.jupiter.engine.extension.MutableExtensionRegistry; import org.junit.platform.commons.JUnitException; import org.junit.platform.engine.EngineExecutionListener; @@ -33,9 +34,9 @@ public class JupiterEngineExecutionContext implements EngineExecutionContext { private boolean beforeAllCallbacksExecuted = false; private boolean beforeAllMethodsExecuted = false; - public JupiterEngineExecutionContext(EngineExecutionListener executionListener, - JupiterConfiguration configuration) { - this(new State(executionListener, configuration)); + public JupiterEngineExecutionContext(EngineExecutionListener executionListener, JupiterConfiguration configuration, + LauncherStoreFacade launcherStoreFacade) { + this(new State(executionListener, configuration, launcherStoreFacade)); } private JupiterEngineExecutionContext(State state) { @@ -62,6 +63,10 @@ public JupiterConfiguration getConfiguration() { return this.state.configuration; } + public LauncherStoreFacade getLauncherStoreFacade() { + return this.state.launcherStoreFacade; + } + public TestInstancesProvider getTestInstancesProvider() { return this.state.testInstancesProvider; } @@ -119,14 +124,17 @@ private static final class State implements Cloneable { final EngineExecutionListener executionListener; final JupiterConfiguration configuration; + final LauncherStoreFacade launcherStoreFacade; TestInstancesProvider testInstancesProvider; MutableExtensionRegistry extensionRegistry; ExtensionContext extensionContext; ThrowableCollector throwableCollector; - State(EngineExecutionListener executionListener, JupiterConfiguration configuration) { + State(EngineExecutionListener executionListener, JupiterConfiguration configuration, + LauncherStoreFacade launcherStoreFacade) { this.executionListener = executionListener; this.configuration = configuration; + this.launcherStoreFacade = launcherStoreFacade; } @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java index a39b4a189474..d63c7f13a940 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java @@ -16,10 +16,10 @@ import java.util.function.Supplier; import org.apiguardian.api.API; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ExtensionContextException; import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.engine.support.store.NamespacedHierarchicalStoreException; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java index 5349499d4f09..d619da57f849 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java @@ -53,7 +53,7 @@ import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; +import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.io.CleanupMode; @@ -129,12 +129,13 @@ public void beforeEach(ExtensionContext context) { } private static void installFailureTracker(ExtensionContext context) { - context.getStore(NAMESPACE).put(FAILURE_TRACKER, (CloseableResource) () -> context.getParent() // - .ifPresent(parentContext -> { - if (selfOrChildFailed(context)) { - parentContext.getStore(NAMESPACE).put(CHILD_FAILED, true); - } - })); + context.getParent() // + .filter(parentContext -> !context.getRoot().equals(parentContext)) // + .ifPresent(parentContext -> installFailureTracker(context, parentContext)); + } + + private static void installFailureTracker(ExtensionContext context, ExtensionContext parentContext) { + context.getStore(NAMESPACE).put(FAILURE_TRACKER, new FailureTracker(context, parentContext)); } private void injectStaticFields(ExtensionContext context, Class testClass) { @@ -286,10 +287,15 @@ static CloseablePath createTempDir(TempDirFactory factory, CleanupMode cleanupMo private static boolean selfOrChildFailed(ExtensionContext context) { return context.getExecutionException().isPresent() // - || context.getStore(NAMESPACE).getOrDefault(CHILD_FAILED, Boolean.class, false); + || getContextSpecificStore(context).getOrDefault(CHILD_FAILED, Boolean.class, false); + } + + private static ExtensionContext.Store getContextSpecificStore(ExtensionContext context) { + return context.getStore(NAMESPACE.append(context)); } - static class CloseablePath implements CloseableResource { + @SuppressWarnings("deprecation") + static class CloseablePath implements Store.CloseableResource, AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(CloseablePath.class); @@ -604,4 +610,23 @@ public String toString() { } + @SuppressWarnings("deprecation") + private static class FailureTracker implements Store.CloseableResource, AutoCloseable { + + private final ExtensionContext context; + private final ExtensionContext parentContext; + + private FailureTracker(ExtensionContext context, ExtensionContext parentContext) { + this.context = context; + this.parentContext = parentContext; + } + + @Override + public void close() { + if (selfOrChildFailed(context)) { + getContextSpecificStore(parentContext).put(CHILD_FAILED, true); + } + } + } + } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java index 5e9b04c18ef8..7843c012711b 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java @@ -17,7 +17,6 @@ import org.junit.jupiter.api.Timeout.ThreadMode; import org.junit.jupiter.api.extension.ExtensionContext.Store; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.Preconditions; @@ -52,7 +51,8 @@ private ScheduledExecutorService getThreadExecutorForSameThreadInvocation() { return store.getOrComputeIfAbsent(SingleThreadExecutorResource.class).get(); } - private static abstract class ExecutorResource implements CloseableResource { + @SuppressWarnings({ "deprecation", "try" }) + private static abstract class ExecutorResource implements Store.CloseableResource, AutoCloseable { protected final ScheduledExecutorService executor; @@ -65,7 +65,7 @@ ScheduledExecutorService get() { } @Override - public void close() throws Throwable { + public void close() throws Exception { executor.shutdown(); boolean terminated = executor.awaitTermination(5, TimeUnit.SECONDS); if (!terminated) { @@ -75,6 +75,7 @@ public void close() throws Throwable { } } + @SuppressWarnings("try") static class SingleThreadExecutorResource extends ExecutorResource { @SuppressWarnings("unused") diff --git a/junit-jupiter-engine/src/nativeImage/initialize-at-build-time b/junit-jupiter-engine/src/nativeImage/initialize-at-build-time deleted file mode 100644 index 4e405ef9656f..000000000000 --- a/junit-jupiter-engine/src/nativeImage/initialize-at-build-time +++ /dev/null @@ -1,24 +0,0 @@ -org.junit.jupiter.engine.JupiterTestEngine -org.junit.jupiter.engine.config.CachingJupiterConfiguration -org.junit.jupiter.engine.config.DefaultJupiterConfiguration -org.junit.jupiter.engine.config.EnumConfigurationParameterConverter -org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter -org.junit.jupiter.engine.descriptor.ClassTestDescriptor -org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor -org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$ClassInfo -org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$LifecycleMethods -org.junit.jupiter.engine.descriptor.DynamicDescendantFilter -org.junit.jupiter.engine.descriptor.ExclusiveResourceCollector$1 -org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor -org.junit.jupiter.engine.descriptor.JupiterTestDescriptor -org.junit.jupiter.engine.descriptor.JupiterTestDescriptor$1 -org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor -org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor -org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor -org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor -org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor -org.junit.jupiter.engine.execution.ConditionEvaluator -org.junit.jupiter.engine.execution.InterceptingExecutableInvoker -org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall -org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall$VoidMethodInterceptorCall -org.junit.jupiter.engine.execution.InvocationInterceptorChain diff --git a/junit-jupiter-engine/src/testFixtures/java/org/junit/jupiter/engine/discovery/JupiterUniqueIdBuilder.java b/junit-jupiter-engine/src/testFixtures/java/org/junit/jupiter/engine/discovery/JupiterUniqueIdBuilder.java index d7f6e252e096..d504f3605d65 100644 --- a/junit-jupiter-engine/src/testFixtures/java/org/junit/jupiter/engine/discovery/JupiterUniqueIdBuilder.java +++ b/junit-jupiter-engine/src/testFixtures/java/org/junit/jupiter/engine/discovery/JupiterUniqueIdBuilder.java @@ -51,7 +51,7 @@ public static UniqueId uniqueIdForStaticClass(String className) { private static String staticClassSegmentType(String className) { return ReflectionSupport.tryToLoadClass(className).toOptional() // .map(it -> classSegmentType(it, ClassTestDescriptor.SEGMENT_TYPE, - ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE)) // + ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE)) // .orElse(ClassTestDescriptor.SEGMENT_TYPE); } diff --git a/junit-jupiter-params/junit-jupiter-params.gradle.kts b/junit-jupiter-params/junit-jupiter-params.gradle.kts index e481fdd13674..80ade03f4b27 100644 --- a/junit-jupiter-params/junit-jupiter-params.gradle.kts +++ b/junit-jupiter-params/junit-jupiter-params.gradle.kts @@ -2,7 +2,7 @@ plugins { id("junitbuild.kotlin-library-conventions") id("junitbuild.shadow-conventions") id("junitbuild.jmh-conventions") - id("junitbuild.native-image-properties") + `java-test-fixtures` } description = "JUnit Jupiter Params" diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeParameterizedClassInvocation.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeParameterizedClassInvocation.java index a71573eb1897..1391fd5a240e 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeParameterizedClassInvocation.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeParameterizedClassInvocation.java @@ -164,7 +164,7 @@ /** * Whether the arguments of the parameterized test class should be injected - * into the annotated method (defaults to {@code false}). + * into the annotated method (defaults to {@code true}). */ boolean injectArguments() default true; diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java index 207817c69ebb..e310abb94843 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java @@ -72,11 +72,12 @@ private void storeParameterInfo(ExtensionContext context) { ParameterDeclarations declarations = this.declarationContext.getResolverFacade().getIndexedParameterDeclarations(); ClassLoader classLoader = getClassLoader(this.declarationContext.getTestClass()); Object[] arguments = this.arguments.getConsumedPayloads(); - ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments); + ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments); new DefaultParameterInfo(declarations, accessor).store(context); } - private static class CloseableArgument implements ExtensionContext.Store.CloseableResource { + @SuppressWarnings({ "deprecation", "try" }) + private static class CloseableArgument implements ExtensionContext.Store.CloseableResource, AutoCloseable { private final AutoCloseable autoCloseable; @@ -85,7 +86,7 @@ private static class CloseableArgument implements ExtensionContext.Store.Closeab } @Override - public void close() throws Throwable { + public void close() throws Exception { this.autoCloseable.close(); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContextProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContextProvider.java index ffa324c13df6..77e5a98059b7 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContextProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContextProvider.java @@ -17,6 +17,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TemplateInvocationValidationException; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -47,12 +48,20 @@ protected Stream provideInvocationContexts(ExtensionContext extensionContext, invocationCount.incrementAndGet(); return declarationContext.createInvocationContext(formatter, arguments, invocationCount.intValue()); }) - .onClose(() -> - Preconditions.condition(invocationCount.get() > 0 || declarationContext.isAllowingZeroInvocations(), - () -> String.format("Configuration error: You must configure at least one set of arguments for this @%s", declarationContext.getAnnotationName()))); + .onClose(() -> validateInvokedAtLeastOnce(invocationCount.get(),declarationContext )); // @formatter:on } + private static void validateInvokedAtLeastOnce(long invocationCount, + ParameterizedDeclarationContext declarationContext) { + if (invocationCount == 0 && !declarationContext.isAllowingZeroInvocations()) { + String message = String.format( + "Configuration error: You must configure at least one set of arguments for this @%s", + declarationContext.getAnnotationName()); + throw new TemplateInvocationValidationException(message); + } + } + private static List collectArgumentSources(ParameterizedDeclarationContext declarationContext) { List argumentsSources = findRepeatableAnnotations(declarationContext.getAnnotatedElement(), ArgumentsSource.class); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java index 28eda4b8186a..a1cad8e98419 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java @@ -425,7 +425,7 @@ private static Converter createConverter(ParameterDeclaration declaration, Exten .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentConverter.class, clazz, extensionContext)) .map(converter -> AnnotationConsumerInitializer.initialize(declaration.getAnnotatedElement(), converter)) .map(Converter::new) - .orElse(Converter.DEFAULT); + .orElseGet(() -> Converter.createDefault(extensionContext)); } // @formatter:on catch (Exception ex) { throw parameterResolutionException("Error creating ArgumentConverter", ex, declaration.getParameterIndex()); @@ -467,10 +467,12 @@ Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, Eva private static class Converter implements Resolver { - private static final Converter DEFAULT = new Converter(DefaultArgumentConverter.INSTANCE); - private final ArgumentConverter argumentConverter; + private static Converter createDefault(ExtensionContext context) { + return new Converter(new DefaultArgumentConverter(context)); + } + Converter(ArgumentConverter argumentConverter) { this.argumentConverter = argumentConverter; } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java index 811a8abd0518..40bf7213e1c7 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java @@ -19,6 +19,7 @@ import java.util.function.BiFunction; import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.converter.DefaultArgumentConverter; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.commons.util.Preconditions; @@ -40,10 +41,11 @@ public class DefaultArgumentsAccessor implements ArgumentsAccessor { private final Object[] arguments; private final BiFunction, Object> converter; - public static DefaultArgumentsAccessor create(int invocationIndex, ClassLoader classLoader, Object[] arguments) { + public static DefaultArgumentsAccessor create(ExtensionContext context, int invocationIndex, + ClassLoader classLoader, Object[] arguments) { Preconditions.notNull(classLoader, "ClassLoader must not be null"); - BiFunction, Object> converter = (source, targetType) -> DefaultArgumentConverter.INSTANCE // + BiFunction, Object> converter = (source, targetType) -> new DefaultArgumentConverter(context) // .convert(source, targetType, classLoader); return new DefaultArgumentsAccessor(converter, invocationIndex, arguments); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java index 8544019c1894..eea0e734508a 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java @@ -21,8 +21,10 @@ import java.util.Currency; import java.util.Locale; import java.util.UUID; +import java.util.function.Function; import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.support.conversion.ConversionException; @@ -50,10 +52,31 @@ @API(status = INTERNAL, since = "5.0") public class DefaultArgumentConverter implements ArgumentConverter { - public static final DefaultArgumentConverter INSTANCE = new DefaultArgumentConverter(); + /** + * Property name used to set the format for the conversion of {@link Locale} + * arguments: {@value} + * + *

    Supported Values

    + *
      + *
    • {@code bcp_47}: uses the IETF BCP 47 language tag format, delegating + * the conversion to {@link Locale#forLanguageTag(String)}
    • + *
    • {@code iso_639}: uses the ISO 639 alpha-2 or alpha-3 language code + * format, delegating the conversion to {@link Locale#Locale(String)}
    • + *
    + * + *

    If not specified, the default is {@code bcp_47}. + * + * @since 5.13 + */ + public static final String DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME = "junit.jupiter.params.arguments.conversion.locale.format"; - private DefaultArgumentConverter() { - // nothing to initialize + private static final Function TRANSFORMER = value -> LocaleConversionFormat.valueOf( + value.trim().toUpperCase(Locale.ROOT)); + + private final ExtensionContext context; + + public DefaultArgumentConverter(ExtensionContext context) { + this.context = context; } @Override @@ -84,6 +107,10 @@ public final Object convert(Object source, Class targetType, ClassLoader clas } if (source instanceof String) { + if (targetType == Locale.class && getLocaleConversionFormat() == LocaleConversionFormat.BCP_47) { + return Locale.forLanguageTag((String) source); + } + try { return convert((String) source, targetType, classLoader); } @@ -97,8 +124,21 @@ public final Object convert(Object source, Class targetType, ClassLoader clas source.getClass().getTypeName(), targetType.getTypeName())); } + private LocaleConversionFormat getLocaleConversionFormat() { + return context.getConfigurationParameter(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, TRANSFORMER) // + .orElse(LocaleConversionFormat.BCP_47); + } + Object convert(String source, Class targetType, ClassLoader classLoader) { return ConversionSupport.convert(source, targetType, classLoader); } + enum LocaleConversionFormat { + + BCP_47, + + ISO_639 + + } + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java index 575d8dd59463..ee9a2747a6f1 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java @@ -76,6 +76,13 @@ protected Stream provideArguments(ExtensionContext context, getClass().getName())); } + /** + * The returned {@code Stream} will be {@link Stream#close() properly closed} + * by the default implementation of + * {@link #provideArguments(ParameterDeclarations, ExtensionContext)}, + * making it safe to use a resource such as + * {@link java.nio.file.Files#lines(java.nio.file.Path) Files.lines()}. + */ protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, A annotation) { return provideArguments(context, annotation); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java index a84e040ee4c9..4ca4717fd4d7 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java @@ -61,6 +61,12 @@ * use one of these types, you can wrap it in a {@code Supplier} — for * example, {@code Supplier}. * + *

    If the {@code Supplier} return type is {@code Stream} or + * one of the primitive streams, JUnit will properly close it by calling + * {@link java.util.stream.BaseStream#close() BaseStream.close()}, + * making it safe to use a resource such as + * {@link java.nio.file.Files#lines(java.nio.file.Path) Files.lines()}. + * *

    Please note that a one-dimensional array of objects supplied as a set of * "arguments" will be handled differently than other types of arguments. * Specifically, all of the elements of a one-dimensional array of objects will diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java index c7685a58f9a5..aec745a5188b 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java @@ -50,6 +50,12 @@ * {@code String[]}, etc.), or a single value if the parameterized test * method accepts a single argument. * + *

    If the return type is {@code Stream} or + * one of the primitive streams, JUnit will properly close it by calling + * {@link java.util.stream.BaseStream#close() BaseStream.close()}, + * making it safe to use a resource such as + * {@link java.nio.file.Files#lines(java.nio.file.Path) Files.lines()}. + * *

    Please note that a one-dimensional array of objects supplied as a set of * "arguments" will be handled differently than other types of arguments. * Specifically, all of the elements of a one-dimensional array of objects will diff --git a/junit-jupiter-params/src/nativeImage/initialize-at-build-time b/junit-jupiter-params/src/nativeImage/initialize-at-build-time deleted file mode 100644 index 44ca7ffbd8ad..000000000000 --- a/junit-jupiter-params/src/nativeImage/initialize-at-build-time +++ /dev/null @@ -1,2 +0,0 @@ -org.junit.jupiter.params.provider.EnumSource$Mode -org.junit.jupiter.params.provider.EnumSource$Mode$Validator diff --git a/junit-jupiter-params/src/testFixtures/java/org/junit/jupiter/params/provider/RecordArguments.java b/junit-jupiter-params/src/testFixtures/java/org/junit/jupiter/params/provider/RecordArguments.java new file mode 100644 index 000000000000..96213c64e57e --- /dev/null +++ b/junit-jupiter-params/src/testFixtures/java/org/junit/jupiter/params/provider/RecordArguments.java @@ -0,0 +1,26 @@ +/* + * Copyright 2015-2025 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.params.provider; + +import java.util.Arrays; + +import org.junit.platform.commons.support.ReflectionSupport; + +public interface RecordArguments extends Arguments { + + @Override + default Object[] get() { + return Arrays.stream(getClass().getRecordComponents()) // + .map(component -> ReflectionSupport.invokeMethod(component.getAccessor(), this)) // + .toArray(); + } + +} diff --git a/junit-platform-commons/junit-platform-commons.gradle.kts b/junit-platform-commons/junit-platform-commons.gradle.kts index 3de45a7edfee..3465b0078020 100644 --- a/junit-platform-commons/junit-platform-commons.gradle.kts +++ b/junit-platform-commons/junit-platform-commons.gradle.kts @@ -3,7 +3,6 @@ import junitbuild.java.UpdateJarAction plugins { id("junitbuild.java-library-conventions") id("junitbuild.java-multi-release-sources") - id("junitbuild.native-image-properties") `java-test-fixtures` } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ModifierSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ModifierSupport.java index 21302c9f24b0..e92289ecc05e 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ModifierSupport.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ModifierSupport.java @@ -10,6 +10,7 @@ package org.junit.platform.commons.support; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import java.lang.reflect.Member; @@ -142,6 +143,32 @@ public static boolean isAbstract(Member member) { return ReflectionUtils.isAbstract(member); } + /** + * Determine if the supplied class is not {@code abstract}. + * + * @param clazz the class to check; never {@code null} + * @return {@code true} if the class is not {@code abstract} + * @since 1.13 + * @see java.lang.reflect.Modifier#isAbstract(int) + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static boolean isNotAbstract(Class clazz) { + return ReflectionUtils.isNotAbstract(clazz); + } + + /** + * Determine if the supplied member is not {@code abstract}. + * + * @param member the class to check; never {@code null} + * @return {@code true} if the member is not {@code abstract} + * @since 1.13 + * @see java.lang.reflect.Modifier#isAbstract(int) + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static boolean isNotAbstract(Member member) { + return ReflectionUtils.isNotAbstract(member); + } + /** * Determine if the supplied class is {@code static}. * 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 01f0e89d0fd2..f122e8c77958 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 @@ -13,7 +13,6 @@ import static java.lang.String.format; import static java.util.Collections.synchronizedMap; import static java.util.stream.Collectors.toCollection; -import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.apiguardian.api.API.Status.DEPRECATED; import static org.apiguardian.api.API.Status.INTERNAL; @@ -44,6 +43,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.LinkedHashSet; @@ -289,11 +289,21 @@ public static boolean isAbstract(Class clazz) { return Modifier.isAbstract(clazz.getModifiers()); } + @API(status = INTERNAL, since = "1.13") + public static boolean isNotAbstract(Class clazz) { + return !isAbstract(clazz); + } + public static boolean isAbstract(Member member) { Preconditions.notNull(member, "Member must not be null"); return Modifier.isAbstract(member.getModifiers()); } + @API(status = INTERNAL, since = "1.13") + public static boolean isNotAbstract(Member member) { + return !isAbstract(member); + } + public static boolean isStatic(Class clazz) { Preconditions.notNull(clazz, "Class must not be null"); return Modifier.isStatic(clazz.getModifiers()); @@ -1226,10 +1236,35 @@ public static List> findNestedClasses(Class clazz, Predicate> candidates = new LinkedHashSet<>(); - findNestedClasses(clazz, predicate, candidates); + visitNestedClasses(clazz, predicate, nestedClass -> { + candidates.add(nestedClass); + return true; + }); return Collections.unmodifiableList(new ArrayList<>(candidates)); } + /** + * Determine if a nested class within the supplied class, or inherited by the + * supplied class, that conforms to the supplied predicate is present. + * + *

    This method does not search for nested classes + * recursively. + * + * @param clazz the class to be searched; never {@code null} + * @param predicate the predicate against which the list of nested classes is + * checked; never {@code null} + * @return {@code true} if such a nested class is present + * @throws JUnitException if a cycle is detected within an inner class hierarchy + */ + @API(status = INTERNAL, since = "1.13") + public static boolean isNestedClassPresent(Class clazz, Predicate> predicate) { + Preconditions.notNull(clazz, "Class must not be null"); + Preconditions.notNull(predicate, "Predicate must not be null"); + + boolean visitorWasNotCalled = visitNestedClasses(clazz, predicate, __ -> false); + return !visitorWasNotCalled; + } + /** * since 1.10 * @see org.junit.platform.commons.support.ReflectionSupport#streamNestedClasses(Class, Predicate) @@ -1238,9 +1273,10 @@ public static Stream> streamNestedClasses(Class clazz, Predicate clazz, Predicate> predicate, Set> candidates) { + private static boolean visitNestedClasses(Class clazz, Predicate> predicate, + Visitor> visitor) { if (!isSearchable(clazz)) { - return; + return true; } if (isInnerClass(clazz) && predicate.test(clazz)) { @@ -1252,7 +1288,10 @@ private static void findNestedClasses(Class clazz, Predicate> predic for (Class nestedClass : clazz.getDeclaredClasses()) { if (predicate.test(nestedClass)) { detectInnerClassCycle(nestedClass); - candidates.add(nestedClass); + boolean shouldContinue = visitor.accept(nestedClass); + if (!shouldContinue) { + return false; + } } } } @@ -1261,12 +1300,20 @@ private static void findNestedClasses(Class clazz, Predicate> predic } // Search class hierarchy - findNestedClasses(clazz.getSuperclass(), predicate, candidates); + boolean shouldContinue = visitNestedClasses(clazz.getSuperclass(), predicate, visitor); + if (!shouldContinue) { + return false; + } // Search interface hierarchy for (Class ifc : clazz.getInterfaces()) { - findNestedClasses(ifc, predicate, candidates); + shouldContinue = visitNestedClasses(ifc, predicate, visitor); + if (!shouldContinue) { + return false; + } } + + return true; } /** @@ -1320,14 +1367,14 @@ private static void detectInnerClassCycle(Class clazz) { public static Constructor getDeclaredConstructor(Class clazz) { Preconditions.notNull(clazz, "Class must not be null"); try { - List> constructors = Arrays.stream(clazz.getDeclaredConstructors())// + Constructor[] constructors = Arrays.stream(clazz.getDeclaredConstructors())// .filter(ctor -> !ctor.isSynthetic())// - .collect(toList()); + .toArray(Constructor[]::new); - Preconditions.condition(constructors.size() == 1, + Preconditions.condition(constructors.length == 1, () -> String.format("Class [%s] must declare a single constructor", clazz.getName())); - return (Constructor) constructors.get(0); + return (Constructor) constructors[0]; } catch (Throwable t) { throw ExceptionUtils.throwAsUncheckedException(getUnderlyingCause(t)); @@ -1397,26 +1444,26 @@ private static List findAllFieldsInHierarchy(Class clazz, HierarchyTra Preconditions.notNull(traversalMode, "HierarchyTraversalMode must not be null"); // @formatter:off - List localFields = getDeclaredFields(clazz).stream() + Field[] localFields = getDeclaredFields(clazz).stream() .filter(field -> !field.isSynthetic()) - .collect(toList()); - List superclassFields = getSuperclassFields(clazz, traversalMode).stream() - .filter(field -> !isFieldShadowedByLocalFields(field, localFields)) - .collect(toList()); - List interfaceFields = getInterfaceFields(clazz, traversalMode).stream() - .filter(field -> !isFieldShadowedByLocalFields(field, localFields)) - .collect(toList()); + .toArray(Field[]::new); + Field[] superclassFields = getSuperclassFields(clazz, traversalMode).stream() + .filter(field -> isNotShadowedByLocalFields(field, localFields)) + .toArray(Field[]::new); + Field[] interfaceFields = getInterfaceFields(clazz, traversalMode).stream() + .filter(field -> isNotShadowedByLocalFields(field, localFields)) + .toArray(Field[]::new); // @formatter:on - List fields = new ArrayList<>(); + List fields = new ArrayList<>(superclassFields.length + interfaceFields.length + localFields.length); if (traversalMode == TOP_DOWN) { - fields.addAll(superclassFields); - fields.addAll(interfaceFields); + Collections.addAll(fields, superclassFields); + Collections.addAll(fields, interfaceFields); } - fields.addAll(localFields); + Collections.addAll(fields, localFields); if (traversalMode == BOTTOM_UP) { - fields.addAll(interfaceFields); - fields.addAll(superclassFields); + Collections.addAll(fields, interfaceFields); + Collections.addAll(fields, superclassFields); } return fields; } @@ -1688,26 +1735,27 @@ private static List findAllMethodsInHierarchy(Class clazz, HierarchyT Preconditions.notNull(traversalMode, "HierarchyTraversalMode must not be null"); // @formatter:off - List localMethods = getDeclaredMethods(clazz, traversalMode).stream() + Method[] localMethods = getDeclaredMethods(clazz, traversalMode).stream() .filter(method -> !method.isSynthetic()) - .collect(toList()); - List superclassMethods = getSuperclassMethods(clazz, traversalMode).stream() - .filter(method -> !isMethodOverriddenByLocalMethods(method, localMethods)) - .collect(toList()); - List interfaceMethods = getInterfaceMethods(clazz, traversalMode).stream() - .filter(method -> !isMethodOverriddenByLocalMethods(method, localMethods)) - .collect(toList()); + .toArray(Method[]::new); + Method[] superclassMethods = getSuperclassMethods(clazz, traversalMode).stream() + .filter(method -> isNotOverriddenByLocalMethods(method, localMethods)) + .toArray(Method[]::new); + Method[] interfaceMethods = getInterfaceMethods(clazz, traversalMode).stream() + .filter(method -> isNotOverriddenByLocalMethods(method, localMethods)) + .toArray(Method[]::new); // @formatter:on - List methods = new ArrayList<>(); + List methods = new ArrayList<>( + superclassMethods.length + interfaceMethods.length + localMethods.length); if (traversalMode == TOP_DOWN) { - methods.addAll(superclassMethods); - methods.addAll(interfaceMethods); + Collections.addAll(methods, superclassMethods); + Collections.addAll(methods, interfaceMethods); } - methods.addAll(localMethods); + Collections.addAll(methods, localMethods); if (traversalMode == BOTTOM_UP) { - methods.addAll(interfaceMethods); - methods.addAll(superclassMethods); + Collections.addAll(methods, interfaceMethods); + Collections.addAll(methods, superclassMethods); } return methods; } @@ -1788,21 +1836,18 @@ private static List getDefaultMethods(Class clazz) { } private static List toSortedMutableList(Field[] fields) { - // @formatter:off - return Arrays.stream(fields) - .sorted(ReflectionUtils::defaultFieldSorter) - // Use toCollection() instead of toList() to ensure list is mutable. - .collect(toCollection(ArrayList::new)); - // @formatter:on + return toSortedMutableList(fields, ReflectionUtils::defaultFieldSorter); } private static List toSortedMutableList(Method[] methods) { - // @formatter:off - return Arrays.stream(methods) - .sorted(ReflectionUtils::defaultMethodSorter) - // Use toCollection() instead of toList() to ensure list is mutable. - .collect(toCollection(ArrayList::new)); - // @formatter:on + return toSortedMutableList(methods, ReflectionUtils::defaultMethodSorter); + } + + private static List toSortedMutableList(T[] items, Comparator comparator) { + List result = new ArrayList<>(items.length); + Collections.addAll(result, items); + result.sort(comparator); + return result; } /** @@ -1835,21 +1880,21 @@ private static List getInterfaceMethods(Class clazz, HierarchyTravers for (Class ifc : clazz.getInterfaces()) { // @formatter:off - List localInterfaceMethods = getMethods(ifc).stream() + Method[] localInterfaceMethods = getMethods(ifc).stream() .filter(m -> !isAbstract(m)) - .collect(toList()); + .toArray(Method[]::new); - List superinterfaceMethods = getInterfaceMethods(ifc, traversalMode).stream() - .filter(method -> !isMethodOverriddenByLocalMethods(method, localInterfaceMethods)) - .collect(toList()); + Method[] superinterfaceMethods = getInterfaceMethods(ifc, traversalMode).stream() + .filter(method -> isNotOverriddenByLocalMethods(method, localInterfaceMethods)) + .toArray(Method[]::new); // @formatter:on if (traversalMode == TOP_DOWN) { - allInterfaceMethods.addAll(superinterfaceMethods); + Collections.addAll(allInterfaceMethods, superinterfaceMethods); } - allInterfaceMethods.addAll(localInterfaceMethods); + Collections.addAll(allInterfaceMethods, localInterfaceMethods); if (traversalMode == BOTTOM_UP) { - allInterfaceMethods.addAll(superinterfaceMethods); + Collections.addAll(allInterfaceMethods, superinterfaceMethods); } } return allInterfaceMethods; @@ -1858,20 +1903,21 @@ private static List getInterfaceMethods(Class clazz, HierarchyTravers private static List getInterfaceFields(Class clazz, HierarchyTraversalMode traversalMode) { List allInterfaceFields = new ArrayList<>(); for (Class ifc : clazz.getInterfaces()) { - List localInterfaceFields = getFields(ifc); + Field[] localInterfaceFields = ifc.getFields(); + Arrays.sort(localInterfaceFields, ReflectionUtils::defaultFieldSorter); // @formatter:off - List superinterfaceFields = getInterfaceFields(ifc, traversalMode).stream() - .filter(field -> !isFieldShadowedByLocalFields(field, localInterfaceFields)) - .collect(toList()); + Field[] superinterfaceFields = getInterfaceFields(ifc, traversalMode).stream() + .filter(field -> isNotShadowedByLocalFields(field, localInterfaceFields)) + .toArray(Field[]::new); // @formatter:on if (traversalMode == TOP_DOWN) { - allInterfaceFields.addAll(superinterfaceFields); + Collections.addAll(allInterfaceFields, superinterfaceFields); } - allInterfaceFields.addAll(localInterfaceFields); + Collections.addAll(allInterfaceFields, localInterfaceFields); if (traversalMode == BOTTOM_UP) { - allInterfaceFields.addAll(superinterfaceFields); + Collections.addAll(allInterfaceFields, superinterfaceFields); } } return allInterfaceFields; @@ -1885,11 +1931,16 @@ private static List getSuperclassFields(Class clazz, HierarchyTraversa return findAllFieldsInHierarchy(superclass, traversalMode); } - private static boolean isFieldShadowedByLocalFields(Field field, List localFields) { + private static boolean isNotShadowedByLocalFields(Field field, Field[] localFields) { if (useLegacySearchSemantics) { - return localFields.stream().anyMatch(local -> local.getName().equals(field.getName())); + for (Field local : localFields) { + if (local.getName().equals(field.getName())) { + return false; + } + } + return true; } - return false; + return true; } private static List getSuperclassMethods(Class clazz, HierarchyTraversalMode traversalMode) { @@ -1900,8 +1951,13 @@ private static List getSuperclassMethods(Class clazz, HierarchyTraver return findAllMethodsInHierarchy(superclass, traversalMode); } - private static boolean isMethodOverriddenByLocalMethods(Method method, List localMethods) { - return localMethods.stream().anyMatch(local -> isMethodOverriddenBy(method, local)); + private static boolean isNotOverriddenByLocalMethods(Method method, Method[] localMethods) { + for (Method local : localMethods) { + if (isMethodOverriddenBy(method, local)) { + return false; + } + } + return true; } private static boolean isMethodOverriddenBy(Method upper, Method lower) { @@ -2063,4 +2119,14 @@ private static boolean getLegacySearchSemanticsFlag() { return isTrue; } + private interface Visitor { + + /** + * @return {@code true} if the visitor should continue searching; + * {@code false} if the visitor should stop + */ + boolean accept(T value); + + } + } diff --git a/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java b/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java index 176b5fd5c805..bb545c577f82 100644 --- a/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java +++ b/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java @@ -45,6 +45,7 @@ org.junit.jupiter.params, org.junit.platform.console, org.junit.platform.engine, + org.junit.platform.jfr, org.junit.platform.launcher, org.junit.platform.reporting, org.junit.platform.runner, diff --git a/junit-platform-commons/src/nativeImage/initialize-at-build-time b/junit-platform-commons/src/nativeImage/initialize-at-build-time deleted file mode 100644 index a6c384232123..000000000000 --- a/junit-platform-commons/src/nativeImage/initialize-at-build-time +++ /dev/null @@ -1,5 +0,0 @@ -org.junit.platform.commons.util.StringUtils -org.junit.platform.commons.logging.LoggerFactory$DelegatingLogger -org.junit.platform.commons.logging.LoggerFactory -org.junit.platform.commons.util.ReflectionUtils -org.junit.platform.commons.util.LruCache diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/ConsoleLauncher.java b/junit-platform-console/src/main/java/org/junit/platform/console/ConsoleLauncher.java index 240d6216f61e..929ec8c035fe 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/ConsoleLauncher.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/ConsoleLauncher.java @@ -19,6 +19,7 @@ import org.junit.platform.console.options.CommandFacade; import org.junit.platform.console.options.CommandResult; import org.junit.platform.console.tasks.ConsoleTestExecutor; +import org.junit.platform.console.tasks.CustomClassLoaderCloseStrategy; /** * The {@code ConsoleLauncher} is a stand-alone application for launching the @@ -30,17 +31,20 @@ public class ConsoleLauncher { public static void main(String... args) { - CommandResult result = newCommandFacade().run(args); + CommandFacade facade = newCommandFacade(CustomClassLoaderCloseStrategy.KEEP_OPEN); + CommandResult result = facade.run(args); System.exit(result.getExitCode()); } @API(status = INTERNAL, since = "1.0") public static CommandResult run(PrintWriter out, PrintWriter err, String... args) { - return newCommandFacade().run(args, out, err); + CommandFacade facade = newCommandFacade(CustomClassLoaderCloseStrategy.CLOSE_AFTER_CALLING_LAUNCHER); + return facade.run(args, out, err); } - private static CommandFacade newCommandFacade() { - return new CommandFacade(ConsoleTestExecutor::new); + private static CommandFacade newCommandFacade(CustomClassLoaderCloseStrategy classLoaderCleanupStrategy) { + return new CommandFacade((discoveryOptions, outputOptions) -> new ConsoleTestExecutor(discoveryOptions, + outputOptions, classLoaderCleanupStrategy)); } } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java index 1d1404650f23..7d423f17c9bd 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java @@ -42,7 +42,7 @@ class TestDiscoveryOptionsMixin { SelectorOptions selectorOptions; @ArgGroup(validate = false, order = 3, heading = "%n For more information on selectors including syntax examples, see" - + "%n @|underline https://junit.org/junit5/docs/current/user-guide/#running-tests-discovery-selectors|@" + + "%n @|underline https://junit.org/junit5/docs/${junit.docs.version}/user-guide/#running-tests-discovery-selectors|@" + "%n%n@|bold FILTERS|@%n%n") FilterOptions filterOptions; diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java index ed08f30b5adc..eb0f3874a8e3 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java @@ -50,31 +50,48 @@ public class ConsoleTestExecutor { private final TestDiscoveryOptions discoveryOptions; private final TestConsoleOutputOptions outputOptions; private final Supplier launcherSupplier; + private final CustomClassLoaderCloseStrategy classLoaderCloseStrategy; public ConsoleTestExecutor(TestDiscoveryOptions discoveryOptions, TestConsoleOutputOptions outputOptions) { - this(discoveryOptions, outputOptions, LauncherFactory::create); + this(discoveryOptions, outputOptions, CustomClassLoaderCloseStrategy.CLOSE_AFTER_CALLING_LAUNCHER); + } + + public ConsoleTestExecutor(TestDiscoveryOptions discoveryOptions, TestConsoleOutputOptions outputOptions, + CustomClassLoaderCloseStrategy classLoaderCloseStrategy) { + this(discoveryOptions, outputOptions, classLoaderCloseStrategy, LauncherFactory::create); } // for tests only ConsoleTestExecutor(TestDiscoveryOptions discoveryOptions, TestConsoleOutputOptions outputOptions, Supplier launcherSupplier) { + this(discoveryOptions, outputOptions, CustomClassLoaderCloseStrategy.CLOSE_AFTER_CALLING_LAUNCHER, + launcherSupplier); + } + + private ConsoleTestExecutor(TestDiscoveryOptions discoveryOptions, TestConsoleOutputOptions outputOptions, + CustomClassLoaderCloseStrategy classLoaderCloseStrategy, Supplier launcherSupplier) { this.discoveryOptions = discoveryOptions; this.outputOptions = outputOptions; this.launcherSupplier = launcherSupplier; + this.classLoaderCloseStrategy = classLoaderCloseStrategy; } public void discover(PrintWriter out) { - new CustomContextClassLoaderExecutor(createCustomClassLoader()).invoke(() -> { + createCustomContextClassLoaderExecutor().invoke(() -> { discoverTests(out); return null; }); } public TestExecutionSummary execute(PrintWriter out, Optional reportsDir) { - return new CustomContextClassLoaderExecutor(createCustomClassLoader()) // + return createCustomContextClassLoaderExecutor() // .invoke(() -> executeTests(out, reportsDir)); } + private CustomContextClassLoaderExecutor createCustomContextClassLoaderExecutor() { + return new CustomContextClassLoaderExecutor(createCustomClassLoader(), classLoaderCloseStrategy); + } + private void discoverTests(PrintWriter out) { Launcher launcher = launcherSupplier.get(); Optional commandLineTestPrinter = createDetailsPrintingListener(out); diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/CustomClassLoaderCloseStrategy.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/CustomClassLoaderCloseStrategy.java new file mode 100644 index 000000000000..8a4694ba2156 --- /dev/null +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/CustomClassLoaderCloseStrategy.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015-2025 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.platform.console.tasks; + +import static org.apiguardian.api.API.Status.INTERNAL; + +import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; + +/** + * Defines the strategy for closing custom class loaders created for test + * discovery and execution. + */ +@API(status = INTERNAL, since = "1.13") +public enum CustomClassLoaderCloseStrategy { + + /** + * Close the custom class loader after calling the + * {@link org.junit.platform.launcher.Launcher} for test discovery or + * execution. + */ + CLOSE_AFTER_CALLING_LAUNCHER { + + @Override + public void handle(ClassLoader customClassLoader) { + if (customClassLoader instanceof AutoCloseable) { + close((AutoCloseable) customClassLoader); + } + } + + private void close(AutoCloseable customClassLoader) { + try { + customClassLoader.close(); + } + catch (Exception e) { + throw new JUnitException("Failed to close custom class loader", e); + } + } + }, + + /** + * Rely on the JVM to release resources held by the custom class loader when + * it terminates. + * + *

    This mode is only safe to use when calling {@link System#exit(int)} + * afterward. + */ + KEEP_OPEN { + @Override + public void handle(ClassLoader customClassLoader) { + // do nothing + } + }; + + /** + * Handle the class loader according to the strategy. + */ + public abstract void handle(ClassLoader classLoader); + +} diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/CustomContextClassLoaderExecutor.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/CustomContextClassLoaderExecutor.java index 0313fe16ca5c..4e1a4e0f3dda 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/CustomContextClassLoaderExecutor.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/CustomContextClassLoaderExecutor.java @@ -13,17 +13,22 @@ import java.util.Optional; import java.util.function.Supplier; -import org.junit.platform.commons.JUnitException; - /** * @since 1.0 */ class CustomContextClassLoaderExecutor { private final Optional customClassLoader; + private final CustomClassLoaderCloseStrategy closeStrategy; CustomContextClassLoaderExecutor(Optional customClassLoader) { + this(customClassLoader, CustomClassLoaderCloseStrategy.CLOSE_AFTER_CALLING_LAUNCHER); + } + + CustomContextClassLoaderExecutor(Optional customClassLoader, + CustomClassLoaderCloseStrategy closeStrategy) { this.customClassLoader = customClassLoader; + this.closeStrategy = closeStrategy; } T invoke(Supplier supplier) { @@ -43,18 +48,7 @@ private T replaceThreadContextClassLoaderAndInvoke(ClassLoader customClassLo } finally { Thread.currentThread().setContextClassLoader(originalClassLoader); - if (customClassLoader instanceof AutoCloseable) { - close((AutoCloseable) customClassLoader); - } - } - } - - private static void close(AutoCloseable customClassLoader) { - try { - customClassLoader.close(); - } - catch (Exception e) { - throw new JUnitException("Failed to close custom class loader", e); + closeStrategy.handle(customClassLoader); } } diff --git a/junit-platform-engine/junit-platform-engine.gradle.kts b/junit-platform-engine/junit-platform-engine.gradle.kts index ef73763146a5..416b227b00c1 100644 --- a/junit-platform-engine/junit-platform-engine.gradle.kts +++ b/junit-platform-engine/junit-platform-engine.gradle.kts @@ -1,6 +1,5 @@ plugins { id("junitbuild.java-library-conventions") - id("junitbuild.native-image-properties") `java-test-fixtures` } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoveryIssue.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoveryIssue.java index 4524b291a1b3..287523181921 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoveryIssue.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoveryIssue.java @@ -13,8 +13,10 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; import java.util.Optional; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; /** * {@code DiscoveryIssue} represents an issue that was encountered during test @@ -29,6 +31,8 @@ public interface DiscoveryIssue { * Create a new {@code DiscoveryIssue} with the supplied {@link Severity} and * message. * + * @param severity the severity of the issue; never {@code null} + * @param message the message of the issue; never blank * @see #builder(Severity, String) */ static DiscoveryIssue create(Severity severity, String message) { @@ -39,10 +43,14 @@ static DiscoveryIssue create(Severity severity, String message) { * Create a new {@link Builder} for creating a {@code DiscoveryIssue} with * the supplied {@link Severity} and message. * + * @param severity the severity of the issue; never {@code null} + * @param message the message of the issue; never blank * @see Builder * @see #create(Severity, String) */ static Builder builder(Severity severity, String message) { + Preconditions.notNull(severity, "severity must not be null"); + Preconditions.notBlank(message, "message must not be blank"); return new DefaultDiscoveryIssue.Builder(severity, message); } @@ -66,6 +74,22 @@ static Builder builder(Severity severity, String message) { */ Optional cause(); + /** + * Create a copy of this issue with the modified message produced by the + * supplied operator. + */ + default DiscoveryIssue withMessage(UnaryOperator messageModifier) { + String oldMessage = message(); + String newMessage = messageModifier.apply(oldMessage); + if (oldMessage.equals(newMessage)) { + return this; + } + return DiscoveryIssue.builder(severity(), newMessage) // + .source(source()) // + .cause(cause()) // + .build(); + } + /** * The severity of a {@code DiscoveryIssue}. */ @@ -76,17 +100,12 @@ enum Severity { * potentially problematic, but could also happen due to a valid setup * or configuration. */ - NOTICE, - - /** - * Indicates that a deprecated feature was used that might be removed - * or change behavior in a future release. - */ - DEPRECATION, + INFO, /** * Indicates that the engine encountered something that is problematic - * and might lead to unexpected behavior. + * and might lead to unexpected behavior or will be removed or changed + * in a future release. */ WARNING, @@ -104,6 +123,9 @@ interface Builder { /** * Set the {@link TestSource} for the {@code DiscoveryIssue}. + * + * @param source the {@link TestSource} for the {@code DiscoveryIssue}; + * never {@code null} but potentially empty */ default Builder source(Optional source) { source.ifPresent(this::source); @@ -112,11 +134,17 @@ default Builder source(Optional source) { /** * Set the {@link TestSource} for the {@code DiscoveryIssue}. + * + * @param source the {@link TestSource} for the {@code DiscoveryIssue}; + * may be {@code null} */ Builder source(TestSource source); /** * Set the {@link Throwable} that caused the {@code DiscoveryIssue}. + * + * @param cause the {@link Throwable} that caused the + * {@code DiscoveryIssue}; never {@code null} but potentially empty */ default Builder cause(Optional cause) { cause.ifPresent(this::cause); @@ -125,6 +153,9 @@ default Builder cause(Optional cause) { /** * Set the {@link Throwable} that caused the {@code DiscoveryIssue}. + * + * @param cause the {@link Throwable} that caused the + * {@code DiscoveryIssue}; may be {@code null} */ Builder cause(Throwable cause); diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java index 447790814427..3bdacace4401 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryRequest.java @@ -94,4 +94,5 @@ default OutputDirectoryProvider getOutputDirectoryProvider() { throw new JUnitException( "OutputDirectoryProvider not available; probably due to unaligned versions of the junit-platform-engine and junit-platform-launcher jars on the classpath/module path."); } + } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java index 3d320a0d1c74..1267551da486 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java @@ -19,6 +19,8 @@ import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.reporting.OutputDirectoryProvider; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; /** * Provides a single {@link TestEngine} access to the information necessary to @@ -40,22 +42,25 @@ public class ExecutionRequest { private final EngineExecutionListener engineExecutionListener; private final ConfigurationParameters configurationParameters; private final OutputDirectoryProvider outputDirectoryProvider; + private final NamespacedHierarchicalStore requestLevelStore; @Deprecated @API(status = DEPRECATED, since = "1.11") public ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener, ConfigurationParameters configurationParameters) { - this(rootTestDescriptor, engineExecutionListener, configurationParameters, null); + this(rootTestDescriptor, engineExecutionListener, configurationParameters, null, null); } private ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener, - ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider) { + ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider, + NamespacedHierarchicalStore requestLevelStore) { this.rootTestDescriptor = Preconditions.notNull(rootTestDescriptor, "rootTestDescriptor must not be null"); this.engineExecutionListener = Preconditions.notNull(engineExecutionListener, "engineExecutionListener must not be null"); this.configurationParameters = Preconditions.notNull(configurationParameters, "configurationParameters must not be null"); this.outputDirectoryProvider = outputDirectoryProvider; + this.requestLevelStore = requestLevelStore; } /** @@ -68,7 +73,7 @@ private ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListe * engine may use to influence test execution * @return a new {@code ExecutionRequest}; never {@code null} * @since 1.9 - * @deprecated Use {@link #create(TestDescriptor, EngineExecutionListener, ConfigurationParameters, OutputDirectoryProvider)} + * @deprecated without replacement */ @Deprecated @API(status = DEPRECATED, since = "1.11") @@ -88,16 +93,19 @@ public static ExecutionRequest create(TestDescriptor rootTestDescriptor, * engine may use to influence test execution; never {@code null} * @param outputDirectoryProvider {@link OutputDirectoryProvider} for * writing reports and other output files; never {@code null} + * @param requestLevelStore {@link NamespacedHierarchicalStore} for storing + * request-scoped data; never {@code null} * @return a new {@code ExecutionRequest}; never {@code null} - * @since 1.12 + * @since 1.13 */ - @API(status = INTERNAL, since = "1.12") + @API(status = INTERNAL, since = "1.13") public static ExecutionRequest create(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener, ConfigurationParameters configurationParameters, - OutputDirectoryProvider outputDirectoryProvider) { + OutputDirectoryProvider outputDirectoryProvider, NamespacedHierarchicalStore requestLevelStore) { return new ExecutionRequest(rootTestDescriptor, engineExecutionListener, configurationParameters, - Preconditions.notNull(outputDirectoryProvider, "outputDirectoryProvider must not be null")); + Preconditions.notNull(outputDirectoryProvider, "outputDirectoryProvider must not be null"), + Preconditions.notNull(requestLevelStore, "requestLevelStore must not be null")); } /** @@ -138,8 +146,25 @@ public ConfigurationParameters getConfigurationParameters() { */ @API(status = EXPERIMENTAL, since = "1.12") public OutputDirectoryProvider getOutputDirectoryProvider() { - return Preconditions.notNull(outputDirectoryProvider, + return Preconditions.notNull(this.outputDirectoryProvider, "No OutputDirectoryProvider was configured for this request"); } + /** + * {@return the {@link NamespacedHierarchicalStore} for this request for + * storing request-scoped data} + * + *

    All stored values that implement {@link AutoCloseable} are notified by + * invoking their {@code close()} methods when this request has been + * executed. + * + * @since 1.13 + * @see NamespacedHierarchicalStore + */ + @API(status = EXPERIMENTAL, since = "1.13") + public NamespacedHierarchicalStore getStore() { + return Preconditions.notNull(this.requestLevelStore, + "No NamespacedHierarchicalStore was configured for this request"); + } + } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ClassContainerSelectorResolver.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ClassContainerSelectorResolver.java index 6c383e418847..4dfae26276f5 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ClassContainerSelectorResolver.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ClassContainerSelectorResolver.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.function.Predicate; +import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.discovery.ClasspathRootSelector; import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.discovery.ModuleSelector; @@ -34,8 +35,8 @@ class ClassContainerSelectorResolver implements SelectorResolver { private final Predicate classNameFilter; ClassContainerSelectorResolver(Predicate> classFilter, Predicate classNameFilter) { - this.classFilter = classFilter; - this.classNameFilter = classNameFilter; + this.classFilter = Preconditions.notNull(classFilter, "classFilter must not be null"); + this.classNameFilter = Preconditions.notNull(classNameFilter, "classNameFilter must not be null"); } @Override diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/DiscoveryIssueReporter.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/DiscoveryIssueReporter.java index 1f17d4128b5c..8226df455562 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/DiscoveryIssueReporter.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/DiscoveryIssueReporter.java @@ -12,6 +12,9 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -26,11 +29,12 @@ * {@code DiscoveryIssueReporter} defines the API for reporting * {@link DiscoveryIssue DiscoveryIssues}. * + *

    This interface is not intended to be implemented by clients. + * * @since 1.13 * @see SelectorResolver.Context */ @API(status = EXPERIMENTAL, since = "1.13") -@FunctionalInterface public interface DiscoveryIssueReporter { /** @@ -41,12 +45,53 @@ public interface DiscoveryIssueReporter { * {@code null} * @param engineId the unique identifier of the engine; never {@code null} */ - static DiscoveryIssueReporter create(EngineDiscoveryListener engineDiscoveryListener, UniqueId engineId) { + static DiscoveryIssueReporter forwarding(EngineDiscoveryListener engineDiscoveryListener, UniqueId engineId) { Preconditions.notNull(engineDiscoveryListener, "engineDiscoveryListener must not be null"); Preconditions.notNull(engineId, "engineId must not be null"); return issue -> engineDiscoveryListener.issueEncountered(engineId, issue); } + /** + * Create a new {@code DiscoveryIssueReporter} that adds reported issues to + * the supplied collection. + * + * @param collection the collection to add issues to; never {@code null} + */ + static DiscoveryIssueReporter collecting(Collection collection) { + Preconditions.notNull(collection, "collection must not be null"); + return consuming(collection::add); + } + + /** + * Create a new {@code DiscoveryIssueReporter} that adds reported issues to + * the supplied consumer. + * + * @param consumer the consumer to report issues to; never {@code null} + */ + static DiscoveryIssueReporter consuming(Consumer consumer) { + Preconditions.notNull(consumer, "consumer must not be null"); + return consumer::accept; + } + + /** + * Create a new {@code DiscoveryIssueReporter} that avoids reporting + * duplicate issues. + * + *

    The implementation returned by this method is not thread-safe. + * + * @param delegate the delegate to forward issues to; never {@code null} + */ + static DiscoveryIssueReporter deduplicating(DiscoveryIssueReporter delegate) { + Preconditions.notNull(delegate, "delegate must not be null"); + Set seen = new HashSet<>(); + return issue -> { + boolean notSeen = seen.add(issue); + if (notSeen) { + delegate.reportIssue(issue); + } + }; + } + /** * Build the supplied {@link DiscoveryIssue.Builder Builder} and report the * resulting {@link DiscoveryIssue}. @@ -92,45 +137,52 @@ default Condition createReportingCondition(Predicate predicate, * for filtering, or to {@link java.util.stream.Stream#peek(Consumer)} if it * is only used for reporting or other side effects. * + *

    This interface is not intended to be implemented by clients. + * * @see #createReportingCondition(Predicate, Function) */ - @FunctionalInterface - interface Condition extends Predicate, Consumer { + interface Condition { + + /** + * Create a {@link Condition} that is always satisfied. + */ + static Condition alwaysSatisfied() { + return __ -> true; + } /** - * Return a composed condition that represents a logical AND of the - * supplied conditions without short-circuiting. + * Evaluate this condition to potentially report an issue. + */ + boolean check(T value); + + /** + * Return a composed condition that represents a logical AND of this + * and the supplied condition. * - *

    All of the supplied conditions will be evaluated even if - * one or more of them return {@code false} to ensure that all issues - * are reported. + *

    The default implementation avoids short-circuiting so + * both conditions will be evaluated even if this condition + * returns {@code false} to ensure that all issues are reported. * - * @param conditions the conditions to compose; never {@code null}, not - * empty, and must not contain any {@code null} elements * @return the composed condition; never {@code null} */ - @SafeVarargs - @SuppressWarnings("varargs") - static Condition allOf(Condition... conditions) { - Preconditions.notNull(conditions, "conditions must not be null"); - Preconditions.notEmpty(conditions, "conditions must not be empty"); - Preconditions.containsNoNullElements(conditions, "conditions must not contain null elements"); - return value -> { - boolean result = true; - for (Condition condition : conditions) { - result &= condition.test(value); - } - return result; - }; + default Condition and(Condition that) { + Preconditions.notNull(that, "condition must not be null"); + return value -> this.check(value) & that.check(value); + } + + /** + * {@return this condition as a {@link Predicate}} + */ + default Predicate toPredicate() { + return this::check; } /** - * Evaluate the {@code #test(Object)} method of this condition to - * potentially report an issue. + * {@return this condition as a {@link Consumer}} */ - @Override - default void accept(T value) { - test(value); + default Consumer toConsumer() { + return this::check; } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolution.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolution.java index 46670c941fc3..36a939414a7d 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolution.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolution.java @@ -61,7 +61,6 @@ class EngineDiscoveryRequestResolution { private final Context defaultContext; private final List resolvers; private final List visitors; - private final DiscoveryIssueReporter issueReporter; private final TestDescriptor engineDescriptor; private final Map resolvedSelectors = new LinkedHashMap<>(); private final Map resolvedUniqueIds = new LinkedHashMap<>(); @@ -69,13 +68,11 @@ class EngineDiscoveryRequestResolution { private final Map contextBySelector = new HashMap<>(); EngineDiscoveryRequestResolution(EngineDiscoveryRequest request, TestDescriptor engineDescriptor, - List resolvers, List visitors, - DiscoveryIssueReporter issueReporter) { + List resolvers, List visitors) { this.request = request; this.engineDescriptor = engineDescriptor; this.resolvers = resolvers; this.visitors = visitors; - this.issueReporter = issueReporter; this.defaultContext = new DefaultContext(null); this.resolvedUniqueIds.put(engineDescriptor.getUniqueId(), Match.exact(engineDescriptor)); } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolver.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolver.java index 28e24d09a705..4c5dbf3037d9 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolver.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolver.java @@ -65,7 +65,9 @@ private EngineDiscoveryRequestResolver(List, S /** * Resolve the supplied {@link EngineDiscoveryRequest} and collect the - * results into the supplied {@link TestDescriptor}. + * results into the supplied {@link TestDescriptor} while forwarding + * encountered discovery issues to the {@link EngineDiscoveryRequest}'s + * {@link org.junit.platform.engine.EngineDiscoveryListener}. * *

    The algorithm works as follows: * @@ -110,13 +112,40 @@ private EngineDiscoveryRequestResolver(List, S public void resolve(EngineDiscoveryRequest request, T engineDescriptor) { Preconditions.notNull(request, "request must not be null"); Preconditions.notNull(engineDescriptor, "engineDescriptor must not be null"); - DiscoveryIssueReporter issueReporter = DiscoveryIssueReporter.create(request.getDiscoveryListener(), + DiscoveryIssueReporter issueReporter = DiscoveryIssueReporter.forwarding(request.getDiscoveryListener(), engineDescriptor.getUniqueId()); + resolve(request, engineDescriptor, issueReporter); + } + + /** + * Resolve the supplied {@link EngineDiscoveryRequest} and collect the + * results into the supplied {@link TestDescriptor} using the supplied + * {@link DiscoveryIssueReporter} to report issues encountered during + * resolution. + * + *

    The algorithm works as described in + * {@link #resolve(EngineDiscoveryRequest, TestDescriptor)}. + * + * @param request the request to be resolved; never {@code null} + * @param engineDescriptor the engine's {@code TestDescriptor} to be used + * for adding direct children + * @param issueReporter the {@link DiscoveryIssueReporter} to report issues + * encountered during resolution + * @since 1.13 + * @see #resolve(EngineDiscoveryRequest, TestDescriptor) + * @see SelectorResolver + * @see TestDescriptor.Visitor + */ + @API(status = EXPERIMENTAL, since = "1.13") + public void resolve(EngineDiscoveryRequest request, T engineDescriptor, DiscoveryIssueReporter issueReporter) { + Preconditions.notNull(request, "request must not be null"); + Preconditions.notNull(engineDescriptor, "engineDescriptor must not be null"); + Preconditions.notNull(issueReporter, "issueReporter must not be null"); InitializationContext initializationContext = new DefaultInitializationContext<>(request, engineDescriptor, issueReporter); List resolvers = instantiate(resolverCreators, initializationContext); List visitors = instantiate(visitorCreators, initializationContext); - new EngineDiscoveryRequestResolution(request, engineDescriptor, resolvers, visitors, issueReporter).run(); + new EngineDiscoveryRequestResolution(request, engineDescriptor, resolvers, visitors).run(); } private List instantiate(List, R>> creators, @@ -162,8 +191,28 @@ private Builder() { */ public Builder addClassContainerSelectorResolver(Predicate> classFilter) { Preconditions.notNull(classFilter, "classFilter must not be null"); - return addSelectorResolver( - context -> new ClassContainerSelectorResolver(classFilter, context.getClassNameFilter())); + return addClassContainerSelectorResolverWithContext(__ -> classFilter); + } + + /** + * Add a predefined resolver that resolves {@link ClasspathRootSelector + * ClasspathRootSelectors}, {@link ModuleSelector ModuleSelectors}, and + * {@link PackageSelector PackageSelectors} into {@link ClassSelector + * ClassSelectors} by scanning for classes that satisfy the predicate + * created by the supplied {@code Function} in the respective class + * containers to this builder. + * + * @param classFilterCreator the function that will be called to create + * the predicate the resolved classes must satisfy; never + * {@code null} + * @return this builder for method chaining + */ + @API(status = EXPERIMENTAL, since = "1.13") + public Builder addClassContainerSelectorResolverWithContext( + Function, Predicate>> classFilterCreator) { + Preconditions.notNull(classFilterCreator, "classFilterCreator must not be null"); + return addSelectorResolver(context -> new ClassContainerSelectorResolver(classFilterCreator.apply(context), + context.getClassNameFilter())); } /** diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/Namespace.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/Namespace.java new file mode 100644 index 000000000000..dcf06d571833 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/Namespace.java @@ -0,0 +1,105 @@ +/* + * Copyright 2015-2025 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.platform.engine.support.store; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; + +/** + * A {@code Namespace} is used to provide a scope for data saved by + * extensions within a {@link NamespacedHierarchicalStore}. + * + *

    Storing data in custom namespaces allows extensions to avoid accidentally + * mixing data between extensions or across different invocations within the + * lifecycle of a single extension. + */ +@API(status = EXPERIMENTAL, since = "1.13") +public class Namespace { + + /** + * The default, global namespace which allows access to stored data from + * all extensions. + */ + public static final Namespace GLOBAL = Namespace.create(new Object()); + + /** + * Create a namespace which restricts access to data to all extensions + * which use the same sequence of {@code parts} for creating a namespace. + * + *

    The order of the {@code parts} is significant. + * + *

    Internally the {@code parts} are compared using {@link Object#equals(Object)}. + */ + public static Namespace create(Object... parts) { + Preconditions.notEmpty(parts, "parts array must not be null or empty"); + Preconditions.containsNoNullElements(parts, "individual parts must not be null"); + return new Namespace(Arrays.asList(parts)); + } + + /** + * Create a namespace which restricts access to data to all extensions + * which use the same sequence of {@code objects} for creating a namespace. + * + *

    The order of the {@code objects} is significant. + * + *

    Internally the {@code objects} are compared using {@link Object#equals(Object)}. + */ + public static Namespace create(List objects) { + Preconditions.notEmpty(objects, "objects list must not be null or empty"); + Preconditions.containsNoNullElements(objects, "individual objects must not be null"); + return new Namespace(objects); + } + + private final List parts; + + private Namespace(List parts) { + this.parts = new ArrayList<>(parts); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Namespace that = (Namespace) o; + return this.parts.equals(that.parts); + } + + @Override + public int hashCode() { + return this.parts.hashCode(); + } + + /** + * Create a new namespace by appending the supplied {@code parts} to the + * existing sequence of parts in this namespace. + * + * @return new namespace; never {@code null} + */ + public Namespace append(Object... parts) { + Preconditions.notEmpty(parts, "parts array must not be null or empty"); + Preconditions.containsNoNullElements(parts, "individual parts must not be null"); + ArrayList newParts = new ArrayList<>(this.parts.size() + parts.length); + newParts.addAll(this.parts); + Collections.addAll(newParts, parts); + return new Namespace(newParts); + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java index 9167dde42b13..bd27996973b1 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java @@ -17,6 +17,7 @@ import java.util.Comparator; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; @@ -85,6 +86,19 @@ public NamespacedHierarchicalStore newChild() { return new NamespacedHierarchicalStore<>(this, this.closeAction); } + /** + * Returns the parent store of this {@code NamespacedHierarchicalStore}. + * + *

    If this store does not have a parent, an empty {@code Optional} is returned. + * + * @return an {@code Optional} containing the parent store, or an empty {@code Optional} if there is no parent + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + public Optional> getParent() { + return Optional.ofNullable(this.parentStore); + } + /** * Determine if this store has been {@linkplain #close() closed}. * @@ -447,6 +461,15 @@ public Failure(Throwable throwable) { @FunctionalInterface public interface CloseAction { + @API(status = EXPERIMENTAL, since = "1.13") + static CloseAction closeAutoCloseables() { + return (__, ___, value) -> { + if (value instanceof AutoCloseable) { + ((AutoCloseable) value).close(); + } + }; + } + /** * Close the supplied {@code value}. * diff --git a/junit-platform-engine/src/nativeImage/initialize-at-build-time b/junit-platform-engine/src/nativeImage/initialize-at-build-time deleted file mode 100644 index 5b1168bb74a6..000000000000 --- a/junit-platform-engine/src/nativeImage/initialize-at-build-time +++ /dev/null @@ -1,7 +0,0 @@ -org.junit.platform.engine.TestDescriptor$Type -org.junit.platform.engine.UniqueId -org.junit.platform.engine.UniqueId$Segment -org.junit.platform.engine.UniqueIdFormat -org.junit.platform.engine.support.descriptor.ClassSource -org.junit.platform.engine.support.descriptor.MethodSource -org.junit.platform.engine.support.hierarchical.Node$ExecutionMode diff --git a/junit-platform-jfr/src/main/java/org/junit/platform/jfr/FlightRecordingDiscoveryListener.java b/junit-platform-jfr/src/main/java/org/junit/platform/jfr/FlightRecordingDiscoveryListener.java index f36d31cd2ff3..ed34f1289360 100644 --- a/junit-platform-jfr/src/main/java/org/junit/platform/jfr/FlightRecordingDiscoveryListener.java +++ b/junit-platform-jfr/src/main/java/org/junit/platform/jfr/FlightRecordingDiscoveryListener.java @@ -23,7 +23,9 @@ import jdk.jfr.StackTrace; import org.apiguardian.api.API; +import org.junit.platform.commons.util.ExceptionUtils; import org.junit.platform.engine.DiscoveryFilter; +import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.launcher.EngineDiscoveryResult; import org.junit.platform.launcher.LauncherDiscoveryListener; @@ -71,13 +73,23 @@ public void engineDiscoveryFinished(org.junit.platform.engine.UniqueId engineId, event.commit(); } + @Override + public void issueEncountered(org.junit.platform.engine.UniqueId engineId, DiscoveryIssue issue) { + DiscoveryIssueEvent event = new DiscoveryIssueEvent(); + event.engineId = engineId.toString(); + event.severity = issue.severity().name(); + event.message = issue.message(); + event.source = issue.source().map(Object::toString).orElse(null); + event.cause = issue.cause().map(ExceptionUtils::readStackTrace).orElse(null); + event.commit(); + } + @Category({ "JUnit", "Discovery" }) @StackTrace(false) abstract static class DiscoveryEvent extends Event { } @Label("Test Discovery") - @Category({ "JUnit", "Discovery" }) @Name("org.junit.LauncherDiscovery") static class LauncherDiscoveryEvent extends DiscoveryEvent { @@ -89,7 +101,6 @@ static class LauncherDiscoveryEvent extends DiscoveryEvent { } @Label("Engine Discovery") - @Category({ "JUnit", "Discovery" }) @Name("org.junit.EngineDiscovery") static class EngineDiscoveryEvent extends DiscoveryEvent { @@ -100,4 +111,24 @@ static class EngineDiscoveryEvent extends DiscoveryEvent { @Label("Result") String result; } + + @Label("Discovery Issue") + @Name("org.junit.DiscoveryIssue") + static class DiscoveryIssueEvent extends DiscoveryEvent { + + @Label("Engine Id") + String engineId; + + @Label("Severity") + String severity; + + @Label("Message") + String message; + + @Label("Source") + String source; + + @Label("Cause") + String cause; + } } diff --git a/junit-platform-launcher/junit-platform-launcher.gradle.kts b/junit-platform-launcher/junit-platform-launcher.gradle.kts index acda6a79436f..a9b3630762c8 100644 --- a/junit-platform-launcher/junit-platform-launcher.gradle.kts +++ b/junit-platform-launcher/junit-platform-launcher.gradle.kts @@ -1,6 +1,5 @@ plugins { id("junitbuild.java-library-conventions") - id("junitbuild.native-image-properties") `java-test-fixtures` } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java index a2af5d187a58..b356e376b54c 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java @@ -219,6 +219,31 @@ public class LauncherConstants { @API(status = EXPERIMENTAL, since = "1.12") public static final String OUTPUT_DIR_UNIQUE_NUMBER_PLACEHOLDER = "{uniqueNumber}"; + /** + * Property name used to configure the critical severity of issues + * encountered during test discovery. + * + *

    If an engine reports an issue with a severity equal to or higher than + * the configured critical severity, its tests will not be executed. + * Instead, the engine will be reported as failed during execution with a + * {@link org.junit.platform.launcher.core.DiscoveryIssueException} listing + * all critical issues. + * + *

    Supported Values

    + * + *

    Supported values include names of enum constants defined in + * {@link org.junit.platform.engine.DiscoveryIssue.Severity Severity}, + * ignoring case. + * + *

    If not specified, the default is "error" which corresponds to + * {@code Severity.ERROR)}. + * + * @since 1.13 + * @see org.junit.platform.engine.DiscoveryIssue.Severity + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static final String CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME = "junit.platform.discovery.issue.severity.critical"; + private LauncherConstants() { /* no-op */ } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java index c78ed7761888..285621b59812 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java @@ -10,9 +10,12 @@ package org.junit.platform.launcher; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import org.apiguardian.api.API; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.core.LauncherFactory; /** @@ -47,4 +50,19 @@ public interface LauncherSession extends AutoCloseable { @Override void close(); + /** + * Get the {@link NamespacedHierarchicalStore} associated with this session. + * + *

    All stored values that implement {@link AutoCloseable} are notified by + * invoking their {@code close()} methods when this session is closed. + * + *

    Any call to the store returned by this method after the session has + * been closed will throw an exception. + * + * @since 1.13 + * @see NamespacedHierarchicalStore + */ + @API(status = EXPERIMENTAL, since = "1.13") + NamespacedHierarchicalStore getStore(); + } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java index 6bd73c4b641e..70106c48fb72 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java @@ -103,4 +103,5 @@ public LauncherDiscoveryListener getDiscoveryListener() { public OutputDirectoryProvider getOutputDirectoryProvider() { return this.outputDirectoryProvider; } + } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java index 5ebc4f9a638e..2a9fa3bf1ed4 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java @@ -11,6 +11,7 @@ package org.junit.platform.launcher.core; import static java.util.Collections.unmodifiableCollection; +import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables; import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.DISCOVERY; import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.EXECUTION; @@ -18,6 +19,8 @@ import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; @@ -41,6 +44,7 @@ class DefaultLauncher implements Launcher { private final EngineExecutionOrchestrator executionOrchestrator = new EngineExecutionOrchestrator( listenerRegistry.testExecutionListeners); private final EngineDiscoveryOrchestrator discoveryOrchestrator; + private final NamespacedHierarchicalStore sessionLevelStore; /** * Construct a new {@code DefaultLauncher} with the supplied test engines. @@ -50,7 +54,8 @@ class DefaultLauncher implements Launcher { * @param postDiscoveryFilters the additional post discovery filters for * discovery requests; never {@code null} */ - DefaultLauncher(Iterable testEngines, Collection postDiscoveryFilters) { + DefaultLauncher(Iterable testEngines, Collection postDiscoveryFilters, + NamespacedHierarchicalStore sessionLevelStore) { Preconditions.condition(testEngines != null && testEngines.iterator().hasNext(), () -> "Cannot create Launcher without at least one TestEngine; " + "consider adding an engine implementation JAR to the classpath"); @@ -59,6 +64,7 @@ class DefaultLauncher implements Launcher { "PostDiscoveryFilter array must not contain null elements"); this.discoveryOrchestrator = new EngineDiscoveryOrchestrator(testEngines, unmodifiableCollection(postDiscoveryFilters), listenerRegistry.launcherDiscoveryListeners); + this.sessionLevelStore = sessionLevelStore; } @Override @@ -100,7 +106,13 @@ private LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryReque } private void execute(InternalTestPlan internalTestPlan, TestExecutionListener[] listeners) { - executionOrchestrator.execute(internalTestPlan, listeners); + try (NamespacedHierarchicalStore requestLevelStore = createRequestLevelStore()) { + executionOrchestrator.execute(internalTestPlan, requestLevelStore, listeners); + } + } + + private NamespacedHierarchicalStore createRequestLevelStore() { + return new NamespacedHierarchicalStore<>(sessionLevelStore, closeAutoCloseables()); } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java index 018eb41cf8a5..b128e09b1258 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java @@ -10,10 +10,15 @@ package org.junit.platform.launcher.core; +import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables; + import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; @@ -40,21 +45,26 @@ public void close() { } }; + private final NamespacedHierarchicalStore store = new NamespacedHierarchicalStore<>(null, + closeAutoCloseables()); private final LauncherInterceptor interceptor; private final LauncherSessionListener listener; private final DelegatingLauncher launcher; - DefaultLauncherSession(List interceptors, Supplier listenerSupplier, - Supplier launcherSupplier) { + DefaultLauncherSession(List interceptors, // + Supplier listenerSupplier, // + Function, Launcher> launcherFactory // + ) { interceptor = composite(interceptors); Launcher launcher; if (interceptor == NOOP_INTERCEPTOR) { this.listener = listenerSupplier.get(); - launcher = launcherSupplier.get(); + launcher = launcherFactory.apply(this.store); } else { this.listener = interceptor.intercept(listenerSupplier::get); - launcher = new InterceptingLauncher(interceptor.intercept(launcherSupplier::get), interceptor); + launcher = new InterceptingLauncher(interceptor.intercept(() -> launcherFactory.apply(this.store)), + interceptor); } this.launcher = new DelegatingLauncher(launcher); listener.launcherSessionOpened(this); @@ -74,10 +84,16 @@ public void close() { if (launcher.delegate != ClosedLauncher.INSTANCE) { launcher.delegate = ClosedLauncher.INSTANCE; listener.launcherSessionClosed(this); + store.close(); interceptor.close(); } } + @Override + public NamespacedHierarchicalStore getStore() { + return store; + } + private static class ClosedLauncher implements Launcher { static final ClosedLauncher INSTANCE = new ClosedLauncher(); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueCollector.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueCollector.java index 4c7fa3538aa3..2b19b1e92f81 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueCollector.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueCollector.java @@ -15,18 +15,46 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import org.junit.platform.commons.logging.Logger; +import org.junit.platform.commons.logging.LoggerFactory; +import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.SelectorResolutionResult; +import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.ClassSelector; +import org.junit.platform.engine.discovery.ClasspathResourceSelector; +import org.junit.platform.engine.discovery.DirectorySelector; +import org.junit.platform.engine.discovery.FileSelector; +import org.junit.platform.engine.discovery.MethodSelector; +import org.junit.platform.engine.discovery.PackageSelector; import org.junit.platform.engine.discovery.UniqueIdSelector; +import org.junit.platform.engine.discovery.UriSelector; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; +import org.junit.platform.engine.support.descriptor.DirectorySource; +import org.junit.platform.engine.support.descriptor.FilePosition; +import org.junit.platform.engine.support.descriptor.FileSource; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.descriptor.PackageSource; +import org.junit.platform.engine.support.descriptor.UriSource; +import org.junit.platform.launcher.LauncherConstants; import org.junit.platform.launcher.LauncherDiscoveryListener; class DiscoveryIssueCollector implements LauncherDiscoveryListener { + private static final Logger logger = LoggerFactory.getLogger(DiscoveryIssueCollector.class); + final List issues = new ArrayList<>(); + private final ConfigurationParameters configurationParameters; + + DiscoveryIssueCollector(ConfigurationParameters configurationParameters) { + this.configurationParameters = configurationParameters; + } @Override public void engineDiscoveryStarted(UniqueId engineId) { @@ -38,6 +66,7 @@ public void selectorProcessed(UniqueId engineId, DiscoverySelector selector, Sel if (result.getStatus() == FAILED) { this.issues.add(DiscoveryIssue.builder(Severity.ERROR, selector + " resolution failed") // .cause(result.getThrowable()) // + .source(toSource(selector)) // .build()); } else if (result.getStatus() == UNRESOLVED && selector instanceof UniqueIdSelector) { @@ -48,16 +77,75 @@ else if (result.getStatus() == UNRESOLVED && selector instanceof UniqueIdSelecto } } + static TestSource toSource(DiscoverySelector selector) { + if (selector instanceof ClassSelector) { + return ClassSource.from(((ClassSelector) selector).getClassName()); + } + if (selector instanceof MethodSelector) { + MethodSelector methodSelector = (MethodSelector) selector; + return MethodSource.from(methodSelector.getClassName(), methodSelector.getMethodName(), + methodSelector.getParameterTypeNames()); + } + if (selector instanceof ClasspathResourceSelector) { + ClasspathResourceSelector resourceSelector = (ClasspathResourceSelector) selector; + String resourceName = resourceSelector.getClasspathResourceName(); + return resourceSelector.getPosition() // + .map(DiscoveryIssueCollector::convert) // + .map(position -> ClasspathResourceSource.from(resourceName, position)) // + .orElseGet(() -> ClasspathResourceSource.from(resourceName)); + } + if (selector instanceof PackageSelector) { + return PackageSource.from(((PackageSelector) selector).getPackageName()); + } + if (selector instanceof FileSelector) { + FileSelector fileSelector = (FileSelector) selector; + return fileSelector.getPosition() // + .map(DiscoveryIssueCollector::convert) // + .map(position -> FileSource.from(fileSelector.getFile(), position)) // + .orElseGet(() -> FileSource.from(fileSelector.getFile())); + } + if (selector instanceof DirectorySelector) { + return DirectorySource.from(((DirectorySelector) selector).getDirectory()); + } + if (selector instanceof UriSelector) { + return UriSource.from(((UriSelector) selector).getUri()); + } + return null; + } + + private static FilePosition convert(org.junit.platform.engine.discovery.FilePosition position) { + return position.getColumn() // + .map(column -> FilePosition.from(position.getLine(), column)) // + .orElseGet(() -> FilePosition.from(position.getLine())); + } + @Override public void issueEncountered(UniqueId engineId, DiscoveryIssue issue) { this.issues.add(issue); } DiscoveryIssueNotifier toNotifier() { - if (issues.isEmpty()) { + if (this.issues.isEmpty()) { return DiscoveryIssueNotifier.NO_ISSUES; } - Severity criticalSeverity = Severity.ERROR; // TODO #242 - make this configurable - return DiscoveryIssueNotifier.from(criticalSeverity, issues); + return DiscoveryIssueNotifier.from(getCriticalSeverity(), this.issues); + } + + private Severity getCriticalSeverity() { + Severity defaultValue = Severity.ERROR; + return this.configurationParameters // + .get(LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, value -> { + try { + return Severity.valueOf(value.toUpperCase(Locale.ROOT)); + } + catch (Exception e) { + logger.warn(() -> String.format( + "Invalid DiscoveryIssue.Severity '%s' set via the '%s' configuration parameter. " + + "Falling back to the %s default value.", + value, LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, defaultValue)); + return defaultValue; + } + }) // + .orElse(defaultValue); } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueNotifier.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueNotifier.java index 8bb14f68dcdd..592af7bd866b 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueNotifier.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueNotifier.java @@ -13,9 +13,8 @@ import static java.util.Collections.emptyList; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.partitioningBy; +import static org.junit.platform.commons.util.ExceptionUtils.readStackTrace; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -91,10 +90,10 @@ private void logIssues(TestEngine testEngine, List issues, Strin } private static Consumer> logger(Severity severity) { + // TODO [#4246] Use switch expression switch (severity) { - case NOTICE: + case INFO: return logger::info; - case DEPRECATION: case WARNING: return logger::warn; case ERROR: @@ -133,7 +132,7 @@ else if (source instanceof ClassSource) { appendIdeCompatibleLink(message, classSource.getClassName(), ""); } }); - issue.cause().ifPresent(t -> message.append("\n Cause: ").append(getStackTrace(t))); + issue.cause().ifPresent(t -> message.append("\n Cause: ").append(readStackTrace(t))); } return message.toString(); } @@ -141,13 +140,4 @@ else if (source instanceof ClassSource) { private static void appendIdeCompatibleLink(StringBuilder message, String className, String methodName) { message.append("\n at ").append(className).append(".").append(methodName).append("(SourceFile:0)"); } - - private static String getStackTrace(Throwable cause) { - StringWriter stringWriter = new StringWriter(); - try (PrintWriter writer = new PrintWriter(stringWriter, true)) { - cause.printStackTrace(writer); - writer.flush(); - } - return stringWriter.toString(); - } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java index 87057a6c1f4d..00bcdae9ada8 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java @@ -28,7 +28,6 @@ import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.UnrecoverableExceptions; -import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.Filter; import org.junit.platform.engine.FilterResult; import org.junit.platform.engine.TestDescriptor; @@ -91,8 +90,8 @@ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase *

    Note: The test descriptors in the discovery result can safely be used * as non-root descriptors. Engine-test descriptor entries are pruned from * the returned result. As such execution by - * {@link EngineExecutionOrchestrator#execute(LauncherDiscoveryResult, EngineExecutionListener)} - * will not emit start or emit events for engines without tests. + * {@link EngineExecutionOrchestrator} will not emit start or emit events + * for engines without tests. */ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase phase, UniqueId parentId) { LauncherDiscoveryResult result = discover(request, phase, parentId::appendEngine); @@ -101,7 +100,7 @@ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase private LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase phase, Function uniqueIdCreator) { - DiscoveryIssueCollector issueCollector = new DiscoveryIssueCollector(); + DiscoveryIssueCollector issueCollector = new DiscoveryIssueCollector(request.getConfigurationParameters()); LauncherDiscoveryListener listener = getLauncherDiscoveryListener(request, issueCollector); LauncherDiscoveryRequest delegatingRequest = new DelegatingLauncherDiscoveryRequest(request) { @Override diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java index 9c5e1b3eaaf1..eccb29fd82df 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java @@ -29,6 +29,8 @@ import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.reporting.OutputDirectoryProvider; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; @@ -52,12 +54,14 @@ public EngineExecutionOrchestrator() { this.listenerRegistry = listenerRegistry; } - void execute(InternalTestPlan internalTestPlan, TestExecutionListener... listeners) { + void execute(InternalTestPlan internalTestPlan, NamespacedHierarchicalStore requestLevelStore, + TestExecutionListener... listeners) { ConfigurationParameters configurationParameters = internalTestPlan.getConfigurationParameters(); ListenerRegistry testExecutionListenerListeners = buildListenerRegistryForExecution( listeners); withInterceptedStreams(configurationParameters, testExecutionListenerListeners, - testExecutionListener -> execute(internalTestPlan, EngineExecutionListener.NOOP, testExecutionListener)); + testExecutionListener -> execute(internalTestPlan, EngineExecutionListener.NOOP, testExecutionListener, + requestLevelStore)); } /** @@ -69,17 +73,18 @@ void execute(InternalTestPlan internalTestPlan, TestExecutionListener... listene */ @API(status = INTERNAL, since = "1.9", consumers = { "org.junit.platform.suite.engine" }) public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener engineExecutionListener, - TestExecutionListener testExecutionListener) { + TestExecutionListener testExecutionListener, NamespacedHierarchicalStore requestLevelStore) { Preconditions.notNull(discoveryResult, "discoveryResult must not be null"); Preconditions.notNull(engineExecutionListener, "engineExecutionListener must not be null"); Preconditions.notNull(testExecutionListener, "testExecutionListener must not be null"); + Preconditions.notNull(requestLevelStore, "requestLevelStore must not be null"); InternalTestPlan internalTestPlan = InternalTestPlan.from(discoveryResult); - execute(internalTestPlan, engineExecutionListener, testExecutionListener); + execute(internalTestPlan, engineExecutionListener, testExecutionListener, requestLevelStore); } private void execute(InternalTestPlan internalTestPlan, EngineExecutionListener parentEngineExecutionListener, - TestExecutionListener testExecutionListener) { + TestExecutionListener testExecutionListener, NamespacedHierarchicalStore requestLevelStore) { internalTestPlan.markStarted(); // Do not directly pass the internal test plan to test execution listeners. @@ -93,7 +98,8 @@ private void execute(InternalTestPlan internalTestPlan, EngineExecutionListener } else { execute(discoveryResult, - buildEngineExecutionListener(parentEngineExecutionListener, testExecutionListener, testPlan)); + buildEngineExecutionListener(parentEngineExecutionListener, testExecutionListener, testPlan), + requestLevelStore); } testExecutionListener.testPlanExecutionFinished(testPlan); } @@ -153,7 +159,8 @@ private void withInterceptedStreams(ConfigurationParameters configurationParamet * EngineExecutionListener listener} of execution events. */ @API(status = INTERNAL, since = "1.7", consumers = { "org.junit.platform.testkit" }) - public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener engineExecutionListener) { + public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener engineExecutionListener, + NamespacedHierarchicalStore requestLevelStore) { Preconditions.notNull(discoveryResult, "discoveryResult must not be null"); Preconditions.notNull(engineExecutionListener, "engineExecutionListener must not be null"); @@ -161,7 +168,7 @@ public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionList EngineExecutionListener listener = selectExecutionListener(engineExecutionListener, configurationParameters); for (TestEngine testEngine : discoveryResult.getTestEngines()) { - failOrExecuteEngine(discoveryResult, listener, testEngine); + failOrExecuteEngine(discoveryResult, listener, testEngine, requestLevelStore); } } @@ -176,8 +183,7 @@ private static EngineExecutionListener selectExecutionListener(EngineExecutionLi } private void failOrExecuteEngine(LauncherDiscoveryResult discoveryResult, EngineExecutionListener listener, - TestEngine testEngine) { - + TestEngine testEngine, NamespacedHierarchicalStore requestLevelStore) { EngineResultInfo engineDiscoveryResult = discoveryResult.getEngineResult(testEngine); DiscoveryIssueNotifier discoveryIssueNotifier = engineDiscoveryResult.getDiscoveryIssueNotifier(); TestDescriptor engineDescriptor = engineDiscoveryResult.getRootDescriptor(); @@ -193,7 +199,7 @@ private void failOrExecuteEngine(LauncherDiscoveryResult discoveryResult, Engine } else { executeEngine(engineDescriptor, listener, discoveryResult.getConfigurationParameters(), testEngine, - discoveryResult.getOutputDirectoryProvider(), discoveryIssueNotifier); + discoveryResult.getOutputDirectoryProvider(), discoveryIssueNotifier, requestLevelStore); } } @@ -207,13 +213,13 @@ private ListenerRegistry buildListenerRegistryForExecutio private void executeEngine(TestDescriptor engineDescriptor, EngineExecutionListener listener, ConfigurationParameters configurationParameters, TestEngine testEngine, - OutputDirectoryProvider outputDirectoryProvider, DiscoveryIssueNotifier discoveryIssueNotifier) { - + OutputDirectoryProvider outputDirectoryProvider, DiscoveryIssueNotifier discoveryIssueNotifier, + NamespacedHierarchicalStore requestLevelStore) { OutcomeDelayingEngineExecutionListener delayingListener = new OutcomeDelayingEngineExecutionListener(listener, engineDescriptor); try { testEngine.execute(ExecutionRequest.create(engineDescriptor, delayingListener, configurationParameters, - outputDirectoryProvider)); + outputDirectoryProvider, requestLevelStore)); discoveryIssueNotifier.logNonCriticalIssues(testEngine); delayingListener.reportEngineOutcome(); } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineFilterer.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineFilterer.java index 10e939f1ce84..529524a637ea 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineFilterer.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineFilterer.java @@ -12,11 +12,14 @@ import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toCollection; +import static java.util.stream.Collectors.toSet; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; @@ -69,12 +72,14 @@ private void checkNoUnmatchedIncludeFilter() { } private SortedSet getUnmatchedEngineIdsOfIncludeFilters() { + Set checkedTestEngineIds = checkedTestEngines.keySet().stream() // + .map(TestEngine::getId) // + .collect(toSet()); return engineFilters.stream() // .filter(EngineFilter::isIncludeFilter) // - .filter(engineFilter -> checkedTestEngines.keySet().stream() // - .map(engineFilter::apply) // - .noneMatch(FilterResult::included)) // - .flatMap(engineFilter -> engineFilter.getEngineIds().stream()) // + .map(EngineFilter::getEngineIds) // + .flatMap(Collection::stream) // + .filter(id -> !checkedTestEngineIds.contains(id)) // .collect(toCollection(TreeSet::new)); } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java index c756f27351f0..b3d6ab2613a0 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java @@ -26,6 +26,8 @@ import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherInterceptor; @@ -96,7 +98,8 @@ public static LauncherSession openSession(LauncherConfig config) throws Precondi Preconditions.notNull(config, "LauncherConfig must not be null"); LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); return new DefaultLauncherSession(collectLauncherInterceptors(configurationParameters), - () -> createLauncherSessionListener(config), () -> createDefaultLauncher(config, configurationParameters)); + () -> createLauncherSessionListener(config), + sessionLevelStore -> createDefaultLauncher(config, configurationParameters, sessionLevelStore)); } /** @@ -125,17 +128,17 @@ public static Launcher create() throws PreconditionViolationException { public static Launcher create(LauncherConfig config) throws PreconditionViolationException { Preconditions.notNull(config, "LauncherConfig must not be null"); LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); - return new SessionPerRequestLauncher(() -> createDefaultLauncher(config, configurationParameters), + return new SessionPerRequestLauncher( + sessionLevelStore -> createDefaultLauncher(config, configurationParameters, sessionLevelStore), () -> createLauncherSessionListener(config), () -> collectLauncherInterceptors(configurationParameters)); } private static DefaultLauncher createDefaultLauncher(LauncherConfig config, - LauncherConfigurationParameters configurationParameters) { + LauncherConfigurationParameters configurationParameters, + NamespacedHierarchicalStore sessionLevelStore) { Set engines = collectTestEngines(config); List filters = collectPostDiscoveryFilters(config); - - DefaultLauncher launcher = new DefaultLauncher(engines, filters); - + DefaultLauncher launcher = new DefaultLauncher(engines, filters, sessionLevelStore); registerLauncherDiscoveryListeners(config, launcher); registerTestExecutionListeners(config, launcher, configurationParameters); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java index dffde014867a..cb12eafb7282 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java @@ -11,8 +11,11 @@ package org.junit.platform.launcher.core; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; @@ -28,14 +31,14 @@ class SessionPerRequestLauncher implements Launcher { private final LauncherListenerRegistry listenerRegistry = new LauncherListenerRegistry(); - private final Supplier launcherSupplier; + private final Function, Launcher> launcherFactory; private final Supplier sessionListenerSupplier; private final Supplier> interceptorFactory; - SessionPerRequestLauncher(Supplier launcherSupplier, + SessionPerRequestLauncher(Function, Launcher> launcherFactory, Supplier sessionListenerSupplier, Supplier> interceptorFactory) { - this.launcherSupplier = launcherSupplier; + this.launcherFactory = launcherFactory; this.sessionListenerSupplier = sessionListenerSupplier; this.interceptorFactory = interceptorFactory; } @@ -73,7 +76,7 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) { private LauncherSession createSession() { LauncherSession session = new DefaultLauncherSession(interceptorFactory.get(), sessionListenerSupplier, - launcherSupplier); + this.launcherFactory); Launcher launcher = session.getLauncher(); listenerRegistry.launcherDiscoveryListeners.getListeners().forEach( launcher::registerLauncherDiscoveryListeners); diff --git a/junit-platform-launcher/src/nativeImage/initialize-at-build-time b/junit-platform-launcher/src/nativeImage/initialize-at-build-time deleted file mode 100644 index 66e181d6e90d..000000000000 --- a/junit-platform-launcher/src/nativeImage/initialize-at-build-time +++ /dev/null @@ -1,21 +0,0 @@ -org.junit.platform.launcher.LauncherSessionListener$1 -org.junit.platform.launcher.TestIdentifier -org.junit.platform.launcher.core.DefaultLauncher -org.junit.platform.launcher.core.DefaultLauncherConfig -org.junit.platform.launcher.core.DiscoveryIssueNotifier -org.junit.platform.launcher.core.EngineDiscoveryOrchestrator -org.junit.platform.launcher.core.EngineExecutionOrchestrator -org.junit.platform.launcher.core.HierarchicalOutputDirectoryProvider -org.junit.platform.launcher.core.InternalTestPlan -org.junit.platform.launcher.core.LauncherConfig -org.junit.platform.launcher.core.LauncherConfigurationParameters -org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$1 -org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$2 -org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$3 -org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$4 -org.junit.platform.launcher.core.LauncherDiscoveryResult -org.junit.platform.launcher.core.LauncherDiscoveryResult$EngineResultInfo -org.junit.platform.launcher.core.LauncherListenerRegistry -org.junit.platform.launcher.core.ListenerRegistry -org.junit.platform.launcher.core.SessionPerRequestLauncher -org.junit.platform.launcher.listeners.UniqueIdTrackingListener diff --git a/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/NamespacedHierarchicalStoreProviders.java b/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/NamespacedHierarchicalStoreProviders.java new file mode 100644 index 000000000000..7beef2b5f0f1 --- /dev/null +++ b/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/NamespacedHierarchicalStoreProviders.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015-2025 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.platform.launcher.core; + +import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables; + +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; + +public class NamespacedHierarchicalStoreProviders { + + public static NamespacedHierarchicalStore dummyNamespacedHierarchicalStore() { + return new NamespacedHierarchicalStore<>(dummyNamespacedHierarchicalStoreWithNoParent(), closeAutoCloseables()); + } + + public static NamespacedHierarchicalStore dummyNamespacedHierarchicalStoreWithNoParent() { + return new NamespacedHierarchicalStore<>(null, closeAutoCloseables()); + } +} diff --git a/junit-platform-reporting/junit-platform-reporting.gradle.kts b/junit-platform-reporting/junit-platform-reporting.gradle.kts index e9094c04f2e1..f1dede61555d 100644 --- a/junit-platform-reporting/junit-platform-reporting.gradle.kts +++ b/junit-platform-reporting/junit-platform-reporting.gradle.kts @@ -1,6 +1,5 @@ plugins { id("junitbuild.java-library-conventions") - id("junitbuild.native-image-properties") id("junitbuild.shadow-conventions") `java-test-fixtures` } diff --git a/junit-platform-reporting/src/nativeImage/initialize-at-build-time b/junit-platform-reporting/src/nativeImage/initialize-at-build-time deleted file mode 100644 index 1b4f355f53cf..000000000000 --- a/junit-platform-reporting/src/nativeImage/initialize-at-build-time +++ /dev/null @@ -1,2 +0,0 @@ -org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener -org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.api.DocumentWriter$1 diff --git a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java index ccee1edb4eb8..79e8ef209728 100644 --- a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java +++ b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java @@ -11,6 +11,7 @@ package org.junit.platform.suite.commons; import static java.util.stream.Collectors.toList; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations; import static org.junit.platform.engine.discovery.ClassNameFilter.STANDARD_INCLUDE_PATTERN; @@ -30,7 +31,6 @@ import java.util.stream.Stream; import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.StringUtils; import org.junit.platform.engine.ConfigurationParameters; @@ -43,6 +43,7 @@ import org.junit.platform.engine.discovery.PackageNameFilter; import org.junit.platform.engine.reporting.OutputDirectoryProvider; import org.junit.platform.launcher.EngineFilter; +import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.TagFilter; import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; @@ -111,8 +112,7 @@ * @see org.junit.platform.launcher.EngineFilter * @see org.junit.platform.launcher.TagFilter */ -@API(status = Status.INTERNAL, since = "1.8", consumers = { "org.junit.platform.suite.engine", - "org.junit.platform.runner" }) +@API(status = INTERNAL, since = "1.8", consumers = { "org.junit.platform.suite.engine", "org.junit.platform.runner" }) public final class SuiteLauncherDiscoveryRequestBuilder { private final LauncherDiscoveryRequestBuilder delegate = LauncherDiscoveryRequestBuilder.request(); @@ -268,6 +268,12 @@ public SuiteLauncherDiscoveryRequestBuilder outputDirectoryProvider( return this; } + @API(status = INTERNAL, since = "1.13") + public SuiteLauncherDiscoveryRequestBuilder listener(LauncherDiscoveryListener listener) { + delegate.listeners(listener); + return this; + } + /** * Apply a suite's annotation-based configuration, selectors, and filters to * this builder. diff --git a/junit-platform-suite-engine/junit-platform-suite-engine.gradle.kts b/junit-platform-suite-engine/junit-platform-suite-engine.gradle.kts index 72f90de35321..36abcdbc088d 100644 --- a/junit-platform-suite-engine/junit-platform-suite-engine.gradle.kts +++ b/junit-platform-suite-engine/junit-platform-suite-engine.gradle.kts @@ -1,6 +1,5 @@ plugins { id("junitbuild.java-library-conventions") - id("junitbuild.native-image-properties") } description = "JUnit Platform Suite Engine" diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/ClassSelectorResolver.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/ClassSelectorResolver.java index 20289f336e5c..9694f15c9860 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/ClassSelectorResolver.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/ClassSelectorResolver.java @@ -16,16 +16,18 @@ import java.util.Optional; import java.util.function.Predicate; -import org.junit.platform.commons.logging.Logger; -import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.EngineDiscoveryListener; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.UniqueId.Segment; import org.junit.platform.engine.discovery.ClassSelector; import org.junit.platform.engine.discovery.UniqueIdSelector; import org.junit.platform.engine.reporting.OutputDirectoryProvider; +import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; import org.junit.platform.engine.support.discovery.SelectorResolver; @@ -34,23 +36,24 @@ */ final class ClassSelectorResolver implements SelectorResolver { - private static final Logger log = LoggerFactory.getLogger(ClassSelectorResolver.class); - - private static final IsSuiteClass isSuiteClass = new IsSuiteClass(); - + private final IsSuiteClass isSuiteClass; private final Predicate classNameFilter; private final SuiteEngineDescriptor suiteEngineDescriptor; private final ConfigurationParameters configurationParameters; private final OutputDirectoryProvider outputDirectoryProvider; + private final EngineDiscoveryListener discoveryListener; private final DiscoveryIssueReporter issueReporter; ClassSelectorResolver(Predicate classNameFilter, SuiteEngineDescriptor suiteEngineDescriptor, ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider, - DiscoveryIssueReporter issueReporter) { + EngineDiscoveryListener discoveryListener, DiscoveryIssueReporter issueReporter) { + + this.isSuiteClass = new IsSuiteClass(issueReporter); this.classNameFilter = classNameFilter; this.suiteEngineDescriptor = suiteEngineDescriptor; this.configurationParameters = configurationParameters; this.outputDirectoryProvider = outputDirectoryProvider; + this.discoveryListener = discoveryListener; this.issueReporter = issueReporter; } @@ -106,12 +109,14 @@ private static Resolution toResolution(Optional suite) { private Optional newSuiteDescriptor(Class suiteClass, TestDescriptor parent) { UniqueId id = parent.getUniqueId().append(SuiteTestDescriptor.SEGMENT_TYPE, suiteClass.getName()); if (containsCycle(id)) { - log.config(() -> createConfigContainsCycleMessage(suiteClass, id)); + issueReporter.reportIssue( + DiscoveryIssue.builder(Severity.INFO, createConfigContainsCycleMessage(suiteClass, id)) // + .source(ClassSource.from(suiteClass))); return Optional.empty(); } - return Optional.of( - new SuiteTestDescriptor(id, suiteClass, configurationParameters, outputDirectoryProvider, issueReporter)); + return Optional.of(new SuiteTestDescriptor(id, suiteClass, configurationParameters, outputDirectoryProvider, + discoveryListener, issueReporter)); } private static boolean containsCycle(UniqueId id) { diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/DiscoverySelectorResolver.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/DiscoverySelectorResolver.java index 0c7aead14921..871bf6e7d3d7 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/DiscoverySelectorResolver.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/DiscoverySelectorResolver.java @@ -12,6 +12,7 @@ import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver; /** @@ -21,12 +22,13 @@ final class DiscoverySelectorResolver { // @formatter:off private static final EngineDiscoveryRequestResolver resolver = EngineDiscoveryRequestResolver.builder() - .addClassContainerSelectorResolver(new IsSuiteClass()) + .addClassContainerSelectorResolverWithContext(context -> new IsSuiteClass(context.getIssueReporter())) .addSelectorResolver(context -> new ClassSelectorResolver( context.getClassNameFilter(), context.getEngineDescriptor(), context.getDiscoveryRequest().getConfigurationParameters(), context.getDiscoveryRequest().getOutputDirectoryProvider(), + context.getDiscoveryRequest().getDiscoveryListener(), context.getIssueReporter())) .build(); // @formatter:on @@ -40,7 +42,9 @@ private static void discoverSuites(SuiteEngineDescriptor engineDescriptor) { } void resolveSelectors(EngineDiscoveryRequest request, SuiteEngineDescriptor engineDescriptor) { - resolver.resolve(request, engineDescriptor); + DiscoveryIssueReporter issueReporter = DiscoveryIssueReporter.deduplicating( + DiscoveryIssueReporter.forwarding(request.getDiscoveryListener(), engineDescriptor.getUniqueId())); + resolver.resolve(request, engineDescriptor, issueReporter); discoverSuites(engineDescriptor); engineDescriptor.accept(TestDescriptor::prune); } diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/IsPotentialTestContainer.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/IsPotentialTestContainer.java deleted file mode 100644 index ebfa4cc50e05..000000000000 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/IsPotentialTestContainer.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2015-2025 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.platform.suite.engine; - -import static org.junit.platform.commons.support.ModifierSupport.isAbstract; -import static org.junit.platform.commons.support.ModifierSupport.isPrivate; -import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass; - -import java.util.function.Predicate; - -/** - * @since 1.8 - */ -final class IsPotentialTestContainer implements Predicate> { - - @Override - public boolean test(Class candidate) { - // Please do not collapse the following into a single statement. - if (isPrivate(candidate)) { - return false; - } - if (isAbstract(candidate)) { - return false; - } - if (candidate.isLocalClass()) { - return false; - } - if (candidate.isAnonymousClass()) { - return false; - } - return !isInnerClass(candidate); - } - -} diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/IsSuiteClass.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/IsSuiteClass.java index 97947eefc831..6d9fcbe98ffa 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/IsSuiteClass.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/IsSuiteClass.java @@ -10,9 +10,18 @@ package org.junit.platform.suite.engine; +import static org.junit.platform.commons.support.ModifierSupport.isAbstract; +import static org.junit.platform.commons.support.ModifierSupport.isNotAbstract; +import static org.junit.platform.commons.support.ModifierSupport.isNotPrivate; + import java.util.function.Predicate; import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.platform.commons.util.ReflectionUtils; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter.Condition; import org.junit.platform.suite.api.Suite; /** @@ -20,15 +29,47 @@ */ final class IsSuiteClass implements Predicate> { - private static final IsPotentialTestContainer isPotentialTestContainer = new IsPotentialTestContainer(); + private final Condition> condition; + + IsSuiteClass(DiscoveryIssueReporter issueReporter) { + this.condition = isNotPrivateUnlessAbstract(issueReporter) // + .and(isNotLocal(issueReporter)) // + .and(isNotInner(issueReporter)); + } @Override public boolean test(Class testClass) { - return isPotentialTestContainer.test(testClass) && hasSuiteAnnotation(testClass); + return hasSuiteAnnotation(testClass) // + && condition.check(testClass) // + && isNotAbstract(testClass); } private boolean hasSuiteAnnotation(Class testClass) { return AnnotationSupport.isAnnotated(testClass, Suite.class); } + private static Condition> isNotPrivateUnlessAbstract(DiscoveryIssueReporter issueReporter) { + // Allow abstract test classes to be private because @Suite is inherited and subclasses may widen access. + return issueReporter.createReportingCondition(testClass -> isNotPrivate(testClass) || isAbstract(testClass), + testClass -> createIssue(testClass, "must not be private.")); + } + + private static Condition> isNotLocal(DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(testClass -> !testClass.isLocalClass(), + testClass -> createIssue(testClass, "must not be a local class.")); + } + + private static Condition> isNotInner(DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(testClass -> !ReflectionUtils.isInnerClass(testClass), + testClass -> createIssue(testClass, "must not be an inner class. Did you forget to declare it static?")); + } + + private static DiscoveryIssue createIssue(Class testClass, String detailMessage) { + String message = String.format("@Suite class '%s' %s It will not be executed.", testClass.getName(), + detailMessage); + return DiscoveryIssue.builder(DiscoveryIssue.Severity.WARNING, message) // + .source(ClassSource.from(testClass)) // + .build(); + } + } diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/LifecycleMethodUtils.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/LifecycleMethodUtils.java index 87ce1ae662a4..df8d894c3444 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/LifecycleMethodUtils.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/LifecycleMethodUtils.java @@ -12,7 +12,6 @@ import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods; import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList; -import static org.junit.platform.engine.support.discovery.DiscoveryIssueReporter.Condition.allOf; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -54,12 +53,12 @@ private static List findMethodsAndCheckStaticAndNonPrivate(Class test DiscoveryIssueReporter issueReporter) { return findAnnotatedMethods(testClass, annotationType, traversalMode).stream() // - .filter(allOf( // - returnsPrimitiveVoid(annotationType, issueReporter), // - isStatic(annotationType, issueReporter), // - isNotPrivate(annotationType, issueReporter), // - hasNoParameters(annotationType, issueReporter) // - )) // + .filter(// + returnsPrimitiveVoid(annotationType, issueReporter) // + .and(isStatic(annotationType, issueReporter)) // + .and(isNotPrivate(annotationType, issueReporter)) // + .and(hasNoParameters(annotationType, issueReporter)) // + .toPredicate()) // .collect(toUnmodifiableList()); } diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java index c3a62006e859..6e3bf311ccfb 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java @@ -19,6 +19,8 @@ import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator; import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase; @@ -58,9 +60,10 @@ LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, Uniq } TestExecutionSummary execute(LauncherDiscoveryResult discoveryResult, - EngineExecutionListener parentEngineExecutionListener) { + EngineExecutionListener parentEngineExecutionListener, + NamespacedHierarchicalStore requestLevelStore) { SummaryGeneratingListener listener = new SummaryGeneratingListener(); - executionOrchestrator.execute(discoveryResult, parentEngineExecutionListener, listener); + executionOrchestrator.execute(discoveryResult, parentEngineExecutionListener, listener, requestLevelStore); return listener.getSummary(); } diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java index 9a4257d8a0cc..d9837a6a2319 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java @@ -10,21 +10,29 @@ package org.junit.platform.suite.engine; +import static java.util.function.Predicate.isEqual; +import static java.util.stream.Collectors.joining; import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; +import static org.junit.platform.commons.util.FunctionUtils.where; import static org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilder.request; import java.lang.reflect.Method; import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Predicate; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.StringUtils; import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.EngineDiscoveryListener; import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.UniqueId.Segment; import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.reporting.OutputDirectoryProvider; import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; @@ -32,6 +40,9 @@ import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; import org.junit.platform.engine.support.hierarchical.OpenTest4JAwareThrowableCollector; import org.junit.platform.engine.support.hierarchical.ThrowableCollector; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; +import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.core.LauncherDiscoveryResult; import org.junit.platform.launcher.listeners.TestExecutionSummary; @@ -64,13 +75,15 @@ final class SuiteTestDescriptor extends AbstractTestDescriptor { private SuiteLauncher launcher; SuiteTestDescriptor(UniqueId id, Class suiteClass, ConfigurationParameters configurationParameters, - OutputDirectoryProvider outputDirectoryProvider, DiscoveryIssueReporter issueReporter) { + OutputDirectoryProvider outputDirectoryProvider, EngineDiscoveryListener discoveryListener, + DiscoveryIssueReporter issueReporter) { super(id, getSuiteDisplayName(suiteClass), ClassSource.from(suiteClass)); this.configurationParameters = configurationParameters; this.outputDirectoryProvider = outputDirectoryProvider; this.failIfNoTests = getFailIfNoTests(suiteClass); this.suiteClass = suiteClass; this.lifecycleMethods = new LifecycleMethods(suiteClass, issueReporter); + this.discoveryRequestBuilder.listener(DiscoveryIssueForwardingListener.create(id, discoveryListener)); } private static Boolean getFailIfNoTests(Class suiteClass) { @@ -133,13 +146,15 @@ private static String getSuiteDisplayName(Class testClass) { // @formatter:on } - void execute(EngineExecutionListener parentEngineExecutionListener) { + void execute(EngineExecutionListener parentEngineExecutionListener, + NamespacedHierarchicalStore requestLevelStore) { parentEngineExecutionListener.executionStarted(this); ThrowableCollector throwableCollector = new OpenTest4JAwareThrowableCollector(); executeBeforeSuiteMethods(throwableCollector); - TestExecutionSummary summary = executeTests(parentEngineExecutionListener, throwableCollector); + TestExecutionSummary summary = executeTests(parentEngineExecutionListener, requestLevelStore, + throwableCollector); executeAfterSuiteMethods(throwableCollector); @@ -160,7 +175,7 @@ private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) { } private TestExecutionSummary executeTests(EngineExecutionListener parentEngineExecutionListener, - ThrowableCollector throwableCollector) { + NamespacedHierarchicalStore requestLevelStore, ThrowableCollector throwableCollector) { if (throwableCollector.isNotEmpty()) { return null; } @@ -170,7 +185,7 @@ private TestExecutionSummary executeTests(EngineExecutionListener parentEngineEx // be pruned accordingly. LauncherDiscoveryResult discoveryResult = this.launcherDiscoveryResult.withRetainedEngines( getChildren()::contains); - return launcher.execute(discoveryResult, parentEngineExecutionListener); + return launcher.execute(discoveryResult, parentEngineExecutionListener, requestLevelStore); } private void executeAfterSuiteMethods(ThrowableCollector throwableCollector) { @@ -209,4 +224,45 @@ private static class LifecycleMethods { } } + private static class DiscoveryIssueForwardingListener implements LauncherDiscoveryListener { + + private static final Predicate SUITE_SEGMENTS = where(Segment::getType, isEqual(SEGMENT_TYPE)); + + static DiscoveryIssueForwardingListener create(UniqueId id, EngineDiscoveryListener discoveryListener) { + boolean isNestedSuite = id.getSegments().stream().filter(SUITE_SEGMENTS).count() > 1; + if (isNestedSuite) { + return new DiscoveryIssueForwardingListener(discoveryListener, (__, issue) -> issue); + } + return new DiscoveryIssueForwardingListener(discoveryListener, + (engineUniqueId, issue) -> issue.withMessage(message -> { + String engineId = engineUniqueId.getLastSegment().getValue(); + if (SuiteEngineDescriptor.ENGINE_ID.equals(engineId)) { + return message; + } + String suitePath = engineUniqueId.getSegments().stream() // + .filter(SUITE_SEGMENTS) // + .map(Segment::getValue) // + .collect(joining(" > ")); + if (message.endsWith(".")) { + message = message.substring(0, message.length() - 1); + } + return String.format("[%s] %s (via @Suite %s).", engineId, message, suitePath); + })); + } + + private final EngineDiscoveryListener discoveryListener; + private final BiFunction issueTransformer; + + private DiscoveryIssueForwardingListener(EngineDiscoveryListener discoveryListener, + BiFunction issueTransformer) { + this.discoveryListener = discoveryListener; + this.issueTransformer = issueTransformer; + } + + @Override + public void issueEncountered(UniqueId engineUniqueId, DiscoveryIssue issue) { + DiscoveryIssue transformedIssue = this.issueTransformer.apply(engineUniqueId, issue); + this.discoveryListener.issueEncountered(engineUniqueId, transformedIssue); + } + } } diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java index c0f754639c80..d75cf8c3dfbf 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java @@ -22,6 +22,8 @@ import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; /** * The JUnit Platform Suite {@link org.junit.platform.engine.TestEngine TestEngine}. @@ -63,6 +65,7 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId public void execute(ExecutionRequest request) { SuiteEngineDescriptor suiteEngineDescriptor = (SuiteEngineDescriptor) request.getRootTestDescriptor(); EngineExecutionListener engineExecutionListener = request.getEngineExecutionListener(); + NamespacedHierarchicalStore requestLevelStore = request.getStore(); engineExecutionListener.executionStarted(suiteEngineDescriptor); @@ -70,7 +73,7 @@ public void execute(ExecutionRequest request) { suiteEngineDescriptor.getChildren() .stream() .map(SuiteTestDescriptor.class::cast) - .forEach(suiteTestDescriptor -> suiteTestDescriptor.execute(engineExecutionListener)); + .forEach(suiteTestDescriptor -> suiteTestDescriptor.execute(engineExecutionListener, requestLevelStore)); // @formatter:on engineExecutionListener.executionFinished(suiteEngineDescriptor, TestExecutionResult.successful()); } diff --git a/junit-platform-suite-engine/src/nativeImage/initialize-at-build-time b/junit-platform-suite-engine/src/nativeImage/initialize-at-build-time deleted file mode 100644 index 5313fc54879d..000000000000 --- a/junit-platform-suite-engine/src/nativeImage/initialize-at-build-time +++ /dev/null @@ -1,5 +0,0 @@ -org.junit.platform.suite.engine.SuiteEngineDescriptor -org.junit.platform.suite.engine.SuiteLauncher -org.junit.platform.suite.engine.SuiteTestDescriptor -org.junit.platform.suite.engine.SuiteTestDescriptor$LifecycleMethods -org.junit.platform.suite.engine.SuiteTestEngine diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java index df1505b1c4d8..9f7f3e18501a 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java @@ -16,6 +16,7 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import static org.apiguardian.api.API.Status.STABLE; +import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables; import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.DISCOVERY; import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.EXECUTION; @@ -23,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.ServiceLoader; +import java.util.function.Consumer; import java.util.stream.Stream; import org.apiguardian.api.API; @@ -41,6 +43,8 @@ import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.reporting.OutputDirectoryProvider; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator; import org.junit.platform.launcher.core.EngineExecutionOrchestrator; @@ -324,15 +328,30 @@ private static void executeDirectly(TestEngine testEngine, EngineDiscoveryReques EngineExecutionListener listener) { UniqueId engineUniqueId = UniqueId.forEngine(testEngine.getId()); TestDescriptor engineTestDescriptor = testEngine.discover(discoveryRequest, engineUniqueId); - ExecutionRequest request = ExecutionRequest.create(engineTestDescriptor, listener, - discoveryRequest.getConfigurationParameters(), discoveryRequest.getOutputDirectoryProvider()); - testEngine.execute(request); + withRequestLevelStore(store -> { + ExecutionRequest request = ExecutionRequest.create(engineTestDescriptor, listener, + discoveryRequest.getConfigurationParameters(), discoveryRequest.getOutputDirectoryProvider(), store); + testEngine.execute(request); + }); } private static void executeUsingLauncherOrchestration(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest, EngineExecutionListener listener) { LauncherDiscoveryResult discoveryResult = discover(testEngine, discoveryRequest, EXECUTION); - new EngineExecutionOrchestrator().execute(discoveryResult, listener); + TestDescriptor engineTestDescriptor = discoveryResult.getEngineTestDescriptor(testEngine); + Preconditions.notNull(engineTestDescriptor, "TestEngine did not yield a TestDescriptor"); + withRequestLevelStore(store -> new EngineExecutionOrchestrator().execute(discoveryResult, listener, store)); + } + + private static void withRequestLevelStore(Consumer> action) { + try (NamespacedHierarchicalStore sessionLevelStore = newStore(null); + NamespacedHierarchicalStore requestLevelStore = newStore(sessionLevelStore)) { + action.accept(requestLevelStore); + } + } + + private static NamespacedHierarchicalStore newStore(NamespacedHierarchicalStore parentStore) { + return new NamespacedHierarchicalStore<>(parentStore, closeAutoCloseables()); } private static LauncherDiscoveryResult discover(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest, diff --git a/junit-vintage-engine/junit-vintage-engine.gradle.kts b/junit-vintage-engine/junit-vintage-engine.gradle.kts index 3d1231829fac..54d444b43c81 100644 --- a/junit-vintage-engine/junit-vintage-engine.gradle.kts +++ b/junit-vintage-engine/junit-vintage-engine.gradle.kts @@ -1,7 +1,6 @@ plugins { id("junitbuild.java-library-conventions") id("junitbuild.junit4-compatibility") - id("junitbuild.native-image-properties") id("junitbuild.testing-conventions") `java-test-fixtures` groovy diff --git a/junit-vintage-engine/src/nativeImage/initialize-at-build-time b/junit-vintage-engine/src/nativeImage/initialize-at-build-time deleted file mode 100644 index 75ff3d41de5a..000000000000 --- a/junit-vintage-engine/src/nativeImage/initialize-at-build-time +++ /dev/null @@ -1,5 +0,0 @@ -org.junit.vintage.engine.VintageTestEngine -org.junit.vintage.engine.descriptor.RunnerTestDescriptor -org.junit.vintage.engine.descriptor.VintageEngineDescriptor -org.junit.vintage.engine.support.UniqueIdReader -org.junit.vintage.engine.support.UniqueIdStringifier diff --git a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java index 2fd7a4b4080b..8811f97d404d 100644 --- a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java +++ b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java @@ -16,6 +16,7 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore; import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.junit.platform.testkit.engine.EventConditions.abortedWithReason; import static org.junit.platform.testkit.engine.EventConditions.container; @@ -924,8 +925,9 @@ private static void execute(Class testClass, EngineExecutionListener listener TestEngine testEngine = new VintageTestEngine(); var discoveryRequest = request(testClass); var engineTestDescriptor = testEngine.discover(discoveryRequest, UniqueId.forEngine(testEngine.getId())); - testEngine.execute(ExecutionRequest.create(engineTestDescriptor, listener, - discoveryRequest.getConfigurationParameters(), dummyOutputDirectoryProvider())); + testEngine.execute( + ExecutionRequest.create(engineTestDescriptor, listener, discoveryRequest.getConfigurationParameters(), + dummyOutputDirectoryProvider(), dummyNamespacedHierarchicalStore())); } private static LauncherDiscoveryRequest request(Class testClass) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java index 27578670dab7..323470e59556 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java @@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; -import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; import java.lang.reflect.Method; import java.util.EmptyStackException; @@ -30,7 +30,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; -import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.testkit.engine.EngineExecutionResults; import org.junit.platform.testkit.engine.Event; /** @@ -189,6 +190,30 @@ void checkDisplayNameGeneratedForIndicativeGeneratorWithCustomSentenceFragments( ); } + @Test + void blankSentenceFragmentOnClassYieldsError() { + var results = discoverTests(selectClass(BlankSentenceFragmentOnClassTestCase.class)); + + var discoveryIssues = results.getDiscoveryIssues(); + assertThat(discoveryIssues).hasSize(1); + assertThat(discoveryIssues.getFirst().severity()).isEqualTo(Severity.ERROR); + assertThat(discoveryIssues.getFirst().cause().orElseThrow()) // + .hasMessage("@SentenceFragment on [%s] must be declared with a non-blank value.", + BlankSentenceFragmentOnClassTestCase.class); + } + + @Test + void blankSentenceFragmentOnMethodYieldsError() throws Exception { + var results = discoverTests(selectMethod(BlankSentenceFragmentOnMethodTestCase.class, "test")); + + var discoveryIssues = results.getDiscoveryIssues(); + assertThat(discoveryIssues).hasSize(1); + assertThat(discoveryIssues.getFirst().severity()).isEqualTo(Severity.ERROR); + assertThat(discoveryIssues.getFirst().cause().orElseThrow()) // + .hasMessage("@SentenceFragment on [%s] must be declared with a non-blank value.", + BlankSentenceFragmentOnMethodTestCase.class.getDeclaredMethod("test")); + } + @Test void displayNameGenerationInheritance() { check(DisplayNameGenerationInheritanceTestCase.InnerNestedTestCase.class, // @@ -273,15 +298,17 @@ void indicativeSentencesOnClassTemplate() { } private void check(Class testClass, String... expectedDisplayNames) { - var request = request().selectors(selectClass(testClass)).build(); - var descriptors = executeTests(request).allEvents().started().stream() // - .map(Event::getTestDescriptor) // - .skip(1); // Skip engine descriptor - assertThat(descriptors).map(this::describe).containsExactlyInAnyOrder(expectedDisplayNames); + var results = executeTestsForClass(testClass); + check(results, expectedDisplayNames); } - private String describe(TestDescriptor descriptor) { - return descriptor.getType() + ": " + descriptor.getDisplayName(); + private void check(EngineExecutionResults results, String[] expectedDisplayNames) { + var descriptors = results.allEvents().started().stream() // + .map(Event::getTestDescriptor) // + .skip(1); // Skip engine descriptor + assertThat(descriptors) // + .map(it -> it.getType() + ": " + it.getDisplayName()) // + .containsExactlyInAnyOrder(expectedDisplayNames); } // ------------------------------------------------------------------------- @@ -622,4 +649,22 @@ public String getDisplayName(int invocationIndex) { } } + @SuppressWarnings("JUnitMalformedDeclaration") + @IndicativeSentencesGeneration + @SentenceFragment("") + static class BlankSentenceFragmentOnClassTestCase { + @Test + void test() { + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @IndicativeSentencesGeneration + static class BlankSentenceFragmentOnMethodTestCase { + @SentenceFragment("\t") + @Test + void test() { + } + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/CloseableResourceIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/CloseableResourceIntegrationTests.java index d6a60aa09e08..0cec81f6cdf2 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/CloseableResourceIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/CloseableResourceIntegrationTests.java @@ -46,8 +46,7 @@ void closesCloseableResourcesInExtensionContext(ExtensionContext extensionContex store.put("baz", reportEntryOnClose(extensionContext, "3")); } - private ExtensionContext.Store.CloseableResource reportEntryOnClose(ExtensionContext extensionContext, - String key) { + private AutoCloseable reportEntryOnClose(ExtensionContext extensionContext, String key) { return () -> extensionContext.publishReportEntry(Map.of(key, "closed")); } } @@ -80,9 +79,16 @@ static class ThrowingOnCloseExtension implements BeforeEachCallback { @Override public void beforeEach(ExtensionContext context) { - context.getStore(GLOBAL).put("throwingResource", (ExtensionContext.Store.CloseableResource) () -> { - throw new RuntimeException("Exception in onClose"); - }); + context.getStore(GLOBAL).put("throwingResource", new ThrowingResource()); + } + } + + @SuppressWarnings({ "deprecation", "try" }) + static class ThrowingResource implements ExtensionContext.Store.CloseableResource, AutoCloseable { + + @Override + public void close() throws Exception { + throw new RuntimeException("Exception in onClose"); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java index c8b93723188c..35a2e8941a73 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java @@ -13,12 +13,14 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; +import static org.junit.platform.launcher.LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME; import static org.junit.platform.launcher.LauncherConstants.STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import java.util.function.Consumer; +import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.UniqueId; import org.junit.platform.launcher.LauncherDiscoveryRequest; @@ -62,20 +64,32 @@ protected EngineDiscoveryResults discoverTestsForClass(Class testClass) { return discoverTests(selectClass(testClass)); } + protected EngineDiscoveryResults discoverTests(Consumer configurer) { + var builder = defaultRequest(); + configurer.accept(builder); + return discoverTests(builder); + } + protected EngineDiscoveryResults discoverTests(DiscoverySelector... selectors) { - return discoverTests(defaultRequest().selectors(selectors).build()); + return discoverTests(request -> request.selectors(selectors)); } - private static LauncherDiscoveryRequestBuilder defaultRequest() { - return request() // - .outputDirectoryProvider(dummyOutputDirectoryProvider()) // - .configurationParameter(STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME, String.valueOf(false)); + protected EngineDiscoveryResults discoverTests(LauncherDiscoveryRequestBuilder builder) { + return discoverTests(builder.build()); } protected EngineDiscoveryResults discoverTests(LauncherDiscoveryRequest request) { return EngineTestKit.discover(this.engine, request); } + private static LauncherDiscoveryRequestBuilder defaultRequest() { + return request() // + .outputDirectoryProvider(dummyOutputDirectoryProvider()) // + .configurationParameter(STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME, String.valueOf(false)) // + .configurationParameter(CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, Severity.INFO.name()) // + .enableImplicitConfigurationParameters(false); + } + protected UniqueId discoverUniqueId(Class clazz, String methodName) { var results = discoverTests(selectMethod(clazz, methodName)); var engineDescriptor = results.getEngineDescriptor(); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ClassTemplateInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ClassTemplateInvocationTests.java index 364f78a7306d..cd15da225011 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ClassTemplateInvocationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ClassTemplateInvocationTests.java @@ -74,7 +74,6 @@ import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; @@ -110,7 +109,7 @@ public class ClassTemplateInvocationTests extends AbstractJupiterTestEngineTests }) void executesClassTemplateClassTwice(String selectorIdentifierTemplate) { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, TwoInvocationsTestCase.class.getName()); var invocationId1 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); var invocation1MethodAId = invocationId1.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); @@ -299,7 +298,7 @@ void executesNestedClassTemplateClassTwiceWithNestedClassSelector() { @Test void executesNestedClassTemplatesTwiceEach() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var outerClassTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var outerClassTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, TwoTimesTwoInvocationsTestCase.class.getName()); var outerInvocation1Id = outerClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); @@ -403,7 +402,7 @@ void eachInvocationHasSeparateExtensionContext() { @Test void supportsTestTemplateMethodsInsideClassTemplateClasses() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, CombinationWithTestTemplateTestCase.class.getName()); var invocationId1 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); var testTemplateId1 = invocationId1.append(TestTemplateTestDescriptor.SEGMENT_TYPE, "test(int)"); @@ -459,7 +458,7 @@ void supportsTestTemplateMethodsInsideClassTemplateClasses() { @Test void testTemplateInvocationInsideClassTemplateClassCanBeSelectedByUniqueId() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, CombinationWithTestTemplateTestCase.class.getName()); var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); var testTemplateId2 = invocationId2.append(TestTemplateTestDescriptor.SEGMENT_TYPE, "test(int)"); @@ -490,7 +489,7 @@ void testTemplateInvocationInsideClassTemplateClassCanBeSelectedByUniqueId() { @Test void supportsTestFactoryMethodsInsideClassTemplateClasses() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, CombinationWithTestFactoryTestCase.class.getName()); var invocationId1 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); var testFactoryId1 = invocationId1.append(TestFactoryTestDescriptor.SEGMENT_TYPE, "test()"); @@ -546,7 +545,7 @@ void supportsTestFactoryMethodsInsideClassTemplateClasses() { @Test void specificDynamicTestInsideClassTemplateClassCanBeSelectedByUniqueId() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, CombinationWithTestFactoryTestCase.class.getName()); var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); var testFactoryId2 = invocationId2.append(TestFactoryTestDescriptor.SEGMENT_TYPE, "test()"); @@ -617,7 +616,7 @@ void failsIfNoSupportingProviderIsRegistered(Class testClass) { @Test void classTemplateInvocationCanBeSelectedByUniqueId() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, TwoInvocationsTestCase.class.getName()); var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); var methodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); @@ -650,7 +649,7 @@ void classTemplateInvocationCanBeSelectedByUniqueId() { @Test void classTemplateInvocationCanBeSelectedByIteration() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, TwoInvocationsTestCase.class.getName()); var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); var methodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); @@ -687,7 +686,7 @@ void classTemplateInvocationCanBeSelectedByIteration() { }) void executesAllInvocationsForRedundantSelectors(String classTemplateSelectorIdentifier) { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, TwoInvocationsTestCase.class.getName()); var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); @@ -700,7 +699,7 @@ void executesAllInvocationsForRedundantSelectors(String classTemplateSelectorIde @Test void methodInClassTemplateInvocationCanBeSelectedByUniqueId() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, TwoInvocationsTestCase.class.getName()); var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); var methodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); @@ -725,7 +724,7 @@ void methodInClassTemplateInvocationCanBeSelectedByUniqueId() { @Test void nestedMethodInClassTemplateInvocationCanBeSelectedByUniqueId() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, TwoInvocationsTestCase.class.getName()); var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); var nestedClassId = invocationId2.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); @@ -754,7 +753,7 @@ void nestedMethodInClassTemplateInvocationCanBeSelectedByUniqueId() { @Test void nestedClassTemplateInvocationCanBeSelectedByUniqueId() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var outerClassTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var outerClassTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, TwoTimesTwoInvocationsWithMultipleMethodsTestCase.class.getName()); var outerInvocation2Id = outerClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); var outerInvocation2NestedClassTemplateId = outerInvocation2Id.append( @@ -794,7 +793,7 @@ void nestedClassTemplateInvocationCanBeSelectedByUniqueId() { @Test void nestedClassTemplateInvocationCanBeSelectedByIteration() { var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); - var outerClassTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + var outerClassTemplateId = engineId.append(ClassTemplateTestDescriptor.STANDALONE_CLASS_SEGMENT_TYPE, TwoTimesTwoInvocationsTestCase.class.getName()); var outerInvocation1Id = outerClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); var outerInvocation1NestedClassTemplateId = outerInvocation1Id.append( @@ -1175,7 +1174,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } } - static class SomeResource implements CloseableResource { + static class SomeResource implements AutoCloseable { private boolean closed; @Override @@ -1439,7 +1438,8 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } - private static class CustomCloseableResource implements CloseableResource { + @SuppressWarnings("deprecation") + private static class CustomCloseableResource implements ExtensionContext.Store.CloseableResource { static boolean closed; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineBasicTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineBasicTests.java deleted file mode 100644 index dd242d20132a..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineBasicTests.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2015-2025 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.engine; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -/** - * Basic assertions regarding {@link org.junit.platform.engine.TestEngine} - * functionality in JUnit Jupiter. - * - * @since 5.0 - */ -class JupiterTestEngineBasicTests { - - private final JupiterTestEngine jupiter = new JupiterTestEngine(); - - @Test - void id() { - assertEquals("junit-jupiter", jupiter.getId()); - } - - @Test - void groupId() { - assertEquals("org.junit.jupiter", jupiter.getGroupId().orElseThrow()); - } - - @Test - void artifactId() { - assertEquals("junit-jupiter-engine", jupiter.getArtifactId().orElseThrow()); - } - - @Test - void version() { - assertThat(jupiter.getVersion().orElseThrow()).isIn( // - System.getProperty("developmentVersion"), // with Test Distribution - "DEVELOPMENT" // without Test Distribution - ); - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineTests.java new file mode 100644 index 000000000000..79b2652c2028 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2015-2025 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.engine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; +import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders; + +/** + * @since 5.13 + */ +public class JupiterTestEngineTests { + + private final JupiterEngineDescriptor jupiterEngineDescriptor = mock(); + + private final ConfigurationParameters configurationParameters = mock(); + + private final EngineExecutionListener engineExecutionListener = mock(); + + private final ExecutionRequest executionRequest = mock(); + + private final JupiterTestEngine engine = new JupiterTestEngine(); + + private final JupiterTestEngine jupiter = new JupiterTestEngine(); + + @BeforeEach + void setUp() { + when(executionRequest.getEngineExecutionListener()).thenReturn(engineExecutionListener); + when(executionRequest.getConfigurationParameters()).thenReturn(configurationParameters); + when(executionRequest.getRootTestDescriptor()).thenReturn(jupiterEngineDescriptor); + } + + @Test + void createExecutionContextWithValidRequest() { + when(executionRequest.getStore()).thenReturn( + NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore()); + + JupiterEngineExecutionContext context = engine.createExecutionContext(executionRequest); + assertThat(context).isNotNull(); + } + + @Test + void createExecutionContextWithNoParentsRequestLevelStore() { + when(executionRequest.getStore()).thenReturn( + NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStoreWithNoParent()); + + assertThatThrownBy(() -> engine // + .createExecutionContext(executionRequest)) // + .isInstanceOf(JUnitException.class) // + .hasMessageContaining("Request-level store must have a parent"); + } + + @Test + void id() { + assertEquals("junit-jupiter", jupiter.getId()); + } + + @Test + void groupId() { + assertEquals("org.junit.jupiter", jupiter.getGroupId().orElseThrow()); + } + + @Test + void artifactId() { + assertEquals("junit-jupiter-engine", jupiter.getArtifactId().orElseThrow()); + } + + @Test + void version() { + assertThat(jupiter.getVersion().orElseThrow()).isIn( // + System.getProperty("developmentVersion"), // with Test Distribution + "DEVELOPMENT" // without Test Distribution + ); + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/NestedTestClassesTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/NestedTestClassesTests.java index 328ef26a73db..2fe8352f41b8 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/NestedTestClassesTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/NestedTestClassesTests.java @@ -10,15 +10,19 @@ package org.junit.jupiter.engine; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.launcher.LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; +import java.util.List; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -27,7 +31,9 @@ import org.junit.jupiter.engine.NestedTestClassesTests.OuterClass.NestedClass; import org.junit.jupiter.engine.NestedTestClassesTests.OuterClass.NestedClass.RecursiveNestedClass; import org.junit.jupiter.engine.NestedTestClassesTests.OuterClass.NestedClass.RecursiveNestedSiblingClass; +import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.testkit.engine.EngineExecutionResults; import org.junit.platform.testkit.engine.Events; @@ -95,7 +101,14 @@ void doublyNestedTestsAreExecuted() { @Test void inheritedNestedTestsAreExecuted() { - EngineExecutionResults executionResults = executeTestsForClass(TestCaseWithInheritedNested.class); + var discoveryIssues = discoverTestsForClass(TestCaseWithInheritedNested.class).getDiscoveryIssues(); + assertThat(discoveryIssues).hasSize(1); + assertThat(discoveryIssues.getFirst().source()) // + .contains(ClassSource.from(InterfaceWithNestedClass.NestedInInterface.class)); + + var executionResults = executeTests(request -> request // + .selectors(selectClass(TestCaseWithInheritedNested.class)) // + .configurationParameter(CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, Severity.ERROR.name())); Events containers = executionResults.containerEvents(); Events tests = executionResults.testEvents(); @@ -109,7 +122,14 @@ void inheritedNestedTestsAreExecuted() { @Test void extendedNestedTestsAreExecuted() { - var executionResults = executeTestsForClass(TestCaseWithExtendedNested.class); + var discoveryIssues = discoverTestsForClass(TestCaseWithExtendedNested.class).getDiscoveryIssues(); + assertThat(discoveryIssues).hasSize(1); + assertThat(discoveryIssues.getFirst().source()) // + .contains(ClassSource.from(InterfaceWithNestedClass.NestedInInterface.class)); + + var executionResults = executeTests(request -> request // + .selectors(selectClass(TestCaseWithExtendedNested.class)) // + .configurationParameter(CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, Severity.ERROR.name())); Events containers = executionResults.containerEvents(); Events tests = executionResults.testEvents(); @@ -123,10 +143,21 @@ void extendedNestedTestsAreExecuted() { @Test void deeplyNestedInheritedMethodsAreExecutedWhenSelectedViaUniqueId() { - var executionResults = executeTests(selectUniqueId( - "[engine:junit-jupiter]/[class:org.junit.jupiter.engine.NestedTestClassesTests$TestCaseWithExtendedNested]/[nested-class:ConcreteInner1]/[nested-class:NestedInAbstractClass]/[nested-class:SecondLevelInherited]/[method:test()]"), + var selectors = List.of( // + selectUniqueId( + "[engine:junit-jupiter]/[class:org.junit.jupiter.engine.NestedTestClassesTests$TestCaseWithExtendedNested]/[nested-class:ConcreteInner1]/[nested-class:NestedInAbstractClass]/[nested-class:SecondLevelInherited]/[method:test()]"), selectUniqueId( "[engine:junit-jupiter]/[class:org.junit.jupiter.engine.NestedTestClassesTests$TestCaseWithExtendedNested]/[nested-class:ConcreteInner2]/[nested-class:NestedInAbstractClass]/[nested-class:SecondLevelInherited]/[method:test()]")); + + var discoveryIssues = discoverTests(request -> request.selectors(selectors)).getDiscoveryIssues(); + assertThat(discoveryIssues).hasSize(1); + assertThat(discoveryIssues.getFirst().source()) // + .contains(ClassSource.from(InterfaceWithNestedClass.NestedInInterface.class)); + + var executionResults = executeTests(request -> request // + .selectors(selectors) // + .configurationParameter(CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, Severity.ERROR.name())); + Events containers = executionResults.containerEvents(); Events tests = executionResults.testEvents(); @@ -281,7 +312,7 @@ void failing() { interface InterfaceWithNestedClass { - @SuppressWarnings("JUnitMalformedDeclaration") + @SuppressWarnings({ "JUnitMalformedDeclaration", "NewClassNamingConvention" }) @Nested class NestedInInterface { @@ -333,7 +364,7 @@ class ConcreteInner2 extends AbstractSuperClass { static class AbstractOuterClass { } - @SuppressWarnings("JUnitMalformedDeclaration") + @SuppressWarnings({ "JUnitMalformedDeclaration", "NewClassNamingConvention" }) static class OuterClass extends AbstractOuterClass { @Test diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/NonVoidTestableMethodIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/NonVoidTestableMethodIntegrationTests.java deleted file mode 100644 index c00cac6960dd..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/NonVoidTestableMethodIntegrationTests.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2015-2025 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.engine; - -import static org.junit.jupiter.api.Assertions.fail; - -import org.junit.jupiter.api.RepeatedTest; -import org.junit.jupiter.api.Test; - -class NonVoidTestableMethodIntegrationTests { - - @Test - void valid() { - } - - @SuppressWarnings("JUnitMalformedDeclaration") - @Test - int invalidMethodReturningPrimitive() { - fail("This method should never have been called."); - return 1; - } - - @SuppressWarnings("JUnitMalformedDeclaration") - @Test - String invalidMethodReturningObject() { - fail("This method should never have been called."); - return ""; - } - - @RepeatedTest(3) - int invalidMethodVerifyingTestTemplateMethod() { - fail("This method should never have been called."); - return 1; - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java index 677df08911b0..5cb6dba4dd82 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java @@ -61,7 +61,6 @@ import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; @@ -924,7 +923,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } - private static class CustomCloseableResource implements CloseableResource { + private static class CustomCloseableResource implements AutoCloseable { static boolean closed; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/DisplayNameUtilsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/DisplayNameUtilsTests.java index eb7b16f74964..ee7aa76cd0cd 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/DisplayNameUtilsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/DisplayNameUtilsTests.java @@ -25,7 +25,6 @@ import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.platform.commons.logging.LogRecordListener; @@ -48,15 +47,12 @@ void shouldGetDisplayNameFromDisplayNameAnnotation() { } @Test - void shouldGetDisplayNameFromSupplierIfNoDisplayNameAnnotationWithBlankStringPresent( - @TrackLogRecords LogRecordListener listener) { + void shouldGetDisplayNameFromSupplierIfDisplayNameAnnotationProvidesBlankString() { String displayName = DisplayNameUtils.determineDisplayName(BlankDisplayNameTestCase.class, () -> "default-name"); assertThat(displayName).isEqualTo("default-name"); - assertThat(firstWarningLogRecord(listener).getMessage()).isEqualTo( - "Configuration error: @DisplayName on [class org.junit.jupiter.engine.descriptor.DisplayNameUtilsTests$BlankDisplayNameTestCase] must be declared with a non-blank value."); } @Test @@ -189,7 +185,7 @@ private LogRecord firstWarningLogRecord(LogRecordListener listener) throws Asser () -> new AssertionError("Failed to find warning log record")); } - @DisplayName("my-test-case") + @DisplayName("my-test-case\t") @DisplayNameGeneration(value = CustomDisplayNameGenerator.class) static class MyTestCase { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java index ee9716201092..2417b145c380 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java @@ -65,6 +65,7 @@ import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.engine.support.hierarchical.OpenTest4JAwareThrowableCollector; +import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders; import org.mockito.ArgumentCaptor; /** @@ -78,6 +79,8 @@ public class ExtensionContextTests { private final JupiterConfiguration configuration = mock(); private final ExtensionRegistry extensionRegistry = mock(); + private final LauncherStoreFacade launcherStoreFacade = new LauncherStoreFacade( + NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore()); @BeforeEach void setUp() { @@ -92,7 +95,7 @@ void fromJupiterEngineDescriptor() { var engineTestDescriptor = new JupiterEngineDescriptor(UniqueId.root("engine", "junit-jupiter"), configuration); try (var engineContext = new JupiterEngineExtensionContext(null, engineTestDescriptor, configuration, - extensionRegistry)) { + extensionRegistry, launcherStoreFacade)) { // @formatter:off assertAll("engineContext", () -> assertThat(engineContext.getElement()).isEmpty(), @@ -113,6 +116,7 @@ void fromJupiterEngineDescriptor() { } @Test + @SuppressWarnings("resource") void fromClassTestDescriptor() { var nestedClassDescriptor = nestedClassDescriptor(); var outerClassDescriptor = outerClassDescriptor(nestedClassDescriptor); @@ -122,7 +126,7 @@ void fromClassTestDescriptor() { nestedClassDescriptor.addChild(methodTestDescriptor); var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD, - configuration, extensionRegistry, null); + configuration, extensionRegistry, launcherStoreFacade, null); // @formatter:off assertAll("outerContext", @@ -142,7 +146,7 @@ void fromClassTestDescriptor() { // @formatter:on var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor, - PER_METHOD, configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null); // @formatter:off assertAll("nestedContext", () -> assertThat(nestedExtensionContext.getParent()).containsSame(outerExtensionContext), @@ -152,7 +156,7 @@ void fromClassTestDescriptor() { // @formatter:on var doublyNestedExtensionContext = new ClassExtensionContext(nestedExtensionContext, null, - doublyNestedClassDescriptor, PER_METHOD, configuration, extensionRegistry, null); + doublyNestedClassDescriptor, PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null); // @formatter:off assertAll("doublyNestedContext", () -> assertThat(doublyNestedExtensionContext.getParent()).containsSame(nestedExtensionContext), @@ -162,7 +166,7 @@ void fromClassTestDescriptor() { // @formatter:on var methodExtensionContext = new MethodExtensionContext(nestedExtensionContext, null, methodTestDescriptor, - configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); + configuration, extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector()); // @formatter:off assertAll("methodContext", () -> assertThat(methodExtensionContext.getParent()).containsSame(nestedExtensionContext), @@ -176,7 +180,7 @@ void fromClassTestDescriptor() { void ExtensionContext_With_ExtensionRegistry_getExtensions() { var classTestDescriptor = nestedClassDescriptor(); try (var ctx = new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, configuration, - extensionRegistry, null)) { + extensionRegistry, launcherStoreFacade, null)) { Extension ext = mock(); when(extensionRegistry.getExtensions(Extension.class)).thenReturn(List.of(ext)); @@ -186,6 +190,7 @@ void ExtensionContext_With_ExtensionRegistry_getExtensions() { } @Test + @SuppressWarnings("resource") void tagsCanBeRetrievedInExtensionContext() { var nestedClassDescriptor = nestedClassDescriptor(); var outerClassDescriptor = outerClassDescriptor(nestedClassDescriptor); @@ -193,24 +198,25 @@ void tagsCanBeRetrievedInExtensionContext() { outerClassDescriptor.addChild(methodTestDescriptor); var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD, - configuration, extensionRegistry, null); + configuration, extensionRegistry, launcherStoreFacade, null); assertThat(outerExtensionContext.getTags()).containsExactly("outer-tag"); assertThat(outerExtensionContext.getRoot()).isSameAs(outerExtensionContext); var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor, - PER_METHOD, configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null); assertThat(nestedExtensionContext.getTags()).containsExactlyInAnyOrder("outer-tag", "nested-tag"); assertThat(nestedExtensionContext.getRoot()).isSameAs(outerExtensionContext); var methodExtensionContext = new MethodExtensionContext(outerExtensionContext, null, methodTestDescriptor, - configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); + configuration, extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector()); methodExtensionContext.setTestInstances(DefaultTestInstances.of(new OuterClassTestCase())); assertThat(methodExtensionContext.getTags()).containsExactlyInAnyOrder("outer-tag", "method-tag"); assertThat(methodExtensionContext.getRoot()).isSameAs(outerExtensionContext); } @Test + @SuppressWarnings("resource") void fromMethodTestDescriptor() { var methodTestDescriptor = methodDescriptor(); var classTestDescriptor = outerClassDescriptor(methodTestDescriptor); @@ -221,11 +227,11 @@ void fromMethodTestDescriptor() { var testMethod = methodTestDescriptor.getTestMethod(); var engineExtensionContext = new JupiterEngineExtensionContext(null, engineDescriptor, configuration, - extensionRegistry); + extensionRegistry, launcherStoreFacade); var classExtensionContext = new ClassExtensionContext(engineExtensionContext, null, classTestDescriptor, - PER_METHOD, configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null); var methodExtensionContext = new MethodExtensionContext(classExtensionContext, null, methodTestDescriptor, - configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); + configuration, extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector()); methodExtensionContext.setTestInstances(DefaultTestInstances.of(testInstance)); // @formatter:off @@ -252,7 +258,7 @@ void reportEntriesArePublishedToExecutionListener() { var classTestDescriptor = outerClassDescriptor(null); var engineExecutionListener = spy(EngineExecutionListener.class); ExtensionContext extensionContext = new ClassExtensionContext(null, engineExecutionListener, - classTestDescriptor, PER_METHOD, configuration, extensionRegistry, null); + classTestDescriptor, PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null); var map1 = Collections.singletonMap("key", "value"); var map2 = Collections.singletonMap("other key", "other value"); @@ -378,7 +384,7 @@ private ExtensionContext createExtensionContextForFilePublishing(Path tempDir, when(configuration.getOutputDirectoryProvider()) // .thenReturn(hierarchicalOutputDirectoryProvider(tempDir)); return new ClassExtensionContext(null, engineExecutionListener, classTestDescriptor, PER_METHOD, configuration, - extensionRegistry, null); + extensionRegistry, launcherStoreFacade, null); } @Test @@ -387,9 +393,9 @@ void usingStore() { var methodTestDescriptor = methodDescriptor(); var classTestDescriptor = outerClassDescriptor(methodTestDescriptor); ExtensionContext parentContext = new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, - configuration, extensionRegistry, null); + configuration, extensionRegistry, launcherStoreFacade, null); var childContext = new MethodExtensionContext(parentContext, null, methodTestDescriptor, configuration, - extensionRegistry, new OpenTest4JAwareThrowableCollector()); + extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector()); childContext.setTestInstances(DefaultTestInstances.of(new OuterClassTestCase())); var childStore = childContext.getStore(Namespace.GLOBAL); @@ -436,18 +442,20 @@ void configurationParameter(Function>> extensionContextFactories() { ExtensionRegistry extensionRegistry = mock(); + LauncherStoreFacade launcherStoreFacade = mock(); var testClass = ExtensionContextTests.class; return List.of( // named("engine", (JupiterConfiguration configuration) -> { var engineUniqueId = UniqueId.parse("[engine:junit-jupiter]"); var engineDescriptor = new JupiterEngineDescriptor(engineUniqueId, configuration); - return new JupiterEngineExtensionContext(null, engineDescriptor, configuration, extensionRegistry); + return new JupiterEngineExtensionContext(null, engineDescriptor, configuration, extensionRegistry, + launcherStoreFacade); }), // named("class", (JupiterConfiguration configuration) -> { var classUniqueId = UniqueId.parse("[engine:junit-jupiter]/[class:MyClass]"); var classTestDescriptor = new ClassTestDescriptor(classUniqueId, testClass, configuration); return new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, configuration, - extensionRegistry, null); + extensionRegistry, launcherStoreFacade, null); }), // named("method", (JupiterConfiguration configuration) -> { var method = ReflectionSupport.findMethod(testClass, "extensionContextFactories").orElseThrow(); @@ -455,7 +463,7 @@ void configurationParameter(Function requestLevelStore; + private NamespacedHierarchicalStore sessionLevelStore; + private ExtensionContext.Namespace extensionNamespace; + + @BeforeEach + void setUp() { + sessionLevelStore = new NamespacedHierarchicalStore<>(null); + requestLevelStore = new NamespacedHierarchicalStore<>(sessionLevelStore); + extensionNamespace = ExtensionContext.Namespace.create("foo", "bar"); + } + + @Test + void createsInstanceSuccessfullyWithValidStore() { + assertDoesNotThrow(() -> new LauncherStoreFacade(requestLevelStore)); + } + + @Test + void throwsExceptionWhenRequestLevelStoreHasNoParent() { + assertThrowsExactly(JUnitException.class, () -> new LauncherStoreFacade(sessionLevelStore), () -> { + throw new JUnitException("Request-level store must have a parent"); + }); + } + + @Test + void returnsRequestLevelStore() { + LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore); + assertEquals(requestLevelStore, facade.getRequestLevelStore()); + } + + @Test + void returnsNamespaceAwareStoreWithRequestLevelStore() { + LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore); + ExtensionContext.Store store = facade.getRequestLevelStore(extensionNamespace); + + assertNotNull(store); + assertInstanceOf(NamespaceAwareStore.class, store); + } + + @Test + void returnsNamespaceAwareStore() { + LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore); + NamespaceAwareStore adapter = facade.getStoreAdapter(requestLevelStore, extensionNamespace); + + assertNotNull(adapter); + } + + @Test + void throwsExceptionWhenNamespaceIsNull() { + LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore); + assertThrows(PreconditionViolationException.class, () -> facade.getStoreAdapter(requestLevelStore, null)); + } + + @Test + void returnsNamespaceAwareStoreWithGlobalNamespace() { + requestLevelStore.put(Namespace.GLOBAL, "foo", "bar"); + + LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore); + ExtensionContext.Store store = facade.getRequestLevelStore(ExtensionContext.Namespace.GLOBAL); + + assertEquals("bar", store.get("foo")); + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtilsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtilsTests.java index 2da2aa759d2e..d4f56b9b684f 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtilsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/LifecycleMethodUtilsTests.java @@ -32,6 +32,7 @@ import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; /** * Unit tests for {@link LifecycleMethodUtils}. @@ -41,10 +42,11 @@ class LifecycleMethodUtilsTests { List discoveryIssues = new ArrayList<>(); + DiscoveryIssueReporter issueReporter = DiscoveryIssueReporter.collecting(discoveryIssues); @Test void findNonVoidBeforeAllMethodsWithStandardLifecycle() throws Exception { - var methods = findBeforeAllMethods(TestCaseWithInvalidLifecycleMethods.class, true, discoveryIssues::add); + var methods = findBeforeAllMethods(TestCaseWithInvalidLifecycleMethods.class, true, issueReporter); assertThat(methods).isEmpty(); var methodSource = MethodSource.from(TestCaseWithInvalidLifecycleMethods.class.getDeclaredMethod("cc")); @@ -56,7 +58,7 @@ void findNonVoidBeforeAllMethodsWithStandardLifecycle() throws Exception { "@BeforeAll method 'private java.lang.Double org.junit.jupiter.engine.descriptor.TestCaseWithInvalidLifecycleMethods.cc()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).") // .source(methodSource) // .build(); - var privateIssue = DiscoveryIssue.builder(Severity.DEPRECATION, + var privateIssue = DiscoveryIssue.builder(Severity.WARNING, "@BeforeAll method 'private java.lang.Double org.junit.jupiter.engine.descriptor.TestCaseWithInvalidLifecycleMethods.cc()' should not be private. This will be disallowed in a future release.") // .source(methodSource) // .build(); @@ -65,7 +67,7 @@ void findNonVoidBeforeAllMethodsWithStandardLifecycle() throws Exception { @Test void findNonVoidAfterAllMethodsWithStandardLifecycle() throws Exception { - var methods = findAfterAllMethods(TestCaseWithInvalidLifecycleMethods.class, true, discoveryIssues::add); + var methods = findAfterAllMethods(TestCaseWithInvalidLifecycleMethods.class, true, issueReporter); assertThat(methods).isEmpty(); var methodSource = MethodSource.from(TestCaseWithInvalidLifecycleMethods.class.getDeclaredMethod("dd")); @@ -77,7 +79,7 @@ void findNonVoidAfterAllMethodsWithStandardLifecycle() throws Exception { "@AfterAll method 'private java.lang.String org.junit.jupiter.engine.descriptor.TestCaseWithInvalidLifecycleMethods.dd()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).") // .source(methodSource) // .build(); - var privateIssue = DiscoveryIssue.builder(Severity.DEPRECATION, + var privateIssue = DiscoveryIssue.builder(Severity.WARNING, "@AfterAll method 'private java.lang.String org.junit.jupiter.engine.descriptor.TestCaseWithInvalidLifecycleMethods.dd()' should not be private. This will be disallowed in a future release.") // .source(methodSource) // .build(); @@ -86,7 +88,7 @@ void findNonVoidAfterAllMethodsWithStandardLifecycle() throws Exception { @Test void findNonVoidBeforeEachMethodsWithStandardLifecycle() throws Exception { - var methods = findBeforeEachMethods(TestCaseWithInvalidLifecycleMethods.class, discoveryIssues::add); + var methods = findBeforeEachMethods(TestCaseWithInvalidLifecycleMethods.class, issueReporter); assertThat(methods).isEmpty(); var methodSource = MethodSource.from(TestCaseWithInvalidLifecycleMethods.class.getDeclaredMethod("aa")); @@ -94,7 +96,7 @@ void findNonVoidBeforeEachMethodsWithStandardLifecycle() throws Exception { "@BeforeEach method 'private java.lang.String org.junit.jupiter.engine.descriptor.TestCaseWithInvalidLifecycleMethods.aa()' must not return a value.") // .source(methodSource) // .build(); - var privateIssue = DiscoveryIssue.builder(Severity.DEPRECATION, + var privateIssue = DiscoveryIssue.builder(Severity.WARNING, "@BeforeEach method 'private java.lang.String org.junit.jupiter.engine.descriptor.TestCaseWithInvalidLifecycleMethods.aa()' should not be private. This will be disallowed in a future release.") // .source(methodSource) // .build(); @@ -103,7 +105,7 @@ void findNonVoidBeforeEachMethodsWithStandardLifecycle() throws Exception { @Test void findNonVoidAfterEachMethodsWithStandardLifecycle() throws Exception { - var methods = findAfterEachMethods(TestCaseWithInvalidLifecycleMethods.class, discoveryIssues::add); + var methods = findAfterEachMethods(TestCaseWithInvalidLifecycleMethods.class, issueReporter); assertThat(methods).isEmpty(); var methodSource = MethodSource.from(TestCaseWithInvalidLifecycleMethods.class.getDeclaredMethod("bb")); @@ -111,7 +113,7 @@ void findNonVoidAfterEachMethodsWithStandardLifecycle() throws Exception { "@AfterEach method 'private int org.junit.jupiter.engine.descriptor.TestCaseWithInvalidLifecycleMethods.bb()' must not return a value.") // .source(methodSource) // .build(); - var privateIssue = DiscoveryIssue.builder(Severity.DEPRECATION, + var privateIssue = DiscoveryIssue.builder(Severity.WARNING, "@AfterEach method 'private int org.junit.jupiter.engine.descriptor.TestCaseWithInvalidLifecycleMethods.bb()' should not be private. This will be disallowed in a future release.") // .source(methodSource) // .build(); @@ -120,7 +122,7 @@ void findNonVoidAfterEachMethodsWithStandardLifecycle() throws Exception { @Test void findBeforeEachMethodsWithStandardLifecycle() { - List methods = findBeforeEachMethods(TestCaseWithStandardLifecycle.class, discoveryIssues::add); + List methods = findBeforeEachMethods(TestCaseWithStandardLifecycle.class, issueReporter); assertThat(namesOf(methods)).containsExactlyInAnyOrder("nine", "ten"); assertThat(discoveryIssues).isEmpty(); @@ -128,14 +130,14 @@ void findBeforeEachMethodsWithStandardLifecycle() { @Test void findAfterEachMethodsWithStandardLifecycle() { - List methods = findAfterEachMethods(TestCaseWithStandardLifecycle.class, discoveryIssues::add); + List methods = findAfterEachMethods(TestCaseWithStandardLifecycle.class, issueReporter); assertThat(namesOf(methods)).containsExactlyInAnyOrder("eleven", "twelve"); } @Test void findBeforeAllMethodsWithStandardLifecycleAndWithoutRequiringStatic() { - List methods = findBeforeAllMethods(TestCaseWithStandardLifecycle.class, false, discoveryIssues::add); + List methods = findBeforeAllMethods(TestCaseWithStandardLifecycle.class, false, issueReporter); assertThat(namesOf(methods)).containsExactly("one"); assertThat(discoveryIssues).isEmpty(); @@ -143,7 +145,7 @@ void findBeforeAllMethodsWithStandardLifecycleAndWithoutRequiringStatic() { @Test void findBeforeAllMethodsWithStandardLifecycleAndRequiringStatic() throws Exception { - var methods = findBeforeAllMethods(TestCaseWithStandardLifecycle.class, true, discoveryIssues::add); + var methods = findBeforeAllMethods(TestCaseWithStandardLifecycle.class, true, issueReporter); assertThat(methods).isEmpty(); var expectedIssue = DiscoveryIssue.builder(Severity.ERROR, @@ -155,7 +157,7 @@ void findBeforeAllMethodsWithStandardLifecycleAndRequiringStatic() throws Except @Test void findBeforeAllMethodsWithLifeCyclePerClassAndRequiringStatic() { - List methods = findBeforeAllMethods(TestCaseWithLifecyclePerClass.class, false, discoveryIssues::add); + List methods = findBeforeAllMethods(TestCaseWithLifecyclePerClass.class, false, issueReporter); assertThat(namesOf(methods)).containsExactlyInAnyOrder("three", "four"); assertThat(discoveryIssues).isEmpty(); @@ -163,7 +165,7 @@ void findBeforeAllMethodsWithLifeCyclePerClassAndRequiringStatic() { @Test void findAfterAllMethodsWithStandardLifecycleAndWithoutRequiringStatic() { - List methods = findAfterAllMethods(TestCaseWithStandardLifecycle.class, false, discoveryIssues::add); + List methods = findAfterAllMethods(TestCaseWithStandardLifecycle.class, false, issueReporter); assertThat(namesOf(methods)).containsExactlyInAnyOrder("five", "six"); assertThat(discoveryIssues).isEmpty(); @@ -171,7 +173,7 @@ void findAfterAllMethodsWithStandardLifecycleAndWithoutRequiringStatic() { @Test void findAfterAllMethodsWithStandardLifecycleAndRequiringStatic() { - var methods = findAfterAllMethods(TestCaseWithStandardLifecycle.class, true, discoveryIssues::add); + var methods = findAfterAllMethods(TestCaseWithStandardLifecycle.class, true, issueReporter); assertThat(methods).isEmpty(); assertThat(discoveryIssues) // @@ -181,7 +183,7 @@ void findAfterAllMethodsWithStandardLifecycleAndRequiringStatic() { @Test void findAfterAllMethodsWithLifeCyclePerClassAndRequiringStatic() { - List methods = findAfterAllMethods(TestCaseWithLifecyclePerClass.class, false, discoveryIssues::add); + List methods = findAfterAllMethods(TestCaseWithLifecyclePerClass.class, false, issueReporter); assertThat(namesOf(methods)).containsExactlyInAnyOrder("seven", "eight"); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ResourceAutoClosingTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ResourceAutoClosingTests.java new file mode 100644 index 000000000000..ce217f3b838f --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ResourceAutoClosingTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2015-2025 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.engine.descriptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.fixtures.TrackLogRecords; +import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.extension.ExtensionRegistry; +import org.junit.platform.commons.logging.LogRecordListener; +import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders; +import org.junit.platform.testkit.engine.ExecutionRecorder; + +class ResourceAutoClosingTests { + + private final JupiterConfiguration configuration = mock(); + private final ExtensionRegistry extensionRegistry = mock(); + private final JupiterEngineDescriptor testDescriptor = mock(); + private final LauncherStoreFacade launcherStoreFacade = new LauncherStoreFacade( + NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore()); + + @Test + void shouldCloseAutoCloseableWhenIsClosingStoredAutoCloseablesEnabledIsTrue() throws Exception { + AutoCloseableResource resource = new AutoCloseableResource(); + when(configuration.isClosingStoredAutoCloseablesEnabled()).thenReturn(true); + + ExtensionContext extensionContext = new JupiterEngineExtensionContext(null, testDescriptor, configuration, + extensionRegistry, launcherStoreFacade); + ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL); + store.put("resource", resource); + + ((AutoCloseable) extensionContext).close(); + + assertThat(resource.closed).isTrue(); + } + + @Test + void shouldNotCloseAutoCloseableWhenIsClosingStoredAutoCloseablesEnabledIsFalse() throws Exception { + AutoCloseableResource resource = new AutoCloseableResource(); + when(configuration.isClosingStoredAutoCloseablesEnabled()).thenReturn(false); + + ExtensionContext extensionContext = new JupiterEngineExtensionContext(null, testDescriptor, configuration, + extensionRegistry, launcherStoreFacade); + ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL); + store.put("resource", resource); + + ((AutoCloseable) extensionContext).close(); + + assertThat(resource.closed).isFalse(); + } + + @Test + void shouldLogWarningWhenResourceImplementsCloseableResourceButNotAutoCloseableAndConfigIsTrue( + @TrackLogRecords LogRecordListener listener) throws Exception { + ExecutionRecorder executionRecorder = new ExecutionRecorder(); + CloseableResource resource = new CloseableResource(); + String msg = "Type implements CloseableResource but not AutoCloseable: " + resource.getClass().getName(); + when(configuration.isClosingStoredAutoCloseablesEnabled()).thenReturn(true); + + ExtensionContext extensionContext = new JupiterEngineExtensionContext(executionRecorder, testDescriptor, + configuration, extensionRegistry, launcherStoreFacade); + ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL); + store.put("resource", resource); + + ((AutoCloseable) extensionContext).close(); + + assertThat(listener.stream(Level.WARNING)).map(LogRecord::getMessage).contains(msg); + assertThat(resource.closed).isTrue(); + } + + @Test + void shouldNotLogWarningWhenResourceImplementsCloseableResourceAndAutoCloseableAndConfigIsFalse( + @TrackLogRecords LogRecordListener listener) throws Exception { + ExecutionRecorder executionRecorder = new ExecutionRecorder(); + CloseableResource resource = new CloseableResource(); + when(configuration.isClosingStoredAutoCloseablesEnabled()).thenReturn(false); + + ExtensionContext extensionContext = new JupiterEngineExtensionContext(executionRecorder, testDescriptor, + configuration, extensionRegistry, launcherStoreFacade); + ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL); + store.put("resource", resource); + + ((AutoCloseable) extensionContext).close(); + + assertThat(listener.stream(Level.WARNING)).isEmpty(); + assertThat(resource.closed).isTrue(); + } + + static class AutoCloseableResource implements AutoCloseable { + private boolean closed = false; + + @Override + public void close() { + closed = true; + } + } + + @SuppressWarnings("deprecation") + static class CloseableResource implements ExtensionContext.Store.CloseableResource { + private boolean closed = false; + + @Override + public void close() { + closed = true; + } + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java index ad1ef49fc983..d072738824d9 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java @@ -154,17 +154,16 @@ class Streams { private ExtensionContext extensionContext; private TestFactoryTestDescriptor descriptor; private boolean isClosed; - private JupiterConfiguration jupiterConfiguration; @BeforeEach void before() throws Exception { - jupiterConfiguration = mock(); + JupiterConfiguration jupiterConfiguration = mock(); when(jupiterConfiguration.getDefaultDisplayNameGenerator()).thenReturn(new DisplayNameGenerator.Standard()); extensionContext = mock(); isClosed = false; - context = new JupiterEngineExecutionContext(null, null) // + context = new JupiterEngineExecutionContext(null, null, null) // .extend() // .withThrowableCollector(new OpenTest4JAwareThrowableCollector()) // .withExtensionContext(extensionContext) // diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoveryTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoveryTests.java index 002980399131..ab2d904f3253 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoveryTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoveryTests.java @@ -10,13 +10,20 @@ package org.junit.jupiter.engine.discovery; +import static java.util.Comparator.comparing; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Named.named; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestTemplateMethod; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; +import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectNestedClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectNestedMethod; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; @@ -24,8 +31,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.util.List; +import java.util.regex.Pattern; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestTemplate; @@ -34,7 +45,12 @@ import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; import org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor; import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.launcher.LauncherDiscoveryRequest; /** @@ -160,13 +176,168 @@ void discoverDeeplyNestedTestMethodByNestedMethodSelector() throws Exception { assertThat(methodDescriptor.getTestMethod().getName()).isEqualTo("test"); } + @ParameterizedTest + @MethodSource("requestsForTestClassWithInvalidTestMethod") + void reportsWarningForTestClassWithInvalidTestMethod(LauncherDiscoveryRequest request) throws Exception { + + var method = InvalidTestCases.InvalidTestMethodTestCase.class.getDeclaredMethod("test"); + + var results = discoverTests(request); + + var discoveryIssues = results.getDiscoveryIssues().stream().sorted(comparing(DiscoveryIssue::message)).toList(); + assertThat(discoveryIssues).hasSize(3); + assertThat(discoveryIssues.getFirst().message()) // + .isEqualTo("@Test method '%s' must not be private. It will not be executed.", method.toGenericString()); + assertThat(discoveryIssues.get(1).message()) // + .isEqualTo("@Test method '%s' must not be static. It will not be executed.", method.toGenericString()); + assertThat(discoveryIssues.getLast().message()) // + .isEqualTo("@Test method '%s' must not return a value. It will not be executed.", + method.toGenericString()); + } + + static List> requestsForTestClassWithInvalidTestMethod() { + return List.of( // + named("directly selected", + request().selectors(selectClass(InvalidTestCases.InvalidTestMethodTestCase.class)).build()), // + named("indirectly selected", request() // + .selectors(selectPackage(InvalidTestCases.InvalidTestMethodTestCase.class.getPackageName())) // + .filters(includeClassNamePatterns( + Pattern.quote(InvalidTestCases.InvalidTestMethodTestCase.class.getName()))).build()), // + named("subclasses", request() // + .selectors(selectClass(InvalidTestCases.InvalidTestMethodSubclass1TestCase.class), + selectClass(InvalidTestCases.InvalidTestMethodSubclass2TestCase.class)) // + .build()) // + ); + } + + @ParameterizedTest + @MethodSource("requestsForTestClassWithInvalidStandaloneTestClass") + void reportsWarningForInvalidStandaloneTestClass(LauncherDiscoveryRequest request, Class testClass) { + + var results = discoverTests(request); + + var discoveryIssues = results.getDiscoveryIssues().stream().sorted(comparing(DiscoveryIssue::message)).toList(); + assertThat(discoveryIssues).hasSize(2); + assertThat(discoveryIssues.getFirst().message()) // + .isEqualTo( + "Test class '%s' must not be an inner class unless annotated with @Nested. It will not be executed.", + testClass.getName()); + assertThat(discoveryIssues.getLast().message()) // + .isEqualTo("Test class '%s' must not be private. It will not be executed.", testClass.getName()); + } + + static List requestsForTestClassWithInvalidStandaloneTestClass() { + return List.of( // + argumentSet("directly selected", + request().selectors(selectClass(InvalidTestCases.InvalidTestClassTestCase.class)).build(), + InvalidTestCases.InvalidTestClassTestCase.class), // + argumentSet("indirectly selected", request() // + .selectors(selectPackage(InvalidTestCases.InvalidTestClassTestCase.class.getPackageName())) // + .filters(includeClassNamePatterns( + Pattern.quote(InvalidTestCases.InvalidTestClassTestCase.class.getName()))).build(), // + InvalidTestCases.InvalidTestClassTestCase.class), // + argumentSet("subclass", request() // + .selectors(selectClass(InvalidTestCases.InvalidTestClassSubclassTestCase.class)) // + .build(), // + InvalidTestCases.InvalidTestClassSubclassTestCase.class) // + ); + } + + @ParameterizedTest + @MethodSource("requestsForTestClassWithInvalidNestedTestClass") + void reportsWarningForInvalidNestedTestClass(LauncherDiscoveryRequest request) { + + var results = discoverTests(request); + + var discoveryIssues = results.getDiscoveryIssues().stream().sorted(comparing(DiscoveryIssue::message)).toList(); + assertThat(discoveryIssues).hasSize(2); + assertThat(discoveryIssues.getFirst().message()) // + .isEqualTo("@Nested class '%s' must not be private. It will not be executed.", + InvalidTestCases.InvalidTestClassTestCase.Inner.class.getName()); + assertThat(discoveryIssues.getLast().message()) // + .isEqualTo("@Nested class '%s' must not be static. It will not be executed.", + InvalidTestCases.InvalidTestClassTestCase.Inner.class.getName()); + } + + static List> requestsForTestClassWithInvalidNestedTestClass() { + return List.of( // + named("directly selected", + request().selectors(selectClass(InvalidTestCases.InvalidTestClassTestCase.Inner.class)).build()), // + named("subclass", request() // + .selectors(selectNestedClass(List.of(InvalidTestCases.InvalidTestClassSubclassTestCase.class), + InvalidTestCases.InvalidTestClassTestCase.Inner.class)) // + .build()) // + ); + } + + @Test + void reportsWarningForTestClassWithPotentialNestedTestClasses() { + + var results = discoverTestsForClass(InvalidTestCases.class); + + var discoveryIssues = results.getDiscoveryIssues().stream().sorted(comparing(DiscoveryIssue::message)).toList(); + assertThat(discoveryIssues).hasSize(2); + assertThat(discoveryIssues.getFirst().message()) // + .isEqualTo( + "Inner class '%s' looks like it was intended to be a test class but will not be executed. It must be static or annotated with @Nested.", + InvalidTestCases.InvalidTestClassSubclassTestCase.class.getName()); + assertThat(discoveryIssues.getLast().message()) // + .isEqualTo( + "Inner class '%s' looks like it was intended to be a test class but will not be executed. It must be static or annotated with @Nested.", + InvalidTestCases.InvalidTestClassTestCase.class.getName()); + } + + @Test + void reportsWarningsForInvalidTags() throws NoSuchMethodException { + + var results = discoverTestsForClass(InvalidTagsTestCase.class); + + var discoveryIssues = results.getDiscoveryIssues().stream().sorted(comparing(DiscoveryIssue::message)).toList(); + assertThat(discoveryIssues).hasSize(2); + + assertThat(discoveryIssues.getFirst().message()) // + .isEqualTo("Invalid tag syntax in @Tag(\"\") declaration on class '%s'. Tag will be ignored.", + InvalidTagsTestCase.class.getName()); + assertThat(discoveryIssues.getFirst().source()) // + .contains(ClassSource.from(InvalidTagsTestCase.class)); + + var method = InvalidTagsTestCase.class.getDeclaredMethod("test"); + assertThat(discoveryIssues.getLast().message()) // + .isEqualTo("Invalid tag syntax in @Tag(\"|\") declaration on method '%s'. Tag will be ignored.", + method.toGenericString()); + assertThat(discoveryIssues.getLast().source()) // + .contains(org.junit.platform.engine.support.descriptor.MethodSource.from(method)); + } + + @Test + void reportsWarningsForBlankDisplayNames() throws NoSuchMethodException { + + var results = discoverTestsForClass(BlankDisplayNamesTestCase.class); + + var discoveryIssues = results.getDiscoveryIssues().stream().sorted(comparing(DiscoveryIssue::message)).toList(); + assertThat(discoveryIssues).hasSize(2); + + assertThat(discoveryIssues.getFirst().message()) // + .isEqualTo("@DisplayName on class '%s' must be declared with a non-blank value.", + BlankDisplayNamesTestCase.class.getName()); + assertThat(discoveryIssues.getFirst().source()) // + .contains(ClassSource.from(BlankDisplayNamesTestCase.class)); + + var method = BlankDisplayNamesTestCase.class.getDeclaredMethod("test"); + assertThat(discoveryIssues.getLast().message()) // + .isEqualTo("@DisplayName on method '%s' must be declared with a non-blank value.", + method.toGenericString()); + assertThat(discoveryIssues.getLast().source()) // + .contains(org.junit.platform.engine.support.descriptor.MethodSource.from(method)); + } + // ------------------------------------------------------------------- + @SuppressWarnings("unused") private static abstract class AbstractTestCase { @Test void abstractTest() { - } } @@ -228,4 +399,63 @@ class ConcreteInner1 extends AbstractSuperClass { } } + static class InvalidTestCases { + + @SuppressWarnings("JUnitMalformedDeclaration") + static class InvalidTestMethodTestCase { + @Test + private static int test() { + return fail("should not be called"); + } + } + + static class InvalidTestMethodSubclass1TestCase extends InvalidTestMethodTestCase { + } + + static class InvalidTestMethodSubclass2TestCase extends InvalidTestMethodTestCase { + } + + @SuppressWarnings({ "JUnitMalformedDeclaration", "InnerClassMayBeStatic" }) + private class InvalidTestClassTestCase { + + @SuppressWarnings("unused") + @Test + void test() { + fail("should not be called"); + } + + @Nested + private static class Inner { + @SuppressWarnings("unused") + @Test + void test() { + fail("should not be called"); + } + } + + } + + private class InvalidTestClassSubclassTestCase extends InvalidTestClassTestCase { + } + + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @Tag("") + static class InvalidTagsTestCase { + @Test + @Tag("|") + void test() { + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @DisplayName("") + static class BlankDisplayNamesTestCase { + @Test + @DisplayName("\t") + void test() { + } + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsInnerClassTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsInnerClassTests.java deleted file mode 100644 index d371fd3f2e03..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsInnerClassTests.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2015-2025 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.engine.discovery.predicates; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.function.Predicate; - -import org.junit.jupiter.api.Test; - -/** - * @since 5.0 - */ -class IsInnerClassTests { - - private final Predicate> isInnerClass = new IsInnerClass(); - - @Test - void innerClassEvaluatesToTrue() { - assertThat(isInnerClass).accepts(InnerClassesTestCase.InnerClass.class); - } - - @Test - void staticNestedClassEvaluatesToFalse() { - assertThat(isInnerClass).rejects(InnerClassesTestCase.StaticNestedClass.class); - } - - @Test - void privateInnerClassEvaluatesToFalse() { - assertThat(isInnerClass).rejects(InnerClassesTestCase.PrivateInnerClass.class); - } - - private static class InnerClassesTestCase { - - class InnerClass { - } - - static class StaticNestedClass { - } - - private class PrivateInnerClass { - } - - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsNestedTestClassTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsNestedTestClassTests.java deleted file mode 100644 index 609785f4a3e4..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsNestedTestClassTests.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2015-2025 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.engine.discovery.predicates; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.function.Predicate; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -/** - * @since 5.0 - */ -class IsNestedTestClassTests { - - private final Predicate> isNestedTestClass = new IsNestedTestClass(); - - @Test - void innerClassEvaluatesToTrue() { - assertThat(isNestedTestClass).accepts(NestedClassesTestCase.InnerClass.class); - } - - @Test - void staticNestedClassEvaluatesToFalse() { - assertThat(isNestedTestClass).rejects(NestedClassesTestCase.StaticNestedClass.class); - } - - @Test - void privateNestedClassEvaluatesToFalse() { - assertThat(isNestedTestClass).rejects(NestedClassesTestCase.PrivateInnerClass.class); - } - - private static class NestedClassesTestCase { - - @Nested - class InnerClass { - } - - @SuppressWarnings("JUnitMalformedDeclaration") - @Nested - static class StaticNestedClass { - } - - @SuppressWarnings("JUnitMalformedDeclaration") - @Nested - private class PrivateInnerClass { - } - - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsPotentialTestContainerTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsPotentialTestContainerTests.java deleted file mode 100644 index 47c9c7143815..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsPotentialTestContainerTests.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2015-2025 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.engine.discovery.predicates; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -/** - * @since 5.0 - */ -class IsPotentialTestContainerTests { - - private final IsPotentialTestContainer isPotentialTestContainer = new IsPotentialTestContainer(); - - @Test - void staticClassEvaluatesToTrue() { - assertTrue(isPotentialTestContainer.test(StaticClass.class)); - } - - @Test - void privateStaticClassEvaluatesToFalse() { - assertFalse(isPotentialTestContainer.test(PrivateStaticClass.class)); - } - - @Test - void abstractClassEvaluatesToFalse() { - assertFalse(isPotentialTestContainer.test(AbstractClass.class)); - } - - @Test - void localClassEvaluatesToFalse() { - - class LocalClass { - } - - assertFalse(isPotentialTestContainer.test(LocalClass.class)); - } - - @Test - void anonymousClassEvaluatesToFalse() { - - Object object = new Object() { - @Override - public String toString() { - return ""; - } - }; - - assertFalse(isPotentialTestContainer.test(object.getClass())); - } - - private static class PrivateStaticClass { - } - - static class StaticClass { - } - -} - -abstract class AbstractClass { -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestClassWithTestsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestClassWithTestsTests.java deleted file mode 100644 index c07e5eff03c1..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestClassWithTestsTests.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2015-2025 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.engine.discovery.predicates; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.function.Predicate; - -import org.junit.jupiter.api.DynamicTest; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestFactory; -import org.junit.jupiter.api.TestTemplate; - -/** - * Unit tests for {@link IsTestClassWithTests}. - * - * @since 5.0 - */ -class IsTestClassWithTestsTests { - - private final Predicate> isTestClassWithTests = new IsTestClassWithTests(); - - @Test - void classWithTestMethodEvaluatesToTrue() { - assertTrue(isTestClassWithTests.test(ClassWithTestMethod.class)); - } - - @Test - void classWithTestFactoryEvaluatesToTrue() { - assertTrue(isTestClassWithTests.test(ClassWithTestFactory.class)); - } - - @Test - void classWithTestTemplateEvaluatesToTrue() { - assertTrue(isTestClassWithTests.test(ClassWithTestTemplate.class)); - } - - @Test - void classWithNestedTestClassEvaluatesToTrue() { - assertTrue(isTestClassWithTests.test(ClassWithNestedTestClass.class)); - } - - @Test - void staticTestClassEvaluatesToTrue() { - assertTrue(isTestClassWithTests.test(StaticTestCase.class)); - } - - // ------------------------------------------------------------------------- - - @Test - void privateClassWithTestMethodEvaluatesToFalse() { - assertFalse(isTestClassWithTests.test(PrivateClassWithTestMethod.class)); - } - - @Test - void privateClassWithTestFactoryEvaluatesToFalse() { - assertFalse(isTestClassWithTests.test(PrivateClassWithTestFactory.class)); - } - - @Test - void privateClassWithTestTemplateEvaluatesToFalse() { - assertFalse(isTestClassWithTests.test(PrivateClassWithTestTemplate.class)); - } - - @Test - void privateClassWithNestedTestCasesEvaluatesToFalse() { - assertFalse(isTestClassWithTests.test(PrivateClassWithNestedTestClass.class)); - } - - @Test - void privateStaticTestClassEvaluatesToFalse() { - assertFalse(isTestClassWithTests.test(PrivateStaticTestCase.class)); - } - - /** - * @see https://github.com/junit-team/junit5/issues/2249 - */ - @Test - void recursiveHierarchies() { - assertTrue(isTestClassWithTests.test(OuterClass.class)); - assertFalse(isTestClassWithTests.test(OuterClass.RecursiveInnerClass.class)); - } - - // ------------------------------------------------------------------------- - - @SuppressWarnings("JUnitMalformedDeclaration") - private class PrivateClassWithTestMethod { - - @Test - void test() { - } - - } - - private class PrivateClassWithTestFactory { - - @TestFactory - Collection factory() { - return new ArrayList<>(); - } - - } - - private class PrivateClassWithTestTemplate { - - @TestTemplate - void template(int a) { - } - - } - - private class PrivateClassWithNestedTestClass { - - @Nested - class InnerClass { - - @Test - void first() { - } - - @Test - void second() { - } - - } - } - - // ------------------------------------------------------------------------- - - @SuppressWarnings("JUnitMalformedDeclaration") - static class StaticTestCase { - - @Test - void test() { - } - } - - @SuppressWarnings("JUnitMalformedDeclaration") - private static class PrivateStaticTestCase { - - @Test - void test() { - } - } - - static class OuterClass { - - @Nested - class InnerClass { - - @Test - void test() { - } - } - - // Intentionally commented out so that RecursiveInnerClass is NOT a candidate test class - // @Nested - class RecursiveInnerClass extends OuterClass { - } - } - -} - -// ----------------------------------------------------------------------------- - -class ClassWithTestMethod { - - @Test - void test() { - } - -} - -class ClassWithTestFactory { - - @TestFactory - Collection factory() { - return new ArrayList<>(); - } - -} - -class ClassWithTestTemplate { - - @TestTemplate - void template(int a) { - } - -} - -class ClassWithNestedTestClass { - - @Nested - class InnerClass { - - @Test - void first() { - } - - @Test - void second() { - } - - } -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethodTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethodTests.java index 6487d0927db3..f7b7534e38be 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethodTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethodTests.java @@ -11,17 +11,28 @@ package org.junit.jupiter.engine.discovery.predicates; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.function.Predicate; +import java.util.stream.Stream; -import org.junit.jupiter.api.Disabled; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.DynamicNode; 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.ValueSource; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; /** * Unit tests for {@link IsTestFactoryMethod}. @@ -30,50 +41,102 @@ */ class IsTestFactoryMethodTests { - private static final Predicate isTestFactoryMethod = new IsTestFactoryMethod(); - - @Test - void factoryMethodReturningCollectionOfDynamicTests() { - assertThat(isTestFactoryMethod).accepts(method("dynamicTestsFactory")); + final List discoveryIssues = new ArrayList<>(); + final Predicate isTestFactoryMethod = new IsTestFactoryMethod( + DiscoveryIssueReporter.collecting(discoveryIssues)); + + @ParameterizedTest + @ValueSource(strings = { "dynamicTestsFactoryFromCollection", "dynamicTestsFactoryFromStreamWithExtendsWildcard", + "dynamicTestsFactoryFromNode", "dynamicTestsFactoryFromTest", "dynamicTestsFactoryFromContainer", + "dynamicTestsFactoryFromNodeArray", "dynamicTestsFactoryFromTestArray", + "dynamicTestsFactoryFromContainerArray" }) + void validFactoryMethods(String methodName) { + assertThat(isTestFactoryMethod).accepts(method(methodName)); + assertThat(discoveryIssues).isEmpty(); } - @Test - void bogusFactoryMethodReturningVoid() { - assertThat(isTestFactoryMethod).rejects(method("bogusVoidFactory")); + @ParameterizedTest + @ValueSource(strings = { "bogusVoidFactory", "bogusStringsFactory", "bogusStringArrayFactory", + "dynamicTestsFactoryFromStreamWithSuperWildcard" }) + void invalidFactoryMethods(String methodName) { + var method = method(methodName); + + assertThat(isTestFactoryMethod).rejects(method); + + var issue = getOnlyElement(discoveryIssues); + assertThat(issue.severity()).isEqualTo(DiscoveryIssue.Severity.WARNING); + assertThat(issue.message()).isEqualTo( + "@TestFactory method '%s' must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, Iterator, or array of org.junit.jupiter.api.DynamicNode. " + + "It will not be executed.", + method.toGenericString()); + assertThat(issue.source()).contains(MethodSource.from(method)); } - // TODO [#949] Enable test once IsTestFactoryMethod properly checks return type. - @Disabled("Disabled until IsTestFactoryMethod properly checks return type") - @Test - void bogusFactoryMethodReturningObject() { - assertThat(isTestFactoryMethod).rejects(method("bogusObjectFactory")); - } + @ParameterizedTest + @ValueSource(strings = { "objectFactory", "objectArrayFactory", "rawCollectionFactory", "unboundStreamFactory" }) + void suspiciousFactoryMethods(String methodName) { + var method = method(methodName); + + assertThat(isTestFactoryMethod).accepts(method); - // TODO [#949] Enable test once IsTestFactoryMethod properly checks return type. - @Disabled("Disabled until IsTestFactoryMethod properly checks return type") - @Test - void bogusFactoryMethodReturningCollectionOfStrings() { - assertThat(isTestFactoryMethod).rejects(method("bogusStringsFactory")); + var issue = getOnlyElement(discoveryIssues); + assertThat(issue.severity()).isEqualTo(DiscoveryIssue.Severity.INFO); + assertThat(issue.message()).isEqualTo( + "The declared return type of @TestFactory method '%s' does not support static validation. " + + "It must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, Iterator, or array of org.junit.jupiter.api.DynamicNode.", + method.toGenericString()); + assertThat(issue.source()).contains(MethodSource.from(method)); } private static Method method(String name) { - return ReflectionSupport.findMethod(ClassWithTestFactoryMethods.class, name).get(); + return ReflectionSupport.findMethod(ClassWithTestFactoryMethods.class, name).orElseThrow(); } + @SuppressWarnings("unused") private static class ClassWithTestFactoryMethods { @TestFactory - Collection dynamicTestsFactory() { + Collection dynamicTestsFactoryFromCollection() { return new ArrayList<>(); } @TestFactory - void bogusVoidFactory() { + Stream dynamicTestsFactoryFromStreamWithExtendsWildcard() { + return Stream.empty(); + } + + @TestFactory + DynamicTest dynamicTestsFactoryFromNode() { + return dynamicTest("foo", Assertions::fail); + } + + @TestFactory + DynamicTest dynamicTestsFactoryFromTest() { + return dynamicTest("foo", Assertions::fail); + } + + @TestFactory + DynamicNode dynamicTestsFactoryFromContainer() { + return dynamicContainer("foo", Stream.empty()); + } + + @TestFactory + DynamicNode[] dynamicTestsFactoryFromNodeArray() { + return new DynamicNode[0]; + } + + @TestFactory + DynamicTest[] dynamicTestsFactoryFromTestArray() { + return new DynamicTest[0]; + } + + @TestFactory + DynamicContainer[] dynamicTestsFactoryFromContainerArray() { + return new DynamicContainer[0]; } @TestFactory - Object bogusObjectFactory() { - return new Object(); + void bogusVoidFactory() { } @TestFactory @@ -81,6 +144,37 @@ Collection bogusStringsFactory() { return new ArrayList<>(); } + @TestFactory + String[] bogusStringArrayFactory() { + return new String[0]; + } + + @TestFactory + Stream dynamicTestsFactoryFromStreamWithSuperWildcard() { + return Stream.empty(); + } + + @TestFactory + Object objectFactory() { + return dynamicTest("foo", Assertions::fail); + } + + @TestFactory + Object[] objectArrayFactory() { + return new DynamicNode[0]; + } + + @SuppressWarnings("rawtypes") + @TestFactory + Collection rawCollectionFactory() { + return new ArrayList<>(); + } + + @TestFactory + Stream unboundStreamFactory() { + return Stream.of(); + } + } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestMethodTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestMethodTests.java index c8787aafaa20..b5d4bc5aa89c 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestMethodTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestMethodTests.java @@ -10,16 +10,26 @@ package org.junit.jupiter.engine.discovery.predicates; +import static java.util.Comparator.comparing; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import java.util.function.Predicate; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.support.ModifierSupport; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; /** * Unit tests for {@link IsTestMethod}. @@ -28,7 +38,8 @@ */ class IsTestMethodTests { - private static final Predicate isTestMethod = new IsTestMethod(); + final List discoveryIssues = new ArrayList<>(); + final Predicate isTestMethod = new IsTestMethod(DiscoveryIssueReporter.collecting(discoveryIssues)); @Test void publicTestMethod() { @@ -58,75 +69,136 @@ void packageVisibleTestMethod() { @Test void bogusAbstractTestMethod() { - assertThat(isTestMethod).rejects(abstractMethod("bogusAbstractTestMethod")); + var method = abstractMethod("bogusAbstractTestMethod"); + + assertThat(isTestMethod).rejects(method); + + var issue = getOnlyElement(discoveryIssues); + assertThat(issue.severity()).isEqualTo(Severity.WARNING); + assertThat(issue.message()).isEqualTo("@Test method '%s' must not be abstract. It will not be executed.", + method.toGenericString()); + assertThat(issue.source()).contains(MethodSource.from(method)); } @Test - void bogusStaticTestMethod() { - assertThat(isTestMethod).rejects(method("bogusStaticTestMethod")); + void bogusAbstractNonVoidTestMethod() { + var method = abstractMethod("bogusAbstractNonVoidTestMethod"); + + assertThat(isTestMethod).rejects(method); + + assertThat(discoveryIssues).hasSize(2); + discoveryIssues.sort(comparing(DiscoveryIssue::message)); + assertThat(discoveryIssues.getFirst().message()) // + .isEqualTo("@Test method '%s' must not be abstract. It will not be executed.", + method.toGenericString()); + assertThat(discoveryIssues.getLast().message()) // + .isEqualTo("@Test method '%s' must not return a value. It will not be executed.", + method.toGenericString()); } @Test - void bogusPrivateTestMethod() { - assertThat(isTestMethod).rejects(method("bogusPrivateTestMethod")); + void bogusStaticTestMethod() { + var method = method("bogusStaticTestMethod"); + + assertThat(isTestMethod).rejects(method); + + var issue = getOnlyElement(discoveryIssues); + assertThat(issue.severity()).isEqualTo(Severity.WARNING); + assertThat(issue.message()).isEqualTo("@Test method '%s' must not be static. It will not be executed.", + method.toGenericString()); + assertThat(issue.source()).contains(MethodSource.from(method)); } @Test - void bogusTestMethodReturningObject() { - assertThat(isTestMethod).rejects(method("bogusTestMethodReturningObject")); + void bogusPrivateTestMethod() { + var method = method("bogusPrivateTestMethod"); + + assertThat(isTestMethod).rejects(method); + + var issue = getOnlyElement(discoveryIssues); + assertThat(issue.severity()).isEqualTo(Severity.WARNING); + assertThat(issue.message()).isEqualTo("@Test method '%s' must not be private. It will not be executed.", + method.toGenericString()); + assertThat(issue.source()).contains(MethodSource.from(method)); } - @Test - void bogusTestMethodReturningVoidReference() { - assertThat(isTestMethod).rejects(method("bogusTestMethodReturningVoidReference")); + @ParameterizedTest + @ValueSource(strings = { "bogusTestMethodReturningObject", "bogusTestMethodReturningVoidReference", + "bogusTestMethodReturningPrimitive" }) + void bogusNonVoidTestMethods(String methodName) { + var method = method(methodName); + + assertThat(isTestMethod).rejects(method); + + var issue = getOnlyElement(discoveryIssues); + assertThat(issue.severity()).isEqualTo(Severity.WARNING); + assertThat(issue.message()).isEqualTo("@Test method '%s' must not return a value. It will not be executed.", + method.toGenericString()); + assertThat(issue.source()).contains(MethodSource.from(method)); } @Test - void bogusTestMethodReturningPrimitive() { - assertThat(isTestMethod).rejects(method("bogusTestMethodReturningPrimitive")); + void bogusStaticPrivateNonVoidTestMethod() { + var method = method("bogusStaticPrivateNonVoidTestMethod"); + + assertThat(isTestMethod).rejects(method); + + assertThat(discoveryIssues).hasSize(3); + discoveryIssues.sort(comparing(DiscoveryIssue::message)); + assertThat(discoveryIssues.getFirst().message()) // + .isEqualTo("@Test method '%s' must not be private. It will not be executed.", method.toGenericString()); + assertThat(discoveryIssues.get(1).message()) // + .isEqualTo("@Test method '%s' must not be static. It will not be executed.", method.toGenericString()); + assertThat(discoveryIssues.getLast().message()) // + .isEqualTo("@Test method '%s' must not return a value. It will not be executed.", + method.toGenericString()); } private static Method method(String name, Class... parameterTypes) { - return ReflectionSupport.findMethod(ClassWithTestMethods.class, name, parameterTypes).get(); + return ReflectionSupport.findMethod(ClassWithTestMethods.class, name, parameterTypes).orElseThrow(); } private Method abstractMethod(String name) { - return ReflectionSupport.findMethod(AbstractClassWithAbstractTestMethod.class, name).get(); + return ReflectionSupport.findMethod(AbstractClassWithAbstractTestMethod.class, name).orElseThrow(); } + @SuppressWarnings({ "JUnitMalformedDeclaration", "unused" }) private static abstract class AbstractClassWithAbstractTestMethod { @Test abstract void bogusAbstractTestMethod(); + @Test + abstract int bogusAbstractNonVoidTestMethod(); + } - @SuppressWarnings("JUnitMalformedDeclaration") + @SuppressWarnings({ "JUnitMalformedDeclaration", "unused" }) private static class ClassWithTestMethods { - @SuppressWarnings("JUnitMalformedDeclaration") @Test static void bogusStaticTestMethod() { } - @SuppressWarnings("JUnitMalformedDeclaration") @Test private void bogusPrivateTestMethod() { } - @SuppressWarnings("JUnitMalformedDeclaration") + @Test + private static int bogusStaticPrivateNonVoidTestMethod() { + return 42; + } + @Test String bogusTestMethodReturningObject() { return ""; } - @SuppressWarnings("JUnitMalformedDeclaration") @Test Void bogusTestMethodReturningVoidReference() { return null; } - @SuppressWarnings("JUnitMalformedDeclaration") @Test int bogusTestMethodReturningPrimitive() { return 0; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestTemplateMethodTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestTemplateMethodTests.java index a71da9ec39b0..dc41da828eb2 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestTemplateMethodTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestTemplateMethodTests.java @@ -11,12 +11,18 @@ package org.junit.jupiter.engine.discovery.predicates; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestTemplate; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; /** * Unit tests for {@link IsTestTemplateMethod}. @@ -25,7 +31,9 @@ */ class IsTestTemplateMethodTests { - private static final IsTestTemplateMethod isTestTemplateMethod = new IsTestTemplateMethod(); + final List discoveryIssues = new ArrayList<>(); + final IsTestTemplateMethod isTestTemplateMethod = new IsTestTemplateMethod( + DiscoveryIssueReporter.collecting(discoveryIssues)); @Test void testTemplateMethodReturningVoid() { @@ -34,13 +42,22 @@ void testTemplateMethodReturningVoid() { @Test void bogusTestTemplateMethodReturningObject() { - assertThat(isTestTemplateMethod).rejects(method("bogusTemplateReturningObject")); + var method = method("bogusTemplateReturningObject"); + + assertThat(isTestTemplateMethod).rejects(method); + + var issue = getOnlyElement(discoveryIssues); + assertThat(issue.severity()).isEqualTo(DiscoveryIssue.Severity.WARNING); + assertThat(issue.message()).isEqualTo( + "@TestTemplate method '%s' must not return a value. It will not be executed.", method.toGenericString()); + assertThat(issue.source()).contains(MethodSource.from(method)); } private static Method method(String name) { - return ReflectionSupport.findMethod(ClassWithTestTemplateMethods.class, name).get(); + return ReflectionSupport.findMethod(ClassWithTestTemplateMethods.class, name).orElseThrow(); } + @SuppressWarnings("unused") private static class ClassWithTestTemplateMethods { @TestTemplate diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/TestClassPredicatesTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/TestClassPredicatesTests.java new file mode 100644 index 000000000000..757d4a8cfa6c --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/TestClassPredicatesTests.java @@ -0,0 +1,494 @@ +/* + * Copyright 2015-2025 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.engine.discovery.predicates; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestTemplate; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; + +public class TestClassPredicatesTests { + + private final List discoveryIssues = new ArrayList<>(); + private final TestClassPredicates predicates = new TestClassPredicates( + DiscoveryIssueReporter.collecting(discoveryIssues)); + + @Nested + class StandaloneTestClasses { + + @Test + void classWithTestMethodEvaluatesToTrue() { + assertTrue(predicates.looksLikeIntendedTestClass(ClassWithTestMethod.class)); + assertTrue(predicates.isValidStandaloneTestClass(ClassWithTestMethod.class)); + } + + @Test + void classWithTestFactoryEvaluatesToTrue() { + assertTrue(predicates.looksLikeIntendedTestClass(ClassWithTestFactory.class)); + assertTrue(predicates.isValidStandaloneTestClass(ClassWithTestFactory.class)); + } + + @Test + void classWithTestTemplateEvaluatesToTrue() { + assertTrue(predicates.looksLikeIntendedTestClass(ClassWithTestTemplate.class)); + assertTrue(predicates.isValidStandaloneTestClass(ClassWithTestTemplate.class)); + } + + @Test + void classWithNestedTestClassEvaluatesToTrue() { + assertTrue(predicates.looksLikeIntendedTestClass(ClassWithNestedTestClass.class)); + assertTrue(predicates.isValidStandaloneTestClass(ClassWithNestedTestClass.class)); + } + + @Test + void staticTestClassEvaluatesToTrue() { + assertTrue(predicates.looksLikeIntendedTestClass(TestCases.StaticTestCase.class)); + assertTrue(predicates.isValidStandaloneTestClass(TestCases.StaticTestCase.class)); + } + + // ------------------------------------------------------------------------- + + @Test + void abstractClassEvaluatesToFalse() { + assertTrue(predicates.looksLikeIntendedTestClass(AbstractClass.class)); + assertFalse(predicates.isValidStandaloneTestClass(AbstractClass.class)); + assertThat(discoveryIssues).isEmpty(); + } + + @Test + void localClassEvaluatesToFalse() { + + @SuppressWarnings({ "JUnitMalformedDeclaration", "NewClassNamingConvention" }) + class LocalClass { + @SuppressWarnings("unused") + @Test + void test() { + } + } + + var candidate = LocalClass.class; + + assertTrue(predicates.looksLikeIntendedTestClass(candidate)); + assertFalse(predicates.isValidStandaloneTestClass(candidate)); + + var issue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be a local class. It will not be executed.".formatted(candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues).containsExactly(issue); + } + + @Test + void anonymousClassEvaluatesToFalse() { + + Object object = new Object() { + @SuppressWarnings("unused") + @Test + void test() { + } + }; + + Class candidate = object.getClass(); + + assertTrue(predicates.looksLikeIntendedTestClass(candidate)); + assertFalse(predicates.isValidStandaloneTestClass(candidate)); + + var issue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be anonymous. It will not be executed.".formatted(candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues).containsExactly(issue); + } + + @Test + void privateClassWithTestMethodEvaluatesToFalse() { + var candidate = TestCases.PrivateClassWithTestMethod.class; + + assertTrue(predicates.looksLikeIntendedTestClass(candidate)); + assertFalse(predicates.isValidStandaloneTestClass(candidate)); + + var notPrivateIssue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be private. It will not be executed.".formatted(candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + var notInnerClassIssue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be an inner class unless annotated with @Nested. It will not be executed.".formatted( + candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues).containsExactlyInAnyOrder(notPrivateIssue, notInnerClassIssue); + } + + @Test + void privateClassWithTestFactoryEvaluatesToFalse() { + var candidate = TestCases.PrivateClassWithTestFactory.class; + + assertTrue(predicates.looksLikeIntendedTestClass(candidate)); + assertFalse(predicates.isValidStandaloneTestClass(candidate)); + + var notPrivateIssue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be private. It will not be executed.".formatted(candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + var notInnerClassIssue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be an inner class unless annotated with @Nested. It will not be executed.".formatted( + candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues).containsExactlyInAnyOrder(notPrivateIssue, notInnerClassIssue); + } + + @Test + void privateClassWithTestTemplateEvaluatesToFalse() { + var candidate = TestCases.PrivateClassWithTestTemplate.class; + + assertTrue(predicates.looksLikeIntendedTestClass(candidate)); + assertFalse(predicates.isValidStandaloneTestClass(candidate)); + + var notPrivateIssue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be private. It will not be executed.".formatted(candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + var notInnerClassIssue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be an inner class unless annotated with @Nested. It will not be executed.".formatted( + candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues).containsExactlyInAnyOrder(notPrivateIssue, notInnerClassIssue); + } + + @Test + void privateClassWithNestedTestCasesEvaluatesToFalse() { + var candidate = TestCases.PrivateClassWithNestedTestClass.class; + + assertTrue(predicates.looksLikeIntendedTestClass(candidate)); + assertFalse(predicates.isValidStandaloneTestClass(candidate)); + + var notPrivateIssue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be private. It will not be executed.".formatted(candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + var notInnerClassIssue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be an inner class unless annotated with @Nested. It will not be executed.".formatted( + candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues).containsExactlyInAnyOrder(notPrivateIssue, notInnerClassIssue); + } + + @Test + void privateStaticTestClassEvaluatesToFalse() { + var candidate = TestCases.PrivateStaticTestCase.class; + + assertTrue(predicates.looksLikeIntendedTestClass(candidate)); + assertFalse(predicates.isValidStandaloneTestClass(candidate)); + + var notPrivateIssue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be private. It will not be executed.".formatted(candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues).containsExactly(notPrivateIssue); + } + + /* + * see https://github.com/junit-team/junit5/issues/2249 + */ + @Test + void recursiveHierarchies() { + assertThrows(JUnitException.class, () -> predicates.looksLikeIntendedTestClass(TestCases.OuterClass.class)); + assertTrue(predicates.isValidStandaloneTestClass(TestCases.OuterClass.class)); + assertThat(discoveryIssues).isEmpty(); + + var candidate = TestCases.OuterClass.RecursiveInnerClass.class; + + assertTrue(predicates.looksLikeIntendedTestClass(candidate)); + assertFalse(predicates.isValidStandaloneTestClass(candidate)); + + var notInnerClassIssue = DiscoveryIssue.builder(Severity.WARNING, + "Test class '%s' must not be an inner class unless annotated with @Nested. It will not be executed.".formatted( + candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues).containsExactly(notInnerClassIssue); + } + + } + + @Nested + class NestedTestClasses { + + @Test + void innerClassEvaluatesToTrue() { + var candidate = TestCases.NestedClassesTestCase.InnerClass.class; + assertThat(predicates.isAnnotatedWithNested).accepts(candidate); + assertTrue(predicates.isValidNestedTestClass(candidate)); + assertThat(predicates.isAnnotatedWithNestedAndValid).accepts(candidate); + } + + @Test + void staticNestedClassEvaluatesToFalse() { + var candidate = TestCases.NestedClassesTestCase.StaticNestedClass.class; + assertThat(predicates.isAnnotatedWithNested).accepts(candidate); + assertFalse(predicates.isValidNestedTestClass(candidate)); + assertThat(predicates.isAnnotatedWithNestedAndValid).rejects(candidate); + + var issue = DiscoveryIssue.builder(Severity.WARNING, + "@Nested class '%s' must not be static. It will not be executed.".formatted(candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues.stream().distinct()).containsExactly(issue); + } + + @Test + void topLevelClassEvaluatesToFalse() { + var candidate = InvalidTopLevelNestedTestClass.class; + assertThat(predicates.isAnnotatedWithNested).accepts(candidate); + assertFalse(predicates.isValidNestedTestClass(candidate)); + assertThat(predicates.isAnnotatedWithNestedAndValid).rejects(candidate); + + var issue = DiscoveryIssue.builder(Severity.WARNING, + "@Nested class '%s' must not be a top-level class. It will not be executed.".formatted( + candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues.stream().distinct()).containsExactly(issue); + } + + @Test + void privateNestedClassEvaluatesToFalse() { + var candidate = TestCases.NestedClassesTestCase.PrivateInnerClass.class; + assertThat(predicates.isAnnotatedWithNested).accepts(candidate); + assertFalse(predicates.isValidNestedTestClass(candidate)); + assertThat(predicates.isAnnotatedWithNestedAndValid).rejects(candidate); + + var issue = DiscoveryIssue.builder(Severity.WARNING, + "@Nested class '%s' must not be private. It will not be executed.".formatted(candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues.stream().distinct()).containsExactly(issue); + } + + @Test + void abstractInnerClassEvaluatesToFalse() { + var candidate = TestCases.NestedClassesTestCase.AbstractInnerClass.class; + assertThat(predicates.isAnnotatedWithNested).accepts(candidate); + assertFalse(predicates.isValidNestedTestClass(candidate)); + assertThat(predicates.isAnnotatedWithNestedAndValid).rejects(candidate); + assertThat(discoveryIssues).isEmpty(); + } + + @Test + void localClassEvaluatesToFalse() { + + @Nested + class LocalClass { + } + + var candidate = LocalClass.class; + + assertThat(predicates.isAnnotatedWithNested).accepts(candidate); + assertFalse(predicates.isValidNestedTestClass(candidate)); + assertThat(predicates.isAnnotatedWithNestedAndValid).rejects(candidate); + + var issue = DiscoveryIssue.builder(Severity.WARNING, + "@Nested class '%s' must not be static. It will not be executed.".formatted(candidate.getName())) // + .source(ClassSource.from(candidate)) // + .build(); + assertThat(discoveryIssues.stream().distinct()).containsExactly(issue); + } + } + + // ------------------------------------------------------------------------- + + static class TestCases { + + @SuppressWarnings({ "JUnitMalformedDeclaration", "InnerClassMayBeStatic" }) + private class PrivateClassWithTestMethod { + + @Test + void test() { + } + + } + + @SuppressWarnings("InnerClassMayBeStatic") + private class PrivateClassWithTestFactory { + + @TestFactory + Collection factory() { + return new ArrayList<>(); + } + + } + + @SuppressWarnings("InnerClassMayBeStatic") + private class PrivateClassWithTestTemplate { + + @TestTemplate + void template(int a) { + } + + } + + @SuppressWarnings("InnerClassMayBeStatic") + private class PrivateClassWithNestedTestClass { + + @Nested + class InnerClass { + + @Test + void first() { + } + + @Test + void second() { + } + + } + } + + // ------------------------------------------------------------------------- + + @SuppressWarnings("JUnitMalformedDeclaration") + static class StaticTestCase { + + @Test + void test() { + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + private static class PrivateStaticTestCase { + + @Test + void test() { + } + } + + @SuppressWarnings("NewClassNamingConvention") + static class OuterClass { + + @Nested + class InnerClass { + + @Test + void test() { + } + } + + // Intentionally commented out so that RecursiveInnerClass is NOT a candidate test class + // @Nested + @SuppressWarnings("InnerClassMayBeStatic") + class RecursiveInnerClass extends OuterClass { + } + } + + private static class NestedClassesTestCase { + + @Nested + class InnerClass { + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @Nested + static class StaticNestedClass { + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @Nested + private class PrivateInnerClass { + } + + @Nested + private abstract class AbstractInnerClass { + } + + } + } + +} + +// ----------------------------------------------------------------------------- + +abstract class AbstractClass { + @SuppressWarnings("unused") + @Test + void test() { + } +} + +@SuppressWarnings("NewClassNamingConvention") +class ClassWithTestMethod { + + @Test + void test() { + } + +} + +@SuppressWarnings("NewClassNamingConvention") +class ClassWithTestFactory { + + @TestFactory + Collection factory() { + return new ArrayList<>(); + } + +} + +@SuppressWarnings("NewClassNamingConvention") +class ClassWithTestTemplate { + + @TestTemplate + void template(int a) { + } + +} + +@SuppressWarnings("NewClassNamingConvention") +class ClassWithNestedTestClass { + + @Nested + class InnerClass { + + @Test + void first() { + } + + @Test + void second() { + } + + } +} + +@SuppressWarnings("NewClassNamingConvention") +@Nested +class InvalidTopLevelNestedTestClass { + @Test + void test() { + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java index 199b6903f46b..c39cc37aae7c 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java @@ -16,8 +16,8 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; /** diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java index 2c62a186b9fa..849d481212c6 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java @@ -18,9 +18,9 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ExtensionContextException; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.engine.support.store.NamespacedHierarchicalStoreException; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java index 80eeca958fbd..5e0b21658b32 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.descriptor.LauncherStoreFacade; import org.junit.jupiter.engine.extension.MutableExtensionRegistry; import org.junit.platform.engine.EngineExecutionListener; @@ -34,8 +35,10 @@ class JupiterEngineExecutionContextTests { private final EngineExecutionListener engineExecutionListener = mock(); + private final LauncherStoreFacade launcherStoreFacade = mock(); + private final JupiterEngineExecutionContext originalContext = new JupiterEngineExecutionContext( - engineExecutionListener, configuration); + engineExecutionListener, configuration, launcherStoreFacade); @Test void executionListenerIsHandedOnWhenContextIsExtended() { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java index 37cfcdd57f92..5c66048d03f6 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java @@ -62,7 +62,6 @@ import org.junit.jupiter.api.extension.AnnotatedElementContext; import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.io.TempDir; @@ -73,6 +72,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.logging.LogRecordListener; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; /** diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedClassTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedClassTests.java index f0e232eb7363..b1e163d7cfa9 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedClassTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedClassTests.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.stream.Stream; @@ -38,7 +39,11 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.platform.commons.logging.LogRecordListener; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.testkit.engine.EngineDiscoveryResults; import org.junit.platform.testkit.engine.EngineTestKit; import org.junit.platform.testkit.engine.Events; @@ -57,8 +62,23 @@ void clearCallSequence() { callSequence.clear(); } + @Test + void noOrderer() { + var discoveryIssues = discoverTests(null).getDiscoveryIssues(); + assertIneffectiveOrderAnnotationIssues(discoveryIssues); + + executeTests(null)// + .assertStatistics(stats -> stats.succeeded(callSequence.size())); + + assertThat(callSequence)// + .containsExactlyInAnyOrder("A_TestCase", "B_TestCase", "C_TestCase"); + } + @Test void className() { + var discoveryIssues = discoverTests(ClassOrderer.ClassName.class).getDiscoveryIssues(); + assertIneffectiveOrderAnnotationIssues(discoveryIssues); + executeTests(ClassOrderer.ClassName.class)// .assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -86,6 +106,9 @@ void classNameAcrossPackages() { @Test void displayName() { + var discoveryIssues = discoverTests(ClassOrderer.DisplayName.class).getDiscoveryIssues(); + assertIneffectiveOrderAnnotationIssues(discoveryIssues); + executeTests(ClassOrderer.DisplayName.class)// .assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -95,6 +118,9 @@ void displayName() { @Test void orderAnnotation() { + var discoveryIssues = discoverTests(ClassOrderer.OrderAnnotation.class).getDiscoveryIssues(); + assertThat(discoveryIssues).isEmpty(); + executeTests(ClassOrderer.OrderAnnotation.class)// .assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -130,6 +156,9 @@ void orderAnnotationOnNestedTestClassesWithLocalConfig(@TrackLogRecords LogRecor @Test void random() { + var discoveryIssues = discoverTests(ClassOrderer.Random.class).getDiscoveryIssues(); + assertIneffectiveOrderAnnotationIssues(discoveryIssues); + executeTests(ClassOrderer.Random.class)// .assertStatistics(stats -> stats.succeeded(callSequence.size())); } @@ -170,6 +199,16 @@ void classTemplateWithGlobalConfig() { .containsSubsequence(classTemplate.getSimpleName(), otherClass.getSimpleName()); } + private static void assertIneffectiveOrderAnnotationIssues(List discoveryIssues) { + assertThat(discoveryIssues).hasSize(2); + assertThat(discoveryIssues).extracting(DiscoveryIssue::severity).containsOnly(Severity.INFO); + assertThat(discoveryIssues).extracting(DiscoveryIssue::message) // + .allMatch(it -> it.startsWith("Ineffective @Order annotation on class") + && it.endsWith("It will not be applied because ClassOrderer.OrderAnnotation is not in use.")); + assertThat(discoveryIssues).extracting(DiscoveryIssue::source).extracting(Optional::orElseThrow) // + .containsExactlyInAnyOrder(ClassSource.from(A_TestCase.class), ClassSource.from(C_TestCase.class)); + } + private Events executeTests(Class classOrderer) { return executeTests(classOrderer, selectClass(A_TestCase.class), selectClass(B_TestCase.class), selectClass(C_TestCase.class)); @@ -177,14 +216,32 @@ private Events executeTests(Class classOrderer) { private Events executeTests(Class classOrderer, DiscoverySelector... selectors) { // @formatter:off - return EngineTestKit.engine("junit-jupiter") - .configurationParameter(DEFAULT_TEST_CLASS_ORDER_PROPERTY_NAME, classOrderer.getName()) - .selectors(selectors) - .execute() - .testEvents(); + return testKit(classOrderer, selectors) + .execute() + .testEvents(); // @formatter:on } + private EngineDiscoveryResults discoverTests(Class classOrderer) { + return discoverTests(classOrderer, selectClass(A_TestCase.class), selectClass(B_TestCase.class), + selectClass(C_TestCase.class)); + } + + private EngineDiscoveryResults discoverTests(Class classOrderer, + DiscoverySelector... selectors) { + return testKit(classOrderer, selectors).discover(); + } + + private static EngineTestKit.Builder testKit(Class classOrderer, + DiscoverySelector[] selectors) { + + var testKit = EngineTestKit.engine("junit-jupiter"); + if (classOrderer != null) { + testKit.configurationParameter(DEFAULT_TEST_CLASS_ORDER_PROPERTY_NAME, classOrderer.getName()); + } + return testKit.selectors(selectors); + } + static abstract class BaseTestCase { @BeforeEach diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedMethodTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedMethodTests.java index 90f34e291e63..0517a6dd026a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedMethodTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedMethodTests.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME; import static org.junit.jupiter.engine.Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.launcher.LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -56,6 +57,11 @@ import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.logging.LogRecordListener; import org.junit.platform.commons.util.ClassUtils; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.testkit.engine.EngineDiscoveryResults; import org.junit.platform.testkit.engine.EngineTestKit; import org.junit.platform.testkit.engine.Events; import org.mockito.Mockito; @@ -90,7 +96,7 @@ void alphanumeric() { // on the class names. assertThat(testClass.getSuperclass().getName()).isGreaterThan(testClass.getName()); - var tests = executeTestsInParallel(testClass); + var tests = executeTestsInParallel(testClass, Random.class); tests.assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -112,7 +118,7 @@ void methodName() { // on the class names. assertThat(testClass.getSuperclass().getName()).isLessThan(testClass.getName()); - var tests = executeTestsInParallel(testClass); + var tests = executeTestsInParallel(testClass, Random.class); tests.assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -123,7 +129,7 @@ void methodName() { @Test void displayName() { - var tests = executeTestsInParallel(DisplayNameTestCase.class); + var tests = executeTestsInParallel(DisplayNameTestCase.class, Random.class); tests.assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -146,7 +152,7 @@ void orderAnnotationInNestedTestClass() { @Test void orderAnnotationWithNestedTestClass() { - var tests = executeTestsInParallel(OrderAnnotationWithNestedClassTestCase.class); + var tests = executeTestsInParallel(OrderAnnotationWithNestedClassTestCase.class, Random.class); tests.assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -157,7 +163,7 @@ void orderAnnotationWithNestedTestClass() { } private void assertOrderAnnotationSupport(Class testClass) { - var tests = executeTestsInParallel(testClass); + var tests = executeTestsInParallel(testClass, Random.class); tests.assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -168,7 +174,7 @@ private void assertOrderAnnotationSupport(Class testClass) { @Test void random() { - var tests = executeTestsInParallel(RandomTestCase.class); + var tests = executeTestsInParallel(RandomTestCase.class, Random.class); tests.assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -273,35 +279,62 @@ void randomWithCustomSeed(@TrackLogRecords LogRecordListener listener) { } @Test - void misbehavingMethodOrdererThatAddsElements(@TrackLogRecords LogRecordListener listener) { + void reportsDiscoveryIssuesForIneffectiveOrderAnnotations() throws Exception { + var results = discoverTests(WithoutTestMethodOrderTestCase.class, OrderAnnotation.class); + assertThat(results.getDiscoveryIssues()).isEmpty(); + + results = discoverTests(WithoutTestMethodOrderTestCase.class, null); + assertIneffectiveOrderAnnotationIssues(results.getDiscoveryIssues()); + + results = discoverTests(WithoutTestMethodOrderTestCase.class, Random.class); + assertIneffectiveOrderAnnotationIssues(results.getDiscoveryIssues()); + } + + @Test + void misbehavingMethodOrdererThatAddsElements() { Class testClass = MisbehavingByAddingTestCase.class; - executeTestsInParallel(testClass).assertStatistics(stats -> stats.succeeded(2)); + var discoveryIssues = discoverTests(testClass, null).getDiscoveryIssues(); + assertThat(discoveryIssues).hasSize(1); - assertThat(callSequence).containsExactly("test1()", "test2()"); + var issue = discoveryIssues.getFirst(); + assertThat(issue.severity()).isEqualTo(Severity.WARNING); + assertThat(issue.message()).isEqualTo( + "MethodOrderer [%s] added 2 MethodDescriptor(s) for test class [%s] which will be ignored.", + MisbehavingByAdding.class.getName(), testClass.getName()); + assertThat(issue.source()).contains(ClassSource.from(testClass)); - var expectedMessage = "MethodOrderer [" + MisbehavingByAdding.class.getName() - + "] added 2 MethodDescriptor(s) for test class [" + testClass.getName() + "] which will be ignored."; + executeTestsInParallel(testClass, null, Severity.ERROR) // + .assertStatistics(stats -> stats.succeeded(2)); - assertExpectedLogMessage(listener, expectedMessage); + assertThat(callSequence).containsExactly("test1()", "test2()"); } @Test - void misbehavingMethodOrdererThatImpersonatesElements(@TrackLogRecords LogRecordListener listener) { + void misbehavingMethodOrdererThatImpersonatesElements() { Class testClass = MisbehavingByImpersonatingTestCase.class; - executeTestsInParallel(testClass).assertStatistics(stats -> stats.succeeded(2)); + executeTestsInParallel(testClass, Random.class).assertStatistics(stats -> stats.succeeded(2)); assertThat(callSequence).containsExactlyInAnyOrder("test1()", "test2()"); - - assertThat(listener.stream(Level.WARNING)).isEmpty(); } @Test - void misbehavingMethodOrdererThatRemovesElements(@TrackLogRecords LogRecordListener listener) { + void misbehavingMethodOrdererThatRemovesElements() { Class testClass = MisbehavingByRemovingTestCase.class; - executeTestsInParallel(testClass).assertStatistics(stats -> stats.succeeded(4)); + var discoveryIssues = discoverTests(testClass, null).getDiscoveryIssues(); + assertThat(discoveryIssues).hasSize(1); + + var issue = discoveryIssues.getFirst(); + assertThat(issue.severity()).isEqualTo(Severity.WARNING); + assertThat(issue.message()).isEqualTo( + "MethodOrderer [%s] removed 2 MethodDescriptor(s) for test class [%s] which will be retained with arbitrary ordering.", + MisbehavingByRemoving.class.getName(), testClass.getName()); + assertThat(issue.source()).contains(ClassSource.from(testClass)); + + executeTestsInParallel(testClass, null, Severity.ERROR) // + .assertStatistics(stats -> stats.succeeded(4)); assertThat(callSequence) // .containsExactlyInAnyOrder("test1()", "test2()", "test3()", "test4()") // @@ -310,37 +343,33 @@ void misbehavingMethodOrdererThatRemovesElements(@TrackLogRecords LogRecordListe .containsSubsequence("test1()", "test4()") // removed item is re-added before ordered item .containsSubsequence("test2()", "test3()") // removed item is re-added before ordered item .containsSubsequence("test2()", "test4()");// removed item is re-added before ordered item - - var expectedMessage = "MethodOrderer [" + MisbehavingByRemoving.class.getName() - + "] removed 2 MethodDescriptor(s) for test class [" + testClass.getName() - + "] which will be retained with arbitrary ordering."; - - assertExpectedLogMessage(listener, expectedMessage); } - private void assertExpectedLogMessage(LogRecordListener listener, String expectedMessage) { - // @formatter:off - assertThat(listener.stream(Level.WARNING) - .map(LogRecord::getMessage)) - .contains(expectedMessage); - // @formatter:on + private EngineDiscoveryResults discoverTests(Class testClass, Class defaultOrderer) { + return testKit(testClass, defaultOrderer, Severity.INFO).discover(); } - private Events executeTestsInParallel(Class testClass) { - return executeTestsInParallel(testClass, Random.class); + private Events executeTestsInParallel(Class testClass, Class defaultOrderer) { + return executeTestsInParallel(testClass, defaultOrderer, Severity.INFO); } - private Events executeTestsInParallel(Class testClass, Class defaultOrderer) { - // @formatter:off - return EngineTestKit - .engine("junit-jupiter") - .configurationParameter(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, "true") - .configurationParameter(DEFAULT_PARALLEL_EXECUTION_MODE, "concurrent") - .configurationParameter(DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME, defaultOrderer.getName()) - .selectors(selectClass(testClass)) - .execute() + private Events executeTestsInParallel(Class testClass, Class defaultOrderer, + Severity criticalSeverity) { + return testKit(testClass, defaultOrderer, criticalSeverity) // + .execute() // .testEvents(); - // @formatter:on + } + + private static EngineTestKit.Builder testKit(Class testClass, Class defaultOrderer, + Severity criticalSeverity) { + var testKit = EngineTestKit.engine("junit-jupiter") // + .configurationParameter(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, "true") // + .configurationParameter(DEFAULT_PARALLEL_EXECUTION_MODE, "concurrent") // + .configurationParameter(CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, criticalSeverity.name()); + if (defaultOrderer != null) { + testKit.configurationParameter(DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME, defaultOrderer.getName()); + } + return testKit.selectors(selectClass(testClass)); } private Events executeRandomTestCaseInParallelWithRandomSeed(String seed) { @@ -360,6 +389,19 @@ private Events executeRandomTestCaseInParallelWithRandomSeed(String seed) { // @formatter:on } + private static void assertIneffectiveOrderAnnotationIssues(List discoveryIssues) throws Exception { + assertThat(discoveryIssues).hasSize(3); + assertThat(discoveryIssues).extracting(DiscoveryIssue::severity).containsOnly(Severity.INFO); + assertThat(discoveryIssues).extracting(DiscoveryIssue::message) // + .allMatch(it -> it.startsWith("Ineffective @Order annotation on method") + && it.endsWith("It will not be applied because MethodOrderer.OrderAnnotation is not in use.")); + var testClass = WithoutTestMethodOrderTestCase.class; + assertThat(discoveryIssues).extracting(DiscoveryIssue::source).extracting(Optional::orElseThrow) // + .containsExactlyInAnyOrder(MethodSource.from(testClass.getDeclaredMethod("test1")), + MethodSource.from(testClass.getDeclaredMethod("test2")), + MethodSource.from(testClass.getDeclaredMethod("test3"))); + } + // ------------------------------------------------------------------------- @SuppressWarnings("JUnitMalformedDeclaration") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java index 90333d84631e..e549af821ba7 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java @@ -17,7 +17,6 @@ import static org.junit.jupiter.api.condition.OS.WINDOWS; import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE; import static org.junit.jupiter.api.parallel.Resources.SYSTEM_OUT; -import static org.junit.jupiter.api.parallel.Resources.SYSTEM_PROPERTIES; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.testkit.engine.EventConditions.event; import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; @@ -34,6 +33,7 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.UnaryOperator; import org.assertj.core.api.Condition; import org.junit.jupiter.api.AfterEach; @@ -48,6 +48,7 @@ import org.junit.jupiter.api.parallel.ResourceLock; import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; import org.junit.jupiter.engine.Constants; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; import org.junit.platform.testkit.engine.EngineExecutionResults; import org.junit.platform.testkit.engine.Events; @@ -59,7 +60,6 @@ class PreInterruptCallbackTests extends AbstractJupiterTestEngineTests { private static final String TC = "test"; private static final String TIMEOUT_ERROR_MSG = TC + "() timed out after 1 microsecond"; - private static final String DEFAULT_ENABLE_PROPERTY = Constants.EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME; private static final AtomicBoolean interruptedTest = new AtomicBoolean(); private static final CompletableFuture testThreadExecutionDone = new CompletableFuture<>(); private static final AtomicReference interruptedTestThread = new AtomicReference<>(); @@ -80,34 +80,26 @@ void tearDown() { } @Test - @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE) @ResourceLock(value = SYSTEM_OUT, mode = READ_WRITE) void testCaseWithDefaultInterruptCallbackEnabled() { - String orgValue = System.getProperty(DEFAULT_ENABLE_PROPERTY); - System.setProperty(DEFAULT_ENABLE_PROPERTY, Boolean.TRUE.toString()); PrintStream orgOutStream = System.out; - Events tests; + EngineExecutionResults results; String output; try { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - PrintStream outStream = new PrintStream(buffer); + PrintStream outStream = new PrintStream(buffer, false, StandardCharsets.UTF_8); System.setOut(outStream); // Use larger timeout to increase likelihood of the test being started when the timeout is reached - tests = executeDefaultPreInterruptCallbackTimeoutOnMethodTestCase(WINDOWS.isCurrentOs() ? "1 s" : "100 ms") // - .testEvents(); + var timeout = WINDOWS.isCurrentOs() ? "1 s" : "100 ms"; + results = executeDefaultPreInterruptCallbackTimeoutOnMethodTestCase(timeout, request -> request // + .configurationParameter(Constants.EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME, "true")); output = buffer.toString(StandardCharsets.UTF_8); } finally { System.setOut(orgOutStream); - if (orgValue != null) { - System.setProperty(DEFAULT_ENABLE_PROPERTY, orgValue); - } - else { - System.clearProperty(DEFAULT_ENABLE_PROPERTY); - } } - assertTestHasTimedOut(tests, message(it -> it.startsWith(TC + "() timed out after"))); + assertTestHasTimedOut(results.testEvents(), message(it -> it.startsWith(TC + "() timed out after"))); assertTrue(interruptedTest.get()); Thread thread = Thread.currentThread(); @@ -127,16 +119,17 @@ void testCaseWithDefaultInterruptCallbackEnabled() { @Test void testCaseWithNoInterruptCallbackEnabled() { - Events tests = executeDefaultPreInterruptCallbackTimeoutOnMethodTestCase("1 μs") // + Events tests = executeDefaultPreInterruptCallbackTimeoutOnMethodTestCase("1 μs", UnaryOperator.identity()) // .testEvents(); assertTestHasTimedOut(tests); assertTrue(interruptedTest.get()); } - private EngineExecutionResults executeDefaultPreInterruptCallbackTimeoutOnMethodTestCase(String timeout) { - return executeTests(request -> request // + private EngineExecutionResults executeDefaultPreInterruptCallbackTimeoutOnMethodTestCase(String timeout, + UnaryOperator configurer) { + return executeTests(request -> configurer.apply(request // .selectors(selectClass(DefaultPreInterruptCallbackTimeoutOnMethodTestCase.class)) // - .configurationParameter(Constants.DEFAULT_TEST_METHOD_TIMEOUT_PROPERTY_NAME, timeout)); + .configurationParameter(Constants.DEFAULT_TEST_METHOD_TIMEOUT_PROPERTY_NAME, timeout))); } @Test diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/RepeatedTestTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/RepeatedTestTests.java index 567a38431463..bed689112b13 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/RepeatedTestTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/RepeatedTestTests.java @@ -18,7 +18,9 @@ import static org.junit.jupiter.engine.Constants.PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME; import static org.junit.jupiter.engine.Constants.PARALLEL_CONFIG_STRATEGY_PROPERTY_NAME; import static org.junit.jupiter.engine.Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; +import static org.junit.platform.launcher.LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; import static org.junit.platform.testkit.engine.EventConditions.container; import static org.junit.platform.testkit.engine.EventConditions.displayName; @@ -45,6 +47,7 @@ import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.testkit.engine.Events; @@ -62,10 +65,13 @@ void customDisplayName(TestInfo testInfo) { assertThat(testInfo.getDisplayName()).isEqualTo("repetition 1 of 1"); } - @RepeatedTest(1) - @DisplayName(" \t ") - void customDisplayNameWithBlankName(TestInfo testInfo) { - assertThat(testInfo.getDisplayName()).isEqualTo("repetition 1 of 1"); + @Test + void customDisplayNameWithBlankName() { + executeTests(request -> request // + .selectors(selectClass(BlankDisplayNameTestCase.class)) // + .configurationParameter(CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, Severity.ERROR.name())) // + .testEvents() // + .assertStatistics(stats -> stats.started(1).succeeded(1)); } @RepeatedTest(value = 1, name = "{displayName}") @@ -406,4 +412,13 @@ void failureThresholdWithConcurrentExecution() { } + static class BlankDisplayNameTestCase { + + @RepeatedTest(1) + @DisplayName(" \t ") + void test(TestInfo testInfo) { + assertThat(testInfo.getDisplayName()).isEqualTo("repetition 1 of 1"); + } + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java index 750611a5c67a..04e2735f4805 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; import org.junit.jupiter.engine.execution.NamespaceAwareStore; import org.junit.jupiter.engine.extension.TimeoutInvocationFactory.TimeoutInvocationParameters; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; /** @@ -71,7 +72,8 @@ void shouldThrowInvocationException() { private static SeparateThreadTimeoutInvocation aSeparateThreadInvocation(Invocation invocation) { var namespace = ExtensionContext.Namespace.create(SeparateThreadTimeoutInvocationTests.class); - var store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), namespace); + var store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), + Namespace.create(namespace.getParts())); var parameters = new TimeoutInvocationParameters<>(invocation, new TimeoutDuration(PREEMPTIVE_TIMEOUT_MILLIS, MILLISECONDS), () -> "method()", PreInterruptCallbackInvocation.NOOP); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryCleanupTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryCleanupTests.java index d6d161db3381..b8d0b90468aa 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryCleanupTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryCleanupTests.java @@ -27,7 +27,7 @@ import java.util.logging.Level; import java.util.logging.LogRecord; -import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; @@ -60,6 +60,7 @@ class TempDirFieldTests { private static Path alwaysFieldDir; private static Path onSuccessFailingFieldDir; private static Path onSuccessPassingFieldDir; + private static Path onSuccessPassingParameterDir; /** * Ensure the cleanup mode defaults to ALWAYS for fields. @@ -152,6 +153,14 @@ void cleanupModeOnSuccessFailingField() { assertThat(onSuccessFailingFieldDir).exists(); } + @Test + void cleanupModeOnSuccessFailingThenPassingField() { + executeTests(selectClass(OnSuccessFailingFieldCase.class), selectClass(OnSuccessPassingFieldCase.class)); + + assertThat(onSuccessFailingFieldDir).exists(); + assertThat(onSuccessPassingFieldDir).doesNotExist(); + } + /** * Ensure that ON_SUCCESS cleanup modes are obeyed for static fields when tests are failing. *

    @@ -174,21 +183,20 @@ void cleanupModeOnSuccessFailingStaticField() { */ @Test void cleanupModeOnSuccessFailingStaticFieldWithNesting() { - LauncherDiscoveryRequest request = request()// - .selectors(selectClass(OnSuccessFailingStaticFieldWithNestingCase.class))// - .build(); - executeTests(request); + executeTestsForClass(OnSuccessFailingStaticFieldWithNestingCase.class); assertThat(onSuccessFailingFieldDir).exists(); + assertThat(onSuccessPassingParameterDir).doesNotExist(); } - @AfterAll - static void afterAll() throws IOException { + @AfterEach + void deleteTempDirs() throws IOException { deleteIfNotNullAndExists(defaultFieldDir); deleteIfNotNullAndExists(neverFieldDir); deleteIfNotNullAndExists(alwaysFieldDir); deleteIfNotNullAndExists(onSuccessFailingFieldDir); deleteIfNotNullAndExists(onSuccessPassingFieldDir); + deleteIfNotNullAndExists(onSuccessPassingParameterDir); } static void deleteIfNotNullAndExists(Path dir) throws IOException { @@ -286,13 +294,21 @@ static class OnSuccessFailingStaticFieldWithNestingCase { static Path onSuccessFailingFieldDir; @Nested + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class NestedTestCase { @Test - void test() { + @Order(1) + void failingTest() { TempDirFieldTests.onSuccessFailingFieldDir = onSuccessFailingFieldDir; fail(); } + + @Test + @Order(2) + void passingTest(@TempDir(cleanup = ON_SUCCESS) Path tempDir) { + TempDirFieldTests.onSuccessPassingParameterDir = tempDir; + } } } @@ -400,8 +416,16 @@ void cleanupModeOnSuccessFailingParameter() { assertThat(onSuccessFailingParameterDir).exists(); } - @AfterAll - static void afterAll() throws IOException { + @Test + void cleanupModeOnSuccessFailingThenPassingParameter() { + executeTestsForClass(OnSuccessFailingThenPassingParameterCase.class); + + assertThat(onSuccessFailingParameterDir).exists(); + assertThat(onSuccessPassingParameterDir).doesNotExist(); + } + + @AfterEach + void deleteTempDirs() throws IOException { TempDirFieldTests.deleteIfNotNullAndExists(defaultParameterDir); TempDirFieldTests.deleteIfNotNullAndExists(neverParameterDir); TempDirFieldTests.deleteIfNotNullAndExists(alwaysParameterDir); @@ -457,6 +481,24 @@ void testOnSuccessFailingParameter(@TempDir(cleanup = ON_SUCCESS) Path onSuccess } } + @SuppressWarnings({ "JUnitMalformedDeclaration", "NewClassNamingConvention" }) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + static class OnSuccessFailingThenPassingParameterCase { + + @Test + @Order(1) + void testOnSuccessFailingParameter(@TempDir(cleanup = ON_SUCCESS) Path onSuccessFailingParameterDir) { + TempDirParameterTests.onSuccessFailingParameterDir = onSuccessFailingParameterDir; + fail(); + } + + @Test + @Order(2) + void testOnSuccessPassingParameter(@TempDir(cleanup = ON_SUCCESS) Path onSuccessPassingParameterDir) { + TempDirParameterTests.onSuccessPassingParameterDir = onSuccessPassingParameterDir; + } + } + } @Nested diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInfoParameterResolverTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInfoParameterResolverTests.java index 835b5e518ddd..1c3bc1410fe5 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInfoParameterResolverTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInfoParameterResolverTests.java @@ -13,6 +13,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.launcher.LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME; import java.util.Arrays; import java.util.List; @@ -26,6 +28,8 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.junit.platform.engine.DiscoveryIssue.Severity; /** * Integration tests for {@link TestInfoParameterResolver}. @@ -33,10 +37,10 @@ * @since 5.0 */ @Tag("class-tag") -class TestInfoParameterResolverTests { +class TestInfoParameterResolverTests extends AbstractJupiterTestEngineTests { private static final List allDisplayNames = Arrays.asList("defaultDisplayName(TestInfo)", - "custom display name", "getTags(TestInfo)", "customDisplayNameThatIsEmpty(TestInfo)"); + "custom display name", "getTags(TestInfo)", "customDisplayNameThatIsEmpty()"); public TestInfoParameterResolverTests(TestInfo testInfo) { assertThat(testInfo.getTestClass()).contains(TestInfoParameterResolverTests.class); @@ -54,11 +58,13 @@ void providedDisplayName(TestInfo testInfo) { assertEquals("custom display name", testInfo.getDisplayName()); } - // TODO Update test to expect an exception once #743 is fixed. @Test - @DisplayName("") - void customDisplayNameThatIsEmpty(TestInfo testInfo) { - assertEquals("customDisplayNameThatIsEmpty(TestInfo)", testInfo.getDisplayName()); + void customDisplayNameThatIsEmpty() { + executeTests(request -> request // + .selectors(selectClass(BlankDisplayNameTestCase.class)) // + .configurationParameter(CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, Severity.ERROR.name())) // + .testEvents() // + .assertStatistics(stats -> stats.started(1).succeeded(1)); } @Test @@ -88,4 +94,14 @@ static void beforeAndAfterAll(TestInfo testInfo) { assertEquals(TestInfoParameterResolverTests.class.getSimpleName(), testInfo.getDisplayName()); } + @SuppressWarnings("JUnitMalformedDeclaration") + static class BlankDisplayNameTestCase { + + @Test + @DisplayName("") + void test(TestInfo testInfo) { + assertEquals("test(TestInfo)", testInfo.getDisplayName()); + } + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java index 8e670d0a3601..8433ad7f5824 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java @@ -743,8 +743,7 @@ public Object createTestInstance(TestInstanceFactoryContext factoryContext, Exte instantiated(getClass(), testClass); extensionContext.getStore(ExtensionContext.Namespace.create(this)).put(new Object(), - (ExtensionContext.Store.CloseableResource) () -> callSequence.add( - "close " + testClass.getSimpleName())); + (AutoCloseable) () -> callSequence.add("close " + testClass.getSimpleName())); if (factoryContext.getOuterInstance().isPresent()) { return ReflectionSupport.newInstance(testClass, factoryContext.getOuterInstance().get()); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java index 8299c1cbff1e..698c59068383 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java @@ -195,8 +195,7 @@ public void postProcessTestInstance(Object testInstance, ExtensionContext contex String instanceType = testInstance.getClass().getSimpleName(); callSequence.add(name + ":" + instanceType); context.getStore(ExtensionContext.Namespace.create(this)).put(new Object(), - (ExtensionContext.Store.CloseableResource) () -> callSequence.add( - "close:" + name + ":" + instanceType)); + (AutoCloseable) () -> callSequence.add("close:" + name + ":" + instanceType)); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java index a73e0c494505..11be7325151a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java @@ -553,8 +553,7 @@ else if (context.getTestInstanceLifecycle().orElse(null) != TestInstance.Lifecyc callSequence.add("PreConstructCallback: name=" + name + ", testClass=" + testClass + ", outerInstance: " + factoryContext.getOuterInstance().orElse(null)); context.getStore(ExtensionContext.Namespace.create(this)).put(new Object(), - (ExtensionContext.Store.CloseableResource) () -> callSequence.add( - "close: name=" + name + ", testClass=" + testClass)); + (AutoCloseable) () -> callSequence.add("close: name=" + name + ", testClass=" + testClass)); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java index 234a2c19e63e..b630fd112054 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java @@ -20,12 +20,12 @@ import org.junit.jupiter.api.Timeout.ThreadMode; import org.junit.jupiter.api.condition.DisabledIf; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; import org.junit.jupiter.engine.execution.NamespaceAwareStore; import org.junit.jupiter.engine.extension.TimeoutInvocationFactory.SingleThreadExecutorResource; import org.junit.jupiter.engine.extension.TimeoutInvocationFactory.TimeoutInvocationParameters; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.mockito.Mock; import org.mockito.Spy; @@ -42,7 +42,7 @@ class TimeoutInvocationFactoryTests { @Spy private final Store store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), - ExtensionContext.Namespace.create(TimeoutInvocationFactoryTests.class)); + Namespace.create(TimeoutInvocationFactoryTests.class)); @Mock private Invocation invocation; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java index 5fa99e90480b..6e678900084e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java @@ -40,6 +40,7 @@ import static org.junit.platform.testkit.engine.EventConditions.started; import static org.junit.platform.testkit.engine.EventConditions.test; import static org.junit.platform.testkit.engine.EventConditions.uniqueId; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed; @@ -69,6 +70,7 @@ import org.junit.jupiter.api.extension.AnnotatedElementContext; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.TemplateInvocationValidationException; import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; import org.junit.jupiter.engine.Constants; import org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor; @@ -368,8 +370,9 @@ void failsWhenInvocationIsRequiredButNoArgumentSetsAreProvided() { var results = executeTestsForClass(ForbiddenZeroInvocationsTestCase.class); results.containerEvents().assertThatEvents() // - .haveExactly(1, event(finishedWithFailure(message( - "Configuration error: You must configure at least one set of arguments for this @ParameterizedClass")))); + .haveExactly(1, + event(finishedWithFailure(instanceOf(TemplateInvocationValidationException.class), message( + "Configuration error: You must configure at least one set of arguments for this @ParameterizedClass")))); } @Test diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java index 472429698597..9eb006afbbc9 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java @@ -36,6 +36,7 @@ import org.junit.jupiter.api.extension.ExecutableInvoker; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.MediaType; +import org.junit.jupiter.api.extension.TemplateInvocationValidationException; import org.junit.jupiter.api.extension.TestInstances; import org.junit.jupiter.api.function.ThrowingConsumer; import org.junit.jupiter.api.parallel.ExecutionMode; @@ -145,7 +146,7 @@ void throwsExceptionWhenParameterizedTestIsNotInvokedAtLeastOnce() { extensionContextWithAnnotatedTestMethod); // cause the stream to be evaluated stream.toArray(); - var exception = assertThrows(JUnitException.class, stream::close); + var exception = assertThrows(TemplateInvocationValidationException.class, stream::close); assertThat(exception).hasMessage( "Configuration error: You must configure at least one set of arguments for this @ParameterizedTest"); @@ -215,7 +216,8 @@ private ExtensionContext getExtensionContextReturningSingleMethod(Object testCas return new ExtensionContext() { - private final NamespacedHierarchicalStore store = new NamespacedHierarchicalStore<>(null); + private final NamespacedHierarchicalStore store = new NamespacedHierarchicalStore<>( + null); @Override public Optional getTestMethod() { @@ -268,7 +270,7 @@ public Optional getTestInstanceLifecycle() { } @Override - public java.util.Optional getTestInstance() { + public Optional getTestInstance() { return Optional.empty(); } @@ -306,7 +308,8 @@ public void publishDirectory(String name, ThrowingConsumer action) { @Override public Store getStore(Namespace namespace) { - var store = new NamespaceAwareStore(this.store, namespace); + var store = new NamespaceAwareStore(this.store, + org.junit.platform.engine.support.store.Namespace.create(namespace.getParts())); method // .map(it -> new ParameterizedTestContext(testClass, it, it.getAnnotation(ParameterizedTest.class))) // @@ -314,6 +317,11 @@ public Store getStore(Namespace namespace) { return store; } + @Override + public Store getStore(StoreScope scope, Namespace namespace) { + return getStore(namespace); + } + @Override public ExecutionMode getExecutionMode() { return ExecutionMode.SAME_THREAD; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java index 0c0c44842427..be726c17fba1 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java @@ -22,8 +22,8 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.appendTestTemplateInvocationSegment; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestTemplateMethod; +import static org.junit.jupiter.params.converter.DefaultArgumentConverter.DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME; import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; @@ -87,6 +87,8 @@ import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.extension.TemplateInvocationValidationException; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; import org.junit.jupiter.engine.JupiterTestEngine; import org.junit.jupiter.params.ParameterizedTestIntegrationTests.RepeatableSourcesTestCase.Action; import org.junit.jupiter.params.aggregator.AggregateWith; @@ -111,8 +113,8 @@ import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.ClassUtils; -import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.testkit.engine.EngineExecutionResults; import org.junit.platform.testkit.engine.EngineTestKit; import org.junit.platform.testkit.engine.Event; @@ -122,7 +124,7 @@ /** * @since 5.0 */ -class ParameterizedTestIntegrationTests { +class ParameterizedTestIntegrationTests extends AbstractJupiterTestEngineTests { private final Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT); @@ -394,7 +396,7 @@ void executesLifecycleMethods() { LifecycleTestCase.lifecycleEvents.clear(); LifecycleTestCase.testMethods.clear(); - var results = execute(selectClass(LifecycleTestCase.class)); + var results = executeTestsForClass(LifecycleTestCase.class); results.allEvents().assertThatEvents() // .haveExactly(1, event(test("test1"), displayName("[1] argument=foo"), finishedWithFailure(message("foo")))) // @@ -455,8 +457,9 @@ void failsWhenInvocationIsRequiredButNoArgumentSetsAreProvided() { var results = execute(ZeroInvocationsTestCase.class, "testThatRequiresInvocations", String.class); results.containerEvents().assertThatEvents() // - .haveExactly(1, event(finishedWithFailure(message( - "Configuration error: You must configure at least one set of arguments for this @ParameterizedTest")))); + .haveExactly(1, + event(finishedWithFailure(instanceOf(TemplateInvocationValidationException.class), message( + "Configuration error: You must configure at least one set of arguments for this @ParameterizedTest")))); } @Test @@ -476,18 +479,61 @@ void failsWhenNoArgumentsSourceIsDeclared() { "Configuration error: You must configure at least one arguments source for this @ParameterizedTest")))); } - private EngineExecutionResults execute(DiscoverySelector... selectors) { - return EngineTestKit.engine(new JupiterTestEngine()).selectors(selectors).execute(); + @Test + void executesWithDefaultLocaleConversionFormat() { + var results = execute(LocaleConversionTestCase.class, "testWithBcp47", Locale.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); } - private EngineExecutionResults execute(Class testClass, String methodName, Class... methodParameterTypes) { - return execute(selectMethod(testClass, methodName, ClassUtils.nullSafeToString(methodParameterTypes))); + @Test + void executesWithBcp47LocaleConversionFormat() { + var results = execute(Map.of(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, "bcp_47"), + LocaleConversionTestCase.class, "testWithBcp47", Locale.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + + @Test + void executesWithIso639LocaleConversionFormat() { + var results = execute(Map.of(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, "iso_639"), + LocaleConversionTestCase.class, "testWithIso639", Locale.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + + @Test + void reportsExceptionInStaticInitializersWithoutInvocationCountValidation() { + var results = executeTestsForClass(ExceptionInStaticInitializerTestCase.class); + + var failure = results.containerEvents().stream() // + .filter(finishedWithFailure()::matches) // + .findAny() // + .orElseThrow(); + + var throwable = failure.getRequiredPayload(TestExecutionResult.class).getThrowable().orElseThrow(); + + assertThat(throwable) // + .isInstanceOf(ExceptionInInitializerError.class) // + .hasNoSuppressedExceptions(); + } + + private EngineExecutionResults execute(Map configurationParameters, Class testClass, + String methodName, Class... methodParameterTypes) { + return EngineTestKit.engine(new JupiterTestEngine()) // + .selectors(selectMethod(testClass, methodName, ClassUtils.nullSafeToString(methodParameterTypes))) // + .configurationParameters(configurationParameters) // + .execute(); } private EngineExecutionResults execute(String methodName, Class... methodParameterTypes) { return execute(TestCase.class, methodName, methodParameterTypes); } + private EngineExecutionResults execute(Class testClass, String methodName, Class... methodParameterTypes) { + return executeTests(selectMethod(testClass, methodName, ClassUtils.nullSafeToString(methodParameterTypes))); + } + /** * @since 5.4 */ @@ -915,7 +961,7 @@ void duplicateMethodNames() { // 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))// + executeTestsForClass(DuplicateMethodNamesMethodSourceTestCase.class)// .testEvents()// .assertStatistics(stats -> stats.started(8).failed(0).finished(8)); } @@ -1334,7 +1380,7 @@ void closeAutoCloseableArgumentsAfterTestDespiteEarlyFailure() { @Test void executesTwoIterationsBasedOnIterationAndUniqueIdSelector() { var methodId = uniqueIdForTestTemplateMethod(TestCase.class, "testWithThreeIterations(int)"); - var results = execute(selectUniqueId(appendTestTemplateInvocationSegment(methodId, 3)), + var results = executeTests(selectUniqueId(appendTestTemplateInvocationSegment(methodId, 3)), selectIteration(selectMethod(TestCase.class, "testWithThreeIterations", "int"), 1)); results.allEvents().assertThatEvents() // @@ -2508,6 +2554,24 @@ public static Stream zeroArgumentsProvider() { } } + static class LocaleConversionTestCase { + + @ParameterizedTest + @ValueSource(strings = "en-US") + void testWithBcp47(Locale locale) { + assertEquals("en", locale.getLanguage()); + assertEquals("US", locale.getCountry()); + } + + @ParameterizedTest + @ValueSource(strings = "en-US") + void testWithIso639(Locale locale) { + assertEquals("en-us", locale.getLanguage()); + assertEquals("", locale.getCountry()); + } + + } + private static class TwoSingleStringArgumentsProvider implements ArgumentsProvider { @Override @@ -2598,4 +2662,24 @@ void test(AutoCloseableArgument autoCloseable) { } } + static class ExceptionInStaticInitializerTestCase { + + static { + //noinspection ConstantValue + if (true) + throw new RuntimeException("boom"); + } + + private static Stream getArguments() { + return Stream.of("foo", "bar"); + } + + @ParameterizedTest + @MethodSource("getArguments") + void test(String value) { + fail("should not be called: " + value); + } + + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java index 792bae865f48..b5f6e941bc6e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java @@ -16,10 +16,12 @@ import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; import java.util.Arrays; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.commons.PreconditionViolationException; /** @@ -164,8 +166,9 @@ void size() { } private static DefaultArgumentsAccessor defaultArgumentsAccessor(int invocationIndex, Object... arguments) { + var context = mock(ExtensionContext.class); var classLoader = DefaultArgumentsAccessorTests.class.getClassLoader(); - return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments); + return DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments); } @SuppressWarnings("unused") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java index 5336690e1c1e..501f4c09a40c 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java @@ -12,15 +12,25 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.params.converter.DefaultArgumentConverter.DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME; +import static org.junit.jupiter.params.converter.DefaultArgumentConverter.LocaleConversionFormat.BCP_47; +import static org.junit.jupiter.params.converter.DefaultArgumentConverter.LocaleConversionFormat.ISO_639; import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Locale; +import java.util.Optional; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.support.ReflectionSupport; @@ -35,7 +45,8 @@ */ class DefaultArgumentConverterTests { - private final DefaultArgumentConverter underTest = spy(DefaultArgumentConverter.INSTANCE); + private final ExtensionContext context = mock(); + private final DefaultArgumentConverter underTest = spy(new DefaultArgumentConverter(context)); @Test void isAwareOfNull() { @@ -100,6 +111,36 @@ void delegatesStringsConversion() { verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class)); } + @Test + void convertsLocaleWithDefaultFormat() { + when(context.getConfigurationParameter(eq(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME), any())) // + .thenReturn(Optional.empty()); + + assertConverts("en", Locale.class, Locale.ENGLISH); + assertConverts("en-US", Locale.class, Locale.US); + } + + @Test + void convertsLocaleWithExplicitBcp47Format() { + when(context.getConfigurationParameter(eq(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME), any())) // + .thenReturn(Optional.of(BCP_47)); + + assertConverts("en", Locale.class, Locale.ENGLISH); + assertConverts("en-US", Locale.class, Locale.US); + } + + @Test + void delegatesLocaleConversionWithExplicitIso639Format() { + when(context.getConfigurationParameter(eq(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME), any())) // + .thenReturn(Optional.of(ISO_639)); + + doReturn(null).when(underTest).convert(any(), any(), any(ClassLoader.class)); + + convert("en", Locale.class); + + verify(underTest).convert("en", Locale.class, getClassLoader(DefaultArgumentConverterTests.class)); + } + @Test void throwsExceptionForDelegatedConversionFailure() { ConversionException exception = new ConversionException("fail"); diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt index eb1aa43ed5fb..cbd6ca3a787b 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt @@ -13,6 +13,8 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtensionContext +import org.mockito.Mockito.mock /** * Unit tests for using [ArgumentsAccessor] from Kotlin. @@ -52,8 +54,9 @@ class ArgumentsAccessorKotlinTests { invocationIndex: Int, vararg arguments: Any ): DefaultArgumentsAccessor { + val context = mock(ExtensionContext::class.java) val classLoader = ArgumentsAccessorKotlinTests::class.java.classLoader - return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments) + return DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments) } fun foo() { diff --git a/jupiter-tests/src/test/resources/log4j2-test.xml b/jupiter-tests/src/test/resources/log4j2-test.xml index 509679b9abb2..3c0a10e68306 100644 --- a/jupiter-tests/src/test/resources/log4j2-test.xml +++ b/jupiter-tests/src/test/resources/log4j2-test.xml @@ -9,6 +9,7 @@ + diff --git a/platform-tests/platform-tests.gradle.kts b/platform-tests/platform-tests.gradle.kts index dd8508f495c4..3e63fe5fe14f 100644 --- a/platform-tests/platform-tests.gradle.kts +++ b/platform-tests/platform-tests.gradle.kts @@ -46,6 +46,7 @@ dependencies { testImplementation(testFixtures(projects.junitPlatformLauncher)) testImplementation(projects.junitJupiterEngine) testImplementation(testFixtures(projects.junitJupiterEngine)) + testImplementation(testFixtures(projects.junitJupiterParams)) testImplementation(libs.apiguardian) testImplementation(libs.classgraph) testImplementation(libs.jfrunit) { diff --git a/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java b/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java index ce5370f1ec28..ab9207301eff 100644 --- a/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java +++ b/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; @@ -52,12 +51,12 @@ interface Resource { /** * Demo resource class. * - *

    The class implements interface {@link CloseableResource} + *

    The class implements interface {@link AutoCloseable} * and interface {@link AutoCloseable} to show and ensure that a single * {@link ResourceValue#close()} method implementation is needed to comply * with both interfaces. */ - static class ResourceValue implements Resource, CloseableResource, AutoCloseable { + static class ResourceValue implements Resource, AutoCloseable { static final AtomicInteger creations = new AtomicInteger(); private final AtomicInteger usages = new AtomicInteger(); @@ -80,7 +79,7 @@ public int usages() { } } - private static class CloseableOnlyOnceResource implements CloseableResource { + private static class CloseableOnlyOnceResource implements AutoCloseable { private final AtomicBoolean closed = new AtomicBoolean(); diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/ModifierSupportTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/ModifierSupportTests.java index 27e1092c4441..18332317aa51 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/support/ModifierSupportTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/ModifierSupportTests.java @@ -102,6 +102,23 @@ void isAbstractDelegates(Method method) { assertEquals(ReflectionUtils.isAbstract(method), ModifierSupport.isAbstract(method)); } + @Test + void isNotAbstractPreconditions() { + assertPreconditionViolationException("Class", () -> ModifierSupport.isNotAbstract((Class) null)); + assertPreconditionViolationException("Member", () -> ModifierSupport.isNotAbstract((Member) null)); + } + + @Classes + void isNotAbstractDelegates(Class clazz) { + assertEquals(ReflectionUtils.isNotAbstract(clazz), ModifierSupport.isNotAbstract(clazz)); + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @Methods + void isNotAbstractDelegates(Method method) { + assertEquals(ReflectionUtils.isNotAbstract(method), ModifierSupport.isNotAbstract(method)); + } + @Test void isStaticPreconditions() { assertPreconditionViolationException("Class", () -> ModifierSupport.isStatic((Class) 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 5c7fea2a0f1a..11c2259b28a1 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 @@ -1036,24 +1036,43 @@ void findNestedClassesPreconditions() { // @formatter:on } + @Test + void isNestedClassPresentPreconditions() { + // @formatter:off + assertThrows(PreconditionViolationException.class, () -> ReflectionUtils.isNestedClassPresent(null, null)); + assertThrows(PreconditionViolationException.class, () -> ReflectionUtils.isNestedClassPresent(null, clazz -> true)); + assertThrows(PreconditionViolationException.class, () -> ReflectionUtils.isNestedClassPresent(getClass(), null)); + // @formatter:on + } + @Test void findNestedClasses() { // @formatter:off assertThat(findNestedClasses(Object.class)).isEmpty(); + assertThat(isNestedClassPresent(Object.class)).isFalse(); assertThat(findNestedClasses(ClassWithNestedClasses.class)) .containsOnly(Nested1.class, Nested2.class, Nested3.class); + assertThat(isNestedClassPresent(ClassWithNestedClasses.class)) + .isTrue(); assertThat(ReflectionUtils.findNestedClasses(ClassWithNestedClasses.class, clazz -> clazz.getName().contains("1"))) .containsExactly(Nested1.class); + assertThat(ReflectionUtils.isNestedClassPresent(ClassWithNestedClasses.class, clazz -> clazz.getName().contains("1"))) + .isTrue(); assertThat(ReflectionUtils.findNestedClasses(ClassWithNestedClasses.class, ReflectionUtils::isStatic)) .containsExactly(Nested3.class); + assertThat(ReflectionUtils.isNestedClassPresent(ClassWithNestedClasses.class, ReflectionUtils::isStatic)) + .isTrue(); assertThat(findNestedClasses(ClassExtendingClassWithNestedClasses.class)) .containsOnly(Nested1.class, Nested2.class, Nested3.class, Nested4.class, Nested5.class); + assertThat(isNestedClassPresent(ClassExtendingClassWithNestedClasses.class)) + .isTrue(); assertThat(findNestedClasses(ClassWithNestedClasses.Nested1.class)).isEmpty(); + assertThat(isNestedClassPresent(ClassWithNestedClasses.Nested1.class)).isFalse(); // @formatter:on } @@ -1064,26 +1083,39 @@ void findNestedClasses() { void findNestedClassesWithSeeminglyRecursiveHierarchies() { assertThat(findNestedClasses(AbstractOuterClass.class))// .containsExactly(AbstractOuterClass.InnerClass.class); + assertThat(isNestedClassPresent(AbstractOuterClass.class))// + .isTrue(); // OuterClass contains recursive hierarchies, but the non-matching // predicate should prevent cycle detection. // See https://github.com/junit-team/junit5/issues/2249 assertThat(ReflectionUtils.findNestedClasses(OuterClass.class, clazz -> false)).isEmpty(); + assertThat(ReflectionUtils.isNestedClassPresent(OuterClass.class, clazz -> false)).isFalse(); + // RecursiveInnerInnerClass is part of a recursive hierarchy, but the non-matching // predicate should prevent cycle detection. assertThat(ReflectionUtils.findNestedClasses(RecursiveInnerInnerClass.class, clazz -> false)).isEmpty(); + assertThat(ReflectionUtils.isNestedClassPresent(RecursiveInnerInnerClass.class, clazz -> false)).isFalse(); // Sibling types don't actually result in cycles. assertThat(findNestedClasses(StaticNestedSiblingClass.class))// .containsExactly(AbstractOuterClass.InnerClass.class); + assertThat(isNestedClassPresent(StaticNestedSiblingClass.class))// + .isTrue(); assertThat(findNestedClasses(InnerSiblingClass.class))// .containsExactly(AbstractOuterClass.InnerClass.class); + assertThat(isNestedClassPresent(InnerSiblingClass.class))// + .isTrue(); // Interfaces with static nested classes assertThat(findNestedClasses(OuterClassImplementingInterface.class))// .containsExactly(InnerClassImplementingInterface.class, Nested4.class); + assertThat(isNestedClassPresent(OuterClassImplementingInterface.class))// + .isTrue(); assertThat(findNestedClasses(InnerClassImplementingInterface.class))// .containsExactly(Nested4.class); + assertThat(isNestedClassPresent(InnerClassImplementingInterface.class))// + .isTrue(); } /** @@ -1105,6 +1137,10 @@ private static List> findNestedClasses(Class clazz) { return ReflectionUtils.findNestedClasses(clazz, c -> true); } + private static boolean isNestedClassPresent(Class clazz) { + return ReflectionUtils.isNestedClassPresent(clazz, c -> true); + } + private void assertNestedCycle(Class from, Class to) { assertNestedCycle(from, from, to); } diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/CustomContextClassLoaderExecutorTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/CustomContextClassLoaderExecutorTests.java index 0c0d6e9ec935..9307d4a26754 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/CustomContextClassLoaderExecutorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/CustomContextClassLoaderExecutorTests.java @@ -11,6 +11,7 @@ package org.junit.platform.console.tasks; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -28,7 +29,7 @@ class CustomContextClassLoaderExecutorTests { @Test - void invokeWithoutCustomClassLoaderDoesNotSetClassLoader() throws Exception { + void invokeWithoutCustomClassLoaderDoesNotSetClassLoader() { var originalClassLoader = Thread.currentThread().getContextClassLoader(); var executor = new CustomContextClassLoaderExecutor(Optional.empty()); @@ -42,7 +43,7 @@ void invokeWithoutCustomClassLoaderDoesNotSetClassLoader() throws Exception { } @Test - void invokeWithCustomClassLoaderSetsCustomAndResetsToOriginal() throws Exception { + void invokeWithCustomClassLoaderSetsCustomAndResetsToOriginal() { var originalClassLoader = Thread.currentThread().getContextClassLoader(); ClassLoader customClassLoader = URLClassLoader.newInstance(new URL[0]); var executor = new CustomContextClassLoaderExecutor(Optional.of(customClassLoader)); @@ -57,7 +58,7 @@ void invokeWithCustomClassLoaderSetsCustomAndResetsToOriginal() throws Exception } @Test - void invokeWithCustomClassLoaderAndEnsureItIsClosedAfterUsage() throws Exception { + void invokeWithCustomClassLoaderAndEnsureItIsClosedAfterUsage() { var closed = new AtomicBoolean(false); ClassLoader localClassLoader = new URLClassLoader(new URL[0]) { @Override @@ -73,4 +74,23 @@ public void close() throws IOException { assertEquals(4711, result); assertTrue(closed.get()); } + + @Test + void invokeWithCustomClassLoaderAndKeepItOpenAfterUsage() { + var closed = new AtomicBoolean(false); + ClassLoader localClassLoader = new URLClassLoader(new URL[0]) { + @Override + public void close() throws IOException { + closed.set(true); + super.close(); + } + }; + var executor = new CustomContextClassLoaderExecutor(Optional.of(localClassLoader), + CustomClassLoaderCloseStrategy.KEEP_OPEN); + + int result = executor.invoke(() -> 4711); + + assertEquals(4711, result); + assertFalse(closed.get()); + } } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/DiscoveryIssueTests.java b/platform-tests/src/test/java/org/junit/platform/engine/DiscoveryIssueTests.java index ed23c6099ae1..4ed3ff25a262 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/DiscoveryIssueTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/DiscoveryIssueTests.java @@ -15,6 +15,7 @@ import static org.mockito.Mockito.mock; import java.util.Optional; +import java.util.function.UnaryOperator; import org.junit.jupiter.api.Test; import org.junit.platform.engine.DiscoveryIssue.Severity; @@ -95,4 +96,32 @@ void stringRepresentationWithOptionalAttributes() { .isEqualTo( "DiscoveryIssue [severity = WARNING, message = 'message', source = ClassSource [className = 'org.junit.platform.engine.DiscoveryIssue', filePosition = null], cause = java.lang.RuntimeException: boom]"); } + + @Test + void withNewMessage() { + var issue = DiscoveryIssue.builder(Severity.WARNING, "message") // + .source(ClassSource.from(DiscoveryIssue.class)) // + .cause(new RuntimeException("boom")) // + .build(); + + var newIssue = issue.withMessage(__ -> "new message"); + + assertThat(newIssue.severity()).isEqualTo(Severity.WARNING); + assertThat(newIssue.message()).isEqualTo("new message"); + assertThat(newIssue.source()).containsSame(issue.source().orElseThrow()); + assertThat(newIssue.cause()).containsSame(issue.cause().orElseThrow()); + } + + @Test + void withSameMessage() { + var issue = DiscoveryIssue.builder(Severity.WARNING, "message") // + .source(ClassSource.from(DiscoveryIssue.class)) // + .cause(new RuntimeException("boom")) // + .build(); + + var newIssue = issue.withMessage(UnaryOperator.identity()); + + assertThat(newIssue).isSameAs(issue); + } + } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolverTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolverTests.java index e93c016d0521..5c407fc0deb0 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolverTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolverTests.java @@ -10,7 +10,7 @@ package org.junit.platform.engine.support.discovery; -import static org.junit.platform.engine.DiscoveryIssue.Severity.NOTICE; +import static org.junit.platform.engine.DiscoveryIssue.Severity.INFO; import static org.junit.platform.engine.DiscoveryIssue.Severity.WARNING; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.support.discovery.SelectorResolver.Resolution.unresolved; @@ -35,7 +35,7 @@ void allowsSelectorResolversToReportDiscoveryIssues() { @Override public Resolution resolve(ClassSelector selector, Context context) { ctx.getIssueReporter() // - .reportIssue(DiscoveryIssue.builder(NOTICE, "test") // + .reportIssue(DiscoveryIssue.builder(INFO, "test") // .source(ClassSource.from(selector.getClassName()))); return unresolved(); } @@ -52,7 +52,7 @@ public Resolution resolve(ClassSelector selector, Context context) { resolver.resolve(request, engineDescriptor); - var issue = DiscoveryIssue.builder(NOTICE, "test") // + var issue = DiscoveryIssue.builder(INFO, "test") // .source(ClassSource.from(EngineDiscoveryRequestResolverTests.class)) // .build(); verify(listener).issueEncountered(engineId, issue); diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java index 09c83e273e60..85fb2c028746 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java @@ -18,6 +18,7 @@ import static org.junit.platform.engine.TestExecutionResult.Status.FAILED; import static org.junit.platform.engine.TestExecutionResult.Status.SUCCESSFUL; import static org.junit.platform.engine.TestExecutionResult.successful; +import static org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore; import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -80,7 +81,7 @@ void init() { private HierarchicalTestExecutor createExecutor( HierarchicalTestExecutorService executorService) { var request = ExecutionRequest.create(root, listener, mock(ConfigurationParameters.class), - dummyOutputDirectoryProvider()); + dummyOutputDirectoryProvider(), dummyNamespacedHierarchicalStore()); return new HierarchicalTestExecutor<>(request, rootContext, executorService, OpenTest4JAwareThrowableCollector::new); } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespaceTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespaceTests.java new file mode 100644 index 000000000000..512d2ad9fef8 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespaceTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2025 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.platform.engine.support.store; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.EqualsAndHashCodeAssertions.assertEqualsAndHashCode; + +import org.junit.jupiter.api.Test; + +public class NamespaceTests { + + @Test + void namespacesEqualForSamePartsSequence() { + Namespace ns1 = Namespace.create("part1", "part2"); + Namespace ns2 = Namespace.create("part1", "part2"); + Namespace ns3 = Namespace.create("part2", "part1"); + + assertEqualsAndHashCode(ns1, ns2, ns3); + } + + @Test + void orderOfNamespacePartsDoesMatter() { + Namespace ns1 = Namespace.create("part1", "part2"); + Namespace ns2 = Namespace.create("part2", "part1"); + + assertNotEquals(ns1, ns2); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java index 77b14f19ca6a..81b1797c8b2b 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java @@ -569,5 +569,4 @@ public String toString() { } }; } - } diff --git a/platform-tests/src/test/java/org/junit/platform/jfr/FlightRecordingDiscoveryListenerIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/jfr/FlightRecordingDiscoveryListenerIntegrationTests.java index 5fe5dbe2aeb6..e893428d0246 100644 --- a/platform-tests/src/test/java/org/junit/platform/jfr/FlightRecordingDiscoveryListenerIntegrationTests.java +++ b/platform-tests/src/test/java/org/junit/platform/jfr/FlightRecordingDiscoveryListenerIntegrationTests.java @@ -10,6 +10,7 @@ package org.junit.platform.jfr; +import static org.junit.platform.commons.util.ExceptionUtils.readStackTrace; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; import static org.moditect.jfrunit.ExpectedEvent.event; @@ -17,8 +18,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.DisabledOnOpenJ9; -import org.junit.jupiter.engine.JupiterTestEngine; -import org.junit.platform.launcher.core.LauncherFactoryForTestingPurposesOnly; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.fakes.TestEngineStub; +import org.junit.platform.testkit.engine.EngineTestKit; import org.moditect.jfrunit.EnableEvent; import org.moditect.jfrunit.JfrEventTest; import org.moditect.jfrunit.JfrEvents; @@ -32,22 +39,39 @@ public class FlightRecordingDiscoveryListenerIntegrationTests { @Test @EnableEvent("org.junit.*") void reportsEvents() { - var launcher = LauncherFactoryForTestingPurposesOnly.createLauncher(new JupiterTestEngine()); - var request = request() // + var source = ClassSource.from(FlightRecordingDiscoveryListenerIntegrationTests.class); + var cause = new RuntimeException("boom"); + var issue = DiscoveryIssue.builder(Severity.WARNING, "some message") // + .source(source) // + .cause(cause) // + .build(); + + var testEngine = new TestEngineStub() { + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + discoveryRequest.getDiscoveryListener().issueEncountered(uniqueId, issue); + return super.discover(discoveryRequest, uniqueId); + } + }; + + EngineTestKit.discover(testEngine, request() // .selectors(selectClass(FlightRecordingDiscoveryListenerIntegrationTests.class)) // .listeners(new FlightRecordingDiscoveryListener()) // - .build(); + .build()); - launcher.discover(request); jfrEvents.awaitEvents(); assertThat(jfrEvents) // .contains(event("org.junit.LauncherDiscovery") // - // TODO JfrUnit does not yey support checking int values - // .with("selectors", 1) // - // .with("filters", 0) // - ) // + .with("selectors", 1) // + .with("filters", 0)) // .contains(event("org.junit.EngineDiscovery") // - .with("uniqueId", "[engine:junit-jupiter]")); + .with("uniqueId", "[engine:TestEngineStub]")) // + .contains(event("org.junit.DiscoveryIssue") // + .with("engineId", "[engine:TestEngineStub]") // + .with("severity", "WARNING") // + .with("message", "some message") // + .with("source", source.toString()) // + .with("cause", readStackTrace(cause))); } } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherEngineFilterTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherEngineFilterTests.java index 04c9492a09ce..d94678d62364 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherEngineFilterTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherEngineFilterTests.java @@ -167,6 +167,26 @@ void launcherThrowsExceptionWhenNoEngineMatchesIncludeEngineFilter(@TrackLogReco assertThat(log.stream(WARNING)).isEmpty(); } + @Test + void launcherThrowsExceptionWhenNoEngineMatchesIdInIncludeEngineFilter(@TrackLogRecords LogRecordListener log) { + var engine = new DemoHierarchicalTestEngine("first"); + TestDescriptor test1 = engine.addTest("test1", noOp); + LauncherDiscoveryRequest request = request() // + .selectors(selectUniqueId(test1.getUniqueId())) // + .filters(includeEngines("first", "second")) // + .build(); + + var launcher = createLauncher(engine); + var exception = assertThrows(JUnitException.class, () -> launcher.discover(request)); + + assertThat(exception.getMessage()) // + .startsWith("No TestEngine ID matched the following include EngineFilters: [second].") // + .contains("Please fix/remove the filter or add the engine.") // + .contains("Registered TestEngines:\n- first (") // + .endsWith("Registered EngineFilters:\n- EngineFilter that includes engines with IDs [first, second]"); + assertThat(log.stream(WARNING)).isEmpty(); + } + @Test void launcherWillLogWarningWhenAllEnginesWereExcluded(@TrackLogRecords LogRecordListener log) { var engine = new DemoHierarchicalTestEngine("first"); diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherTests.java index e7a0f1a7cc3e..1722bd9bfc42 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/DefaultLauncherTests.java @@ -73,6 +73,7 @@ import org.junit.platform.fakes.TestEngineSpy; import org.junit.platform.fakes.TestEngineStub; import org.junit.platform.launcher.EngineDiscoveryResult; +import org.junit.platform.launcher.LauncherConstants; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.PostDiscoveryFilter; import org.junit.platform.launcher.PostDiscoveryFilterStub; @@ -739,6 +740,7 @@ public Set getTags() { assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.FAILED); assertThat(result.testExecutionResult().getThrowable().orElseThrow()) // + .isInstanceOf(DiscoveryIssueException.class) // .hasMessageStartingWith( "TestEngine with ID 'engine-id' encountered a critical issue during test discovery") // .hasMessageContaining("(1) [ERROR] error"); @@ -758,7 +760,7 @@ void logsNonCriticalIssuesForRegularEngineExecution(@TrackLogRecords LogRecordLi @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { var listener = discoveryRequest.getDiscoveryListener(); - listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.NOTICE, "notice")); + listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.INFO, "info")); return new EngineDescriptor(uniqueId, "Engine"); } @@ -777,7 +779,7 @@ public void execute(ExecutionRequest request) { var logRecord = findFirstDiscoveryIssueLogRecord(listener, Level.INFO); assertThat(logRecord.getMessage()) // .startsWith("TestEngine with ID 'engine-id' encountered a non-critical issue during test discovery") // - .contains("(1) [NOTICE] notice"); + .contains("(1) [INFO] info"); assertThat(logRecord.getInstant()) // .isBetween(result.startTime(), result.finishTime()); } @@ -790,7 +792,7 @@ void logsAllIssuesForDiscoveryFailure(@TrackLogRecords LogRecordListener listene public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { var listener = discoveryRequest.getDiscoveryListener(); listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.ERROR, "error")); - listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.NOTICE, "notice")); + listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.INFO, "info")); throw new RuntimeException("boom"); } }); @@ -813,7 +815,7 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId logRecord = findFirstDiscoveryIssueLogRecord(listener, Level.INFO); assertThat(logRecord.getMessage()) // .startsWith("TestEngine with ID 'engine-id' encountered a non-critical issue during test discovery") // - .contains("(1) [NOTICE] notice"); + .contains("(1) [INFO] info"); assertThat(logRecord.getInstant()) // .isBetween(result.startTime(), result.finishTime()); } @@ -825,7 +827,7 @@ void logsNonCriticalIssuesForExecutionFailure(@TrackLogRecords LogRecordListener @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { var listener = discoveryRequest.getDiscoveryListener(); - listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.NOTICE, "notice")); + listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.INFO, "info")); return new EngineDescriptor(uniqueId, "Engine"); } @@ -844,7 +846,7 @@ public void execute(ExecutionRequest request) { var logRecord = findFirstDiscoveryIssueLogRecord(listener, Level.INFO); assertThat(logRecord.getMessage()) // .startsWith("TestEngine with ID 'engine-id' encountered a non-critical issue during test discovery") // - .contains("(1) [NOTICE] notice"); + .contains("(1) [INFO] info"); assertThat(logRecord.getInstant()) // .isBetween(result.startTime(), result.finishTime()); } @@ -857,6 +859,7 @@ void reportsEngineExecutionFailureOnUnresolvedUniqueIdSelectorWithEnginePrefix() assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.FAILED); assertThat(result.testExecutionResult().getThrowable().orElseThrow()) // + .isInstanceOf(DiscoveryIssueException.class) // .hasMessageStartingWith( "TestEngine with ID 'some-engine' encountered a critical issue during test discovery") // .hasMessageContaining("(1) [ERROR] %s could not be resolved", selector); @@ -879,12 +882,72 @@ void reportsEngineExecutionFailureForSelectorResolutionFailure() { assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.FAILED); assertThat(result.testExecutionResult().getThrowable().orElseThrow()) // + .isInstanceOf(DiscoveryIssueException.class) // .hasMessageStartingWith( "TestEngine with ID 'some-engine' encountered a critical issue during test discovery") // .hasMessageContaining("(1) [ERROR] %s resolution failed", selector) // .hasMessageContaining("Cause: java.lang.RuntimeException: boom"); } + @Test + void allowsConfiguringCriticalDiscoveryIssueSeverity() { + + var engine = new TestEngineStub("engine-id") { + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + var listener = discoveryRequest.getDiscoveryListener(); + listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.INFO, "info")); + return new EngineDescriptor(uniqueId, "Engine"); + } + }; + + var result = execute(engine, request -> request // + .configurationParameter(LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, "info")); + + assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.FAILED); + assertThat(result.testExecutionResult().getThrowable().orElseThrow()) // + .isInstanceOf(DiscoveryIssueException.class) // + .hasMessageStartingWith( + "TestEngine with ID 'engine-id' encountered a critical issue during test discovery") // + .hasMessageContaining("(1) [INFO] info"); + } + + @Test + void fallsBackToErrorSeverityIfCriticalSeverityIsConfiguredIncorrectly( + @TrackLogRecords LogRecordListener listener) { + + var engine = new TestEngineStub("engine-id") { + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + var listener = discoveryRequest.getDiscoveryListener(); + listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.INFO, "info")); + return new EngineDescriptor(uniqueId, "Engine"); + } + + @Override + public void execute(ExecutionRequest request) { + var executionListener = request.getEngineExecutionListener(); + var engineDescriptor = request.getRootTestDescriptor(); + executionListener.executionStarted(engineDescriptor); + executionListener.executionFinished(engineDescriptor, successful()); + } + }; + + var result = execute(engine, request -> request // + .configurationParameter(LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, "wrong")); + + assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.SUCCESSFUL); + + var logRecord = listener.stream(DiscoveryIssueCollector.class, Level.WARNING) // + .findFirst() // + .orElseThrow(); + assertThat(logRecord.getMessage()) // + .isEqualTo( + "Invalid DiscoveryIssue.Severity 'wrong' set via the '%s' configuration parameter. " + + "Falling back to the ERROR default value.", + LauncherConstants.CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME); + } + private static ReportedData execute(TestEngine engine) { return execute(engine, identity()); } @@ -905,6 +968,7 @@ private static ReportedData execute(TestEngine engine, UnaryOperator pairs() { + return Stream.of( // + new Pair(selectClass("SomeClass"), ClassSource.from("SomeClass")), // + new Pair(selectMethod("SomeClass#someMethod(int,int)"), + org.junit.platform.engine.support.descriptor.MethodSource.from("SomeClass", "someMethod", "int,int")), // + new Pair(selectClasspathResource("someResource"), ClasspathResourceSource.from("someResource")), // + new Pair(selectClasspathResource("someResource", FilePosition.from(42)), + ClasspathResourceSource.from("someResource", + org.junit.platform.engine.support.descriptor.FilePosition.from(42))), // + new Pair(selectClasspathResource("someResource", FilePosition.from(42, 23)), + ClasspathResourceSource.from("someResource", + org.junit.platform.engine.support.descriptor.FilePosition.from(42, 23))), // + new Pair(selectPackage("some.package"), PackageSource.from("some.package")), // + new Pair(selectFile("someFile"), FileSource.from(new File("someFile"))), // + new Pair(selectFile("someFile", FilePosition.from(42)), + FileSource.from(new File("someFile"), + org.junit.platform.engine.support.descriptor.FilePosition.from(42))), // + new Pair(selectFile("someFile", FilePosition.from(42, 23)), + FileSource.from(new File("someFile"), + org.junit.platform.engine.support.descriptor.FilePosition.from(42, 23))), // + new Pair(selectDirectory("someDir"), DirectorySource.from(new File("someDir"))), // + new Pair(selectUri("some:uri"), UriSource.from(URI.create("some:uri"))) // + ); + } + + record Pair(DiscoverySelector selector, TestSource source) implements RecordArguments { + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java index 06f6c308242d..f1687ea26d97 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java @@ -28,6 +28,10 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.StoreScope; import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.engine.JupiterTestEngine; import org.junit.platform.commons.PreconditionViolationException; @@ -37,11 +41,13 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.fakes.TestEngineSpy; import org.junit.platform.launcher.InterceptedTestEngine; import org.junit.platform.launcher.InterceptorInjectedLauncherSessionListener; import org.junit.platform.launcher.LauncherConstants; import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.LauncherSession; import org.junit.platform.launcher.LauncherSessionListener; import org.junit.platform.launcher.TagFilter; import org.junit.platform.launcher.TestExecutionListener; @@ -333,6 +339,83 @@ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult })); } + @Test + void extensionCanReadValueFromSessionStoreAndReadByLauncherSessionListenerOnOpened() { + var config = LauncherConfig.builder() // + .addLauncherSessionListeners(new LauncherSessionListenerOpenedExample()) // + .build(); + + try (LauncherSession session = LauncherFactory.openSession(config)) { + var launcher = session.getLauncher(); + var request = request().selectors(selectClass(SessionTrackingTestCase.class)).build(); + + AtomicReference errorRef = new AtomicReference<>(); + launcher.execute(request, new TestExecutionListener() { + @Override + public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + testExecutionResult.getThrowable().ifPresent(errorRef::set); + } + }); + + assertThat(errorRef.get()).isNull(); + } + } + + @Test + void extensionCanReadValueFromSessionStoreAndReadByLauncherSessionListenerOnClose() { + var config = LauncherConfig.builder() // + .addLauncherSessionListeners(new LauncherSessionListenerClosedExample()) // + .build(); + + try (LauncherSession session = LauncherFactory.openSession(config)) { + var launcher = session.getLauncher(); + var request = request().selectors(selectClass(SessionStoringTestCase.class)).build(); + + AtomicReference errorRef = new AtomicReference<>(); + launcher.execute(request, new TestExecutionListener() { + @Override + public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + testExecutionResult.getThrowable().ifPresent(errorRef::set); + } + }); + + assertThat(errorRef.get()).isNull(); + } + } + + @Test + void sessionResourceClosedOnSessionClose() { + CloseTrackingResource.closed = false; + var config = LauncherConfig.builder() // + .addLauncherSessionListeners(new AutoCloseCheckListener()) // + .build(); + + try (LauncherSession session = LauncherFactory.openSession(config)) { + var launcher = session.getLauncher(); + var request = request().selectors(selectClass(SessionResourceAutoCloseTestCase.class)).build(); + + launcher.execute(request); + assertThat(CloseTrackingResource.closed).isFalse(); + } + + assertThat(CloseTrackingResource.closed).isTrue(); + } + + @Test + void requestResourceClosedOnExecutionClose() { + CloseTrackingResource.closed = false; + var config = LauncherConfig.builder().build(); + + try (LauncherSession session = LauncherFactory.openSession(config)) { + var launcher = session.getLauncher(); + var request = request().selectors(selectClass(RequestResourceAutoCloseTestCase.class)).build(); + + launcher.execute(request); + + assertThat(CloseTrackingResource.closed).isTrue(); + } + } + @SuppressWarnings("SameParameterValue") private static void withSystemProperty(String key, String value, Runnable runnable) { var oldValue = System.getProperty(key); @@ -390,6 +473,125 @@ static class JUnit5Example { @Test void testJ5() { } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(SessionTrackingExtension.class) + static class SessionTrackingTestCase { + @Test + void dummyTest() { + // Just a placeholder to trigger the extension + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(SessionStoringExtension.class) + static class SessionStoringTestCase { + + @Test + void dummyTest() { + // Just a placeholder to trigger the extension + } + } + + static class LauncherSessionListenerOpenedExample implements LauncherSessionListener { + @Override + public void launcherSessionOpened(LauncherSession session) { + session.getStore().put(Namespace.GLOBAL, "testKey", "testValue"); + } + } + + static class LauncherSessionListenerClosedExample implements LauncherSessionListener { + @Override + public void launcherSessionClosed(LauncherSession session) { + Object storedValue = session.getStore().get(Namespace.GLOBAL, "testKey"); + assertThat(storedValue).isEqualTo("testValue"); + } + } + + static class SessionTrackingExtension implements BeforeAllCallback { + @Override + public void beforeAll(ExtensionContext context) { + var value = context.getStore(ExtensionContext.Namespace.GLOBAL).get("testKey"); + if (!"testValue".equals(value)) { + throw new IllegalStateException("Expected 'testValue' but got: " + value); + } + + value = context.getStore(StoreScope.LAUNCHER_SESSION, ExtensionContext.Namespace.GLOBAL).get("testKey"); + if (!"testValue".equals(value)) { + throw new IllegalStateException("Expected 'testValue' but got: " + value); + } + } + } + + static class SessionStoringExtension implements BeforeAllCallback { + @Override + public void beforeAll(ExtensionContext context) { + context.getStore(StoreScope.LAUNCHER_SESSION, ExtensionContext.Namespace.GLOBAL).put("testKey", + "testValue"); + } + } + + private static class CloseTrackingResource implements AutoCloseable { + private static boolean closed = false; + + @Override + public void close() { + closed = true; + } + + public boolean isClosed() { + return closed; + } + } + + private static class SessionResourceStoreUsingExtension implements BeforeAllCallback { + @Override + public void beforeAll(ExtensionContext context) { + CloseTrackingResource sessionResource = new CloseTrackingResource(); + context.getStore(StoreScope.LAUNCHER_SESSION, ExtensionContext.Namespace.GLOBAL).put("sessionResource", + sessionResource); + } + } + + private static class RequestResourceStoreUsingExtension implements BeforeAllCallback { + @Override + public void beforeAll(ExtensionContext context) { + CloseTrackingResource requestResource = new CloseTrackingResource(); + context.getStore(StoreScope.EXECUTION_REQUEST, ExtensionContext.Namespace.GLOBAL).put("requestResource", + requestResource); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(SessionResourceStoreUsingExtension.class) + static class SessionResourceAutoCloseTestCase { + + @Test + void dummyTest() { + // Just a placeholder to trigger the extension + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(RequestResourceStoreUsingExtension.class) + static class RequestResourceAutoCloseTestCase { + + @Test + void dummyTest() { + // Just a placeholder to trigger the extension + } + } + + private static class AutoCloseCheckListener implements LauncherSessionListener { + @Override + public void launcherSessionClosed(LauncherSession session) { + CloseTrackingResource sessionResource = session // + .getStore() // + .get(Namespace.GLOBAL, "sessionResource", CloseTrackingResource.class); + + assertThat(sessionResource.isClosed()).isFalse(); + } } } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/StoreSharingTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/StoreSharingTests.java new file mode 100644 index 000000000000..043a88952cda --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/StoreSharingTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015-2025 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.platform.launcher.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.fakes.TestEngineSpy; +import org.junit.platform.fakes.TestEngineStub; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; + +/** + * @since 5.13 + */ +class StoreSharingTests { + + @Test + void twoDummyEnginesUseRequestLevelStore() { + TestEngineSpy engineWriter = new TestEngineSpy("Writer") { + @Override + public void execute(ExecutionRequest request) { + request.getStore().put(Namespace.GLOBAL, "sharedKey", "Hello from Writer"); + super.execute(request); + } + }; + + TestEngineStub engineReader = new TestEngineStub("Reader") { + @Override + public void execute(ExecutionRequest request) { + Object value = request.getStore().get(Namespace.GLOBAL, "sharedKey"); + assertEquals("Hello from Writer", value); + super.execute(request); + } + }; + + ExecutionRequest request = mock(ExecutionRequest.class); + when(request.getStore()).thenReturn(NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore()); + + Launcher launcher = LauncherFactory.create( // + LauncherConfig.builder() // + .addTestEngines(engineWriter, engineReader) // + .build()); + + LauncherDiscoveryRequest discoveryRequest = LauncherDiscoveryRequestBuilder // + .request() // + .build(); + + launcher.execute(discoveryRequest); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java index 8041c89f1cf7..91b8b5d73676 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java @@ -25,6 +25,9 @@ import static org.junit.platform.testkit.engine.EventConditions.test; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import java.nio.file.Path; @@ -33,14 +36,27 @@ import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.FilterResult; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; +import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.PostDiscoveryFilter; +import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; import org.junit.platform.suite.engine.testcases.ConfigurationSensitiveTestCase; import org.junit.platform.suite.engine.testcases.DynamicTestsTestCase; +import org.junit.platform.suite.engine.testcases.ErroneousTestCase; import org.junit.platform.suite.engine.testcases.JUnit4TestsTestCase; import org.junit.platform.suite.engine.testcases.MultipleTestsTestCase; import org.junit.platform.suite.engine.testcases.SingleTestTestCase; @@ -54,6 +70,8 @@ import org.junit.platform.suite.engine.testsuites.EmptyDynamicTestWithFailIfNoTestFalseSuite; import org.junit.platform.suite.engine.testsuites.EmptyTestCaseSuite; import org.junit.platform.suite.engine.testsuites.EmptyTestCaseWithFailIfNoTestFalseSuite; +import org.junit.platform.suite.engine.testsuites.ErroneousTestSuite; +import org.junit.platform.suite.engine.testsuites.InheritedSuite; import org.junit.platform.suite.engine.testsuites.MultiEngineSuite; import org.junit.platform.suite.engine.testsuites.MultipleSuite; import org.junit.platform.suite.engine.testsuites.NestedSuite; @@ -62,6 +80,7 @@ import org.junit.platform.suite.engine.testsuites.SelectMethodsSuite; import org.junit.platform.suite.engine.testsuites.SuiteDisplayNameSuite; import org.junit.platform.suite.engine.testsuites.SuiteSuite; +import org.junit.platform.suite.engine.testsuites.SuiteWithErroneousTestSuite; import org.junit.platform.suite.engine.testsuites.ThreePartCyclicSuite; import org.junit.platform.testkit.engine.EngineTestKit; @@ -73,16 +92,22 @@ class SuiteEngineTests { @TempDir private Path outputDir; - @Test - void selectClasses() { + @ParameterizedTest + @ValueSource(classes = { SelectClassesSuite.class, InheritedSuite.class }) + void selectClasses(Class suiteClass) { // @formatter:off - EngineTestKit.engine(ENGINE_ID) - .selectors(selectClass(SelectClassesSuite.class)) - .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)) + var testKit = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(suiteClass)) + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)); + + assertThat(testKit.discover().getDiscoveryIssues()) + .isEmpty(); + + testKit .execute() .testEvents() .assertThatEvents() - .haveExactly(1, event(test(SelectClassesSuite.class.getName()), finishedSuccessfully())) + .haveExactly(1, event(test(suiteClass.getName()), finishedSuccessfully())) .haveExactly(1, event(test(SingleTestTestCase.class.getName()), finishedSuccessfully())); // @formatter:on } @@ -90,8 +115,13 @@ void selectClasses() { @Test void selectMethods() { // @formatter:off - EngineTestKit.engine(ENGINE_ID) - .selectors(selectClass(SelectMethodsSuite.class)) + var testKit = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(SelectMethodsSuite.class)); + + assertThat(testKit.discover().getDiscoveryIssues()) + .isEmpty(); + + testKit .execute() .testEvents() .assertThatEvents() @@ -115,8 +145,13 @@ void suiteDisplayName() { @Test void abstractSuiteIsNotExecuted() { // @formatter:off - EngineTestKit.engine(ENGINE_ID) - .selectors(selectClass(AbstractSuite.class)) + var testKit = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(AbstractSuite.class)); + + assertThat(testKit.discover().getDiscoveryIssues()) + .isEmpty(); + + testKit .execute() .testEvents() .assertThatEvents() @@ -127,8 +162,18 @@ void abstractSuiteIsNotExecuted() { @Test void privateSuiteIsNotExecuted() { // @formatter:off - EngineTestKit.engine(ENGINE_ID) - .selectors(selectClass(PrivateSuite.class)) + var message = "@Suite class '%s' must not be private. It will not be executed." + .formatted(PrivateSuite.class.getName()); + var issue = DiscoveryIssue.builder(Severity.WARNING, message) + .source(ClassSource.from(PrivateSuite.class)) + .build(); + var testKit = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(PrivateSuite.class)); + + assertThat(testKit.discover().getDiscoveryIssues()) + .containsExactly(issue); + + testKit .execute() .testEvents() .assertThatEvents() @@ -137,10 +182,86 @@ void privateSuiteIsNotExecuted() { } @Test - void innerSuiteIsNotExecuted() { + void abstractPrivateSuiteIsNotExecuted() { // @formatter:off - EngineTestKit.engine(ENGINE_ID) - .selectors(selectClass(InnerSuite.class)) + var testKit = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(AbstractPrivateSuite.class)); + + assertThat(testKit.discover().getDiscoveryIssues()) + .isEmpty(); + + testKit + .execute() + .testEvents() + .assertThatEvents() + .isEmpty(); + // @formatter:on + } + + @ParameterizedTest + @ValueSource(classes = { InnerSuite.class, AbstractInnerSuite.class }) + void innerSuiteIsNotExecuted(Class suiteClass) { + // @formatter:off + var message = "@Suite class '%s' must not be an inner class. Did you forget to declare it static? It will not be executed." + .formatted(suiteClass.getName()); + var issue = DiscoveryIssue.builder(Severity.WARNING, message) + .source(ClassSource.from(suiteClass)) + .build(); + var testKit = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(suiteClass)); + + assertThat(testKit.discover().getDiscoveryIssues()) + .containsExactly(issue); + + testKit + .execute() + .testEvents() + .assertThatEvents() + .isEmpty(); + // @formatter:on + } + + @Test + void localSuiteIsNotExecuted() { + + @Suite + @SelectClasses(names = "org.junit.platform.suite.engine.testcases.SingleTestTestCase") + class LocalSuite { + } + + // @formatter:off + var message = "@Suite class '%s' must not be a local class. It will not be executed." + .formatted(LocalSuite.class.getName()); + var issue = DiscoveryIssue.builder(Severity.WARNING, message) + .source(ClassSource.from(LocalSuite.class)) + .build(); + var testKit = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(LocalSuite.class)); + + assertThat(testKit.discover().getDiscoveryIssues()) + .containsExactly(issue); + + testKit + .execute() + .testEvents() + .assertThatEvents() + .isEmpty(); + // @formatter:on + } + + @Test + void anonymousSuiteIsNotExecuted() { + var object = new Object() { + }; + + // @formatter:off + var testKit = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(object.getClass())); + + assertThat(testKit.discover().getDiscoveryIssues()) + .isEmpty(); + + testKit .execute() .testEvents() .assertThatEvents() @@ -401,9 +522,24 @@ void pruneAfterPostDiscoveryFilters() { @Test void cyclicSuite() { // @formatter:off - EngineTestKit.engine(ENGINE_ID) + var expectedUniqueId = UniqueId.forEngine(ENGINE_ID) + .append(SuiteTestDescriptor.SEGMENT_TYPE, CyclicSuite.class.getName()) + .appendEngine(ENGINE_ID) + .append(SuiteTestDescriptor.SEGMENT_TYPE, CyclicSuite.class.getName()); + var message = "The suite configuration of [%s] resulted in a cycle [%s] and will not be discovered a second time." + .formatted(CyclicSuite.class.getName(), expectedUniqueId); + var issue = DiscoveryIssue.builder(Severity.INFO, message) + .source(ClassSource.from(CyclicSuite.class)) + .build(); + + var testKit = EngineTestKit.engine(ENGINE_ID) .selectors(selectClass(CyclicSuite.class)) - .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)) + .outputDirectoryProvider(hierarchicalOutputDirectoryProvider(outputDir)); + + assertThat(testKit.discover().getDiscoveryIssues()) + .containsExactly(issue); + + testKit .execute() .allEvents() .assertThatEvents() @@ -467,6 +603,56 @@ void passesOutputDirectoryProviderToEnginesInSuite() { assertThat(outputDir).isDirectoryRecursivelyContaining("glob:**/test.txt"); } + @Test + void discoveryIssueOfNestedTestEnginesAreReported() throws Exception { + // @formatter:off + var testKit = EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(SuiteWithErroneousTestSuite.class)); + + var discoveryIssues = testKit.discover().getDiscoveryIssues(); + assertThat(discoveryIssues).hasSize(1); + + var issue = discoveryIssues.getFirst(); + assertThat(issue.message()) // + .startsWith("[junit-jupiter] @BeforeAll method") // + .endsWith(" (via @Suite %s > %s).".formatted(SuiteWithErroneousTestSuite.class.getName(), + ErroneousTestSuite.class.getName())); + + var method = ErroneousTestCase.class.getDeclaredMethod("nonStaticLifecycleMethod"); + assertThat(issue.source()).contains(MethodSource.from(method)); + + testKit + .execute() + .testEvents() + .assertThatEvents() + .isEmpty(); + // @formatter:on + } + + @Suite + @SelectClasses(SingleTestTestCase.class) + abstract private static class AbstractPrivateSuite { + } + + @Test + void suiteEnginePassesRequestLevelStoreToSuiteTestDescriptors() { + UniqueId engineId = UniqueId.forEngine(SuiteEngineDescriptor.ENGINE_ID); + SuiteEngineDescriptor engineDescriptor = new SuiteEngineDescriptor(engineId); + + SuiteTestDescriptor mockDescriptor = mock(SuiteTestDescriptor.class); + engineDescriptor.addChild(mockDescriptor); + + EngineExecutionListener listener = mock(EngineExecutionListener.class); + NamespacedHierarchicalStore requestLevelStore = NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore(); + + ExecutionRequest request = ExecutionRequest.create(engineDescriptor, listener, + mock(ConfigurationParameters.class), mock(OutputDirectoryProvider.class), requestLevelStore); + + new SuiteTestEngine().execute(request); + + verify(mockDescriptor).execute(same(listener), same(requestLevelStore)); + } + @Suite @SelectClasses(SingleTestTestCase.class) private static class PrivateSuite { @@ -474,7 +660,12 @@ private static class PrivateSuite { @Suite @SelectClasses(names = "org.junit.platform.suite.engine.testcases.SingleTestTestCase") - private class InnerSuite { + abstract class AbstractInnerSuite { + } + + @Suite + @SelectClasses(names = "org.junit.platform.suite.engine.testcases.SingleTestTestCase") + class InnerSuite { } } diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteTestDescriptorTests.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteTestDescriptorTests.java index fac2f2cf97ea..d9b2be9a741e 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteTestDescriptorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteTestDescriptorTests.java @@ -50,9 +50,9 @@ class SuiteTestDescriptorTests { final ConfigurationParameters configurationParameters = new EmptyConfigurationParameters(); final OutputDirectoryProvider outputDirectoryProvider = OutputDirectoryProviders.dummyOutputDirectoryProvider(); - final DiscoveryIssueReporter discoveryIssueReporter = DiscoveryIssueReporter.create(mock(), engineId); + final DiscoveryIssueReporter discoveryIssueReporter = DiscoveryIssueReporter.forwarding(mock(), engineId); final SuiteTestDescriptor suite = new SuiteTestDescriptor(suiteId, TestSuite.class, configurationParameters, - outputDirectoryProvider, discoveryIssueReporter); + outputDirectoryProvider, mock(), discoveryIssueReporter); @Test void suiteIsEmptyBeforeDiscovery() { diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/testcases/ErroneousTestCase.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/testcases/ErroneousTestCase.java new file mode 100644 index 000000000000..bd73a27ea69e --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/testcases/ErroneousTestCase.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015-2025 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.platform.suite.engine.testcases; + +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ErroneousTestCase { + + @SuppressWarnings({ "JUnitMalformedDeclaration", "unused" }) + @BeforeAll + void nonStaticLifecycleMethod() { + fail("should not be called"); + } + + @Test + void name() { + fail("should not be called"); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/ErroneousTestSuite.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/ErroneousTestSuite.java new file mode 100644 index 000000000000..988770cd5ec9 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/ErroneousTestSuite.java @@ -0,0 +1,20 @@ +/* + * Copyright 2015-2025 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.platform.suite.engine.testsuites; + +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.engine.testcases.ErroneousTestCase; + +@Suite +@SelectClasses(ErroneousTestCase.class) +public class ErroneousTestSuite { +} diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/InheritedSuite.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/InheritedSuite.java new file mode 100644 index 000000000000..fd6bb9a3aea1 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/InheritedSuite.java @@ -0,0 +1,14 @@ +/* + * Copyright 2015-2025 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.platform.suite.engine.testsuites; + +public class InheritedSuite extends AbstractSuite { +} diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SuiteWithErroneousTestSuite.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SuiteWithErroneousTestSuite.java new file mode 100644 index 000000000000..3b0825d0fafd --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SuiteWithErroneousTestSuite.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015-2025 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.platform.suite.engine.testsuites; + +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +@Suite +@SelectClasses(ErroneousTestSuite.class) +public class SuiteWithErroneousTestSuite { +} diff --git a/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java b/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java index 55325c1962c4..60776a7b6c4e 100644 --- a/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java +++ b/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java @@ -11,7 +11,14 @@ package org.junit.platform.testkit.engine; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.mockito.ArgumentCaptor.forClass; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.util.Optional; import java.util.function.UnaryOperator; @@ -23,7 +30,15 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; +import org.junit.platform.launcher.LauncherDiscoveryListener; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.core.EngineExecutionOrchestrator; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedConstruction; class EngineTestKitTests { @@ -46,6 +61,29 @@ void ignoresImplicitConfigurationParametersByDefault() { assertThat(value).isEmpty(); } + @Test + @SuppressWarnings("unchecked") + void verifyRequestLevelStoreIsUsedInExecution() { + TestEngine testEngine = mock(TestEngine.class); + when(testEngine.getId()).thenReturn("test-engine"); + + LauncherDiscoveryRequest request = mock(LauncherDiscoveryRequest.class); + when(request.getDiscoveryListener()).thenReturn(LauncherDiscoveryListener.NOOP); + + try (MockedConstruction mockedConstruction = mockConstruction( + EngineExecutionOrchestrator.class)) { + EngineTestKit.execute(testEngine, request); + assertThat(mockedConstruction.constructed()).isNotEmpty(); + + EngineExecutionOrchestrator mockOrchestrator = mockedConstruction.constructed().getFirst(); + ArgumentCaptor> storeCaptor = forClass( + NamespacedHierarchicalStore.class); + + verify(mockOrchestrator).execute(any(), any(), storeCaptor.capture()); + assertNotNull(storeCaptor.getValue(), "Request level store should be passed to execute"); + } + } + @ParameterizedTest @CsvSource({ "true, from system property", "false," }) void usesImplicitConfigurationParametersWhenEnabled(boolean enabled, String expectedValue) { 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 25cd44f39522..94bc99c60916 100644 --- a/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts +++ b/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts @@ -192,12 +192,6 @@ val test by testing.suites.getting(JvmTestSuite::class) { jvmArgumentProviders += JarPath(project, antJarsClasspath.get(), "antJars") jvmArgumentProviders += MavenDistribution(project, unzipMavenDistribution, mavenDistributionDir) - if (buildParameters.javaToolchain.version.getOrElse(21) < 24) { - (options as JUnitPlatformOptions).apply { - includeEngines("archunit") - } - } - inputs.apply { dir("projects").withPathSensitivity(RELATIVE) file("${rootDir}/gradle.properties").withPathSensitivity(RELATIVE) @@ -227,6 +221,7 @@ val test by testing.suites.getting(JvmTestSuite::class) { val gradleJavaVersion = JavaVersion.current().majorVersion.toInt() jvmArgumentProviders += JavaHomeDir(project, gradleJavaVersion, develocity.testDistribution.enabled) + jvmArgumentProviders += JavaHomeDir(project, gradleJavaVersion, develocity.testDistribution.enabled, nativeImage = true) systemProperty("gradle.java.version", gradleJavaVersion) } } @@ -253,7 +248,7 @@ class MavenRepo(project: Project, @get:Internal val repoDir: Provider) : C override fun asArguments() = listOf("-Dmaven.repo=${repoDir.get().absolutePath}") } -class JavaHomeDir(project: Project, @Input val version: Int, testDistributionEnabled: Provider) : CommandLineArgumentProvider { +class JavaHomeDir(project: Project, @Input val version: Int, testDistributionEnabled: Provider, @Input val nativeImage: Boolean = false) : CommandLineArgumentProvider { @Internal val javaLauncher: Property = project.objects.property() @@ -261,6 +256,7 @@ class JavaHomeDir(project: Project, @Input val version: Int, testDistributionEna try { project.javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(version) + nativeImageCapable = nativeImage }.get() } catch (e: Exception) { null @@ -276,7 +272,7 @@ class JavaHomeDir(project: Project, @Input val version: Int, testDistributionEna } val metadata = javaLauncher.map { it.metadata } val javaHome = metadata.map { it.installationPath.asFile.absolutePath }.orNull - return javaHome?.let { listOf("-Djava.home.$version=$it") } ?: emptyList() + return javaHome?.let { listOf("-Djava.home.$version${if (nativeImage) ".nativeImage" else ""}=$it") } ?: emptyList() } } diff --git a/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts b/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts index 6203c65fdcac..7bdc3af69a56 100644 --- a/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts +++ b/platform-tooling-support-tests/projects/graalvm-starter/build.gradle.kts @@ -34,11 +34,29 @@ tasks.test { } } +// These will be part of the next version of native-build-tools +// see https://github.com/graalvm/native-build-tools/pull/693 +val initializeAtBuildTime = listOf( + "org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor\$ClassInfo", + "org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor\$LifecycleMethods", + "org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor", + "org.junit.jupiter.engine.descriptor.ClassTemplateTestDescriptor", + "org.junit.jupiter.engine.descriptor.DynamicDescendantFilter\$Mode", + "org.junit.jupiter.engine.descriptor.ExclusiveResourceCollector\$1", + "org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor\$MethodInfo", + "org.junit.jupiter.engine.discovery.ClassSelectorResolver\$DummyClassTemplateInvocationContext", + "org.junit.platform.launcher.core.DiscoveryIssueNotifier", + "org.junit.platform.launcher.core.HierarchicalOutputDirectoryProvider", + "org.junit.platform.launcher.core.LauncherDiscoveryResult\$EngineResultInfo", + "org.junit.platform.suite.engine.SuiteTestDescriptor\$LifecycleMethods", +) + graalvmNative { binaries { named("test") { buildArgs.add("--strict-image-heap") buildArgs.add("-H:+ReportExceptionStackTraces") + buildArgs.add("--initialize-at-build-time=${initializeAtBuildTime.joinToString(",")}") } } } 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..d33cdba6790d --- /dev/null +++ b/platform-tooling-support-tests/projects/graalvm-starter/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.java.installations.fromEnv=GRAALVM_HOME +org.gradle.java.installations.auto-download=false diff --git a/platform-tooling-support-tests/projects/graalvm-starter/settings.gradle.kts b/platform-tooling-support-tests/projects/graalvm-starter/settings.gradle.kts index 3214a579e059..a53a82439ab6 100644 --- a/platform-tooling-support-tests/projects/graalvm-starter/settings.gradle.kts +++ b/platform-tooling-support-tests/projects/graalvm-starter/settings.gradle.kts @@ -1,5 +1,6 @@ pluginManagement { plugins { + // TODO Remove custom config in build.gradle.kts when upgrading id("org.graalvm.buildtools.native") version "0.10.6" } repositories { @@ -9,7 +10,7 @@ pluginManagement { } plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0" } rootProject.name = "graalvm-starter" diff --git a/platform-tooling-support-tests/projects/graalvm-starter/src/test/java/com/example/project/CalculatorParameterizedClassTests.java b/platform-tooling-support-tests/projects/graalvm-starter/src/test/java/com/example/project/CalculatorParameterizedClassTests.java new file mode 100644 index 000000000000..b26b3795d62e --- /dev/null +++ b/platform-tooling-support-tests/projects/graalvm-starter/src/test/java/com/example/project/CalculatorParameterizedClassTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2025 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.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@ParameterizedClass +@ValueSource(ints = { 1, 2 }) +class CalculatorParameterizedClassTests { + + @Parameter + int i; + + @ParameterizedTest + @ValueSource(ints = { 1, 2 }) + void parameterizedTest(int j) { + Calculator calculator = new Calculator(); + assertEquals(i + j, calculator.add(i, j)); + } + + @Nested + @ParameterizedClass + @ValueSource(ints = { 1, 2 }) + @Disabled("https://github.com/junit-team/junit5/issues/4440") + class Inner { + + final int j; + + Inner(int j) { + this.j = j; + } + + @Test + void regularTest() { + Calculator calculator = new Calculator(); + assertEquals(i + j, calculator.add(i, j)); + } + } +} diff --git a/platform-tooling-support-tests/projects/graalvm-starter/src/test/java/com/example/project/ClassLevelAnnotationTests.java b/platform-tooling-support-tests/projects/graalvm-starter/src/test/java/com/example/project/ClassLevelAnnotationTests.java index 709f4a0de1cb..14ecaa12888d 100644 --- a/platform-tooling-support-tests/projects/graalvm-starter/src/test/java/com/example/project/ClassLevelAnnotationTests.java +++ b/platform-tooling-support-tests/projects/graalvm-starter/src/test/java/com/example/project/ClassLevelAnnotationTests.java @@ -10,11 +10,14 @@ package com.example.project; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.IndicativeSentencesGeneration; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledInNativeImage; @EnabledInNativeImage +@IndicativeSentencesGeneration(generator = DisplayNameGenerator.ReplaceUnderscores.class) class ClassLevelAnnotationTests { @Nested class Inner { diff --git a/platform-tooling-support-tests/projects/graalvm-starter/src/test/resources/junit-platform.properties b/platform-tooling-support-tests/projects/graalvm-starter/src/test/resources/junit-platform.properties new file mode 100644 index 000000000000..8fc84b7fb834 --- /dev/null +++ b/platform-tooling-support-tests/projects/graalvm-starter/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.platform.stacktrace.pruning.enabled=false diff --git a/platform-tooling-support-tests/projects/gradle-kotlin-extensions/settings.gradle.kts b/platform-tooling-support-tests/projects/gradle-kotlin-extensions/settings.gradle.kts index d7d6bc0549a6..963ac745898a 100644 --- a/platform-tooling-support-tests/projects/gradle-kotlin-extensions/settings.gradle.kts +++ b/platform-tooling-support-tests/projects/gradle-kotlin-extensions/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0" } rootProject.name = "gradle-kotlin-extensions" diff --git a/platform-tooling-support-tests/projects/gradle-missing-engine/settings.gradle.kts b/platform-tooling-support-tests/projects/gradle-missing-engine/settings.gradle.kts index 9eb91b3f491b..4e05cf034d46 100644 --- a/platform-tooling-support-tests/projects/gradle-missing-engine/settings.gradle.kts +++ b/platform-tooling-support-tests/projects/gradle-missing-engine/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0" } rootProject.name = "gradle-missing-engine" diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt index 11e66a7ca44f..e8104a7e3dd8 100644 --- a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt @@ -11,4 +11,4 @@ requires java.management requires org.apiguardian.api static transitive uses org.junit.platform.commons.support.scanning.ClasspathScanner qualified exports org.junit.platform.commons.logging to org.junit.jupiter.api org.junit.jupiter.engine org.junit.jupiter.migrationsupport org.junit.jupiter.params org.junit.platform.console org.junit.platform.engine org.junit.platform.launcher org.junit.platform.reporting org.junit.platform.runner org.junit.platform.suite.api org.junit.platform.suite.engine org.junit.platform.testkit org.junit.vintage.engine -qualified exports org.junit.platform.commons.util to org.junit.jupiter.api org.junit.jupiter.engine org.junit.jupiter.migrationsupport org.junit.jupiter.params org.junit.platform.console org.junit.platform.engine org.junit.platform.launcher org.junit.platform.reporting org.junit.platform.runner org.junit.platform.suite.api org.junit.platform.suite.commons org.junit.platform.suite.engine org.junit.platform.testkit org.junit.vintage.engine +qualified exports org.junit.platform.commons.util to org.junit.jupiter.api org.junit.jupiter.engine org.junit.jupiter.migrationsupport org.junit.jupiter.params org.junit.platform.console org.junit.platform.engine org.junit.platform.jfr org.junit.platform.launcher org.junit.platform.reporting org.junit.platform.runner org.junit.platform.suite.api org.junit.platform.suite.commons org.junit.platform.suite.engine org.junit.platform.testkit org.junit.vintage.engine diff --git a/platform-tooling-support-tests/projects/java-versions/pom.xml b/platform-tooling-support-tests/projects/java-versions/pom.xml index cdbb8cf6aaf0..664c9092e437 100644 --- a/platform-tooling-support-tests/projects/java-versions/pom.xml +++ b/platform-tooling-support-tests/projects/java-versions/pom.xml @@ -39,7 +39,7 @@ maven-surefire-plugin - 3.5.2 + 3.5.3 diff --git a/platform-tooling-support-tests/projects/jupiter-starter/pom.xml b/platform-tooling-support-tests/projects/jupiter-starter/pom.xml index a0b01281e086..5fa4fdabaebb 100644 --- a/platform-tooling-support-tests/projects/jupiter-starter/pom.xml +++ b/platform-tooling-support-tests/projects/jupiter-starter/pom.xml @@ -53,7 +53,7 @@ maven-surefire-plugin - 3.5.2 + 3.5.3 diff --git a/platform-tooling-support-tests/projects/jupiter-starter/settings.gradle.kts b/platform-tooling-support-tests/projects/jupiter-starter/settings.gradle.kts index 3a1befd4bfac..2301fb77b192 100644 --- a/platform-tooling-support-tests/projects/jupiter-starter/settings.gradle.kts +++ b/platform-tooling-support-tests/projects/jupiter-starter/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0" } rootProject.name = "gradle-starter" diff --git a/platform-tooling-support-tests/projects/reflection-tests/settings.gradle.kts b/platform-tooling-support-tests/projects/reflection-tests/settings.gradle.kts index 7ec746923227..dc695374e2d9 100644 --- a/platform-tooling-support-tests/projects/reflection-tests/settings.gradle.kts +++ b/platform-tooling-support-tests/projects/reflection-tests/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0" } rootProject.name = "reflection-tests" diff --git a/platform-tooling-support-tests/projects/standalone/src/other/OtherwiseNotReferencedClass.java b/platform-tooling-support-tests/projects/standalone/src/other/OtherwiseNotReferencedClass.java new file mode 100644 index 000000000000..81be14e5346c --- /dev/null +++ b/platform-tooling-support-tests/projects/standalone/src/other/OtherwiseNotReferencedClass.java @@ -0,0 +1,14 @@ +/* + * Copyright 2015-2025 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 other; + +public class OtherwiseNotReferencedClass { +} diff --git a/platform-tooling-support-tests/projects/standalone/src/standalone/JupiterIntegration.java b/platform-tooling-support-tests/projects/standalone/src/standalone/JupiterIntegration.java index e8bbce2489cb..a2a1267e3f44 100644 --- a/platform-tooling-support-tests/projects/standalone/src/standalone/JupiterIntegration.java +++ b/platform-tooling-support-tests/projects/standalone/src/standalone/JupiterIntegration.java @@ -19,6 +19,9 @@ class JupiterIntegration { @Test void successful() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + new other.OtherwiseNotReferencedClass(); + })); } @Test diff --git a/platform-tooling-support-tests/projects/vintage/pom.xml b/platform-tooling-support-tests/projects/vintage/pom.xml index 3553f799ee3a..74d0ef2dc367 100644 --- a/platform-tooling-support-tests/projects/vintage/pom.xml +++ b/platform-tooling-support-tests/projects/vintage/pom.xml @@ -42,7 +42,7 @@ maven-surefire-plugin - 3.5.2 + 3.5.3 diff --git a/platform-tooling-support-tests/projects/vintage/settings.gradle.kts b/platform-tooling-support-tests/projects/vintage/settings.gradle.kts index a890cc361b07..00a3ba7dcdba 100644 --- a/platform-tooling-support-tests/projects/vintage/settings.gradle.kts +++ b/platform-tooling-support-tests/projects/vintage/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0" } rootProject.name = "vintage" diff --git a/platform-tooling-support-tests/src/main/java/platform/tooling/support/Helper.java b/platform-tooling-support-tests/src/main/java/platform/tooling/support/Helper.java index 7deff0c5ed1c..0841181a1903 100644 --- a/platform-tooling-support-tests/src/main/java/platform/tooling/support/Helper.java +++ b/platform-tooling-support-tests/src/main/java/platform/tooling/support/Helper.java @@ -69,7 +69,7 @@ public static List loadModuleDirectoryNames() { } } - public static Optional getJavaHome(String version) { + public static Optional getJavaHome(int version) { // First, try various system sources... var sources = Stream.of( // System.getProperty("java.home." + version), // @@ -82,4 +82,9 @@ public static Optional getJavaHome(String version) { ); return sources.filter(Objects::nonNull).findFirst().map(Path::of); } + + public static Optional getJavaHomeWithNativeImageSupport(int version) { + var value = System.getProperty("java.home." + version + ".nativeImage"); + return Optional.ofNullable(value).map(Path::of); + } } diff --git a/platform-tooling-support-tests/src/main/java/platform/tooling/support/ProcessStarters.java b/platform-tooling-support-tests/src/main/java/platform/tooling/support/ProcessStarters.java index 773b360aed7c..e2519e89f958 100644 --- a/platform-tooling-support-tests/src/main/java/platform/tooling/support/ProcessStarters.java +++ b/platform-tooling-support-tests/src/main/java/platform/tooling/support/ProcessStarters.java @@ -72,6 +72,10 @@ private static String windowsOrOtherExecutable(String cmdOrExe, String other) { } public static Optional getGradleJavaHome() { - return Helper.getJavaHome(System.getProperty("gradle.java.version")); + return Helper.getJavaHome(getGradleJavaVersion()); + } + + public static int getGradleJavaVersion() { + return Integer.parseInt(System.getProperty("gradle.java.version")); } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java index 6aaa2b1c29be..001e02c5bddc 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java @@ -61,13 +61,13 @@ void version() { @Test void nonExistingJdkVersionYieldsAnEmptyOptional() { - assertEquals(Optional.empty(), Helper.getJavaHome("does not exist")); + assertEquals(Optional.empty(), Helper.getJavaHome(-1)); } @ParameterizedTest @ValueSource(ints = 8) void checkJavaHome(int version) { - var home = Helper.getJavaHome(String.valueOf(version)); + var home = Helper.getJavaHome(version); assumeTrue(home.isPresent(), "No 'jdk' element found in Maven toolchain for: " + version); assertTrue(Files.isDirectory(home.get())); } 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 index 092c48967b09..e74c1400bd9c 100644 --- 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 @@ -13,6 +13,8 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static platform.tooling.support.Helper.getJavaHomeWithNativeImageSupport; +import static platform.tooling.support.ProcessStarters.getGradleJavaVersion; import static platform.tooling.support.tests.Projects.copyToWorkspace; import java.nio.file.Path; @@ -20,10 +22,10 @@ import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.api.extension.DisabledOnOpenJ9; import org.junit.jupiter.api.io.TempDir; import org.junit.platform.tests.process.OutputFiles; +import org.opentest4j.TestAbortedException; import platform.tooling.support.MavenRepo; import platform.tooling.support.ProcessStarters; @@ -33,16 +35,20 @@ */ @Order(Integer.MIN_VALUE) @DisabledOnOpenJ9 -@EnabledIfEnvironmentVariable(named = "GRAALVM_HOME", matches = ".+") class GraalVmStarterTests { @Test @Timeout(value = 10, unit = MINUTES) void runsTestsInNativeImage(@TempDir Path workspace, @FilePrefix("gradle") OutputFiles outputFiles) throws Exception { + + var graalVmHome = getJavaHomeWithNativeImageSupport(getGradleJavaVersion()); + var result = ProcessStarters.gradlew() // .workingDir(copyToWorkspace(Projects.GRAALVM_STARTER, workspace)) // - .addArguments("-Dmaven.repo=" + MavenRepo.dir()) // + .putEnvironment("GRAALVM_HOME", + graalVmHome.orElseThrow(TestAbortedException::new).toString()).addArguments( + "-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("javaToolchains", "nativeTest", "--no-daemon", "--stacktrace", "--no-build-cache", "--warning-mode=fail") // .redirectOutput(outputFiles) // @@ -52,7 +58,12 @@ void runsTestsInNativeImage(@TempDir Path workspace, @FilePrefix("gradle") Outpu assertThat(result.stdOutLines()) // .anyMatch(line -> line.contains("CalculatorTests > 1 + 1 = 2 SUCCESSFUL")) // .anyMatch(line -> line.contains("CalculatorTests > 1 + 100 = 101 SUCCESSFUL")) // - .anyMatch(line -> line.contains("ClassLevelAnnotationTests$Inner > test() SUCCESSFUL")) // + .anyMatch(line -> line.contains( + "ClassLevelAnnotationTests$Inner > ClassLevelAnnotationTests, Inner, test SUCCESSFUL")) // + .anyMatch( + line -> line.contains("com.example.project.CalculatorParameterizedClassTests > [1] 1 SUCCESSFUL")) // + .anyMatch( + line -> line.contains("com.example.project.CalculatorParameterizedClassTests > [2] 2 SUCCESSFUL")) // .anyMatch(line -> line.contains("com.example.project.VintageTests > test SUCCESSFUL")) // .anyMatch(line -> line.contains("BUILD SUCCESSFUL")); } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleKotlinExtensionsTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleKotlinExtensionsTests.java index 6c05aef99240..750f628ceeb2 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleKotlinExtensionsTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleKotlinExtensionsTests.java @@ -36,7 +36,7 @@ void gradle_wrapper(@TempDir Path workspace, @FilePrefix("gradle") OutputFiles o .workingDir(copyToWorkspace(Projects.GRADLE_KOTLIN_EXTENSIONS, workspace)) // .addArguments("-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("build", "--no-daemon", "--stacktrace", "--no-build-cache", "--warning-mode=fail") // - .putEnvironment("JDK8", Helper.getJavaHome("8").orElseThrow(TestAbortedException::new).toString()) // + .putEnvironment("JDK8", Helper.getJavaHome(8).orElseThrow(TestAbortedException::new).toString()) // .redirectOutput(outputFiles) // .startAndWait(); diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleMissingEngineTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleMissingEngineTests.java index fdc04d46445e..e70d548761c6 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleMissingEngineTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleMissingEngineTests.java @@ -37,7 +37,7 @@ void gradle_wrapper(@TempDir Path workspace, @FilePrefix("gradle") OutputFiles o .workingDir(copyToWorkspace(Projects.GRADLE_MISSING_ENGINE, workspace)) // .addArguments("-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("build", "--no-daemon", "--stacktrace", "--no-build-cache", "--warning-mode=fail") // - .putEnvironment("JDK8", Helper.getJavaHome("8").orElseThrow(TestAbortedException::new).toString()) // + .putEnvironment("JDK8", Helper.getJavaHome(8).orElseThrow(TestAbortedException::new).toString()) // .redirectOutput(outputFiles).startAndWait(); assertEquals(1, result.exitCode()); 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 830ef794f125..02b7a6c26861 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 @@ -108,8 +108,8 @@ private ProcessResult runGradle(OutputFiles outputFiles, int javaVersion, String .addArguments("-Djava.toolchain.version=" + javaVersion) // .addArguments("--stacktrace", "--no-build-cache", "--warning-mode=fail") // .addArguments(extraArgs) // - .putEnvironment("JDK8", Helper.getJavaHome("8").orElseThrow(TestAbortedException::new).toString()) // - .putEnvironment("JDK17", Helper.getJavaHome("17").orElseThrow(TestAbortedException::new).toString()) // + .putEnvironment("JDK8", Helper.getJavaHome(8).orElseThrow(TestAbortedException::new).toString()) // + .putEnvironment("JDK17", Helper.getJavaHome(17).orElseThrow(TestAbortedException::new).toString()) // .redirectOutput(outputFiles) // .startAndWait(); diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/JavaVersionsTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/JavaVersionsTests.java index 5c35fa41b844..e2316b2520d6 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/JavaVersionsTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/JavaVersionsTests.java @@ -42,7 +42,7 @@ class JavaVersionsTests { @Test void java_8(@FilePrefix("maven") OutputFiles outputFiles) throws Exception { - var java8Home = Helper.getJavaHome("8"); + var java8Home = Helper.getJavaHome(8); assumeTrue(java8Home.isPresent(), "Java 8 installation directory not found!"); var actualLines = execute(java8Home.get(), outputFiles, Map.of()); diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java index c278f0a8c0e8..ce872f8f0f95 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java @@ -20,7 +20,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; @@ -100,7 +99,8 @@ private Resource getOrCreateResource(ExtensionContext extensionContext, C } } - class Resource implements CloseableResource { + @SuppressWarnings("try") + class Resource implements AutoCloseable { private final T value; @@ -115,7 +115,7 @@ private T get() { } @Override - public void close() throws Throwable { + public void close() throws Exception { ((AutoCloseable) value).close(); } } 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 072d5c16730e..03eb57945fa2 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 @@ -81,7 +81,7 @@ void runOnlyOneMethodInClassTemplate(@FilePrefix("maven") OutputFiles outputFile } private ProcessResult runMaven(OutputFiles outputFiles, String... extraArgs) throws InterruptedException { - var result = ProcessStarters.maven(Helper.getJavaHome("8").orElseThrow(TestAbortedException::new)) // + var result = ProcessStarters.maven(Helper.getJavaHome(8).orElseThrow(TestAbortedException::new)) // .workingDir(workspace) // .addArguments(localMavenRepo.toCliArgument(), "-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("-Dsnapshot.repo.url=" + mavenRepoProxy.getBaseUri()) // diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenSurefireCompatibilityTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenSurefireCompatibilityTests.java index 3a62853d52aa..5228ed35b8e7 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenSurefireCompatibilityTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenSurefireCompatibilityTests.java @@ -44,7 +44,7 @@ class MavenSurefireCompatibilityTests { void testMavenSurefireCompatibilityProject(String surefireVersion, String extraArg, @TempDir Path workspace, @FilePrefix("maven") OutputFiles outputFiles) throws Exception { var extraArgs = extraArg == null ? new String[0] : new String[] { extraArg }; - var result = ProcessStarters.maven(Helper.getJavaHome("8").orElseThrow(TestAbortedException::new)) // + var result = ProcessStarters.maven(Helper.getJavaHome(8).orElseThrow(TestAbortedException::new)) // .workingDir(copyToWorkspace(Projects.MAVEN_SUREFIRE_COMPATIBILITY, workspace)) // .addArguments(localMavenRepo.toCliArgument(), "-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("-Dsurefire.version=" + surefireVersion) // diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java index 7d31baf23423..9621414e9d4c 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java @@ -76,10 +76,11 @@ private static boolean notEmpty(Path file) { } } - record OutputDir(Path root) implements ExtensionContext.Store.CloseableResource { + @SuppressWarnings("try") + record OutputDir(Path root) implements AutoCloseable { @Override - public void close() throws Throwable { + public void close() throws Exception { try (var stream = Files.walk(root).sorted(Comparator. naturalOrder().reversed())) { stream.forEach(path -> { try { diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ReflectionCompatibilityTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ReflectionCompatibilityTests.java index bc9135a1cc54..1eb8272082bb 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ReflectionCompatibilityTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ReflectionCompatibilityTests.java @@ -37,7 +37,7 @@ void gradle_wrapper(@TempDir Path workspace, @FilePrefix("gradle") OutputFiles o .workingDir(copyToWorkspace(Projects.REFLECTION_TESTS, workspace)) // .addArguments("-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("build", "--no-daemon", "--stacktrace", "--no-build-cache", "--warning-mode=fail") // - .putEnvironment("JDK8", Helper.getJavaHome("8").orElseThrow(TestAbortedException::new).toString()) // + .putEnvironment("JDK8", Helper.getJavaHome(8).orElseThrow(TestAbortedException::new).toString()) // .redirectOutput(outputFiles) // .startAndWait(); diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/StandaloneTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/StandaloneTests.java index 6cbb5bc931e1..a86b65a8aa51 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/StandaloneTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/StandaloneTests.java @@ -30,7 +30,6 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -155,8 +154,7 @@ void printVersionViaModule(@FilePrefix("java") OutputFiles outputFiles) throws E @Test @Order(1) @Execution(SAME_THREAD) - void compile(@FilePrefix("javac") OutputFiles javacOutputFiles, @FilePrefix("jar") OutputFiles jarOutputFiles) - throws Exception { + void compile(@FilePrefix("javac") OutputFiles javacOutputFiles) throws Exception { var result = ProcessStarters.javaCommand("javac") // .workingDir(workspace) // .addArguments("-Xlint:-options") // @@ -164,6 +162,7 @@ void compile(@FilePrefix("javac") OutputFiles javacOutputFiles, @FilePrefix("jar .addArguments("-proc:none") // .addArguments("-d", workspace.resolve("bin").toString()) // .addArguments("--class-path", MavenRepo.jar("junit-platform-console-standalone").toString()) // + .addArguments(workspace.resolve("src/other/OtherwiseNotReferencedClass.java").toString()) // .addArguments(workspace.resolve("src/standalone/JupiterIntegration.java").toString()) // .addArguments(workspace.resolve("src/standalone/JupiterParamsIntegration.java").toString()) // .addArguments(workspace.resolve("src/standalone/SuiteIntegration.java").toString()) // @@ -174,17 +173,6 @@ void compile(@FilePrefix("javac") OutputFiles javacOutputFiles, @FilePrefix("jar assertEquals(0, result.exitCode()); assertTrue(result.stdOut().isEmpty()); assertTrue(result.stdErr().isEmpty()); - - // create "tests.jar" that'll be picked-up by "testWithJarredTestClasses()" later - var jarFolder = Files.createDirectories(workspace.resolve("jar")); - var jarResult = ProcessStarters.javaCommand("jar") // - .workingDir(workspace) // - .addArguments("--create") // - .addArguments("--file", jarFolder.resolve("tests.jar").toString()) // - .addArguments("-C", workspace.resolve("bin").toString(), ".") // - .redirectOutput(jarOutputFiles) // - .startAndWait(); - assertEquals(0, jarResult.exitCode()); } @Test @@ -431,30 +419,14 @@ void execute(@FilePrefix("console-launcher") OutputFiles outputFiles) throws Exc assertEquals(1, result.exitCode()); - var expectedOutLines = Files.readAllLines(workspace.resolve("expected-out.txt")); - var expectedErrLines = Files.readAllLines(workspace.resolve("expected-err.txt")); - assertLinesMatch(expectedOutLines, result.stdOutLines()); - var actualErrLines = result.stdErrLines(); - if (actualErrLines.getFirst().contains("stty: /dev/tty: No such device or address")) { - // Happens intermittently on GitHub Actions on Windows - actualErrLines = new ArrayList<>(actualErrLines); - actualErrLines.removeFirst(); - } - assertLinesMatch(expectedErrLines, actualErrLines); - - var jupiterVersion = Helper.version("junit-jupiter-engine"); - var vintageVersion = Helper.version("junit-vintage-engine"); - assertTrue(result.stdErr().contains("junit-jupiter" - + " (group ID: org.junit.jupiter, artifact ID: junit-jupiter-engine, version: " + jupiterVersion)); - assertTrue(result.stdErr().contains("junit-vintage" - + " (group ID: org.junit.vintage, artifact ID: junit-vintage-engine, version: " + vintageVersion)); + assertOutputOnCurrentJvm(result); } @Test @Order(4) @Execution(SAME_THREAD) void executeOnJava8(@FilePrefix("console-launcher") OutputFiles outputFiles) throws Exception { - var java8Home = Helper.getJavaHome("8").orElseThrow(TestAbortedException::new); + var java8Home = Helper.getJavaHome(8).orElseThrow(TestAbortedException::new); var result = ProcessStarters.java(java8Home) // .workingDir(workspace) // .addArguments("-showversion") // @@ -490,7 +462,7 @@ void executeOnJava8(@FilePrefix("console-launcher") OutputFiles outputFiles) thr @Execution(SAME_THREAD) // https://github.com/junit-team/junit5/issues/2600 void executeOnJava8SelectPackage(@FilePrefix("console-launcher") OutputFiles outputFiles) throws Exception { - var java8Home = Helper.getJavaHome("8").orElseThrow(TestAbortedException::new); + var java8Home = Helper.getJavaHome(8).orElseThrow(TestAbortedException::new); var result = ProcessStarters.java(java8Home) // .workingDir(workspace).addArguments("-showversion") // .addArguments("-enableassertions") // @@ -530,27 +502,57 @@ private static List getExpectedErrLinesOnJava8(Path workspace) throws IO @Test @Order(6) @Execution(SAME_THREAD) - @Disabled("https://github.com/junit-team/junit5/issues/1724") - void executeWithJarredTestClasses(@FilePrefix("console-launcher") OutputFiles outputFiles) throws Exception { - var jar = MavenRepo.jar("junit-platform-console-standalone"); - var path = new ArrayList(); - // path.add("bin"); // "exploded" test classes are found, see also test() above - path.add(workspace.resolve("standalone/jar/tests.jar").toAbsolutePath().toString()); - path.add(jar.toString()); + void executeWithJarredTestClasses(@FilePrefix("jar") OutputFiles jarOutputFiles, + @FilePrefix("console-launcher") OutputFiles outputFiles) throws Exception { + var jar = workspace.resolve("tests.jar"); + var jarResult = ProcessStarters.javaCommand("jar") // + .workingDir(workspace) // + .addArguments("--create") // + .addArguments("--file", jar.toAbsolutePath().toString()) // + .addArguments("-C", workspace.resolve("bin").toString(), ".") // + .redirectOutput(jarOutputFiles) // + .startAndWait(); + + assertEquals(0, jarResult.exitCode()); + var result = ProcessStarters.java() // + .workingDir(workspace) // + .putEnvironment("NO_COLOR", "1") // --disable-ansi-colors .addArguments("--show-version") // .addArguments("-enableassertions") // .addArguments("-Djava.util.logging.config.file=logging.properties") // - .addArguments("--class-path", String.join(File.pathSeparator, path)) // - .addArguments("org.junit.platform.console.ConsoleLauncher") // + .addArguments("-Djunit.platform.launcher.interceptors.enabled=true") // + .addArguments("-jar", MavenRepo.jar("junit-platform-console-standalone").toString()) // .addArguments("execute") // .addArguments("--scan-class-path") // .addArguments("--disable-banner") // .addArguments("--include-classname", "standalone.*") // - .addArguments("--fail-if-no-tests") // + .addArguments("--classpath", jar.toAbsolutePath().toString()) // .redirectOutput(outputFiles) // .startAndWait(); assertEquals(1, result.exitCode()); + + assertOutputOnCurrentJvm(result); + } + + private static void assertOutputOnCurrentJvm(ProcessResult result) throws IOException { + var expectedOutLines = Files.readAllLines(workspace.resolve("expected-out.txt")); + var expectedErrLines = Files.readAllLines(workspace.resolve("expected-err.txt")); + assertLinesMatch(expectedOutLines, result.stdOutLines()); + var actualErrLines = result.stdErrLines(); + if (actualErrLines.getFirst().contains("stty: /dev/tty: No such device or address")) { + // Happens intermittently on GitHub Actions on Windows + actualErrLines = new ArrayList<>(actualErrLines); + actualErrLines.removeFirst(); + } + assertLinesMatch(expectedErrLines, actualErrLines); + + var jupiterVersion = Helper.version("junit-jupiter-engine"); + var vintageVersion = Helper.version("junit-vintage-engine"); + assertTrue(result.stdErr().contains("junit-jupiter" + + " (group ID: org.junit.jupiter, artifact ID: junit-jupiter-engine, version: " + jupiterVersion)); + assertTrue(result.stdErr().contains("junit-vintage" + + " (group ID: org.junit.vintage, artifact ID: junit-vintage-engine, version: " + vintageVersion)); } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/UnalignedClasspathTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/UnalignedClasspathTests.java index 6ce670bc5ecf..ab84f6f83392 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/UnalignedClasspathTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/UnalignedClasspathTests.java @@ -67,7 +67,7 @@ void verifyErrorMessageForUnalignedClasspath(JRE jre, Path javaHome, @TempDir Pa static Stream javaVersions() { return Stream.concat( // - Helper.getJavaHome("8").map(path -> Arguments.of(JRE.JAVA_8, path)).stream(), // + Helper.getJavaHome(8).map(path -> Arguments.of(JRE.JAVA_8, path)).stream(), // Stream.of(Arguments.of(JRE.currentJre(), currentJdkHome())) // ); } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/VintageGradleIntegrationTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/VintageGradleIntegrationTests.java index 39a02d21168e..92d88ef4ab91 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/VintageGradleIntegrationTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/VintageGradleIntegrationTests.java @@ -59,7 +59,7 @@ void supportedVersions(String version, @FilePrefix("gradle") OutputFiles outputF private ProcessResult run(OutputFiles outputFiles, String version) throws Exception { return ProcessStarters.gradlew() // .workingDir(copyToWorkspace(Projects.VINTAGE, workspace)) // - .putEnvironment("JDK8", Helper.getJavaHome("8").orElseThrow(TestAbortedException::new).toString()) // + .putEnvironment("JDK8", Helper.getJavaHome(8).orElseThrow(TestAbortedException::new).toString()) // .addArguments("build", "--no-daemon", "--stacktrace", "--no-build-cache", "--warning-mode=fail") // .addArguments("-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("-Djunit4Version=" + version) // diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/VintageMavenIntegrationTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/VintageMavenIntegrationTests.java index 2b95c5b891d5..4b2d909f28c6 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/VintageMavenIntegrationTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/VintageMavenIntegrationTests.java @@ -62,7 +62,7 @@ void supportedVersions(String version, @FilePrefix("maven") OutputFiles outputFi } private ProcessResult run(OutputFiles outputFiles, String version) throws Exception { - return ProcessStarters.maven(Helper.getJavaHome("8").orElseThrow(TestAbortedException::new)) // + return ProcessStarters.maven(Helper.getJavaHome(8).orElseThrow(TestAbortedException::new)) // .workingDir(copyToWorkspace(Projects.VINTAGE, workspace)) // .addArguments("clean", "test", "--update-snapshots", "--batch-mode") // .addArguments(localMavenRepo.toCliArgument(), "-Dmaven.repo=" + MavenRepo.dir()) //