diff --git a/.github/actions/main-build/action.yml b/.github/actions/main-build/action.yml index ecafefb92517..338667f8d39b 100644 --- a/.github/actions/main-build/action.yml +++ b/.github/actions/main-build/action.yml @@ -16,7 +16,7 @@ runs: with: arguments: ${{ inputs.arguments }} encryptionKey: ${{ inputs.encryptionKey }} - - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: ${{ always() }} with: name: Open Test Reports (${{ github.job }}) diff --git a/.github/actions/run-gradle/action.yml b/.github/actions/run-gradle/action.yml index 86436e44afb3..ad901772a30c 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 + - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 id: setup-gradle-jdk with: distribution: temurin java-version: 21 check-latest: true - - uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4 + - uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0 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 48b4a3c11e1f..3fd0f16bdc48 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 + - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: ${{ inputs.distribution }} java-version: 8 diff --git a/.github/workflows/close-inactive-issues.yml b/.github/workflows/close-inactive-issues.yml index a443402a9720..4453ca192264 100644 --- a/.github/workflows/close-inactive-issues.yml +++ b/.github/workflows/close-inactive-issues.yml @@ -11,7 +11,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: only-labels: "status: waiting-for-feedback" days-before-stale: 14 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b5aa4b56c8dd..93dd4a2889de 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,9 +32,9 @@ jobs: - javascript steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 with: languages: ${{ matrix.language }} tools: linked @@ -47,4 +47,4 @@ jobs: -Dscan.tag.CodeQL \ allMainClasses - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 diff --git a/.github/workflows/cross-version.yml b/.github/workflows/cross-version.yml index 534ea6c7b1a8..1a69c3acb333 100644 --- a/.github/workflows/cross-version.yml +++ b/.github/workflows/cross-version.yml @@ -22,34 +22,30 @@ jobs: fail-fast: false matrix: jdk: - - version: 23 - type: ga - - version: 24 - type: ea - version: 24 - type: ea - release: leyden + type: ga + distribution: oracle - version: 25 type: ea name: "OpenJDK ${{ matrix.jdk.version }} (${{ matrix.jdk.release || matrix.jdk.type }})" runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Set up Test JDK uses: ./.github/actions/setup-test-jdk - name: "Set up JDK ${{ matrix.jdk.version }} (${{ matrix.jdk.release || 'ea' }})" if: matrix.jdk.type == 'ea' - uses: oracle-actions/setup-java@2e744f723b003fdd759727d0ff654c8717024845 # v1.4.0 + uses: oracle-actions/setup-java@8fb9d7717810ccde9f8d4bef1e6f43d180f506b5 # v1.4.1 with: website: jdk.java.net release: ${{ matrix.jdk.release || matrix.jdk.version }} version: latest - name: "Set up JDK ${{ matrix.jdk.version }} (${{ matrix.jdk.distribution || 'temurin' }})" if: matrix.jdk.type == 'ga' - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: ${{ matrix.jdk.distribution || 'temurin' }} java-version: ${{ matrix.jdk.version }} @@ -67,7 +63,7 @@ jobs: -Dscan.tag.JDK_${{ matrix.jdk.version }} \ build \ --no-configuration-cache #Disable configuration cache due to https://github.com/diffplug/spotless/issues/2318 - - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: ${{ always() }} with: name: Open Test Reports (${{ github.job }} ${{ matrix.jdk.version }} (${{ matrix.jdk.release || matrix.jdk.type }})) @@ -81,7 +77,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Set up Test JDK @@ -89,7 +85,7 @@ jobs: with: distribution: semeru - name: 'Set up JDK ${{ matrix.jdk }}' - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: semeru java-version: ${{ matrix.jdk }} @@ -109,7 +105,7 @@ jobs: -Dscan.tag.OpenJ9 \ build \ --no-configuration-cache # Disable configuration cache due to https://github.com/diffplug/spotless/issues/2318 - - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: ${{ always() }} with: name: Open Test Reports (${{ github.job }}) diff --git a/.github/workflows/gradle-dependency-submission.yml b/.github/workflows/gradle-dependency-submission.yml index 6dff6b23897a..1e1c037ef8af 100644 --- a/.github/workflows/gradle-dependency-submission.yml +++ b/.github/workflows/gradle-dependency-submission.yml @@ -15,14 +15,14 @@ jobs: contents: write steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Setup Java - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: temurin java-version: 21 check-latest: true - name: Generate and submit dependency graph - uses: gradle/actions/dependency-submission@94baf225fe0a508e581a564467443d0e2379123b # v4 + uses: gradle/actions/dependency-submission@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0 diff --git a/.github/workflows/label-opened-issues.yml b/.github/workflows/label-opened-issues.yml index bbf37c72db6e..f18377bd22a1 100644 --- a/.github/workflows/label-opened-issues.yml +++ b/.github/workflows/label-opened-issues.yml @@ -10,7 +10,7 @@ jobs: permissions: issues: write steps: - - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const issue = await github.rest.issues.get({ @@ -20,7 +20,7 @@ jobs: }); const originalLabels = issue.data.labels.map(l => l.name); const statusLabels = originalLabels.filter(l => l.startsWith("status: ")); - if (statusLabels.length === 0) { + if (statusLabels.length === 0 && !originalLabels.includes("up-for-grabs")) { github.rest.issues.addLabels({ issue_number: context.issue.number, owner: context.repo.owner, diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8188d59943f1..f8d924a763fa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,11 +21,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Install GraalVM - uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1 + uses: graalvm/setup-graalvm@01ed653ac833fe80569f1ef9f25585ba2811baab # v1.3.3 with: distribution: graalvm-community version: 'latest' @@ -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@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5 + uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -49,7 +49,7 @@ jobs: runs-on: windows-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Build @@ -61,7 +61,7 @@ jobs: runs-on: macos-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Build @@ -79,7 +79,7 @@ jobs: if: github.event_name == 'push' && github.repository == 'junit-team/junit5' && (startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main') steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Publish @@ -93,7 +93,7 @@ jobs: publish -x check \ prepareGitHubAttestation - name: Generate build provenance attestations - uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0 + uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 with: subject-path: documentation/build/attestation/*.jar @@ -106,7 +106,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Install Graphviz diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 9a935daabb85..079c60cf265d 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -21,12 +21,12 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 with: results_file: results.sarif results_format: sarif @@ -48,7 +48,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.pre.node20 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif @@ -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@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 with: sarif_file: results.sarif diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1ff8f451505..96b65473e615 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: id-token: write # required for build provenance attestation steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 ref: "refs/tags/${{ env.RELEASE_TAG }}" @@ -51,11 +51,11 @@ jobs: :verifyArtifactsInStagingRepositoryAreReproducible \ --remote-repo-url=${{ env.STAGING_REPO_URL }} - name: Generate build provenance attestations - uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0 + uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 with: subject-path: build/repo/**/*.jar - name: Upload local repository for later jobs - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: local-maven-repository path: build/repo @@ -65,17 +65,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out samples repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: ${{ github.repository_owner }}/junit5-samples token: ${{ secrets.GH_TOKEN }} fetch-depth: 1 - name: Set up JDK - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 21 distribution: temurin - - uses: sbt/setup-sbt@96cf3f09dc501acdad7807fffe97dba9fa0709be # v1 + - uses: sbt/setup-sbt@26ab4b0fa1c47fa62fc1f6e51823a658fb6c760c # v1.1.7 - name: Update JUnit dependencies in samples run: java src/Updater.java ${{ github.event.inputs.releaseVersion }} - name: Inject staging repository URL @@ -90,7 +90,7 @@ jobs: issues: write steps: - name: Close GitHub milestone - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: result-encoding: string script: | @@ -122,7 +122,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 ref: "refs/tags/${{ env.RELEASE_TAG }}" @@ -143,7 +143,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 ref: "refs/tags/${{ env.RELEASE_TAG }}" @@ -186,12 +186,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 ref: "refs/tags/${{ env.RELEASE_TAG }}" - name: Download local Maven repository - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: local-maven-repository path: build/repo @@ -206,17 +206,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out samples repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: ${{ github.repository_owner }}/junit5-samples token: ${{ secrets.GH_TOKEN }} fetch-depth: 1 - name: Set up JDK - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 21 distribution: temurin - - uses: sbt/setup-sbt@96cf3f09dc501acdad7807fffe97dba9fa0709be # v1 + - uses: sbt/setup-sbt@26ab4b0fa1c47fa62fc1f6e51823a658fb6c760c # v1.1.7 - name: Update JUnit dependencies in samples run: java src/Updater.java ${{ github.event.inputs.releaseVersion }} - name: Build samples @@ -244,7 +244,7 @@ jobs: contents: write steps: - name: Create GitHub release - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const releaseVersion = "${{ github.event.inputs.releaseVersion }}"; diff --git a/.github/workflows/reproducible-build.yml b/.github/workflows/reproducible-build.yml index 6d5f3bb7a1cf..546ff5b9cd18 100644 --- a/.github/workflows/reproducible-build.yml +++ b/.github/workflows/reproducible-build.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Restore Gradle cache and display toolchains diff --git a/.github/workflows/sanitize-closed-issues.yml b/.github/workflows/sanitize-closed-issues.yml index 6f0721a0d2db..046be82f7804 100644 --- a/.github/workflows/sanitize-closed-issues.yml +++ b/.github/workflows/sanitize-closed-issues.yml @@ -10,7 +10,7 @@ jobs: permissions: issues: write steps: - - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const issue = await github.rest.issues.get({ @@ -28,7 +28,7 @@ jobs: labels: newLabels, }); } - if (issue.data.state_reason === "not_planned") { + if (issue.data.state_reason === "not_planned" || issue.data.state_reason === "duplicate") { if (issue.data.milestone) { await github.rest.issues.update({ issue_number: issue.data.number, @@ -39,18 +39,28 @@ jobs: } const statusLabels = newLabels.filter(l => l.startsWith("status: ")); if (statusLabels.length === 0) { - await github.rest.issues.createComment({ - issue_number: issue.data.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Please assign a status label to this issue.", - }); - await github.rest.issues.update({ - issue_number: issue.data.number, - owner: context.repo.owner, - repo: context.repo.repo, - state: "open", - }); + if (issue.data.state_reason === "not_planned") { + await github.rest.issues.createComment({ + issue_number: issue.data.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Please assign a status label to this issue.", + }); + await github.rest.issues.update({ + issue_number: issue.data.number, + owner: context.repo.owner, + repo: context.repo.repo, + state: "open", + }); + } else { + newLabels.push("status: duplicate"); + await github.rest.issues.update({ + issue_number: issue.data.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: newLabels, + }); + } } } else { if (!(newLabels.includes("type: task") || newLabels.includes("type: question")) && !issue.data.milestone) { diff --git a/README.md b/README.md index b8302e82cc62..0c4465796871 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.11.4](https://github.com/junit-team/junit5/releases/tag/r5.11.4) (December 16, 2024) -- Preview (Milestone/Release Candidate): [JUnit 5.12.0-RC2](https://github.com/junit-team/junit5/releases/tag/r5.12.0-RC2) (February 12, 2025) +- 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-M1](https://github.com/junit-team/junit5/releases/tag/r5.13.0-M1) (March 21, 2025) ## Documentation diff --git a/SECURITY.md b/SECURITY.md index 788b41ebd7e7..4d4bad5f6641 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,8 +11,8 @@ You'll find more information about the key here: [KEYS](./KEYS) | Version | Supported | |---------| ------------------ | -| 5.11.x | :white_check_mark: | -| < 5.11 | :x: | +| 5.12.x | :white_check_mark: | +| < 5.12 | :x: | ## Reporting a Vulnerability diff --git a/documentation/documentation.gradle.kts b/documentation/documentation.gradle.kts index 02c96f3035fc..5b2ddbfd8df0 100644 --- a/documentation/documentation.gradle.kts +++ b/documentation/documentation.gradle.kts @@ -6,7 +6,6 @@ import junitbuild.javadoc.ModuleSpecificJavadocFileOption import org.asciidoctor.gradle.base.AsciidoctorAttributeProvider import org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask import org.gradle.api.tasks.PathSensitivity.RELATIVE -import org.jetbrains.kotlin.incremental.deleteRecursivelyOrThrow plugins { alias(libs.plugins.asciidoctorConvert) @@ -55,6 +54,7 @@ dependencies { // Pull in all "modular projects" to ensure that they are included // in reports generated by the ApiReportGenerator. modularProjects.forEach { apiReport(it) } + apiReport(libs.openTestReporting.tooling.spi) // Pull in all "mavenized projects" to ensure that they are included // in the generation of build provenance attestation. @@ -172,7 +172,7 @@ tasks { args.addAll("--exclude-tag", "timeout") doFirst { - consoleLauncherTestReportsDir.get().asFile.deleteRecursivelyOrThrow() + consoleLauncherTestReportsDir.get().asFile.deleteRecursively() } finalizedBy(generateOpenTestHtmlReport) @@ -453,7 +453,7 @@ tasks { project.sourceSets.named { it.startsWith("main") }.map { it.allJava.srcDirs } ).asPath })) - addStringOption("-add-modules", "info.picocli") + addStringOption("-add-modules", "info.picocli,org.opentest4j.reporting.events") addOption(ModuleSpecificJavadocFileOption("-add-reads", mapOf( "org.junit.platform.console" to "info.picocli", "org.junit.platform.reporting" to "org.opentest4j.reporting.events", diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 4010e54b7085..0fc1d24435cd 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -111,6 +111,7 @@ endif::[] :ClassOrderer_OrderAnnotation: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ClassOrderer.OrderAnnotation.html[ClassOrderer.OrderAnnotation] :ClassOrderer_Random: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ClassOrderer.Random.html[ClassOrderer.Random] :ClassOrderer: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ClassOrderer.html[ClassOrderer] +:ClassTemplate: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ClassTemplate.html[@ClassTemplate] :Disabled: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/Disabled.html[@Disabled] :MethodOrderer_Alphanumeric: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/MethodOrderer.Alphanumeric.html[MethodOrderer.Alphanumeric] :MethodOrderer_DisplayName: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/MethodOrderer.DisplayName.html[MethodOrderer.DisplayName] @@ -136,12 +137,16 @@ endif::[] // Jupiter Extension APIs :extension-api-package: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/package-summary.html[org.junit.jupiter.api.extension] :AfterAllCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/AfterAllCallback.html[AfterAllCallback] +:AfterClassTemplateInvocationCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/AfterClassTemplateInvocationCallback.html[AfterClassTemplateInvocationCallback] :AfterEachCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/AfterEachCallback.html[AfterEachCallback] :AfterTestExecutionCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/AfterTestExecutionCallback.html[AfterTestExecutionCallback] :ParameterContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ParameterContext.html[ParameterContext] :BeforeAllCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeAllCallback.html[BeforeAllCallback] +:BeforeClassTemplateInvocationCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeClassTemplateInvocationCallback.html[BeforeClassTemplateInvocationCallback] :BeforeEachCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeEachCallback.html[BeforeEachCallback] :BeforeTestExecutionCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.html[BeforeTestExecutionCallback] +:ClassTemplateInvocationContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ClassTemplateInvocationContext.html[ClassTemplateInvocationContext] +:ClassTemplateInvocationContextProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ClassTemplateInvocationContextProvider.html[ClassTemplateInvocationContextProvider] :ExecutableInvoker: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutableInvoker.html[ExecutableInvoker] :ExecutionCondition: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutionCondition.html[ExecutionCondition] :ExtendWith: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExtendWith.html[@ExtendWith] @@ -180,17 +185,25 @@ endif::[] :TempDir: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/io/TempDir.html[@TempDir] // Jupiter Params :params-provider-package: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/package-summary.html[org.junit.jupiter.params.provider] +:AfterParameterizedClassInvocation: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/AfterParameterizedClassInvocation.html[@AfterParameterizedClassInvocation] :AnnotationBasedArgumentConverter: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.html[AnnotationBasedArgumentConverter] :AnnotationBasedArgumentsProvider: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.html[AnnotationBasedArgumentsProvider] +:AggregateWith: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/AggregateWith.html[@AggregateWith] +:Arguments: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/Arguments.html[Arguments] +:ArgumentsProvider: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/ArgumentsProvider.html[ArgumentsProvider] :ArgumentsAccessor: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/ArgumentsAccessor.html[ArgumentsAccessor] :ArgumentsAggregator: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/ArgumentsAggregator.html[ArgumentsAggregator] +:BeforeParameterizedClassInvocation: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/BeforeParameterizedClassInvocation.html[@BeforeParameterizedClassInvocation] :CsvArgumentsProvider: {junit5-repo}/blob/main/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java[CsvArgumentsProvider] :EmptySource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/EmptySource.html[@EmptySource] :FieldSource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/FieldSource.html[@FieldSource] :MethodSource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/MethodSource.html[@MethodSource] :NullAndEmptySource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/NullAndEmptySource.html[@NullAndEmptySource] :NullSource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/NullSource.html[@NullSource] +:Parameter: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/Parameter.html[@Parameter] +:ParameterizedClass: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/ParameterizedClass.html[@ParameterizedClass] :ParameterizedTest: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/ParameterizedTest.html[@ParameterizedTest] +:ParameterInfo: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/support/ParameterInfo.html[ParameterInfo] :ValueArgumentsProvider: {junit5-repo}/blob/main/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueArgumentsProvider.java[ValueArgumentsProvider] // Jupiter Engine :junit-jupiter-engine: {javadoc-root}/org.junit.jupiter.engine/org/junit/jupiter/engine/package-summary.html[junit-jupiter-engine] diff --git a/documentation/src/docs/asciidoc/release-notes/index.adoc b/documentation/src/docs/asciidoc/release-notes/index.adoc index 097e417e7854..db9492e11d39 100644 --- a/documentation/src/docs/asciidoc/release-notes/index.adoc +++ b/documentation/src/docs/asciidoc/release-notes/index.adoc @@ -17,14 +17,8 @@ authors as well as build tool and IDE vendors. include::{includedir}/link-attributes.adoc[] -include::{basedir}/release-notes-5.12.0.adoc[] - -include::{basedir}/release-notes-5.11.4.adoc[] - -include::{basedir}/release-notes-5.11.3.adoc[] +include::{basedir}/release-notes-5.13.0-M1.adoc[] -include::{basedir}/release-notes-5.11.2.adoc[] +include::{basedir}/release-notes-5.12.1.adoc[] -include::{basedir}/release-notes-5.11.1.adoc[] - -include::{basedir}/release-notes-5.11.0.adoc[] +include::{basedir}/release-notes-5.12.0.adoc[] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc deleted file mode 100644 index 19fdbc72dfa0..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc +++ /dev/null @@ -1,19 +0,0 @@ -[[release-notes-5.11.0]] -== 5.11.0 - -*Date of Release:* August 14, 2024 - -*Scope:* - -* `@FieldSource` annotation for use with `@ParameterizedTest` methods -* Repeatable `@..Source` annotations for parameterized tests -* Enhancements for authoring dynamic and parameterized tests -* `@AutoClose` annotation to automatically close field resources in tests -* `ConversionSupport` utility for converting from a string to a supported target type -* Extensible syntax for specifying discovery selectors -* `@BeforeSuite` and `@AfterSuite` annotations -* Classpath resource scanning support for engines -* Numerous bug fixes and enhancements regarding field and method search algorithms - -For complete details consult the -https://junit.org/junit5/docs/5.11.0/release-notes/index.html[5.11.0 Release Notes] online. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.1.adoc deleted file mode 100644 index 80af4a1a53d3..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.1.adoc +++ /dev/null @@ -1,55 +0,0 @@ -[[release-notes-5.11.1]] -== 5.11.1 - -*Date of Release:* September 25, 2024 - -*Scope:* Bug fixes and enhancements since 5.11.0 - -For a complete list of all _closed_ issues and pull requests for this release, consult the -link:{junit5-repo}+/milestone/80?closed=1+[5.11.1] milestone page in the JUnit repository -on GitHub. - - -[[release-notes-5.11.1-junit-platform]] -=== JUnit Platform - -[[release-notes-5.11.1-junit-platform-bug-fixes]] -==== Bug Fixes - -* Fix support for disabling ANSI colors on the console when the `NO_COLOR` environment - variable is available. -* `NamespacedHierarchicalStore` no longer throws an exception after it has been closed if - the store is queried via one of the `get(...)` or `getOrComputeIfAbsent(...)` methods; - however, if a `getOrComputeIfAbsent(...)` invocation results in the computation of a new - value, an exception will still be thrown. -* Fixed potential locking issue with `ExclusiveResource` in the - `HierarchicalTestExecutorService`, which could lead to deadlocks in certain scenarios. - -[[release-notes-5.11.1-junit-platform-new-features-and-improvements]] -==== New Features and Improvements - -* Improve parallelism and reduce number of blocked threads used by - `HierarchicalTestEngine` implementations when parallel execution is enabled and the - global read-write lock is used. - - -[[release-notes-5.11.1-junit-jupiter]] -=== JUnit Jupiter - -[[release-notes-5.11.1-junit-jupiter-bug-fixes]] -==== Bug Fixes - -* `TestWatcher` callback methods can once again access data in the - `ExtensionContext.Store`. - -[[release-notes-5.11.1-junit-jupiter-new-features-and-improvements]] -==== New Features and Improvements - -* Improve parallelism and reduce number of blocked threads in the presence of `@Isolated` - tests when parallel execution is enabled - - -[[release-notes-5.11.1-junit-vintage]] -=== JUnit Vintage - -No changes. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.2.adoc deleted file mode 100644 index 0bb4b292e1d9..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.2.adoc +++ /dev/null @@ -1,33 +0,0 @@ -[[release-notes-5.11.2]] -== 5.11.2 - -*Date of Release:* October 4, 2024 - -*Scope:* Bug fixes and enhancements since 5.11.1 - -For a complete list of all _closed_ issues and pull requests for this release, consult the -link:{junit5-repo}+/milestone/82?closed=1+[5.11.2] milestone page in the JUnit repository -on GitHub. - - -[[release-notes-5.11.2-junit-platform]] -=== JUnit Platform - -[[release-notes-5.11.2-junit-platform-bug-fixes]] -==== Bug Fixes - -* Fix regression in parallel execution that was introduced in 5.11.1 regarding global - read-write locks. When such a lock was declared on descendants of top-level nodes in the - test tree, such as Cucumber scenarios, test execution failed. - - -[[release-notes-5.11.2-junit-jupiter]] -=== JUnit Jupiter - -No changes. - - -[[release-notes-5.11.2-junit-vintage]] -=== JUnit Vintage - -No changes. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.3.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.3.adoc deleted file mode 100644 index 0588af0f1cc7..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.3.adoc +++ /dev/null @@ -1,40 +0,0 @@ -[[release-notes-5.11.3]] -== 5.11.3 - -*Date of Release:* October 21, 2024 - -*Scope:* Bug fixes and enhancements since 5.11.2 - -For a complete list of all _closed_ issues and pull requests for this release, consult the -link:{junit5-repo}+/milestone/84?closed=1+[5.11.3] milestone page in the JUnit repository -on GitHub. - - -[[release-notes-5.11.3-junit-platform]] -=== JUnit Platform - -[[release-notes-5.11.3-junit-platform-bug-fixes]] -==== Bug Fixes - -* Fixed a regression in method search algorithms introduced in 5.11.0 when classes reside - in the default package and using a Java 8 runtime. - - -[[release-notes-5.11.3-junit-jupiter]] -=== JUnit Jupiter - -[[release-notes-5.11.3-junit-jupiter-bug-fixes]] -==== Bug Fixes - -* Extensions can once again be registered via multiple `@ExtendWith` meta-annotations on - the same composed annotation on a field within a test class. -* `@ExtendWith` annotations can now also be repeated when used directly on fields and - parameters. -* All `@...Source` annotations of parameterized tests can now also be repeated when used - as meta annotations. - - -[[release-notes-5.11.3-junit-vintage]] -=== JUnit Vintage - -No changes. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.4.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.4.adoc deleted file mode 100644 index e9ed32bc772a..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.4.adoc +++ /dev/null @@ -1,39 +0,0 @@ -[[release-notes-5.11.4]] -== 5.11.4 - -*Date of Release:* December 16, 2024 - -*Scope:* Bug fixes and enhancements since 5.11.3 - -For a complete list of all _closed_ issues and pull requests for this release, consult the -link:{junit5-repo}+/milestone/86?closed=1+[5.11.4] milestone page in the -JUnit repository on GitHub. - - -[[release-notes-5.11.4-junit-platform]] -=== JUnit Platform - -[[release-notes-5.11.4-junit-platform-bug-fixes]] -==== Bug Fixes - -* Escape whitespace characters (such as line breaks) in XML attribute values (such as - exception messages) in the legacy XML report generated by the Console Launcher. This - change ensures the resulting XML files can be processed by downstream tools while - preserving whitespace characters. -* Enable auto-flushing of output in the `ConsoleLauncher` to fix issues with buffering, - in particular when using the `--details=testfeed` option. - - -[[release-notes-5.11.4-junit-jupiter]] -=== JUnit Jupiter - -[[release-notes-5.11.4-junit-jupiter-new-features-and-improvements]] -==== New Features and Improvements - -* `JAVA_25` has been added to the `JRE` enum for use with JRE-based execution conditions. - - -[[release-notes-5.11.4-junit-vintage]] -=== JUnit Vintage - -No changes. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0.adoc index d6176f3bc165..75ff54748e5f 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0.adoc @@ -20,172 +20,5 @@ * Parallel execution support in JUnit Vintage engine * Numerous bug fixes and other enhancements -For a complete list of all _closed_ issues and pull requests for this release, consult the link:{junit5-repo}+/milestone/75?closed=1+[5.12.0-M1], -link:{junit5-repo}+/milestone/88?closed=1+[5.12.0-RC1], -link:{junit5-repo}+/milestone/90?closed=1+[5.12.0-RC2], and -link:{junit5-repo}+/milestone/89?closed=1+[5.12.0] milestone pages in the JUnit repository -on GitHub. - - -[[release-notes-5.12.0-overall-improvements]] -=== Overall Improvements - -[[release-notes-5.12.0-overall-new-features-and-improvements]] -==== New Features and Improvements - -* All affected JAR files now include `native-image.properties` files that contain the -`--initialize-at-build-time` option to avoid breakages in GraalVM projects when updating -to newer versions of JUnit. - - -[[release-notes-5.12.0-junit-platform]] -=== JUnit Platform - -[[release-notes-5.12.0-junit-platform-deprecations-and-breaking-changes]] -==== Deprecations and Breaking Changes - -* `SearchOption` and `AnnotationSupport.findAnnotation(Class, Class, SearchOption)` from - `junit-platform-commons` have been deprecated. - -[[release-notes-5.12.0-junit-platform-new-features-and-improvements]] -==== New Features and Improvements - -* `ConsoleLauncher` now accepts multiple values for all `--select` options. -* `ConsoleLauncher` now supports a `--select-unique-id` option to select containers and - tests by unique ID. -* `ConsoleLauncher` supports new `--exclude-methodname` and `--include-methodname` options - to include or exclude methods based on fully qualified method names without parameters. - For example, `--exclude-methodname=^org\.example\..+#methodname` will exclude all - methods called `methodName` under package `org.example`. -* The `--select-file` and `--select-resource` options for the `ConsoleLauncher` now - support line and column numbers. -* New `ReflectionSupport.makeAccessible(Field)` public utility method to be used by third - parties instead of calling the internal `ReflectionUtils.makeAccessible(Field)` method - directly. -* The `ReflectionSupport.tryToLoadClass(...)` utility methods now support lookups for the - `"void"` pseudo-type, which indirectly supports `String` to `Class` conversion for - `"void"` in parameterized tests in JUnit Jupiter. -* New `addResourceContainerSelectorResolver()` method in - `EngineDiscoveryRequestResolver.Builder` which supports the discovery of class path - resource based tests, analogous to the existing `addClassContainerSelectorResolver()` - method. -* New `getOutputDirectoryProvider()` method in `EngineDiscoveryRequest` and `TestPlan` to - allow test engines to publish/attach files to containers and tests by calling - `EngineExecutionListener.fileEntryPublished(...)`. Registered `TestExecutionListeners` - can then access these files by overriding the `fileEntryPublished(...)` method. -* The following improvements have been made to the - <<../user-guide/index.adoc#junit-platform-reporting-open-test-reporting, Open Test Reporting>> - XML output: - - Information about the Git repository, the current branch, the commit hash, and the - current worktree status are now included in the XML report, if applicable. - - A section containing JUnit-specific metadata about each test/container to the HTML - report is now written by open-test-reporting when added to the classpath/module path - - Information about published files is now included as attachments. - - If <<../user-guide/index.adoc#running-tests-capturing-output, output capturing>> is - enabled, the captured output written to `System.out` and `System.err` is now included - in the XML report. -* Output written to `System.out` and `System.err` from non-test threads is now attributed - to the most recent test or container that was started or has written output. -* New public interface `ClasspathScanner` allowing third parties to provide a custom - implementation for scanning the classpath for classes and resources. -* New `AnnotationSupport.findAnnotation(Class, Class, List)` method to support searching - for an annotation on an inner class and its runtime enclosing instance types. -* New `TestDescriptor.orderChildren(UnaryOperator> orderer)` - method to order children in place - - -[[release-notes-5.12.0-junit-jupiter]] -=== JUnit Jupiter - -[[release-notes-5.12.0-junit-jupiter-bug-fixes]] -==== Bug Fixes - -* Provide _runtime_ enclosing types of `@Nested` test classes and contained test methods - to `DisplayNameGenerator` implementations. Prior to this change, such generators were - only able to access the enclosing class in which `@Nested` was declared, but they could - not access the concrete runtime type of the enclosing instance. -* `@DisplayNameGeneration` annotations are now discovered on the _runtime_ enclosing types - of `@Nested` test classes instead of the compile-time enclosing class in which the - `@Nested` class was _declared_. -* Fix handling of "junctions" on Windows during `@TempDir` cleanup: junctions will no - longer be followed when deleting directories and broken junctions will be deleted. - -[[release-notes-5.12.0-junit-jupiter-deprecations-and-breaking-changes]] -==== Deprecations and Breaking Changes - -* When injecting `TestInfo` into test class constructors, the `TestInfo` now contains data - for the test method for which the test class instance is being created, unless the test - instance lifecycle is set to `PER_CLASS` (in which case it continues to contain the data - for the test class). If you require the `TestInfo` of the test class, you can implement - a `@BeforeAll` lifecycle method and inject `TestInfo` into that method. -* When injecting `TestReporter` into test class constructors the published report entries - are now associated with the test method rather than the test class, unless the test - instance lifecycle is set to `PER_CLASS` (in which case the published report entries - will continue to be associated with the test class). If you want to publish report - entries for the test class, you can implement a `@BeforeAll` lifecycle method and inject - `TestReporter` into that method. - -[[release-notes-5.12.0-junit-jupiter-new-features-and-improvements]] -==== New Features and Improvements - -* Kotlin contracts for Kotlin-specific assertion methods in `Assertions`. -* `@TempDir` is now supported on test class constructors. -* Shared resource locks may now be determined programmatically at runtime via the new - `@ResourceLock#providers` attribute that accepts implementations of - `ResourceLocksProvider`. -* Shared resource locks for _direct_ child nodes may now be configured via the new - `@ResourceLock(target = CHILDREN)` attribute. This may improve parallelization when - a test class declares a `READ` lock, but only a few methods hold a `READ_WRITE` lock. -* `@EnumSource` has new `from` and `to` attributes that support the selection of enum - constants within the specified range. -* In a `@ParameterizedTest` method, a `null` value can now be supplied for Java Date/Time - types such as `LocalDate` if the new `nullable` attribute in - `@JavaTimeConversionPattern` is set to `true`. -* The new `@ParameterizedTest(allowZeroInvocations = true)` attribute allows to specify that - the absence of invocations is expected in some cases and should not cause a test failure. -* Parameterized tests now support argument count validation. If the - `junit.jupiter.params.argumentCountValidation=strict` configuration parameter or the - `@ParameterizedTest(argumentCountValidation = STRICT)` attribute is set, any mismatch - between the declared number of arguments and the number of arguments provided by the - arguments source will result in an error. By default, it is still only an error if there - are fewer arguments provided than declared. -* `ArgumentsProvider` (declared via `@ArgumentsSource`), `ArgumentConverter` (declared via - `@ConvertWith`), and `ArgumentsAggregator` (declared via `@AggregateWith`) - implementations can now use constructor injection from registered `ParameterResolver` - extensions. -* `TestTemplateInvocationContextProvider` extensions can now signal that they may - potentially return zero invocation contexts by overriding the new - `mayReturnZeroTestTemplateInvocationContexts()` method. -* Extensions that implement `TestInstancePreConstructCallback`, `TestInstanceFactory`, - `TestInstancePostProcessor`, `ParameterResolver`, or `InvocationInterceptor` may - override the `getTestInstantiationExtensionContextScope()` method to enable receiving - a test-scoped `ExtensionContext` in `Extension` methods called during test class - instantiation. This behavior will become the default in future versions of JUnit. -* The new `PreInterruptCallback` interface defines the API for `Extensions` that wish to - be called prior to invocations of `Thread#interrupt()` by the `@Timeout` extension. -* When enabled via the `junit.jupiter.execution.timeout.threaddump.enabled` configuration - parameter, an implementation of `PreInterruptCallback` is registered that writes a - thread dump to `System.out` prior to interrupting a test thread due to a timeout. -* `TestReporter` now allows publishing files for a test method or test class which can be - used to include them in test reports, such as the Open Test Reporting format. -* Auto-registered extensions can now be - <<../user-guide/index.adoc#extensions-registration-automatic-filtering, filtered>> using - include and exclude patterns that can be specified as configuration parameters. -* `JRE`-based conditions such as `@EnabledOnJre` and `@DisabledForJreRange` now support - arbitrary Java versions. See the - <<../user-guide/index.adoc#writing-tests-conditional-execution-jre, User Guide>> for - details. -* The `@TempDir` extension now warns during cleanup when deleting symlinks that target - locations outside the temporary directory to signal that the target file or directory is - _not_ deleted, only the link to it. - - -[[release-notes-5.12.0-junit-vintage]] -=== JUnit Vintage - -[[release-notes-5.12.0-junit-vintage-new-features-and-improvements]] -==== New Features and Improvements - -* Added support for executing test classes and/or methods in parallel. Please refer to the - <<../user-guide/index.adoc#migrating-from-junit4-parallel-execution, User Guide>> for - more information. +For complete details consult the +https://junit.org/junit5/docs/5.12.0/release-notes/index.html[5.12.0 Release Notes] online. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.1.adoc new file mode 100644 index 000000000000..f41490897fb5 --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.1.adoc @@ -0,0 +1,38 @@ +[[release-notes-5.12.1]] +== 5.12.1 + +*Date of Release:* March 14, 2025 + +*Scope:* Bug fixes and enhancements since 5.12.0 + +For a complete list of all _closed_ issues and pull requests for this release, consult the +link:{junit5-repo}+/milestone/91?closed=1+[5.12.1] milestone page in the JUnit repository +on GitHub. + + +[[release-notes-5.12.1-junit-platform]] +=== JUnit Platform + +[[release-notes-5.12.1-junit-platform-deprecations-and-breaking-changes]] +==== Deprecations and Breaking Changes + +* Set stable module name `org.junit.platform.console.standalone` for the + `junit-platform-console-standalone` artifact, superseding the unstable name generated + from the name of the JAR file when putting the artifact on the module path. + + +[[release-notes-5.12.1-junit-jupiter]] +=== JUnit Jupiter + +[[release-notes-5.12.1-junit-jupiter-new-features-and-improvements]] +==== New Features and Improvements + +* New `ExtensionContext.getEnclosingTestClasses()` method to help with migration away from + `AnnotationSupport.findAnnotation(Class, Class, SearchOption)` (deprecated since 1.12.0) + to `AnnotationSupport.findAnnotation(Class, Class, List)`. + + +[[release-notes-5.12.1-junit-vintage]] +=== JUnit Vintage + +No changes. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc new file mode 100644 index 000000000000..4fd71ff43b85 --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc @@ -0,0 +1,101 @@ +[[release-notes-5.13.0-M1]] +== 5.13.0-M1 + +*Date of Release:* March 21, 2025 + +*Scope:* + +* Introduce `@ClassTemplate` and `@ParameterizedClass` support in JUnit Jupiter +* Access to `ParameterInfo` for JUnit Jupiter extensions +* New `@SentenceFragment` annotation for use with `IndicativeSentences` display name generator +* Add `--redirect-stdout` and `--redirect-stderr` options to `ConsoleLauncher` +* Introduce test _discovery_ support in `EngineTestKit` +* Bug fixes and other minor improvements + +For a complete list of all _closed_ issues and pull requests for this release, consult the +link:{junit5-repo}+/milestone/85?closed=1+[5.13.0-M1] milestone page in the JUnit +repository on GitHub. + + +[[release-notes-5.13.0-M1-junit-platform]] +=== JUnit Platform + +[[release-notes-5.13.0-M1-junit-platform-bug-fixes]] +==== Bug Fixes + +* Notify `LauncherDiscoveryListener` implementation registered via `LaucherConfig` or on + the `Launcher` of `selectorProcessed` events. + +[[release-notes-5.13.0-M1-junit-platform-new-features-and-improvements]] +==== New Features and Improvements + +* New `ConsoleLauncher` options `--redirect-stdout` and `--redirect-stderr` for + redirecting `stdout` and `stderr` output streams to files. +* Add `TestDescriptor.Visitor.composite(List)` factory method for creating a composite + visitor that delegates to the given visitors in order. +* Introduce test _discovery_ support in `EngineTestKit` to ease testing for discovery + issues produced by a `TestEngine`. Please refer to the + <<../user-guide/index.adoc#testkit-engine, User Guide>> for details. +* This milestone contains preliminary APIs for reporting issues encountered by test + engines during test discovery. However, these are still under active development and not + yet ready for general use. If you're a maintainer of a test engine, please wait for the + next milestone before adopting these new APIs. If you're interested, you can follow + along our progress in link:{junit5-repo}/issues/242[#242]. + + +[[release-notes-5.13.0-M1-junit-jupiter]] +=== JUnit Jupiter + +[[release-notes-5.13.0-M1-junit-jupiter-bug-fixes]] +==== Bug Fixes + +* If the `autoCloseArguments` attribute in `@ParameterizedTest` is set to `true`, all + arguments returned by registered `ArgumentsProvider` implementations are now closed even + if the test method declares fewer parameters. +* `AutoCloseable` arguments returned by an `ArgumentsProvider` are now closed even if they + are wrapped with `Named`. +* `AutoCloseable` arguments returned by an `ArgumentsProvider` are now closed even if a + failure happens prior to invoking the parameterized method. + +[[release-notes-5.13.0-M1-junit-jupiter-new-features-and-improvements]] +==== New Features and Improvements + +* New `@ClassTemplate` annotation and `ClassTemplateInvocationContextProvider` API that + allow declaring a top-level or `@Nested` test class as a template to be invoked multiple + times. This may be used, for example, to inject different parameters to be used by all + tests in the class template or to set up each invocation of the class template + differently. Please refer to the + <<../user-guide/index.adoc#writing-tests-class-templates, User Guide>> for details. +* New `BeforeClassTemplateInvocationCallback` and `AfterClassTemplateInvocationCallback` + extension callback interfaces allow implementing extensions that are invoked before and + after each invocation of a class template. +* New `@ParameterizedClass` support that builds on `@ClassTemplate` and allows declaring a + top-level or `@Nested` test class as a parameterized test class to be invoked multiple + times with different arguments. The same `@...Source` annotations supported with + `@ParameterizedTest` may be used to provide arguments via constructor or field + injection. Please refer to the + <<../user-guide/index.adoc#writing-tests-parameterized-tests, User Guide>> for details. +* New `@ParameterizedClass`-specific + `@BeforeParameterizedClassInvocation`/`@AfterParameterizedClassInvocation` lifecycle + methods that are invoked once before/after each invocation of the parameterized class. +* Provide access to the parameters and resolved arguments of a `@ParameterizedTest` or + `@ParameterizedClass` by storing `ParameterInfo` in the `ExtensionContext.Store` for + retrieval by other extensions. Please refer to the + link:../api/org.junit.jupiter.params/org/junit/jupiter/params/support/ParameterInfo.html[Javadoc] + for details. +* New `@SentenceFragment` annotation which allows one to supply custom text for individual + sentence fragments when using the `IndicativeSentences` `DisplayNameGenerator`. See the + updated documentation in the + <<../user-guide/index.adoc#writing-tests-display-name-generator, User Guide>> for an + example. +* New `TestTemplateInvocationContext.prepareInvocation(ExtensionContext)` callback method + which allows extensions to prepare the `ExtensionContext` before the test template + method is invoked. This may be used, for example, to store entries in the + `ExtensionContext.Store` to benefit from its cleanup support or for retrieval by other + extensions. + + +[[release-notes-5.13.0-M1-junit-vintage]] +=== JUnit Vintage + +No changes. diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc index 2e5b4d7369f1..79f67cc42404 100644 --- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc +++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc @@ -145,8 +145,8 @@ $ java -jar junit-platform-console-standalone-{platform-version}.jar \ --config=junit.platform.reporting.output.dir=reports ---- -Configuration parameters can also be set in a custom properties file supplied as a classpath resource -via the `--config-resource` option: +Configuration parameters can also be set in a custom properties file supplied as a +classpath resource via the `--config-resource` option: [source,console,subs=attributes+] ---- diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/testkit.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/testkit.adoc index 3e74e28b4b7d..3f134f3f8ba1 100644 --- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/testkit.adoc +++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/testkit.adoc @@ -1,3 +1,5 @@ +:testDir: ../../../../../src/test/java + [[testkit]] === JUnit Platform Test Kit @@ -9,16 +11,17 @@ JUnit Platform and then verifying the expected results. As of JUnit Platform [[testkit-engine]] ==== Engine Test Kit -The `{testkit-engine-package}` package provides support for executing a `{TestPlan}` for a -given `{TestEngine}` running on the JUnit Platform and then accessing the results via a -fluent API to verify the expected results. The key entry point into this API is the -`{EngineTestKit}` which provides static factory methods named `engine()` and `execute()`. -It is recommended that you select one of the `engine()` variants to benefit from the -fluent API for building a `LauncherDiscoveryRequest`. +The `{testkit-engine-package}` package provides support for discovering and executing a +`{TestPlan}` for a given `{TestEngine}` running on the JUnit Platform and then accessing +the results via convenient result objects. For execution, a fluent API may be used to +verify the expected execution events were received. The key entry point into this API is +the `{EngineTestKit}` which provides static factory methods named `engine()`, +`discover()`, and `execute()`. It is recommended that you select one of the `engine()` +variants to benefit from the fluent API for building a `LauncherDiscoveryRequest`. NOTE: If you prefer to use the `LauncherDiscoveryRequestBuilder` from the `Launcher` API -to build your `LauncherDiscoveryRequest`, you must use one of the `execute()` variants in -`EngineTestKit`. +to build your `LauncherDiscoveryRequest`, you must use one of the `discover()` or +`execute()` variants in `EngineTestKit`. The following test class written using JUnit Jupiter will be used in subsequent examples. @@ -34,8 +37,24 @@ own `TestEngine` implementation, you need to use its unique engine ID. Alternati may test your own `TestEngine` by supplying an instance of it to the `EngineTestKit.engine(TestEngine)` static factory method. +[[testkit-engine-discovery]] +==== Verifying Test Discovery + +The following test demonstrates how to verify that a `TestPlan` was discovered as expected +by the JUnit Jupiter `TestEngine`. + +[source,java,indent=0] +---- +include::{testDir}/example/testkit/EngineTestKitDiscoveryDemo.java[tags=user_guide] +---- +<1> Select the JUnit Jupiter `TestEngine`. +<2> Select the <> test class. +<3> Discover the `TestPlan`. +<4> Assert engine root descriptor has expected display name. +<5> Assert no discovery issues were encountered. + [[testkit-engine-statistics]] -==== Asserting Statistics +==== Asserting Execution Statistics One of the most common features of the Test Kit is the ability to assert statistics against events fired during the execution of a `TestPlan`. The following tests demonstrate diff --git a/documentation/src/docs/asciidoc/user-guide/appendix.adoc b/documentation/src/docs/asciidoc/user-guide/appendix.adoc index ab63a5a90b3b..9df8622629d5 100644 --- a/documentation/src/docs/asciidoc/user-guide/appendix.adoc +++ b/documentation/src/docs/asciidoc/user-guide/appendix.adoc @@ -105,7 +105,7 @@ Please refer to the corresponding sections for <> in JUnit Jupiter. + Support for <> in JUnit Jupiter. `junit-jupiter-migrationsupport`:: Support for migrating from JUnit 4 to JUnit Jupiter; only required for support for JUnit 4's `@Ignore` annotation and for running selected JUnit 4 rules. diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index 11185fe05019..a89915269d18 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -636,10 +636,14 @@ test execution lifecycle. Consult the following sections for examples and the Ja each of these interfaces in the `{extension-api-package}` package for further details. * `{BeforeAllCallback}` -** `{BeforeEachCallback}` -*** `{BeforeTestExecutionCallback}` -*** `{AfterTestExecutionCallback}` -** `{AfterEachCallback}` +** `{BeforeClassTemplateInvocationCallback}` (only applicable for + <>) +*** `{BeforeEachCallback}` +**** `{BeforeTestExecutionCallback}` +**** `{AfterTestExecutionCallback}` +*** `{AfterEachCallback}` +** `{AfterClassTemplateInvocationCallback}` (only applicable for + <>) * `{AfterAllCallback}` .Implementing Multiple Extension APIs @@ -765,6 +769,49 @@ You may override the `getTestInstantiationExtensionContextScope(...)` method to on the test method level. ==== +[[extensions-class-templates]] +=== Providing Invocation Contexts for Class Templates + +A `{ClassTemplate}` class can only be executed when at least one +`{ClassTemplateInvocationContextProvider}` is registered. Each such provider is +responsible for providing a `Stream` of `{ClassTemplateInvocationContext}` instances. +Each context may specify a custom display name and a list of additional extensions that +will only be used for the next invocation of the `{ClassTemplate}`. + +The following example shows how to write a class template as well as how to register +and implement a `{ClassTemplateInvocationContextProvider}`. + +[source,java,indent=0] +.A class template with accompanying extension +---- +include::{testDir}/example/ClassTemplateDemo.java[tags=user_guide] +---- + +In this example, the class template will be invoked twice, meaning all test methods in +the class template will be executed twice. The display names of the invocations will be +`apple` and `banana` as specified by the invocation context. Each invocation registers a +custom `{TestInstancePostProcessor}` which is used to inject a value into a field. The +output when using the `ConsoleLauncher` is as follows. + +.... +└─ ClassTemplateDemo ✔ + ├─ apple ✔ + │ ├─ notNull() ✔ + │ └─ wellKnown() ✔ + └─ banana ✔ + ├─ notNull() ✔ + └─ wellKnown() ✔ +.... + +The `{ClassTemplateInvocationContextProvider}` extension API is primarily intended for +implementing different kinds of tests that rely on repetitive invocation of _all_ test +methods in a test class albeit in different contexts — for example, with different +parameters, by preparing the test class instance differently, or multiple times without +modifying the context. +Please refer to the implementations of +<> which uses this extension +point to provide its functionality. + [[extensions-test-templates]] === Providing Invocation Contexts for Test Templates @@ -799,8 +846,8 @@ implementing different kinds of tests that rely on repetitive invocation of a te method albeit in different contexts — for example, with different parameters, by preparing the test class instance differently, or multiple times without modifying the context. Please refer to the implementations of <> or -<> which use this extension point to provide their -functionality. +<> which use this extension point +to provide their functionality. [[extensions-keeping-state]] === Keeping State in Extensions @@ -967,81 +1014,50 @@ image::extensions_lifecycle.png[caption='',title='{figure-caption}'] The following table further explains the sixteen steps in the <> diagram. -[cols="5,15,80"] -|=== -| Step | Interface/Annotation | Description - -| 1 -| interface `org.junit.jupiter.api.extension.BeforeAllCallback` -| extension code executed before all tests of the container are executed - -| 2 -| annotation `org.junit.jupiter.api.BeforeAll` -| user code executed before all tests of the container are executed - -| 3 -| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler -#handleBeforeAllMethodExecutionException` -| extension code for handling exceptions thrown from `@BeforeAll` methods - -| 4 -| interface `org.junit.jupiter.api.extension.BeforeEachCallback` -| extension code executed before each test is executed - -| 5 -| annotation `org.junit.jupiter.api.BeforeEach` -| user code executed before each test is executed - -| 6 -| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler -#handleBeforeEachMethodExecutionException` -| extension code for handling exceptions thrown from `@BeforeEach` methods - -| 7 -| interface `org.junit.jupiter.api.extension.BeforeTestExecutionCallback` -| extension code executed immediately before a test is executed - -| 8 -| annotation `org.junit.jupiter.api.Test` -| user code of the actual test method - -| 9 -| interface `org.junit.jupiter.api.extension.TestExecutionExceptionHandler` -| extension code for handling exceptions thrown during a test - -| 10 -| interface `org.junit.jupiter.api.extension.AfterTestExecutionCallback` -| extension code executed immediately after test execution and its corresponding exception handlers - -| 11 -| annotation `org.junit.jupiter.api.AfterEach` -| user code executed after each test is executed - -| 12 -| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler -#handleAfterEachMethodExecutionException` -| extension code for handling exceptions thrown from `@AfterEach` methods - -| 13 -| interface `org.junit.jupiter.api.extension.AfterEachCallback` -| extension code executed after each test is executed - -| 14 -| annotation `org.junit.jupiter.api.AfterAll` -| user code executed after all tests of the container are executed - -| 15 -| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler -#handleAfterAllMethodExecutionException` -| extension code for handling exceptions thrown from `@AfterAll` methods - -| 16 -| interface `org.junit.jupiter.api.extension.AfterAllCallback` -| extension code executed after all tests of the container are executed - -|=== - -In the simplest case only the actual test method will be executed (step 8); all other +. *interface* `*org.junit.jupiter.api.extension.BeforeAllCallback*` + +extension code executed before all tests of the container are executed +. *annotation* `*org.junit.jupiter.api.BeforeAll*` + +user code executed before all tests of the container are executed +. *interface* `*org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleBeforeAllMethodExecutionException*` + +extension code for handling exceptions thrown from `@BeforeAll` methods +. *interface* `*org.junit.jupiter.api.extension.BeforeClassTemplateInvocationCallback*` + +extension code executed before each class template invocation is executed (only applicable +if the test class is a <>) +. *interface* `*org.junit.jupiter.api.extension.BeforeEachCallback*` + +extension code executed before each test is executed +. *annotation* `*org.junit.jupiter.api.BeforeEach*` + +user code executed before each test is executed +. *interface* `*org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleBeforeEachMethodExecutionException*` + +extension code for handling exceptions thrown from `@BeforeEach` methods +. *interface* `*org.junit.jupiter.api.extension.BeforeTestExecutionCallback*` + +extension code executed immediately before a test is executed +. *annotation* `*org.junit.jupiter.api.Test*` + +user code of the actual test method +. *interface* `*org.junit.jupiter.api.extension.TestExecutionExceptionHandler*` + +extension code for handling exceptions thrown during a test +. *interface* `*org.junit.jupiter.api.extension.AfterTestExecutionCallback*` + +extension code executed immediately after test execution and its corresponding exception handlers +. *annotation* `*org.junit.jupiter.api.AfterEach*` + +user code executed after each test is executed +. *interface* `*org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleAfterEachMethodExecutionException*` + +extension code for handling exceptions thrown from `@AfterEach` methods +. *interface* `*org.junit.jupiter.api.extension.AfterEachCallback*` + +extension code executed after each test is executed +. *interface* `*org.junit.jupiter.api.extension.AfterClassTemplateInvocationCallback*` + +extension code executed after each class template invocation is executed (only applicable +if the test class is a <>) +. *annotation* `*org.junit.jupiter.api.AfterAll*` + +user code executed after all tests of the container are executed +. *interface* `*org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleAfterAllMethodExecutionException*` + +extension code for handling exceptions thrown from `@AfterAll` methods +. *interface* `*org.junit.jupiter.api.extension.AfterAllCallback*` + +extension code executed after all tests of the container are executed + +In the simplest case only the actual test method will be executed (step 9); all other steps are optional depending on the presence of user code or extension support for the corresponding lifecycle callback. For further details on the various lifecycle callbacks please consult the respective Javadoc for each annotation and extension. @@ -1054,6 +1070,7 @@ by implementing <> JUnit Jupiter always guarantees _wrapping_ behavior for multiple registered extensions that implement lifecycle callbacks such as `BeforeAllCallback`, `AfterAllCallback`, +`BeforeClassTemplateInvocationCallback`, `AfterClassTemplateInvocationCallback`, `BeforeEachCallback`, `AfterEachCallback`, `BeforeTestExecutionCallback`, and `AfterTestExecutionCallback`. diff --git a/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png b/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png index bf8671b33f14..50d4f74e38c5 100644 Binary files a/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png and b/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png differ diff --git a/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle_source.docx b/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle_source.docx index ed2d03f158c3..bc453b4e44e2 100644 Binary files a/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle_source.docx and b/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle_source.docx differ diff --git a/documentation/src/docs/asciidoc/user-guide/migration-from-junit4.adoc b/documentation/src/docs/asciidoc/user-guide/migration-from-junit4.adoc index 29290532756a..da24b1665ada 100644 --- a/documentation/src/docs/asciidoc/user-guide/migration-from-junit4.adoc +++ b/documentation/src/docs/asciidoc/user-guide/migration-from-junit4.adoc @@ -1,3 +1,5 @@ +:testDir: ../../../../src/test/java + [[migrating-from-junit4]] == Migrating from JUnit 4 @@ -58,7 +60,87 @@ concurrent test execution. It can be enabled and configured using the following Specifies the size of the thread pool to be used for parallel execution. By default, the number of available processors is used. -Example configuration in `junit-platform.properties`: +[[migrating-from-junit4-parallel-execution-class-level]] +==== Parallelization at Class Level + +Let's assume we have two test classes `FooTest` and `BarTest` with each class containing +three unit tests. Now, let's enable parallel execution of test classes: + +[source,properties] +---- +junit.vintage.execution.parallel.enabled=true +junit.vintage.execution.parallel.classes=true +---- + +With this setup, the `VintageTestEngine` will use two different threads, +one for each test class: + +[source,plaintext] +---- +ForkJoinPool-1-worker-1 - BarTest::test1 +ForkJoinPool-1-worker-2 - FooTest::test1 +ForkJoinPool-1-worker-1 - BarTest::test2 +ForkJoinPool-1-worker-2 - FooTest::test2 +ForkJoinPool-1-worker-1 - BarTest::test3 +ForkJoinPool-1-worker-2 - FooTest::test3 +---- + +[[migrating-from-junit4-parallel-execution-method-level]] +==== Parallelization at Method Level + +Alternatively, we can enable parallel test execution at a method level, +rather than the class level: + +[source,properties] +---- +junit.vintage.execution.parallel.enabled=true +junit.vintage.execution.parallel.methods=true +---- + +Therefore, the test methods within each class will be executed in parallel, while +different test classes will be executed sequentially: + +[source,plaintext] +---- +ForkJoinPool-1-worker-1 - BarTest::test1 +ForkJoinPool-1-worker-2 - BarTest::test2 +ForkJoinPool-1-worker-3 - BarTest::test3 + +ForkJoinPool-1-worker-3 - FooTest::test1 +ForkJoinPool-1-worker-2 - FooTest::test2 +ForkJoinPool-1-worker-1 - FooTest::test3 +---- + +[[migrating-from-junit4-parallel-execution-class-and-method-level]] +==== Full Parallelization + +Finally, we can also enable parallelization at both class and method level: + +[source,properties] +---- +junit.vintage.execution.parallel.enabled=true +junit.vintage.execution.parallel.classes=true +junit.vintage.execution.parallel.methods=true +---- + +With these properties set, the `VintageTestEngine` will execute all tests classes and +methods in parallel, potentially significantly reducing the overall test suite execution time: + +[source,plaintext] +---- +ForkJoinPool-1-worker-6 - FooTest::test2 +ForkJoinPool-1-worker-7 - BarTest::test3 +ForkJoinPool-1-worker-3 - FooTest::test1 +ForkJoinPool-1-worker-8 - FooTest::test3 +ForkJoinPool-1-worker-5 - BarTest::test2 +ForkJoinPool-1-worker-4 - BarTest::test1 +---- + +[[migrating-from-junit4-parallel-execution-pool-size]] +==== Configuring the Pool Size + +The default thread pool size is equal to the number of available processors. However, we +can also configure the pool size explicitly: [source,properties] ---- @@ -68,8 +150,39 @@ junit.vintage.execution.parallel.methods=true junit.vintage.execution.parallel.pool-size=4 ---- -With these properties set, the `VintageTestEngine` will execute tests in parallel, -potentially significantly reducing the overall test suite execution time. +For instance, if we update our previous example that uses full parallelization and +configure the pool size to four, we can expect to see our six test methods executed with +a parallelism of four: + +[source,plaintext] +---- +ForkJoinPool-1-worker-2 - FooTest::test1 +ForkJoinPool-1-worker-4 - BarTest::test2 +ForkJoinPool-1-worker-3 - BarTest::test1 +ForkJoinPool-1-worker-4 - BarTest::test3 +ForkJoinPool-1-worker-2 - FooTest::test2 +ForkJoinPool-1-worker-3 - FooTest::test3 +---- + +As we can see, even though we set the thread pool size was four, only three threads were +used in this case. This happens because the pool adjusts the number of active threads +based on workload and system needs. + +[[migrating-from-junit4-parallel-execution-disabled]] +==== Sequential Execution + +On the other hand, if we disable parallel execution, the `VintageTestEngine` +will execute all tests sequentially, regardless of the other properties: + +[source,properties] +---- +junit.vintage.execution.parallel.enabled=false +junit.vintage.execution.parallel.classes=true +junit.vintage.execution.parallel.methods=true +---- + +Similarly, tests will be executed sequentially if you enable parallel execution in general +but enable neither class-level nor method-level parallelization. [[migrating-from-junit4-tips]] === Migration Tips @@ -94,6 +207,8 @@ tests to JUnit Jupiter. - See also <>. * `@Category` no longer exists; use `@Tag` instead. * `@RunWith` no longer exists; superseded by `@ExtendWith`. + - For `@RunWith(Enclosed.class)` use `@Nested`. + - For `@RunWith(Parameterized.class)` see <>. * `@Rule` and `@ClassRule` no longer exist; superseded by `@ExtendWith` and `@RegisterExtension`. - See also <>. @@ -105,6 +220,38 @@ tests to JUnit Jupiter. argument instead of the first one. - See <> for details. +[[migrating-from-junit4-tips-parameterized]] +==== Parameterized test classes + +Unless `@UseParametersRunnerFactory` is used, a JUnit 4 parameterized test class can be +converted into a JUnit Jupiter +<> by following these steps: + +. Replace `@RunWith(Parameterized.class)` with `@ParameterizedClass`. +. Add a class-level `@MethodSource("methodName")` annotation where `methodName` is the + name of the method annotated with `@Parameters` and remove the `@Parameters` annotation + from the method. +. Replace `@BeforeParam` and `@AfterParam` with `@BeforeParameterizedClassInvocation` and + `@AfterParameterizedClassInvocation`, respectively, if there are any methods with such + annotations. +. Change the imports of the `@Test` and `@Parameter` annotations to use the + `org.junit.jupiter.params` package. +. Change assertions etc. to use the `org.junit.jupiter.api` package as usual. +. Optionally, remove all `public` modifiers from the class and its methods and fields. + +==== +[source,java,indent=0] +.Before +---- +include::{testDir}/example/ParameterizedMigrationDemo.java[tags=before] +---- + +[source,java,indent=0] +.After +---- +include::{testDir}/example/ParameterizedMigrationDemo.java[tags=after] +---- +==== [[migrating-from-junit4-rule-support]] === Limited JUnit 4 Rule Support diff --git a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc index dfd49d779a28..a8c4bc0824fb 100644 --- a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc @@ -13,11 +13,6 @@ however, that it is recommended to use IDEA 2017.3 or newer since more recent ve IDEA download the following JARs automatically based on the API version used in the project: `junit-platform-launcher`, `junit-jupiter-engine`, and `junit-vintage-engine`. -WARNING: IntelliJ IDEA releases prior to IDEA 2017.3 bundle specific versions of JUnit 5. -Thus, if you want to use a newer version of JUnit Jupiter, execution of tests within the -IDE might fail due to version conflicts. In such cases, please follow the instructions -below to use a newer version of JUnit 5 than the one bundled with IntelliJ IDEA. - In order to use a different JUnit 5 version (e.g., {jupiter-version}), you may need to include the corresponding versions of the `junit-platform-launcher`, `junit-jupiter-engine`, and `junit-vintage-engine` JARs in the classpath. @@ -27,9 +22,7 @@ include the corresponding versions of the `junit-platform-launcher`, [subs=attributes+] ---- testImplementation(platform("org.junit:junit-bom:{bom-version}")) -testRuntimeOnly("org.junit.platform:junit-platform-launcher") { - because("Only needed to run tests in a version of IntelliJ IDEA that bundles older versions") -} +testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.vintage:junit-vintage-engine") ---- @@ -40,7 +33,6 @@ testRuntimeOnly("org.junit.vintage:junit-vintage-engine") ---- - org.junit.platform junit-platform-launcher @@ -150,48 +142,63 @@ test { ---- Please refer to the -https://docs.gradle.org/current/userguide/java_plugin.html#sec:java_test[official Gradle documentation] +https://docs.gradle.org/current/userguide/java_testing.html[official Gradle documentation] for a comprehensive list of options. [[running-tests-build-gradle-bom]] ===== Aligning dependency versions +TIP: See <> for details on how to override the version +of JUnit used in your Spring Boot application. + Unless you're using Spring Boot which defines its own way of managing dependencies, it is -recommended to use the JUnit Platform BOM to align the versions of all JUnit 5 artifacts. +recommended to use the JUnit Platform <> to align the +versions of all JUnit 5 artifacts. [source,groovy,indent=0] [subs=attributes+] +.Explicit platform dependency on the BOM ---- dependencies { testImplementation(platform("org.junit:junit-bom:{bom-version}")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } ---- Using the BOM allows you to omit the version when declaring dependencies on all artifacts with the `org.junit.platform`, `org.junit.jupiter`, and `org.junit.vintage` group IDs. -TIP: See <> for details on how to override the version -of JUnit used in your Spring Boot application. - -[[running-tests-build-gradle-config-params]] -===== Configuration Parameters - -The standard Gradle `test` task currently does not provide a dedicated DSL to set JUnit -Platform <> to influence test -discovery and execution. However, you can provide configuration parameters within the -build script via system properties (as shown below) or via the -`junit-platform.properties` file. +Since all JUnit artifacts declare a +https://docs.gradle.org/current/userguide/platforms.html[platform] dependency on the BOM, +you usually don't need to declare an explicit dependency on it yourself. Instead, it's +sufficient to declare _one_ regular dependency that includes a version number. Gradle will +then pull in the BOM automatically so you can omit the version for all other JUnit 5 +artifacts. [source,groovy,indent=0] +[subs=attributes+] +.Implicit platform dependency on the BOM ---- -test { - // ... - systemProperty("junit.jupiter.conditions.deactivate", "*") - systemProperty("junit.jupiter.extensions.autodetection.enabled", true) - systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_class") - // ... +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter:{jupiter-version}") // <1> + testRuntimeOnly("org.junit.platform:junit-platform-launcher") // <2> } ---- +<1> Dependency declaration with explicit version. Pulls in the `junit-bom` automatically. +<2> Dependency declaration without version. The version is supplied by the `junit-bom`. + +[WARNING] +.Declaring a dependency on junit-platform-launcher +==== +Even though pre-8.0 versions of Gradle don't require declaring an explicit +dependency on `junit-platform-launcher`, it is recommended to do so to ensure the versions +of JUnit artifacts on the test runtime classpath are aligned. + +Moreover, doing so is recommended and in some cases even required when importing the +project into an IDE like <> or +<>. +==== [[running-tests-build-gradle-engines-configure]] ===== Configuring Test Engines @@ -205,7 +212,38 @@ on the dependency-aggregating JUnit Jupiter artifact similar to the following. [subs=attributes+] ---- dependencies { - testImplementation("org.junit.jupiter:junit-jupiter:{jupiter-version}") // version can be omitted when using the BOM + testImplementation("org.junit.jupiter:junit-jupiter:{jupiter-version}") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} +---- + +Alternatively, you can use Gradle's +https://docs.gradle.org/current/userguide/jvm_test_suite_plugin.html[JVM Test Suite] +support. + +[source,kotlin,indent=0] +[subs=attributes+] +.Kotlin DSL +---- +testing { + suites { + named("test") { + useJUnitJupiter("{jupiter-version}") + } + } +} +---- + +[source,groovy,indent=0] +[subs=attributes+] +.Groovy DSL +---- +testing { + suites { + test { + useJUnitJupiter("{jupiter-version}") + } + } } ---- @@ -218,7 +256,28 @@ implementation similar to the following. ---- dependencies { testImplementation("junit:junit:{junit4-version}") - testRuntimeOnly("org.junit.vintage:junit-vintage-engine:{vintage-version}") // version can be omitted when using the BOM + testRuntimeOnly("org.junit.vintage:junit-vintage-engine:{vintage-version}") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} +---- + +[[running-tests-build-gradle-config-params]] +===== Configuration Parameters + +The standard Gradle `test` task currently does not provide a dedicated DSL to set JUnit +Platform <> to influence test +discovery and execution. However, you can provide configuration parameters within the +build script via system properties (as shown below) or via the +`junit-platform.properties` file. + +[source,groovy,indent=0] +---- +test { + // ... + systemProperty("junit.jupiter.conditions.deactivate", "*") + systemProperty("junit.jupiter.extensions.autodetection.enabled", true) + systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_class") + // ... } ---- @@ -248,8 +307,8 @@ test { Other logging frameworks provide different means to redirect messages logged using `java.util.logging`. For example, for {Logback} you can use the -https://www.slf4j.org/legacy.html#jul-to-slf4j[JUL to SLF4J Bridge] by adding an -additional dependency to the runtime classpath. +https://www.slf4j.org/legacy.html#jul-to-slf4j[JUL to SLF4J Bridge] by adding it as a +dependency to the test runtime classpath. [[running-tests-build-maven]] ==== Maven @@ -288,7 +347,8 @@ Maven build as follows. ===== Aligning dependency versions Unless you're using Spring Boot which defines its own way of managing dependencies, it is -recommended to use the JUnit Platform BOM to align the versions of all JUnit 5 artifacts. +recommended to use the JUnit Platform <> to align the +versions of all JUnit 5 artifacts. [source,xml,indent=0] [subs=attributes+] @@ -580,8 +640,8 @@ managing the version of JUnit used in your project. In addition, the Jupiter, AssertJ, Mockito, etc. If your build relies on dependency management support from Spring Boot, you should not -import the <> in your build script since that -will result in duplicate (and potentially conflicting) management of JUnit dependencies. +import JUnit's <> in your build script since that would +result in duplicate (and potentially conflicting) management of JUnit dependencies. If you need to override the version of a dependency used in your Spring Boot application, you have to override the exact name of the @@ -752,8 +812,29 @@ You can pass a real parameter with an initial `@` character by escaping it with additional `@` symbol. For example, `@@somearg` will become `@somearg` and will not be subject to expansion. +[[running-tests-console-launcher-redirecting-stdout-and-stderr]] +==== Redirecting Standard Output/Error to Files + +You can redirect the `System.out` (stdout) and `System.err` (stderr) output streams to +files using the `--redirect-stdout` and `--redirect-stderr` options: + +[source,console,subs=attributes+] +---- +$ java -jar junit-platform-console-standalone-{platform-version}.jar \ + --redirect-stdout=stdout.txt \ + --redirect-stderr=stderr.txt +---- + +[NOTE] +==== +If the `--redirect-stdout` and `--redirect-stderr` arguments point to the same file, both +output streams will be redirected to that file. + +The default charset is used for writing to the files. +==== + [[running-tests-console-launcher-color-customization]] -==== Color customization +==== Color Customization The colors used in the output of the `{ConsoleLauncher}` can be customized. The option `--single-color` will apply a built-in monochrome style, while diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 8442461ae2e1..850c65b9d627 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -1,4 +1,5 @@ :testDir: ../../../../src/test/java +:testResourcesDir: ../../../../src/test/resources :testRelease21Dir: ../../../../src/test/java21 :kotlinTestDir: ../../../../src/test/kotlin @@ -24,33 +25,125 @@ framework. Unless otherwise stated, all core annotations are located in the `{api-package}` package in the `junit-jupiter-api` module. -[cols="20,80"] -|=== -| Annotation | Description - -| `@Test` | Denotes that a method is a test method. Unlike JUnit 4's `@Test` annotation, this annotation does not declare any attributes, since test extensions in JUnit Jupiter operate based on their own dedicated annotations. Such methods are inherited unless they are overridden. -| `@ParameterizedTest` | Denotes that a method is a <>. Such methods are inherited unless they are overridden. -| `@RepeatedTest` | Denotes that a method is a test template for a <>. Such methods are inherited unless they are overridden. -| `@TestFactory` | Denotes that a method is a test factory for <>. Such methods are inherited unless they are overridden. -| `@TestTemplate` | Denotes that a method is a <> designed to be invoked multiple times depending on the number of invocation contexts returned by the registered <>. Such methods are inherited unless they are overridden. -| `@TestClassOrder` | Used to configure the <> for `@Nested` test classes in the annotated test class. Such annotations are inherited. -| `@TestMethodOrder` | Used to configure the <> for the annotated test class; similar to JUnit 4's `@FixMethodOrder`. Such annotations are inherited. -| `@TestInstance` | Used to configure the <> for the annotated test class. Such annotations are inherited. -| `@DisplayName` | Declares a custom <> for the test class or test method. Such annotations are not inherited. -| `@DisplayNameGeneration` | Declares a custom <> for the test class. Such annotations are inherited. -| `@BeforeEach` | Denotes that the annotated method should be executed _before_ *each* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, or `@TestFactory` method in the current class; analogous to JUnit 4's `@Before`. Such methods are inherited unless they are overridden. -| `@AfterEach` | Denotes that the annotated method should be executed _after_ *each* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, or `@TestFactory` method in the current class; analogous to JUnit 4's `@After`. Such methods are inherited unless they are overridden. -| `@BeforeAll` | Denotes that the annotated method should be executed _before_ *all* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4's `@BeforeClass`. Such methods are inherited unless they are overridden and must be `static` unless the "per-class" <> is used. -| `@AfterAll` | Denotes that the annotated method should be executed _after_ *all* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4's `@AfterClass`. Such methods are inherited unless they are overridden and must be `static` unless the "per-class" <> is used. -| `@Nested` | Denotes that the annotated class is a non-static <>. On Java 8 through Java 15, `@BeforeAll` and `@AfterAll` methods cannot be used directly in a `@Nested` test class unless the "per-class" <> is used. Beginning with Java 16, `@BeforeAll` and `@AfterAll` methods can be declared as `static` in a `@Nested` test class with either test instance lifecycle mode. Such annotations are not inherited. -| `@Tag` | Used to declare <>, either at the class or method level; analogous to test groups in TestNG or Categories in JUnit 4. Such annotations are inherited at the class level but not at the method level. -| `@Disabled` | Used to <> a test class or test method; analogous to JUnit 4's `@Ignore`. Such annotations are not inherited. -| `@AutoClose` | Denotes that the annotated field represents a resource that will be <> after test execution. -| `@Timeout` | Used to fail a test, test factory, test template, or lifecycle method if its execution exceeds a given duration. Such annotations are inherited. -| `@TempDir` | Used to supply a <> via field injection or parameter injection in a test class constructor, lifecycle method, or test method; located in the `org.junit.jupiter.api.io` package. Such fields are inherited. -| `@ExtendWith` | Used to <>. Such annotations are inherited. -| `@RegisterExtension` | Used to <> via fields. Such fields are inherited. -|=== +`*@Test*`:: Denotes that a method is a test method. Unlike JUnit 4's `@Test` annotation, +this annotation does not declare any attributes, since test extensions in JUnit Jupiter +operate based on their own dedicated annotations. Such methods are inherited unless they +are overridden. + +`*@ParameterizedTest*`:: Denotes that a method is a +<>. Such methods are inherited +unless they are overridden. + +`*@RepeatedTest*`:: Denotes that a method is a test template for a +<>. Such methods are inherited unless they +are overridden. + +`*@TestFactory*`:: Denotes that a method is a test factory for +<>. Such methods are inherited unless they are +overridden. + +`*@TestTemplate*`:: Denotes that a method is a +<> designed to be invoked multiple +times depending on the number of invocation contexts returned by the registered +<>. Such methods are inherited unless they are +overridden. + +`*@TestClassOrder*`:: Used to configure the +<> for `@Nested` +test classes in the annotated test class. Such annotations are inherited. + +`*@TestMethodOrder*`:: Used to configure the +<> for the +annotated test class; similar to JUnit 4's `@FixMethodOrder`. Such annotations are +inherited. + +`*@TestInstance*`:: Used to configure the +<> for the annotated test +class. Such annotations are inherited. + +`*@DisplayName*`:: Declares a custom <> for the +test class or test method. Such annotations are not inherited. + +`*@DisplayNameGeneration*`:: Declares a custom +<> for the test class. Such +annotations are inherited. + +`*@BeforeEach*`:: Denotes that the annotated method should be executed _before_ *each* +`@Test`, `@RepeatedTest`, `@ParameterizedTest`, or `@TestFactory` method in the current +class; analogous to JUnit 4's `@Before`. Such methods are inherited unless they are +overridden. + +`*@AfterEach*`:: Denotes that the annotated method should be executed _after_ *each* +`@Test`, `@RepeatedTest`, `@ParameterizedTest`, or `@TestFactory` method in the current +class; analogous to JUnit 4's `@After`. Such methods are inherited unless they are +overridden. + +`*@BeforeAll*`:: Denotes that the annotated method should be executed _before_ *all* +`@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current +class; analogous to JUnit 4's `@BeforeClass`. Such methods are inherited unless they are +overridden and must be `static` unless the "per-class" +<> is used. + +`*@AfterAll*`:: Denotes that the annotated method should be executed _after_ *all* +`@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current +class; analogous to JUnit 4's `@AfterClass`. Such methods are inherited unless they are +overridden and must be `static` unless the "per-class" +<> is used. + +`*@ParameterizedClass*`:: Denotes that the annotated class is a +<>. Such annotations are +inherited. + +`*@BeforeParameterizedClassInvocation*`:: Denotes that the annotated method should be +executed once _before_ each invocation of a +<>. Such methods are inherited +unless they are overridden. + +`*@AfterParameterizedClassInvocation*`:: Denotes that the annotated method should be +executed once _after_ each invocation of a +<>. Such methods are inherited +unless they are overridden. + +`*@ClassTemplate*`:: Denotes that the annotated class is a +<> designed to be executed +multiple times depending on the number of invocation contexts returned by the registered +<>. Such annotations are inherited. + +`*@Nested*`:: Denotes that the annotated class is a non-static +<>. On Java 8 through Java 15, `@BeforeAll` and +`@AfterAll` methods cannot be used directly in a `@Nested` test class unless the +"per-class" <> is used. +Beginning with Java 16, `@BeforeAll` and `@AfterAll` methods can be declared as `static` +in a `@Nested` test class with either test instance lifecycle mode. Such annotations are +not inherited. + +`*@Tag*`:: Used to declare +<>, either at the class or +method level; analogous to test groups in TestNG or Categories in JUnit 4. Such +annotations are inherited at the class level but not at the method level. + +`*@Disabled*`:: Used to <> a test class or test method; +analogous to JUnit 4's `@Ignore`. Such annotations are not inherited. + +`*@AutoClose*`:: Denotes that the annotated field represents a resource that will be +<> after test +execution. Such fields are inherited. + +`*@Timeout*`:: Used to fail a test, test factory, test template, or lifecycle method if +its execution exceeds a given duration. Such annotations are inherited. + +`*@TempDir*`:: Used to supply a +<> via field +injection or parameter injection in a test class constructor, lifecycle method, or test +method; located in the `org.junit.jupiter.api.io` package. Such fields are inherited. + +`*@ExtendWith*`:: Used to +<>. Such +annotations are inherited. + +`*@RegisterExtension*`:: Used to +<> via fields. +Such fields are inherited. WARNING: Some annotations may currently be _experimental_. Consult the table in <> for details. @@ -208,45 +301,86 @@ include::{testDir}/example/DisplayNameDemo.java[tags=user_guide] ==== Display Name Generators JUnit Jupiter supports custom display name generators that can be configured via the -`@DisplayNameGeneration` annotation. Values provided via `@DisplayName` annotations -always take precedence over display names generated by a `DisplayNameGenerator`. +`@DisplayNameGeneration` annotation. -Generators can be created by implementing `DisplayNameGenerator`. Here are some default -ones available in Jupiter: +Generators can be created by implementing the `DisplayNameGenerator` API. The following +table lists the default display name generators available in Jupiter. [cols="20,80"] |=== | DisplayNameGenerator | Behavior -| `Standard` | Matches the standard display name generation behavior in place since JUnit Jupiter 5.0 was released. -| `Simple` | Removes trailing parentheses for methods with no parameters. -| `ReplaceUnderscores` | Replaces underscores with spaces. -| `IndicativeSentences` | Generates complete sentences by concatenating the names of the test and the enclosing classes. +| `Standard` | Matches the standard display name generation behavior in place since JUnit Jupiter 5.0 was released. +| `Simple` | Extends the functionality of `Standard` by removing trailing parentheses for methods with no parameters. +| `ReplaceUnderscores` | Replaces underscores with spaces. +| `IndicativeSentences` | Generates complete sentences by concatenating the names of the test and the enclosing classes. |=== -Note that for `IndicativeSentences`, you can customize the separator and the -underlying generator by using `@IndicativeSentencesGeneration` as shown in the +NOTE: Values provided via `@DisplayName` annotations always take precedence over display +names generated by a `DisplayNameGenerator`. + +====== +The following example demonstrates the use of the `ReplaceUnderscores` display name +generator. + +[source,java,indent=0] +---- +include::{testDir}/example/DisplayNameGeneratorDemo.java[tags=user_guide_replace_underscores] +---- + +Running the above test class results in the following display names. + +``` +A year is not supported ✔ +├─ if it is zero ✔ +└─ A negative value for year is not supported by the leap year computation. ✔ + ├─ For example, year -1 is not supported. ✔ + └─ For example, year -4 is not supported. ✔ +``` +====== + +====== +With the `IndicativeSentences` display name generator, you can customize the separator and +the underlying generator by using `@IndicativeSentencesGeneration` as shown in the following example. [source,java,indent=0] ---- -include::{testDir}/example/DisplayNameGeneratorDemo.java[tags=user_guide] +include::{testDir}/example/DisplayNameGeneratorDemo.java[tags=user_guide_indicative_sentences] +---- + +Running the above test class results in the following display names. + +``` +A year is a leap year ✔ +├─ A year is a leap year -> if it is divisible by 4 but not by 100 ✔ +└─ A year is a leap year -> if it is one of the following years ✔ + ├─ Year 2016 is a leap year. ✔ + ├─ Year 2020 is a leap year. ✔ + └─ Year 2048 is a leap year. ✔ +``` +====== + +====== +With `IndicativeSentences`, you can optionally specify custom sentence fragments via the +`@SentenceFragment` annotation as demonstrated in the following example. + +[source,java,indent=0] +---- +include::{testDir}/example/DisplayNameGeneratorDemo.java[tags=user_guide_custom_sentence_fragments] ---- +Running the above test class results in the following display names. + ``` -+-- DisplayNameGeneratorDemo [OK] - +-- A year is not supported [OK] - | +-- A negative value for year is not supported by the leap year computation. [OK] - | | +-- For example, year -1 is not supported. [OK] - | | '-- For example, year -4 is not supported. [OK] - | '-- if it is zero() [OK] - '-- A year is a leap year [OK] - +-- A year is a leap year -> if it is divisible by 4 but not by 100. [OK] - '-- A year is a leap year -> if it is one of the following years. [OK] - +-- Year 2016 is a leap year. [OK] - +-- Year 2020 is a leap year. [OK] - '-- Year 2048 is a leap year. [OK] +A year is a leap year ✔ +├─ A year is a leap year, if it is divisible by 4 but not by 100 ✔ +└─ A year is a leap year, if it is one of the following years ✔ + ├─ 2016 ✔ + ├─ 2020 ✔ + └─ 2048 ✔ ``` +====== [[writing-tests-display-name-generator-default]] @@ -1051,6 +1185,47 @@ class with `@TestInstance(Lifecycle.PER_CLASS)` (see `@BeforeAll` and `@AfterAll` methods can be declared as `static` in `@Nested` test classes, and this restriction no longer applies. +[[writing-tests-nested-interoperability]] +==== Interoperability + +`@Nested` may be combined with +<> in which case the nested test +class is parameterized. + +The following example illustrates how to combine `@Nested` with `@ParameterizedClass` and +`@ParameterizedTest`. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedClassDemo.java[tags=nested] +---- + +Executing the above test class yields the following output: + +.... +FruitTests ✔ +├─ [1] fruit=apple ✔ +│ └─ QuantityTests ✔ +│ ├─ [1] quantity=23 ✔ +│ │ └─ test(Duration) ✔ +│ │ ├─ [1] duration=PT1H ✔ +│ │ └─ [2] duration=PT2H ✔ +│ └─ [2] quantity=42 ✔ +│ └─ test(Duration) ✔ +│ ├─ [1] duration=PT1H ✔ +│ └─ [2] duration=PT2H ✔ +└─ [2] fruit=banana ✔ + └─ QuantityTests ✔ + ├─ [1] quantity=23 ✔ + │ └─ test(Duration) ✔ + │ ├─ [1] duration=PT1H ✔ + │ └─ [2] duration=PT2H ✔ + └─ [2] quantity=42 ✔ + └─ test(Duration) ✔ + ├─ [1] duration=PT1H ✔ + └─ [2] duration=PT2H ✔ +.... + [[writing-tests-dependency-injection]] === Dependency Injection for Constructors and Methods @@ -1402,13 +1577,26 @@ When using the `ConsoleLauncher` with the unicode theme enabled, execution of [[writing-tests-parameterized-tests]] -=== Parameterized Tests +=== Parameterized Classes and Tests -Parameterized tests make it possible to run a test multiple times with different +_Parameterized tests_ make it possible to run a test method multiple times with different arguments. They are declared just like regular `@Test` methods but use the -`{ParameterizedTest}` annotation instead. In addition, you must declare at least one -_source_ that will provide the arguments for each invocation and then _consume_ the -arguments in the test method. +`{ParameterizedTest}` annotation instead. + +_Parameterized classes_ make it possible to run _all_ tests in test class, including +<>, multiple times with different arguments. They are declared just +like regular test classes and may contain any supported test method type (including +`@ParameterizedTest`) but annotated with the `{ParameterizedClass}` annotation. + +WARNING: _Parameterized classes_ are currently an _experimental_ feature. You're invited +to give it a try and provide feedback to the JUnit team so they can improve and eventually +<> this feature. + +Regardless of whether you are parameterizing a test method or a test class, you must +declare at least one <> that will +provide the arguments for each invocation and then +<> the arguments in the +parameterized method or class, respectively. The following example demonstrates a parameterized test that uses the `@ValueSource` annotation to specify a `String` array as the source of arguments. @@ -1429,18 +1617,46 @@ palindromes(String) ✔ └─ [3] candidate=able was I ere I saw elba ✔ .... +The same `@ValueSource` annotation can be used to specify the source of arguments for a +`@ParameterizedClass`. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedClassDemo.java[tags=first_example] +---- + +When executing the above parameterized test class, each invocation will be reported +separately. For instance, the `ConsoleLauncher` will print output similar to the +following. + +.... +PalindromeTests ✔ +├─ [1] candidate=racecar ✔ +│ ├─ palindrome() ✔ +│ └─ reversePalindrome() ✔ +├─ [2] candidate=radar ✔ +│ ├─ palindrome() ✔ +│ └─ reversePalindrome() ✔ +└─ [3] candidate=able was I ere I saw elba ✔ + ├─ palindrome() ✔ + └─ reversePalindrome() ✔ +.... + [[writing-tests-parameterized-tests-setup]] ==== Required Setup -In order to use parameterized tests you need to add a dependency on the +In order to use parameterized classes or tests you need to add a dependency on the `junit-jupiter-params` artifact. Please refer to <> for details. [[writing-tests-parameterized-tests-consuming-arguments]] ==== Consuming Arguments -Parameterized test methods typically _consume_ arguments directly from the configured -source (see <>) following a one-to-one -correlation between argument source index and method parameter index (see examples in +[[writing-tests-parameterized-tests-consuming-arguments-methods]] +===== Parameterized Tests + +Parameterized test methods _consume_ arguments directly from the configured source (see +<>) following a one-to-one correlation between +argument source index and method parameter index (see examples in <>). However, a parameterized test method may also choose to _aggregate_ arguments from the source into a single object passed to the method (see <>). @@ -1448,31 +1664,120 @@ Additional arguments may also be provided by a `ParameterResolver` (e.g., to obt instance of `TestInfo`, `TestReporter`, etc.). Specifically, a parameterized test method must declare formal parameters according to the following rules. -* Zero or more _indexed arguments_ must be declared first. +* Zero or more _indexed parameters_ must be declared first. * Zero or more _aggregators_ must be declared next. * Zero or more arguments supplied by a `ParameterResolver` must be declared last. -In this context, an _indexed argument_ is an argument for a given index in the -`Arguments` provided by an `ArgumentsProvider` that is passed as an argument to the +In this context, an _indexed parameter_ is an argument for a given index in the +`{Arguments}` provided by an `{ArgumentsProvider}` that is passed as an argument to the parameterized method at the same index in the method's formal parameter list. An -_aggregator_ is any parameter of type `ArgumentsAccessor` or any parameter annotated with -`@AggregateWith`. +_aggregator_ is any parameter of type `{ArgumentsAccessor}` or any parameter annotated +with `{AggregateWith}`. + +[[writing-tests-parameterized-tests-consuming-arguments-classes]] +===== Parameterized Classes + +Parameterized classes _consume_ arguments directly from the configured source (see +<>); either via their unique constructor or via +field injection. If a `{Parameter}`-annotated field is declared in the parameterized class +or one of its superclasses, field injection will be used. Otherwise, constructor injection +will be used. + +[[writing-tests-parameterized-tests-consuming-arguments-constructor-injection]] +====== Constructor Injection + +WARNING: Constructor injection can only be used with the (default) `PER_METHOD` +<> mode. Please use +<> +with the `PER_CLASS` mode instead. + +For constructor injection, the same rules apply as defined for +<> +above. In the following example, two arguments are injected into the constructor of the +test class. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedClassDemo.java[tags=constructor_injection] +---- + +If your programming language level you are using supports _records_ -- for example, Java +16 or higher -- you may use them to implement parameterized classes that avoid the +boilerplate code of declaring a test class constructor. + +[source,java,indent=0] +---- +include::{testRelease21Dir}/example/ParameterizedRecordDemo.java[tags=example] +---- + +[[writing-tests-parameterized-tests-consuming-arguments-field-injection]] +====== Field Injection + +For field injection, the following rules apply for fields annotated with `@Parameter`. + +* Zero or more _indexed parameters_ may be declared; each must have a unique index + specified in its `@Parameter(index)` annotation. The index may be omitted if there is + only one indexed parameter. If there are at least two indexed parameter declarations, + there must be declarations for all indexes from 0 to the largest declared index. +* Zero or more _aggregators_ may be declared; each without specifying an index in its + `@Parameter` annotation. +* Zero or more other fields may be declared as usual as long as they're not annotated with + `@Parameter`. + +In this context, an _indexed parameter_ is an argument for a given index in the +`{Arguments}` provided by an `{ArgumentsProvider}` that is injected into a field annotated +with `@Parameter(index)`. An _aggregator_ is any `@Parameter`-annotated field of type +{ArgumentsAccessor} or any field annotated with {AggregateWith}. + +The following example demonstrates how to use field injection to consume multiple +arguments in a parameterized class. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedClassDemo.java[tags=field_injection] +---- + +If field injection is used, no constructor parameters will be resolved with arguments from +the source. Other <> +may resolve constructor parameters as usual, though. + +[[writing-tests-parameterized-tests-consuming-arguments-lifecycle-method]] +====== Lifecycle Methods + +`{BeforeParameterizedClassInvocation}` and `{AfterParameterizedClassInvocation}` can also +be used to consume arguments if their `injectArguments` attribute is set to `true` (the +default). If so, their method signatures must follow the same rules apply as defined for +<> and +additionally use the same parameter types as the _indexed parameters_ of the parameterized +test class. Please refer to the Javadoc of `{BeforeParameterizedClassInvocation}` and +`{AfterParameterizedClassInvocation}` for details and to the +<> section for an +example. [NOTE] .AutoCloseable arguments ==== Arguments that implement `java.lang.AutoCloseable` (or `java.io.Closeable` which extends -`java.lang.AutoCloseable`) will be automatically closed after `@AfterEach` methods and -`AfterEachCallback` extensions have been called for the current parameterized test -invocation. +`java.lang.AutoCloseable`) will be automatically closed after the parameterized class or +test invocation. To prevent this from happening, set the `autoCloseArguments` attribute in `@ParameterizedTest` to `false`. Specifically, if an argument that implements -`AutoCloseable` is reused for multiple invocations of the same parameterized test method, -you must annotate the method with `@ParameterizedTest(autoCloseArguments = false)` to -ensure that the argument is not closed between invocations. +`AutoCloseable` is reused for multiple invocations of the same parameterized class or test +method, you must specify the `autoCloseArguments = false` on the `{ParameterizedClass}` or +`{ParameterizedTest}` annotation to ensure that the argument is not closed between +invocations. ==== +[[writing-tests-parameterized-tests-consuming-arguments-other-extensions]] +===== Other Extensions + +Other extensions can access the parameters and resolved arguments of a parameterized test +or class by retrieving a `{ParameterInfo}` object from the `{ExtensionContext_Store}`. +Please refer to the Javadoc of `{ParameterInfo}` for details. + +[[writing-tests-parameterized-tests-argument-aggregation]] + [[writing-tests-parameterized-tests-sources]] ==== Sources of Arguments @@ -1481,6 +1786,10 @@ following subsections provides a brief overview and an example for each of them. refer to the Javadoc in the `{params-provider-package}` package for additional information. +TIP: All source annotations in this section are applicable to both `{ParameterizedClass}` +and `{ParameterizedTest}`. For the sake of brevity, the examples in this section will only +show how to use them with `{ParameterizedTest}` methods. + [[writing-tests-parameterized-tests-sources-ValueSource]] ===== @ValueSource @@ -1517,22 +1826,23 @@ supplied _bad input_, it can be useful to have `null` and _empty_ values supplie parameterized tests. The following annotations serve as sources of `null` and empty values for parameterized tests that accept a single argument. -* `{NullSource}`: provides a single `null` argument to the annotated `@ParameterizedTest` - method. +* `{NullSource}`: provides a single `null` argument to the annotated `@ParameterizedClass` + or `@ParameterizedTest`. - `@NullSource` cannot be used for a parameter that has a primitive type. * `{EmptySource}`: provides a single _empty_ argument to the annotated - `@ParameterizedTest` method for parameters of the following types: `java.lang.String`, - `java.util.Collection` (and concrete subtypes with a `public` no-arg constructor), - `java.util.List`, `java.util.Set`, `java.util.SortedSet`, `java.util.NavigableSet`, - `java.util.Map` (and concrete subtypes with a `public` no-arg constructor), - `java.util.SortedMap`, `java.util.NavigableMap`, primitive arrays (e.g., `int[]`, - `char[][]`, etc.), object arrays (e.g., `String[]`, `Integer[][]`, etc.). + `@ParameterizedClass` or `@ParameterizedTest` for parameters of the following types: + `java.lang.String`, `java.util.Collection` (and concrete subtypes with a `public` no-arg + constructor), `java.util.List`, `java.util.Set`, `java.util.SortedSet`, + `java.util.NavigableSet`, `java.util.Map` (and concrete subtypes with a `public` no-arg + constructor), `java.util.SortedMap`, `java.util.NavigableMap`, primitive arrays (e.g., + `int[]`, `char[][]`, etc.), object arrays (e.g., `String[]`, `Integer[][]`, etc.). * `{NullAndEmptySource}`: a _composed annotation_ that combines the functionality of `@NullSource` and `@EmptySource`. -If you need to supply multiple varying types of _blank_ strings to a parameterized test, -you can achieve that using <> -- -for example, `@ValueSource(strings = {"{nbsp}", "{nbsp}{nbsp}{nbsp}", "\t", "\n"})`. +If you need to supply multiple varying types of _blank_ strings to a parameterized +class or test, you can achieve that using +<> -- for example, +`@ValueSource(strings = {"{nbsp}", "{nbsp}{nbsp}{nbsp}", "\t", "\n"})`. You can also combine `@NullSource`, `@EmptySource`, and `@ValueSource` to test a wider range of `null`, _empty_, and _blank_ input. The following example demonstrates how to @@ -1566,7 +1876,7 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=EnumSource_example] ---- The annotation's `value` attribute is optional. When omitted, the declared type of the -first method parameter is used. The test will fail if it does not reference an enum type. +first parameter is used. The test will fail if it does not reference an enum type. Thus, the `value` attribute is required in the above example because the method parameter is declared as `TemporalUnit`, i.e. the interface implemented by `ChronoUnit`, which isn't an enum type. Changing the method parameter type to `ChronoUnit` allows you to omit the @@ -1635,14 +1945,14 @@ must always be `static`. Each factory method must generate a _stream_ of _arguments_, and each set of arguments within the stream will be provided as the physical arguments for individual invocations -of the annotated `@ParameterizedTest` method. Generally speaking this translates to a -`Stream` of `Arguments` (i.e., `Stream`); however, the actual concrete return -type can take on many forms. In this context, a "stream" is anything that JUnit can -reliably convert into a `Stream`, such as `Stream`, `DoubleStream`, `LongStream`, -`IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects, or an array of -primitives. The "arguments" within the stream can be supplied as an instance of -`Arguments`, an array of objects (e.g., `Object[]`), or a single value if the -parameterized test method accepts a single argument. +of the annotated `@ParameterizedClass` or `@ParameterizedTest`. Generally speaking this +translates to a `Stream` of `Arguments` (i.e., `Stream`); however, the actual +concrete return type can take on many forms. In this context, a "stream" is anything that +JUnit can reliably convert into a `Stream`, such as `Stream`, `DoubleStream`, +`LongStream`, `IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects, or +an array of primitives. The "arguments" within the stream can be supplied as an instance +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 you only need a single parameter, you can return a `Stream` of instances of the parameter type as demonstrated in the following example. @@ -1652,8 +1962,9 @@ parameter type as demonstrated in the following example. include::{testDir}/example/ParameterizedTestDemo.java[tags=simple_MethodSource_example] ---- -If you do not explicitly provide a factory method name via `@MethodSource`, JUnit Jupiter -will search for a _factory_ method that has the same name as the current +For a `@ParameterizedClass`, providing a factory method name via `@MethodSource` is +mandatory. For a `@ParameterizedTest`, if you do not explicitly provide a factory method +name, JUnit Jupiter will search for a _factory_ method with the same name as the current `@ParameterizedTest` method by convention. This is demonstrated in the following example. [source,java,indent=0] @@ -1669,11 +1980,11 @@ supported as demonstrated by the following example. include::{testDir}/example/ParameterizedTestDemo.java[tags=primitive_MethodSource_example] ---- -If a parameterized test method declares multiple parameters, you need to return a -collection, stream, or array of `Arguments` instances or object arrays as shown below -(see the Javadoc for `{MethodSource}` for further details on supported return types). -Note that `arguments(Object...)` is a static factory method defined in the `Arguments` -interface. In addition, `Arguments.of(Object...)` may be used as an alternative to +If a parameterized class or test method declares multiple parameters, you need to return a +collection, stream, or array of `Arguments` instances or object arrays as shown below (see +the Javadoc for `{MethodSource}` for further details on supported return types). Note that +`arguments(Object...)` is a static factory method defined in the `Arguments` interface. In +addition, `Arguments.of(Object...)` may be used as an alternative to `arguments(Object...)`. [source,java,indent=0] @@ -1717,7 +2028,7 @@ Fields within the test class must be `static` unless the test class is annotated Each field must be able to supply a _stream_ of arguments, and each set of "arguments" within the "stream" will be provided as the physical arguments for individual invocations -of the annotated `@ParameterizedTest` method. +of the annotated `@ParameterizedClass` or `@ParameterizedTest`. In this context, a "stream" is anything that JUnit can reliably convert to a `Stream`; however, the actual concrete field type can take on many forms. Generally speaking this @@ -1725,8 +2036,8 @@ translates to a `Collection`, an `Iterable`, a `Supplier` of a stream (`Stream`, `DoubleStream`, `LongStream`, or `IntStream`), a `Supplier` of an `Iterator`, an array of objects, or an array of primitives. Each set of "arguments" within the "stream" can be supplied as an instance of `Arguments`, an array of objects (for example, `Object[]`, -`String[]`, etc.), or a single value if the parameterized test method accepts a single -argument. +`String[]`, etc.), or a single value if the parameterized class or test method accepts a +single argument. [WARNING] ==== @@ -1739,11 +2050,13 @@ these types, you can wrap it in a `Supplier` — for example, `Supplier> to `strict`. -To change this behavior for a single test, -use the `argumentCountValidation` attribute of the `@ParameterizedTest` annotation: +To change this behavior for a single parameterized class or test method, +use the `argumentCountValidation` attribute of the `@ParameterizedClass` or +`@ParameterizedTest` annotation: [source,java,indent=0] ---- @@ -2093,10 +2410,10 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=argument_count_valida JUnit Jupiter supports https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.2[Widening Primitive -Conversion] for arguments supplied to a `@ParameterizedTest`. For example, a -parameterized test annotated with `@ValueSource(ints = { 1, 2, 3 })` can be declared to -accept not only an argument of type `int` but also an argument of type `long`, `float`, -or `double`. +Conversion] for arguments supplied to a `@ParameterizedClass` or `@ParameterizedTest`. +For example, a parameterized class or test method annotated with +`@ValueSource(ints = { 1, 2, 3 })` can be declared to accept not only an argument of type +`int` but also an argument of type `long`, `float`, or `double`. [[writing-tests-parameterized-tests-argument-conversion-implicit]] ===== Implicit Conversion @@ -2105,9 +2422,9 @@ To support use cases like `@CsvSource`, JUnit Jupiter provides a number of built implicit type converters. The conversion process depends on the declared type of each method parameter. -For example, if a `@ParameterizedTest` declares a parameter of type `TimeUnit` and the -actual type supplied by the declared source is a `String`, the string will be -automatically converted into the corresponding `TimeUnit` enum constant. +For example, if a `@ParameterizedClass` or `@ParameterizedTest` declares a parameter +of type `TimeUnit` and the actual type supplied by the declared source is a `String`, the +string will be automatically converted into the corresponding `TimeUnit` enum constant. [source,java,indent=0] ---- @@ -2238,9 +2555,10 @@ If you wish to implement a custom `ArgumentConverter` that also consumes an anno [[writing-tests-parameterized-tests-argument-aggregation]] ==== Argument Aggregation -By default, each _argument_ provided to a `@ParameterizedTest` method corresponds to a -single method parameter. Consequently, argument sources which are expected to supply a -large number of arguments can lead to large method signatures. +By default, each _argument_ provided to a `@ParameterizedClass` or `@ParameterizedTest` +corresponds to a single method parameter. Consequently, argument sources which are +expected to supply a large number of arguments can lead to large constructor or method +signatures, respectively. In such cases, an `{ArgumentsAccessor}` can be used instead of multiple parameters. Using this API, you can access the provided arguments through a single argument passed to your @@ -2261,16 +2579,16 @@ _An instance of `ArgumentsAccessor` is automatically injected into any parameter [[writing-tests-parameterized-tests-argument-aggregation-custom]] ===== Custom Aggregators -Apart from direct access to a `@ParameterizedTest` method's arguments using an -`ArgumentsAccessor`, JUnit Jupiter also supports the usage of custom, reusable -_aggregators_. +Apart from direct access to the arguments of a `@ParameterizedClass` or +`@ParameterizedTest` using an `ArgumentsAccessor`, JUnit Jupiter also supports the usage +of custom, reusable _aggregators_. To use a custom aggregator, implement the `{ArgumentsAggregator}` interface and register -it via the `@AggregateWith` annotation on a compatible parameter in the -`@ParameterizedTest` method. The result of the aggregation will then be provided as an -argument for the corresponding parameter when the parameterized test is invoked. Note -that an implementation of `ArgumentsAggregator` must be declared as either a top-level -class or as a `static` nested class. +it via the `@AggregateWith` annotation on a compatible parameter of the +`@ParameterizedClass` or `@ParameterizedTest`. The result of the aggregation will then be +provided as an argument for the corresponding parameter when the parameterized test is +invoked. Note that an implementation of `ArgumentsAggregator` must be declared as either a +top-level class or as a `static` nested class. [source,java,indent=0] ---- @@ -2283,8 +2601,8 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=ArgumentsAggregator_e ---- If you find yourself repeatedly declaring `@AggregateWith(MyTypeAggregator.class)` for -multiple parameterized test methods across your codebase, you may wish to create a custom -_composed annotation_ such as `@CsvToMyType` that is meta-annotated with +multiple parameterized classes or methods across your codebase, you may wish to create a +custom _composed annotation_ such as `@CsvToMyType` that is meta-annotated with `@AggregateWith(MyTypeAggregator.class)`. The following example demonstrates this in action with a custom `@CsvToPerson` annotation. @@ -2302,14 +2620,15 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=ArgumentsAggregator_w [[writing-tests-parameterized-tests-display-names]] ==== Customizing Display Names -By default, the display name of a parameterized test invocation contains the invocation -index and the `String` representation of all arguments for that specific invocation. Each -argument is preceded by its parameter name (unless the argument is only available via an -`ArgumentsAccessor` or `ArgumentAggregator`), if the parameter name is present in the -bytecode (for Java, test code must be compiled with the `-parameters` compiler flag). +By default, the display name of a parameterized class or test invocation contains the +invocation index and the `String` representation of all arguments for that specific +invocation. Each argument is preceded by its parameter name (unless the argument is only +available via an `ArgumentsAccessor` or `ArgumentAggregator`), if the parameter name is +present in the bytecode (for Java, test code must be compiled with the `-parameters` +compiler flag; for Kotlin, with `-java-parameters`). However, you can customize invocation display names via the `name` attribute of the -`@ParameterizedTest` annotation like in the following example. +`@ParameterizedClass` or `@ParameterizedTest` annotation as in the following example. ====== [source,java,indent=0] @@ -2338,15 +2657,15 @@ The following placeholders are supported within custom display names. [cols="20,80"] |=== -| Placeholder | Description - -| `{displayName}` | the display name of the method -| `{index}` | the current invocation index (1-based) -| `{arguments}` | the complete, comma-separated arguments list -| `{argumentsWithNames}` | the complete, comma-separated arguments list with parameter names -| `{argumentSetName}` | the name of the argument set -| `{argumentSetNameOrArgumentsWithNames}` | `{argumentSetName}` or `{argumentsWithNames}`, depending on how the arguments are supplied -| `{0}`, `{1}`, ... | an individual argument +| Placeholder | Description + +| `\{displayName}` | the display name of the method +| `\{index}` | the current invocation index (1-based) +| `\{arguments}` | the complete, comma-separated arguments list +| `\{argumentsWithNames}` | the complete, comma-separated arguments list with parameter names +| `\{argumentSetName}` | the name of the argument set +| `\{argumentSetNameOrArgumentsWithNames}` | `\{argumentSetName}` or `\{argumentsWithNames}`, depending on how the arguments are supplied +| `\{0}`, `\{1}`, ... | an individual argument |=== NOTE: When including arguments in display names, their string representations are truncated @@ -2411,9 +2730,9 @@ Note that `argumentSet(String, Object...)` is a static factory method defined in `org.junit.jupiter.params.provider.Arguments` interface. ==== -If you'd like to set a default name pattern for all parameterized tests in your project, -you can declare the `junit.jupiter.params.displayname.default` configuration parameter in -the `junit-platform.properties` file as demonstrated in the following example (see +If you'd like to set a default name pattern for all parameterized classes and tests in +your project, you can declare the `junit.jupiter.params.displayname.default` configuration +parameter in the `junit-platform.properties` file as demonstrated in the following example (see <> for other options). [source,properties,indent=0] @@ -2421,16 +2740,20 @@ the `junit-platform.properties` file as demonstrated in the following example (s junit.jupiter.params.displayname.default = {index} ---- -The display name for a parameterized test is determined according to the following -precedence rules: +The display name for a parameterized class or test is determined according to the +following precedence rules: -1. `name` attribute in `@ParameterizedTest`, if present +1. `name` attribute in `@ParameterizedClass` or `@ParameterizedTest`, if present 2. value of the `junit.jupiter.params.displayname.default` configuration parameter, if present -3. `DEFAULT_DISPLAY_NAME` constant defined in `@ParameterizedTest` +3. `DEFAULT_DISPLAY_NAME` constant defined in + `org.junit.jupiter.params.ParameterizedInvocationConstants` [[writing-tests-parameterized-tests-lifecycle-interop]] ==== Lifecycle and Interoperability +[[writing-tests-parameterized-tests-lifecycle-interop-methods]] +===== Parameterized Tests + Each invocation of a parameterized test has the same lifecycle as a regular `@Test` method. For example, `@BeforeEach` methods will be executed before each invocation. Similar to <>, invocations will appear one by one in the @@ -2439,7 +2762,7 @@ methods within the same test class. You may use `ParameterResolver` extensions with `@ParameterizedTest` methods. However, method parameters that are resolved by argument sources need to come first in the -argument list. Since a test class may contain regular tests as well as parameterized +parameter list. Since a test class may contain regular tests as well as parameterized tests with different parameter lists, values from argument sources are not resolved for lifecycle methods (e.g. `@BeforeEach`) and test class constructors. @@ -2448,20 +2771,73 @@ lifecycle methods (e.g. `@BeforeEach`) and test class constructors. include::{testDir}/example/ParameterizedTestDemo.java[tags=ParameterResolver_example] ---- +[[writing-tests-parameterized-tests-lifecycle-interop-classes]] +===== Parameterized Classes + +Each invocation of a parameterized class has the same lifecycle as a regular test class. +For example, `@BeforeAll` methods will be executed _once_ before all invocations and +`@BeforeEach` methods will be executed before each _test method_ invocation. Similar to +<>, invocations will appear one by one in the test tree of an +IDE. + +You may use `ParameterResolver` extensions with `@ParameterizedClass` constructors. +However, if constructor injection is used, constructor parameters that are resolved by +argument sources need to come first in the parameter list. Values from argument sources +are not resolved for regular lifecycle methods (e.g. `@BeforeEach`). + +In addition to regular lifecycle methods, parameterized classes may declare +`{BeforeParameterizedClassInvocation}` and `{AfterParameterizedClassInvocation}` lifecycle +methods that are called once before/after each invocation of the parameterized class. +These methods must be `static` unless the parameterized class is configured to use +`@TestInstance(Lifecycle.PER_CLASS)` (see <>). + +These lifecycle methods may optionally declare parameters that are resolved depending on +the setting of the `injectArguments` annotation attribute. If it is set to `false`, the +parameters must be resolved by other registered {ParameterResolver} extensions. If the +attribute is set to `true` (the default), the method may declare parameters that match the +arguments of the parameterized class (see the Javadoc of +`{BeforeParameterizedClassInvocation}` and `{AfterParameterizedClassInvocation}` for +details). This may, for example, be used to initialize the used arguments as demonstrated +by the following example. + +[source,java,indent=0] +.Using parameterized class lifecycle methods +---- +include::{testRelease21Dir}/example/ParameterizedLifecycleDemo.java[tags=example] +---- +<1> Initialization of the argument _before_ each invocation of the parameterized class +<2> Usage of the previously initialized argument in a test method +<3> Validation and cleanup of the argument _after_ each invocation of the parameterized + class + +[[writing-tests-class-templates]] +=== Class Templates + +A `{ClassTemplate}` is not a regular test class but rather a template for the contained +test cases. As such, it is designed to be invoked multiple times depending on invocation +contexts returned by the registered providers. Thus, it must be used in conjunction with a +registered `{ClassTemplateInvocationContextProvider}` extension. +Each invocation of a class template behaves like the execution of a regular test class +with full support for the same lifecycle callbacks and extensions. Please refer to +<> for usage examples. + +NOTE: <> are a built-in +specialization of class templates. [[writing-tests-test-templates]] === Test Templates -A `{TestTemplate}` method is not a regular test case but rather a template for test -cases. As such, it is designed to be invoked multiple times depending on the number of +A `{TestTemplate}` method is not a regular test case but rather a template for a test +case. As such, it is designed to be invoked multiple times depending on the number of invocation contexts returned by the registered providers. Thus, it must be used in conjunction with a registered `{TestTemplateInvocationContextProvider}` extension. Each invocation of a test template method behaves like the execution of a regular `@Test` method with full support for the same lifecycle callbacks and extensions. Please refer to <> for usage examples. -NOTE: <> and <> are -built-in specializations of test templates. +NOTE: <> and +<> are built-in specializations of +test templates. [[writing-tests-dynamic-tests]] === Dynamic Tests diff --git a/documentation/src/plantuml/component-diagram.puml b/documentation/src/plantuml/component-diagram.puml index 4874f5e1abb8..cc06c35ccbd2 100644 --- a/documentation/src/plantuml/component-diagram.puml +++ b/documentation/src/plantuml/component-diagram.puml @@ -39,6 +39,10 @@ package org.opentest4j { [opentest4j] } +package org.opentest4j.reporting { + [open-test-reporting-tooling-spi] as otr_tooling_spi +} + package org.apiguardian { [apiguardian-api] as apiguardian note bottom of apiguardian #white @@ -77,6 +81,7 @@ engine ....> opentest4j engine ..> commons reporting ..> launcher +reporting ......> otr_tooling_spi runner ..> suite_commons runner ...> junit4 diff --git a/documentation/src/test/java/example/ClassTemplateDemo.java b/documentation/src/test/java/example/ClassTemplateDemo.java new file mode 100644 index 000000000000..d0291dedfbdf --- /dev/null +++ b/documentation/src/test/java/example/ClassTemplateDemo.java @@ -0,0 +1,97 @@ +/* + * 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.util.Collections.singletonList; +import static java.util.Collections.unmodifiableList; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.ClassTemplate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; + +// tag::user_guide[] +@ClassTemplate +@ExtendWith(ClassTemplateDemo.MyClassTemplateInvocationContextProvider.class) +class ClassTemplateDemo { + + static final List WELL_KNOWN_FRUITS + // tag::custom_line_break[] + = unmodifiableList(Arrays.asList("apple", "banana", "lemon")); + + private String fruit; + + @Test + void notNull() { + assertNotNull(fruit); + } + + @Test + void wellKnown() { + assertTrue(WELL_KNOWN_FRUITS.contains(fruit)); + } + + // end::user_guide[] + static + // tag::user_guide[] + public class MyClassTemplateInvocationContextProvider + // tag::custom_line_break[] + implements ClassTemplateInvocationContextProvider { + + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream + // tag::custom_line_break[] + provideClassTemplateInvocationContexts(ExtensionContext context) { + + return Stream.of(invocationContext("apple"), invocationContext("banana")); + } + + private ClassTemplateInvocationContext invocationContext(String parameter) { + return new ClassTemplateInvocationContext() { + @Override + public String getDisplayName(int invocationIndex) { + return parameter; + } + + // end::user_guide[] + @SuppressWarnings("Convert2Lambda") + // tag::user_guide[] + @Override + public List getAdditionalExtensions() { + return singletonList(new TestInstancePostProcessor() { + @Override + public void postProcessTestInstance( + // tag::custom_line_break[] + Object testInstance, ExtensionContext context) { + ((ClassTemplateDemo) testInstance).fruit = parameter; + } + }); + } + }; + } + } +} +// end::user_guide[] diff --git a/documentation/src/test/java/example/DisplayNameGeneratorDemo.java b/documentation/src/test/java/example/DisplayNameGeneratorDemo.java index db76b7a8e55f..07fd777b9177 100644 --- a/documentation/src/test/java/example/DisplayNameGeneratorDemo.java +++ b/documentation/src/test/java/example/DisplayNameGeneratorDemo.java @@ -10,11 +10,10 @@ package example; -// tag::user_guide[] - import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.DisplayNameGenerator.IndicativeSentences.SentenceFragment; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.IndicativeSentencesGeneration; import org.junit.jupiter.api.Nested; @@ -25,6 +24,7 @@ class DisplayNameGeneratorDemo { @Nested + // tag::user_guide_replace_underscores[] @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class A_year_is_not_supported { @@ -39,8 +39,10 @@ void if_it_is_negative(int year) { } } + // end::user_guide_replace_underscores[] @Nested + // tag::user_guide_indicative_sentences[] @IndicativeSentencesGeneration(separator = " -> ", generator = ReplaceUnderscores.class) class A_year_is_a_leap_year { @@ -54,6 +56,26 @@ void if_it_is_one_of_the_following_years(int year) { } } + // end::user_guide_indicative_sentences[] + + @Nested + // tag::user_guide_custom_sentence_fragments[] + @SentenceFragment("A year is a leap year") + @IndicativeSentencesGeneration + class LeapYearTests { + + @SentenceFragment("if it is divisible by 4 but not by 100") + @Test + void divisibleBy4ButNotBy100() { + } + + @SentenceFragment("if it is one of the following years") + @ParameterizedTest(name = "{0}") + @ValueSource(ints = { 2016, 2020, 2048 }) + void validLeapYear(int year) { + } + + } + // end::user_guide_custom_sentence_fragments[] } -// end::user_guide[] diff --git a/documentation/src/test/java/example/ParameterizedClassDemo.java b/documentation/src/test/java/example/ParameterizedClassDemo.java new file mode 100644 index 000000000000..544a55ab6a48 --- /dev/null +++ b/documentation/src/test/java/example/ParameterizedClassDemo.java @@ -0,0 +1,150 @@ +/* + * 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 org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; + +import java.time.Duration; +import java.util.Arrays; + +import example.util.StringUtils; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class ParameterizedClassDemo { + + @Nested + // tag::first_example[] + @ParameterizedClass + @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) + class PalindromeTests { + + @Parameter + String candidate; + + @Test + void palindrome() { + assertTrue(StringUtils.isPalindrome(candidate)); + } + + @Test + void reversePalindrome() { + String reverseCandidate = new StringBuilder(candidate).reverse().toString(); + assertTrue(StringUtils.isPalindrome(reverseCandidate)); + } + } + // end::first_example[] + + @Nested + class ConstructorInjection { + @Nested + // tag::constructor_injection[] + @ParameterizedClass + @CsvSource({ "apple, 23", "banana, 42" }) + class FruitTests { + + final String fruit; + final int quantity; + + FruitTests(String fruit, int quantity) { + this.fruit = fruit; + this.quantity = quantity; + } + + @Test + void test() { + assertFruit(fruit); + assertQuantity(quantity); + } + + @Test + void anotherTest() { + // ... + } + } + // end::constructor_injection[] + } + + @Nested + class FieldInjection { + @Nested + // tag::field_injection[] + @ParameterizedClass + @CsvSource({ "apple, 23", "banana, 42" }) + class FruitTests { + + @Parameter(0) + String fruit; + + @Parameter(1) + int quantity; + + @Test + void test() { + assertFruit(fruit); + assertQuantity(quantity); + } + + @Test + void anotherTest() { + // ... + } + } + // end::field_injection[] + } + + @Nested + // tag::nested[] + @Execution(SAME_THREAD) + @ParameterizedClass + @ValueSource(strings = { "apple", "banana" }) + class FruitTests { + + @Parameter + String fruit; + + @Nested + @ParameterizedClass + @ValueSource(ints = { 23, 42 }) + class QuantityTests { + + @Parameter + int quantity; + + @ParameterizedTest + @ValueSource(strings = { "PT1H", "PT2H" }) + void test(Duration duration) { + assertFruit(fruit); + assertQuantity(quantity); + assertFalse(duration.isNegative()); + } + } + } + // end::nested[] + + static void assertFruit(String fruit) { + assertTrue(Arrays.asList("apple", "banana", "cherry", "dewberry").contains(fruit), + () -> "not a fruit: " + fruit); + } + + static void assertQuantity(int quantity) { + assertTrue(quantity > 0); + } +} diff --git a/documentation/src/test/java/example/ParameterizedMigrationDemo.java b/documentation/src/test/java/example/ParameterizedMigrationDemo.java new file mode 100644 index 000000000000..e5425f73412e --- /dev/null +++ b/documentation/src/test/java/example/ParameterizedMigrationDemo.java @@ -0,0 +1,101 @@ +/* + * 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 java.util.Arrays; + +import org.junit.jupiter.params.AfterParameterizedClassInvocation; +import org.junit.jupiter.params.BeforeParameterizedClassInvocation; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +public class ParameterizedMigrationDemo { + + @SuppressWarnings("JUnitMalformedDeclaration") + // tag::before[] + @RunWith(Parameterized.class) + // end::before[] + static + // tag::before[] + public class JUnit4ParameterizedClassTests { + + @Parameterized.Parameters + public static Iterable data() { + return Arrays.asList(new Object[][] { { 1, "foo" }, { 2, "bar" } }); + } + + // end::before[] + @SuppressWarnings("DefaultAnnotationParam") + // tag::before[] + @Parameterized.Parameter(0) + public int number; + + @Parameterized.Parameter(1) + public String text; + + @Parameterized.BeforeParam + public static void before(int number, String text) { + } + + @Parameterized.AfterParam + public static void after() { + } + + @org.junit.Test + public void someTest() { + } + + @org.junit.Test + public void anotherTest() { + } + } + // end::before[] + + @SuppressWarnings("JUnitMalformedDeclaration") + // tag::after[] + @ParameterizedClass + @MethodSource("data") + // end::after[] + static + // tag::after[] + class JupiterParameterizedClassTests { + + static Iterable data() { + return Arrays.asList(new Object[][] { { 1, "foo" }, { 2, "bar" } }); + } + + @org.junit.jupiter.params.Parameter(0) + int number; + + @org.junit.jupiter.params.Parameter(1) + String text; + + @BeforeParameterizedClassInvocation + static void before(int number, String text) { + } + + @AfterParameterizedClassInvocation + static void after() { + } + + @org.junit.jupiter.api.Test + void someTest() { + } + + @org.junit.jupiter.api.Test + void anotherTest() { + } + } + // end::after[] + +} diff --git a/documentation/src/test/java/example/ParameterizedTestDemo.java b/documentation/src/test/java/example/ParameterizedTestDemo.java index 2a9d0b77fb78..50bf1ff882fa 100644 --- a/documentation/src/test/java/example/ParameterizedTestDemo.java +++ b/documentation/src/test/java/example/ParameterizedTestDemo.java @@ -44,18 +44,19 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestReporter; +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.parallel.Execution; import org.junit.jupiter.params.ArgumentCountValidationMode; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.aggregator.AggregateWith; import org.junit.jupiter.params.aggregator.ArgumentsAccessor; -import org.junit.jupiter.params.aggregator.ArgumentsAggregator; +import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator; import org.junit.jupiter.params.converter.ConvertWith; import org.junit.jupiter.params.converter.JavaTimeConversionPattern; import org.junit.jupiter.params.converter.SimpleArgumentConverter; @@ -72,6 +73,7 @@ import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.support.ParameterDeclarations; @Execution(SAME_THREAD) class ParameterizedTestDemo { @@ -360,7 +362,8 @@ void testWithArgumentsSource(String argument) { public class MyArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of("apple", "banana").map(Arguments::of); } } @@ -383,7 +386,8 @@ public MyArgumentsProviderWithConstructorInjection(TestInfo testInfo) { } @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of(Arguments.of(testInfo.getDisplayName())); } } @@ -536,9 +540,10 @@ void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person p // end::ArgumentsAggregator_example[] static // tag::ArgumentsAggregator_example_PersonAggregator[] - public class PersonAggregator implements ArgumentsAggregator { + public class PersonAggregator extends SimpleArgumentsAggregator { @Override - public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { + protected Person aggregateArguments(ArgumentsAccessor arguments, Class targetType, + AnnotatedElementContext context, int parameterIndex) { return new Person( arguments.getString(0), arguments.getString(1), @@ -628,7 +633,7 @@ static Stream otherProvider() { } // end::repeatable_annotations[] - @extensions.ExpectToFail + @Disabled("Fails prior to invoking the test method") // tag::argument_count_validation[] @ParameterizedTest(argumentCountValidation = ArgumentCountValidationMode.STRICT) @CsvSource({ "42, -666" }) diff --git a/documentation/src/test/java/example/testkit/EngineTestKitDiscoveryDemo.java b/documentation/src/test/java/example/testkit/EngineTestKitDiscoveryDemo.java new file mode 100644 index 000000000000..6e92f8724b4a --- /dev/null +++ b/documentation/src/test/java/example/testkit/EngineTestKitDiscoveryDemo.java @@ -0,0 +1,37 @@ +/* + * 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.testkit; + +// tag::user_guide[] +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +import example.ExampleTestCase; + +import org.junit.jupiter.api.Test; +import org.junit.platform.testkit.engine.EngineDiscoveryResults; +import org.junit.platform.testkit.engine.EngineTestKit; + +class EngineTestKitDiscoveryDemo { + + @Test + void verifyJupiterDiscovery() { + EngineDiscoveryResults results = EngineTestKit.engine("junit-jupiter") // <1> + .selectors(selectClass(ExampleTestCase.class)) // <2> + .discover(); // <3> + + assertEquals("JUnit Jupiter", results.getEngineDescriptor().getDisplayName()); // <4> + assertEquals(emptyList(), results.getDiscoveryIssues()); // <5> + } + +} +// end::user_guide[] diff --git a/documentation/src/test/java21/example/ParameterizedLifecycleDemo.java b/documentation/src/test/java21/example/ParameterizedLifecycleDemo.java new file mode 100644 index 000000000000..c55f121cfb3f --- /dev/null +++ b/documentation/src/test/java21/example/ParameterizedLifecycleDemo.java @@ -0,0 +1,93 @@ +/* + * 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 org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.AfterParameterizedClassInvocation; +import org.junit.jupiter.params.BeforeParameterizedClassInvocation; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.MethodSource; + +public class ParameterizedLifecycleDemo { + + @Nested + // tag::example[] + @ParameterizedClass + @MethodSource("textFiles") + class TextFileTests { + + static List textFiles() { + return List.of( + // tag::custom_line_break[] + new TextFile("file1", "first content"), + // tag::custom_line_break[] + new TextFile("file2", "second content") + // tag::custom_line_break[] + ); + } + + @Parameter + TextFile textFile; + + @BeforeParameterizedClassInvocation + static void beforeInvocation(TextFile textFile, @TempDir Path tempDir) throws Exception { + var filePath = tempDir.resolve(textFile.fileName); // <1> + textFile.path = Files.writeString(filePath, textFile.content); + } + + @AfterParameterizedClassInvocation + static void afterInvocation(TextFile textFile) throws Exception { + var actualContent = Files.readString(textFile.path); // <3> + assertEquals(textFile.content, actualContent, "Content must not have changed"); + // Custom cleanup logic, if necessary + // File will be deleted automatically by @TempDir support + } + + @Test + void test() { + assertTrue(Files.exists(textFile.path)); // <2> + } + + @Test + void anotherTest() { + // ... + } + + static class TextFile { + + final String fileName; + final String content; + Path path; + + TextFile(String fileName, String content) { + this.fileName = fileName; + this.content = content; + } + + @Override + public String toString() { + return fileName; + } + } + } + // end::example[] + +} diff --git a/documentation/src/test/java21/example/ParameterizedRecordDemo.java b/documentation/src/test/java21/example/ParameterizedRecordDemo.java new file mode 100644 index 000000000000..2e974207046d --- /dev/null +++ b/documentation/src/test/java21/example/ParameterizedRecordDemo.java @@ -0,0 +1,49 @@ +/* + * 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 org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.CsvSource; + +public class ParameterizedRecordDemo { + + @SuppressWarnings("JUnitMalformedDeclaration") + // tag::example[] + @ParameterizedClass + @CsvSource({ "apple, 23", "banana, 42" }) + record FruitTests(String fruit, int quantity) { + + @Test + void test() { + assertFruit(fruit); + assertQuantity(quantity); + } + + @Test + void anotherTest() { + // ... + } + } + // end::example[] + + static void assertFruit(String fruit) { + assertTrue(Arrays.asList("apple", "banana", "cherry", "dewberry").contains(fruit)); + } + + static void assertQuantity(int quantity) { + assertTrue(quantity >= 0); + } +} diff --git a/documentation/src/test/resources/junit-platform.properties b/documentation/src/test/resources/junit-platform.properties index 6f2ed6e735fa..0f0255f62dbb 100644 --- a/documentation/src/test/resources/junit-platform.properties +++ b/documentation/src/test/resources/junit-platform.properties @@ -2,3 +2,5 @@ junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent junit.jupiter.execution.parallel.config.strategy=fixed junit.jupiter.execution.parallel.config.fixed.parallelism=6 + +junit.platform.stacktrace.pruning.enabled=false diff --git a/documentation/src/tools/java/org/junit/api/tools/AbstractApiReportWriter.java b/documentation/src/tools/java/org/junit/api/tools/AbstractApiReportWriter.java index 4e08a529fbe1..a35b60211bb4 100644 --- a/documentation/src/tools/java/org/junit/api/tools/AbstractApiReportWriter.java +++ b/documentation/src/tools/java/org/junit/api/tools/AbstractApiReportWriter.java @@ -11,10 +11,13 @@ package org.junit.api.tools; import static java.lang.String.format; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; import java.io.PrintWriter; import java.util.List; import java.util.Set; +import java.util.TreeMap; import org.apiguardian.api.API.Status; @@ -49,12 +52,22 @@ public void printDeclarationInfo(PrintWriter out, Set statuses) { protected void printDeclarationSection(Set statuses, Status status, List declarations, PrintWriter out) { printDeclarationSectionHeader(statuses, status, declarations, out); - if (!declarations.isEmpty()) { - printDeclarationTableHeader(out); - declarations.forEach(it -> printDeclarationTableRow(it, out)); - printDeclarationTableFooter(out); - out.println(); - } + declarations.stream() // + .collect(groupingBy(Declaration::moduleName, TreeMap::new, toList())) // + .forEach((moduleName, moduleDeclarations) -> { + out.println(h4("Module " + moduleName)); + out.println(); + moduleDeclarations.stream() // + .collect(groupingBy(Declaration::packageName, TreeMap::new, toList())) // + .forEach((packageName, packageDeclarations) -> { + out.println(h5("Package " + packageName)); + out.println(); + printDeclarationTableHeader(out); + packageDeclarations.forEach(it -> printDeclarationTableRow(it, out)); + printDeclarationTableFooter(out); + out.println(); + }); + }); } protected void printDeclarationSectionHeader(Set statuses, Status status, List declarations, @@ -74,6 +87,10 @@ protected void printDeclarationSectionHeader(Set statuses, Status status protected abstract String h2(String header); + protected abstract String h4(String header); + + protected abstract String h5(String header); + protected abstract String code(String element); protected abstract String italic(String element); diff --git a/documentation/src/tools/java/org/junit/api/tools/ApiReportGenerator.java b/documentation/src/tools/java/org/junit/api/tools/ApiReportGenerator.java index 02b7baa3f47d..01e6b7832988 100644 --- a/documentation/src/tools/java/org/junit/api/tools/ApiReportGenerator.java +++ b/documentation/src/tools/java/org/junit/api/tools/ApiReportGenerator.java @@ -10,13 +10,16 @@ package org.junit.api.tools; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.toCollection; import java.io.BufferedOutputStream; +import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.UncheckedIOException; +import java.lang.module.ModuleFinder; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -25,6 +28,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.stream.Stream; @@ -52,30 +56,30 @@ public static void main(String... args) { // CAUTION: The output produced by this method is used to // generate a table in the User Guide. - var reportGenerator = new ApiReportGenerator(); + try (var scanResult = scanClasspath()) { - // scan all types below "org.junit" package - var apiReport = reportGenerator.generateReport("org.junit"); - - // ApiReportWriter reportWriter = new MarkdownApiReportWriter(apiReport); - ApiReportWriter reportWriter = new AsciidocApiReportWriter(apiReport); - // ApiReportWriter reportWriter = new HtmlApiReportWriter(apiReport); - - // reportWriter.printReportHeader(new PrintWriter(System.out, true)); - - // Print report for all Usage enum constants - // reportWriter.printDeclarationInfo(new PrintWriter(System.out, true), EnumSet.allOf(Status.class)); - - // Print report only for specific Status constants, defaults to only EXPERIMENTAL - parseArgs(args).forEach((status, opener) -> { - try (var stream = opener.openStream()) { - var writer = new PrintWriter(stream == null ? System.out : stream, true); - reportWriter.printDeclarationInfo(writer, EnumSet.of(status)); - } - catch (IOException e) { - throw new UncheckedIOException("Failed to write report", e); - } - }); + var apiReport = generateReport(scanResult); + + // ApiReportWriter reportWriter = new MarkdownApiReportWriter(apiReport); + ApiReportWriter reportWriter = new AsciidocApiReportWriter(apiReport); + // ApiReportWriter reportWriter = new HtmlApiReportWriter(apiReport); + + // reportWriter.printReportHeader(new PrintWriter(System.out, true)); + + // Print report for all Usage enum constants + // reportWriter.printDeclarationInfo(new PrintWriter(System.out, true), EnumSet.allOf(Status.class)); + + // Print report only for specific Status constants, defaults to only EXPERIMENTAL + parseArgs(args).forEach((status, opener) -> { + try (var stream = opener.openStream()) { + var writer = new PrintWriter(stream == null ? System.out : stream, true, UTF_8); + reportWriter.printDeclarationInfo(writer, EnumSet.of(status)); + } + catch (IOException e) { + throw new UncheckedIOException("Failed to write report", e); + } + }); + } } // ------------------------------------------------------------------------- @@ -102,44 +106,49 @@ private interface StreamOpener { OutputStream openStream() throws IOException; } - ApiReport generateReport(String... packages) { + private static ApiReport generateReport(ScanResult scanResult) { Map> declarations = new EnumMap<>(Status.class); for (var status : Status.values()) { declarations.put(status, new ArrayList<>()); } - try (var scanResult = scanClasspath(packages)) { - - var types = collectTypes(scanResult); - types.stream() // - .map(Declaration.Type::new) // - .forEach(type -> declarations.get(type.status()).add(type)); + var types = collectTypes(scanResult); + types.stream() // + .map(Declaration.Type::new) // + .forEach(type -> declarations.get(type.status()).add(type)); - collectMethods(scanResult) // - .map(Declaration.Method::new) // - .filter(method -> !declarations.get(method.status()) // - .contains(new Declaration.Type(method.classInfo()))) // - .forEach(method -> { - types.add(method.classInfo()); - declarations.get(method.status()).add(method); - }); + collectMethods(scanResult) // + .map(Declaration.Method::new) // + .filter(method -> !declarations.get(method.status()) // + .contains(new Declaration.Type(method.classInfo()))) // + .forEach(method -> { + types.add(method.classInfo()); + declarations.get(method.status()).add(method); + }); - declarations.values().forEach(list -> list.sort(null)); + declarations.values().forEach(list -> list.sort(null)); - return new ApiReport(types, declarations); - } + return new ApiReport(types, declarations); } - private static ScanResult scanClasspath(String[] packages) { + private static ScanResult scanClasspath() { + // scan all types below "org.junit" package var classGraph = new ClassGraph() // - .acceptPackages(packages) // + .acceptPackages("org.junit") // + .rejectPackages("*.shadow.*", "org.opentest4j.*", "org.junit.platform.commons.logging", + "org.junit.platform.commons.util") // .disableNestedJarScanning() // .enableClassInfo() // .enableMethodInfo() // .enableAnnotationInfo(); // var apiClasspath = System.getProperty("api.classpath"); if (apiClasspath != null) { - classGraph = classGraph.overrideClasspath(apiClasspath); + var paths = Arrays.stream(apiClasspath.split(File.pathSeparator)).map(Path::of).toArray(Path[]::new); + var bootLayer = ModuleLayer.boot(); + var configuration = bootLayer.configuration().resolveAndBind(ModuleFinder.of(), ModuleFinder.of(paths), + Set.of()); + var layer = bootLayer.defineModulesWithOneLoader(configuration, ClassLoader.getPlatformClassLoader()); + classGraph = classGraph.overrideModuleLayers(layer); } return classGraph.scan(); } diff --git a/documentation/src/tools/java/org/junit/api/tools/AsciidocApiReportWriter.java b/documentation/src/tools/java/org/junit/api/tools/AsciidocApiReportWriter.java index 24ff7b9eab1a..0a285d3ffdea 100644 --- a/documentation/src/tools/java/org/junit/api/tools/AsciidocApiReportWriter.java +++ b/documentation/src/tools/java/org/junit/api/tools/AsciidocApiReportWriter.java @@ -17,7 +17,7 @@ */ class AsciidocApiReportWriter extends AbstractApiReportWriter { - private static final String ASCIIDOC_FORMAT = "| %-52s | %-" + NAME_COLUMN_WIDTH + "s | %-12s%n"; + private static final String ASCIIDOC_FORMAT = "|%-" + NAME_COLUMN_WIDTH + "s | %-12s%n"; AsciidocApiReportWriter(ApiReport apiReport) { super(apiReport); @@ -33,6 +33,16 @@ protected String h2(String header) { return "== " + header; } + @Override + protected String h4(String header) { + return "[discrete]%n==== %s".formatted(header); + } + + @Override + protected String h5(String header) { + return "[discrete]%n===== %s".formatted(header); + } + @Override protected String code(String element) { return "`" + element + "`"; @@ -45,16 +55,16 @@ protected String italic(String element) { @Override protected void printDeclarationTableHeader(PrintWriter out) { + out.println("[cols=\"99,1\"]"); out.println("|==="); - out.printf(ASCIIDOC_FORMAT, "Package Name", "Name", "Since"); + out.printf(ASCIIDOC_FORMAT, "Name", "Since"); out.println(); } @Override protected void printDeclarationTableRow(Declaration declaration, PrintWriter out) { out.printf(ASCIIDOC_FORMAT, // - code(declaration.packageName()), // - code(declaration.name()) + " " + italic("(" + declaration.kind() + ")"), // + code(declaration.name().replace(".", ".​")) + " " + italic("(" + declaration.kind() + ")"), // code(declaration.since()) // ); } diff --git a/documentation/src/tools/java/org/junit/api/tools/Declaration.java b/documentation/src/tools/java/org/junit/api/tools/Declaration.java index a6921cce8862..7e8fdd5d1cd9 100644 --- a/documentation/src/tools/java/org/junit/api/tools/Declaration.java +++ b/documentation/src/tools/java/org/junit/api/tools/Declaration.java @@ -24,6 +24,8 @@ sealed interface Declaration extends Comparable { + String moduleName(); + String packageName(); String fullName(); @@ -43,6 +45,11 @@ default int compareTo(Declaration o) { record Type(ClassInfo classInfo) implements Declaration { + @Override + public String moduleName() { + return classInfo.getModuleRef().getName(); + } + @Override public String packageName() { return classInfo.getPackageName(); @@ -55,7 +62,8 @@ public String fullName() { @Override public String name() { - return getShortClassName(classInfo); + var shortClassName = getShortClassName(classInfo); + return classInfo.isAnnotation() ? "@" + shortClassName : shortClassName; } @Override @@ -86,6 +94,11 @@ private AnnotationParameterValueList getParameterValues() { record Method(MethodInfo methodInfo) implements Declaration { + @Override + public String moduleName() { + return classInfo().getModuleRef().getName(); + } + @Override public String packageName() { return classInfo().getPackageName(); @@ -98,14 +111,23 @@ public String fullName() { @Override public String name() { + if (classInfo().isAnnotation()) { + return "@%s(%s=...)".formatted(getShortClassName(classInfo()), methodInfo.getName()); + } + if (methodInfo.isConstructor()) { + return "%s%s".formatted(getShortClassName(classInfo()), methodParameters()); + } return "%s.%s".formatted(getShortClassName(classInfo()), methodSignature()); } private String methodSignature() { - var parameters = Arrays.stream(methodInfo.getParameterInfo()) // + return methodInfo.getName() + methodParameters(); + } + + private String methodParameters() { + return Arrays.stream(methodInfo.getParameterInfo()) // .map(parameterInfo -> parameterInfo.getTypeSignatureOrTypeDescriptor().toStringWithSimpleNames()) // .collect(joining(", ", "(", ")")); - return methodInfo.getName() + parameters; } @Override @@ -152,6 +174,6 @@ private static String getShortClassName(ClassInfo classInfo) { if (typeName.startsWith(packageName + '.')) { typeName = typeName.substring(packageName.length() + 1); } - return typeName; + return typeName.replace('$', '.'); } } diff --git a/documentation/src/tools/java/org/junit/api/tools/HtmlApiReportWriter.java b/documentation/src/tools/java/org/junit/api/tools/HtmlApiReportWriter.java index 56020962d54a..c0f5a237b00a 100644 --- a/documentation/src/tools/java/org/junit/api/tools/HtmlApiReportWriter.java +++ b/documentation/src/tools/java/org/junit/api/tools/HtmlApiReportWriter.java @@ -17,8 +17,8 @@ */ class HtmlApiReportWriter extends AbstractApiReportWriter { - private static final String HTML_HEADER_FORMAT = "\t%s%s%s%n"; - private static final String HTML_ROW_FORMAT = "\t%s%s%s%n"; + private static final String HTML_HEADER_FORMAT = "\t%s%s%n"; + private static final String HTML_ROW_FORMAT = "\t%s%s%n"; HtmlApiReportWriter(ApiReport apiReport) { super(apiReport); @@ -34,6 +34,16 @@ protected String h2(String header) { return "

" + header + "

"; } + @Override + protected String h4(String header) { + return "

" + header + "

"; + } + + @Override + protected String h5(String header) { + return "
" + header + "
"; + } + @Override protected String code(String element) { return "" + element + ""; @@ -52,13 +62,12 @@ protected String paragraph(String element) { @Override protected void printDeclarationTableHeader(PrintWriter out) { out.println(""); - out.printf(HTML_HEADER_FORMAT, "Package Name", "Name", "Since"); + out.printf(HTML_HEADER_FORMAT, "Name", "Since"); } @Override protected void printDeclarationTableRow(Declaration declaration, PrintWriter out) { out.printf(HTML_ROW_FORMAT, // - code(declaration.packageName()), // code(declaration.name()) + " " + italic("(" + declaration.kind() + ")"), // code(declaration.since()) // ); diff --git a/documentation/src/tools/java/org/junit/api/tools/MarkdownApiReportWriter.java b/documentation/src/tools/java/org/junit/api/tools/MarkdownApiReportWriter.java index 385d27d9ae07..458d6c721e8a 100644 --- a/documentation/src/tools/java/org/junit/api/tools/MarkdownApiReportWriter.java +++ b/documentation/src/tools/java/org/junit/api/tools/MarkdownApiReportWriter.java @@ -18,7 +18,7 @@ */ class MarkdownApiReportWriter extends AbstractApiReportWriter { - private static final String MARKDOWN_FORMAT = "%-52s | %-" + NAME_COLUMN_WIDTH + "s | %-12s%n"; + private static final String MARKDOWN_FORMAT = "%-" + NAME_COLUMN_WIDTH + "s | %-12s%n"; MarkdownApiReportWriter(ApiReport apiReport) { super(apiReport); @@ -34,6 +34,16 @@ protected String h2(String header) { return "## " + header; } + @Override + protected String h4(String header) { + return "#### " + header; + } + + @Override + protected String h5(String header) { + return "##### " + header; + } + @Override protected String code(String element) { return "`" + element + "`"; @@ -46,8 +56,8 @@ protected String italic(String element) { @Override protected void printDeclarationTableHeader(PrintWriter out) { - out.printf(MARKDOWN_FORMAT, "Package Name", "Name", "Since"); - out.printf(MARKDOWN_FORMAT, dashes(52), dashes(NAME_COLUMN_WIDTH), dashes(12)); + out.printf(MARKDOWN_FORMAT, "Name", "Since"); + out.printf(MARKDOWN_FORMAT, dashes(NAME_COLUMN_WIDTH), dashes(12)); } private String dashes(int length) { @@ -57,7 +67,6 @@ private String dashes(int length) { @Override protected void printDeclarationTableRow(Declaration declaration, PrintWriter out) { out.printf(MARKDOWN_FORMAT, // - code(declaration.packageName()), // code(declaration.name()) + " " + italic("(" + declaration.kind() + ")"), // code(declaration.since()) // ); diff --git a/gradle.properties b/gradle.properties index 4f2ab7df9946..bcf2ff415df6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,27 +1,22 @@ group = org.junit -version = 5.12.0-SNAPSHOT +version = 5.13.0-M1 jupiterGroup = org.junit.jupiter platformGroup = org.junit.platform -platformVersion = 1.12.0-SNAPSHOT +platformVersion = 1.13.0-M1 vintageGroup = org.junit.vintage -vintageVersion = 5.12.0-SNAPSHOT +vintageVersion = 5.13.0-M1 # We need more metaspace due to apparent memory leak in Asciidoctor/JRuby -# The exports are needed due to https://github.com/diffplug/spotless/issues/834 -org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError \ - --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +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.kotlin.dsl.allWarningsAsErrors=true +org.gradle.warning.mode=fail # Test Distribution develocity.internal.testdistribution.writeTraceFile=true @@ -29,3 +24,6 @@ develocity.internal.testdistribution.writeTraceFile=true # Omit automatic compile dependency on kotlin-stdlib # https://kotlinlang.org/docs/gradle.html#dependency-on-the-standard-library kotlin.stdlib.default.dependency=false + +# Avoid Gradle deprecation warnings from Kotlin plugin +kotlin.mpp.keepMppDependenciesIntactInPoms=true diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 000000000000..63e5bbdf4845 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,2 @@ +#This file is generated by updateDaemonJvm +toolchainVersion=21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e8ee505d0b6d..50df32541cac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,19 +5,18 @@ 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.2" -eclipse = "4.34.0" -jackson = "2.18.2" +checkstyle = "10.21.4" +eclipse = "4.35.0" +jackson = "2.18.3" jacoco = "0.8.12" jmh = "1.37" junit4 = "4.13.2" junit4Min = "4.12" ktlint = "1.5.0" log4j = "2.24.3" -logback = "1.5.16" -mockito = "5.15.2" +logback = "1.5.18" opentest4j = "1.3.0" -openTestReporting = "0.2.0-M2" +openTestReporting = "0.2.2" snapshotTests = "1.11.0" surefire = "3.5.2" xmlunit = "2.10.0" @@ -36,7 +35,7 @@ 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" } -groovy4 = { module = "org.apache.groovy:groovy", version = "4.0.25" } +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" } jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } @@ -55,8 +54,9 @@ log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4 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-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } -mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } +mockito-bom = { module = "org.mockito:mockito-bom", version = "5.16.1" } +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" } opentest4j = { module = "org.opentest4j:opentest4j", version.ref = "opentest4j" } openTestReporting-cli = { module = "org.opentest4j.reporting:open-test-reporting-cli", version.ref = "openTestReporting" } @@ -64,7 +64,7 @@ openTestReporting-events = { module = "org.opentest4j.reporting:open-test-report 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" } -slf4j-julBinding = { module = "org.slf4j:slf4j-jdk14", version = "2.0.16" } +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" } spock1 = { module = "org.spockframework:spock-core", version = "1.3-groovy-2.5" } @@ -94,12 +94,14 @@ asciidoctorPdf = { id = "org.asciidoctor.jvm.pdf", version.ref = "asciidoctor-pl 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.1" } +develocity = { id = "com.gradle.develocity", version = "3.19.2" } foojayResolver = { id = "org.gradle.toolchains.foojay-resolver", version = "0.9.0" } -gitPublish = { id = "org.ajoberstar.git-publish", version = "5.1.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.12.1" } +plantuml = { id = "io.freefair.plantuml", version = "8.13" } 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/common/build.gradle.kts b/gradle/plugins/common/build.gradle.kts index 6015360a7105..48c05c7f0a80 100644 --- a/gradle/plugins/common/build.gradle.kts +++ b/gradle/plugins/common/build.gradle.kts @@ -4,7 +4,7 @@ plugins { dependencies { implementation(projects.buildParameters) - implementation(kotlin("gradle-plugin")) + implementation(libs.plugins.kotlin.markerCoordinates) implementation(libs.plugins.bnd.markerCoordinates) implementation(libs.plugins.commonCustomUserData.markerCoordinates) implementation(libs.plugins.develocity.markerCoordinates) diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.jacoco-aggregation-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.jacoco-aggregation-conventions.gradle.kts index 17a613090463..140f1f001fac 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.jacoco-aggregation-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.jacoco-aggregation-conventions.gradle.kts @@ -4,7 +4,7 @@ plugins { } val jacocoRootReport by reporting.reports.creating(JacocoCoverageReport::class) { - testType = TestSuiteType.UNIT_TEST + testSuiteName = "test" } val classesView = configurations["aggregateCodeCoverageReportResults"].incoming.artifactView { diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.kotlin-library-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.kotlin-library-conventions.gradle.kts index 046cf094dfad..73680fcf6e6d 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.kotlin-library-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.kotlin-library-conventions.gradle.kts @@ -27,6 +27,7 @@ afterEvaluate { tasks { withType().configureEach { compilerOptions.jvmTarget = JvmTarget.fromTarget(extension.mainJavaVersion.toString()) + compilerOptions.javaParameters = true } named("compileTestKotlin") { compilerOptions.jvmTarget = JvmTarget.fromTarget(extension.testJavaVersion.toString()) 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 5ec321f5f5d4..696548cf8e86 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 @@ -24,7 +24,8 @@ var openTestReportingCliClasspath = configurations.resolvable("openTestReporting val generateOpenTestHtmlReport by tasks.registering(JavaExec::class) { mustRunAfter(tasks.withType()) - mainClass.set("org.opentest4j.reporting.cli.ReportingCli") + mainModule.set("org.opentest4j.reporting.cli") + modularity.inferModulePath = true args("html-report") classpath(openTestReportingCliClasspath) argumentProviders += objects.newInstance(HtmlReportParameters::class).apply { @@ -70,12 +71,12 @@ tasks.withType().configureEach { } develocity { testRetry { - maxRetries = buildParameters.testing.retries.orElse(if (buildParameters.ci) 2 else 0) + maxRetries.convention(buildParameters.testing.retries.orElse(if (buildParameters.ci) 2 else 0)) } testDistribution { enabled.convention(buildParameters.junit.develocity.testDistribution.enabled && (!buildParameters.ci || !System.getenv("DEVELOCITY_ACCESS_KEY").isNullOrBlank())) - maxLocalExecutors = buildParameters.junit.develocity.testDistribution.maxLocalExecutors - maxRemoteExecutors = buildParameters.junit.develocity.testDistribution.maxRemoteExecutors + maxLocalExecutors.convention(buildParameters.junit.develocity.testDistribution.maxLocalExecutors) + maxRemoteExecutors.convention(buildParameters.junit.develocity.testDistribution.maxRemoteExecutors) if (buildParameters.ci) { when { OperatingSystem.current().isLinux -> requirements.add("os=linux") @@ -85,10 +86,10 @@ tasks.withType().configureEach { } } predictiveTestSelection { - enabled = buildParameters.junit.develocity.predictiveTestSelection.enabled + enabled.convention(buildParameters.junit.develocity.predictiveTestSelection.enabled) if (buildParameters.junit.develocity.predictiveTestSelection.selectRemainingTests) { - mode = PredictiveTestSelectionMode.REMAINING_TESTS + mode.convention(PredictiveTestSelectionMode.REMAINING_TESTS) } // Ensure PTS works when publishing Build Scans to scans.gradle.com @@ -149,6 +150,7 @@ tasks.withType().configureEach { } dependencies { + testImplementation(platform(dependencyFromLibs("mockito-bom"))) testImplementation(dependencyFromLibs("assertj")) testImplementation(dependencyFromLibs("mockito-junit-jupiter")) testImplementation(dependencyFromLibs("testingAnnotations")) @@ -169,6 +171,7 @@ dependencies { openTestReportingCli(dependencyFromLibs("openTestReporting-cli")) openTestReportingCli(project(":junit-platform-reporting")) + javaAgent(platform(dependencyFromLibs("mockito-bom"))) javaAgent(dependencyFromLibs("mockito-core")) { isTransitive = false } diff --git a/gradle/plugins/settings.gradle.kts b/gradle/plugins/settings.gradle.kts index 163866db2805..41935db0aa36 100644 --- a/gradle/plugins/settings.gradle.kts +++ b/gradle/plugins/settings.gradle.kts @@ -1,9 +1,3 @@ -val expectedJavaVersion = JavaVersion.VERSION_21 -val actualJavaVersion = JavaVersion.current() -require(actualJavaVersion == expectedJavaVersion) { - "The JUnit 5 build must be executed with Java ${expectedJavaVersion.majorVersion}. Currently executing with Java ${actualJavaVersion.majorVersion}." -} - dependencyResolutionManagement { versionCatalogs { create("libs") { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d6..9bbc975c742b 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 d71047787f80..36e4933e1da7 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=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3b0d4f..faf93008b77e 100755 --- a/gradlew +++ b/gradlew @@ -205,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AfterAll.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AfterAll.java index 171f530fb324..7b8cf98c88dc 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AfterAll.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AfterAll.java @@ -25,7 +25,13 @@ * executed after all tests in the current test class. * *

In contrast to {@link AfterEach @AfterEach} methods, {@code @AfterAll} - * methods are only executed once for a given test class. + * methods are only executed once per execution of a given test class. If the + * test class is annotated with {@link ClassTemplate @ClassTemplate}, the + * {@code @AfterAll} methods are executed once after the last invocation of the + * class template. If a {@link Nested @Nested} test class is declared in a + * {@link ClassTemplate @ClassTemplate}, its {@code @AfterAll} methods are + * called once per execution of the nested test class, namely, once per + * invocation of the outer class template. * *

Method Signatures

* diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java index 82b0148780a9..1eaea9ce4f3e 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java @@ -143,14 +143,17 @@ private static class ExecutionTimeoutException extends JUnitException { /** * The thread factory used for preemptive timeout. - *

- * The factory creates threads with meaningful names, helpful for debugging purposes. + * + *

The factory creates threads with meaningful names, helpful for debugging + * purposes. */ private static class TimeoutThreadFactory implements ThreadFactory { private static final AtomicInteger threadNumber = new AtomicInteger(1); + @Override public Thread newThread(Runnable r) { return new Thread(r, "junit-timeout-thread-" + threadNumber.getAndIncrement()); } } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java index ab65c8048db7..9e98fb06f3a9 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java @@ -21,8 +21,8 @@ /** * Builder for {@link AssertionFailedError AssertionFailedErrors}. - *

- * Using this builder ensures consistency in how failure message are formatted + * + *

Using this builder ensures consistency in how failure message are formatted * within JUnit Jupiter and for custom user-defined assertions. * * @since 5.9 @@ -51,8 +51,8 @@ private AssertionFailureBuilder() { /** * Set the user-defined message of the assertion. - *

- * The {@code message} may be passed as a {@link Supplier} or plain + * + *

The {@code message} may be passed as a {@link Supplier} or plain * {@link String}. If any other type is passed, it is converted to * {@code String} as per {@link StringUtils#nullSafeToString(Object)}. * @@ -202,4 +202,5 @@ private static String getClassName(Object obj) { return (obj == null ? "null" : obj instanceof Class ? getCanonicalName((Class) obj) : obj.getClass().getName()); } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/BeforeAll.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/BeforeAll.java index e327653c46c3..d5ba1c91e889 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/BeforeAll.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/BeforeAll.java @@ -25,7 +25,13 @@ * executed before all tests in the current test class. * *

In contrast to {@link BeforeEach @BeforeEach} methods, {@code @BeforeAll} - * methods are only executed once for a given test class. + * methods are only executed once per execution of a given test class. If the + * test class is annotated with {@link ClassTemplate @ClassTemplate}, the + * {@code @BeforeAll} methods are executed once before the first invocation of + * the class template. If a {@link Nested @Nested} test class is declared in a + * {@link ClassTemplate @ClassTemplate}, its {@code @BeforeAll} methods are + * called once per execution of the nested test class, namely, once per + * invocation of the outer class template. * *

Method Signatures

* diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ClassTemplate.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ClassTemplate.java new file mode 100644 index 000000000000..95cf2ba436d7 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ClassTemplate.java @@ -0,0 +1,72 @@ +/* + * 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; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.AfterClassTemplateInvocationCallback; +import org.junit.jupiter.api.extension.BeforeClassTemplateInvocationCallback; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContextProvider; +import org.junit.platform.commons.annotation.Testable; + +/** + * {@code @ClassTemplate} is used to signal that the annotated class is a + * class template. + * + *

In contrast to regular test classes, a class template is not directly + * a test class but rather a template for a set of test cases. As such, it is + * designed to be invoked multiple times depending on the number of {@linkplain + * ClassTemplateInvocationContext invocation + * contexts} returned by the registered {@linkplain + * ClassTemplateInvocationContextProvider + * providers}. Must be used together with at least one provider. Otherwise, + * execution will fail. + * + *

Each invocation of a class template method behaves like the execution + * of a regular test class with full support for the same lifecycle callbacks + * and extensions. + * + *

{@code @ClassTemplate} may be combined with {@link Nested @Nested} and + * a class template may contain regular nested test classes or nested + * class templates. + * + *

{@code @ClassTemplate} may also be used as a meta-annotation in order + * to create a custom composed annotation that inherits the semantics + * of {@code @ClassTemplate}. + * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * + * @since 5.13 + * @see TestTemplate + * @see ClassTemplateInvocationContext + * @see ClassTemplateInvocationContextProvider + * @see BeforeClassTemplateInvocationCallback + * @see AfterClassTemplateInvocationCallback + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@API(status = EXPERIMENTAL, since = "5.13") +@Testable +public @interface ClassTemplate { +} 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 9852c79bffd5..7ef4db7ed9b9 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 @@ -18,15 +18,23 @@ import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; import static org.junit.platform.commons.support.ModifierSupport.isStatic; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.List; import java.util.Optional; 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 @@ -232,7 +240,10 @@ public String generateDisplayNameForMethod(List> enclosingInstanceTypes *

This generator extends the functionality of {@link Standard} by * removing parentheses ({@code '()'}) found at the end of method names * with no parameters. + * + * @since 5.7 */ + @API(status = STABLE, since = "5.7") class Simple extends Standard { static final DisplayNameGenerator INSTANCE = new Simple(); @@ -303,19 +314,54 @@ private static String replaceUnderscores(String name) { * via the {@link IndicativeSentencesGeneration @IndicativeSentencesGeneration} * annotation. * + *

If you do not want to rely on a display name generator for individual + * sentence fragments, you can supply custom text for individual fragments + * via the {@link SentenceFragment @SentenceFragment} annotation. + * * @since 5.7 */ @API(status = STABLE, since = "5.10") class IndicativeSentences implements DisplayNameGenerator { + /** + * {@code @SentenceFragment} is used to configure a custom sentence fragment + * for a sentence generated by the {@link IndicativeSentences IndicativeSentences} + * {@code DisplayNameGenerator}. + * + *

Note that {@link DisplayName @DisplayName} always takes precedence + * over {@code @SentenceFragment}. + * + * @since 5.13 + */ + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @API(status = EXPERIMENTAL, since = "5.13") + public @interface SentenceFragment { + + /** + * Custom sentence fragment for the annotated class or method. + * + * @return a custom sentence fragment; never blank or consisting solely + * of whitespace + */ + String value(); + + } + 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() { } @Override public String generateDisplayNameForClass(Class testClass) { - return getGeneratorFor(testClass, emptyList()).generateDisplayNameForClass(testClass); + String sentenceFragment = getSentenceFragment(testClass); + return (sentenceFragment != null ? sentenceFragment + : getGeneratorFor(testClass, emptyList()).generateDisplayNameForClass(testClass)); } @Override @@ -326,27 +372,35 @@ public String generateDisplayNameForNestedClass(List> enclosingInstance @Override public String generateDisplayNameForMethod(List> enclosingInstanceTypes, Class testClass, Method testMethod) { - return getSentenceBeginning(testClass, enclosingInstanceTypes) - + getFragmentSeparator(testClass, enclosingInstanceTypes) - + getGeneratorFor(testClass, enclosingInstanceTypes).generateDisplayNameForMethod( - enclosingInstanceTypes, testClass, testMethod); + + String displayName = getSentenceBeginning(testClass, enclosingInstanceTypes) + + getFragmentSeparator(testClass, enclosingInstanceTypes); + + String sentenceFragment = getSentenceFragment(testMethod); + displayName += (sentenceFragment != null ? sentenceFragment + : getGeneratorFor(testClass, enclosingInstanceTypes).generateDisplayNameForMethod( + enclosingInstanceTypes, testClass, testMethod)); + return displayName; } private String getSentenceBeginning(Class testClass, List> enclosingInstanceTypes) { Class enclosingClass = enclosingInstanceTypes.isEmpty() ? null : enclosingInstanceTypes.get(enclosingInstanceTypes.size() - 1); boolean topLevelTestClass = (enclosingClass == null || isStatic(testClass)); - Optional displayName = findAnnotation(testClass, DisplayName.class)// - .map(DisplayName::value).map(String::trim); + + String sentenceFragment = findAnnotation(testClass, DisplayName.class)// + .map(DisplayName::value)// + .map(String::trim)// + .orElseGet(() -> getSentenceFragment(testClass)); if (topLevelTestClass) { - if (displayName.isPresent()) { - return displayName.get(); + if (sentenceFragment != null) { + return sentenceFragment; } Class generatorClass = findDisplayNameGeneration(testClass, enclosingInstanceTypes)// .map(DisplayNameGeneration::value)// - .filter(not(IndicativeSentences.class))// + .filter(notIndicativeSentences)// .orElse(null); if (generatorClass != null) { return getDisplayNameGenerator(generatorClass).generateDisplayNameForClass(testClass); @@ -369,9 +423,9 @@ private String getSentenceBeginning(Class testClass, List> enclosing + getFragmentSeparator(testClass, enclosingInstanceTypes) : ""); - return prefix + displayName.orElseGet( - () -> getGeneratorFor(testClass, enclosingInstanceTypes).generateDisplayNameForNestedClass( - remainingEnclosingInstanceTypes, testClass)); + return prefix + (sentenceFragment != null ? sentenceFragment + : getGeneratorFor(testClass, enclosingInstanceTypes).generateDisplayNameForNestedClass( + remainingEnclosingInstanceTypes, testClass)); } /** @@ -411,7 +465,7 @@ private static String getFragmentSeparator(Class testClass, List> en private static DisplayNameGenerator getGeneratorFor(Class testClass, List> enclosingInstanceTypes) { return findIndicativeSentencesGeneration(testClass, enclosingInstanceTypes)// .map(IndicativeSentencesGeneration::generator)// - .filter(not(IndicativeSentences.class))// + .filter(notIndicativeSentences)// .map(DisplayNameGenerator::getDisplayNameGenerator)// .orElseGet(() -> getDisplayNameGenerator(IndicativeSentencesGeneration.DEFAULT_GENERATOR)); } @@ -447,8 +501,23 @@ private static Optional findIndicativeSentencesGe return findAnnotation(testClass, IndicativeSentencesGeneration.class, enclosingInstanceTypes); } - private static Predicate> not(Class clazz) { - return ((Predicate>) clazz::equals).negate(); + 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; } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Nested.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Nested.java index 4d96618238d0..2c04fd3badd1 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Nested.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Nested.java @@ -31,6 +31,8 @@ *

{@code @Nested} test classes may be ordered via * {@link TestClassOrder @TestClassOrder} or a global {@link ClassOrderer}. * + *

{@code @Nested} may be combined with {@link ClassTemplate @ClassTemplate}. + * *

Test Instance Lifecycle

* *
    @@ -42,6 +44,7 @@ *
* * @since 5.0 + * @see ClassTemplate * @see Test * @see TestInstance * @see TestClassOrder diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java index 80f904a41987..5ef5c25ec990 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java @@ -86,7 +86,12 @@ enum Lifecycle { /** * When using this mode, a new test instance will be created once per - * test class. + * test class or class template. + * + *

For {@link Nested @Nested}

test classes declared inside an + * enclosing {@link ClassTemplate @ClassTemplate}, an instance of the + * {@code @Nested} class will be created for each invocation of the + * {@code @ClassTemplate}. * * @see #PER_METHOD */ diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java index 85e28acc3bce..7805ab93ab29 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java @@ -89,8 +89,8 @@ default void publishEntry(String value) { /** * Publish the supplied file and attach it to the current test or container. - *

- * The file will be copied to the report output directory replacing any + * + *

The file will be copied to the report output directory replacing any * potentially existing file with the same name. * * @param file the file to be attached; never {@code null} or blank @@ -108,8 +108,8 @@ default void publishFile(Path file, MediaType mediaType) { /** * Publish the supplied directory and attach it to the current test or * container. - *

- * The entire directory will be copied to the report output directory + * + *

The entire directory will be copied to the report output directory * replacing any potentially existing files with the same name. * * @param directory the file to be attached; never {@code null} or blank @@ -142,8 +142,8 @@ default void publishDirectory(Path directory) { /** * Publish a file or directory with the supplied name and media type written * by the supplied action and attach it to the current test or container. - *

- * The {@link Path} passed to the supplied action will be relative to the + * + *

The {@link Path} passed to the supplied action will be relative to the * report output directory, but it's up to the action to write the file. * * @param name the name of the file to be attached; never {@code null} or @@ -161,8 +161,8 @@ default void publishFile(String name, MediaType mediaType, ThrowingConsumer - * The {@link Path} passed to the supplied action will be relative to the + * + *

The {@link Path} passed to the supplied action will be relative to the * report output directory and point to an existing directory, but it's up * to the action to write files to it. * diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestTemplate.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestTemplate.java index 9636a2d01679..5c85bc465e0e 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestTemplate.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestTemplate.java @@ -79,6 +79,7 @@ * * @since 5.0 * @see Test + * @see ClassTemplate * @see org.junit.jupiter.api.extension.TestTemplateInvocationContext * @see org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider */ diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterAllCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterAllCallback.java index 4c50c5f46c12..d4b60e65a252 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterAllCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterAllCallback.java @@ -34,8 +34,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeClassTemplateInvocationCallback}, + * {@link AfterClassTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -54,6 +56,8 @@ * @see AfterEachCallback * @see BeforeTestExecutionCallback * @see AfterTestExecutionCallback + * @see BeforeClassTemplateInvocationCallback + * @see AfterClassTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterClassTemplateInvocationCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterClassTemplateInvocationCallback.java new file mode 100644 index 000000000000..1a278418b2ef --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterClassTemplateInvocationCallback.java @@ -0,0 +1,74 @@ +/* + * 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.jupiter.api.ClassTemplate; + +/** + * {@code AfterClassTemplateInvocationCallback} defines the API for + * {@link Extension Extensions} that wish to provide additional behavior + * once before each invocation of a + * {@link ClassTemplate @ClassTemplate}. + * + *

Concrete implementations often implement + * {@link BeforeClassTemplateInvocationCallback} as well. + * + *

Constructor Requirements

+ * + *

Consult the documentation in {@link Extension} for details on + * constructor requirements. + * + *

Wrapping Behavior

+ * + *

JUnit Jupiter guarantees wrapping behavior for multiple + * registered extensions that implement lifecycle callbacks such as + * {@link BeforeAllCallback}, {@link AfterAllCallback}, + * {@link AfterClassTemplateInvocationCallback}, + * {@link AfterClassTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. + * + *

That means that, given two extensions {@code Extension1} and + * {@code Extension2} with {@code Extension1} registered before + * {@code Extension2}, any "before" callbacks implemented by {@code Extension1} + * are guaranteed to execute before any "before" callbacks implemented by + * {@code Extension2}. Similarly, given the two same two extensions registered + * in the same order, any "after" callbacks implemented by {@code Extension1} + * are guaranteed to execute after any "after" callbacks implemented by + * {@code Extension2}. {@code Extension1} is therefore said to wrap + * {@code Extension2}. + * + * @since 5.13 + * @see ClassTemplate + * @see BeforeClassTemplateInvocationCallback + * @see BeforeAllCallback + * @see AfterAllCallback + * @see BeforeEachCallback + * @see AfterEachCallback + * @see BeforeTestExecutionCallback + * @see AfterTestExecutionCallback + */ +@FunctionalInterface +@API(status = EXPERIMENTAL, since = "5.13") +public interface AfterClassTemplateInvocationCallback extends Extension { + + /** + * Callback that is invoked after each invocation of a container + * template. + * + * @param context the current extension context; never {@code null} + */ + void afterClassTemplateInvocation(ExtensionContext context) throws Exception; + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterEachCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterEachCallback.java index a1be4e60e374..7fb99759d7af 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterEachCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterEachCallback.java @@ -37,8 +37,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeClassTemplateInvocationCallback}, + * {@link AfterClassTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -57,6 +59,8 @@ * @see AfterTestExecutionCallback * @see BeforeAllCallback * @see AfterAllCallback + * @see BeforeClassTemplateInvocationCallback + * @see AfterClassTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterTestExecutionCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterTestExecutionCallback.java index 367985a93914..e537eb969713 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterTestExecutionCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterTestExecutionCallback.java @@ -38,8 +38,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeClassTemplateInvocationCallback}, + * {@link AfterClassTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -58,6 +60,8 @@ * @see AfterEachCallback * @see BeforeAllCallback * @see AfterAllCallback + * @see BeforeClassTemplateInvocationCallback + * @see AfterClassTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeAllCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeAllCallback.java index d546e0da035f..9a08dd8fb992 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeAllCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeAllCallback.java @@ -34,8 +34,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeClassTemplateInvocationCallback}, + * {@link AfterClassTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -54,6 +56,8 @@ * @see AfterEachCallback * @see BeforeTestExecutionCallback * @see AfterTestExecutionCallback + * @see BeforeClassTemplateInvocationCallback + * @see AfterClassTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeClassTemplateInvocationCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeClassTemplateInvocationCallback.java new file mode 100644 index 000000000000..e5c3b97bea72 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeClassTemplateInvocationCallback.java @@ -0,0 +1,74 @@ +/* + * 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.jupiter.api.ClassTemplate; + +/** + * {@code BeforeClassTemplateInvocationCallback} defines the API for + * {@link Extension Extensions} that wish to provide additional behavior + * once before each invocation of a + * {@link ClassTemplate @ClassTemplate}. + * + *

Concrete implementations often implement + * {@link AfterClassTemplateInvocationCallback} as well. + * + *

Constructor Requirements

+ * + *

Consult the documentation in {@link Extension} for details on + * constructor requirements. + * + *

Wrapping Behavior

+ * + *

JUnit Jupiter guarantees wrapping behavior for multiple + * registered extensions that implement lifecycle callbacks such as + * {@link BeforeAllCallback}, {@link AfterAllCallback}, + * {@link BeforeClassTemplateInvocationCallback}, + * {@link AfterClassTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. + * + *

That means that, given two extensions {@code Extension1} and + * {@code Extension2} with {@code Extension1} registered before + * {@code Extension2}, any "before" callbacks implemented by {@code Extension1} + * are guaranteed to execute before any "before" callbacks implemented by + * {@code Extension2}. Similarly, given the two same two extensions registered + * in the same order, any "after" callbacks implemented by {@code Extension1} + * are guaranteed to execute after any "after" callbacks implemented by + * {@code Extension2}. {@code Extension1} is therefore said to wrap + * {@code Extension2}. + * + * @since 5.13 + * @see ClassTemplate + * @see AfterClassTemplateInvocationCallback + * @see BeforeAllCallback + * @see AfterAllCallback + * @see BeforeEachCallback + * @see AfterEachCallback + * @see BeforeTestExecutionCallback + * @see AfterTestExecutionCallback + */ +@FunctionalInterface +@API(status = EXPERIMENTAL, since = "5.13") +public interface BeforeClassTemplateInvocationCallback extends Extension { + + /** + * Callback that is invoked before each invocation of a container + * template. + * + * @param context the current extension context; never {@code null} + */ + void beforeClassTemplateInvocation(ExtensionContext context) throws Exception; + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeEachCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeEachCallback.java index 6b23ad51df73..4239d7d37485 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeEachCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeEachCallback.java @@ -37,8 +37,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeClassTemplateInvocationCallback}, + * {@link AfterClassTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -57,6 +59,8 @@ * @see AfterTestExecutionCallback * @see BeforeAllCallback * @see AfterAllCallback + * @see BeforeClassTemplateInvocationCallback + * @see AfterClassTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.java index 56094d6a133f..bfd856007f36 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.java @@ -38,8 +38,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeClassTemplateInvocationCallback}, + * {@link AfterClassTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -58,6 +60,8 @@ * @see AfterEachCallback * @see BeforeAllCallback * @see AfterAllCallback + * @see BeforeClassTemplateInvocationCallback + * @see AfterClassTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationContext.java new file mode 100644 index 000000000000..00bb30b2ed61 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationContext.java @@ -0,0 +1,82 @@ +/* + * 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 java.util.Collections.emptyList; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.List; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.ClassTemplate; + +/** + * {@code ClassTemplateInvocationContext} represents the context of + * a single invocation of a {@link ClassTemplate @ClassTemplate}. + * + *

Each context is provided by a + * {@link ClassTemplateInvocationContextProvider}. + * + * @since 5.13 + * @see ClassTemplate + * @see ClassTemplateInvocationContextProvider + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface ClassTemplateInvocationContext { + + /** + * Get the display name for this invocation. + * + *

The supplied {@code invocationIndex} is incremented by the framework + * with each container invocation. Thus, in the case of multiple active + * {@linkplain ClassTemplateInvocationContextProvider providers}, only the + * first active provider receives indices starting with {@code 1}. + * + *

The default implementation returns the supplied {@code invocationIndex} + * wrapped in brackets — for example, {@code [1]}, {@code [42]}, etc. + * + * @param invocationIndex the index of this invocation (1-based). + * @return the display name for this invocation; never {@code null} or blank + */ + default String getDisplayName(int invocationIndex) { + return "[" + invocationIndex + "]"; + } + + /** + * Get the additional {@linkplain Extension extensions} for this invocation. + * + *

The extensions provided by this method will only be used for this + * invocation of the class template. Thus, it does not make sense to return + * an extension that acts solely on the container level (e.g. + * {@link BeforeAllCallback}). + * + *

The default implementation returns an empty list. + * + * @return the additional extensions for this invocation; never {@code null} + * or containing {@code null} elements, but potentially empty + */ + default List getAdditionalExtensions() { + return emptyList(); + } + + /** + * Prepare the imminent invocation of the class template. + * + *

This may be used, for example, to store entries in the + * {@link ExtensionContext.Store Store} to benefit from its cleanup support + * or for retrieval by other extensions. + * + * @param context The invocation-level extension context. + */ + default void prepareInvocation(ExtensionContext context) { + } + +} 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 new file mode 100644 index 000000000000..51a2edada45b --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationContextProvider.java @@ -0,0 +1,118 @@ +/* + * 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 java.util.stream.Stream; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.ClassTemplate; + +/** + * {@code ClassTemplateInvocationContextProvider} defines the API for + * {@link Extension Extensions} that wish to provide one or multiple contexts + * for the invocation of a {@link ClassTemplate @ClassTemplate}. + * + *

This extension point makes it possible to execute a class template in + * different contexts — for example, with different parameters, by + * preparing the test class instance differently, or multiple times without + * modifying the context. + * + *

This interface defines two main methods: + * {@link #supportsClassTemplate} and + * {@link #provideClassTemplateInvocationContexts}. The former is called by the + * framework to determine whether this extension wants to act on a container + * template that is about to be executed. If so, the latter is called and must + * return a {@link Stream} of {@link ClassTemplateInvocationContext} instances. + * Otherwise, this provider is ignored for the execution of the current class + * template. + * + *

A provider that has returned {@code true} from its + * {@link #supportsClassTemplate} method is called active. When + * multiple providers are active for a class template, the + * {@code Streams} returned by their + * {@link #provideClassTemplateInvocationContexts} methods will be chained, and + * the class template method will be invoked using the contexts of all active + * providers. + * + *

An active provider may return zero invocation contexts from its + * {@link #provideClassTemplateInvocationContexts} method if it overrides + * {@link #mayReturnZeroClassTemplateInvocationContexts} to return + * {@code true}. + * + *

Constructor Requirements

+ * + *

Consult the documentation in {@link Extension} for details on + * constructor requirements. + * + * @since 5.13 + * @see ClassTemplate + * @see ClassTemplateInvocationContext + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface ClassTemplateInvocationContextProvider extends Extension { + + /** + * Determine if this provider supports providing invocation contexts for the + * class template represented by the supplied {@code context}. + * + * @param context the extension context for the class template + * about to be invoked; never {@code null} + * @return {@code true} if this provider can provide invocation contexts + * @see #provideClassTemplateInvocationContexts + * @see ExtensionContext + */ + boolean supportsClassTemplate(ExtensionContext context); + + /** + * Provide {@linkplain ClassTemplateInvocationContext invocation contexts} + * for the class template represented by the supplied {@code context}. + * + *

This method is only called by the framework if + * {@link #supportsClassTemplate} previously returned {@code true} for the + * same {@link ExtensionContext}; this method is allowed to return an empty + * {@code Stream} but not {@code null}. + * + *

The returned {@code Stream} will be properly closed by calling + * {@link Stream#close()}, making it safe to use a resource such as + * {@link java.nio.file.Files#lines(java.nio.file.Path) Files.lines()}. + * + * @param context the extension context for the class template about to be + * invoked; never {@code null} + * @return a {@code Stream} of {@code ClassTemplateInvocationContext} + * instances for the invocation of the class template; never {@code null} + * @see #supportsClassTemplate + * @see ExtensionContext + */ + Stream provideClassTemplateInvocationContexts(ExtensionContext context); + + /** + * Signal that this provider may provide zero + * {@linkplain ClassTemplateInvocationContext invocation contexts} for + * the class template represented by the supplied {@code context}. + * + *

If this method returns {@code false} (which is the default) and the + * provider returns an empty stream from + * {@link #provideClassTemplateInvocationContexts}, this will be considered + * an execution error. Override this method to return {@code true} to ignore + * the absence of invocation contexts for this provider. + * + * @param context the extension context for the class template + * about to be invoked; never {@code null} + * @return {@code true} to allow zero contexts, {@code false} to fail + * execution in case of zero contexts + */ + default boolean mayReturnZeroClassTemplateInvocationContexts(ExtensionContext context) { + return false; + } + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationLifecycleMethod.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationLifecycleMethod.java new file mode 100644 index 000000000000..8f2dd238dd3d --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ClassTemplateInvocationLifecycleMethod.java @@ -0,0 +1,45 @@ +/* + * 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.INTERNAL; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * Internal marker annotation for lifecycle methods specific to implementations + * of {@link ClassTemplateInvocationContextProvider}. + * + * @since 5.13 + */ +@API(status = INTERNAL, since = "5.13") +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface ClassTemplateInvocationLifecycleMethod { + + /** + * The corresponding {@link org.junit.jupiter.api.ClassTemplate}-derived + * annotation class. + */ + Class classTemplateAnnotation(); + + /** + * The actual lifecycle method annotation class. + */ + Class lifecycleMethodAnnotation(); + +} 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 a2bfd4db7d80..6d047292a98d 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 @@ -40,6 +40,8 @@ *

{@link Extension Extensions} are provided an instance of * {@code ExtensionContext} to perform their work. * + *

This interface is not intended to be implemented by clients. + * * @since 5.0 * @see Store * @see Namespace @@ -129,6 +131,31 @@ public interface ExtensionContext { */ Optional> getTestClass(); + /** + * Get the enclosing test classes of the current test or container. + * + *

This method is useful to look up annotations on nested test classes + * and their enclosing runtime types: + * + *

{@code
+	 * AnnotationSupport.findAnnotation(
+	 *     extensionContext.getRequiredTestClass(),
+	 *     MyAnnotation.class,
+	 *     extensionContext.getEnclosingTestClasses()
+	 * );
+	 * }
+ * + * @return an empty list if there is no class associated with the current + * test or container or when it is not nested; otherwise, a list containing + * the enclosing test classes in order from outermost to innermost; never + * {@code null} + * + * @since 5.12.1 + * @see org.junit.platform.commons.support.AnnotationSupport#findAnnotation(Class, Class, List) + */ + @API(status = EXPERIMENTAL, since = "5.12.1") + List> getEnclosingTestClasses(); + /** * Get the required {@link Class} associated with the current test * or container. @@ -370,8 +397,8 @@ default void publishReportEntry(String value) { /** * Publish a file with the supplied name written by the supplied action and * attach it to the current test or container. - *

- * The file will be resolved in the report output directory prior to + * + *

The file will be resolved in the report output directory prior to * invoking the supplied action. * * @param name the name of the file to be attached; never {@code null} or @@ -388,8 +415,8 @@ default void publishReportEntry(String value) { /** * Publish a directory with the supplied name written by the supplied action * and attach it to the current test or container. - *

- * The directory will be resolved and created in the report output directory + * + *

The directory will be resolved and created in the report output directory * prior to invoking the supplied action. * * @param name the name of the directory to be attached; never {@code null} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/MediaType.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/MediaType.java index 00ad02dadfd2..82513d94bcc2 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/MediaType.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/MediaType.java @@ -86,8 +86,8 @@ public class MediaType { /** * Parse the given media type value. - *

- * Must be valid according to + * + *

Must be valid according to * RFC 2045. * * @param value the media type value to parse; never {@code null} @@ -142,8 +142,9 @@ public String toString() { @Override public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) + if (o == null || getClass() != o.getClass()) { return false; + } MediaType that = (MediaType) o; return Objects.equals(this.value, that.value); } @@ -152,4 +153,5 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hashCode(value); } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContext.java index 37a11b3b923d..1d74bed2dd7d 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContext.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContext.java @@ -11,6 +11,7 @@ package org.junit.jupiter.api.extension; import static java.util.Collections.emptyList; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.List; @@ -66,4 +67,18 @@ default List getAdditionalExtensions() { return emptyList(); } + /** + * Prepare the imminent invocation of the test template. + * + *

This may be used, for example, to store entries in the + * {@link ExtensionContext.Store Store} to benefit from its cleanup support + * or for retrieval by other extensions. + * + * @param context The invocation-level extension context. + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + default void prepareInvocation(ExtensionContext context) { + } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java index 24c85cb268c8..370e0c679a4f 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java @@ -21,6 +21,7 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.junit.jupiter.api.ClassTemplate; /** * {@code @ResourceLock} is used to declare that the annotated test class or test @@ -70,6 +71,10 @@ * attribute remains applicable, and the target of "dynamic" shared resources added * via implementations of {@link ResourceLocksProvider} is not changed. * + *

Shared resources declared on or provided for methods or nested test + * classes in a {@link ClassTemplate @ClassTemplate} are propagated as if they + * were declared on the outermost enclosing {@code @ClassTemplate} class itself. + * * @see Isolated * @see Resources * @see ResourceAccessMode diff --git a/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/EqualsAndHashCodeAssertions.java b/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/EqualsAndHashCodeAssertions.java index ed33d21ebf3d..7ca4175b0f58 100644 --- a/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/EqualsAndHashCodeAssertions.java +++ b/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/EqualsAndHashCodeAssertions.java @@ -16,7 +16,7 @@ * Assertions for unit tests that wish to test * {@link Object#equals(Object)} and {@link Object#hashCode()}. * - * @since 1.3 + * @since 5.3 */ public class EqualsAndHashCodeAssertions { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/CallbackSupport.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/CallbackSupport.java new file mode 100644 index 000000000000..c7c6cf0e34bf --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/CallbackSupport.java @@ -0,0 +1,59 @@ +/* + * 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.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder; + +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.jupiter.engine.extension.ExtensionRegistry; +import org.junit.platform.engine.support.hierarchical.ThrowableCollector; + +/** + * @since 5.13 + */ +class CallbackSupport { + + static void invokeBeforeCallbacks(Class type, JupiterEngineExecutionContext context, + CallbackInvoker callbackInvoker) { + + ExtensionRegistry registry = context.getExtensionRegistry(); + ExtensionContext extensionContext = context.getExtensionContext(); + ThrowableCollector throwableCollector = context.getThrowableCollector(); + + for (T callback : registry.getExtensions(type)) { + throwableCollector.execute(() -> callbackInvoker.invoke(callback, extensionContext)); + if (throwableCollector.isNotEmpty()) { + break; + } + } + } + + static void invokeAfterCallbacks(Class type, JupiterEngineExecutionContext context, + CallbackInvoker callbackInvoker) { + + ExtensionRegistry registry = context.getExtensionRegistry(); + ExtensionContext extensionContext = context.getExtensionContext(); + ThrowableCollector throwableCollector = context.getThrowableCollector(); + + forEachInReverseOrder(registry.getExtensions(type), // + callback -> throwableCollector.execute(() -> callbackInvoker.invoke(callback, extensionContext))); + } + + @FunctionalInterface + protected interface CallbackInvoker { + + void invoke(T t, ExtensionContext context) throws Throwable; + + } + +} 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 5c514362e8d9..eb4a5141c667 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 @@ -12,6 +12,8 @@ import static java.util.stream.Collectors.joining; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeAfterCallbacks; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeBeforeCallbacks; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromConstructorParameters; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters; @@ -23,7 +25,6 @@ import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findBeforeEachMethods; import static org.junit.jupiter.engine.descriptor.TestInstanceLifecycleUtils.getTestInstanceLifecycle; import static org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory.createThrowableCollector; -import static org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -70,10 +71,12 @@ import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.commons.util.StringUtils; 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.ClassSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; import org.junit.platform.engine.support.hierarchical.ThrowableCollector; /** @@ -82,78 +85,98 @@ * @since 5.5 */ @API(status = INTERNAL, since = "5.5") -public abstract class ClassBasedTestDescriptor extends JupiterTestDescriptor implements ResourceLockAware { +public abstract class ClassBasedTestDescriptor extends JupiterTestDescriptor + implements ResourceLockAware, TestClassAware, Validatable { private static final InterceptingExecutableInvoker executableInvoker = new InterceptingExecutableInvoker(); - private final Class testClass; - protected final Set tags; - protected final Lifecycle lifecycle; - private final ExclusiveResourceCollector exclusiveResourceCollector; + protected final ClassInfo classInfo; - private ExecutionMode defaultChildExecutionMode; + private LifecycleMethods lifecycleMethods; private TestInstanceFactory testInstanceFactory; - private List beforeAllMethods; - private List afterAllMethods; ClassBasedTestDescriptor(UniqueId uniqueId, Class testClass, Supplier displayNameSupplier, JupiterConfiguration configuration) { super(uniqueId, testClass, displayNameSupplier, ClassSource.from(testClass), configuration); - this.testClass = testClass; - this.tags = getTags(testClass); - this.lifecycle = getTestInstanceLifecycle(testClass, configuration); - this.defaultChildExecutionMode = (this.lifecycle == Lifecycle.PER_CLASS ? ExecutionMode.SAME_THREAD : null); - this.exclusiveResourceCollector = ExclusiveResourceCollector.from(testClass); + this.classInfo = new ClassInfo(testClass, configuration); + this.lifecycleMethods = new LifecycleMethods(this.classInfo); } - // --- TestDescriptor ------------------------------------------------------ + ClassBasedTestDescriptor(UniqueId uniqueId, Class testClass, String displayName, + JupiterConfiguration configuration) { + super(uniqueId, displayName, ClassSource.from(testClass), configuration); + + this.classInfo = new ClassInfo(testClass, configuration); + this.lifecycleMethods = new LifecycleMethods(this.classInfo); + } + + // --- TestClassAware ------------------------------------------------------ + @Override public final Class getTestClass() { - return this.testClass; + return this.classInfo.testClass; } - public abstract List> getEnclosingTestClasses(); + // --- TestDescriptor ------------------------------------------------------ @Override - public Type getType() { + public final Type getType() { return Type.CONTAINER; } @Override - public String getLegacyReportingName() { - return this.testClass.getName(); + public final String getLegacyReportingName() { + return getTestClass().getName(); + } + + // --- Validatable --------------------------------------------------------- + + @Override + public final void validate(DiscoveryIssueReporter reporter) { + validateCoreLifecycleMethods(reporter); + validateClassTemplateInvocationLifecycleMethods(reporter); + } + + protected void validateCoreLifecycleMethods(DiscoveryIssueReporter reporter) { + List discoveryIssues = this.lifecycleMethods.discoveryIssues; + discoveryIssues.forEach(reporter::reportIssue); + discoveryIssues.clear(); + } + + protected void validateClassTemplateInvocationLifecycleMethods(DiscoveryIssueReporter reporter) { + LifecycleMethodUtils.validateNoClassTemplateInvocationLifecycleMethodsAreDeclared(getTestClass(), reporter); } // --- Node ---------------------------------------------------------------- @Override - protected Optional getExplicitExecutionMode() { + protected final Optional getExplicitExecutionMode() { return getExecutionModeFromAnnotation(getTestClass()); } @Override - protected Optional getDefaultChildExecutionMode() { - return Optional.ofNullable(this.defaultChildExecutionMode); + protected final Optional getDefaultChildExecutionMode() { + return Optional.ofNullable(this.classInfo.defaultChildExecutionMode); } - public void setDefaultChildExecutionMode(ExecutionMode defaultChildExecutionMode) { - this.defaultChildExecutionMode = defaultChildExecutionMode; + public final void setDefaultChildExecutionMode(ExecutionMode defaultChildExecutionMode) { + this.classInfo.defaultChildExecutionMode = defaultChildExecutionMode; } @Override public final ExclusiveResourceCollector getExclusiveResourceCollector() { - return exclusiveResourceCollector; + return this.classInfo.exclusiveResourceCollector; } @Override - public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { + public final JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { MutableExtensionRegistry registry = populateNewExtensionRegistryFromExtendWithAnnotation( - context.getExtensionRegistry(), this.testClass); + context.getExtensionRegistry(), getTestClass()); // Register extensions from static fields here, at the class level but // after extensions registered via @ExtendWith. - registerExtensionsFromStaticFields(registry, this.testClass); + registerExtensionsFromStaticFields(registry, getTestClass()); // Resolve the TestInstanceFactory at the class level in order to fail // the entire class in case of configuration errors (e.g., more than @@ -161,13 +184,10 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte this.testInstanceFactory = resolveTestInstanceFactory(registry); if (this.testInstanceFactory == null) { - registerExtensionsFromConstructorParameters(registry, this.testClass); + registerExtensionsFromConstructorParameters(registry, getTestClass()); } - this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD); - this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD); - - this.beforeAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method)); + this.lifecycleMethods.beforeAll.forEach(method -> registerExtensionsFromExecutableParameters(registry, method)); // Since registerBeforeEachMethodAdapters() and registerAfterEachMethodAdapters() also // invoke registerExtensionsFromExecutableParameters(), we invoke those methods before // invoking registerExtensionsFromExecutableParameters() for @AfterAll methods, @@ -175,12 +195,12 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte // on parameters in lifecycle methods. registerBeforeEachMethodAdapters(registry); registerAfterEachMethodAdapters(registry); - this.afterAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method)); - registerExtensionsFromInstanceFields(registry, this.testClass); + this.lifecycleMethods.afterAll.forEach(method -> registerExtensionsFromExecutableParameters(registry, method)); + registerExtensionsFromInstanceFields(registry, getTestClass()); ThrowableCollector throwableCollector = createThrowableCollector(); ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(), - context.getExecutionListener(), this, this.lifecycle, context.getConfiguration(), registry, + context.getExecutionListener(), this, this.classInfo.lifecycle, context.getConfiguration(), registry, throwableCollector); // @formatter:off @@ -194,7 +214,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte } @Override - public JupiterEngineExecutionContext before(JupiterEngineExecutionContext context) { + public final JupiterEngineExecutionContext before(JupiterEngineExecutionContext context) { ThrowableCollector throwableCollector = context.getThrowableCollector(); if (isPerClassLifecycle(context)) { @@ -223,7 +243,7 @@ public JupiterEngineExecutionContext before(JupiterEngineExecutionContext contex } @Override - public void after(JupiterEngineExecutionContext context) { + public final void after(JupiterEngineExecutionContext context) { ThrowableCollector throwableCollector = context.getThrowableCollector(); Throwable previousThrowable = throwableCollector.getThrowable(); @@ -250,6 +270,13 @@ public void after(JupiterEngineExecutionContext context) { } } + @Override + public void cleanUp(JupiterEngineExecutionContext context) throws Exception { + super.cleanUp(context); + this.lifecycleMethods = null; + this.testInstanceFactory = null; + } + private TestInstanceFactory resolveTestInstanceFactory(ExtensionRegistry registry) { List factories = registry.getExtensions(TestInstanceFactory.class); @@ -264,7 +291,7 @@ private TestInstanceFactory resolveTestInstanceFactory(ExtensionRegistry registr String errorMessage = String.format( "The following TestInstanceFactory extensions were registered for test class [%s], but only one is permitted: %s", - testClass.getName(), factoryNames); + getTestClass().getName(), factoryNames); throw new ExtensionConfigurationException(errorMessage); } @@ -294,7 +321,7 @@ private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecuti // In addition, we initialize extension registered programmatically from instance fields here // since the best time to do that is immediately following test class instantiation // and post-processing. - context.getExtensionRegistry().initializeExtensions(this.testClass, instances.getInnermostInstance()); + context.getExtensionRegistry().initializeExtensions(getTestClass(), instances.getInnermostInstance()); }); return instances; } @@ -303,17 +330,17 @@ protected abstract TestInstances instantiateTestClass(JupiterEngineExecutionCont ExtensionContextSupplier extensionContext, ExtensionRegistry registry, JupiterEngineExecutionContext context); - protected TestInstances instantiateTestClass(Optional outerInstances, ExtensionRegistry registry, - ExtensionContextSupplier extensionContext) { + protected final TestInstances instantiateTestClass(Optional outerInstances, + ExtensionRegistry registry, ExtensionContextSupplier extensionContext) { Optional outerInstance = outerInstances.map(TestInstances::getInnermostInstance); - invokeTestInstancePreConstructCallbacks(new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), + invokeTestInstancePreConstructCallbacks(new DefaultTestInstanceFactoryContext(getTestClass(), outerInstance), registry, extensionContext); Object instance = this.testInstanceFactory != null // ? invokeTestInstanceFactory(outerInstance, extensionContext) // : invokeTestClassConstructor(outerInstance, registry, extensionContext); - return outerInstances.map(instances -> DefaultTestInstances.of(instances, instance)).orElse( - DefaultTestInstances.of(instance)); + return outerInstances.map(instances -> DefaultTestInstances.of(instances, instance)) // + .orElse(DefaultTestInstances.of(instance)); } private Object invokeTestInstanceFactory(Optional outerInstance, @@ -323,7 +350,7 @@ private Object invokeTestInstanceFactory(Optional outerInstance, try { ExtensionContext actualExtensionContext = extensionContext.get(this.testInstanceFactory); instance = this.testInstanceFactory.createTestInstance( - new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), actualExtensionContext); + new DefaultTestInstanceFactoryContext(getTestClass(), outerInstance), actualExtensionContext); } catch (Throwable throwable) { UnrecoverableExceptions.rethrowIfUnrecoverable(throwable); @@ -333,15 +360,15 @@ private Object invokeTestInstanceFactory(Optional outerInstance, } String message = String.format("TestInstanceFactory [%s] failed to instantiate test class [%s]", - this.testInstanceFactory.getClass().getName(), this.testClass.getName()); + this.testInstanceFactory.getClass().getName(), getTestClass().getName()); if (StringUtils.isNotBlank(throwable.getMessage())) { message += ": " + throwable.getMessage(); } throw new TestInstantiationException(message, throwable); } - if (!this.testClass.isInstance(instance)) { - String testClassName = this.testClass.getName(); + if (!getTestClass().isInstance(instance)) { + String testClassName = getTestClass().getName(); Class instanceClass = (instance == null ? null : instance.getClass()); String instanceClassName = (instanceClass == null ? "null" : instanceClass.getName()); @@ -349,7 +376,7 @@ private Object invokeTestInstanceFactory(Optional outerInstance, // the identity hash codes to the type names to help users disambiguate // between otherwise identical "fully qualified class names". if (testClassName.equals(instanceClassName)) { - testClassName += "@" + Integer.toHexString(System.identityHashCode(this.testClass)); + testClassName += "@" + Integer.toHexString(System.identityHashCode(getTestClass())); instanceClassName += "@" + Integer.toHexString(System.identityHashCode(instanceClass)); } String message = String.format( @@ -365,7 +392,7 @@ private Object invokeTestInstanceFactory(Optional outerInstance, private Object invokeTestClassConstructor(Optional outerInstance, ExtensionRegistry registry, ExtensionContextSupplier extensionContext) { - Constructor constructor = ReflectionUtils.getDeclaredConstructor(this.testClass); + Constructor constructor = ReflectionUtils.getDeclaredConstructor(getTestClass()); return executableInvoker.invoke(constructor, outerInstance, extensionContext, registry, InvocationInterceptor::interceptTestClassConstructor); } @@ -393,16 +420,7 @@ private void executeAndMaskThrowable(Executable executable) { } private void invokeBeforeAllCallbacks(JupiterEngineExecutionContext context) { - ExtensionRegistry registry = context.getExtensionRegistry(); - ExtensionContext extensionContext = context.getExtensionContext(); - ThrowableCollector throwableCollector = context.getThrowableCollector(); - - for (BeforeAllCallback callback : registry.getExtensions(BeforeAllCallback.class)) { - throwableCollector.execute(() -> callback.beforeAll(extensionContext)); - if (throwableCollector.isNotEmpty()) { - break; - } - } + invokeBeforeCallbacks(BeforeAllCallback.class, context, BeforeAllCallback::beforeAll); } private void invokeBeforeAllMethods(JupiterEngineExecutionContext context) { @@ -411,7 +429,7 @@ private void invokeBeforeAllMethods(JupiterEngineExecutionContext context) { ThrowableCollector throwableCollector = context.getThrowableCollector(); Object testInstance = extensionContext.getTestInstance().orElse(null); - for (Method method : this.beforeAllMethods) { + for (Method method : this.lifecycleMethods.beforeAll) { throwableCollector.execute(() -> { try { executableInvoker.invoke(method, testInstance, extensionContext, registry, @@ -440,7 +458,7 @@ private void invokeAfterAllMethods(JupiterEngineExecutionContext context) { ThrowableCollector throwableCollector = context.getThrowableCollector(); Object testInstance = extensionContext.getTestInstance().orElse(null); - this.afterAllMethods.forEach(method -> throwableCollector.execute(() -> { + this.lifecycleMethods.afterAll.forEach(method -> throwableCollector.execute(() -> { try { executableInvoker.invoke(method, testInstance, extensionContext, registry, ReflectiveInterceptorCall.ofVoidMethod(InvocationInterceptor::interceptAfterAllMethod)); @@ -459,20 +477,12 @@ private void invokeAfterAllMethodExecutionExceptionHandlers(ExtensionRegistry re } private void invokeAfterAllCallbacks(JupiterEngineExecutionContext context) { - ExtensionRegistry registry = context.getExtensionRegistry(); - ExtensionContext extensionContext = context.getExtensionContext(); - ThrowableCollector throwableCollector = context.getThrowableCollector(); - - forEachInReverseOrder(registry.getExtensions(AfterAllCallback.class), // - extension -> throwableCollector.execute(() -> extension.afterAll(extensionContext))); + invokeAfterCallbacks(AfterAllCallback.class, context, AfterAllCallback::afterAll); } private void invokeTestInstancePreDestroyCallbacks(JupiterEngineExecutionContext context) { - ExtensionContext extensionContext = context.getExtensionContext(); - ThrowableCollector throwableCollector = context.getThrowableCollector(); - - forEachInReverseOrder(context.getExtensionRegistry().getExtensions(TestInstancePreDestroyCallback.class), // - extension -> throwableCollector.execute(() -> extension.preDestroyTestInstance(extensionContext))); + invokeAfterCallbacks(TestInstancePreDestroyCallback.class, context, + TestInstancePreDestroyCallback::preDestroyTestInstance); } private boolean isPerClassLifecycle(JupiterEngineExecutionContext context) { @@ -481,13 +491,13 @@ private boolean isPerClassLifecycle(JupiterEngineExecutionContext context) { } private void registerBeforeEachMethodAdapters(ExtensionRegistrar registrar) { - List beforeEachMethods = findBeforeEachMethods(this.testClass); - registerMethodsAsExtensions(beforeEachMethods, registrar, this::synthesizeBeforeEachMethodAdapter); + registerMethodsAsExtensions(this.lifecycleMethods.beforeEach, registrar, + this::synthesizeBeforeEachMethodAdapter); } private void registerAfterEachMethodAdapters(ExtensionRegistrar registrar) { // Make a local copy since findAfterEachMethods() returns an immutable list. - List afterEachMethods = new ArrayList<>(findAfterEachMethods(this.testClass)); + List afterEachMethods = new ArrayList<>(this.lifecycleMethods.afterEach); // Since the bottom-up ordering of afterEachMethods will later be reversed when the // synthesized AfterEachMethodAdapters are executed within TestMethodTestDescriptor, @@ -520,11 +530,47 @@ private AfterEachMethodAdapter synthesizeAfterEachMethodAdapter(Method method) { private void invokeMethodInExtensionContext(Method method, ExtensionContext context, ExtensionRegistry registry, VoidMethodInterceptorCall interceptorCall) { TestInstances testInstances = context.getRequiredTestInstances(); - Object target = testInstances.findInstance(this.testClass).orElseThrow( + Object target = testInstances.findInstance(getTestClass()).orElseThrow( () -> new JUnitException("Failed to find instance for method: " + method.toGenericString())); executableInvoker.invoke(method, target, context, registry, ReflectiveInterceptorCall.ofVoidMethod(interceptorCall)); } + protected static class ClassInfo { + + final Class testClass; + final Set tags; + final Lifecycle lifecycle; + ExecutionMode defaultChildExecutionMode; + final ExclusiveResourceCollector exclusiveResourceCollector; + + ClassInfo(Class testClass, JupiterConfiguration configuration) { + this.testClass = testClass; + this.tags = getTags(testClass); + this.lifecycle = getTestInstanceLifecycle(testClass, configuration); + this.defaultChildExecutionMode = (this.lifecycle == Lifecycle.PER_CLASS ? ExecutionMode.SAME_THREAD : null); + this.exclusiveResourceCollector = ExclusiveResourceCollector.from(testClass); + } + } + + private static class LifecycleMethods { + + private final List discoveryIssues = new ArrayList<>(); + + private final List beforeAll; + private final List afterAll; + private final List beforeEach; + private final List afterEach; + + 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); + } + } + } 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 aace0e86e16d..9350bfe4ea2d 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 @@ -12,6 +12,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -34,20 +35,6 @@ final class ClassExtensionContext extends AbstractExtensionContext> getTestClass() { return Optional.of(getTestDescriptor().getTestClass()); } + @Override + public List> getEnclosingTestClasses() { + return getTestDescriptor().getEnclosingTestClasses(); + } + @Override public Optional getTestInstanceLifecycle() { return Optional.of(this.lifecycle); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java new file mode 100644 index 000000000000..d04824dd223b --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java @@ -0,0 +1,83 @@ +/* + * 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 java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstances; +import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.extension.ExtensionRegistry; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.support.hierarchical.Node; + +/** + * @since 5.13 + */ +final class ClassTemplateInvocationExtensionContext + extends AbstractExtensionContext { + + ClassTemplateInvocationExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, + ClassTemplateInvocationTestDescriptor testDescriptor, JupiterConfiguration configuration, + ExtensionRegistry extensionRegistry) { + super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry); + } + + @Override + public Optional getElement() { + return Optional.of(getTestDescriptor().getTestClass()); + } + + @Override + public Optional> getTestClass() { + return Optional.of(getTestDescriptor().getTestClass()); + } + + @Override + public List> getEnclosingTestClasses() { + return getTestDescriptor().getEnclosingTestClasses(); + } + + @Override + public Optional getTestInstanceLifecycle() { + return getParent().flatMap(ExtensionContext::getTestInstanceLifecycle); + } + + @Override + public Optional getTestInstance() { + return getParent().flatMap(ExtensionContext::getTestInstance); + } + + @Override + public Optional getTestInstances() { + return getParent().flatMap(ExtensionContext::getTestInstances); + } + + @Override + public Optional getTestMethod() { + return Optional.empty(); + } + + @Override + public Optional getExecutionException() { + return Optional.empty(); + } + + @Override + protected Node.ExecutionMode getPlatformExecutionMode() { + return getTestDescriptor().getExecutionMode(); + } + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationTestDescriptor.java new file mode 100644 index 000000000000..2ca97029dc0b --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationTestDescriptor.java @@ -0,0 +1,179 @@ +/* + * 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 static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeAfterCallbacks; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeBeforeCallbacks; +import static org.junit.jupiter.engine.extension.MutableExtensionRegistry.createRegistryFrom; +import static org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory.createThrowableCollector; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.AfterClassTemplateInvocationCallback; +import org.junit.jupiter.api.extension.BeforeClassTemplateInvocationCallback; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.parallel.ResourceLocksProvider; +import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.jupiter.engine.extension.MutableExtensionRegistry; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.hierarchical.ThrowableCollector; + +/** + * @since 5.13 + */ +@API(status = INTERNAL, since = "5.13") +public class ClassTemplateInvocationTestDescriptor extends JupiterTestDescriptor + implements TestClassAware, ResourceLockAware { + + public static final String SEGMENT_TYPE = "class-template-invocation"; + + private final ClassTemplateTestDescriptor parent; + private ClassTemplateInvocationContext invocationContext; + private final int index; + + public ClassTemplateInvocationTestDescriptor(UniqueId uniqueId, ClassTemplateTestDescriptor parent, + ClassTemplateInvocationContext invocationContext, int index, TestSource source, + JupiterConfiguration configuration) { + super(uniqueId, invocationContext.getDisplayName(index), source, configuration); + this.parent = parent; + this.invocationContext = invocationContext; + this.index = index; + } + + public int getIndex() { + return index; + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected ClassTemplateInvocationTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new ClassTemplateInvocationTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), parent, + this.invocationContext, this.index, getSource().orElse(null), this.configuration); + } + + // --- TestDescriptor ------------------------------------------------------ + + @Override + public Type getType() { + return Type.CONTAINER; + } + + @Override + public String getLegacyReportingName() { + return getTestClass().getName() + "[" + index + "]"; + } + + // --- TestClassAware ------------------------------------------------------ + + @Override + public Class getTestClass() { + return parent.getTestClass(); + } + + @Override + public List> getEnclosingTestClasses() { + return parent.getEnclosingTestClasses(); + } + + // --- ResourceLockAware --------------------------------------------------- + + @Override + public ExclusiveResourceCollector getExclusiveResourceCollector() { + return parent.getExclusiveResourceCollector(); + } + + @Override + public Function> getResourceLocksProviderEvaluator() { + return parent.getResourceLocksProviderEvaluator(); + } + + // --- Node ---------------------------------------------------------------- + + @Override + public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { + MutableExtensionRegistry registry = context.getExtensionRegistry(); + List additionalExtensions = this.invocationContext.getAdditionalExtensions(); + if (!additionalExtensions.isEmpty()) { + MutableExtensionRegistry childRegistry = createRegistryFrom(registry, Stream.empty()); + additionalExtensions.forEach( + extension -> childRegistry.registerExtension(extension, this.invocationContext)); + registry = childRegistry; + } + ExtensionContext extensionContext = new ClassTemplateInvocationExtensionContext(context.getExtensionContext(), + context.getExecutionListener(), this, context.getConfiguration(), registry); + ThrowableCollector throwableCollector = createThrowableCollector(); + throwableCollector.execute(() -> this.invocationContext.prepareInvocation(extensionContext)); + return context.extend() // + .withExtensionRegistry(registry) // + .withExtensionContext(extensionContext) // + .withThrowableCollector(throwableCollector) // + .build(); + } + + @Override + public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) { + context.getThrowableCollector().assertEmpty(); + return SkipResult.doNotSkip(); + } + + @Override + public JupiterEngineExecutionContext before(JupiterEngineExecutionContext context) throws Exception { + invokeBeforeCallbacks(BeforeClassTemplateInvocationCallback.class, context, + BeforeClassTemplateInvocationCallback::beforeClassTemplateInvocation); + context.getThrowableCollector().assertEmpty(); + return context; + } + + @Override + public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context, + DynamicTestExecutor dynamicTestExecutor) throws Exception { + Visitor visitor = context.getExecutionListener()::dynamicTestRegistered; + getChildren().forEach(child -> child.accept(visitor)); + return context; + } + + @Override + public void after(JupiterEngineExecutionContext context) throws Exception { + + ThrowableCollector throwableCollector = context.getThrowableCollector(); + Throwable previousThrowable = throwableCollector.getThrowable(); + + invokeAfterCallbacks(AfterClassTemplateInvocationCallback.class, context, + AfterClassTemplateInvocationCallback::afterClassTemplateInvocation); + + // If the previous Throwable was not null when this method was called, + // that means an exception was already thrown either before or during + // the execution of this Node. If an exception was already thrown, any + // later exceptions were added as suppressed exceptions to that original + // exception unless a more severe exception occurred in the meantime. + if (previousThrowable != throwableCollector.getThrowable()) { + throwableCollector.assertEmpty(); + } + } + + @Override + public void cleanUp(JupiterEngineExecutionContext context) throws Exception { + // forget invocationContext so it can be garbage collected + this.invocationContext = null; + super.cleanUp(context); + } +} 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 new file mode 100644 index 000000000000..b3be1dfa5df1 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateTestDescriptor.java @@ -0,0 +1,278 @@ +/* + * 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 java.util.stream.Collectors.toCollection; +import static java.util.stream.Collectors.toList; +import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; +import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.validateClassTemplateInvocationLifecycleMethodsAreDeclaredCorrectly; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.ClassTemplate; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstances; +import org.junit.jupiter.api.parallel.ResourceLocksProvider; +import org.junit.jupiter.engine.execution.ExtensionContextSupplier; +import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.jupiter.engine.extension.ExtensionRegistry; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource; +import org.junit.platform.engine.support.hierarchical.Node; + +/** + * @since 5.13 + */ +@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 NESTED_CLASS_SEGMENT_TYPE = "nested-class-template"; + + private final Map> childrenPrototypesByIndex = new HashMap<>(); + private final List childrenPrototypes = new ArrayList<>(); + private final ClassBasedTestDescriptor delegate; + private final DynamicDescendantFilter dynamicDescendantFilter; + + public ClassTemplateTestDescriptor(UniqueId uniqueId, ClassBasedTestDescriptor delegate) { + this(uniqueId, delegate, new DynamicDescendantFilter()); + } + + private ClassTemplateTestDescriptor(UniqueId uniqueId, ClassBasedTestDescriptor delegate, + DynamicDescendantFilter dynamicDescendantFilter) { + super(uniqueId, delegate.getTestClass(), delegate.getDisplayName(), delegate.configuration); + this.delegate = delegate; + this.dynamicDescendantFilter = dynamicDescendantFilter; + } + + // --- TestDescriptor ------------------------------------------------------ + + @Override + public Set getTags() { + return this.delegate.getTags(); + } + + // --- Validatable --------------------------------------------------------- + + @Override + protected void validateCoreLifecycleMethods(DiscoveryIssueReporter reporter) { + this.delegate.validateCoreLifecycleMethods(reporter); + } + + @Override + protected void validateClassTemplateInvocationLifecycleMethods(DiscoveryIssueReporter reporter) { + boolean requireStatic = this.classInfo.lifecycle == PER_METHOD; + validateClassTemplateInvocationLifecycleMethodsAreDeclaredCorrectly(getTestClass(), requireStatic, reporter); + } + + // --- Filterable ---------------------------------------------------------- + + @Override + public DynamicDescendantFilter getDynamicDescendantFilter() { + return this.dynamicDescendantFilter; + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected JupiterTestDescriptor copyIncludingDescendants(UnaryOperator uniqueIdTransformer) { + ClassTemplateTestDescriptor copy = (ClassTemplateTestDescriptor) super.copyIncludingDescendants( + uniqueIdTransformer); + this.childrenPrototypes.forEach(oldChild -> { + TestDescriptor newChild = ((JupiterTestDescriptor) oldChild).copyIncludingDescendants(uniqueIdTransformer); + copy.childrenPrototypes.add(newChild); + }); + this.childrenPrototypesByIndex.forEach((index, oldChildren) -> { + List newChildren = oldChildren.stream() // + .map(oldChild -> ((JupiterTestDescriptor) oldChild).copyIncludingDescendants(uniqueIdTransformer)) // + .collect(toList()); + copy.childrenPrototypesByIndex.put(index, newChildren); + }); + return copy; + } + + @Override + protected ClassTemplateTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new ClassTemplateTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), this.delegate, + this.dynamicDescendantFilter.copy(uniqueIdTransformer)); + } + + // --- TestDescriptor ------------------------------------------------------ + + @Override + public void prune() { + super.prune(); + if (this.children.isEmpty()) { + return; + } + // Create copy to avoid ConcurrentModificationException + new LinkedHashSet<>(this.children).forEach(child -> child.accept(TestDescriptor::prune)); + // Second iteration to avoid processing children that were pruned in the first iteration + this.children.forEach(child -> { + if (child instanceof ClassTemplateInvocationTestDescriptor) { + int index = ((ClassTemplateInvocationTestDescriptor) child).getIndex(); + this.dynamicDescendantFilter.allowIndex(index - 1); + this.childrenPrototypesByIndex.put(index, child.getChildren()); + } + else { + this.childrenPrototypes.add(child); + } + }); + this.children.clear(); + } + + @Override + public boolean mayRegisterTests() { + return !childrenPrototypes.isEmpty() || !childrenPrototypesByIndex.isEmpty(); + } + + // --- TestClassAware ------------------------------------------------------ + + @Override + public List> getEnclosingTestClasses() { + return delegate.getEnclosingTestClasses(); + } + + // --- ClassBasedTestDescriptor -------------------------------------------- + + @Override + public TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, + ExtensionContextSupplier extensionContext, ExtensionRegistry registry, + JupiterEngineExecutionContext context) { + return delegate.instantiateTestClass(parentExecutionContext, extensionContext, registry, context); + } + + // --- ResourceLockAware --------------------------------------------------- + + @Override + public Function> getResourceLocksProviderEvaluator() { + return delegate.getResourceLocksProviderEvaluator(); + } + + // --- Node ---------------------------------------------------------------- + + @Override + public Set getExclusiveResources() { + Set result = determineExclusiveResources().collect(toCollection(HashSet::new)); + Visitor visitor = testDescriptor -> { + if (testDescriptor instanceof Node) { + result.addAll(((Node) testDescriptor).getExclusiveResources()); + } + }; + this.childrenPrototypes.forEach(child -> child.accept(visitor)); + this.childrenPrototypesByIndex.values() // + .forEach(prototypes -> prototypes // + .forEach(child -> child.accept(visitor))); + return result; + } + + @Override + public void cleanUp(JupiterEngineExecutionContext context) throws Exception { + this.childrenPrototypes.clear(); + this.childrenPrototypesByIndex.clear(); + this.dynamicDescendantFilter.allowAll(); + super.cleanUp(context); + } + + @Override + public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context, + DynamicTestExecutor dynamicTestExecutor) throws Exception { + + new ClassTemplateExecutor().execute(context, dynamicTestExecutor); + return context; + } + + class ClassTemplateExecutor + extends TemplateExecutor { + + public ClassTemplateExecutor() { + super(ClassTemplateTestDescriptor.this, ClassTemplateInvocationContextProvider.class); + } + + @Override + boolean supports(ClassTemplateInvocationContextProvider provider, ExtensionContext extensionContext) { + return provider.supportsClassTemplate(extensionContext); + } + + @Override + protected String getNoRegisteredProviderErrorMessage() { + return String.format("You must register at least one %s that supports @%s class [%s]", + ClassTemplateInvocationContextProvider.class.getSimpleName(), ClassTemplate.class.getSimpleName(), + getTestClass().getName()); + } + + @Override + Stream provideContexts( + ClassTemplateInvocationContextProvider provider, ExtensionContext extensionContext) { + return provider.provideClassTemplateInvocationContexts(extensionContext); + } + + @Override + boolean mayReturnZeroContexts(ClassTemplateInvocationContextProvider provider, + ExtensionContext extensionContext) { + return provider.mayReturnZeroClassTemplateInvocationContexts(extensionContext); + } + + @Override + protected String getZeroContextsProvidedErrorMessage(ClassTemplateInvocationContextProvider provider) { + return String.format( + "Provider [%s] did not provide any invocation contexts, but was expected to do so. " + + "You may override mayReturnZeroClassTemplateInvocationContexts() to allow this.", + provider.getClass().getSimpleName()); + } + + @Override + UniqueId createInvocationUniqueId(UniqueId parentUniqueId, int index) { + return parentUniqueId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#" + index); + } + + @Override + TestDescriptor createInvocationTestDescriptor(UniqueId uniqueId, + ClassTemplateInvocationContext invocationContext, int index) { + ClassTemplateInvocationTestDescriptor containerInvocationDescriptor = new ClassTemplateInvocationTestDescriptor( + uniqueId, ClassTemplateTestDescriptor.this, invocationContext, index, getSource().orElse(null), + ClassTemplateTestDescriptor.this.configuration); + + collectChildren(index, uniqueId) // + .forEach(containerInvocationDescriptor::addChild); + + return containerInvocationDescriptor; + } + + private Stream collectChildren(int index, UniqueId invocationUniqueId) { + if (ClassTemplateTestDescriptor.this.childrenPrototypesByIndex.containsKey(index)) { + return ClassTemplateTestDescriptor.this.childrenPrototypesByIndex.remove(index).stream(); + } + UnaryOperator transformer = new UniqueIdPrefixTransformer(getUniqueId(), invocationUniqueId); + return ClassTemplateTestDescriptor.this.childrenPrototypes.stream() // + .map(JupiterTestDescriptor.class::cast) // + .map(it -> it.copyIncludingDescendants(transformer)); + } + } + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java index e935db38e782..6cd75e33efc8 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.TestInstances; @@ -51,14 +52,29 @@ public ClassTestDescriptor(UniqueId uniqueId, Class testClass, JupiterConfigu super(uniqueId, testClass, createDisplayNameSupplierForClass(testClass, configuration), configuration); } + private ClassTestDescriptor(UniqueId uniqueId, Class testClass, String displayName, + JupiterConfiguration configuration) { + super(uniqueId, testClass, displayName, configuration); + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected ClassTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new ClassTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getTestClass(), getDisplayName(), + configuration); + } + // --- TestDescriptor ------------------------------------------------------ @Override public Set getTags() { // return modifiable copy - return new LinkedHashSet<>(this.tags); + return new LinkedHashSet<>(this.classInfo.tags); } + // --- TestClassAware ------------------------------------------------------ + @Override public List> getEnclosingTestClasses() { return emptyList(); @@ -72,6 +88,8 @@ public ExecutionMode getExecutionMode() { () -> JupiterTestDescriptor.toExecutionMode(configuration.getDefaultClassesExecutionMode())); } + // --- ClassBasedTestDescriptor -------------------------------------------- + @Override protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, ExtensionContextSupplier extensionContext, ExtensionRegistry registry, @@ -79,6 +97,8 @@ protected TestInstances instantiateTestClass(JupiterEngineExecutionContext paren return instantiateTestClass(Optional.empty(), registry, extensionContext); } + // --- ResourceLockAware --------------------------------------------------- + @Override public Function> getResourceLocksProviderEvaluator() { return provider -> provider.provideForClass(getTestClass()); 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 05f7ae7d1977..b61ba1586de1 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 @@ -82,7 +82,7 @@ static String determineDisplayName(AnnotatedElement element, Supplier di // 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-empty value.", element)); + "Configuration error: @DisplayName on [%s] must be declared with a non-blank value.", element)); } else { return displayName; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java index 96b7d6e132ba..8f157b9f8726 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java @@ -14,6 +14,7 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.junit.jupiter.api.DynamicContainer; @@ -46,6 +47,12 @@ class DynamicContainerTestDescriptor extends DynamicNodeTestDescriptor { this.dynamicDescendantFilter = dynamicDescendantFilter; } + @Override + protected DynamicContainerTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new DynamicContainerTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), this.index, + this.dynamicContainer, this.testSource, this.dynamicDescendantFilter, this.configuration); + } + @Override public Type getType() { return Type.CONTAINER; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicDescendantFilter.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicDescendantFilter.java index 15b059ca47b4..ded323498b1a 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicDescendantFilter.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicDescendantFilter.java @@ -15,6 +15,7 @@ import java.util.HashSet; import java.util.Set; import java.util.function.BiPredicate; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; import org.junit.platform.engine.TestDescriptor; @@ -40,6 +41,12 @@ public void allowUniqueIdPrefix(UniqueId uniqueId) { } } + public void allowIndex(int index) { + if (this.mode == Mode.EXPLICIT) { + this.allowedIndices.add(index); + } + } + public void allowIndex(Set indices) { if (this.mode == Mode.EXPLICIT) { this.allowedIndices.addAll(indices); @@ -79,6 +86,18 @@ private enum Mode { EXPLICIT, ALLOW_ALL } + public DynamicDescendantFilter copy(UnaryOperator uniqueIdTransformer) { + return configure(uniqueIdTransformer, new DynamicDescendantFilter()); + } + + protected DynamicDescendantFilter configure(UnaryOperator uniqueIdTransformer, + DynamicDescendantFilter copy) { + this.allowedUniqueIds.stream().map(uniqueIdTransformer).forEach(copy.allowedUniqueIds::add); + copy.allowedIndices.addAll(this.allowedIndices); + copy.mode = this.mode; + return copy; + } + private class WithoutIndexFiltering extends DynamicDescendantFilter { @Override @@ -90,5 +109,10 @@ public boolean test(UniqueId uniqueId, Integer index) { public DynamicDescendantFilter withoutIndexFiltering() { return this; } + + @Override + public DynamicDescendantFilter copy(UnaryOperator uniqueIdTransformer) { + return configure(uniqueIdTransformer, new WithoutIndexFiltering()); + } } } 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 cd3c292709e2..fc87ef3f3a68 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 @@ -10,8 +10,11 @@ package org.junit.jupiter.engine.descriptor; +import static java.util.Collections.emptyList; + import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.TestInstance; @@ -40,6 +43,11 @@ public Optional> getTestClass() { return Optional.empty(); } + @Override + public List> getEnclosingTestClasses() { + return emptyList(); + } + @Override public Optional getTestInstanceLifecycle() { return Optional.empty(); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java index c5e83da28df1..9ed1f81ca6b1 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java @@ -11,6 +11,7 @@ package org.junit.jupiter.engine.descriptor; import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; import org.junit.platform.engine.TestDescriptor; @@ -24,7 +25,7 @@ */ abstract class DynamicNodeTestDescriptor extends JupiterTestDescriptor { - private final int index; + protected final int index; DynamicNodeTestDescriptor(UniqueId uniqueId, int index, DynamicNode dynamicNode, TestSource testSource, JupiterConfiguration configuration) { @@ -44,7 +45,7 @@ public String getLegacyReportingName() { @Override public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { - DynamicExtensionContext extensionContext = new DynamicExtensionContext(context.getExtensionContext(), + ExtensionContext extensionContext = new DynamicExtensionContext(context.getExtensionContext(), context.getExecutionListener(), this, context.getConfiguration(), context.getExtensionRegistry()); // @formatter:off return context.extend() diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java index e504b311a5df..9209b8c1f3d6 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java @@ -10,6 +10,8 @@ package org.junit.jupiter.engine.descriptor; +import java.util.function.UnaryOperator; + import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.extension.DynamicTestInvocationContext; import org.junit.jupiter.api.extension.ExtensionContext; @@ -41,6 +43,12 @@ class DynamicTestTestDescriptor extends DynamicNodeTestDescriptor { this.dynamicTest = dynamicTest; } + @Override + protected DynamicTestTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new DynamicTestTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), this.index, this.dynamicTest, + this.getSource().orElse(null), this.configuration); + } + @Override public Type getType() { return Type.TEST; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java index 6ecd845d6ded..66ba8a94c7ac 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java @@ -10,8 +10,11 @@ package org.junit.jupiter.engine.descriptor; +import static java.util.Collections.emptyList; + import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -43,6 +46,11 @@ public Optional> getTestClass() { return Optional.empty(); } + @Override + public List> getEnclosingTestClasses() { + return emptyList(); + } + @Override public Optional getTestInstanceLifecycle() { return Optional.empty(); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java index 0101f9d2b5c6..914971a37144 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java @@ -26,11 +26,13 @@ import java.util.Optional; import java.util.Set; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.ConditionEvaluator; @@ -187,7 +189,7 @@ public Set getExclusiveResources() { } @Override - public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) throws Exception { + public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) { context.getThrowableCollector().assertEmpty(); ConditionEvaluationResult evaluationResult = conditionEvaluator.evaluate(context.getExtensionRegistry(), context.getConfiguration(), context.getExtensionContext()); @@ -202,7 +204,8 @@ private SkipResult toSkipResult(ConditionEvaluationResult evaluationResult) { } /** - * Must be overridden and return a new context so cleanUp() does not accidentally close the parent context. + * Must be overridden and return a new context with a new {@link ExtensionContext} + * so cleanUp() does not accidentally close the parent context. */ @Override public abstract JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) throws Exception; @@ -212,6 +215,23 @@ public void cleanUp(JupiterEngineExecutionContext context) throws Exception { context.close(); } + /** + * {@return a deep copy (with copies of children) of this descriptor with the supplied unique ID} + */ + protected JupiterTestDescriptor copyIncludingDescendants(UnaryOperator uniqueIdTransformer) { + JupiterTestDescriptor result = withUniqueId(uniqueIdTransformer); + getChildren().forEach(oldChild -> { + TestDescriptor newChild = ((JupiterTestDescriptor) oldChild).copyIncludingDescendants(uniqueIdTransformer); + result.addChild(newChild); + }); + return result; + } + + /** + * {@return shallow copy (without children) of this descriptor with the supplied unique ID} + */ + protected abstract JupiterTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer); + /** * @since 5.5 */ 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 7d76ba1a5a16..5103639abc17 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 @@ -11,19 +11,31 @@ package org.junit.jupiter.engine.descriptor; import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods; -import static org.junit.platform.commons.util.ReflectionUtils.returnsPrimitiveVoid; +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 java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.platform.commons.JUnitException; +import org.junit.jupiter.api.extension.ClassTemplateInvocationLifecycleMethod; import org.junit.platform.commons.support.HierarchyTraversalMode; 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; /** * Collection of utilities for working with test lifecycle methods. @@ -36,70 +48,157 @@ private LifecycleMethodUtils() { /* no-op */ } - static List findBeforeAllMethods(Class testClass, boolean requireStatic) { - return findMethodsAndAssertStaticAndNonPrivate(testClass, requireStatic, BeforeAll.class, - HierarchyTraversalMode.TOP_DOWN); + static List findBeforeAllMethods(Class testClass, boolean requireStatic, + DiscoveryIssueReporter issueReporter) { + return findMethodsAndCheckStatic(testClass, requireStatic, BeforeAll.class, HierarchyTraversalMode.TOP_DOWN, + issueReporter); } - static List findAfterAllMethods(Class testClass, boolean requireStatic) { - return findMethodsAndAssertStaticAndNonPrivate(testClass, requireStatic, AfterAll.class, - HierarchyTraversalMode.BOTTOM_UP); + static List findAfterAllMethods(Class testClass, boolean requireStatic, + DiscoveryIssueReporter issueReporter) { + return findMethodsAndCheckStatic(testClass, requireStatic, AfterAll.class, HierarchyTraversalMode.BOTTOM_UP, + issueReporter); } - static List findBeforeEachMethods(Class testClass) { - return findMethodsAndAssertNonStaticAndNonPrivate(testClass, BeforeEach.class, HierarchyTraversalMode.TOP_DOWN); + static List findBeforeEachMethods(Class testClass, DiscoveryIssueReporter issueReporter) { + return findMethodsAndCheckNonStatic(testClass, BeforeEach.class, HierarchyTraversalMode.TOP_DOWN, + issueReporter); } - static List findAfterEachMethods(Class testClass) { - return findMethodsAndAssertNonStaticAndNonPrivate(testClass, AfterEach.class, HierarchyTraversalMode.BOTTOM_UP); + static List findAfterEachMethods(Class testClass, DiscoveryIssueReporter issueReporter) { + return findMethodsAndCheckNonStatic(testClass, AfterEach.class, HierarchyTraversalMode.BOTTOM_UP, + issueReporter); } - private static List findMethodsAndAssertStaticAndNonPrivate(Class testClass, boolean requireStatic, - Class annotationType, HierarchyTraversalMode traversalMode) { + static void validateNoClassTemplateInvocationLifecycleMethodsAreDeclared(Class testClass, + DiscoveryIssueReporter issueReporter) { + + findAllClassTemplateInvocationLifecycleMethods(testClass) // + .forEach(method -> findClassTemplateInvocationLifecycleMethodAnnotation(method) // + .ifPresent(annotation -> { + String message = String.format( + "@%s method '%s' must not be declared in test class '%s' because it is not annotated with @%s.", + annotation.lifecycleMethodAnnotation().getSimpleName(), method.toGenericString(), + testClass.getName(), annotation.classTemplateAnnotation().getSimpleName()); + issueReporter.reportIssue(createIssue(Severity.ERROR, message, method)); + })); + } + + static void validateClassTemplateInvocationLifecycleMethodsAreDeclaredCorrectly(Class testClass, + boolean requireStatic, DiscoveryIssueReporter issueReporter) { + + findAllClassTemplateInvocationLifecycleMethods(testClass) // + .forEach(allOf( // + isNotPrivateError(issueReporter), // + returnsPrimitiveVoid(issueReporter, + LifecycleMethodUtils::classTemplateInvocationLifecycleMethodAnnotationName), // + requireStatic + ? isStatic(issueReporter, + LifecycleMethodUtils::classTemplateInvocationLifecycleMethodAnnotationName) + : __ -> true // + )); + } - List methods = findMethodsAndCheckVoidReturnType(testClass, annotationType, traversalMode); - if (requireStatic) { - methods.forEach(method -> assertStatic(annotationType, method)); - } - return methods; + private static Stream findAllClassTemplateInvocationLifecycleMethods(Class testClass) { + Stream allMethods = Stream.concat( // + findAnnotatedMethods(testClass, ClassTemplateInvocationLifecycleMethod.class, + HierarchyTraversalMode.TOP_DOWN).stream(), // + findAnnotatedMethods(testClass, ClassTemplateInvocationLifecycleMethod.class, + HierarchyTraversalMode.BOTTOM_UP).stream() // + ); + return allMethods.distinct(); } - private static List findMethodsAndAssertNonStaticAndNonPrivate(Class testClass, - Class annotationType, HierarchyTraversalMode traversalMode) { + private static List findMethodsAndCheckStatic(Class testClass, boolean requireStatic, + Class annotationType, HierarchyTraversalMode traversalMode, + DiscoveryIssueReporter issueReporter) { - List methods = findMethodsAndCheckVoidReturnType(testClass, annotationType, traversalMode); - methods.forEach(method -> assertNonStatic(annotationType, method)); - return methods; + Condition additionalCondition = requireStatic + ? isStatic(issueReporter, __ -> annotationType.getSimpleName()) + : __ -> true; + return findMethodsAndCheckVoidReturnType(testClass, annotationType, traversalMode, issueReporter, + additionalCondition); } - private static List findMethodsAndCheckVoidReturnType(Class testClass, - Class annotationType, HierarchyTraversalMode traversalMode) { + private static List findMethodsAndCheckNonStatic(Class testClass, + Class annotationType, HierarchyTraversalMode traversalMode, + DiscoveryIssueReporter issueReporter) { - List methods = findAnnotatedMethods(testClass, annotationType, traversalMode); - methods.forEach(method -> assertVoid(annotationType, method)); - return methods; + return findMethodsAndCheckVoidReturnType(testClass, annotationType, traversalMode, issueReporter, + isNotStatic(issueReporter, __ -> annotationType.getSimpleName())); } - private static void assertStatic(Class annotationType, Method method) { - if (ModifierSupport.isNotStatic(method)) { - throw new JUnitException(String.format( + private static List findMethodsAndCheckVoidReturnType(Class testClass, + Class annotationType, HierarchyTraversalMode traversalMode, + DiscoveryIssueReporter issueReporter, Condition additionalCondition) { + + return findAnnotatedMethods(testClass, annotationType, traversalMode).stream() // + .peek(isNotPrivateDeprecation(issueReporter, annotationType::getSimpleName)) // + .filter(allOf(returnsPrimitiveVoid(issueReporter, __ -> annotationType.getSimpleName()), + additionalCondition)) // + .collect(toUnmodifiableList()); + } + + private static Condition isStatic(DiscoveryIssueReporter issueReporter, + Function annotationNameProvider) { + return issueReporter.createReportingCondition(ModifierSupport::isStatic, method -> { + String message = String.format( "@%s method '%s' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).", - annotationType.getSimpleName(), method.toGenericString())); - } + annotationNameProvider.apply(method), method.toGenericString()); + return createIssue(Severity.ERROR, message, method); + }); + } + + private static Condition isNotStatic(DiscoveryIssueReporter issueReporter, + Function annotationNameProvider) { + return issueReporter.createReportingCondition(ModifierSupport::isNotStatic, method -> { + String message = String.format("@%s method '%s' must not be static.", annotationNameProvider.apply(method), + method.toGenericString()); + return createIssue(Severity.ERROR, message, method); + }); + } + + private static Condition isNotPrivateError(DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(ModifierSupport::isNotPrivate, method -> { + String message = String.format("@%s method '%s' must not be private.", + classTemplateInvocationLifecycleMethodAnnotationName(method), method.toGenericString()); + return createIssue(Severity.ERROR, message, method); + }); + } + + private static Condition isNotPrivateDeprecation(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); + }); + } + + private static Condition returnsPrimitiveVoid(DiscoveryIssueReporter issueReporter, + Function annotationNameProvider) { + return issueReporter.createReportingCondition(ReflectionUtils::returnsPrimitiveVoid, method -> { + String message = String.format("@%s method '%s' must not return a value.", + annotationNameProvider.apply(method), method.toGenericString()); + return createIssue(Severity.ERROR, message, method); + }); + } + + private static String classTemplateInvocationLifecycleMethodAnnotationName(Method method) { + return findClassTemplateInvocationLifecycleMethodAnnotation(method) // + .map(ClassTemplateInvocationLifecycleMethod::lifecycleMethodAnnotation) // + .map(Class::getSimpleName) // + .orElseGet(ClassTemplateInvocationLifecycleMethod.class::getSimpleName); } - private static void assertNonStatic(Class annotationType, Method method) { - if (ModifierSupport.isStatic(method)) { - throw new JUnitException(String.format("@%s method '%s' must not be static.", - annotationType.getSimpleName(), method.toGenericString())); - } + private static Optional findClassTemplateInvocationLifecycleMethodAnnotation( + Method method) { + return findAnnotation(method, ClassTemplateInvocationLifecycleMethod.class); } - private static void assertVoid(Class annotationType, Method method) { - if (!returnsPrimitiveVoid(method)) { - throw new JUnitException(String.format("@%s method '%s' must not return a value.", - annotationType.getSimpleName(), method.toGenericString())); - } + private static DiscoveryIssue createIssue(Severity severity, String message, Method method) { + return DiscoveryIssue.builder(severity, message).source(MethodSource.from(method)).build(); } } 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 3a5b785591f8..7fc96c4cf07d 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 @@ -50,7 +50,8 @@ * @since 5.0 */ @API(status = INTERNAL, since = "5.0") -public abstract class MethodBasedTestDescriptor extends JupiterTestDescriptor implements ResourceLockAware { +public abstract class MethodBasedTestDescriptor extends JupiterTestDescriptor + implements ResourceLockAware, TestClassAware { private static final Logger logger = LoggerFactory.getLogger(MethodBasedTestDescriptor.class); @@ -106,11 +107,12 @@ public Function> getResou getTestMethod())); } - private List> getEnclosingTestClasses() { + @Override + public List> getEnclosingTestClasses() { return getParent() // - .filter(ClassBasedTestDescriptor.class::isInstance) // - .map(ClassBasedTestDescriptor.class::cast) // - .map(ClassBasedTestDescriptor::getEnclosingTestClasses) // + .filter(TestClassAware.class::isInstance) // + .map(TestClassAware.class::cast) // + .map(TestClassAware::getEnclosingTestClasses) // .orElseGet(Collections::emptyList); } 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 a6d983e7196b..24cdd6914eb3 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 @@ -12,6 +12,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -51,6 +52,11 @@ public Optional> getTestClass() { return Optional.of(getTestDescriptor().getTestClass()); } + @Override + public List> getEnclosingTestClasses() { + return getTestDescriptor().getEnclosingTestClasses(); + } + @Override public Optional getTestInstanceLifecycle() { return getParent().flatMap(ExtensionContext::getTestInstanceLifecycle); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java index b0619fa09cff..deb609ef8814 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.TestInstances; @@ -55,16 +56,31 @@ public NestedClassTestDescriptor(UniqueId uniqueId, Class testClass, createDisplayNameSupplierForNestedClass(enclosingInstanceTypes, testClass, configuration), configuration); } + private NestedClassTestDescriptor(UniqueId uniqueId, Class testClass, String displayName, + JupiterConfiguration configuration) { + super(uniqueId, testClass, displayName, configuration); + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected NestedClassTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new NestedClassTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getTestClass(), getDisplayName(), + configuration); + } + // --- TestDescriptor ------------------------------------------------------ @Override public final Set getTags() { // return modifiable copy - Set allTags = new LinkedHashSet<>(this.tags); + Set allTags = new LinkedHashSet<>(this.classInfo.tags); getParent().ifPresent(parentDescriptor -> allTags.addAll(parentDescriptor.getTags())); return allTags; } + // --- TestClassAware ------------------------------------------------------ + @Override public List> getEnclosingTestClasses() { return getEnclosingTestClasses(getParent().orElse(null)); @@ -72,8 +88,8 @@ public List> getEnclosingTestClasses() { @API(status = INTERNAL, since = "5.12") public static List> getEnclosingTestClasses(TestDescriptor parent) { - if (parent instanceof ClassBasedTestDescriptor) { - ClassBasedTestDescriptor parentClassDescriptor = (ClassBasedTestDescriptor) parent; + if (parent instanceof TestClassAware) { + TestClassAware parentClassDescriptor = (TestClassAware) parent; List> result = new ArrayList<>(parentClassDescriptor.getEnclosingTestClasses()); result.add(parentClassDescriptor.getTestClass()); return result; @@ -81,7 +97,7 @@ public static List> getEnclosingTestClasses(TestDescriptor parent) { return emptyList(); } - // --- Node ---------------------------------------------------------------- + // --- ClassBasedTestDescriptor -------------------------------------------- @Override protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, @@ -95,6 +111,8 @@ protected TestInstances instantiateTestClass(JupiterEngineExecutionContext paren return instantiateTestClass(Optional.of(outerInstances), registry, extensionContext); } + // --- ResourceLockAware --------------------------------------------------- + @Override public Function> getResourceLocksProviderEvaluator() { return enclosingInstanceTypesDependentResourceLocksProviderEvaluator(this::getEnclosingTestClasses, (provider, diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TemplateExecutor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TemplateExecutor.java new file mode 100644 index 000000000000..64988963685f --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TemplateExecutor.java @@ -0,0 +1,100 @@ +/* + * 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 java.util.stream.Collectors.toList; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.jupiter.engine.extension.ExtensionRegistry; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.hierarchical.Node; + +abstract class TemplateExecutor

{ + + private final TestDescriptor parent; + private final Class

providerType; + private final DynamicDescendantFilter dynamicDescendantFilter; + + TemplateExecutor(T parent, Class

providerType) { + this.parent = parent; + this.providerType = providerType; + this.dynamicDescendantFilter = parent.getDynamicDescendantFilter(); + } + + void execute(JupiterEngineExecutionContext context, Node.DynamicTestExecutor dynamicTestExecutor) { + ExtensionContext extensionContext = context.getExtensionContext(); + List

providers = validateProviders(extensionContext, context.getExtensionRegistry()); + AtomicInteger invocationIndex = new AtomicInteger(); + for (P provider : providers) { + executeForProvider(provider, invocationIndex, dynamicTestExecutor, extensionContext); + } + } + + private void executeForProvider(P provider, AtomicInteger invocationIndex, + Node.DynamicTestExecutor dynamicTestExecutor, ExtensionContext extensionContext) { + + int initialValue = invocationIndex.get(); + + try (Stream stream = provideContexts(provider, extensionContext)) { + stream.forEach(invocationContext -> createInvocationTestDescriptor(invocationContext, + invocationIndex.incrementAndGet()) // + .ifPresent(testDescriptor -> execute(dynamicTestExecutor, testDescriptor))); + } + + Preconditions.condition( + invocationIndex.get() != initialValue || mayReturnZeroContexts(provider, extensionContext), + getZeroContextsProvidedErrorMessage(provider)); + } + + private List

validateProviders(ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { + List

providers = extensionRegistry.stream(providerType) // + .filter(provider -> supports(provider, extensionContext)) // + .collect(toList()); + return Preconditions.notEmpty(providers, this::getNoRegisteredProviderErrorMessage); + } + + private Optional createInvocationTestDescriptor(C invocationContext, int index) { + UniqueId invocationUniqueId = createInvocationUniqueId(parent.getUniqueId(), index); + if (this.dynamicDescendantFilter.test(invocationUniqueId, index - 1)) { + return Optional.of(createInvocationTestDescriptor(invocationUniqueId, invocationContext, index)); + } + return Optional.empty(); + } + + private void execute(Node.DynamicTestExecutor dynamicTestExecutor, TestDescriptor testDescriptor) { + testDescriptor.setParent(parent); + dynamicTestExecutor.execute(testDescriptor); + } + + abstract boolean supports(P provider, ExtensionContext extensionContext); + + protected abstract String getNoRegisteredProviderErrorMessage(); + + abstract Stream provideContexts(P provider, ExtensionContext extensionContext); + + abstract boolean mayReturnZeroContexts(P provider, ExtensionContext extensionContext); + + protected abstract String getZeroContextsProvidedErrorMessage(P provider); + + abstract UniqueId createInvocationUniqueId(UniqueId parentUniqueId, int index); + + abstract TestDescriptor createInvocationTestDescriptor(UniqueId uniqueId, C invocationContext, int index); + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestClassAware.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestClassAware.java new file mode 100644 index 000000000000..f337b53ca046 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestClassAware.java @@ -0,0 +1,29 @@ +/* + * 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 java.util.List; + +import org.apiguardian.api.API; + +/** + * @since 5.13 + */ +@API(status = INTERNAL, since = "5.13") +public interface TestClassAware { + + Class getTestClass(); + + List> getEnclosingTestClasses(); + +} 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 f0d37814bbf2..f145e5531a7c 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 @@ -21,6 +21,7 @@ import java.util.List; import java.util.Optional; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.apiguardian.api.API; @@ -61,11 +62,27 @@ public class TestFactoryTestDescriptor extends TestMethodTestDescriptor implemen private static final ReflectiveInterceptorCall interceptorCall = InvocationInterceptor::interceptTestFactoryMethod; private static final InterceptingExecutableInvoker executableInvoker = new InterceptingExecutableInvoker(); - private final DynamicDescendantFilter dynamicDescendantFilter = new DynamicDescendantFilter(); + private final DynamicDescendantFilter dynamicDescendantFilter; public TestFactoryTestDescriptor(UniqueId uniqueId, Class testClass, Method testMethod, Supplier>> enclosingInstanceTypes, JupiterConfiguration configuration) { super(uniqueId, testClass, testMethod, enclosingInstanceTypes, configuration); + this.dynamicDescendantFilter = new DynamicDescendantFilter(); + } + + private TestFactoryTestDescriptor(UniqueId uniqueId, String displayName, Class testClass, Method testMethod, + JupiterConfiguration configuration, DynamicDescendantFilter dynamicDescendantFilter) { + super(uniqueId, displayName, testClass, testMethod, configuration); + this.dynamicDescendantFilter = dynamicDescendantFilter; + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected TestFactoryTestDescriptor withUniqueId(UnaryOperator 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)); } // --- Filterable ---------------------------------------------------------- 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 67fa136a52d1..8d6f961e96fa 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 @@ -11,14 +11,16 @@ package org.junit.jupiter.engine.descriptor; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeAfterCallbacks; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeBeforeCallbacks; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters; import static org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory.createThrowableCollector; -import static org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder; import java.lang.reflect.Method; import java.util.List; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -26,7 +28,6 @@ import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; -import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler; @@ -82,12 +83,27 @@ public TestMethodTestDescriptor(UniqueId uniqueId, Class testClass, Method te this.interceptorCall = defaultInterceptorCall; } + TestMethodTestDescriptor(UniqueId uniqueId, String displayName, Class testClass, Method testMethod, + JupiterConfiguration configuration) { + this(uniqueId, displayName, testClass, testMethod, configuration, defaultInterceptorCall); + } + TestMethodTestDescriptor(UniqueId uniqueId, String displayName, Class testClass, Method testMethod, JupiterConfiguration configuration, ReflectiveInterceptorCall interceptorCall) { super(uniqueId, displayName, testClass, testMethod, configuration); this.interceptorCall = interceptorCall; } + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected TestMethodTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new TestMethodTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getDisplayName(), getTestClass(), + getTestMethod(), this.configuration, interceptorCall); + } + + // --- TestDescriptor ------------------------------------------------------ + @Override public Type getType() { return Type.TEST; @@ -111,10 +127,15 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte throwableCollector.execute(() -> { TestInstances testInstances = newContext.getTestInstancesProvider().getTestInstances(newContext); extensionContext.setTestInstances(testInstances); + prepareExtensionContext(extensionContext); }); return newContext; } + protected void prepareExtensionContext(ExtensionContext extensionContext) { + // nothing to do by default + } + protected MutableExtensionRegistry populateNewExtensionRegistry(JupiterEngineExecutionContext context) { MutableExtensionRegistry registry = populateNewExtensionRegistryFromExtendWithAnnotation( context.getExtensionRegistry(), getTestMethod()); @@ -161,21 +182,19 @@ private boolean isPerMethodLifecycle(JupiterEngineExecutionContext context) { } private void invokeBeforeEachCallbacks(JupiterEngineExecutionContext context) { - invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(BeforeEachCallback.class, context, - (callback, extensionContext) -> callback.beforeEach(extensionContext)); + invokeBeforeCallbacks(BeforeEachCallback.class, context, BeforeEachCallback::beforeEach); } private void invokeBeforeEachMethods(JupiterEngineExecutionContext context) { ExtensionRegistry registry = context.getExtensionRegistry(); - invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(BeforeEachMethodAdapter.class, context, - (adapter, extensionContext) -> { - try { - adapter.invokeBeforeEachMethod(extensionContext, registry); - } - catch (Throwable throwable) { - invokeBeforeEachExecutionExceptionHandlers(extensionContext, registry, throwable); - } - }); + invokeBeforeCallbacks(BeforeEachMethodAdapter.class, context, (adapter, extensionContext) -> { + try { + adapter.invokeBeforeEachMethod(extensionContext, registry); + } + catch (Throwable throwable) { + invokeBeforeEachExecutionExceptionHandlers(extensionContext, registry, throwable); + } + }); } private void invokeBeforeEachExecutionExceptionHandlers(ExtensionContext context, ExtensionRegistry registry, @@ -186,23 +205,8 @@ private void invokeBeforeEachExecutionExceptionHandlers(ExtensionContext context } private void invokeBeforeTestExecutionCallbacks(JupiterEngineExecutionContext context) { - invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(BeforeTestExecutionCallback.class, context, - (callback, extensionContext) -> callback.beforeTestExecution(extensionContext)); - } - - private void invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(Class type, - JupiterEngineExecutionContext context, CallbackInvoker callbackInvoker) { - - ExtensionRegistry registry = context.getExtensionRegistry(); - ExtensionContext extensionContext = context.getExtensionContext(); - ThrowableCollector throwableCollector = context.getThrowableCollector(); - - for (T callback : registry.getExtensions(type)) { - throwableCollector.execute(() -> callbackInvoker.invoke(callback, extensionContext)); - if (throwableCollector.isNotEmpty()) { - break; - } - } + invokeBeforeCallbacks(BeforeTestExecutionCallback.class, context, + BeforeTestExecutionCallback::beforeTestExecution); } protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) { @@ -231,13 +235,12 @@ private void invokeTestExecutionExceptionHandlers(ExtensionRegistry registry, Ex } private void invokeAfterTestExecutionCallbacks(JupiterEngineExecutionContext context) { - invokeAllAfterMethodsOrCallbacks(AfterTestExecutionCallback.class, context, - (callback, extensionContext) -> callback.afterTestExecution(extensionContext)); + invokeAfterCallbacks(AfterTestExecutionCallback.class, context, AfterTestExecutionCallback::afterTestExecution); } private void invokeAfterEachMethods(JupiterEngineExecutionContext context) { ExtensionRegistry registry = context.getExtensionRegistry(); - invokeAllAfterMethodsOrCallbacks(AfterEachMethodAdapter.class, context, (adapter, extensionContext) -> { + invokeAfterCallbacks(AfterEachMethodAdapter.class, context, (adapter, extensionContext) -> { try { adapter.invokeAfterEachMethod(extensionContext, registry); } @@ -255,27 +258,14 @@ private void invokeAfterEachExecutionExceptionHandlers(ExtensionContext context, } private void invokeAfterEachCallbacks(JupiterEngineExecutionContext context) { - invokeAllAfterMethodsOrCallbacks(AfterEachCallback.class, context, - (callback, extensionContext) -> callback.afterEach(extensionContext)); + invokeAfterCallbacks(AfterEachCallback.class, context, AfterEachCallback::afterEach); } private void invokeTestInstancePreDestroyCallbacks(JupiterEngineExecutionContext context) { - invokeAllAfterMethodsOrCallbacks(TestInstancePreDestroyCallback.class, context, + invokeAfterCallbacks(TestInstancePreDestroyCallback.class, context, TestInstancePreDestroyCallback::preDestroyTestInstance); } - private void invokeAllAfterMethodsOrCallbacks(Class type, - JupiterEngineExecutionContext context, CallbackInvoker callbackInvoker) { - - ExtensionRegistry registry = context.getExtensionRegistry(); - ExtensionContext extensionContext = context.getExtensionContext(); - ThrowableCollector throwableCollector = context.getThrowableCollector(); - - forEachInReverseOrder(registry.getExtensions(type), callback -> { - throwableCollector.execute(() -> callbackInvoker.invoke(callback, extensionContext)); - }); - } - /** * Invoke {@link TestWatcher#testSuccessful testSuccessful()}, * {@link TestWatcher#testAborted testAborted()}, or @@ -309,14 +299,4 @@ public void nodeFinished(JupiterEngineExecutionContext context, TestDescriptor d } } - /** - * @since 5.5 - */ - @FunctionalInterface - private interface CallbackInvoker { - - void invoke(T t, ExtensionContext context) throws Throwable; - - } - } 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 ea6df34d7436..5fa74917714b 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 @@ -12,6 +12,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -47,6 +48,11 @@ public Optional> getTestClass() { return Optional.of(getTestDescriptor().getTestClass()); } + @Override + public List> getEnclosingTestClasses() { + return getTestDescriptor().getEnclosingTestClasses(); + } + @Override public Optional getTestInstanceLifecycle() { return getParent().flatMap(ExtensionContext::getTestInstanceLifecycle); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java index cbb66f12d9b9..88d494a9731d 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java @@ -15,8 +15,10 @@ import java.lang.reflect.Method; import java.util.Set; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.engine.config.JupiterConfiguration; @@ -51,6 +53,16 @@ public class TestTemplateInvocationTestDescriptor extends TestMethodTestDescript this.index = index; } + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected TestTemplateInvocationTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new TestTemplateInvocationTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getTestClass(), + getTestMethod(), this.invocationContext, this.index, this.configuration); + } + + // --- TestDescriptor ------------------------------------------------------ + @Override public Set getExclusiveResources() { // Resources are already collected and returned by the enclosing container @@ -65,15 +77,20 @@ public String getLegacyReportingName() { @Override protected MutableExtensionRegistry populateNewExtensionRegistry(JupiterEngineExecutionContext context) { MutableExtensionRegistry registry = super.populateNewExtensionRegistry(context); - invocationContext.getAdditionalExtensions().forEach( + this.invocationContext.getAdditionalExtensions().forEach( extension -> registry.registerExtension(extension, invocationContext)); return registry; } + @Override + protected void prepareExtensionContext(ExtensionContext extensionContext) { + this.invocationContext.prepareInvocation(extensionContext); + } + @Override public void after(JupiterEngineExecutionContext context) { // forget invocationContext so it can be garbage collected - invocationContext = null; + this.invocationContext = null; } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java index e592ba3a6326..05ef02c0c991 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java @@ -10,27 +10,24 @@ package org.junit.jupiter.engine.descriptor; -import static java.util.stream.Collectors.toList; import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation; import java.lang.reflect.Method; import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.apiguardian.api.API; +import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstances; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; -import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.jupiter.engine.extension.MutableExtensionRegistry; -import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; @@ -44,11 +41,27 @@ public class TestTemplateTestDescriptor extends MethodBasedTestDescriptor implements Filterable { public static final String SEGMENT_TYPE = "test-template"; - private final DynamicDescendantFilter dynamicDescendantFilter = new DynamicDescendantFilter(); + private final DynamicDescendantFilter dynamicDescendantFilter; public TestTemplateTestDescriptor(UniqueId uniqueId, Class testClass, Method templateMethod, Supplier>> enclosingInstanceTypes, JupiterConfiguration configuration) { super(uniqueId, testClass, templateMethod, enclosingInstanceTypes, configuration); + this.dynamicDescendantFilter = new DynamicDescendantFilter(); + } + + private TestTemplateTestDescriptor(UniqueId uniqueId, String displayName, Class testClass, Method templateMethod, + JupiterConfiguration configuration, DynamicDescendantFilter dynamicDescendantFilter) { + super(uniqueId, displayName, testClass, templateMethod, configuration); + this.dynamicDescendantFilter = dynamicDescendantFilter; + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected TestTemplateTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new TestTemplateTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getDisplayName(), + getTestClass(), getTestMethod(), this.configuration, + this.dynamicDescendantFilter.copy(uniqueIdTransformer)); } // --- Filterable ---------------------------------------------------------- @@ -95,65 +108,59 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) throws Exception { - ExtensionContext extensionContext = context.getExtensionContext(); - List providers = validateProviders(extensionContext, - context.getExtensionRegistry()); - AtomicInteger invocationIndex = new AtomicInteger(); - for (TestTemplateInvocationContextProvider provider : providers) { - executeForProvider(provider, invocationIndex, dynamicTestExecutor, extensionContext); - } + new TestTemplateExecutor().execute(context, dynamicTestExecutor); return context; } - private void executeForProvider(TestTemplateInvocationContextProvider provider, AtomicInteger invocationIndex, - DynamicTestExecutor dynamicTestExecutor, ExtensionContext extensionContext) { - - int initialValue = invocationIndex.get(); + private class TestTemplateExecutor + extends TemplateExecutor { - try (Stream stream = invocationContexts(provider, extensionContext)) { - stream.forEach(invocationContext -> toTestDescriptor(invocationContext, invocationIndex.incrementAndGet()) // - .ifPresent(testDescriptor -> execute(dynamicTestExecutor, testDescriptor))); + TestTemplateExecutor() { + super(TestTemplateTestDescriptor.this, TestTemplateInvocationContextProvider.class); } - Preconditions.condition( - invocationIndex.get() != initialValue - || provider.mayReturnZeroTestTemplateInvocationContexts(extensionContext), - String.format( - "Provider [%s] did not provide any invocation contexts, but was expected to do so. " - + "You may override mayReturnZeroTestTemplateInvocationContexts() to allow this.", - provider.getClass().getSimpleName())); - } + @Override + boolean supports(TestTemplateInvocationContextProvider provider, ExtensionContext extensionContext) { + return provider.supportsTestTemplate(extensionContext); + } - private static Stream invocationContexts( - TestTemplateInvocationContextProvider provider, ExtensionContext extensionContext) { - return provider.provideTestTemplateInvocationContexts(extensionContext); - } + @Override + protected String getNoRegisteredProviderErrorMessage() { + return String.format("You must register at least one %s that supports @%s method [%s]", + TestTemplateInvocationContextProvider.class.getSimpleName(), TestTemplate.class.getSimpleName(), + getTestMethod()); + } - private List validateProviders(ExtensionContext extensionContext, - ExtensionRegistry extensionRegistry) { + @Override + Stream provideContexts(TestTemplateInvocationContextProvider provider, + ExtensionContext extensionContext) { + return provider.provideTestTemplateInvocationContexts(extensionContext); + } - // @formatter:off - List providers = extensionRegistry.stream(TestTemplateInvocationContextProvider.class) - .filter(provider -> provider.supportsTestTemplate(extensionContext)) - .collect(toList()); - // @formatter:on + @Override + boolean mayReturnZeroContexts(TestTemplateInvocationContextProvider provider, + ExtensionContext extensionContext) { + return provider.mayReturnZeroTestTemplateInvocationContexts(extensionContext); + } - return Preconditions.notEmpty(providers, - () -> String.format("You must register at least one %s that supports @TestTemplate method [%s]", - TestTemplateInvocationContextProvider.class.getSimpleName(), getTestMethod())); - } + @Override + protected String getZeroContextsProvidedErrorMessage(TestTemplateInvocationContextProvider provider) { + return String.format( + "Provider [%s] did not provide any invocation contexts, but was expected to do so. " + + "You may override mayReturnZeroTestTemplateInvocationContexts() to allow this.", + provider.getClass().getSimpleName()); + } - private Optional toTestDescriptor(TestTemplateInvocationContext invocationContext, int index) { - UniqueId uniqueId = getUniqueId().append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#" + index); - if (getDynamicDescendantFilter().test(uniqueId, index - 1)) { - return Optional.of(new TestTemplateInvocationTestDescriptor(uniqueId, getTestClass(), getTestMethod(), - invocationContext, index, configuration)); + @Override + UniqueId createInvocationUniqueId(UniqueId parentUniqueId, int index) { + return parentUniqueId.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#" + index); } - return Optional.empty(); - } - private void execute(DynamicTestExecutor dynamicTestExecutor, TestDescriptor testDescriptor) { - testDescriptor.setParent(this); - dynamicTestExecutor.execute(testDescriptor); + @Override + TestDescriptor createInvocationTestDescriptor(UniqueId uniqueId, + TestTemplateInvocationContext invocationContext, int index) { + return new TestTemplateInvocationTestDescriptor(uniqueId, getTestClass(), getTestMethod(), + invocationContext, index, TestTemplateTestDescriptor.this.configuration); + } } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/UniqueIdPrefixTransformer.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/UniqueIdPrefixTransformer.java new file mode 100644 index 000000000000..20a85ba1382b --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/UniqueIdPrefixTransformer.java @@ -0,0 +1,46 @@ +/* + * 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 java.util.List; +import java.util.function.UnaryOperator; + +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.UniqueId; + +/** + * @since 5.13 + */ +class UniqueIdPrefixTransformer implements UnaryOperator { + + private final UniqueId oldPrefix; + private final UniqueId newPrefix; + private final int oldPrefixLength; + + UniqueIdPrefixTransformer(UniqueId oldPrefix, UniqueId newPrefix) { + this.oldPrefix = oldPrefix; + this.newPrefix = newPrefix; + this.oldPrefixLength = oldPrefix.getSegments().size(); + } + + @Override + public UniqueId apply(UniqueId uniqueId) { + Preconditions.condition(uniqueId.hasPrefix(oldPrefix), + () -> String.format("Unique ID %s does not have the expected prefix %s", uniqueId, oldPrefix)); + List oldSegments = uniqueId.getSegments(); + List suffix = oldSegments.subList(oldPrefixLength, oldSegments.size()); + UniqueId newValue = newPrefix; + for (UniqueId.Segment segment : suffix) { + newValue = newValue.append(segment); + } + return newValue; + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/Validatable.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/Validatable.java new file mode 100644 index 000000000000..b4c83e1944ad --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/Validatable.java @@ -0,0 +1,32 @@ +/* + * 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.platform.engine.support.discovery.DiscoveryIssueReporter; + +/** + * Interface for descriptors that can be validated during discovery. + * + * @since 5.13 + */ +@API(status = INTERNAL, since = "5.13") +public interface Validatable { + + /** + * Validate the state of this descriptor and report any issues found to the + * supplied {@link DiscoveryIssueReporter}. + */ + void validate(DiscoveryIssueReporter reporter); + +} 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 0c7147d987dd..0645aa0abf64 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 @@ -22,7 +22,6 @@ import java.util.function.Function; import java.util.stream.Stream; -import org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.UnrecoverableExceptions; @@ -32,19 +31,18 @@ * Abstract base class for {@linkplain TestDescriptor.Visitor visitors} that * order children nodes. * - * @param the parent container type to search in for matching children - * @param the type of children (containers or tests) to order - * @param the wrapper type for the children to order * @since 5.8 */ -abstract class AbstractOrderingVisitor> - implements TestDescriptor.Visitor { +abstract class AbstractOrderingVisitor implements TestDescriptor.Visitor { private static final Logger logger = LoggerFactory.getLogger(AbstractOrderingVisitor.class); + /** + * @param the parent container type to search in for matching children + */ @SuppressWarnings("unchecked") - protected void doWithMatchingDescriptor(Class parentTestDescriptorType, TestDescriptor testDescriptor, - Consumer action, Function errorMessageBuilder) { + protected void doWithMatchingDescriptor(Class parentTestDescriptorType, + TestDescriptor testDescriptor, Consumer action, Function errorMessageBuilder) { if (parentTestDescriptorType.isInstance(testDescriptor)) { PARENT parentTestDescriptor = (PARENT) testDescriptor; @@ -58,8 +56,17 @@ protected void doWithMatchingDescriptor(Class parentTestDescriptorType, } } - protected void orderChildrenTestDescriptors(TestDescriptor parentTestDescriptor, Class matchingChildrenType, - Function descriptorWrapperFactory, DescriptorWrapperOrderer descriptorWrapperOrderer) { + /** + * @param the type of children (containers or tests) to order + */ + protected > void orderChildrenTestDescriptors( + TestDescriptor parentTestDescriptor, Class matchingChildrenType, + Function descriptorWrapperFactory, + DescriptorWrapperOrderer descriptorWrapperOrderer) { + + if (!descriptorWrapperOrderer.canOrderWrappers()) { + return; + } List matchingDescriptorWrappers = parentTestDescriptor.getChildren()// .stream()// @@ -73,67 +80,43 @@ protected void orderChildrenTestDescriptors(TestDescriptor parentTestDescriptor, return; } - if (descriptorWrapperOrderer.canOrderWrappers()) { - parentTestDescriptor.orderChildren(children -> { - Stream nonMatchingTestDescriptors = children.stream()// - .filter(childTestDescriptor -> !matchingChildrenType.isInstance(childTestDescriptor)); - - descriptorWrapperOrderer.orderWrappers(matchingDescriptorWrappers); - - Stream orderedTestDescriptors = matchingDescriptorWrappers.stream()// - .map(AbstractAnnotatedDescriptorWrapper::getTestDescriptor); - - // If we are ordering children of type ClassBasedTestDescriptor, that means we - // are ordering top-level classes or @Nested test classes. Thus, the - // nonMatchingTestDescriptors list is either empty (for top-level classes) or - // contains only local test methods (for @Nested classes) which must be executed - // before tests in @Nested test classes. So we add the test methods before adding - // the @Nested test classes. - if (matchingChildrenType == ClassBasedTestDescriptor.class) { - return Stream.concat(nonMatchingTestDescriptors, orderedTestDescriptors)// - .collect(toList()); - } - // Otherwise, we add the ordered descriptors before the non-matching descriptors, - // which is the case when we are ordering test methods. In other words, local - // test methods always get added before @Nested test classes. - else { - return Stream.concat(orderedTestDescriptors, nonMatchingTestDescriptors)// - .collect(toList()); - } - }); - } + parentTestDescriptor.orderChildren(children -> { + Stream nonMatchingTestDescriptors = children.stream()// + .filter(childTestDescriptor -> !matchingChildrenType.isInstance(childTestDescriptor)); + + descriptorWrapperOrderer.orderWrappers(matchingDescriptorWrappers); - // Recurse through the children in order to support ordering for @Nested test classes. - matchingDescriptorWrappers.forEach(descriptorWrapper -> { - TestDescriptor newParentTestDescriptor = descriptorWrapper.getTestDescriptor(); - DescriptorWrapperOrderer newDescriptorWrapperOrderer = getDescriptorWrapperOrderer(descriptorWrapperOrderer, - descriptorWrapper); + Stream orderedTestDescriptors = matchingDescriptorWrappers.stream()// + .map(AbstractAnnotatedDescriptorWrapper::getTestDescriptor); - orderChildrenTestDescriptors(newParentTestDescriptor, matchingChildrenType, descriptorWrapperFactory, - newDescriptorWrapperOrderer); + if (shouldNonMatchingDescriptorsComeBeforeOrderedOnes()) { + return Stream.concat(nonMatchingTestDescriptors, orderedTestDescriptors)// + .collect(toList()); + } + else { + return Stream.concat(orderedTestDescriptors, nonMatchingTestDescriptors)// + .collect(toList()); + } }); } + protected abstract boolean shouldNonMatchingDescriptorsComeBeforeOrderedOnes(); + /** - * Get the {@link DescriptorWrapperOrderer} for the supplied {@link AbstractAnnotatedDescriptorWrapper}. - * - *

The default implementation returns the supplied {@code DescriptorWrapperOrderer}. - * - * @return a new {@code DescriptorWrapperOrderer} or the one supplied as an argument + * @param the wrapper type for the children to order */ - protected DescriptorWrapperOrderer getDescriptorWrapperOrderer( - DescriptorWrapperOrderer inheritedDescriptorWrapperOrderer, - AbstractAnnotatedDescriptorWrapper descriptorWrapper) { + protected static class DescriptorWrapperOrderer { - return inheritedDescriptorWrapperOrderer; - } + private static final DescriptorWrapperOrderer NOOP = new DescriptorWrapperOrderer<>(null, __ -> "", + ___ -> ""); - protected class DescriptorWrapperOrderer { + @SuppressWarnings("unchecked") + protected static DescriptorWrapperOrderer noop() { + return (DescriptorWrapperOrderer) NOOP; + } private final Consumer> orderingAction; - private final MessageGenerator descriptorsAddedMessageGenerator; - private final MessageGenerator descriptorsRemovedMessageGenerator; DescriptorWrapperOrderer(Consumer> orderingAction, 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 545ee52d5072..fb886108363a 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,7 +10,6 @@ package org.junit.jupiter.engine.discovery; -import java.lang.reflect.AnnotatedElement; import java.util.List; import java.util.function.Consumer; @@ -21,67 +20,100 @@ import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; 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.TestDescriptor; /** * @since 5.8 */ -class ClassOrderingVisitor - extends AbstractOrderingVisitor { +class ClassOrderingVisitor extends AbstractOrderingVisitor { + private final LruCache> ordererCache = new LruCache<>( + 10); private final JupiterConfiguration configuration; + private final DescriptorWrapperOrderer globalOrderer; ClassOrderingVisitor(JupiterConfiguration configuration) { this.configuration = configuration; + this.globalOrderer = createGlobalOrderer(configuration); } @Override public void visit(TestDescriptor testDescriptor) { - ClassOrderer globalClassOrderer = this.configuration.getDefaultTestClassOrderer().orElse(null); - doWithMatchingDescriptor(JupiterEngineDescriptor.class, testDescriptor, - descriptor -> orderContainedClasses(descriptor, globalClassOrderer), - descriptor -> "Failed to order classes"); + doWithMatchingDescriptor(JupiterEngineDescriptor.class, testDescriptor, this::orderTopLevelClasses, + descriptor -> "Failed to order top-level classes"); + doWithMatchingDescriptor(ClassBasedTestDescriptor.class, testDescriptor, this::orderNestedClasses, + descriptor -> "Failed to order nested classes for " + descriptor.getTestClass()); } - private void orderContainedClasses(JupiterEngineDescriptor jupiterEngineDescriptor, ClassOrderer classOrderer) { + @Override + protected boolean shouldNonMatchingDescriptorsComeBeforeOrderedOnes() { + // Non-matching descriptors can only occur when ordering nested classes in which + // case they contain only local test methods (for @Nested classes) which must be + // executed before tests in @Nested test classes. So we add the test methods before + // adding the @Nested test classes. + return true; + } + + private void orderTopLevelClasses(JupiterEngineDescriptor engineDescriptor) { orderChildrenTestDescriptors(// - jupiterEngineDescriptor, // + engineDescriptor, // ClassBasedTestDescriptor.class, // DefaultClassDescriptor::new, // - createDescriptorWrapperOrderer(classOrderer)); + globalOrderer); } - @Override - protected DescriptorWrapperOrderer getDescriptorWrapperOrderer( - DescriptorWrapperOrderer inheritedDescriptorWrapperOrderer, - AbstractAnnotatedDescriptorWrapper descriptorWrapper) { + private void orderNestedClasses(ClassBasedTestDescriptor descriptor) { + orderChildrenTestDescriptors(// + descriptor, // + ClassBasedTestDescriptor.class, // + DefaultClassDescriptor::new, // + createAndCacheClassLevelOrderer(descriptor)); + } + + private DescriptorWrapperOrderer createGlobalOrderer(JupiterConfiguration configuration) { + ClassOrderer classOrderer = configuration.getDefaultTestClassOrderer().orElse(null); + return classOrderer == null ? DescriptorWrapperOrderer.noop() : createDescriptorWrapperOrderer(classOrderer); + } - AnnotatedElement annotatedElement = descriptorWrapper.getAnnotatedElement(); - return AnnotationSupport.findAnnotation(annotatedElement, TestClassOrder.class)// + private DescriptorWrapperOrderer createAndCacheClassLevelOrderer( + ClassBasedTestDescriptor classBasedTestDescriptor) { + DescriptorWrapperOrderer orderer = createClassLevelOrderer(classBasedTestDescriptor); + ordererCache.put(classBasedTestDescriptor, orderer); + return orderer; + } + + private DescriptorWrapperOrderer createClassLevelOrderer( + ClassBasedTestDescriptor classBasedTestDescriptor) { + return AnnotationSupport.findAnnotation(classBasedTestDescriptor.getTestClass(), TestClassOrder.class)// .map(TestClassOrder::value)// - . map(ReflectionSupport::newInstance)// + .map(ReflectionSupport::newInstance)// .map(this::createDescriptorWrapperOrderer)// - .orElse(inheritedDescriptorWrapperOrderer); + .orElseGet(() -> { + Object parent = classBasedTestDescriptor.getParent().orElse(null); + if (parent instanceof ClassBasedTestDescriptor) { + ClassBasedTestDescriptor parentClassTestDescriptor = (ClassBasedTestDescriptor) parent; + DescriptorWrapperOrderer cacheEntry = ordererCache.get( + parentClassTestDescriptor); + return cacheEntry != null ? cacheEntry : createClassLevelOrderer(parentClassTestDescriptor); + } + return globalOrderer; + }); } - private DescriptorWrapperOrderer createDescriptorWrapperOrderer(ClassOrderer classOrderer) { - Consumer> orderingAction = classOrderer == null ? null : // - classDescriptors -> classOrderer.orderClasses( - new DefaultClassOrdererContext(classDescriptors, this.configuration)); + private DescriptorWrapperOrderer createDescriptorWrapperOrderer(ClassOrderer classOrderer) { + Consumer> orderingAction = classDescriptors -> classOrderer.orderClasses( + new DefaultClassOrdererContext(classDescriptors, this.configuration)); MessageGenerator descriptorsAddedMessageGenerator = number -> String.format( - "ClassOrderer [%s] added %s ClassDescriptor(s) which will be ignored.", nullSafeToString(classOrderer), + "ClassOrderer [%s] added %s ClassDescriptor(s) which will be ignored.", classOrderer.getClass().getName(), number); MessageGenerator descriptorsRemovedMessageGenerator = number -> String.format( "ClassOrderer [%s] removed %s ClassDescriptor(s) which will be retained with arbitrary ordering.", - nullSafeToString(classOrderer), number); + classOrderer.getClass().getName(), number); - return new DescriptorWrapperOrderer(orderingAction, descriptorsAddedMessageGenerator, + return new DescriptorWrapperOrderer<>(orderingAction, descriptorsAddedMessageGenerator, descriptorsRemovedMessageGenerator); } - private static String nullSafeToString(ClassOrderer classOrderer) { - return (classOrderer != null ? classOrderer.getClass().getName() : ""); - } - } 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 0f809dcbde47..3405dad0e7d7 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 @@ -10,10 +10,13 @@ package org.junit.jupiter.engine.discovery; +import static java.util.Collections.emptyList; import static java.util.function.Predicate.isEqual; 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; @@ -27,14 +30,21 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Predicate; 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; +import org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor; +import org.junit.jupiter.engine.descriptor.ClassTemplateTestDescriptor; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; +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.platform.commons.support.ReflectionSupport; @@ -43,6 +53,7 @@ import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.discovery.ClassSelector; import org.junit.platform.engine.discovery.DiscoverySelectors; +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.discovery.SelectorResolver; @@ -54,6 +65,8 @@ 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; @@ -70,12 +83,12 @@ public Resolution resolve(ClassSelector selector, Context context) { // Nested tests are never filtered out if (classNameFilter.test(testClass.getName())) { return toResolution( - context.addToParent(parent -> Optional.of(newClassTestDescriptor(parent, testClass)))); + context.addToParent(parent -> Optional.of(newStaticClassTestDescriptor(parent, testClass)))); } } else if (isNestedTestClass.test(testClass)) { return toResolution(context.addToParent(() -> DiscoverySelectors.selectClass(testClass.getEnclosingClass()), - parent -> Optional.of(newNestedClassTestDescriptor(parent, testClass)))); + parent -> Optional.of(newMemberClassTestDescriptor(parent, testClass)))); } return unresolved(); } @@ -84,7 +97,7 @@ else if (isNestedTestClass.test(testClass)) { public Resolution resolve(NestedClassSelector selector, Context context) { if (isNestedTestClass.test(selector.getNestedClass())) { return toResolution(context.addToParent(() -> selectClass(selector.getEnclosingClasses()), - parent -> Optional.of(newNestedClassTestDescriptor(parent, selector.getNestedClass())))); + parent -> Optional.of(newMemberClassTestDescriptor(parent, selector.getNestedClass())))); } return unresolved(); } @@ -94,55 +107,177 @@ public Resolution resolve(UniqueIdSelector selector, Context context) { UniqueId uniqueId = selector.getUniqueId(); UniqueId.Segment lastSegment = uniqueId.getLastSegment(); if (ClassTestDescriptor.SEGMENT_TYPE.equals(lastSegment.getType())) { - String className = lastSegment.getValue(); - return ReflectionSupport.tryToLoadClass(className).toOptional().filter(isTestClassWithTests).map( - testClass -> toResolution( - context.addToParent(parent -> Optional.of(newClassTestDescriptor(parent, testClass))))).orElse( - unresolved()); + return resolveStaticClassUniqueId(context, lastSegment, __ -> true, this::newClassTestDescriptor); + } + if (ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE.equals(lastSegment.getType())) { + return resolveStaticClassUniqueId(context, lastSegment, isAnnotatedWithClassTemplate, + this::newStaticClassTemplateTestDescriptor); } if (NestedClassTestDescriptor.SEGMENT_TYPE.equals(lastSegment.getType())) { - String simpleClassName = lastSegment.getValue(); - return toResolution(context.addToParent(() -> selectUniqueId(uniqueId.removeLastSegment()), parent -> { - if (parent instanceof ClassBasedTestDescriptor) { - Class parentTestClass = ((ClassBasedTestDescriptor) parent).getTestClass(); - return ReflectionSupport.findNestedClasses(parentTestClass, - isNestedTestClass.and( - where(Class::getSimpleName, isEqual(simpleClassName)))).stream().findFirst().flatMap( - testClass -> Optional.of(newNestedClassTestDescriptor(parent, testClass))); - } - return Optional.empty(); - })); + return resolveNestedClassUniqueId(context, uniqueId, __ -> true, this::newNestedClassTestDescriptor); + } + if (ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE.equals(lastSegment.getType())) { + return resolveNestedClassUniqueId(context, uniqueId, isAnnotatedWithClassTemplate, + this::newNestedClassTemplateTestDescriptor); + } + if (ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE.equals(lastSegment.getType())) { + Optional testDescriptor = context.addToParent( + () -> selectUniqueId(uniqueId.removeLastSegment()), parent -> { + int index = Integer.parseInt(lastSegment.getValue().substring(1)); + return Optional.of(newDummyClassTemplateInvocationTestDescriptor(parent, index)); + }); + return toInvocationMatch(testDescriptor) // + .map(Resolution::match) // + .orElse(unresolved()); + } + return unresolved(); + } + + @Override + public Resolution resolve(IterationSelector selector, Context context) { + DiscoverySelector parentSelector = selector.getParentSelector(); + if (parentSelector instanceof ClassSelector + && isAnnotatedWithClassTemplate.test(((ClassSelector) parentSelector).getJavaClass())) { + return resolveIterations(selector, context); + } + if (parentSelector instanceof NestedClassSelector + && isAnnotatedWithClassTemplate.test(((NestedClassSelector) parentSelector).getNestedClass())) { + return resolveIterations(selector, context); } return unresolved(); } + private Resolution resolveIterations(IterationSelector selector, Context context) { + DiscoverySelector parentSelector = selector.getParentSelector(); + Set matches = selector.getIterationIndices().stream() // + .map(index -> context.addToParent(() -> parentSelector, + parent -> Optional.of(newDummyClassTemplateInvocationTestDescriptor(parent, index + 1)))) // + .map(this::toInvocationMatch) // + .filter(Optional::isPresent) // + .map(Optional::get) // + .collect(toSet()); + return matches.isEmpty() ? unresolved() : Resolution.matches(matches); + } + + private Resolution resolveStaticClassUniqueId(Context context, UniqueId.Segment lastSegment, + Predicate> condition, + BiFunction, ClassBasedTestDescriptor> factory) { + + String className = lastSegment.getValue(); + return ReflectionSupport.tryToLoadClass(className).toOptional() // + .filter(isTestClassWithTests) // + .filter(condition) // + .map(testClass -> toResolution( + context.addToParent(parent -> Optional.of(factory.apply(parent, testClass))))) // + .orElse(unresolved()); + } + + private Resolution resolveNestedClassUniqueId(Context context, UniqueId uniqueId, + Predicate> condition, + BiFunction, ClassBasedTestDescriptor> factory) { + + String simpleClassName = uniqueId.getLastSegment().getValue(); + 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() // + .findFirst() // + .filter(condition) // + .map(testClass -> factory.apply(parent, testClass)); + })); + } + + private ClassTemplateInvocationTestDescriptor newDummyClassTemplateInvocationTestDescriptor(TestDescriptor parent, + int index) { + UniqueId uniqueId = parent.getUniqueId().append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#" + index); + return new ClassTemplateInvocationTestDescriptor(uniqueId, (ClassTemplateTestDescriptor) parent, + DummyClassTemplateInvocationContext.INSTANCE, index, parent.getSource().orElse(null), configuration); + } + + private ClassBasedTestDescriptor newStaticClassTestDescriptor(TestDescriptor parent, Class testClass) { + return isAnnotatedWithClassTemplate.test(testClass) // + ? newStaticClassTemplateTestDescriptor(parent, testClass) // + : newClassTestDescriptor(parent, testClass); + } + + private ClassTemplateTestDescriptor newStaticClassTemplateTestDescriptor(TestDescriptor parent, + Class testClass) { + return newClassTemplateTestDescriptor(parent, ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + newClassTestDescriptor(parent, testClass)); + } + private ClassTestDescriptor newClassTestDescriptor(TestDescriptor parent, Class testClass) { return new ClassTestDescriptor( parent.getUniqueId().append(ClassTestDescriptor.SEGMENT_TYPE, testClass.getName()), testClass, configuration); } + private ClassBasedTestDescriptor newMemberClassTestDescriptor(TestDescriptor parent, Class testClass) { + return isAnnotatedWithClassTemplate.test(testClass) // + ? newNestedClassTemplateTestDescriptor(parent, testClass) // + : newNestedClassTestDescriptor(parent, testClass); + } + + private ClassTemplateTestDescriptor newNestedClassTemplateTestDescriptor(TestDescriptor parent, + Class testClass) { + return newClassTemplateTestDescriptor(parent, ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, + newNestedClassTestDescriptor(parent, testClass)); + } + private NestedClassTestDescriptor newNestedClassTestDescriptor(TestDescriptor parent, Class testClass) { UniqueId uniqueId = parent.getUniqueId().append(NestedClassTestDescriptor.SEGMENT_TYPE, testClass.getSimpleName()); return new NestedClassTestDescriptor(uniqueId, testClass, () -> getEnclosingTestClasses(parent), configuration); } + private ClassTemplateTestDescriptor newClassTemplateTestDescriptor(TestDescriptor parent, String segmentType, + ClassBasedTestDescriptor delegate) { + + delegate.setParent(parent); + String segmentValue = delegate.getUniqueId().getLastSegment().getValue(); + UniqueId uniqueId = parent.getUniqueId().append(segmentType, segmentValue); + return new ClassTemplateTestDescriptor(uniqueId, delegate); + } + + private Optional toInvocationMatch(Optional testDescriptor) { + return testDescriptor // + .map(it -> Match.exact(it, expansionCallback(it, + () -> it.getParent().map(parent -> getTestClasses((TestClassAware) parent)).orElse(emptyList())))); + } + private Resolution toResolution(Optional testDescriptor) { - return testDescriptor.map(it -> { - Class testClass = it.getTestClass(); - List> testClasses = new ArrayList<>(it.getEnclosingTestClasses()); - testClasses.add(testClass); - // @formatter:off - return Resolution.match(Match.exact(it, () -> { - Stream methods = findMethods(testClass, isTestOrTestFactoryOrTestTemplateMethod, TOP_DOWN).stream() - .map(method -> selectMethod(testClasses, method)); - Stream nestedClasses = streamNestedClasses(testClass, isNestedTestClass) - .map(nestedClass -> DiscoverySelectors.selectNestedClass(testClasses, nestedClass)); - return Stream.concat(methods, nestedClasses).collect(toCollection((Supplier>) LinkedHashSet::new)); - })); - // @formatter:on - }).orElse(unresolved()); + return testDescriptor // + .map(it -> Resolution.match(Match.exact(it, expansionCallback(it)))) // + .orElse(unresolved()); + } + + private Supplier> expansionCallback(ClassBasedTestDescriptor testDescriptor) { + return expansionCallback(testDescriptor, () -> getTestClasses(testDescriptor)); + } + + private static List> getTestClasses(TestClassAware testDescriptor) { + List> testClasses = new ArrayList<>(testDescriptor.getEnclosingTestClasses()); + testClasses.add(testDescriptor.getTestClass()); + return testClasses; + } + + private Supplier> expansionCallback(TestDescriptor testDescriptor, + Supplier>> testClassesSupplier) { + return () -> { + if (testDescriptor instanceof Filterable) { + Filterable filterable = (Filterable) testDescriptor; + filterable.getDynamicDescendantFilter().allowAll(); + } + 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)); + return Stream.concat(methods, nestedClasses).collect( + toCollection((Supplier>) LinkedHashSet::new)); + }; } private DiscoverySelector selectClass(List> classes) { @@ -161,4 +296,7 @@ private DiscoverySelector selectMethod(List> classes, Method method) { return DiscoverySelectors.selectNestedMethod(classes.subList(0, lastIndex), classes.get(lastIndex), method); } + static class DummyClassTemplateInvocationContext implements ClassTemplateInvocationContext { + private static final DummyClassTemplateInvocationContext INSTANCE = new DummyClassTemplateInvocationContext(); + } } 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 a828889cc000..974684d680cb 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 @@ -13,11 +13,14 @@ import static org.apiguardian.api.API.Status.INTERNAL; import org.apiguardian.api.API; +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.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver; +import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver.InitializationContext; /** * {@code DiscoverySelectorResolver} resolves {@link TestDescriptor TestDescriptors} @@ -33,16 +36,23 @@ @API(status = INTERNAL, since = "5.0") public class DiscoverySelectorResolver { - // @formatter:off - private static final EngineDiscoveryRequestResolver resolver = EngineDiscoveryRequestResolver.builder() - .addClassContainerSelectorResolver(new IsTestClassWithTests()) - .addSelectorResolver(context -> new ClassSelectorResolver(context.getClassNameFilter(), context.getEngineDescriptor().getConfiguration())) - .addSelectorResolver(context -> new MethodSelectorResolver(context.getEngineDescriptor().getConfiguration())) - .addTestDescriptorVisitor(context -> new ClassOrderingVisitor(context.getEngineDescriptor().getConfiguration())) - .addTestDescriptorVisitor(context -> new MethodOrderingVisitor(context.getEngineDescriptor().getConfiguration())) - .addTestDescriptorVisitor(context -> TestDescriptor::prune) + private static final EngineDiscoveryRequestResolver resolver = EngineDiscoveryRequestResolver. builder() // + .addClassContainerSelectorResolver(new IsTestClassWithTests()) // + .addSelectorResolver(ctx -> new ClassSelectorResolver(ctx.getClassNameFilter(), getConfiguration(ctx))) // + .addSelectorResolver(ctx -> new MethodSelectorResolver(getConfiguration(ctx), ctx.getIssueReporter())) // + .addTestDescriptorVisitor(ctx -> TestDescriptor.Visitor.composite( // + new ClassOrderingVisitor(getConfiguration(ctx)), // + new MethodOrderingVisitor(getConfiguration(ctx)), // + descriptor -> { + if (descriptor instanceof Validatable) { + ((Validatable) descriptor).validate(ctx.getIssueReporter()); + } + })) // .build(); - // @formatter:on + + private static JupiterConfiguration getConfiguration(InitializationContext context) { + return context.getEngineDescriptor().getConfiguration(); + } public void resolveSelectors(EngineDiscoveryRequest request, JupiterEngineDescriptor engineDescriptor) { resolver.resolve(request, engineDescriptor); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodOrderingVisitor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodOrderingVisitor.java index 30ee1342b6f5..bad48c6390a4 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodOrderingVisitor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodOrderingVisitor.java @@ -28,8 +28,7 @@ /** * @since 5.5 */ -class MethodOrderingVisitor - extends AbstractOrderingVisitor { +class MethodOrderingVisitor extends AbstractOrderingVisitor { private final JupiterConfiguration configuration; @@ -44,6 +43,13 @@ public void visit(TestDescriptor testDescriptor) { descriptor -> "Failed to order methods for " + descriptor.getTestClass()); } + @Override + protected boolean shouldNonMatchingDescriptorsComeBeforeOrderedOnes() { + // Non-matching descriptors can only contain @Nested test classes which should be + // added after local test methods. + return false; + } + /** * @since 5.4 */ @@ -65,8 +71,8 @@ private void orderContainedMethods(ClassBasedTestDescriptor classBasedTestDescri "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); + DescriptorWrapperOrderer descriptorWrapperOrderer = new DescriptorWrapperOrderer<>( + orderingAction, descriptorsAddedMessageGenerator, descriptorsRemovedMessageGenerator); orderChildrenTestDescriptors(classBasedTestDescriptor, // MethodBasedTestDescriptor.class, // 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 9d5af96aa103..f956edfd7905 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 @@ -30,8 +30,8 @@ import java.util.stream.Stream; import org.junit.jupiter.engine.config.JupiterConfiguration; -import org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor; import org.junit.jupiter.engine.descriptor.Filterable; +import org.junit.jupiter.engine.descriptor.TestClassAware; import org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor; import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateInvocationTestDescriptor; @@ -41,9 +41,9 @@ 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.platform.commons.logging.Logger; -import org.junit.platform.commons.logging.LoggerFactory; 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.DiscoverySelector; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; @@ -52,6 +52,8 @@ import org.junit.platform.engine.discovery.MethodSelector; import org.junit.platform.engine.discovery.NestedMethodSelector; import org.junit.platform.engine.discovery.UniqueIdSelector; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; import org.junit.platform.engine.support.discovery.SelectorResolver; /** @@ -59,15 +61,16 @@ */ class MethodSelectorResolver implements SelectorResolver { - private static final Logger logger = LoggerFactory.getLogger(MethodSelectorResolver.class); private static final MethodFinder methodFinder = new MethodFinder(); private static final Predicate> testClassPredicate = new IsTestClassWithTests().or( new IsNestedTestClass()); - protected final JupiterConfiguration configuration; + private final JupiterConfiguration configuration; + private final DiscoveryIssueReporter issueReporter; - MethodSelectorResolver(JupiterConfiguration configuration) { + MethodSelectorResolver(JupiterConfiguration configuration, DiscoveryIssueReporter issueReporter) { this.configuration = configuration; + this.issueReporter = issueReporter; } @Override @@ -97,14 +100,14 @@ private Resolution resolve(Context context, List> enclosingClasses, Cla .collect(toSet()); // @formatter:on if (matches.size() > 1) { - logger.warn(() -> { - Stream testDescriptors = matches.stream().map(Match::getTestDescriptor); - return String.format( - "Possible configuration error: method [%s] resulted in multiple TestDescriptors %s. " - + "This is typically the result of annotating a method with multiple competing annotations " - + "such as @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, etc.", - method.toGenericString(), testDescriptors.map(d -> d.getClass().getName()).collect(toList())); - }); + Stream testDescriptors = matches.stream().map(Match::getTestDescriptor); + String message = String.format( + "Possible configuration error: method [%s] resulted in multiple TestDescriptors %s. " + + "This is typically the result of annotating a method with multiple competing annotations " + + "such as @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, etc.", + method.toGenericString(), testDescriptors.map(d -> d.getClass().getName()).collect(toList())); + issueReporter.reportIssue( + DiscoveryIssue.builder(Severity.WARNING, message).source(MethodSource.from(method))); } return matches.isEmpty() ? unresolved() : matches(matches); } @@ -208,8 +211,7 @@ private Optional resolve(List> enclosingClasses, Class< return Optional.empty(); } return context.addToParent(() -> selectClass(enclosingClasses, testClass), // - parent -> Optional.of( - createTestDescriptor((ClassBasedTestDescriptor) parent, testClass, method, configuration))); + parent -> Optional.of(createTestDescriptor(parent, testClass, method, configuration))); } private DiscoverySelector selectClass(List> enclosingClasses, Class testClass) { @@ -225,11 +227,11 @@ private Optional resolveUniqueIdIntoTestDescriptor(UniqueId uniq if (segmentType.equals(lastSegment.getType())) { return context.addToParent(() -> selectUniqueId(uniqueId.removeLastSegment()), parent -> { String methodSpecPart = lastSegment.getValue(); - Class testClass = ((ClassBasedTestDescriptor) parent).getTestClass(); + Class testClass = ((TestClassAware) parent).getTestClass(); // @formatter:off return methodFinder.findMethod(methodSpecPart, testClass) .filter(methodPredicate) - .map(method -> createTestDescriptor((ClassBasedTestDescriptor) parent, testClass, method, configuration)); + .map(method -> createTestDescriptor(parent, testClass, method, configuration)); // @formatter:on }); } @@ -239,10 +241,11 @@ private Optional resolveUniqueIdIntoTestDescriptor(UniqueId uniq return Optional.empty(); } - private TestDescriptor createTestDescriptor(ClassBasedTestDescriptor parent, Class testClass, Method method, + private TestDescriptor createTestDescriptor(TestDescriptor parent, Class testClass, Method method, JupiterConfiguration configuration) { UniqueId uniqueId = createUniqueId(method, parent); - return createTestDescriptor(uniqueId, testClass, method, parent::getEnclosingTestClasses, configuration); + return createTestDescriptor(uniqueId, testClass, method, ((TestClassAware) parent)::getEnclosingTestClasses, + configuration); } private UniqueId createUniqueId(Method method, TestDescriptor parent) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ConditionEvaluator.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ConditionEvaluator.java index e1e0a4a89617..e51126bec2ef 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ConditionEvaluator.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ConditionEvaluator.java @@ -72,7 +72,7 @@ private ConditionEvaluationResult evaluate(ExecutionCondition condition, Extensi private void logResult(Class conditionType, ConditionEvaluationResult result, ExtensionContext context) { logger.trace(() -> format("Evaluation of condition [%s] on [%s] resulted in: %s", conditionType.getName(), - context.getElement().get(), result)); + context.getElement().orElse(null), result)); } private ConditionEvaluationException evaluationException(Class conditionType, Exception ex) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java index 65a4ddc0d38d..b2b846949637 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java @@ -99,6 +99,7 @@ public static Object[] resolveParameters(Executable executable, Optional // Ensure that the outer instance is resolved as the first parameter if // the executable is a constructor for an inner class. if (outerInstance.isPresent()) { + Preconditions.condition(parameters[0].isImplicit(), "First parameter must be implicit"); values[0] = outerInstance.get(); start = 1; } @@ -114,6 +115,9 @@ public static Object[] resolveParameters(Executable executable, Optional private static Object resolveParameter(ParameterContext parameterContext, Executable executable, ExtensionContextSupplier extensionContext, ExtensionRegistry extensionRegistry) { + Preconditions.condition(!parameterContext.getParameter().isImplicit(), + () -> String.format("Parameter at index %d must not be implicit", parameterContext.getIndex())); + try { // @formatter:off List matchingResolvers = extensionRegistry.stream(ParameterResolver.class) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java index 966bb7ef6744..eb1564cda500 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java @@ -20,7 +20,7 @@ import org.junit.platform.commons.util.Preconditions; /** - * @since 1.12 + * @since 5.12 */ class DefaultTestReporter implements TestReporter { diff --git a/junit-jupiter-engine/src/nativeImage/initialize-at-build-time b/junit-jupiter-engine/src/nativeImage/initialize-at-build-time index 05880451fb5a..4e405ef9656f 100644 --- a/junit-jupiter-engine/src/nativeImage/initialize-at-build-time +++ b/junit-jupiter-engine/src/nativeImage/initialize-at-build-time @@ -5,6 +5,8 @@ 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 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 063d0527103f..d7f6e252e096 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 @@ -12,6 +12,9 @@ import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass; +import org.junit.jupiter.api.ClassTemplate; +import org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor; +import org.junit.jupiter.engine.descriptor.ClassTemplateTestDescriptor; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; import org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor; @@ -19,6 +22,8 @@ import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateInvocationTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor; +import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.engine.UniqueId; /** @@ -31,16 +36,29 @@ public class JupiterUniqueIdBuilder { public static UniqueId uniqueIdForClass(Class clazz) { - UniqueId containerId = engineId(); if (isInnerClass(clazz)) { - containerId = uniqueIdForClass(clazz.getEnclosingClass()); - return containerId.append(NestedClassTestDescriptor.SEGMENT_TYPE, clazz.getSimpleName()); + var segmentType = classSegmentType(clazz, NestedClassTestDescriptor.SEGMENT_TYPE, + ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE); + return uniqueIdForClass(clazz.getEnclosingClass()).append(segmentType, clazz.getSimpleName()); } - return containerId.append(ClassTestDescriptor.SEGMENT_TYPE, clazz.getName()); + return uniqueIdForStaticClass(clazz.getName()); } - public static UniqueId uniqueIdForTopLevelClass(String className) { - return engineId().append(ClassTestDescriptor.SEGMENT_TYPE, className); + public static UniqueId uniqueIdForStaticClass(String className) { + return engineId().append(staticClassSegmentType(className), className); + } + + private static String staticClassSegmentType(String className) { + return ReflectionSupport.tryToLoadClass(className).toOptional() // + .map(it -> classSegmentType(it, ClassTestDescriptor.SEGMENT_TYPE, + ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE)) // + .orElse(ClassTestDescriptor.SEGMENT_TYPE); + } + + private static String classSegmentType(Class clazz, String regularSegmentType, String classTemplateSegmentType) { + return AnnotationSupport.isAnnotated(clazz, ClassTemplate.class) // + ? classTemplateSegmentType // + : regularSegmentType; } public static UniqueId uniqueIdForMethod(Class clazz, String methodPart) { @@ -59,6 +77,10 @@ public static UniqueId appendTestTemplateInvocationSegment(UniqueId parentId, in return parentId.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#" + index); } + public static UniqueId appendClassTemplateInvocationSegment(UniqueId parentId, int index) { + return parentId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#" + index); + } + public static UniqueId engineId() { return UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); } diff --git a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java similarity index 74% rename from junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java rename to junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java index dad2ce3e2115..592be9f94445 100644 --- a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java +++ b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java @@ -10,6 +10,9 @@ package org.junit.jupiter.params; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DEFAULT_DISPLAY_NAME; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DISPLAY_NAME_PLACEHOLDER; + import java.util.List; import java.util.stream.IntStream; @@ -28,7 +31,7 @@ @Fork(1) @Warmup(iterations = 1, time = 2) @Measurement(iterations = 3, time = 2) -public class ParameterizedTestNameFormatterBenchmarks { +public class ParameterizedInvocationNameFormatterBenchmarks { @Param({ "1", "2", "4", "10", "100", "1000" }) private int numberOfParameters; @@ -45,13 +48,12 @@ public void setUp() { @Benchmark public void formatTestNames(Blackhole blackhole) throws Exception { var method = TestCase.class.getDeclaredMethod("parameterizedTest", int.class); - var formatter = new ParameterizedTestNameFormatter( - ParameterizedTest.DISPLAY_NAME_PLACEHOLDER + " " + ParameterizedTest.DEFAULT_DISPLAY_NAME + " ({0})", - "displayName", new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class)), - 512); + var formatter = new ParameterizedInvocationNameFormatter( + DISPLAY_NAME_PLACEHOLDER + " " + DEFAULT_DISPLAY_NAME + " ({0})", "displayName", + new ParameterizedTestContext(TestCase.class, method, method.getAnnotation(ParameterizedTest.class)), 512); for (int i = 0; i < argumentsList.size(); i++) { Arguments arguments = argumentsList.get(i); - blackhole.consume(formatter.format(i, arguments, arguments.get())); + blackhole.consume(formatter.format(i, EvaluatedArgumentSet.allOf(arguments))); } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AbstractParameterizedClassInvocationLifecycleMethodInvoker.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AbstractParameterizedClassInvocationLifecycleMethodInvoker.java new file mode 100644 index 000000000000..0d20faee725b --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AbstractParameterizedClassInvocationLifecycleMethodInvoker.java @@ -0,0 +1,72 @@ +/* + * 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; + +import org.junit.jupiter.api.extension.ExecutableInvoker; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * @since 5.13 + */ +abstract class AbstractParameterizedClassInvocationLifecycleMethodInvoker implements ParameterResolver { + + private final ParameterizedClassContext declarationContext; + private final EvaluatedArgumentSet arguments; + private final int invocationIndex; + private final ResolutionCache resolutionCache; + private final ArgumentSetLifecycleMethod lifecycleMethod; + + AbstractParameterizedClassInvocationLifecycleMethodInvoker(ParameterizedClassContext declarationContext, + EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache, + ArgumentSetLifecycleMethod lifecycleMethod) { + this.declarationContext = declarationContext; + this.arguments = arguments; + this.invocationIndex = invocationIndex; + this.resolutionCache = resolutionCache; + this.lifecycleMethod = lifecycleMethod; + } + + @Override + public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { + return ExtensionContextScope.TEST_METHOD; + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.getDeclaringExecutable().equals(this.lifecycleMethod.method) // + && this.lifecycleMethod.parameterResolver.supports(parameterContext); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return this.lifecycleMethod.parameterResolver // + .resolve(parameterContext, extensionContext, this.arguments, this.invocationIndex, + this.resolutionCache); + } + + protected void invoke(ExtensionContext context) { + if (isCorrectTestClass(context)) { + ExecutableInvoker executableInvoker = context.getExecutableInvoker(); + Object testInstance = context.getTestInstance().orElse(null); + executableInvoker.invoke(this.lifecycleMethod.method, testInstance); + } + } + + private boolean isCorrectTestClass(ExtensionContext context) { + return this.declarationContext.getAnnotatedElement().equals(context.getTestClass().orElse(null)); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterParameterizedClassInvocation.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterParameterizedClassInvocation.java new file mode 100644 index 000000000000..b7cb8fd6341a --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterParameterizedClassInvocation.java @@ -0,0 +1,170 @@ +/* + * 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; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ClassTemplateInvocationLifecycleMethod; +import org.junit.jupiter.params.aggregator.AggregateWith; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; + +/** + * {@code @AfterParameterizedClassInvocation} is used to signal that the + * annotated method should be executed before each + * invocation of the current {@link ParameterizedClass @ParameterizedClass}. + * + *

Declaring {@code @AfterParameterizedClassInvocation} methods in a regular, + * non-parameterized test class has no effect and will be ignored. + * + *

Method Signatures

+ * + *

{@code @AfterParameterizedClassInvocation} methods must have a + * {@code void} return type, must not be private, and must be {@code static} by + * default. Consequently, {@code @AfterParameterizedClassInvocation} methods are + * not supported in {@link org.junit.jupiter.api.Nested @Nested} test classes or + * as interface default methods unless the test class is annotated with + * {@link org.junit.jupiter.api.TestInstance @TestInstance(Lifecycle.PER_CLASS)}. + * However, beginning with Java 16 {@code @AfterParameterizedClassInvocation} + * methods may be declared as {@code static} in + * {@link org.junit.jupiter.api.Nested @Nested} test classes, in which case the + * {@code Lifecycle.PER_CLASS} restriction no longer applies. + * + *

Method Arguments

+ * + *

{@code @AfterParameterizedClassInvocation} methods may optionally declare + * parameters that are resolved depending on the setting of the + * {@link #injectArguments()} attribute. + * + *

If {@link #injectArguments()} is set to {@code false}, the parameters must + * be resolved by other registered + * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers}. + * + *

If {@link #injectArguments()} is set to {@code true} (the default), the + * method must declare the same parameters, in the same order, as the + * indexed parameters (see + * {@link ParameterizedClass @ParameterizedClass}) of the parameterized test + * class. It may declare a subset of the indexed parameters starting from the + * first argument. Additionally, the method may declare custom aggregator + * parameters (see {@link ParameterizedClass @ParameterizedClass}) at the + * end of its parameter list. If the method declares additional parameters after + * these aggregator parameters, or more parameters than the class has indexed + * parameters, they may be resolved by other + * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers}. + * + *

For example, given a {@link ParameterizedClass @ParameterizedClass} with + * indexed parameters of type {@code int} and {@code String}, the + * following method signatures are valid: + * + *

{@code
+ * @AfterParameterizedClassInvocation
+ * void afterInvocation() { ... }
+ *
+ * @AfterParameterizedClassInvocation
+ * void afterInvocation(int number) { ... }
+ *
+ * @AfterParameterizedClassInvocation
+ * void afterInvocation(int number, String text) { ... }
+ *
+ * @AfterParameterizedClassInvocation
+ * void afterInvocation(int number, String text, TestInfo testInfo) { ... }
+ *
+ * @AfterParameterizedClassInvocation
+ * void afterInvocation(ArgumentsAccessor accessor) { ... }
+ *
+ * @AfterParameterizedClassInvocation
+ * void afterInvocation(ArgumentsAccessor accessor, TestInfo testInfo) { ... }
+ *
+ * @AfterParameterizedClassInvocation
+ * void afterInvocation(int number, String text, ArgumentsAccessor accessor) { ... }
+ *
+ * @AfterParameterizedClassInvocation
+ * void afterInvocation(int number, String text, ArgumentsAccessor accessor, TestInfo testInfo) { ... }
+ * }
+ * + *

In the snippet above,{@link ArgumentsAccessor} is used as an example of an + * aggregator parameter but the same applies to any parameter annotated with + * {@link AggregateWith @AggregateWith}. The parameter of type + * {@link org.junit.jupiter.api.TestInfo TestInfo} is used as an example of a + * parameter that is resolved by another + * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolver}. + * + *

Inheritance and Execution Order

+ * + *

{@code @AfterParameterizedClassInvocation} methods are inherited from + * superclasses as long as they are not overridden according to the + * visibility rules of the Java language. Furthermore, + * {@code @AfterParameterizedClassInvocation} methods from superclasses will be + * executed before {@code @AfterParameterizedClassInvocation} methods in + * subclasses. + * + *

Similarly, {@code @AfterParameterizedClassInvocation} methods declared in + * an interface are inherited as long as they are not overridden, and + * {@code @AfterParameterizedClassInvocation} methods from an interface will be + * executed before {@code @AfterParameterizedClassInvocation} methods in the + * class that implements the interface. + * + *

JUnit Jupiter does not guarantee the execution order of multiple + * {@code @AfterParameterizedClassInvocation} methods that are declared within a + * single parameterized test class or test interface. While it may at times + * appear that these methods are invoked in alphabetical order, they are in fact + * sorted using an algorithm that is deterministic but intentionally + * non-obvious. + * + *

In addition, {@code @AfterParameterizedClassInvocation} methods are in no + * way linked to {@code @BeforeParameterizedClassInvocation} methods. + * Consequently, there are no guarantees with regard to their wrapping + * behavior. For example, given two {@code @AfterParameterizedClassInvocation} + * methods {@code createA()} and {@code createB()} as well as two + * {@code @BeforeParameterizedClassInvocation} methods {@code destroyA()} and + * {@code destroyB()}, the order in which the + * {@code @AfterParameterizedClassInvocation} methods are executed (e.g. + * {@code createA()} before {@code createB()}) does not imply any order for the + * seemingly corresponding {@code @BeforeParameterizedClassInvocation} methods. + * In other words, {@code destroyA()} might be called before or after + * {@code destroyB()}. The JUnit Team therefore recommends that developers + * declare at most one {@code @AfterParameterizedClassInvocation} method and at + * most one {@code @BeforeParameterizedClassInvocation} method per test class or + * test interface unless there are no dependencies between the + * {@code @AfterParameterizedClassInvocation} methods or between the + * {@code @BeforeParameterizedClassInvocation} methods. + * + *

Composition

+ * + *

{@code @AfterParameterizedClassInvocation} may be used as a + * meta-annotation in order to create a custom composed annotation that + * inherits the semantics of {@code @AfterParameterizedClassInvocation}. + * + * @since 5.13 + * @see ParameterizedClass + * @see BeforeParameterizedClassInvocation + * @see org.junit.jupiter.api.TestInstance + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = EXPERIMENTAL, since = "5.13") +@ClassTemplateInvocationLifecycleMethod(classTemplateAnnotation = ParameterizedClass.class, lifecycleMethodAnnotation = AfterParameterizedClassInvocation.class) +public @interface AfterParameterizedClassInvocation { + + /** + * Whether the arguments of the parameterized test class should be injected + * 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/AfterParameterizedClassInvocationMethodInvoker.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterParameterizedClassInvocationMethodInvoker.java new file mode 100644 index 000000000000..7f44896e3f78 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/AfterParameterizedClassInvocationMethodInvoker.java @@ -0,0 +1,33 @@ +/* + * 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; + +import org.junit.jupiter.api.extension.AfterClassTemplateInvocationCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * @since 5.13 + */ +class AfterParameterizedClassInvocationMethodInvoker extends AbstractParameterizedClassInvocationLifecycleMethodInvoker + implements AfterClassTemplateInvocationCallback { + + AfterParameterizedClassInvocationMethodInvoker(ParameterizedClassContext declarationContext, + EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache, + ArgumentSetLifecycleMethod lifecycleMethod) { + super(declarationContext, arguments, invocationIndex, resolutionCache, lifecycleMethod); + } + + @Override + public void afterClassTemplateInvocation(ExtensionContext context) { + invoke(context); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidationMode.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidationMode.java index fe50db276cdf..2188d8a170b1 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidationMode.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidationMode.java @@ -14,39 +14,46 @@ import org.junit.jupiter.params.provider.ArgumentsSource; /** - * Enumeration of argument count validation modes for {@link ParameterizedTest @ParameterizedTest}. + * Enumeration of argument count validation modes for + * {@link ParameterizedClass @ParameterizedClass} and + * {@link ParameterizedTest @ParameterizedTest}. * - *

When an {@link ArgumentsSource} provides more arguments than declared by the test method, - * there might be a bug in the test method or the {@link ArgumentsSource}. - * By default, the additional arguments are ignored. - * {@link ArgumentCountValidationMode} allows you to control how additional arguments are handled. + *

When an {@link ArgumentsSource} provides more arguments than declared by + * the parameterized class or method, there might be a bug in the class/method + * or the {@link ArgumentsSource}. By default, the additional arguments are + * ignored. {@link ArgumentCountValidationMode} allows you to control how + * additional arguments are handled. * * @since 5.12 - * @see ParameterizedTest + * @see ParameterizedClass#argumentCountValidation() + * @see ParameterizedTest#argumentCountValidation() */ @API(status = API.Status.EXPERIMENTAL, since = "5.12") public enum ArgumentCountValidationMode { + /** * Use the default validation mode. * *

The default validation mode may be changed via the - * {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} configuration parameter - * (see the User Guide for details on configuration parameters). + * {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} + * configuration parameter (see the User Guide for details on configuration + * parameters). */ DEFAULT, /** * Use the "none" argument count validation mode. * - *

When there are more arguments provided than declared by the test method, - * these additional arguments are ignored. + *

When there are more arguments provided than declared by the + * parameterized class or method, these additional arguments are ignored. */ NONE, /** * Use the strict argument count validation mode. * - *

When there are more arguments provided than declared by the test method, this raises an error. + *

When there are more arguments provided than declared by the + * parameterized class or method, this raises an error. */ STRICT, } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java index 42a1f48ea54c..322aa34d557d 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java @@ -10,57 +10,47 @@ package org.junit.jupiter.params; -import java.lang.reflect.Method; import java.util.Arrays; import java.util.Optional; import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.InvocationInterceptor; -import org.junit.jupiter.api.extension.ReflectiveInvocationContext; -import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.Preconditions; -class ArgumentCountValidator implements InvocationInterceptor { +class ArgumentCountValidator { + private static final Logger logger = LoggerFactory.getLogger(ArgumentCountValidator.class); static final String ARGUMENT_COUNT_VALIDATION_KEY = "junit.jupiter.params.argumentCountValidation"; - private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create( - ArgumentCountValidator.class); + private static final Namespace NAMESPACE = Namespace.create(ArgumentCountValidator.class); - private final ParameterizedTestMethodContext methodContext; - private final Arguments arguments; + private final ParameterizedDeclarationContext declarationContext; + private final EvaluatedArgumentSet arguments; - ArgumentCountValidator(ParameterizedTestMethodContext methodContext, Arguments arguments) { - this.methodContext = methodContext; + ArgumentCountValidator(ParameterizedDeclarationContext declarationContext, EvaluatedArgumentSet arguments) { + this.declarationContext = declarationContext; this.arguments = arguments; } - @Override - public void interceptTestTemplateMethod(InvocationInterceptor.Invocation invocation, - ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - validateArgumentCount(extensionContext, arguments); - invocation.proceed(); - } - - private ExtensionContext.Store getStore(ExtensionContext context) { - return context.getRoot().getStore(NAMESPACE); - } - - private void validateArgumentCount(ExtensionContext extensionContext, Arguments arguments) { + void validate(ExtensionContext extensionContext) { ArgumentCountValidationMode argumentCountValidationMode = getArgumentCountValidationMode(extensionContext); switch (argumentCountValidationMode) { case DEFAULT: case NONE: return; case STRICT: - int testParamCount = extensionContext.getRequiredTestMethod().getParameterCount(); - int argumentsCount = arguments.get().length; - Preconditions.condition(testParamCount == argumentsCount, () -> String.format( - "Configuration error: the @ParameterizedTest has %s argument(s) but there were %s argument(s) provided.%nNote: the provided arguments are %s", - testParamCount, argumentsCount, Arrays.toString(arguments.get()))); + int consumedCount = this.declarationContext.getResolverFacade().determineConsumedArgumentCount( + this.arguments); + int totalCount = this.arguments.getTotalLength(); + Preconditions.condition(consumedCount == totalCount, () -> String.format( + "Configuration error: @%s consumes %s %s but there %s %s %s provided.%nNote: the provided arguments were %s", + this.declarationContext.getAnnotationName(), consumedCount, + pluralize(consumedCount, "parameter", "parameters"), pluralize(totalCount, "was", "were"), + totalCount, pluralize(totalCount, "argument", "arguments"), + Arrays.toString(this.arguments.getAllPayloads()))); break; default: throw new ExtensionConfigurationException( @@ -69,9 +59,9 @@ private void validateArgumentCount(ExtensionContext extensionContext, Arguments } private ArgumentCountValidationMode getArgumentCountValidationMode(ExtensionContext extensionContext) { - ParameterizedTest parameterizedTest = methodContext.annotation; - if (parameterizedTest.argumentCountValidation() != ArgumentCountValidationMode.DEFAULT) { - return parameterizedTest.argumentCountValidation(); + ArgumentCountValidationMode mode = declarationContext.getArgumentCountValidationMode(); + if (mode != ArgumentCountValidationMode.DEFAULT) { + return mode; } else { return getArgumentCountValidationModeConfiguration(extensionContext); @@ -108,4 +98,12 @@ private ArgumentCountValidationMode getArgumentCountValidationModeConfiguration( } }, ArgumentCountValidationMode.class); } + + private static String pluralize(int count, String singular, String plural) { + return count == 1 ? singular : plural; + } + + private ExtensionContext.Store getStore(ExtensionContext context) { + return context.getRoot().getStore(NAMESPACE); + } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentSetLifecycleMethod.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentSetLifecycleMethod.java new file mode 100644 index 000000000000..13ed374ebf14 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentSetLifecycleMethod.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.params; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.util.Preconditions; + +/** + * @since 5.13 + */ +class ArgumentSetLifecycleMethod { + + final Method method; + final ParameterResolver parameterResolver; + + ArgumentSetLifecycleMethod(Method method) { + this(method, ParameterResolver.DISABLED); + } + + ArgumentSetLifecycleMethod(Method method, ParameterResolver parameterResolver) { + this.method = Preconditions.notNull(method, "method must not be null"); + this.parameterResolver = Preconditions.notNull(parameterResolver, "parameterResolver must not be null"); + } + + interface ParameterResolver { + + ParameterResolver DISABLED = new ParameterResolver() { + @Override + public boolean supports(ParameterContext parameterContext) { + return false; + } + + @Override + public Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) { + throw new JUnitException("Parameter resolution is disabled"); + } + }; + + boolean supports(ParameterContext parameterContext); + + Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache); + + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeClassTemplateInvocationFieldInjector.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeClassTemplateInvocationFieldInjector.java new file mode 100644 index 000000000000..a6f4a3d2940f --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeClassTemplateInvocationFieldInjector.java @@ -0,0 +1,39 @@ +/* + * 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; + +import org.junit.jupiter.api.extension.BeforeClassTemplateInvocationCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +class BeforeClassTemplateInvocationFieldInjector implements BeforeClassTemplateInvocationCallback { + + private final ResolverFacade resolverFacade; + private final EvaluatedArgumentSet arguments; + private final int invocationIndex; + private final ResolutionCache resolutionCache; + + BeforeClassTemplateInvocationFieldInjector(ResolverFacade resolverFacade, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { + this.resolverFacade = resolverFacade; + this.arguments = arguments; + this.invocationIndex = invocationIndex; + this.resolutionCache = resolutionCache; + } + + @Override + public void beforeClassTemplateInvocation(ExtensionContext extensionContext) { + extensionContext.getTestInstance() // + .ifPresent(testInstance -> this.resolverFacade // + .resolveAndInjectFields(testInstance, extensionContext, this.arguments, this.invocationIndex, + this.resolutionCache)); + } + +} 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 new file mode 100644 index 000000000000..a71573eb1897 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeParameterizedClassInvocation.java @@ -0,0 +1,171 @@ +/* + * 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; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ClassTemplateInvocationLifecycleMethod; +import org.junit.jupiter.params.aggregator.AggregateWith; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; + +/** + * {@code @BeforeParameterizedClassInvocation} is used to signal that the + * annotated method should be executed before each + * invocation of the current {@link ParameterizedClass @ParameterizedClass}. + * + *

Declaring {@code @BeforeParameterizedClassInvocation} methods in a + * regular, non-parameterized test class has no effect and will be ignored. + * + *

Method Signatures

+ * + *

{@code @BeforeParameterizedClassInvocation} methods must have a + * {@code void} return type, must not be private, and must be {@code static} by + * default. Consequently, {@code @BeforeParameterizedClassInvocation} methods + * are not supported in {@link org.junit.jupiter.api.Nested @Nested} test + * classes or as interface default methods unless the test class is + * annotated with + * {@link org.junit.jupiter.api.TestInstance @TestInstance(Lifecycle.PER_CLASS)}. + * However, beginning with Java 16 {@code @BeforeParameterizedClassInvocation} + * methods may be declared as {@code static} in + * {@link org.junit.jupiter.api.Nested @Nested} test classes, in which case the + * {@code Lifecycle.PER_CLASS} restriction no longer applies. + * + *

Method Arguments

+ * + *

{@code @BeforeParameterizedClassInvocation} methods may optionally declare + * parameters that are resolved depending on the setting of the + * {@link #injectArguments()} attribute. + * + *

If {@link #injectArguments()} is set to {@code false}, the parameters must + * be resolved by other registered + * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers}. + * + *

If {@link #injectArguments()} is set to {@code true} (the default), the + * method must declare the same parameters, in the same order, as the + * indexed parameters (see + * {@link ParameterizedClass @ParameterizedClass}) of the parameterized test + * class. It may declare a subset of the indexed parameters starting from the + * first argument. Additionally, the method may declare custom aggregator + * parameters (see {@link ParameterizedClass @ParameterizedClass}) at the + * end of its parameter list. If the method declares additional parameters after + * these aggregator parameters, or more parameters than the class has indexed + * parameters, they may be resolved by other + * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers}. + * + *

For example, given a {@link ParameterizedClass @ParameterizedClass} with + * indexed parameters of type {@code int} and {@code String}, the + * following method signatures are valid: + * + *

{@code
+ * @BeforeParameterizedClassInvocation
+ * void beforeInvocation() { ... }
+ *
+ * @BeforeParameterizedClassInvocation
+ * void beforeInvocation(int number) { ... }
+ *
+ * @BeforeParameterizedClassInvocation
+ * void beforeInvocation(int number, String text) { ... }
+ *
+ * @BeforeParameterizedClassInvocation
+ * void beforeInvocation(int number, String text, TestInfo testInfo) { ... }
+ *
+ * @BeforeParameterizedClassInvocation
+ * void beforeInvocation(ArgumentsAccessor accessor) { ... }
+ *
+ * @BeforeParameterizedClassInvocation
+ * void beforeInvocation(ArgumentsAccessor accessor, TestInfo testInfo) { ... }
+ *
+ * @BeforeParameterizedClassInvocation
+ * void beforeInvocation(int number, String text, ArgumentsAccessor accessor) { ... }
+ *
+ * @BeforeParameterizedClassInvocation
+ * void beforeInvocation(int number, String text, ArgumentsAccessor accessor, TestInfo testInfo) { ... }
+ * }
+ * + *

In the snippet above,{@link ArgumentsAccessor} is used as an example of an + * aggregator parameter but the same applies to any parameter annotated with + * {@link AggregateWith @AggregateWith}. The parameter of type + * {@link org.junit.jupiter.api.TestInfo TestInfo} is used as an example of a + * parameter that is resolved by another + * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolver}. + * + *

Inheritance and Execution Order

+ * + *

{@code @BeforeParameterizedClassInvocation} methods are inherited from + * superclasses as long as they are not overridden according to the + * visibility rules of the Java language. Furthermore, + * {@code @BeforeParameterizedClassInvocation} methods from superclasses will be + * executed before {@code @BeforeParameterizedClassInvocation} methods in + * subclasses. + * + *

Similarly, {@code @BeforeParameterizedClassInvocation} methods declared in + * an interface are inherited as long as they are not overridden, and + * {@code @BeforeParameterizedClassInvocation} methods from an interface will be + * executed before {@code @BeforeParameterizedClassInvocation} methods in the + * class that implements the interface. + * + *

JUnit Jupiter does not guarantee the execution order of multiple + * {@code @BeforeParameterizedClassInvocation} methods that are declared within + * a single parameterized test class or test interface. While it may at times + * appear that these methods are invoked in alphabetical order, they are in fact + * sorted using an algorithm that is deterministic but intentionally + * non-obvious. + * + *

In addition, {@code @BeforeParameterizedClassInvocation} methods are in no + * way linked to {@code @AfterParameterizedClassInvocation} methods. + * Consequently, there are no guarantees with regard to their wrapping + * behavior. For example, given two {@code @BeforeParameterizedClassInvocation} + * methods {@code createA()} and {@code createB()} as well as two + * {@code @AfterParameterizedClassInvocation} methods {@code destroyA()} and + * {@code destroyB()}, the order in which the + * {@code @BeforeParameterizedClassInvocation} methods are executed (e.g. + * {@code createA()} before {@code createB()}) does not imply any order for the + * seemingly corresponding {@code @AfterParameterizedClassInvocation} methods. + * In other words, {@code destroyA()} might be called before or after + * {@code destroyB()}. The JUnit Team therefore recommends that developers + * declare at most one {@code @BeforeParameterizedClassInvocation} method and at + * most one {@code @AfterParameterizedClassInvocation} method per test class or + * test interface unless there are no dependencies between the + * {@code @BeforeParameterizedClassInvocation} methods or between the + * {@code @AfterParameterizedClassInvocation} methods. + * + *

Composition

+ * + *

{@code @BeforeParameterizedClassInvocation} may be used as a + * meta-annotation in order to create a custom composed annotation that + * inherits the semantics of {@code @BeforeParameterizedClassInvocation}. + * + * @since 5.13 + * @see ParameterizedClass + * @see AfterParameterizedClassInvocation + * @see org.junit.jupiter.api.TestInstance + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = EXPERIMENTAL, since = "5.13") +@ClassTemplateInvocationLifecycleMethod(classTemplateAnnotation = ParameterizedClass.class, lifecycleMethodAnnotation = BeforeParameterizedClassInvocation.class) +public @interface BeforeParameterizedClassInvocation { + + /** + * Whether the arguments of the parameterized test class should be injected + * into the annotated method (defaults to {@code false}). + */ + boolean injectArguments() default true; + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeParameterizedClassInvocationMethodInvoker.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeParameterizedClassInvocationMethodInvoker.java new file mode 100644 index 000000000000..201cbf4d3286 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeParameterizedClassInvocationMethodInvoker.java @@ -0,0 +1,33 @@ +/* + * 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; + +import org.junit.jupiter.api.extension.BeforeClassTemplateInvocationCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * @since 5.13 + */ +class BeforeParameterizedClassInvocationMethodInvoker extends AbstractParameterizedClassInvocationLifecycleMethodInvoker + implements BeforeClassTemplateInvocationCallback { + + BeforeParameterizedClassInvocationMethodInvoker(ParameterizedClassContext declarationContext, + EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache, + ArgumentSetLifecycleMethod lifecycleMethod) { + super(declarationContext, arguments, invocationIndex, resolutionCache, lifecycleMethod); + } + + @Override + public void beforeClassTemplateInvocation(ExtensionContext context) { + invoke(context); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ClassTemplateConstructorParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ClassTemplateConstructorParameterResolver.java new file mode 100644 index 000000000000..605f6e473321 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ClassTemplateConstructorParameterResolver.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 org.junit.jupiter.params; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; + +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * @since 5.13 + */ +class ClassTemplateConstructorParameterResolver extends ParameterizedInvocationParameterResolver { + + private final Class classTemplateClass; + + ClassTemplateConstructorParameterResolver(ParameterizedClassContext classContext, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { + super(classContext.getResolverFacade(), arguments, invocationIndex, resolutionCache); + this.classTemplateClass = classContext.getAnnotatedElement(); + } + + @Override + protected boolean isSupportedOnConstructorOrMethod(Executable declaringExecutable, + ExtensionContext extensionContext) { + return declaringExecutable instanceof Constructor // + && this.classTemplateClass.equals(declaringExecutable.getDeclaringClass()); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/DefaultParameterInfo.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/DefaultParameterInfo.java new file mode 100644 index 000000000000..96fffa5039d5 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/DefaultParameterInfo.java @@ -0,0 +1,44 @@ +/* + * 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; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.jupiter.params.support.ParameterInfo; + +/** + * @since 5.13 + */ +class DefaultParameterInfo implements ParameterInfo { + + private final ParameterDeclarations declarations; + private final ArgumentsAccessor arguments; + + DefaultParameterInfo(ParameterDeclarations declarations, ArgumentsAccessor arguments) { + this.declarations = declarations; + this.arguments = arguments; + } + + @Override + public ParameterDeclarations getDeclarations() { + return this.declarations; + } + + @Override + public ArgumentsAccessor getArguments() { + return this.arguments; + } + + void store(ExtensionContext context) { + context.getStore(NAMESPACE).put(KEY, this); + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java new file mode 100644 index 000000000000..623f262ddc91 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java @@ -0,0 +1,111 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.IntUnaryOperator; + +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.Arguments.ArgumentSet; +import org.junit.platform.commons.util.Preconditions; + +/** + * Encapsulates the evaluation of an {@link Arguments} instance (so it happens + * only once) and access to the resulting argument values. + * + *

The provided accessor methods are focused on the different use cases and + * make it less error-prone to access the argument values. + * + * @since 5.13 + */ +class EvaluatedArgumentSet { + + static EvaluatedArgumentSet allOf(Arguments arguments) { + Object[] all = arguments.get(); + return create(all, all, arguments); + } + + static EvaluatedArgumentSet of(Arguments arguments, IntUnaryOperator consumedLengthComputer) { + Object[] all = arguments.get(); + Object[] consumed = dropSurplus(all, consumedLengthComputer.applyAsInt(all.length)); + return create(all, consumed, arguments); + } + + private static EvaluatedArgumentSet create(Object[] all, Object[] consumed, Arguments arguments) { + return new EvaluatedArgumentSet(all, consumed, determineName(arguments)); + } + + private final Object[] all; + private final Object[] consumed; + private final Optional name; + + private EvaluatedArgumentSet(Object[] all, Object[] consumed, Optional name) { + this.all = all; + this.consumed = consumed; + this.name = name; + } + + int getTotalLength() { + return this.all.length; + } + + Object[] getAllPayloads() { + return extractFromNamed(this.all, Named::getPayload); + } + + int getConsumedLength() { + return this.consumed.length; + } + + Object[] getConsumedNames() { + return extractFromNamed(this.consumed, Named::getName); + } + + Object[] getConsumedPayloads() { + return extractFromNamed(this.consumed, Named::getPayload); + } + + Object getConsumedPayload(int index) { + return extractFromNamed(this.consumed[index], Named::getPayload); + } + + Optional getName() { + return this.name; + } + + private static Object[] dropSurplus(Object[] arguments, int newLength) { + Preconditions.condition(newLength <= arguments.length, + () -> String.format("New length %d must be less than or equal to the total length %d", newLength, + arguments.length)); + return arguments.length > newLength ? Arrays.copyOf(arguments, newLength) : arguments; + } + + private static Optional determineName(Arguments arguments) { + if (arguments instanceof ArgumentSet) { + return Optional.of(((ArgumentSet) arguments).getName()); + } + return Optional.empty(); + } + + private static Object[] extractFromNamed(Object[] arguments, Function, Object> mapper) { + return Arrays.stream(arguments) // + .map(argument -> extractFromNamed(argument, mapper)) // + .toArray(); + } + + private static Object extractFromNamed(Object argument, Function, Object> mapper) { + return argument instanceof Named ? mapper.apply((Named) argument) : argument; + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/InstancePostProcessingClassTemplateFieldInjector.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/InstancePostProcessingClassTemplateFieldInjector.java new file mode 100644 index 000000000000..eb6dcd918796 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/InstancePostProcessingClassTemplateFieldInjector.java @@ -0,0 +1,41 @@ +/* + * 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; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; + +class InstancePostProcessingClassTemplateFieldInjector implements TestInstancePostProcessor { + + private final ResolverFacade resolverFacade; + private final EvaluatedArgumentSet arguments; + private final int invocationIndex; + private final ResolutionCache resolutionCache; + + InstancePostProcessingClassTemplateFieldInjector(ResolverFacade resolverFacade, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { + this.resolverFacade = resolverFacade; + this.arguments = arguments; + this.invocationIndex = invocationIndex; + this.resolutionCache = resolutionCache; + } + + @Override + public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { + return ExtensionContextScope.TEST_METHOD; + } + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) { + this.resolverFacade.resolveAndInjectFields(testInstance, extensionContext, this.arguments, this.invocationIndex, + this.resolutionCache); + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/Parameter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/Parameter.java new file mode 100644 index 000000000000..70325ff71746 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/Parameter.java @@ -0,0 +1,65 @@ +/* + * 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; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.junit.jupiter.params.aggregator.AggregateWith; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; + +/** + * {@code @Parameter} is used to signal that a field in a + * {@code @ParameterizedClass} constitutes a parameter and marks it for + * field injection. + * + *

{@code @Parameter} may also be used as a meta-annotation in order to + * create a custom composed annotation that inherits the semantics of + * {@code @Parameter}. + * + * @since 5.13 + * @see ParameterizedClass + * @see ArgumentsAccessor + * @see AggregateWith + * @see org.junit.jupiter.params.converter.ArgumentConverter + * @see org.junit.jupiter.params.converter.ConvertWith + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD }) +@Documented +@API(status = EXPERIMENTAL, since = "5.13") +public @interface Parameter { + + /** + * Constant that indicates that the index of the parameter is unset. + */ + int UNSET_INDEX = -1; + + /** + * {@return the index of the parameter in the list of parameters} + * + *

Must be {@value #UNSET_INDEX} (the default) for aggregators, + * that is any field of type {@link ArgumentsAccessor} or any field + * annotated with {@link AggregateWith @AggregateWith}. + * + *

May be omitted if there's a single indexed parameter. + * Otherwise, must be unique among all indexed parameters of the + * parameterized class and its superclasses. + */ + int value() default UNSET_INDEX; + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java new file mode 100644 index 000000000000..cf64caaab50d --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java @@ -0,0 +1,252 @@ +/* + * 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; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.ClassTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.provider.ArgumentsSource; + +/** + * {@code @ParameterizedClass} is used to signal that the annotated class is + * a parameterized test class. + * + *

Arguments Providers and Sources

+ * + *

A {@code @ParameterizedClass} must specify at least one + * {@link org.junit.jupiter.params.provider.ArgumentsProvider ArgumentsProvider} + * via {@link org.junit.jupiter.params.provider.ArgumentsSource @ArgumentsSource} + * or a corresponding composed annotation (e.g., {@code @ValueSource}, + * {@code @CsvSource}, etc.). The provider is responsible for providing a + * {@link java.util.stream.Stream Stream} of + * {@link org.junit.jupiter.params.provider.Arguments Arguments} that will be + * used to invoke the parameterized class. + * + *

Field or Constructor Injection

+ * + *

The provided arguments can either be injected into fields annotated with + * {@link Parameter @Parameter} or passed to the unique constructor of the + * parameterized class. If a {@code @Parameter}-annotated field is declared in + * the parameterized class or one of its superclasses, field injection will be + * used. Otherwise, constructor injection will be used. + * + *

Constructor Injection

+ * + *

A {@code @ParameterizedClass} constructor may declare additional + * parameters at the end of its parameter list to be resolved by other + * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers} + * (e.g., {@code TestInfo}, {@code TestReporter}, etc.). Specifically, such a + * constructor must declare formal parameters according to the following rules. + * + *

    + *
  1. Zero or more indexed parameters must be declared first.
  2. + *
  3. Zero or more aggregators must be declared next.
  4. + *
  5. Zero or more parameters supplied by other {@code ParameterResolver} + * implementations must be declared last.
  6. + *
+ * + *

In this context, an indexed parameter is an argument for a given + * index in the {@code Arguments} provided by an {@code ArgumentsProvider} that + * is passed as an argument to the parameterized class at the same index in + * the constructor's formal parameter list. An aggregator is any + * parameter of type + * {@link org.junit.jupiter.params.aggregator.ArgumentsAccessor ArgumentsAccessor} + * or any parameter annotated with + * {@link org.junit.jupiter.params.aggregator.AggregateWith @AggregateWith}. + * + *

Field injection

+ * + *

Fields annotated with {@code @Parameter} must be declared according to the + * following rules. + * + *

    + *
  1. Zero or more indexed parameters may be declared; each must have + * a unique index specified in its {@code @Parameter(index)} annotation. The + * index may be omitted if there is only one indexed parameter. If there are at + * least two indexed parameter declarations, there must be declarations for all + * indexes from 0 to the largest declared index.
  2. + *
  3. Zero or more aggregators may be declared; each without + * specifying an index in its {@code @Parameter} annotation.
  4. + *
  5. Zero or more other fields may be declared as usual as long as they're not + * annotated with {@code @Parameter}.
  6. + *
+ * + *

In this context, an indexed parameter is an argument for a given + * index in the {@code Arguments} provided by an {@code ArgumentsProvider} that + * is injected into a field annotated with {@code @Parameter(index)}. An + * aggregator is any {@code @Parameter}-annotated field of type + * {@link org.junit.jupiter.params.aggregator.ArgumentsAccessor ArgumentsAccessor} + * or any field annotated with + * {@link org.junit.jupiter.params.aggregator.AggregateWith @AggregateWith}. + * + *

Argument Conversion

+ * + *

{@code @Parameter}-annotated fields or constructor parameters may be + * annotated with + * {@link org.junit.jupiter.params.converter.ConvertWith @ConvertWith} + * or a corresponding composed annotation to specify an explicit + * {@link org.junit.jupiter.params.converter.ArgumentConverter ArgumentConverter}. + * Otherwise, JUnit Jupiter will attempt to perform an implicit + * conversion to the target type automatically (see the User Guide for further + * details). + * + *

Lifecycle Methods

+ * + *

If you wish to execute custom code before or after each invocation of the + * parameterized class, you may declare methods annotated with + * {@link BeforeParameterizedClassInvocation @BeforeParameterizedClassInvocation} + * or + * {@link AfterParameterizedClassInvocation @AfterParameterizedClassInvocation}. + * This can, for example, be useful to initialize the arguments before they are + * used. + * + *

Composed Annotations

+ * + *

{@code @ParameterizedClass} may also be used as a meta-annotation in + * order to create a custom composed annotation that inherits the + * semantics of {@code @ParameterizedClass}. + * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * + * @since 5.13 + * @see Parameter + * @see BeforeParameterizedClassInvocation + * @see AfterParameterizedClassInvocation + * @see ParameterizedTest + * @see org.junit.jupiter.params.provider.Arguments + * @see org.junit.jupiter.params.provider.ArgumentsProvider + * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.provider.CsvFileSource + * @see org.junit.jupiter.params.provider.CsvSource + * @see org.junit.jupiter.params.provider.EnumSource + * @see org.junit.jupiter.params.provider.MethodSource + * @see org.junit.jupiter.params.provider.ValueSource + * @see org.junit.jupiter.params.aggregator.ArgumentsAccessor + * @see org.junit.jupiter.params.aggregator.AggregateWith + * @see org.junit.jupiter.params.converter.ArgumentConverter + * @see org.junit.jupiter.params.converter.ConvertWith + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@API(status = EXPERIMENTAL, since = "5.13") +@ClassTemplate +@ExtendWith(ParameterizedClassExtension.class) +@SuppressWarnings("exports") +public @interface ParameterizedClass { + + /** + * The display name to be used for individual invocations of the + * parameterized class; never blank or consisting solely of whitespace. + * + *

Defaults to + * {@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}. + * + *

If the default display name flag + * ({@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}) + * is not overridden, JUnit will: + *

    + *
  • Look up the {@value ParameterizedInvocationNameFormatter#DISPLAY_NAME_PATTERN_KEY} + * configuration parameter and use it if available. The configuration + * parameter can be supplied via the {@code Launcher} API, build tools (e.g., + * Gradle and Maven), a JVM system property, or the JUnit Platform configuration + * file (i.e., a file named {@code junit-platform.properties} in the root of + * the class path). Consult the User Guide for further information.
  • + *
  • Otherwise, + * {@value ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME} + * will be used.
  • + *
+ * + *

Supported placeholders

+ *
    + *
  • {@value ParameterizedInvocationConstants#DISPLAY_NAME_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#INDEX_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • + *
  • "{0}", "{1}", etc.: an individual argument (0-based)
  • + *
+ * + *

For the latter, you may use {@link java.text.MessageFormat} patterns + * to customize formatting (for example, {@code {0,number,#.###}}). Please + * note that the original arguments are passed when formatting, regardless + * of any implicit or explicit argument conversions. + * + *

Note that + * {@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME} is + * a flag rather than a placeholder. + * + * @see java.text.MessageFormat + */ + String name() default ParameterizedInvocationNameFormatter.DEFAULT_DISPLAY_NAME; + + /** + * Configure whether all arguments of the parameterized class that implement + * {@link AutoCloseable} will be closed after their corresponding + * invocation. + * + *

Defaults to {@code true}. + * + *

WARNING: if an argument that implements + * {@code AutoCloseable} is reused for multiple invocations of the same + * parameterized class, you must set {@code autoCloseArguments} to + * {@code false} to ensure that the argument is not closed between + * invocations. + * + * @see java.lang.AutoCloseable + */ + boolean autoCloseArguments() default true; + + /** + * Configure whether zero invocations are allowed for this + * parameterized class. + * + *

Set this attribute to {@code true} if the absence of invocations is + * expected in some cases and should not cause a test failure. + * + *

Defaults to {@code false}. + */ + boolean allowZeroInvocations() default false; + + /** + * Configure how the number of arguments provided by an + * {@link ArgumentsSource} are validated. + * + *

Defaults to {@link ArgumentCountValidationMode#DEFAULT}. + * + *

When an {@link ArgumentsSource} provides more arguments than declared + * by the parameterized class constructor or {@link Parameter}-annotated + * fields, there might be a bug in the parameterized class or the + * {@link ArgumentsSource}. By default, the additional arguments are + * ignored. {@code argumentCountValidation} allows you to control how + * additional arguments are handled. The default can be configured via the + * {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} + * configuration parameter (see the User Guide for details on configuration + * parameters). + * + * @see ArgumentCountValidationMode + */ + ArgumentCountValidationMode argumentCountValidation() default ArgumentCountValidationMode.DEFAULT; + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java new file mode 100644 index 000000000000..f73fff4e1e23 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java @@ -0,0 +1,180 @@ +/* + * 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; + +import static java.util.Collections.emptyList; +import static java.util.Collections.reverse; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; +import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods; +import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; +import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; +import static org.junit.platform.commons.support.HierarchyTraversalMode.BOTTOM_UP; +import static org.junit.platform.commons.support.HierarchyTraversalMode.TOP_DOWN; +import static org.junit.platform.commons.support.ReflectionSupport.findFields; +import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList; +import static org.junit.platform.commons.util.ReflectionUtils.isRecordClass; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.support.HierarchyTraversalMode; +import org.junit.platform.commons.support.ModifierSupport; +import org.junit.platform.commons.util.ReflectionUtils; + +class ParameterizedClassContext implements ParameterizedDeclarationContext { + + private final Class testClass; + private final ParameterizedClass annotation; + private final TestInstance.Lifecycle testInstanceLifecycle; + private final ResolverFacade resolverFacade; + private final InjectionType injectionType; + private final List beforeMethods; + private final List afterMethods; + + ParameterizedClassContext(Class testClass, ParameterizedClass annotation, + TestInstance.Lifecycle testInstanceLifecycle) { + this.testClass = testClass; + this.annotation = annotation; + this.testInstanceLifecycle = testInstanceLifecycle; + + List fields = findParameterAnnotatedFields(testClass); + if (fields.isEmpty()) { + this.resolverFacade = ResolverFacade.create(ReflectionUtils.getDeclaredConstructor(testClass), annotation); + this.injectionType = InjectionType.CONSTRUCTOR; + } + else { + this.resolverFacade = ResolverFacade.create(testClass, fields); + this.injectionType = InjectionType.FIELDS; + } + + this.beforeMethods = findLifecycleMethodsAndAssertStaticAndNonPrivate(testClass, testInstanceLifecycle, + TOP_DOWN, BeforeParameterizedClassInvocation.class, BeforeParameterizedClassInvocation::injectArguments, + this.resolverFacade); + + // Make a local copy since findAnnotatedMethods() returns an immutable list. + this.afterMethods = new ArrayList<>(findLifecycleMethodsAndAssertStaticAndNonPrivate(testClass, + testInstanceLifecycle, BOTTOM_UP, AfterParameterizedClassInvocation.class, + AfterParameterizedClassInvocation::injectArguments, this.resolverFacade)); + + // Since the bottom-up ordering of afterMethods will later be reversed when the + // AfterParameterizedClassInvocationMethodInvoker extensions are executed within + // ClassTemplateInvocationTestDescriptor, we have to reverse them to put them + // in top-down order before we register them as extensions. + reverse(afterMethods); + } + + private static List findParameterAnnotatedFields(Class clazz) { + if (isRecordClass(clazz)) { + return emptyList(); + } + return findFields(clazz, it -> isAnnotated(it, Parameter.class), BOTTOM_UP); + } + + @Override + public Class getTestClass() { + return this.testClass; + } + + @Override + public ParameterizedClass getAnnotation() { + return this.annotation; + } + + @Override + public Class getAnnotatedElement() { + return this.testClass; + } + + @Override + public String getDisplayNamePattern() { + return this.annotation.name(); + } + + @Override + public boolean isAutoClosingArguments() { + return this.annotation.autoCloseArguments(); + } + + @Override + public boolean isAllowingZeroInvocations() { + return this.annotation.allowZeroInvocations(); + } + + @Override + public ArgumentCountValidationMode getArgumentCountValidationMode() { + return this.annotation.argumentCountValidation(); + } + + @Override + public ResolverFacade getResolverFacade() { + return this.resolverFacade; + } + + @Override + public ClassTemplateInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter, + Arguments arguments, int invocationIndex) { + return new ParameterizedClassInvocationContext(this, formatter, arguments, invocationIndex); + } + + TestInstance.Lifecycle getTestInstanceLifecycle() { + return testInstanceLifecycle; + } + + InjectionType getInjectionType() { + return injectionType; + } + + List getBeforeMethods() { + return beforeMethods; + } + + List getAfterMethods() { + return afterMethods; + } + + private static List findLifecycleMethodsAndAssertStaticAndNonPrivate( + Class testClass, TestInstance.Lifecycle testInstanceLifecycle, HierarchyTraversalMode traversalMode, + Class annotationType, Predicate injectArgumentsPredicate, ResolverFacade resolverFacade) { + + List methods = findAnnotatedMethods(testClass, annotationType, traversalMode); + + return methods.stream() // + .filter(ModifierSupport::isNotPrivate) // + .filter(testInstanceLifecycle == PER_METHOD ? ModifierSupport::isStatic : __ -> true) // + .filter(ReflectionUtils::returnsPrimitiveVoid) // + .map(method -> { + A annotation = getAnnotation(method, annotationType); + if (injectArgumentsPredicate.test(annotation)) { + return new ArgumentSetLifecycleMethod(method, + resolverFacade.createLifecycleMethodParameterResolver(method, annotation)); + } + return new ArgumentSetLifecycleMethod(method); + }) // + .collect(toUnmodifiableList()); + } + + private static A getAnnotation(Method method, Class annotationType) { + return findAnnotation(method, annotationType) // + .orElseThrow(() -> new JUnitException("Method not annotated with @" + annotationType.getSimpleName())); + } + + enum InjectionType { + CONSTRUCTOR, FIELDS + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassExtension.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassExtension.java new file mode 100644 index 000000000000..db56de4f8f78 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassExtension.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.params; + +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.junit.jupiter.params.ParameterizedClassContext.InjectionType.CONSTRUCTOR; +import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.util.Optional; +import java.util.stream.Stream; + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.PreconditionViolationException; + +/** + * @since 5.13 + */ +class ParameterizedClassExtension extends ParameterizedInvocationContextProvider + implements ClassTemplateInvocationContextProvider, ParameterResolver { + + private static final String DECLARATION_CONTEXT_KEY = "context"; + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + + // This method always returns `false` because it is not intended to be used as a parameter resolver. + // Instead, it is used to provide a better error message when `TestInstance.Lifecycle.PER_CLASS` is + // attempted to be combined with constructor injection of parameters. + + if (isDeclaredOnTestClassConstructor(parameterContext, extensionContext)) { + validateAndStoreClassContext(extensionContext); + } + + return false; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + + // Should never be called (see comment above). + + throw new JUnitException("Unexpected call to resolveParameter"); + } + + @Override + public boolean supportsClassTemplate(ExtensionContext extensionContext) { + return validateAndStoreClassContext(extensionContext); + } + + @Override + public Stream provideClassTemplateInvocationContexts( + ExtensionContext extensionContext) { + + return provideInvocationContexts(extensionContext, getDeclarationContext(extensionContext)); + } + + @Override + public boolean mayReturnZeroClassTemplateInvocationContexts(ExtensionContext extensionContext) { + return getDeclarationContext(extensionContext).isAllowingZeroInvocations(); + } + + private static boolean isDeclaredOnTestClassConstructor(ParameterContext parameterContext, + ExtensionContext extensionContext) { + + Executable declaringExecutable = parameterContext.getDeclaringExecutable(); + return declaringExecutable instanceof Constructor // + && declaringExecutable.getDeclaringClass().equals(extensionContext.getTestClass().orElse(null)); + } + + private boolean validateAndStoreClassContext(ExtensionContext extensionContext) { + + Store store = getStore(extensionContext); + if (store.get(DECLARATION_CONTEXT_KEY) != null) { + return true; + } + + Optional annotation = findAnnotation(extensionContext.getTestClass(), + ParameterizedClass.class); + if (!annotation.isPresent()) { + return false; + } + + store.put(DECLARATION_CONTEXT_KEY, + createClassContext(extensionContext, extensionContext.getRequiredTestClass(), annotation.get())); + + return true; + } + + private static ParameterizedClassContext createClassContext(ExtensionContext extensionContext, Class testClass, + ParameterizedClass annotation) { + + TestInstance.Lifecycle lifecycle = extensionContext.getTestInstanceLifecycle() // + .orElseThrow(() -> new PreconditionViolationException("TestInstance.Lifecycle not present")); + + ParameterizedClassContext classContext = new ParameterizedClassContext(testClass, annotation, lifecycle); + + if (lifecycle == PER_CLASS && classContext.getInjectionType() == CONSTRUCTOR) { + throw new PreconditionViolationException( + "Constructor injection is not supported for @ParameterizedClass classes with @TestInstance(Lifecycle.PER_CLASS)"); + } + + return classContext; + } + + private ParameterizedClassContext getDeclarationContext(ExtensionContext extensionContext) { + return getStore(extensionContext)// + .get(DECLARATION_CONTEXT_KEY, ParameterizedClassContext.class); + } + + private Store getStore(ExtensionContext context) { + return context.getStore(Namespace.create(ParameterizedClassExtension.class, context.getRequiredTestClass())); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java new file mode 100644 index 000000000000..51f74401b29c --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java @@ -0,0 +1,107 @@ +/* + * 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; + +import static java.util.stream.Collectors.toList; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedClassContext.InjectionType; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.util.Preconditions; + +class ParameterizedClassInvocationContext extends ParameterizedInvocationContext + implements ClassTemplateInvocationContext { + + private final ResolutionCache resolutionCache = ResolutionCache.enabled(); + + ParameterizedClassInvocationContext(ParameterizedClassContext classContext, + ParameterizedInvocationNameFormatter formatter, Arguments arguments, int invocationIndex) { + super(classContext, formatter, arguments, invocationIndex); + } + + @Override + public String getDisplayName(int invocationIndex) { + return super.getDisplayName(invocationIndex); + } + + @Override + public List getAdditionalExtensions() { + return Stream.concat(Stream.of(createParameterInjector()), createLifecycleMethodInvokers()) // + .collect(toList()); + } + + @Override + public void prepareInvocation(ExtensionContext context) { + super.prepareInvocation(context); + } + + private Extension createParameterInjector() { + InjectionType injectionType = this.declarationContext.getInjectionType(); + switch (injectionType) { + case CONSTRUCTOR: + return createExtensionForConstructorInjection(); + case FIELDS: + return createExtensionForFieldInjection(); + } + throw new JUnitException("Unsupported injection type: " + injectionType); + } + + private ClassTemplateConstructorParameterResolver createExtensionForConstructorInjection() { + Preconditions.condition(this.declarationContext.getTestInstanceLifecycle() == PER_METHOD, + "Constructor injection is only supported for lifecycle PER_METHOD"); + return new ClassTemplateConstructorParameterResolver(this.declarationContext, this.arguments, + this.invocationIndex, this.resolutionCache); + } + + private Extension createExtensionForFieldInjection() { + ResolverFacade resolverFacade = this.declarationContext.getResolverFacade(); + TestInstance.Lifecycle lifecycle = this.declarationContext.getTestInstanceLifecycle(); + switch (lifecycle) { + case PER_CLASS: + return new BeforeClassTemplateInvocationFieldInjector(resolverFacade, this.arguments, + this.invocationIndex, this.resolutionCache); + case PER_METHOD: + return new InstancePostProcessingClassTemplateFieldInjector(resolverFacade, this.arguments, + this.invocationIndex, this.resolutionCache); + } + throw new JUnitException("Unsupported lifecycle: " + lifecycle); + } + + private Stream createLifecycleMethodInvokers() { + return Stream.concat( // + this.declarationContext.getBeforeMethods().stream().map( + this::createBeforeParameterizedClassInvocationMethodInvoker), // + this.declarationContext.getAfterMethods().stream().map( + this::createAfterParameterizedClassInvocationMethodInvoker) // + ); + } + + private BeforeParameterizedClassInvocationMethodInvoker createBeforeParameterizedClassInvocationMethodInvoker( + ArgumentSetLifecycleMethod method) { + return new BeforeParameterizedClassInvocationMethodInvoker(this.declarationContext, this.arguments, + this.invocationIndex, this.resolutionCache, method); + } + + private AfterParameterizedClassInvocationMethodInvoker createAfterParameterizedClassInvocationMethodInvoker( + ArgumentSetLifecycleMethod method) { + return new AfterParameterizedClassInvocationMethodInvoker(this.declarationContext, this.arguments, + this.invocationIndex, this.resolutionCache, method); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java new file mode 100644 index 000000000000..11dad62adc72 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java @@ -0,0 +1,45 @@ +/* + * 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; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; + +import org.junit.jupiter.params.provider.Arguments; + +/** + * @since 5.13 + */ +interface ParameterizedDeclarationContext { + + Class getTestClass(); + + Annotation getAnnotation(); + + AnnotatedElement getAnnotatedElement(); + + String getDisplayNamePattern(); + + boolean isAutoClosingArguments(); + + boolean isAllowingZeroInvocations(); + + ArgumentCountValidationMode getArgumentCountValidationMode(); + + default String getAnnotationName() { + return getAnnotation().annotationType().getSimpleName(); + } + + ResolverFacade getResolverFacade(); + + C createInvocationContext(ParameterizedInvocationNameFormatter formatter, Arguments arguments, int invocationIndex); + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationConstants.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationConstants.java new file mode 100644 index 000000000000..04eff295b1a2 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationConstants.java @@ -0,0 +1,131 @@ +/* + * 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; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.MAINTAINED; + +import org.apiguardian.api.API; + +/** + * Constants for the use with the + * {@link ParameterizedClass @ParameterizedClass} and + * {@link ParameterizedTest @ParameterizedTest} annotations. + * + * @since 5.13 + */ +@API(status = MAINTAINED, since = "5.13") +public class ParameterizedInvocationConstants { + + /** + * Placeholder for the {@linkplain org.junit.jupiter.api.TestInfo#getDisplayName + * display name} of a {@code @ParameterizedTest} method: {displayName} + * + * @since 5.3 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + */ + public static final String DISPLAY_NAME_PLACEHOLDER = "{displayName}"; + + /** + * Placeholder for the current invocation index of a {@code @ParameterizedTest} + * method (1-based): {index} + * + * @since 5.3 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + * @see #DEFAULT_DISPLAY_NAME + */ + public static final String INDEX_PLACEHOLDER = "{index}"; + + /** + * Placeholder for the complete, comma-separated arguments list of the + * current invocation of a {@code @ParameterizedTest} method: + * {arguments} + * + * @since 5.3 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + */ + public static final String ARGUMENTS_PLACEHOLDER = "{arguments}"; + + /** + * Placeholder for the complete, comma-separated named arguments list + * of the current invocation of a {@code @ParameterizedTest} method: + * {argumentsWithNames} + * + *

Argument names will be retrieved via the {@link java.lang.reflect.Parameter#getName()} + * API if the byte code contains parameter names — for example, if + * the code was compiled with the {@code -parameters} command line argument + * for {@code javac}. + * + * @since 5.6 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + */ + public static final String ARGUMENTS_WITH_NAMES_PLACEHOLDER = "{argumentsWithNames}"; + + /** + * Placeholder for the name of the argument set for the current invocation + * of a {@code @ParameterizedTest} method: {argumentSetName}. + * + *

This placeholder can be used when the current set of arguments was created via + * {@link org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + * argumentSet()}. + * + * @since 5.11 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + */ + @API(status = EXPERIMENTAL, since = "5.11") + public static final String ARGUMENT_SET_NAME_PLACEHOLDER = "{argumentSetName}"; + + /** + * Placeholder for either {@link #ARGUMENT_SET_NAME_PLACEHOLDER} or + * {@link #ARGUMENTS_WITH_NAMES_PLACEHOLDER}, depending on whether the + * current set of arguments was created via + * {@link org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + * argumentSet()}: {argumentSetNameOrArgumentsWithNames}. + * + * @since 5.11 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + * @see #ARGUMENT_SET_NAME_PLACEHOLDER + * @see #ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see #DEFAULT_DISPLAY_NAME + * @see org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + */ + @API(status = EXPERIMENTAL, since = "5.11") + public static final String ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER = "{argumentSetNameOrArgumentsWithNames}"; + + /** + * Default display name pattern for the current invocation of a + * {@code @ParameterizedTest} method: {@value} + * + *

Note that the default pattern does not include the + * {@linkplain #DISPLAY_NAME_PLACEHOLDER display name} of the + * {@code @ParameterizedTest} method. + * + * @since 5.3 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + * @see #DISPLAY_NAME_PLACEHOLDER + * @see #INDEX_PLACEHOLDER + * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + */ + public static final String DEFAULT_DISPLAY_NAME = ParameterizedInvocationNameFormatter.DEFAULT_DISPLAY_NAME_PATTERN; + + private ParameterizedInvocationConstants() { + } +} 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 new file mode 100644 index 000000000000..207817c69ebb --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java @@ -0,0 +1,93 @@ +/* + * 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; + +import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.aggregator.DefaultArgumentsAccessor; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.support.ParameterDeclarations; + +class ParameterizedInvocationContext> { + + private static final Namespace NAMESPACE = Namespace.create(ParameterizedTestInvocationContext.class); + + protected final T declarationContext; + private final ParameterizedInvocationNameFormatter formatter; + protected final EvaluatedArgumentSet arguments; + protected final int invocationIndex; + + ParameterizedInvocationContext(T declarationContext, ParameterizedInvocationNameFormatter formatter, + Arguments arguments, int invocationIndex) { + + this.declarationContext = declarationContext; + this.formatter = formatter; + ResolverFacade resolverFacade = this.declarationContext.getResolverFacade(); + this.arguments = EvaluatedArgumentSet.of(arguments, resolverFacade::determineConsumedArgumentLength); + this.invocationIndex = invocationIndex; + } + + public String getDisplayName(int invocationIndex) { + return this.formatter.format(invocationIndex, this.arguments); + } + + public void prepareInvocation(ExtensionContext context) { + if (this.declarationContext.isAutoClosingArguments()) { + registerAutoCloseableArgumentsInStoreForClosing(context); + } + validateArgumentCount(context); + storeParameterInfo(context); + } + + private void registerAutoCloseableArgumentsInStoreForClosing(ExtensionContext context) { + ExtensionContext.Store store = context.getStore(NAMESPACE); + AtomicInteger argumentIndex = new AtomicInteger(); + + Arrays.stream(this.arguments.getAllPayloads()) // + .filter(AutoCloseable.class::isInstance) // + .map(AutoCloseable.class::cast) // + .map(CloseableArgument::new) // + .forEach(closeable -> store.put(argumentIndex.incrementAndGet(), closeable)); + } + + private void validateArgumentCount(ExtensionContext context) { + new ArgumentCountValidator(this.declarationContext, this.arguments).validate(context); + } + + 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); + new DefaultParameterInfo(declarations, accessor).store(context); + } + + private static class CloseableArgument implements ExtensionContext.Store.CloseableResource { + + private final AutoCloseable autoCloseable; + + CloseableArgument(AutoCloseable autoCloseable) { + this.autoCloseable = autoCloseable; + } + + @Override + public void close() throws Throwable { + 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 new file mode 100644 index 000000000000..ffa324c13df6 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContextProvider.java @@ -0,0 +1,77 @@ +/* + * 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; + +import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.AnnotationConsumerInitializer; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.platform.commons.util.ExceptionUtils; +import org.junit.platform.commons.util.Preconditions; + +class ParameterizedInvocationContextProvider { + + protected Stream provideInvocationContexts(ExtensionContext extensionContext, + ParameterizedDeclarationContext declarationContext) { + + List argumentsSources = collectArgumentSources(declarationContext); + ParameterDeclarations parameters = declarationContext.getResolverFacade().getIndexedParameterDeclarations(); + ParameterizedInvocationNameFormatter formatter = ParameterizedInvocationNameFormatter.create(extensionContext, + declarationContext); + AtomicLong invocationCount = new AtomicLong(0); + + // @formatter:off + return argumentsSources + .stream() + .map(ArgumentsSource::value) + .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsProvider.class, clazz, extensionContext)) + .map(provider -> AnnotationConsumerInitializer.initialize(declarationContext.getAnnotatedElement(), provider)) + .flatMap(provider -> arguments(provider, parameters, extensionContext)) + .map(arguments -> { + 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()))); + // @formatter:on + } + + private static List collectArgumentSources(ParameterizedDeclarationContext declarationContext) { + List argumentsSources = findRepeatableAnnotations(declarationContext.getAnnotatedElement(), + ArgumentsSource.class); + + Preconditions.notEmpty(argumentsSources, + () -> String.format("Configuration error: You must configure at least one arguments source for this @%s", + declarationContext.getAnnotationName())); + + return argumentsSources; + } + + protected static Stream arguments(ArgumentsProvider provider, ParameterDeclarations parameters, + ExtensionContext context) { + try { + return provider.provideArguments(parameters, context); + } + catch (Exception e) { + throw ExceptionUtils.throwAsUncheckedException(e); + } + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatter.java similarity index 65% rename from junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java rename to junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatter.java index 12cd141ec38c..1f1e9f103e65 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatter.java @@ -11,12 +11,12 @@ package org.junit.jupiter.params; import static java.util.stream.Collectors.joining; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENTS_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENTS_WITH_NAMES_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENT_SET_NAME_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.DISPLAY_NAME_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.INDEX_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENTS_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENTS_WITH_NAMES_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENT_SET_NAME_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DISPLAY_NAME_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.INDEX_PLACEHOLDER; import static org.junit.platform.commons.util.StringUtils.isNotBlank; import java.text.FieldPosition; @@ -27,30 +27,56 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import java.util.stream.IntStream; -import org.junit.jupiter.api.Named; import org.junit.jupiter.api.extension.ExtensionConfigurationException; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.Arguments.ArgumentSet; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.StringUtils; /** * @since 5.0 */ -class ParameterizedTestNameFormatter { +class ParameterizedInvocationNameFormatter { + + static final String DEFAULT_DISPLAY_NAME = "{default_display_name}"; + static final String DEFAULT_DISPLAY_NAME_PATTERN = "[" + INDEX_PLACEHOLDER + "] " + + ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; + static final String DISPLAY_NAME_PATTERN_KEY = "junit.jupiter.params.displayname.default"; + static final String ARGUMENT_MAX_LENGTH_KEY = "junit.jupiter.params.displayname.argument.maxlength"; + + static ParameterizedInvocationNameFormatter create(ExtensionContext extensionContext, + ParameterizedDeclarationContext declarationContext) { + + String name = declarationContext.getDisplayNamePattern(); + String pattern = name.equals(DEFAULT_DISPLAY_NAME) + ? extensionContext.getConfigurationParameter(DISPLAY_NAME_PATTERN_KEY) // + .orElse(DEFAULT_DISPLAY_NAME_PATTERN) + : name; + pattern = Preconditions.notBlank(pattern.trim(), () -> String.format( + "Configuration error: @%s on %s must be declared with a non-empty name.", + declarationContext.getAnnotationName(), + declarationContext.getResolverFacade().getIndexedParameterDeclarations().getSourceElementDescription())); + + int argumentMaxLength = extensionContext.getConfigurationParameter(ARGUMENT_MAX_LENGTH_KEY, Integer::parseInt) // + .orElse(512); + + return new ParameterizedInvocationNameFormatter(pattern, extensionContext.getDisplayName(), declarationContext, + argumentMaxLength); + } private final PartialFormatter[] partialFormatters; - ParameterizedTestNameFormatter(String pattern, String displayName, ParameterizedTestMethodContext methodContext, - int argumentMaxLength) { + ParameterizedInvocationNameFormatter(String pattern, String displayName, + ParameterizedDeclarationContext declarationContext, int argumentMaxLength) { try { - this.partialFormatters = parse(pattern, displayName, methodContext, argumentMaxLength); + this.partialFormatters = parse(pattern, displayName, declarationContext, argumentMaxLength); } catch (Exception ex) { String message = "The display name pattern defined for the parameterized test is invalid. " @@ -59,9 +85,9 @@ class ParameterizedTestNameFormatter { } } - String format(int invocationIndex, Arguments arguments, Object[] consumedArguments) { + String format(int invocationIndex, EvaluatedArgumentSet arguments) { try { - return formatSafely(invocationIndex, arguments, consumedArguments); + return formatSafely(invocationIndex, arguments); } catch (Exception ex) { String message = "Failed to format display name for parameterized test. " @@ -70,9 +96,9 @@ String format(int invocationIndex, Arguments arguments, Object[] consumedArgumen } } - private String formatSafely(int invocationIndex, Arguments arguments, Object[] consumedArguments) { - ArgumentsContext context = new ArgumentsContext(invocationIndex, arguments, - extractNamedArguments(consumedArguments)); + private String formatSafely(int invocationIndex, EvaluatedArgumentSet arguments) { + ArgumentsContext context = new ArgumentsContext(invocationIndex, arguments.getConsumedNames(), + arguments.getName()); StringBuffer result = new StringBuffer(); // used instead of StringBuilder so MessageFormat can append directly for (PartialFormatter partialFormatter : this.partialFormatters) { partialFormatter.append(context, result); @@ -80,17 +106,11 @@ private String formatSafely(int invocationIndex, Arguments arguments, Object[] c return result.toString(); } - private Object[] extractNamedArguments(Object[] arguments) { - return Arrays.stream(arguments) // - .map(argument -> argument instanceof Named ? ((Named) argument).getName() : argument) // - .toArray(); - } - - private PartialFormatter[] parse(String pattern, String displayName, ParameterizedTestMethodContext methodContext, - int argumentMaxLength) { + private PartialFormatter[] parse(String pattern, String displayName, + ParameterizedDeclarationContext declarationContext, int argumentMaxLength) { List result = new ArrayList<>(); - PartialFormatters formatters = createPartialFormatters(displayName, methodContext, argumentMaxLength); + PartialFormatters formatters = createPartialFormatters(displayName, declarationContext, argumentMaxLength); String unparsedSegment = pattern; while (isNotBlank(unparsedSegment)) { @@ -135,32 +155,36 @@ private static PartialFormatter determineNonPlaceholderFormatter(String segment, : (context, result) -> result.append(segment); } - private PartialFormatters createPartialFormatters(String displayName, ParameterizedTestMethodContext methodContext, - int argumentMaxLength) { + private PartialFormatters createPartialFormatters(String displayName, + ParameterizedDeclarationContext declarationContext, int argumentMaxLength) { PartialFormatter argumentsWithNamesFormatter = new CachingByArgumentsLengthPartialFormatter( - length -> new MessageFormatPartialFormatter(argumentsWithNamesPattern(length, methodContext), + length -> new MessageFormatPartialFormatter(argumentsWithNamesPattern(length, declarationContext), argumentMaxLength)); + PartialFormatter argumentSetNameFormatter = new ArgumentSetNameFormatter( + declarationContext.getAnnotationName()); + PartialFormatters formatters = new PartialFormatters(); formatters.put(INDEX_PLACEHOLDER, PartialFormatter.INDEX); formatters.put(DISPLAY_NAME_PLACEHOLDER, (context, result) -> result.append(displayName)); - formatters.put(ARGUMENT_SET_NAME_PLACEHOLDER, PartialFormatter.ARGUMENT_SET_NAME); + formatters.put(ARGUMENT_SET_NAME_PLACEHOLDER, argumentSetNameFormatter); formatters.put(ARGUMENTS_WITH_NAMES_PLACEHOLDER, argumentsWithNamesFormatter); formatters.put(ARGUMENTS_PLACEHOLDER, new CachingByArgumentsLengthPartialFormatter( length -> new MessageFormatPartialFormatter(argumentsPattern(length), argumentMaxLength))); formatters.put(ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER, (context, result) -> { - PartialFormatter formatterToUse = context.arguments instanceof ArgumentSet // - ? PartialFormatter.ARGUMENT_SET_NAME // + PartialFormatter formatterToUse = context.argumentSetName.isPresent() // + ? argumentSetNameFormatter // : argumentsWithNamesFormatter; formatterToUse.append(context, result); }); return formatters; } - private static String argumentsWithNamesPattern(int length, ParameterizedTestMethodContext methodContext) { + private static String argumentsWithNamesPattern(int length, ParameterizedDeclarationContext declarationContext) { + ResolverFacade resolverFacade = declarationContext.getResolverFacade(); return IntStream.range(0, length) // - .mapToObj(index -> methodContext.getParameterName(index).map(name -> name + "=").orElse("") + "{" + .mapToObj(index -> resolverFacade.getParameterName(index).map(name -> name + "=").orElse("") + "{" + index + "}") // .collect(joining(", ")); } @@ -186,13 +210,13 @@ private static class PlaceholderPosition { private static class ArgumentsContext { private final int invocationIndex; - private final Arguments arguments; private final Object[] consumedArguments; + private final Optional argumentSetName; - ArgumentsContext(int invocationIndex, Arguments arguments, Object[] consumedArguments) { + ArgumentsContext(int invocationIndex, Object[] consumedArguments, Optional argumentSetName) { this.invocationIndex = invocationIndex; - this.arguments = arguments; this.consumedArguments = consumedArguments; + this.argumentSetName = argumentSetName; } } @@ -201,17 +225,28 @@ private interface PartialFormatter { PartialFormatter INDEX = (context, result) -> result.append(context.invocationIndex); - PartialFormatter ARGUMENT_SET_NAME = (context, result) -> { - if (!(context.arguments instanceof ArgumentSet)) { - throw new ExtensionConfigurationException( - String.format("When the display name pattern for a @ParameterizedTest contains %s, " - + "the arguments must be supplied as an ArgumentSet.", - ARGUMENT_SET_NAME_PLACEHOLDER)); - } - result.append(((ArgumentSet) context.arguments).getName()); - }; - void append(ArgumentsContext context, StringBuffer result); + + } + + private static class ArgumentSetNameFormatter implements PartialFormatter { + + private final String annotationName; + + ArgumentSetNameFormatter(String annotationName) { + this.annotationName = annotationName; + } + + @Override + public void append(ArgumentsContext context, StringBuffer result) { + if (context.argumentSetName.isPresent()) { + result.append(context.argumentSetName.get()); + return; + } + throw new ExtensionConfigurationException(String.format( + "When the display name pattern for a @%s contains %s, the arguments must be supplied as an ArgumentSet.", + this.annotationName, ARGUMENT_SET_NAME_PLACEHOLDER)); + } } private static class MessageFormatPartialFormatter implements PartialFormatter { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationParameterResolver.java new file mode 100644 index 000000000000..84faf1971d23 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationParameterResolver.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.jupiter.params; + +import java.lang.reflect.Executable; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * @since 5.13 + */ +abstract class ParameterizedInvocationParameterResolver implements ParameterResolver { + + private final ResolverFacade resolverFacade; + private final EvaluatedArgumentSet arguments; + private final int invocationIndex; + private final ResolutionCache resolutionCache; + + ParameterizedInvocationParameterResolver(ResolverFacade resolverFacade, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { + + this.resolverFacade = resolverFacade; + this.arguments = arguments; + this.invocationIndex = invocationIndex; + this.resolutionCache = resolutionCache; + } + + @Override + public final ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { + return ExtensionContextScope.TEST_METHOD; + } + + @Override + public final boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + + return isSupportedOnConstructorOrMethod(parameterContext.getDeclaringExecutable(), extensionContext) // + && this.resolverFacade.isSupportedParameter(parameterContext, this.arguments); + + } + + @Override + public final Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + + return this.resolverFacade.resolve(parameterContext, extensionContext, this.arguments, this.invocationIndex, + this.resolutionCache); + } + + protected abstract boolean isSupportedOnConstructorOrMethod(Executable declaringExecutable, + ExtensionContext extensionContext); + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java index ce16337b45d2..eb841a9bdc5b 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java @@ -10,6 +10,7 @@ package org.junit.jupiter.params; +import static org.apiguardian.api.API.Status.DEPRECATED; import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; @@ -46,18 +47,18 @@ *

A {@code @ParameterizedTest} method may declare additional parameters at * the end of the method's parameter list to be resolved by other * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers} - * (e.g., {@code TestInfo}, {@code TestReporter}, etc). Specifically, a + * (e.g., {@code TestInfo}, {@code TestReporter}, etc.). Specifically, a * parameterized test method must declare formal parameters according to the * following rules. * *

    - *
  1. Zero or more indexed arguments must be declared first.
  2. + *
  3. Zero or more indexed parameters must be declared first.
  4. *
  5. Zero or more aggregators must be declared next.
  6. - *
  7. Zero or more arguments supplied by other {@code ParameterResolver} + *
  8. Zero or more parameters supplied by other {@code ParameterResolver} * implementations must be declared last.
  9. *
* - *

In this context, an indexed argument is an argument for a given + *

In this context, an indexed parameter is an argument for a given * index in the {@code Arguments} provided by an {@code ArgumentsProvider} that * is passed as an argument to the parameterized method at the same index in the * method's formal parameter list. An aggregator is any parameter of type @@ -113,6 +114,7 @@ * implementation. * * @since 5.0 + * @see ParameterizedClass * @see org.junit.jupiter.params.provider.Arguments * @see org.junit.jupiter.params.provider.ArgumentsProvider * @see org.junit.jupiter.params.provider.ArgumentsSource @@ -136,127 +138,135 @@ public @interface ParameterizedTest { /** - * Placeholder for the {@linkplain org.junit.jupiter.api.TestInfo#getDisplayName - * display name} of a {@code @ParameterizedTest} method: {displayName} + * See {@link ParameterizedInvocationConstants#DISPLAY_NAME_PLACEHOLDER}. * * @since 5.3 * @see #name + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#DISPLAY_NAME_PLACEHOLDER} + * instead. */ - String DISPLAY_NAME_PLACEHOLDER = "{displayName}"; + @API(status = DEPRECATED, since = "5.13") + @Deprecated + String DISPLAY_NAME_PLACEHOLDER = ParameterizedInvocationConstants.DISPLAY_NAME_PLACEHOLDER; /** - * Placeholder for the current invocation index of a {@code @ParameterizedTest} - * method (1-based): {index} + * See {@link ParameterizedInvocationConstants#INDEX_PLACEHOLDER}. * * @since 5.3 * @see #name - * @see #DEFAULT_DISPLAY_NAME + * @see ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#INDEX_PLACEHOLDER} instead. */ + @API(status = DEPRECATED, since = "5.13") + @Deprecated String INDEX_PLACEHOLDER = "{index}"; /** - * Placeholder for the complete, comma-separated arguments list of the - * current invocation of a {@code @ParameterizedTest} method: - * {arguments} + * See {@link ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER}. * * @since 5.3 * @see #name + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER} instead. */ + @API(status = DEPRECATED, since = "5.13") + @Deprecated String ARGUMENTS_PLACEHOLDER = "{arguments}"; /** - * Placeholder for the complete, comma-separated named arguments list - * of the current invocation of a {@code @ParameterizedTest} method: - * {argumentsWithNames} - * - *

Argument names will be retrieved via the {@link java.lang.reflect.Parameter#getName()} - * API if the byte code contains parameter names — for example, if - * the code was compiled with the {@code -parameters} command line argument - * for {@code javac}. + * See + * {@link ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER}. * * @since 5.6 * @see #name - * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER} + * instead. */ + @API(status = DEPRECATED, since = "5.13") + @Deprecated String ARGUMENTS_WITH_NAMES_PLACEHOLDER = "{argumentsWithNames}"; /** - * Placeholder for the name of the argument set for the current invocation - * of a {@code @ParameterizedTest} method: {argumentSetName}. - * - *

This placeholder can be used when the current set of arguments was created via - * {@link org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) - * argumentSet()}. + * See + * {@link ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER}. * * @since 5.11 * @see #name - * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER * @see org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER} + * instead. */ - @API(status = EXPERIMENTAL, since = "5.11") + @API(status = DEPRECATED, since = "5.13") + @Deprecated String ARGUMENT_SET_NAME_PLACEHOLDER = "{argumentSetName}"; /** - * Placeholder for either {@link #ARGUMENT_SET_NAME_PLACEHOLDER} or - * {@link #ARGUMENTS_WITH_NAMES_PLACEHOLDER}, depending on whether the - * current set of arguments was created via - * {@link org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) - * argumentSet()}: {argumentSetNameOrArgumentsWithNames}. + * See + * {@link ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER}. * * @since 5.11 * @see #name - * @see #ARGUMENT_SET_NAME_PLACEHOLDER - * @see #ARGUMENTS_WITH_NAMES_PLACEHOLDER - * @see #DEFAULT_DISPLAY_NAME + * @see ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER + * @see ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME * @see org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER} + * instead. */ - @API(status = EXPERIMENTAL, since = "5.11") + @API(status = DEPRECATED, since = "5.13") + @Deprecated String ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER = "{argumentSetNameOrArgumentsWithNames}"; /** - * Default display name pattern for the current invocation of a - * {@code @ParameterizedTest} method: {@value} - * - *

Note that the default pattern does not include the - * {@linkplain #DISPLAY_NAME_PLACEHOLDER display name} of the - * {@code @ParameterizedTest} method. + * See + * {@link ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME}. * * @since 5.3 * @see #name - * @see #DISPLAY_NAME_PLACEHOLDER - * @see #INDEX_PLACEHOLDER - * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see ParameterizedInvocationConstants#DISPLAY_NAME_PLACEHOLDER + * @see ParameterizedInvocationConstants#INDEX_PLACEHOLDER + * @see ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME} instead. */ - String DEFAULT_DISPLAY_NAME = "[" + INDEX_PLACEHOLDER + "] " - + ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; + @API(status = DEPRECATED, since = "5.13") + @Deprecated + String DEFAULT_DISPLAY_NAME = ParameterizedInvocationConstants.DEFAULT_DISPLAY_NAME; /** * The display name to be used for individual invocations of the * parameterized test; never blank or consisting solely of whitespace. * - *

Defaults to {@value ParameterizedTestExtension#DEFAULT_DISPLAY_NAME}. + *

Defaults to {@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}. * *

If the default display name flag - * ({@value ParameterizedTestExtension#DEFAULT_DISPLAY_NAME}) + * ({@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}) * is not overridden, JUnit will: *

    - *
  • Look up the {@value ParameterizedTestExtension#DISPLAY_NAME_PATTERN_KEY} + *
  • Look up the {@value ParameterizedInvocationNameFormatter#DISPLAY_NAME_PATTERN_KEY} * configuration parameter and use it if available. The configuration * parameter can be supplied via the {@code Launcher} API, build tools (e.g., * Gradle and Maven), a JVM system property, or the JUnit Platform configuration * file (i.e., a file named {@code junit-platform.properties} in the root of * the class path). Consult the User Guide for further information.
  • - *
  • Otherwise, {@value #DEFAULT_DISPLAY_NAME} will be used.
  • + *
  • Otherwise, {@value ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME} will be used.
  • *
* *

Supported placeholders

*
    - *
  • {@value #DISPLAY_NAME_PLACEHOLDER}
  • - *
  • {@value #INDEX_PLACEHOLDER}
  • - *
  • {@value #ARGUMENT_SET_NAME_PLACEHOLDER}
  • - *
  • {@value #ARGUMENTS_PLACEHOLDER}
  • - *
  • {@value #ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • - *
  • {@value #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#DISPLAY_NAME_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#INDEX_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • *
  • "{0}", "{1}", etc.: an individual argument (0-based)
  • *
* @@ -266,25 +276,25 @@ * of any implicit or explicit argument conversions. * *

Note that - * {@value ParameterizedTestExtension#DEFAULT_DISPLAY_NAME} is + * {@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME} is * a flag rather than a placeholder. * * @see java.text.MessageFormat */ - String name() default ParameterizedTestExtension.DEFAULT_DISPLAY_NAME; + String name() default ParameterizedInvocationNameFormatter.DEFAULT_DISPLAY_NAME; /** - * Configure whether all arguments of the parameterized test that implement {@link AutoCloseable} - * will be closed after {@link org.junit.jupiter.api.AfterEach @AfterEach} methods - * and {@link org.junit.jupiter.api.extension.AfterEachCallback AfterEachCallback} - * extensions have been called for the current parameterized test invocation. + * Configure whether all arguments of the parameterized test that implement + * {@link AutoCloseable} will be closed after their corresponding + * invocation. * *

Defaults to {@code true}. * - *

WARNING: if an argument that implements {@code AutoCloseable} - * is reused for multiple invocations of the same parameterized test method, - * you must set {@code autoCloseArguments} to {@code false} to ensure that - * the argument is not closed between invocations. + *

WARNING: if an argument that implements + * {@code AutoCloseable} is reused for multiple invocations of the same + * parameterized test method, you must set {@code autoCloseArguments} to + * {@code false} to ensure that the argument is not closed between + * invocations. * * @since 5.8 * @see java.lang.AutoCloseable @@ -307,20 +317,24 @@ boolean allowZeroInvocations() default false; /** - * Configure how the number of arguments provided by an {@link ArgumentsSource} are validated. + * Configure how the number of arguments provided by an + * {@link ArgumentsSource} are validated. * *

Defaults to {@link ArgumentCountValidationMode#DEFAULT}. * - *

When an {@link ArgumentsSource} provides more arguments than declared by the test method, - * there might be a bug in the test method or the {@link ArgumentsSource}. - * By default, the additional arguments are ignored. - * {@code argumentCountValidation} allows you to control how additional arguments are handled. - * The default can be configured via the {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} - * configuration parameter (see the User Guide for details on configuration parameters). + *

When an {@link ArgumentsSource} provides more arguments than declared + * by the parameterized test method, there might be a bug in the method or + * the {@link ArgumentsSource}. By default, the additional arguments are + * ignored. {@code argumentCountValidation} allows you to control how + * additional arguments are handled. The default can be configured via the + * {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} + * configuration parameter (see the User Guide for details on configuration + * parameters). * * @since 5.12 * @see ArgumentCountValidationMode */ @API(status = EXPERIMENTAL, since = "5.12") ArgumentCountValidationMode argumentCountValidation() default ArgumentCountValidationMode.DEFAULT; + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java new file mode 100644 index 000000000000..50a9417b3a11 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java @@ -0,0 +1,85 @@ +/* + * 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; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.platform.commons.util.Preconditions; + +/** + * Encapsulates access to the parameters of a parameterized test method and + * caches the converters and aggregators used to resolve them. + * + * @since 5.3 + */ +class ParameterizedTestContext implements ParameterizedDeclarationContext { + + private final Class testClass; + private final Method method; + private final ParameterizedTest annotation; + private final ResolverFacade resolverFacade; + + ParameterizedTestContext(Class testClass, Method method, ParameterizedTest annotation) { + this.testClass = testClass; + this.method = Preconditions.notNull(method, "method must not be null"); + this.annotation = Preconditions.notNull(annotation, "annotation must not be null"); + this.resolverFacade = ResolverFacade.create(method, annotation); + } + + @Override + public Class getTestClass() { + return this.testClass; + } + + @Override + public ParameterizedTest getAnnotation() { + return this.annotation; + } + + @Override + public Method getAnnotatedElement() { + return this.method; + } + + @Override + public String getDisplayNamePattern() { + return this.annotation.name(); + } + + @Override + public boolean isAutoClosingArguments() { + return this.annotation.autoCloseArguments(); + } + + @Override + public boolean isAllowingZeroInvocations() { + return this.annotation.allowZeroInvocations(); + } + + @Override + public ArgumentCountValidationMode getArgumentCountValidationMode() { + return this.annotation.argumentCountValidation(); + } + + @Override + public ResolverFacade getResolverFacade() { + return this.resolverFacade; + } + + @Override + public TestTemplateInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter, + Arguments arguments, int invocationIndex) { + return new ParameterizedTestInvocationContext(this, formatter, arguments, invocationIndex); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java index 9390c1fd3827..5097d9f02322 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java @@ -11,58 +11,34 @@ package org.junit.jupiter.params; import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; -import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations; -import java.lang.reflect.Method; -import java.util.List; import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.support.AnnotationConsumerInitializer; -import org.junit.platform.commons.util.ExceptionUtils; -import org.junit.platform.commons.util.Preconditions; /** * @since 5.0 */ -class ParameterizedTestExtension implements TestTemplateInvocationContextProvider { +class ParameterizedTestExtension extends ParameterizedInvocationContextProvider + implements TestTemplateInvocationContextProvider { - static final String METHOD_CONTEXT_KEY = "context"; - static final String ARGUMENT_MAX_LENGTH_KEY = "junit.jupiter.params.displayname.argument.maxlength"; - static final String DEFAULT_DISPLAY_NAME = "{default_display_name}"; - static final String DISPLAY_NAME_PATTERN_KEY = "junit.jupiter.params.displayname.default"; + static final String DECLARATION_CONTEXT_KEY = "context"; @Override public boolean supportsTestTemplate(ExtensionContext context) { - if (!context.getTestMethod().isPresent()) { - return false; - } - - Method templateMethod = context.getTestMethod().get(); - Optional annotation = findAnnotation(templateMethod, ParameterizedTest.class); + Optional annotation = findAnnotation(context.getTestMethod(), ParameterizedTest.class); if (!annotation.isPresent()) { return false; } - ParameterizedTestMethodContext methodContext = new ParameterizedTestMethodContext(templateMethod, - annotation.get()); - - Preconditions.condition(methodContext.hasPotentiallyValidSignature(), - () -> String.format( - "@ParameterizedTest method [%s] declares formal parameters in an invalid order: " - + "argument aggregators must be declared after any indexed arguments " - + "and before any arguments resolved by another ParameterResolver.", - templateMethod.toGenericString())); + ParameterizedTestContext methodContext = new ParameterizedTestContext(context.getRequiredTestClass(), + context.getRequiredTestMethod(), annotation.get()); - getStore(context).put(METHOD_CONTEXT_KEY, methodContext); + getStore(context).put(DECLARATION_CONTEXT_KEY, methodContext); return true; } @@ -71,80 +47,21 @@ public boolean supportsTestTemplate(ExtensionContext context) { public Stream provideTestTemplateInvocationContexts( ExtensionContext extensionContext) { - ParameterizedTestMethodContext methodContext = getMethodContext(extensionContext); - ParameterizedTestNameFormatter formatter = createNameFormatter(extensionContext, methodContext); - AtomicLong invocationCount = new AtomicLong(0); - - List argumentsSources = findRepeatableAnnotations(methodContext.method, ArgumentsSource.class); - - Preconditions.notEmpty(argumentsSources, - "Configuration error: You must configure at least one arguments source for this @ParameterizedTest"); - - // @formatter:off - return argumentsSources - .stream() - .map(ArgumentsSource::value) - .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsProvider.class, clazz, extensionContext)) - .map(provider -> AnnotationConsumerInitializer.initialize(methodContext.method, provider)) - .flatMap(provider -> arguments(provider, extensionContext)) - .map(arguments -> { - invocationCount.incrementAndGet(); - return createInvocationContext(formatter, methodContext, arguments, invocationCount.intValue()); - }) - .onClose(() -> - Preconditions.condition(invocationCount.get() > 0 || methodContext.annotation.allowZeroInvocations(), - "Configuration error: You must configure at least one set of arguments for this @ParameterizedTest")); - // @formatter:on + return provideInvocationContexts(extensionContext, getDeclarationContext(extensionContext)); } @Override public boolean mayReturnZeroTestTemplateInvocationContexts(ExtensionContext extensionContext) { - ParameterizedTestMethodContext methodContext = getMethodContext(extensionContext); - return methodContext.annotation.allowZeroInvocations(); + return getDeclarationContext(extensionContext).isAllowingZeroInvocations(); } - private ParameterizedTestMethodContext getMethodContext(ExtensionContext extensionContext) { + private ParameterizedTestContext getDeclarationContext(ExtensionContext extensionContext) { return getStore(extensionContext)// - .get(METHOD_CONTEXT_KEY, ParameterizedTestMethodContext.class); + .get(DECLARATION_CONTEXT_KEY, ParameterizedTestContext.class); } private ExtensionContext.Store getStore(ExtensionContext context) { return context.getStore(Namespace.create(ParameterizedTestExtension.class, context.getRequiredTestMethod())); } - private TestTemplateInvocationContext createInvocationContext(ParameterizedTestNameFormatter formatter, - ParameterizedTestMethodContext methodContext, Arguments arguments, int invocationIndex) { - - return new ParameterizedTestInvocationContext(formatter, methodContext, arguments, invocationIndex); - } - - private ParameterizedTestNameFormatter createNameFormatter(ExtensionContext extensionContext, - ParameterizedTestMethodContext methodContext) { - - String name = methodContext.annotation.name(); - String pattern = name.equals(DEFAULT_DISPLAY_NAME) - ? extensionContext.getConfigurationParameter(DISPLAY_NAME_PATTERN_KEY) // - .orElse(ParameterizedTest.DEFAULT_DISPLAY_NAME) - : name; - pattern = Preconditions.notBlank(pattern.trim(), - () -> String.format( - "Configuration error: @ParameterizedTest on method [%s] must be declared with a non-empty name.", - methodContext.method)); - - int argumentMaxLength = extensionContext.getConfigurationParameter(ARGUMENT_MAX_LENGTH_KEY, Integer::parseInt) // - .orElse(512); - - return new ParameterizedTestNameFormatter(pattern, extensionContext.getDisplayName(), methodContext, - argumentMaxLength); - } - - protected static Stream arguments(ArgumentsProvider provider, ExtensionContext context) { - try { - return provider.provideArguments(context); - } - catch (Exception e) { - throw ExceptionUtils.throwAsUncheckedException(e); - } - } - } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java index a6adfd3e5c5f..6fc9c9aa4286 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java @@ -10,52 +10,41 @@ package org.junit.jupiter.params; -import java.util.Arrays; +import static java.util.Collections.singletonList; + import java.util.List; import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.params.provider.Arguments; /** * @since 5.0 */ -class ParameterizedTestInvocationContext implements TestTemplateInvocationContext { - - private final ParameterizedTestNameFormatter formatter; - private final ParameterizedTestMethodContext methodContext; - private final Arguments arguments; - private final Object[] consumedArguments; - private final int invocationIndex; - - ParameterizedTestInvocationContext(ParameterizedTestNameFormatter formatter, - ParameterizedTestMethodContext methodContext, Arguments arguments, int invocationIndex) { - - this.formatter = formatter; - this.methodContext = methodContext; - this.arguments = arguments; - this.consumedArguments = consumedArguments(methodContext, arguments.get()); - this.invocationIndex = invocationIndex; +class ParameterizedTestInvocationContext extends ParameterizedInvocationContext + implements TestTemplateInvocationContext { + + ParameterizedTestInvocationContext(ParameterizedTestContext methodContext, + ParameterizedInvocationNameFormatter formatter, Arguments arguments, int invocationIndex) { + super(methodContext, formatter, arguments, invocationIndex); } @Override public String getDisplayName(int invocationIndex) { - return this.formatter.format(invocationIndex, this.arguments, this.consumedArguments); + return super.getDisplayName(invocationIndex); } @Override public List getAdditionalExtensions() { - return Arrays.asList( - new ParameterizedTestParameterResolver(this.methodContext, this.consumedArguments, this.invocationIndex), - new ArgumentCountValidator(this.methodContext, this.arguments)); + return singletonList( // + new ParameterizedTestMethodParameterResolver(this.declarationContext, this.arguments, this.invocationIndex) // + ); } - private static Object[] consumedArguments(ParameterizedTestMethodContext methodContext, Object[] arguments) { - if (methodContext.hasAggregator()) { - return arguments; - } - int parameterCount = methodContext.getParameterCount(); - return arguments.length > parameterCount ? Arrays.copyOf(arguments, parameterCount) : arguments; + @Override + public void prepareInvocation(ExtensionContext context) { + super.prepareInvocation(context); } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java deleted file mode 100644 index 074b32a1b9cb..000000000000 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java +++ /dev/null @@ -1,282 +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.params; - -import static org.junit.jupiter.params.ParameterizedTestMethodContext.ResolverType.AGGREGATOR; -import static org.junit.jupiter.params.ParameterizedTestMethodContext.ResolverType.CONVERTER; -import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.jupiter.api.extension.ParameterResolutionException; -import org.junit.jupiter.params.aggregator.AggregateWith; -import org.junit.jupiter.params.aggregator.ArgumentsAccessor; -import org.junit.jupiter.params.aggregator.ArgumentsAggregator; -import org.junit.jupiter.params.aggregator.DefaultArgumentsAccessor; -import org.junit.jupiter.params.converter.ArgumentConverter; -import org.junit.jupiter.params.converter.ConvertWith; -import org.junit.jupiter.params.converter.DefaultArgumentConverter; -import org.junit.jupiter.params.support.AnnotationConsumerInitializer; -import org.junit.platform.commons.support.AnnotationSupport; -import org.junit.platform.commons.util.Preconditions; -import org.junit.platform.commons.util.StringUtils; - -/** - * Encapsulates access to the parameters of a parameterized test method and - * caches the converters and aggregators used to resolve them. - * - * @since 5.3 - */ -class ParameterizedTestMethodContext { - - final Method method; - final ParameterizedTest annotation; - - private final Parameter[] parameters; - private final Resolver[] resolvers; - private final List resolverTypes; - - ParameterizedTestMethodContext(Method method, ParameterizedTest annotation) { - this.method = Preconditions.notNull(method, "method must not be null"); - this.annotation = Preconditions.notNull(annotation, "annotation must not be null"); - this.parameters = method.getParameters(); - this.resolvers = new Resolver[this.parameters.length]; - this.resolverTypes = new ArrayList<>(this.parameters.length); - for (Parameter parameter : this.parameters) { - this.resolverTypes.add(isAggregator(parameter) ? AGGREGATOR : CONVERTER); - } - } - - /** - * Determine if the supplied {@link Parameter} is an aggregator (i.e., of - * type {@link ArgumentsAccessor} or annotated with {@link AggregateWith}). - * - * @return {@code true} if the parameter is an aggregator - */ - private static boolean isAggregator(Parameter parameter) { - return ArgumentsAccessor.class.isAssignableFrom(parameter.getType()) - || isAnnotated(parameter, AggregateWith.class); - } - - /** - * Determine if the {@link Method} represented by this context has a - * potentially valid signature (i.e., formal parameter - * declarations) with regard to aggregators. - * - *

This method takes a best-effort approach at enforcing the following - * policy for parameterized test methods that accept aggregators as arguments. - * - *

    - *
  1. zero or more indexed arguments come first.
  2. - *
  3. zero or more aggregators come next.
  4. - *
  5. zero or more arguments supplied by other {@code ParameterResolver} - * implementations come last.
  6. - *
- * - * @return {@code true} if the method has a potentially valid signature - */ - boolean hasPotentiallyValidSignature() { - int indexOfPreviousAggregator = -1; - for (int i = 0; i < getParameterCount(); i++) { - if (isAggregator(i)) { - if ((indexOfPreviousAggregator != -1) && (i != indexOfPreviousAggregator + 1)) { - return false; - } - indexOfPreviousAggregator = i; - } - } - return true; - } - - /** - * Get the number of parameters of the {@link Method} represented by this - * context. - */ - int getParameterCount() { - return parameters.length; - } - - /** - * Get the name of the {@link Parameter} with the supplied index, if - * it is present and declared before the aggregators. - * - * @return an {@code Optional} containing the name of the parameter - */ - Optional getParameterName(int parameterIndex) { - if (parameterIndex >= getParameterCount()) { - return Optional.empty(); - } - Parameter parameter = this.parameters[parameterIndex]; - if (!parameter.isNamePresent()) { - return Optional.empty(); - } - if (hasAggregator() && parameterIndex >= indexOfFirstAggregator()) { - return Optional.empty(); - } - return Optional.of(parameter.getName()); - } - - /** - * Determine if the {@link Method} represented by this context declares at - * least one {@link Parameter} that is an - * {@linkplain #isAggregator aggregator}. - * - * @return {@code true} if the method has an aggregator - */ - boolean hasAggregator() { - return resolverTypes.contains(AGGREGATOR); - } - - /** - * Determine if the {@link Parameter} with the supplied index is an - * aggregator (i.e., of type {@link ArgumentsAccessor} or annotated with - * {@link AggregateWith}). - * - * @return {@code true} if the parameter is an aggregator - */ - boolean isAggregator(int parameterIndex) { - return resolverTypes.get(parameterIndex) == AGGREGATOR; - } - - /** - * Find the index of the first {@linkplain #isAggregator aggregator} - * {@link Parameter} in the {@link Method} represented by this context. - * - * @return the index of the first aggregator, or {@code -1} if not found - */ - int indexOfFirstAggregator() { - return resolverTypes.indexOf(AGGREGATOR); - } - - /** - * Resolve the parameter for the supplied context using the supplied - * arguments. - */ - Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext, Object[] arguments, - int invocationIndex) { - return getResolver(parameterContext, extensionContext).resolve(parameterContext, arguments, invocationIndex); - } - - private Resolver getResolver(ParameterContext parameterContext, ExtensionContext extensionContext) { - int index = parameterContext.getIndex(); - if (resolvers[index] == null) { - resolvers[index] = resolverTypes.get(index).createResolver(parameterContext, extensionContext); - } - return resolvers[index]; - } - - enum ResolverType { - - CONVERTER { - @Override - Resolver createResolver(ParameterContext parameterContext, ExtensionContext extensionContext) { - try { // @formatter:off - return AnnotationSupport.findAnnotation(parameterContext.getParameter(), ConvertWith.class) - .map(ConvertWith::value) - .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentConverter.class, clazz, extensionContext)) - .map(converter -> AnnotationConsumerInitializer.initialize(parameterContext.getParameter(), converter)) - .map(Converter::new) - .orElse(Converter.DEFAULT); - } // @formatter:on - catch (Exception ex) { - throw parameterResolutionException("Error creating ArgumentConverter", ex, parameterContext); - } - } - }, - - AGGREGATOR { - @Override - Resolver createResolver(ParameterContext parameterContext, ExtensionContext extensionContext) { - try { // @formatter:off - return AnnotationSupport.findAnnotation(parameterContext.getParameter(), AggregateWith.class) - .map(AggregateWith::value) - .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsAggregator.class, clazz, extensionContext)) - .map(Aggregator::new) - .orElse(Aggregator.DEFAULT); - } // @formatter:on - catch (Exception ex) { - throw parameterResolutionException("Error creating ArgumentsAggregator", ex, parameterContext); - } - } - }; - - abstract Resolver createResolver(ParameterContext parameterContext, ExtensionContext extensionContext); - - } - - interface Resolver { - - Object resolve(ParameterContext parameterContext, Object[] arguments, int invocationIndex); - - } - - static class Converter implements Resolver { - - private static final Converter DEFAULT = new Converter(DefaultArgumentConverter.INSTANCE); - - private final ArgumentConverter argumentConverter; - - Converter(ArgumentConverter argumentConverter) { - this.argumentConverter = argumentConverter; - } - - @Override - public Object resolve(ParameterContext parameterContext, Object[] arguments, int invocationIndex) { - Object argument = arguments[parameterContext.getIndex()]; - try { - return this.argumentConverter.convert(argument, parameterContext); - } - catch (Exception ex) { - throw parameterResolutionException("Error converting parameter", ex, parameterContext); - } - } - - } - - static class Aggregator implements Resolver { - - private static final Aggregator DEFAULT = new Aggregator((accessor, context) -> accessor); - - private final ArgumentsAggregator argumentsAggregator; - - Aggregator(ArgumentsAggregator argumentsAggregator) { - this.argumentsAggregator = argumentsAggregator; - } - - @Override - public Object resolve(ParameterContext parameterContext, Object[] arguments, int invocationIndex) { - ArgumentsAccessor accessor = new DefaultArgumentsAccessor(parameterContext, invocationIndex, arguments); - try { - return this.argumentsAggregator.aggregateArguments(accessor, parameterContext); - } - catch (Exception ex) { - throw parameterResolutionException("Error aggregating arguments for parameter", ex, parameterContext); - } - } - - } - - private static ParameterResolutionException parameterResolutionException(String message, Exception cause, - ParameterContext parameterContext) { - String fullMessage = message + " at index " + parameterContext.getIndex(); - if (StringUtils.isNotBlank(cause.getMessage())) { - fullMessage += ": " + cause.getMessage(); - } - return new ParameterResolutionException(fullMessage, cause); - } - -} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodParameterResolver.java new file mode 100644 index 000000000000..be3d75322e35 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodParameterResolver.java @@ -0,0 +1,37 @@ +/* + * 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; + +import java.lang.reflect.Executable; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * @since 5.0 + */ +class ParameterizedTestMethodParameterResolver extends ParameterizedInvocationParameterResolver { + + private final Method testTemplateMethod; + + ParameterizedTestMethodParameterResolver(ParameterizedTestContext methodContext, EvaluatedArgumentSet arguments, + int invocationIndex) { + super(methodContext.getResolverFacade(), arguments, invocationIndex, ResolutionCache.DISABLED); + this.testTemplateMethod = methodContext.getAnnotatedElement(); + } + + @Override + protected boolean isSupportedOnConstructorOrMethod(Executable declaringExecutable, + ExtensionContext extensionContext) { + return this.testTemplateMethod.equals(declaringExecutable); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java deleted file mode 100644 index d49cdc29284e..000000000000 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java +++ /dev/null @@ -1,132 +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.params; - -import java.lang.reflect.Executable; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.extension.AfterTestExecutionCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store; -import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.jupiter.api.extension.ParameterResolutionException; -import org.junit.jupiter.api.extension.ParameterResolver; -import org.junit.platform.commons.support.AnnotationSupport; - -/** - * @since 5.0 - */ -class ParameterizedTestParameterResolver implements ParameterResolver, AfterTestExecutionCallback { - - private static final Namespace NAMESPACE = Namespace.create(ParameterizedTestParameterResolver.class); - - private final ParameterizedTestMethodContext methodContext; - private final Object[] arguments; - private final int invocationIndex; - - ParameterizedTestParameterResolver(ParameterizedTestMethodContext methodContext, Object[] arguments, - int invocationIndex) { - - this.methodContext = methodContext; - this.arguments = arguments; - this.invocationIndex = invocationIndex; - } - - @Override - public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { - return ExtensionContextScope.TEST_METHOD; - } - - @Override - public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { - Executable declaringExecutable = parameterContext.getDeclaringExecutable(); - Method testMethod = extensionContext.getTestMethod().orElse(null); - int parameterIndex = parameterContext.getIndex(); - - // Not a @ParameterizedTest method? - if (!declaringExecutable.equals(testMethod)) { - return false; - } - - // Current parameter is an aggregator? - if (this.methodContext.isAggregator(parameterIndex)) { - return true; - } - - // Ensure that the current parameter is declared before aggregators. - // Otherwise, a different ParameterResolver should handle it. - if (this.methodContext.hasAggregator()) { - return parameterIndex < this.methodContext.indexOfFirstAggregator(); - } - - // Else fallback to behavior for parameterized test methods without aggregators. - return parameterIndex < this.arguments.length; - } - - @Override - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) - throws ParameterResolutionException { - return this.methodContext.resolve(parameterContext, extensionContext, extractPayloads(this.arguments), - this.invocationIndex); - } - - /** - * @since 5.8 - */ - @Override - public void afterTestExecution(ExtensionContext context) { - ParameterizedTest parameterizedTest = AnnotationSupport.findAnnotation(context.getRequiredTestMethod(), - ParameterizedTest.class).get(); - if (!parameterizedTest.autoCloseArguments()) { - return; - } - - Store store = context.getStore(NAMESPACE); - AtomicInteger argumentIndex = new AtomicInteger(); - - Arrays.stream(this.arguments) // - .filter(AutoCloseable.class::isInstance) // - .map(AutoCloseable.class::cast) // - .map(CloseableArgument::new) // - .forEach(closeable -> store.put("closeableArgument#" + argumentIndex.incrementAndGet(), closeable)); - } - - private static class CloseableArgument implements Store.CloseableResource { - - private final AutoCloseable autoCloseable; - - CloseableArgument(AutoCloseable autoCloseable) { - this.autoCloseable = autoCloseable; - } - - @Override - public void close() throws Throwable { - this.autoCloseable.close(); - } - - } - - private Object[] extractPayloads(Object[] arguments) { - return Arrays.stream(arguments) // - .map(argument -> { - if (argument instanceof Named) { - return ((Named) argument).getPayload(); - } - return argument; - }) // - .toArray(); - } - -} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolutionCache.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolutionCache.java new file mode 100644 index 000000000000..eeec256dccd0 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolutionCache.java @@ -0,0 +1,41 @@ +/* + * 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; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import org.junit.jupiter.params.support.ParameterDeclaration; + +/** + * @since 5.13 + */ +interface ResolutionCache { + + static ResolutionCache enabled() { + return new Concurrent(); + } + + ResolutionCache DISABLED = (__, resolver) -> resolver.get(); + + Object resolve(ParameterDeclaration declaration, Supplier resolver); + + class Concurrent implements ResolutionCache { + + private final Map cache = new ConcurrentHashMap<>(); + + @Override + public Object resolve(ParameterDeclaration declaration, Supplier resolver) { + return cache.computeIfAbsent(declaration, __ -> resolver.get()); + } + } +} 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 new file mode 100644 index 000000000000..a2660bafdf6a --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java @@ -0,0 +1,771 @@ +/* + * 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; + +import static java.lang.System.lineSeparator; +import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; +import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; +import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; +import static org.junit.platform.commons.support.ReflectionSupport.makeAccessible; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import java.util.stream.Stream; + +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.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.params.aggregator.AggregateWith; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.aggregator.ArgumentsAggregationException; +import org.junit.jupiter.params.aggregator.ArgumentsAggregator; +import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator; +import org.junit.jupiter.params.converter.ArgumentConverter; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.converter.DefaultArgumentConverter; +import org.junit.jupiter.params.support.AnnotationConsumerInitializer; +import org.junit.jupiter.params.support.FieldContext; +import org.junit.jupiter.params.support.ParameterDeclaration; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.jupiter.params.support.ParameterInfo; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.function.Try; +import org.junit.platform.commons.support.ModifierSupport; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.StringUtils; + +class ResolverFacade { + + static ResolverFacade create(Class clazz, List fields) { + Preconditions.notEmpty(fields, "Fields must not be empty"); + + NavigableMap> allIndexedParameters = new TreeMap<>(); + Set aggregatorParameters = new LinkedHashSet<>(); + + for (Field field : fields) { + Parameter annotation = findAnnotation(field, Parameter.class) // + .orElseThrow(() -> new JUnitException("No @Parameter annotation present")); + int index = annotation.value(); + + FieldParameterDeclaration declaration = new FieldParameterDeclaration(field, annotation.value()); + if (declaration.isAggregator()) { + aggregatorParameters.add(declaration); + } + else { + if (fields.size() == 1 && index == Parameter.UNSET_INDEX) { + index = 0; + declaration = new FieldParameterDeclaration(field, 0); + } + allIndexedParameters.computeIfAbsent(index, __ -> new ArrayList<>()) // + .add(declaration); + } + } + + NavigableMap uniqueIndexedParameters = validateFieldDeclarations( + allIndexedParameters, aggregatorParameters); + + Stream.concat(uniqueIndexedParameters.values().stream(), aggregatorParameters.stream()) // + .forEach(declaration -> makeAccessible(declaration.getField())); + + return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0); + } + + static ResolverFacade create(Constructor constructor, ParameterizedClass annotation) { + java.lang.reflect.Parameter[] parameters = constructor.getParameters(); + // Inner classes get the outer instance as first parameter + int implicitParameters = parameters.length > 0 && parameters[0].isImplicit() ? 1 : 0; + return create(constructor, annotation, implicitParameters); + } + + static ResolverFacade create(Method method, Annotation annotation) { + return create(method, annotation, 0); + } + + /** + * Create a new {@link ResolverFacade} for the supplied {@link Executable}. + * + *

This method takes a best-effort approach at enforcing the following + * policy for parameterized class constructors and parameterized test + * methods that accept aggregators as arguments. + *

    + *
  1. zero or more indexed arguments come first.
  2. + *
  3. zero or more aggregators come next.
  4. + *
  5. zero or more arguments supplied by other {@code ParameterResolver} + * implementations come last.
  6. + *
+ */ + private static ResolverFacade create(Executable executable, Annotation annotation, int indexOffset) { + NavigableMap indexedParameters = new TreeMap<>(); + NavigableMap aggregatorParameters = new TreeMap<>(); + java.lang.reflect.Parameter[] parameters = executable.getParameters(); + for (int index = indexOffset; index < parameters.length; index++) { + ExecutableParameterDeclaration declaration = new ExecutableParameterDeclaration(parameters[index], index, + indexOffset); + if (declaration.isAggregator()) { + Preconditions.condition( + aggregatorParameters.isEmpty() + || aggregatorParameters.lastKey() == declaration.getParameterIndex() - 1, + () -> String.format( + "@%s %s declares formal parameters in an invalid order: " + + "argument aggregators must be declared after any indexed arguments " + + "and before any arguments resolved by another ParameterResolver.", + annotation.annotationType().getSimpleName(), + DefaultParameterDeclarations.describe(executable))); + aggregatorParameters.put(declaration.getParameterIndex(), declaration); + } + else if (aggregatorParameters.isEmpty()) { + indexedParameters.put(declaration.getParameterIndex(), declaration); + } + } + return new ResolverFacade(executable, indexedParameters, new LinkedHashSet<>(aggregatorParameters.values()), + indexOffset); + } + + private final int parameterIndexOffset; + private final Map resolvers; + private final DefaultParameterDeclarations indexedParameterDeclarations; + private final Set aggregatorParameters; + + private ResolverFacade(AnnotatedElement sourceElement, + NavigableMap indexedParameters, + Set aggregatorParameters, int parameterIndexOffset) { + this.aggregatorParameters = aggregatorParameters; + this.parameterIndexOffset = parameterIndexOffset; + this.resolvers = new ConcurrentHashMap<>(indexedParameters.size() + aggregatorParameters.size()); + this.indexedParameterDeclarations = new DefaultParameterDeclarations(sourceElement, indexedParameters); + } + + ParameterDeclarations getIndexedParameterDeclarations() { + return this.indexedParameterDeclarations; + } + + boolean isSupportedParameter(ParameterContext parameterContext, EvaluatedArgumentSet arguments) { + int index = toLogicalIndex(parameterContext); + if (this.indexedParameterDeclarations.get(index).isPresent()) { + return index < arguments.getConsumedLength(); + } + return !this.aggregatorParameters.isEmpty() + && this.aggregatorParameters.stream().anyMatch(it -> it.getParameterIndex() == index); + } + + /** + * Get the name of the parameter with the supplied index, if it is present + * and declared before the aggregators. + * + * @return an {@code Optional} containing the name of the parameter + */ + Optional getParameterName(int parameterIndex) { + return this.indexedParameterDeclarations.get(parameterIndex) // + .flatMap(ParameterDeclaration::getParameterName); + } + + /** + * Determine the length of the arguments array that is considered consumed + * by the parameter declarations in this resolver. + * + *

If an aggregator is present, all arguments are considered consumed. + * Otherwise, the consumed argument length is the minimum of the total + * length and the number of indexed parameter declarations. + */ + int determineConsumedArgumentLength(int totalLength) { + NavigableMap declarationsByIndex = this.indexedParameterDeclarations.declarationsByIndex; + return this.aggregatorParameters.isEmpty() // + ? Math.min(totalLength, declarationsByIndex.isEmpty() ? 0 : declarationsByIndex.lastKey() + 1) // + : totalLength; + } + + /** + * Determine the number of arguments that are considered consumed by the + * parameter declarations in this resolver. + * + *

If an aggregator is present, all arguments are considered consumed. + * Otherwise, the consumed argument count, is the number of indexes that + * correspond to indexed parameter declarations. + */ + int determineConsumedArgumentCount(EvaluatedArgumentSet arguments) { + if (this.aggregatorParameters.isEmpty()) { + return this.indexedParameterDeclarations.declarationsByIndex.subMap(0, + arguments.getConsumedLength()).size(); + } + return arguments.getTotalLength(); + } + + ArgumentSetLifecycleMethod.ParameterResolver createLifecycleMethodParameterResolver(Method method, + Annotation annotation) { + ResolverFacade originalResolverFacade = this; + ResolverFacade lifecycleMethodResolverFacade = create(method, annotation); + + Map parameterDeclarationMapping = new HashMap<>(); + List errors = validateLifecycleMethodParameters(method, annotation, originalResolverFacade, + lifecycleMethodResolverFacade, parameterDeclarationMapping); + + return Try // + .call(() -> configurationErrorOrSuccess(errors, + () -> new DefaultArgumentSetLifecycleMethodParameterResolver(originalResolverFacade, + lifecycleMethodResolverFacade, parameterDeclarationMapping))) // + .getOrThrow(cause -> new ExtensionConfigurationException( + String.format("Invalid @%s lifecycle method declaration: %s", + annotation.annotationType().getSimpleName(), method.toGenericString()), + cause)); + } + + /** + * Resolve the parameter for the supplied context using the supplied + * arguments. + */ + Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { + + int parameterIndex = toLogicalIndex(parameterContext); + ResolvableParameterDeclaration declaration = findDeclaration(parameterIndex) // + .orElseThrow( + () -> new ParameterResolutionException("Parameter index out of bounds: " + parameterIndex)); + + return resolutionCache.resolve(declaration, + () -> resolve(declaration, extensionContext, arguments, invocationIndex, Optional.of(parameterContext))); + } + + private Optional findDeclaration(int parameterIndex) { + ResolvableParameterDeclaration declaration = this.indexedParameterDeclarations.declarationsByIndex // + .get(parameterIndex); + if (declaration == null) { + return this.aggregatorParameters.stream() // + .filter(it -> it.getParameterIndex() == parameterIndex) // + .findFirst(); + } + return Optional.of(declaration); + } + + void resolveAndInjectFields(Object testInstance, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { + + if (this.indexedParameterDeclarations.sourceElement.equals(testInstance.getClass())) { + getAllParameterDeclarations() // + .filter(FieldParameterDeclaration.class::isInstance) // + .map(FieldParameterDeclaration.class::cast) // + .forEach(declaration -> setField(testInstance, declaration, extensionContext, arguments, + invocationIndex, resolutionCache)); + } + } + + private Stream getAllParameterDeclarations() { + return Stream.concat(this.indexedParameterDeclarations.declarationsByIndex.values().stream(), + aggregatorParameters.stream()); + } + + private void setField(Object testInstance, FieldParameterDeclaration declaration, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) { + + Object argument = resolutionCache.resolve(declaration, + () -> resolve(declaration, extensionContext, arguments, invocationIndex, Optional.empty())); + try { + declaration.getField().set(testInstance, argument); + } + catch (Exception e) { + throw new JUnitException("Failed to inject parameter value into field: " + declaration.getField(), e); + } + } + + private Object resolve(ResolvableParameterDeclaration parameterDeclaration, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex, Optional parameterContext) { + Resolver resolver = getResolver(extensionContext, parameterDeclaration); + return parameterDeclaration.resolve(resolver, extensionContext, arguments, invocationIndex, parameterContext); + } + + private Resolver getResolver(ExtensionContext extensionContext, ResolvableParameterDeclaration declaration) { + return this.resolvers.computeIfAbsent(declaration, __ -> this.aggregatorParameters.contains(declaration) // + ? createAggregator(declaration, extensionContext) // + : createConverter(declaration, extensionContext)); + } + + private int toLogicalIndex(ParameterContext parameterContext) { + int index = parameterContext.getIndex() - this.parameterIndexOffset; + Preconditions.condition(index >= 0, () -> "Parameter index must be greater than or equal to zero"); + return index; + } + + private static NavigableMap validateFieldDeclarations( + NavigableMap> indexedParameters, + Set aggregatorParameters) { + + List errors = new ArrayList<>(); + validateIndexedParameters(indexedParameters, errors); + validateAggregatorParameters(aggregatorParameters, errors); + + return configurationErrorOrSuccess(errors, () -> indexedParameters.entrySet().stream() // + .collect(toMap(Map.Entry::getKey, entry -> entry.getValue().get(0), (d, __) -> d, TreeMap::new))); + } + + private static List validateLifecycleMethodParameters(Method method, Annotation annotation, + ResolverFacade originalResolverFacade, ResolverFacade lifecycleMethodResolverFacade, + Map parameterDeclarationMapping) { + List actualDeclarations = lifecycleMethodResolverFacade.indexedParameterDeclarations.getAll(); + List errors = new ArrayList<>(); + for (int parameterIndex = 0; parameterIndex < actualDeclarations.size(); parameterIndex++) { + ParameterDeclaration actualDeclaration = actualDeclarations.get(parameterIndex); + ResolvableParameterDeclaration originalDeclaration = originalResolverFacade.indexedParameterDeclarations.declarationsByIndex // + .get(parameterIndex); + if (originalDeclaration == null) { + break; + } + if (!actualDeclaration.getParameterType().equals(originalDeclaration.getParameterType())) { + errors.add(String.format( + "parameter%s with index %d is incompatible with the parameter declared on the parameterized class: expected type '%s' but found '%s'", + parameterName(actualDeclaration), parameterIndex, originalDeclaration.getParameterType(), + actualDeclaration.getParameterType())); + } + else if (findAnnotation(actualDeclaration.getAnnotatedElement(), ConvertWith.class).isPresent()) { + errors.add(String.format("parameter%s with index %d must not be annotated with @ConvertWith", + parameterName(actualDeclaration), parameterIndex)); + } + else if (errors.isEmpty()) { + parameterDeclarationMapping.put(actualDeclaration, originalDeclaration); + } + } + return errors; + } + + private static String parameterName(ParameterDeclaration actualDeclaration) { + return actualDeclaration.getParameterName().map(name -> " '" + name + "'").orElse(""); + } + + private static T configurationErrorOrSuccess(List errors, Supplier successfulResult) { + if (errors.isEmpty()) { + return successfulResult.get(); + } + else if (errors.size() == 1) { + throw new PreconditionViolationException("Configuration error: " + errors.get(0) + "."); + } + else { + throw new PreconditionViolationException(String.format("%d configuration errors:%n%s", errors.size(), + errors.stream().collect(joining(lineSeparator() + "- ", "- ", "")))); + } + } + + private static void validateIndexedParameters( + NavigableMap> indexedParameters, List errors) { + + if (indexedParameters.isEmpty()) { + return; + } + + indexedParameters.forEach( + (index, declarations) -> validateIndexedParameterDeclarations(index, declarations, errors)); + + for (int index = 0; index <= indexedParameters.lastKey(); index++) { + if (!indexedParameters.containsKey(index)) { + errors.add(String.format("no field annotated with @Parameter(%d) declared", index)); + } + } + } + + private static void validateIndexedParameterDeclarations(int index, List declarations, + List errors) { + List fields = declarations.stream().map(FieldParameterDeclaration::getField).collect(toList()); + if (index < 0) { + declarations.stream() // + .map(declaration -> String.format( + "index must be greater than or equal to zero in @Parameter(%d) annotation on field [%s]", index, + declaration.getField())) // + .forEach(errors::add); + } + else if (declarations.size() > 1) { + errors.add( + String.format("duplicate index declared in @Parameter(%d) annotation on fields %s", index, fields)); + } + fields.stream() // + .filter(ModifierSupport::isFinal) // + .map(field -> String.format("@Parameter field [%s] must not be declared as final", field)) // + .forEach(errors::add); + } + + private static void validateAggregatorParameters(Set aggregatorParameters, + List errors) { + aggregatorParameters.stream() // + .filter(declaration -> declaration.getParameterIndex() != Parameter.UNSET_INDEX) // + .map(declaration -> String.format( + "no index may be declared in @Parameter(%d) annotation on aggregator field [%s]", + declaration.getParameterIndex(), declaration.getField())) // + .forEach(errors::add); + } + + private static Converter createConverter(ParameterDeclaration declaration, ExtensionContext extensionContext) { + try { // @formatter:off + return findAnnotation(declaration.getAnnotatedElement(), ConvertWith.class) + .map(ConvertWith::value) + .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentConverter.class, clazz, extensionContext)) + .map(converter -> AnnotationConsumerInitializer.initialize(declaration.getAnnotatedElement(), converter)) + .map(Converter::new) + .orElse(Converter.DEFAULT); + } // @formatter:on + catch (Exception ex) { + throw parameterResolutionException("Error creating ArgumentConverter", ex, declaration.getParameterIndex()); + } + } + + private static Aggregator createAggregator(ParameterDeclaration declaration, ExtensionContext extensionContext) { + try { // @formatter:off + return findAnnotation(declaration.getAnnotatedElement(), AggregateWith.class) + .map(AggregateWith::value) + .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsAggregator.class, clazz, extensionContext)) + .map(Aggregator::new) + .orElse(Aggregator.DEFAULT); + } // @formatter:on + catch (Exception ex) { + throw parameterResolutionException("Error creating ArgumentsAggregator", ex, + declaration.getParameterIndex()); + } + } + + private static ParameterResolutionException parameterResolutionException(String message, Exception cause, + int index) { + String fullMessage = message + " at index " + index; + if (StringUtils.isNotBlank(cause.getMessage())) { + fullMessage += ": " + cause.getMessage(); + } + return new ParameterResolutionException(fullMessage, cause); + } + + private interface Resolver { + + Object resolve(ParameterContext parameterContext, int parameterIndex, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex); + + Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, + int invocationIndex); + + } + + private static class Converter implements Resolver { + + private static final Converter DEFAULT = new Converter(DefaultArgumentConverter.INSTANCE); + + private final ArgumentConverter argumentConverter; + + Converter(ArgumentConverter argumentConverter) { + this.argumentConverter = argumentConverter; + } + + @Override + public Object resolve(ParameterContext parameterContext, int parameterIndex, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex) { + Object argument = arguments.getConsumedPayload(parameterIndex); + try { + return this.argumentConverter.convert(argument, parameterContext); + } + catch (Exception ex) { + throw parameterResolutionException("Error converting parameter", ex, parameterContext.getIndex()); + } + } + + @Override + public Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex) { + Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex()); + try { + return this.argumentConverter.convert(argument, fieldContext); + } + catch (Exception ex) { + throw parameterResolutionException("Error converting parameter", ex, fieldContext.getParameterIndex()); + } + } + } + + private static class Aggregator implements Resolver { + + private static final Aggregator DEFAULT = new Aggregator(new SimpleArgumentsAggregator() { + @Override + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { + return accessor; + } + }); + + private final ArgumentsAggregator argumentsAggregator; + + Aggregator(ArgumentsAggregator argumentsAggregator) { + this.argumentsAggregator = argumentsAggregator; + } + + @Override + public Object resolve(ParameterContext parameterContext, int parameterIndex, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex) { + ArgumentsAccessor accessor = ParameterInfo.get(extensionContext).getArguments(); + try { + return this.argumentsAggregator.aggregateArguments(accessor, parameterContext); + } + catch (Exception ex) { + throw parameterResolutionException("Error aggregating arguments for parameter", ex, + parameterContext.getIndex()); + } + } + + @Override + public Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex) { + ArgumentsAccessor accessor = ParameterInfo.get(extensionContext).getArguments(); + try { + return this.argumentsAggregator.aggregateArguments(accessor, fieldContext); + } + catch (Exception ex) { + throw parameterResolutionException("Error aggregating arguments for parameter", ex, + fieldContext.getParameterIndex()); + } + } + } + + private static class DefaultParameterDeclarations implements ParameterDeclarations { + + private final AnnotatedElement sourceElement; + private final NavigableMap declarationsByIndex; + + DefaultParameterDeclarations(AnnotatedElement sourceElement, + NavigableMap declarationsByIndex) { + this.sourceElement = sourceElement; + this.declarationsByIndex = declarationsByIndex; + } + + @Override + public AnnotatedElement getSourceElement() { + return this.sourceElement; + } + + @Override + public Optional getFirst() { + return this.declarationsByIndex.isEmpty() // + ? Optional.empty() // + : Optional.of(this.declarationsByIndex.firstEntry().getValue()); + } + + @Override + public List getAll() { + return unmodifiableList(new ArrayList<>(this.declarationsByIndex.values())); + } + + @Override + public Optional get(int parameterIndex) { + return Optional.ofNullable(this.declarationsByIndex.get(parameterIndex)); + } + + @Override + public String getSourceElementDescription() { + return describe(this.sourceElement); + } + + static String describe(AnnotatedElement sourceElement) { + if (sourceElement instanceof Method) { + return String.format("method [%s]", ((Method) sourceElement).toGenericString()); + } + if (sourceElement instanceof Constructor) { + return String.format("constructor [%s]", ((Constructor) sourceElement).toGenericString()); + } + if (sourceElement instanceof Class) { + return String.format("class [%s]", ((Class) sourceElement).getName()); + } + return sourceElement.toString(); + } + } + + private abstract static class ResolvableParameterDeclaration implements ParameterDeclaration { + + /** + * Determine if the supplied {@link Parameter} is an aggregator (i.e., of + * type {@link ArgumentsAccessor} or annotated with {@link AggregateWith}). + * + * @return {@code true} if the parameter is an aggregator + */ + boolean isAggregator() { + return ArgumentsAccessor.class.isAssignableFrom(getParameterType()) + || isAnnotated(getAnnotatedElement(), AggregateWith.class); + } + + protected abstract Object resolve(Resolver resolver, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex, + Optional originalParameterContext); + } + + private static class FieldParameterDeclaration extends ResolvableParameterDeclaration implements FieldContext { + + private final Field field; + private final int index; + + FieldParameterDeclaration(Field field, int index) { + this.field = field; + this.index = index; + } + + @Override + public Field getField() { + return this.field; + } + + @Override + public Field getAnnotatedElement() { + return this.field; + } + + @Override + public Class getParameterType() { + return this.field.getType(); + } + + @Override + public int getParameterIndex() { + return index; + } + + @Override + public Optional getParameterName() { + return Optional.of(this.field.getName()); + } + + @Override + public Object resolve(Resolver resolver, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, + int invocationIndex, Optional originalParameterContext) { + return resolver.resolve(this, extensionContext, arguments, invocationIndex); + } + } + + private static class ExecutableParameterDeclaration extends ResolvableParameterDeclaration { + + private final java.lang.reflect.Parameter parameter; + private final int index; + private final int indexOffset; + + ExecutableParameterDeclaration(java.lang.reflect.Parameter parameter, int index, int indexOffset) { + this.parameter = parameter; + this.index = index; + this.indexOffset = indexOffset; + } + + @Override + public java.lang.reflect.Parameter getAnnotatedElement() { + return this.parameter; + } + + @Override + public Class getParameterType() { + return this.parameter.getType(); + } + + @Override + public int getParameterIndex() { + return this.index - this.indexOffset; + } + + @Override + public Optional getParameterName() { + return this.parameter.isNamePresent() ? Optional.of(this.parameter.getName()) : Optional.empty(); + } + + @Override + public Object resolve(Resolver resolver, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, + int invocationIndex, Optional originalParameterContext) { + ParameterContext parameterContext = originalParameterContext // + .filter(it -> it.getParameter().equals(this.parameter)) // + .orElseGet(() -> toParameterContext(extensionContext, originalParameterContext)); + return resolver.resolve(parameterContext, getParameterIndex(), extensionContext, arguments, + invocationIndex); + } + + private ParameterContext toParameterContext(ExtensionContext extensionContext, + Optional originalParameterContext) { + Optional target = originalParameterContext.flatMap(ParameterContext::getTarget); + if (!target.isPresent()) { + target = extensionContext.getTestInstance(); + } + return toParameterContext(target); + } + + private ParameterContext toParameterContext(Optional target) { + return new ParameterContext() { + @Override + public java.lang.reflect.Parameter getParameter() { + return ExecutableParameterDeclaration.this.parameter; + } + + @Override + public int getIndex() { + return ExecutableParameterDeclaration.this.index; + } + + @Override + public Optional getTarget() { + return target; + } + }; + } + } + + private static class DefaultArgumentSetLifecycleMethodParameterResolver + implements ArgumentSetLifecycleMethod.ParameterResolver { + + private final ResolverFacade originalResolverFacade; + private final ResolverFacade lifecycleMethodResolverFacade; + private final Map parameterDeclarationMapping; + + DefaultArgumentSetLifecycleMethodParameterResolver(ResolverFacade originalResolverFacade, + ResolverFacade lifecycleMethodResolverFacade, + Map parameterDeclarationMapping) { + this.originalResolverFacade = originalResolverFacade; + this.lifecycleMethodResolverFacade = lifecycleMethodResolverFacade; + this.parameterDeclarationMapping = parameterDeclarationMapping; + } + + @Override + public boolean supports(ParameterContext parameterContext) { + return this.lifecycleMethodResolverFacade.findDeclaration(parameterContext.getIndex()) // + .filter(it -> this.parameterDeclarationMapping.containsKey(it) || it.isAggregator()) // + .isPresent(); + } + + @Override + public Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) { + + ResolvableParameterDeclaration actualDeclaration = this.lifecycleMethodResolverFacade // + .findDeclaration(parameterContext.getIndex()) // + .orElseThrow(() -> new ParameterResolutionException( + "Parameter index out of bounds: " + parameterContext.getIndex())); + + ResolvableParameterDeclaration originalDeclaration = this.parameterDeclarationMapping // + .get(actualDeclaration); + if (originalDeclaration == null) { + return this.lifecycleMethodResolverFacade.resolve(actualDeclaration, extensionContext, arguments, + invocationIndex, Optional.of(parameterContext)); + } + return resolutionCache.resolve(originalDeclaration, + () -> this.originalResolverFacade.resolve(originalDeclaration, extensionContext, arguments, + invocationIndex, Optional.of(parameterContext))); + } + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/AggregateWith.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/AggregateWith.java index e7fcca21eb50..0b30acecbccd 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/AggregateWith.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/AggregateWith.java @@ -24,10 +24,14 @@ * {@code @AggregateWith} is an annotation that allows one to specify an * {@link ArgumentsAggregator}. * - *

This annotation may be applied to a parameter of a + *

This annotation may be applied to parameters of a + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} + * constructor or its + * {@link org.junit.jupiter.params.Parameter @Parameter}-annotated fields, or to + * parameters of a * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest} method * in order for an aggregated value to be resolved for the annotated parameter - * when the test method is invoked. + * when the parameterized class or method is invoked. * *

{@code @AggregateWith} may also be used as a meta-annotation in order to * create a custom composed annotation that inherits the semantics @@ -38,7 +42,7 @@ * @see org.junit.jupiter.params.ParameterizedTest */ @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER, ElementType.FIELD }) @Documented @API(status = STABLE, since = "5.7") public @interface AggregateWith { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java index 90ce75ddd048..905b69e8fe33 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java @@ -10,11 +10,14 @@ package org.junit.jupiter.params.aggregator; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.params.support.FieldContext; +import org.junit.platform.commons.JUnitException; /** * {@code ArgumentsAggregator} is an abstraction for the aggregation of arguments @@ -43,6 +46,7 @@ * @since 5.2 * @see AggregateWith * @see ArgumentsAccessor + * @see SimpleArgumentsAggregator * @see org.junit.jupiter.params.ParameterizedTest */ @API(status = STABLE, since = "5.7") @@ -64,4 +68,27 @@ public interface ArgumentsAggregator { Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) throws ArgumentsAggregationException; + /** + * Aggregate the arguments contained in the supplied {@code accessor} into a + * single object. + * + * @param accessor an {@link ArgumentsAccessor} containing the arguments to be + * aggregated; never {@code null} + * @param context the field context where the aggregated result is to be + * injected; never {@code null} + * @return the aggregated result; may be {@code null} but only if the target + * type is a reference type + * @throws ArgumentsAggregationException if an error occurs during the + * aggregation + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + default Object aggregateArguments(ArgumentsAccessor accessor, FieldContext context) + throws ArgumentsAggregationException { + throw new JUnitException( + String.format("ArgumentsAggregator does not override the convert(ArgumentsAccessor, FieldContext) method. " + + "Please report this issue to the maintainers of %s.", + getClass().getName())); + } + } 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 79e1f37999c1..811a8abd0518 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 @@ -16,9 +16,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; import org.apiguardian.api.API; -import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.converter.DefaultArgumentConverter; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.commons.util.Preconditions; @@ -36,17 +36,24 @@ @API(status = INTERNAL, since = "5.2") public class DefaultArgumentsAccessor implements ArgumentsAccessor { - private final ParameterContext parameterContext; private final int invocationIndex; private final Object[] arguments; + private final BiFunction, Object> converter; - public DefaultArgumentsAccessor(ParameterContext parameterContext, int invocationIndex, Object... arguments) { - Preconditions.notNull(parameterContext, "ParameterContext must not be null"); - Preconditions.condition(invocationIndex >= 1, () -> "invocation index must be >= 1"); - Preconditions.notNull(arguments, "Arguments array must not be null"); - this.parameterContext = parameterContext; + public static DefaultArgumentsAccessor create(int invocationIndex, ClassLoader classLoader, Object[] arguments) { + Preconditions.notNull(classLoader, "ClassLoader must not be null"); + + BiFunction, Object> converter = (source, targetType) -> DefaultArgumentConverter.INSTANCE // + .convert(source, targetType, classLoader); + return new DefaultArgumentsAccessor(converter, invocationIndex, arguments); + } + + private DefaultArgumentsAccessor(BiFunction, Object> converter, int invocationIndex, + Object... arguments) { + Preconditions.condition(invocationIndex >= 1, () -> "Invocation index must be >= 1"); + this.converter = Preconditions.notNull(converter, "Converter must not be null"); this.invocationIndex = invocationIndex; - this.arguments = arguments; + this.arguments = Preconditions.notNull(arguments, "Arguments array must not be null"); } @Override @@ -61,8 +68,7 @@ public T get(int index, Class requiredType) { Preconditions.notNull(requiredType, "requiredType must not be null"); Object value = get(index); try { - Object convertedValue = DefaultArgumentConverter.INSTANCE.convert(value, requiredType, - this.parameterContext); + Object convertedValue = converter.apply(value, requiredType); return requiredType.cast(convertedValue); } catch (Exception ex) { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/SimpleArgumentsAggregator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/SimpleArgumentsAggregator.java new file mode 100644 index 000000000000..25373b624f75 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/SimpleArgumentsAggregator.java @@ -0,0 +1,48 @@ +/* + * 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.aggregator; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.AnnotatedElementContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.support.FieldContext; + +/** + * {@code SimpleArgumentsAggregator} is an abstract base class for + * {@link ArgumentsAggregator} implementations that do not need to distinguish + * between fields and method/constructor parameters. + * + * @since 5.0 + * @see ArgumentsAggregator + */ +@API(status = EXPERIMENTAL, since = "5.13") +public abstract class SimpleArgumentsAggregator implements ArgumentsAggregator { + + public SimpleArgumentsAggregator() { + } + + @Override + public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) + throws ArgumentsAggregationException { + return aggregateArguments(accessor, context.getParameter().getType(), context, context.getIndex()); + } + + @Override + public Object aggregateArguments(ArgumentsAccessor accessor, FieldContext context) + throws ArgumentsAggregationException { + return aggregateArguments(accessor, null, context, context.getParameterIndex()); + } + + protected abstract Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException; +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.java index b100f3ad4854..40dc578f40b4 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.java @@ -17,6 +17,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.support.AnnotationConsumer; +import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.util.Preconditions; /** @@ -49,6 +50,11 @@ public final Object convert(Object source, ParameterContext context) throws Argu return convert(source, context.getParameter().getType(), this.annotation); } + @Override + public final Object convert(Object source, FieldContext context) throws ArgumentConversionException { + return convert(source, context.getField().getType(), this.annotation); + } + /** * Convert the supplied {@code source} object into the supplied {@code targetType}, * based on metadata in the provided annotation. diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java index 78e4cc55e4d6..eae935d66e75 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java @@ -10,11 +10,14 @@ package org.junit.jupiter.params.converter; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.params.support.FieldContext; +import org.junit.platform.commons.JUnitException; /** * {@code ArgumentConverter} is an abstraction that allows an input object to @@ -40,7 +43,6 @@ * the {@link ParameterContext} to perform the conversion. * * @since 5.0 - * @see SimpleArgumentConverter * @see org.junit.jupiter.params.ParameterizedTest * @see org.junit.jupiter.params.converter.ConvertWith * @see org.junit.jupiter.params.support.AnnotationConsumer @@ -56,7 +58,7 @@ public interface ArgumentConverter { * * @param source the source object to convert; may be {@code null} * @param context the parameter context where the converted object will be - * used; never {@code null} + * supplied; never {@code null} * @return the converted object; may be {@code null} but only if the target * type is a reference type * @throws ArgumentConversionException if an error occurs during the @@ -64,4 +66,24 @@ public interface ArgumentConverter { */ Object convert(Object source, ParameterContext context) throws ArgumentConversionException; + /** + * Convert the supplied {@code source} object according to the supplied + * {@code context}. + * + * @param source the source object to convert; may be {@code null} + * @param context the field context where the converted object will be + * injected; never {@code null} + * @return the converted object; may be {@code null} but only if the target + * type is a reference type + * @throws ArgumentConversionException if an error occurs during the + * conversion + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + default Object convert(Object source, FieldContext context) throws ArgumentConversionException { + throw new JUnitException( + String.format("ArgumentConverter does not override the convert(Object, FieldContext) method. " + + "Please report this issue to the maintainers of %s.", + getClass().getName())); + } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ConvertWith.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ConvertWith.java index d9e7e4fb907d..66bea68bea1d 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ConvertWith.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ConvertWith.java @@ -23,16 +23,20 @@ /** * {@code @ConvertWith} is an annotation that allows one to specify an explicit * {@link ArgumentConverter}. - - *

This annotation may be applied to parameters of - * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest} methods + * + *

This annotation may be applied to parameters of a + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} + * constructor or its + * {@link org.junit.jupiter.params.Parameter @Parameter}-annotated fields, or to + * parameters of a + * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest} method * which need to have their {@code Arguments} converted before consuming them. * * @since 5.0 * @see org.junit.jupiter.params.ParameterizedTest * @see org.junit.jupiter.params.converter.ArgumentConverter */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.7") 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 df77d1f759ac..8544019c1894 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 @@ -11,6 +11,7 @@ package org.junit.jupiter.params.converter; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader; import java.io.File; import java.math.BigDecimal; @@ -23,9 +24,9 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.support.conversion.ConversionException; import org.junit.platform.commons.support.conversion.ConversionSupport; -import org.junit.platform.commons.util.ClassLoaderUtils; import org.junit.platform.commons.util.ReflectionUtils; /** @@ -58,10 +59,18 @@ private DefaultArgumentConverter() { @Override public final Object convert(Object source, ParameterContext context) { Class targetType = context.getParameter().getType(); - return convert(source, targetType, context); + ClassLoader classLoader = getClassLoader(context.getDeclaringExecutable().getDeclaringClass()); + return convert(source, targetType, classLoader); } - public final Object convert(Object source, Class targetType, ParameterContext context) { + @Override + public final Object convert(Object source, FieldContext context) throws ArgumentConversionException { + Class targetType = context.getField().getType(); + ClassLoader classLoader = getClassLoader(context.getField().getDeclaringClass()); + return convert(source, targetType, classLoader); + } + + public final Object convert(Object source, Class targetType, ClassLoader classLoader) { if (source == null) { if (targetType.isPrimitive()) { throw new ArgumentConversionException( @@ -75,10 +84,8 @@ public final Object convert(Object source, Class targetType, ParameterContext } if (source instanceof String) { - Class declaringClass = context.getDeclaringExecutable().getDeclaringClass(); - ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass); try { - return ConversionSupport.convert((String) source, targetType, classLoader); + return convert((String) source, targetType, classLoader); } catch (ConversionException ex) { throw new ArgumentConversionException(ex.getMessage(), ex); @@ -90,4 +97,8 @@ public final Object convert(Object source, Class targetType, ParameterContext source.getClass().getTypeName(), targetType.getTypeName())); } + Object convert(String source, Class targetType, ClassLoader classLoader) { + return ConversionSupport.convert(source, targetType, classLoader); + } + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/JavaTimeConversionPattern.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/JavaTimeConversionPattern.java index d4ab3110e629..c667722c6d8f 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/JavaTimeConversionPattern.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/JavaTimeConversionPattern.java @@ -20,18 +20,21 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; -import org.junit.jupiter.params.ParameterizedTest; /** * {@code @JavaTimeConversionPattern} is an annotation that allows a date/time * conversion pattern to be specified on a parameter of a - * {@link ParameterizedTest @ParameterizedTest} method. + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} + * or + * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest}. * * @since 5.0 + * @see ConvertWith + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see java.time.format.DateTimeFormatterBuilder#appendPattern(String) */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.7") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/SimpleArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/SimpleArgumentConverter.java index dcf714f5cb84..2cb0f3f922a6 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/SimpleArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/SimpleArgumentConverter.java @@ -14,6 +14,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.support.FieldContext; /** * {@code SimpleArgumentConverter} is an abstract base class for @@ -36,6 +37,11 @@ public final Object convert(Object source, ParameterContext context) throws Argu return convert(source, context.getParameter().getType()); } + @Override + public final Object convert(Object source, FieldContext context) throws ArgumentConversionException { + return convert(source, context.getField().getType()); + } + /** * Convert the supplied {@code source} object into the supplied * {@code targetType}. diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/TypedArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/TypedArgumentConverter.java index f229572a2a75..949cba18590c 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/TypedArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/TypedArgumentConverter.java @@ -14,6 +14,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ReflectionUtils; @@ -48,6 +49,15 @@ protected TypedArgumentConverter(Class sourceType, Class targetType) { @Override public final Object convert(Object source, ParameterContext context) throws ArgumentConversionException { + return convert(source, context.getParameter().getType()); + } + + @Override + public final Object convert(Object source, FieldContext context) throws ArgumentConversionException { + return convert(source, context.getField().getType()); + } + + private T convert(Object source, Class actualTargetType) { if (source == null) { return convert(null); } @@ -57,9 +67,9 @@ public final Object convert(Object source, ParameterContext context) throws Argu getClass().getSimpleName(), source.getClass().getName(), this.sourceType.getName()); throw new ArgumentConversionException(message); } - if (!ReflectionUtils.isAssignableTo(this.targetType, context.getParameter().getType())) { + if (!ReflectionUtils.isAssignableTo(this.targetType, actualTargetType)) { String message = String.format("%s cannot convert to type [%s]. Only target type [%s] is supported.", - getClass().getSimpleName(), context.getParameter().getType().getName(), this.targetType.getName()); + getClass().getSimpleName(), actualTargetType.getName(), this.targetType.getName()); throw new ArgumentConversionException(message); } return convert(this.sourceType.cast(source)); 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 b8ecb2f374dc..575d8dd59463 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 @@ -20,6 +20,8 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.support.AnnotationConsumer; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.Preconditions; /** @@ -28,6 +30,7 @@ * annotation in order to provide the arguments. * * @since 5.10 + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see org.junit.jupiter.params.provider.ArgumentsSource * @see org.junit.jupiter.params.provider.Arguments @@ -50,8 +53,8 @@ public final void accept(A annotation) { } @Override - public final Stream provideArguments(ExtensionContext context) { - return annotations.stream().flatMap(annotation -> provideArguments(context, annotation)); + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { + return annotations.stream().flatMap(annotation -> provideArguments(parameters, context, annotation)); } /** @@ -61,7 +64,21 @@ public final Stream provideArguments(ExtensionContext conte * @param context the current extension context; never {@code null} * @param annotation the annotation to process; never {@code null} * @return a stream of arguments; never {@code null} + * @deprecated Please implement + * {@link #provideArguments(ParameterDeclarations, ExtensionContext, Annotation)} + * instead. */ - protected abstract Stream provideArguments(ExtensionContext context, A annotation); + @Deprecated + protected Stream provideArguments(ExtensionContext context, A annotation) { + throw new JUnitException(String.format( + "AnnotationBasedArgumentsProvider does not override the provideArguments(ParameterDeclarations, ExtensionContext, Annotation) method. " + + "Please report this issue to the maintainers of %s.", + getClass().getName())); + } + + 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/ArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java index 253e99cbb149..058ab4803634 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java @@ -10,6 +10,8 @@ package org.junit.jupiter.params.provider; +import static org.apiguardian.api.API.Status.DEPRECATED; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.stream.Stream; @@ -17,11 +19,15 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.platform.commons.JUnitException; /** - * An {@code ArgumentsProvider} is responsible for {@linkplain #provideArguments - * providing} a stream of arguments to be passed to a {@code @ParameterizedTest} - * method. + * An {@code ArgumentsProvider} is responsible for + * {@linkplain #provideArguments(ParameterDeclarations, ExtensionContext) providing} + * a stream of arguments to be passed to a + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} or + * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest}. * *

An {@code ArgumentsProvider} can be registered via the * {@link ArgumentsSource @ArgumentsSource} annotation. @@ -30,6 +36,7 @@ * constructor to use {@linkplain ParameterResolver parameter resolution}. * * @since 5.0 + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see org.junit.jupiter.params.provider.ArgumentsSource * @see org.junit.jupiter.params.provider.Arguments @@ -44,7 +51,39 @@ public interface ArgumentsProvider { * * @param context the current extension context; never {@code null} * @return a stream of arguments; never {@code null} + * @deprecated Please implement + * {@link #provideArguments(ParameterDeclarations, ExtensionContext)} instead. */ - Stream provideArguments(ExtensionContext context) throws Exception; + @Deprecated + @API(status = DEPRECATED, since = "5.13") + default Stream provideArguments(@SuppressWarnings("unused") ExtensionContext context) + throws Exception { + throw new UnsupportedOperationException( + "Please implement provideArguments(ParameterDeclarations, ExtensionContext) instead."); + } + + /** + * Provide a {@link Stream} of {@link Arguments} to be passed to a + * {@code @ParameterizedClass} or {@code @ParameterizedTest}. + * + * @param parameters the parameter declarations for the parameterized + * class or test; never {@code null} + * @param context the current extension context; never {@code null} + * @return a stream of arguments; never {@code null} + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + default Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) + throws Exception { + try { + return provideArguments(context); + } + catch (Exception e) { + throw new JUnitException(String.format( + "ArgumentsProvider does not override the provideArguments(ParameterDeclarations, ExtensionContext) method. " + + "Please report this issue to the maintainers of %s.", + getClass().getName()), e); + } + } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSource.java index 7180cf80ea5a..34561ccd3e1f 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -24,18 +25,25 @@ /** * {@code @ArgumentsSource} is a {@linkplain Repeatable repeatable} annotation * that is used to register {@linkplain ArgumentsProvider arguments providers} - * for the annotated test method. + * for the annotated class or method. * *

{@code @ArgumentsSource} may also be used as a meta-annotation in order to * create a custom composed annotation that inherits the semantics * of {@code @ArgumentsSource}. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.0 * @see org.junit.jupiter.params.provider.ArgumentsProvider + * @see org.junit.jupiter.params.ParameterizedClass + * @see org.junit.jupiter.params.ParameterizedTest */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @Repeatable(ArgumentsSources.class) @API(status = STABLE, since = "5.7") public @interface ArgumentsSource { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSources.java index d40ff40ef6fc..ec85f3a4ba72 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSources.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -28,12 +29,17 @@ * optional since {@code @ArgumentsSource} is a {@linkplain java.lang.annotation.Repeatable * repeatable} annotation. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.0 * @see org.junit.jupiter.params.provider.ArgumentsSource */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @API(status = STABLE, since = "5.7") public @interface ArgumentsSources { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java index d248b2dd1cec..f5d25363c379 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Named; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.UnrecoverableExceptions; @@ -41,7 +42,8 @@ class CsvArgumentsProvider extends AnnotationBasedArgumentsProvider { private CsvParser csvParser; @Override - protected Stream provideArguments(ExtensionContext context, CsvSource csvSource) { + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + CsvSource csvSource) { this.nullValues = toSet(csvSource.nullValues()); this.csvParser = createParserFor(csvSource); final boolean textBlockDeclared = !csvSource.textBlock().isEmpty(); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileArgumentsProvider.java index acc13160d544..f514bb74bed7 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileArgumentsProvider.java @@ -34,6 +34,7 @@ import com.univocity.parsers.csv.CsvParser; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.Preconditions; @@ -58,7 +59,8 @@ class CsvFileArgumentsProvider extends AnnotationBasedArgumentsProvider provideArguments(ExtensionContext context, CsvFileSource csvFileSource) { + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + CsvFileSource csvFileSource) { this.charset = getCharsetFrom(csvFileSource); this.numLinesToSkip = csvFileSource.numLinesToSkip(); this.csvParser = createParserFor(csvFileSource); @@ -90,7 +92,7 @@ private CsvParser beginParsing(InputStream inputStream, CsvFileSource csvFileSou this.csvParser.beginParsing(inputStream, this.charset); } catch (Throwable throwable) { - handleCsvException(throwable, csvFileSource); + throw handleCsvException(throwable, csvFileSource); } return this.csvParser; } @@ -104,7 +106,7 @@ private Stream toStream(CsvParser csvParser, CsvFileSource csvFileSou csvParser.stopParsing(); } catch (Throwable throwable) { - handleCsvException(throwable, csvFileSource); + throw handleCsvException(throwable, csvFileSource); } }); } @@ -154,14 +156,14 @@ private void advance() { } } catch (Throwable throwable) { - handleCsvException(throwable, this.csvFileSource); + throw handleCsvException(throwable, this.csvFileSource); } } } @FunctionalInterface - private interface Source { + interface Source { InputStream open(ExtensionContext context); @@ -178,7 +180,7 @@ default Source classpathResource(String path) { } default Source file(String path) { - return context -> openFile(path); + return __ -> openFile(path); } } @@ -190,6 +192,7 @@ private static class DefaultInputStreamProvider implements InputStreamProvider { @Override public InputStream openClasspathResource(Class baseClass, String path) { Preconditions.notBlank(path, () -> "Classpath resource [" + path + "] must not be null or blank"); + //noinspection resource (closed elsewhere) InputStream inputStream = baseClass.getResourceAsStream(path); return Preconditions.notNull(inputStream, () -> "Classpath resource [" + path + "] does not exist"); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java index 3c2c8c14a3f3..3f06e2ff62f0 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java @@ -14,12 +14,14 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.junit.jupiter.params.ParameterizedInvocationConstants; /** * {@code @CsvFileSource} is a {@linkplain Repeatable repeatable} @@ -27,8 +29,10 @@ * files from one or more classpath {@link #resources} or {@link #files}. * *

The CSV records parsed from these resources and files will be provided as - * arguments to the annotated {@code @ParameterizedTest} method. Note that the - * first record may optionally be used to supply CSV headers (see + * arguments to the annotated + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} or + * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest}. Note + * that the first record may optionally be used to supply CSV headers (see * {@link #useHeadersInDisplayName}). * *

Any line beginning with a {@code #} symbol will be interpreted as a comment @@ -56,14 +60,20 @@ * column is trimmed by default. This behavior can be changed by setting the * {@link #ignoreLeadingAndTrailingWhitespace} attribute to {@code true}. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.0 * @see CsvSource * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @Repeatable(CsvFileSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(CsvFileArgumentsProvider.class) @@ -104,16 +114,16 @@ * for columns. * *

When set to {@code true}, the header names will be used in the - * generated display name for each {@code @ParameterizedTest} method - * invocation. When using this feature, you must ensure that the display name - * pattern for {@code @ParameterizedTest} includes - * {@value org.junit.jupiter.params.ParameterizedTest#ARGUMENTS_PLACEHOLDER} instead of - * {@value org.junit.jupiter.params.ParameterizedTest#ARGUMENTS_WITH_NAMES_PLACEHOLDER} + * generated display name for each {@code @ParameterizedClass} or + * {@code @ParameterizedTest} invocation. When using this feature, you must + * ensure that the display name pattern for {@code @ParameterizedClass} or + * {@code @ParameterizedTest} includes + * {@value ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER} instead of + * {@value ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER} * as demonstrated in the example below. * *

Defaults to {@code false}. * - * *

Example

*
 	 * {@literal @}ParameterizedTest(name = "[{index}] {arguments}")
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java
index c246d1000020..91decbc80993 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java
@@ -14,6 +14,7 @@
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
@@ -28,13 +29,18 @@
  * optional since {@code @CsvFileSource} is a {@linkplain java.lang.annotation.Repeatable
  * repeatable} annotation.
  *
+ * 

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.11 * @see CsvFileSource * @see java.lang.annotation.Repeatable */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @API(status = STABLE, since = "5.11") public @interface CsvFileSources { @@ -43,4 +49,5 @@ * annotations. */ CsvFileSource[] value(); + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java index 09732e2101f1..a06ef984acdd 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java @@ -14,12 +14,14 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.junit.jupiter.params.ParameterizedInvocationConstants; /** * {@code @CsvSource} is a {@linkplain Repeatable repeatable} @@ -28,7 +30,8 @@ * {@link #textBlock} attribute. * *

The supplied values will be provided as arguments to the annotated - * {@code @ParameterizedTest} method. + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} or + * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest}. * *

The column delimiter (which defaults to a comma ({@code ,})) can be customized * via either {@link #delimiter} or {@link #delimiterString}. @@ -59,15 +62,21 @@ * physical line within the text block. Thus, if a CSV column wraps across a * new line in a text block, the column must be a quoted string. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.0 * @see CsvFileSource * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Repeatable(CsvSources.class) @Documented +@Inherited @API(status = STABLE, since = "5.7") @ArgumentsSource(CsvArgumentsProvider.class) @SuppressWarnings("exports") @@ -163,11 +172,12 @@ * for columns. * *

When set to {@code true}, the header names will be used in the - * generated display name for each {@code @ParameterizedTest} method - * invocation. When using this feature, you must ensure that the display name - * pattern for {@code @ParameterizedTest} includes - * {@value org.junit.jupiter.params.ParameterizedTest#ARGUMENTS_PLACEHOLDER} instead of - * {@value org.junit.jupiter.params.ParameterizedTest#ARGUMENTS_WITH_NAMES_PLACEHOLDER} + * generated display name for each {@code @ParameterizedClass} or + * {@code @ParameterizedTest} invocation. When using this feature, you must + * ensure that the display name pattern for {@code @ParameterizedClass} or + * {@code @ParameterizedTest} includes + * {@value ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER} instead of + * {@value ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER} * as demonstrated in the example below. * *

Defaults to {@code false}. diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java index b5e48ab5de00..00981aceba84 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -28,13 +29,18 @@ * optional since {@code @CsvSource} is a {@linkplain java.lang.annotation.Repeatable * repeatable} annotation. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.11 * @see CsvSource * @see java.lang.annotation.Repeatable */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @API(status = STABLE, since = "5.11") public @interface CsvSources { @@ -43,4 +49,5 @@ * annotations. */ CsvSource[] value(); + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptyArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptyArgumentsProvider.java index 18e9d7d6c7b1..65657724932e 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptyArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptyArgumentsProvider.java @@ -15,7 +15,6 @@ import java.lang.reflect.Array; import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -29,6 +28,8 @@ import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclaration; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.Preconditions; @@ -39,15 +40,15 @@ class EmptyArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { - Method testMethod = context.getRequiredTestMethod(); - Class[] parameterTypes = testMethod.getParameterTypes(); + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { - Preconditions.condition(parameterTypes.length > 0, () -> String.format( - "@EmptySource cannot provide an empty argument to method [%s]: the method does not declare any formal parameters.", - testMethod.toGenericString())); + Optional firstParameter = parameters.getFirst(); - Class parameterType = parameterTypes[0]; + Preconditions.condition(firstParameter.isPresent(), + () -> String.format("@EmptySource cannot provide an empty argument to %s: no formal parameters declared.", + parameters.getSourceElementDescription())); + + Class parameterType = firstParameter.get().getParameterType(); if (String.class.equals(parameterType)) { return Stream.of(arguments("")); @@ -88,8 +89,8 @@ public Stream provideArguments(ExtensionContext context) { } // else throw new PreconditionViolationException( - String.format("@EmptySource cannot provide an empty argument to method [%s]: [%s] is not a supported type.", - testMethod.toGenericString(), parameterType.getName())); + String.format("@EmptySource cannot provide an empty argument to %s: [%s] is not a supported type.", + parameters.getSourceElementDescription(), parameterType.getName())); } private static Optional> getDefaultConstructor(Class clazz) { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptySource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptySource.java index fef989fc810d..bd70638ff840 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptySource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptySource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -22,12 +23,13 @@ /** * {@code @EmptySource} is an {@link ArgumentsSource} which provides a single - * empty argument to the annotated {@code @ParameterizedTest} method. + * empty argument to the annotated {@code @ParameterizedClass} + * or {@code @ParameterizedTest}. * *

Supported Parameter Types

* *

This argument source will only provide an empty argument for the following - * method parameter types. + * parameter types. * *

    *
  • {@link java.lang.String}
  • @@ -43,15 +45,21 @@ *
  • object arrays — for example {@code String[]}, {@code Integer[][]}, etc.
  • *
* + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.4 * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see NullSource * @see NullAndEmptySource */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @API(status = STABLE, since = "5.7") @ArgumentsSource(EmptyArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java index 27e2d3a57fc8..e234e3a7f722 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java @@ -13,12 +13,14 @@ import static java.util.Arrays.stream; import static java.util.stream.Collectors.toSet; -import java.lang.reflect.Method; import java.util.EnumSet; import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclaration; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.Preconditions; /** @@ -27,8 +29,9 @@ class EnumArgumentsProvider extends AnnotationBasedArgumentsProvider { @Override - protected Stream provideArguments(ExtensionContext context, EnumSource enumSource) { - Set> constants = getEnumConstants(context, enumSource); + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + EnumSource enumSource) { + Set> constants = getEnumConstants(parameters, enumSource); EnumSource.Mode mode = enumSource.mode(); String[] declaredConstantNames = enumSource.names(); if (declaredConstantNames.length > 0) { @@ -41,8 +44,9 @@ protected Stream provideArguments(ExtensionContext context, return constants.stream().map(Arguments::of); } - private > Set getEnumConstants(ExtensionContext context, EnumSource enumSource) { - Class enumClass = determineEnumClass(context, enumSource); + private > Set getEnumConstants(ParameterDeclarations parameters, + EnumSource enumSource) { + Class enumClass = determineEnumClass(parameters, enumSource); E[] constants = enumClass.getEnumConstants(); if (constants.length == 0) { Preconditions.condition(enumSource.from().isEmpty() && enumSource.to().isEmpty(), @@ -59,17 +63,18 @@ private > Set getEnumConstants(ExtensionContext c } @SuppressWarnings({ "unchecked", "rawtypes" }) - private > Class determineEnumClass(ExtensionContext context, EnumSource enumSource) { + private > Class determineEnumClass(ParameterDeclarations parameters, EnumSource enumSource) { Class enumClass = enumSource.value(); if (enumClass.equals(NullEnum.class)) { - Method method = context.getRequiredTestMethod(); - Class[] parameterTypes = method.getParameterTypes(); - Preconditions.condition(parameterTypes.length > 0, - () -> "Test method must declare at least one parameter: " + method.toGenericString()); - Preconditions.condition(Enum.class.isAssignableFrom(parameterTypes[0]), - () -> "First parameter must reference an Enum type (alternatively, use the annotation's 'value' attribute to specify the type explicitly): " - + method.toGenericString()); - enumClass = parameterTypes[0]; + enumClass = parameters.getFirst() // + .map(ParameterDeclaration::getParameterType).map(parameterType -> { + Preconditions.condition(Enum.class.isAssignableFrom(parameterType), + () -> "First parameter must reference an Enum type (alternatively, use the annotation's 'value' attribute to specify the type explicitly): " + + parameters.getSourceElementDescription()); + return (Class) parameterType; + }).orElseThrow( + () -> new PreconditionViolationException("There must be at least one declared parameter for " + + parameters.getSourceElementDescription())); } return enumClass; } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java index 20eb707d638d..a41b1af29455 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java @@ -16,6 +16,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -34,22 +35,28 @@ * {@link ArgumentsSource} for constants of an {@link Enum}. * *

The enum constants will be provided as arguments to the annotated - * {@code @ParameterizedTest} method. + * {@code @ParameterizedClass} or {@code @ParameterizedTest}. * *

The enum type can be specified explicitly using the {@link #value} * attribute. Otherwise, the declared type of the first parameter of the - * {@code @ParameterizedTest} method is used. + * {@code @ParameterizedClass} or {@code @ParameterizedTest} is used. * *

The set of enum constants can be restricted via the {@link #names}, * {@link #from}, {@link #to} and {@link #mode} attributes. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.0 * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @Repeatable(EnumSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(EnumArgumentsProvider.class) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java index 610589378783..8c6ba45bc6e4 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -28,13 +29,18 @@ * optional since {@code @EnumSource} is a {@linkplain java.lang.annotation.Repeatable * repeatable} annotation. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.11 * @see EnumSource * @see java.lang.annotation.Repeatable */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @API(status = STABLE, since = "5.11") public @interface EnumSources { @@ -43,4 +49,5 @@ * annotations. */ EnumSource[] value(); + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldArgumentsProvider.java index 8d02863a9e34..944419c351a6 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldArgumentsProvider.java @@ -14,15 +14,18 @@ import static java.util.Arrays.stream; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Iterator; +import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.BaseStream; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.support.ModifierSupport; import org.junit.platform.commons.support.ReflectionSupport; @@ -40,12 +43,16 @@ class FieldArgumentsProvider extends AnnotationBasedArgumentsProvider { @Override - protected Stream provideArguments(ExtensionContext context, FieldSource fieldSource) { + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + FieldSource fieldSource) { Class testClass = context.getRequiredTestClass(); Object testInstance = context.getTestInstance().orElse(null); String[] fieldNames = fieldSource.value(); if (fieldNames.length == 0) { - fieldNames = new String[] { context.getRequiredTestMethod().getName() }; + Optional testMethod = context.getTestMethod(); + Preconditions.condition(testMethod.isPresent(), + "You must specify a field name when using @FieldSource with @ParameterizedClass"); + fieldNames = new String[] { testMethod.get().getName() }; } // @formatter:off return stream(fieldNames) 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 8c2db1a90fb1..a84e040ee4c9 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 @@ -14,13 +14,13 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; -import org.junit.jupiter.params.ParameterizedTest; /** * {@code @FieldSource} is a {@linkplain Repeatable repeatable} @@ -32,7 +32,8 @@ *

Each field must be able to supply a stream of arguments, * and each set of "arguments" within the "stream" will be provided as the physical * arguments for individual invocations of the annotated - * {@link ParameterizedTest @ParameterizedTest} method. + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} or + * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest}. * *

In this context, a "stream" is anything that JUnit can reliably convert to * a {@link java.util.stream.Stream Stream}; however, the actual concrete field @@ -46,8 +47,8 @@ * {@link java.util.Iterator Iterator}, an array of objects, or an array of * primitives. Each set of "arguments" within the "stream" can be supplied as an * instance of {@link Arguments}, an array of objects (for example, {@code Object[]}, - * {@code String[]}, etc.), or a single value if the parameterized test - * method accepts a single argument. + * {@code String[]}, etc.), or a single value if the parameterized + * class or test accepts a single argument. * *

In contrast to the supported return types for {@link MethodSource @MethodSource} * factory methods, the value of a {@code @FieldSource} field cannot be an instance of @@ -104,16 +105,26 @@ * test instance lifecycle mode is used; whereas, fields in external classes must * always be {@code static}. * + *

This behavior and the above examples also apply to parameters of a + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass}, + * regardless whether field or constructor injection is used. + * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.11 * @see MethodSource * @see Arguments * @see ArgumentsSource - * @see ParameterizedTest + * @see org.junit.jupiter.params.ParameterizedClass + * @see org.junit.jupiter.params.ParameterizedTest * @see org.junit.jupiter.api.TestInstance */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @Repeatable(FieldSources.class) @API(status = EXPERIMENTAL, since = "5.11") @ArgumentsSource(FieldArgumentsProvider.class) @@ -131,7 +142,10 @@ * static nested class. * *

If no field names are declared, a field within the test class that has - * the same name as the test method will be used as the field by default. + * the same name as the test method will be used as the field by default in + * case this annotation is applied to a {@code @ParameterizedTest} method. + * For a {@code @ParameterizedClass}, at least one field name must be + * declared explicitly. * *

For further information, see the {@linkplain FieldSource class-level Javadoc}. */ diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java index f0ca8ad87940..b49836cd2543 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -28,13 +29,18 @@ * optional since {@code @FieldSource} is a {@linkplain java.lang.annotation.Repeatable * repeatable} annotation. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.11 * @see FieldSource * @see java.lang.annotation.Repeatable */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @API(status = EXPERIMENTAL, since = "5.11") public @interface FieldSources { @@ -43,4 +49,5 @@ * annotations. */ FieldSource[] value(); + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java index 3bfced72e817..4a67985cac84 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java @@ -18,6 +18,7 @@ import java.lang.reflect.Method; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; @@ -25,6 +26,7 @@ import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.ClassLoaderUtils; import org.junit.platform.commons.util.CollectionUtils; @@ -41,9 +43,10 @@ class MethodArgumentsProvider extends AnnotationBasedArgumentsProvider isConvertibleToStream(method.getReturnType()) && !isTestMethod(method); @Override - protected Stream provideArguments(ExtensionContext context, MethodSource methodSource) { + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + MethodSource methodSource) { Class testClass = context.getRequiredTestClass(); - Method testMethod = context.getRequiredTestMethod(); + Optional testMethod = context.getTestMethod(); Object testInstance = context.getTestInstance().orElse(null); String[] methodNames = methodSource.value(); // @formatter:off @@ -56,13 +59,15 @@ protected Stream provideArguments(ExtensionContext context, // @formatter:on } - private static Method findFactoryMethod(Class testClass, Method testMethod, String factoryMethodName) { + private static Method findFactoryMethod(Class testClass, Optional testMethod, String factoryMethodName) { String originalFactoryMethodName = factoryMethodName; // If the user did not provide a factory method name, find a "default" local // factory method with the same name as the parameterized test method. if (StringUtils.isBlank(factoryMethodName)) { - factoryMethodName = testMethod.getName(); + Preconditions.condition(testMethod.isPresent(), + "You must specify a method name when using @MethodSource with @ParameterizedClass"); + factoryMethodName = testMethod.get().getName(); return findFactoryMethodBySimpleName(testClass, testMethod, factoryMethodName); } @@ -103,7 +108,7 @@ private static boolean looksLikeAFullyQualifiedMethodName(String factoryMethodNa } // package-private for testing - static Method findFactoryMethodByFullyQualifiedName(Class testClass, Method testMethod, + static Method findFactoryMethodByFullyQualifiedName(Class testClass, Optional testMethod, String fullyQualifiedMethodName) { String[] methodParts = ReflectionUtils.parseFullyQualifiedMethodName(fullyQualifiedMethodName); String className = methodParts[0]; @@ -142,24 +147,25 @@ static Method findFactoryMethodByFullyQualifiedName(Class testClass, Method t * @throws PreconditionViolationException if the factory method was not found or * multiple competing factory methods with the same name were found */ - private static Method findFactoryMethodBySimpleName(Class clazz, Method testMethod, String factoryMethodName) { + private static Method findFactoryMethodBySimpleName(Class clazz, Optional testMethod, + String factoryMethodName) { Predicate isCandidate = candidate -> factoryMethodName.equals(candidate.getName()) - && !testMethod.equals(candidate); + && !candidate.equals(testMethod.orElse(null)); List candidates = ReflectionUtils.findMethods(clazz, isCandidate); List factoryMethods = candidates.stream().filter(isFactoryMethod).collect(toList()); - Preconditions.condition(factoryMethods.size() > 0, () -> { + Preconditions.notEmpty(factoryMethods, () -> { + if (candidates.isEmpty()) { + // Report that we didn't find anything. + return format("Could not find factory method [%s] in class [%s]", factoryMethodName, clazz.getName()); + } // If we didn't find the factory method using the isFactoryMethod Predicate, perhaps // the specified factory method has an invalid return type or is a test method. // In that case, we report the invalid candidates that were found. - if (candidates.size() > 0) { - return format( - "Could not find valid factory method [%s] in class [%s] but found the following invalid candidates: %s", - factoryMethodName, clazz.getName(), candidates); - } - // Otherwise, report that we didn't find anything. - return format("Could not find factory method [%s] in class [%s]", factoryMethodName, clazz.getName()); + return format( + "Could not find valid factory method [%s] in class [%s] but found the following invalid candidates: %s", + factoryMethodName, clazz.getName(), candidates); }); Preconditions.condition(factoryMethods.size() == 1, () -> format("%d factory methods named [%s] were found in class [%s]: %s", factoryMethods.size(), 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 2ea6da4da72f..c7685a58f9a5 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 @@ -14,13 +14,13 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; -import org.junit.jupiter.params.ParameterizedTest; /** * {@code @MethodSource} is a {@linkplain Repeatable repeatable} @@ -30,13 +30,14 @@ * by fully qualified method name. * *

Each factory method must generate a stream of arguments, - * and each set of "arguments" within the "stream" will be provided as the physical - * arguments for individual invocations of the annotated - * {@link ParameterizedTest @ParameterizedTest} method. Generally speaking this - * translates to a {@link java.util.stream.Stream Stream} of {@link Arguments} - * (i.e., {@code Stream}); however, the actual concrete return type - * can take on many forms. In this context, a "stream" is anything that JUnit - * can reliably convert into a {@code Stream}, such as + * and each set of "arguments" within the "stream" will be provided as the + * physical arguments for individual invocations of the annotated + * {@code org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} or + * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest}. + * Generally speaking this translates to a {@link java.util.stream.Stream Stream} + * of {@link Arguments} (i.e., {@code Stream}); however, the actual + * concrete return type can take on many forms. In this context, a "stream" is + * anything that JUnit can reliably convert into a {@code Stream}, such as * {@link java.util.stream.Stream Stream}, * {@link java.util.stream.DoubleStream DoubleStream}, * {@link java.util.stream.LongStream LongStream}, @@ -92,19 +93,29 @@ * test instance lifecycle mode is used; whereas, factory methods in external * classes must always be {@code static}. * + *

This behavior and the above examples also apply to parameters of a + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass}, + * regardless whether field or constructor injection is used. + * *

Factory methods can declare parameters, which will be provided by registered * implementations of {@link org.junit.jupiter.api.extension.ParameterResolver}. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.0 * @see FieldSource * @see Arguments * @see ArgumentsSource - * @see ParameterizedTest + * @see org.junit.jupiter.params.ParameterizedClass + * @see org.junit.jupiter.params.ParameterizedTest * @see org.junit.jupiter.api.TestInstance */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @Repeatable(MethodSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(MethodArgumentsProvider.class) @@ -131,7 +142,9 @@ * *

If no factory method names are declared, a method within the test class * that has the same name as the test method will be used as the factory - * method by default. + * method by default in case this annotation is applied to a + * {@code @ParameterizedTest} method. For a {@code @ParameterizedClass}, at + * least one method name must be declared explicitly. * *

For further information, see the {@linkplain MethodSource class-level Javadoc}. */ diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java index 605702827d2f..16460e3700be 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -28,13 +29,18 @@ * optional since {@code @MethodSource} is a {@linkplain java.lang.annotation.Repeatable * repeatable} annotation. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.11 * @see MethodSource * @see java.lang.annotation.Repeatable */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @API(status = STABLE, since = "5.11") public @interface MethodSources { @@ -43,4 +49,5 @@ * annotations. */ MethodSource[] value(); + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullAndEmptySource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullAndEmptySource.java index d38b2dff4ee4..727e3eab235e 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullAndEmptySource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullAndEmptySource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -25,18 +26,24 @@ * the functionality of {@link NullSource @NullSource} and * {@link EmptySource @EmptySource}. * - *

Annotating a {@code @ParameterizedTest} method with - * {@code @NullAndEmptySource} is equivalent to annotating the method with - * {@code @NullSource} and {@code @EmptySource}. + *

Annotating a {@code @ParameterizedClass} or {@code @ParameterizedTest} + * with {@code @NullAndEmptySource} is equivalent to annotating the method with + * both {@code @NullSource} and {@code @EmptySource}. + * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. * * @since 5.4 + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see NullSource * @see EmptySource */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @API(status = STABLE, since = "5.7") @NullSource @EmptySource diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullArgumentsProvider.java index eef9d19990c6..8cafbb51e3cf 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullArgumentsProvider.java @@ -12,10 +12,10 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; -import java.lang.reflect.Method; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.util.Preconditions; /** @@ -27,11 +27,10 @@ class NullArgumentsProvider implements ArgumentsProvider { private static final Arguments nullArguments = arguments(new Object[] { null }); @Override - public Stream provideArguments(ExtensionContext context) { - Method testMethod = context.getRequiredTestMethod(); - Preconditions.condition(testMethod.getParameterCount() > 0, () -> String.format( - "@NullSource cannot provide a null argument to method [%s]: the method does not declare any formal parameters.", - testMethod.toGenericString())); + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { + Preconditions.condition(parameters.getFirst().isPresent(), + () -> String.format("@NullSource cannot provide a null argument to %s: no formal parameters declared.", + parameters.getSourceElementDescription())); return Stream.of(nullArguments); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullSource.java index 3dce2cb097ad..07764760994a 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -22,21 +23,28 @@ /** * {@code @NullSource} is an {@link ArgumentsSource} which provides a single - * {@code null} argument to the annotated {@code @ParameterizedTest} method. + * {@code null} argument to the annotated {@code @ParameterizedClass} or + * {@code @ParameterizedTest}. * *

Note that {@code @NullSource} cannot be used for an argument that has * a primitive type, unless the argument is converted to a corresponding wrapper * type with an {@link org.junit.jupiter.params.converter.ArgumentConverter}. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.4 * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see EmptySource * @see NullAndEmptySource */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @API(status = STABLE, since = "5.7") @ArgumentsSource(NullArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueArgumentsProvider.java index 39bc714671da..a3a929dfa2ba 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueArgumentsProvider.java @@ -19,6 +19,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.util.Preconditions; /** @@ -27,7 +28,8 @@ class ValueArgumentsProvider extends AnnotationBasedArgumentsProvider { @Override - protected Stream provideArguments(ExtensionContext context, ValueSource valueSource) { + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + ValueSource valueSource) { Object[] arguments = getArgumentsFromSource(valueSource); return Arrays.stream(arguments).map(Arguments::of); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java index 55d8c50aaa2c..0dfa6bad1c22 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -32,15 +33,21 @@ * {@code @ValueSource} declaration. * *

The supplied literal values will be provided as arguments to the - * annotated {@code @ParameterizedTest} method. + * annotated {@code @ParameterizedClass} or {@code @ParameterizedTest}. + * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. * * @since 5.0 * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @Repeatable(ValueSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(ValueArgumentsProvider.class) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java index 6d52255d9713..d870ea3d641d 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -28,13 +29,18 @@ * optional since {@code @ValueSource} is a {@linkplain java.lang.annotation.Repeatable * repeatable} annotation. * + *

Inheritance

+ * + *

This annotation is inherited to subclasses. + * * @since 5.11 * @see ValueSource * @see java.lang.annotation.Repeatable */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @API(status = STABLE, since = "5.11") public @interface ValueSources { @@ -43,4 +49,5 @@ * annotations. */ ValueSource[] value(); + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java index 27f6b52853d7..785c9e571fc4 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java @@ -40,7 +40,7 @@ public final class AnnotationConsumerInitializer { private static final List annotationConsumingMethodSignatures = asList( // new AnnotationConsumingMethodSignature("accept", 1, 0), // - new AnnotationConsumingMethodSignature("provideArguments", 2, 1), // + new AnnotationConsumingMethodSignature("provideArguments", 3, 2), // new AnnotationConsumingMethodSignature("convert", 3, 2)); private AnnotationConsumerInitializer() { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/FieldContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/FieldContext.java new file mode 100644 index 000000000000..355da13d126e --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/FieldContext.java @@ -0,0 +1,50 @@ +/* + * 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.support; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.reflect.Field; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.AnnotatedElementContext; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; + +/** + * {@code FieldContext} encapsulates the context in which an + * {@link Parameter @Parameter}-annotated {@link Field} is declared in a + * {@link ParameterizedClass @ParameterizedClass}. + * + * @since 5.13 + * @see ParameterizedClass + * @see Parameter + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface FieldContext extends AnnotatedElementContext { + + /** + * {@return the field for this context; never {@code null}} + */ + Field getField(); + + /** + * {@return the index of the parameter} + * + *

This method returns {@value Parameter#UNSET_INDEX} for aggregator + * fields and a value greater than or equal to zero for indexed + * parameters. + * + * @see Parameter#value() + */ + int getParameterIndex(); + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclaration.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclaration.java new file mode 100644 index 000000000000..fca970de9df6 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclaration.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 org.junit.jupiter.params.support; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.reflect.AnnotatedElement; +import java.util.Optional; + +import org.apiguardian.api.API; + +/** + * {@code ParameterDeclaration} encapsulates the declaration of an + * indexed {@code @ParameterizedClass} or {@code @ParameterizedTest} parameter. + * + * @since 5.13 + * @see ParameterDeclarations + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface ParameterDeclaration { + + /** + * {@return the {@link AnnotatedElement} that declares the parameter; never + * {@code null}} + * + *

This is either a {@link java.lang.reflect.Parameter} or a + * {@link java.lang.reflect.Field}. + */ + AnnotatedElement getAnnotatedElement(); + + /** + * {@return the type of the parameter; never {@code null}} + */ + Class getParameterType(); + + /** + * {@return the index of the parameter} + */ + int getParameterIndex(); + + /** + * {@return the name of the parameter, if available; never {@code null} but + * potentially empty} + */ + Optional getParameterName(); + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclarations.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclarations.java new file mode 100644 index 000000000000..bc30402ad63f --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclarations.java @@ -0,0 +1,83 @@ +/* + * 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.support; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.reflect.AnnotatedElement; +import java.util.List; +import java.util.Optional; + +import org.apiguardian.api.API; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; + +/** + * {@code ParameterDeclarations} encapsulates the combined declarations + * of all indexed {@code @ParameterizedClass} or + * {@code @ParameterizedTest} parameters. + * + *

For a {@code @ParameterizedTest}, the parameter declarations are derived + * from the method signature. For a {@code @ParameterizedClass}, they may be + * derived from the constructor or + * {@link java.lang.reflect.Parameter @Parameter}-annotated fields. + * + *

Aggregators, that is parameters of type + * {@link ArgumentsAccessor ArgumentsAccessor} or parameters annotated with + * {@link org.junit.jupiter.params.aggregator.AggregateWith @AggregateWith}, are + * not indexed and thus not included in the list of parameter + * declarations. + * + * @since 5.13 + * @see ParameterDeclaration + * @see org.junit.jupiter.params.ParameterizedClass + * @see org.junit.jupiter.params.ParameterizedTest + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface ParameterDeclarations { + + /** + * {@return all indexed parameter declarations; never {@code null}, + * sorted by index} + */ + List getAll(); + + /** + * {@return the first indexed parameter declaration, if available; + * never {@code null}} + */ + Optional getFirst(); + + /** + * {@return the indexed parameter declaration for the supplied + * index, if available; never {@code null}} + */ + Optional get(int parameterIndex); + + /** + * {@return the source element of all parameter declarations} + * + *

For {@code @ParameterizedTest}, this always corresponds to the + * parameterized test method. For {@code @ParameterizedClass}, this + * corresponds to the parameterized test class constructor, if constructor + * injection is used; or the test class itself, if field injection is used. + */ + AnnotatedElement getSourceElement(); + + /** + * {@return a human-readable description of the source element} + * + *

This may, for example, be used in error messages. + * + * @see #getSourceElement() + */ + String getSourceElementDescription(); + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterInfo.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterInfo.java new file mode 100644 index 000000000000..03fd6b2a811d --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterInfo.java @@ -0,0 +1,85 @@ +/* + * 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.support; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.Nested; +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.params.ParameterizedClass; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; + +/** + * {@code ParameterInfo} is used to provide information about the current + * invocation of a parameterized class or test. + * + *

Registered {@link Extension} implementations may retrieve the current + * {@code ParameterInfo} instance by calling + * {@link ExtensionContext#getStore(Namespace)} with {@link #NAMESPACE} and + * {@link ExtensionContext.Store#get(Object, Class) Store.get(...)} with + * {@link #KEY}. Alternatively, the {@link #get(ExtensionContext)} method may + * be used to retrieve the {@code ParameterInfo} instance for the supplied + * {@code ExtensionContext}. Extensions must not modify any entries in the + * {@link ExtensionContext.Store Store} for {@link #NAMESPACE}. + * + *

When a {@link ParameterizedTest @ParameterizedTest} method is declared + * inside a {@link ParameterizedClass @ParameterizedClass} or a + * {@link Nested @Nested} {@link ParameterizedClass @ParameterizedClass} is + * declared inside an enclosing {@link ParameterizedClass @ParameterizedClass}, + * there will be multiple {@code ParameterInfo} instances available on different + * levels of the {@link ExtensionContext} hierarchy. In such cases, please use + * {@link ExtensionContext#getParent()} to navigate to the right level before + * retrieving the {@code ParameterInfo} instance from the + * {@link ExtensionContext.Store Store}. + * + * + * @since 5.13 + * @see ParameterizedClass + * @see ParameterizedTest + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface ParameterInfo { + + /** + * The {@link Namespace} for accessing the + * {@link ExtensionContext.Store Store} for {@code ParameterInfo}. + */ + Namespace NAMESPACE = Namespace.create(ParameterInfo.class); + + /** + * The key for retrieving the {@code ParameterInfo} instance from the + * {@link ExtensionContext.Store Store}. + */ + Object KEY = ParameterInfo.class; + + /** + * {@return the closest {@code ParameterInfo} instance for the supplied + * {@code ExtensionContext}; potentially {@code null}} + */ + static ParameterInfo get(ExtensionContext context) { + return context.getStore(NAMESPACE).get(KEY, ParameterInfo.class); + } + + /** + * {@return the declarations of all indexed parameters} + */ + ParameterDeclarations getDeclarations(); + + /** + * {@return the accessor to the arguments of the current invocation} + */ + ArgumentsAccessor getArguments(); + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/JUnitException.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/JUnitException.java index c09c739bc087..2dbc032984fd 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/JUnitException.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/JUnitException.java @@ -10,6 +10,7 @@ package org.junit.platform.commons; +import static org.apiguardian.api.API.Status.MAINTAINED; import static org.apiguardian.api.API.Status.STABLE; import org.apiguardian.api.API; @@ -33,4 +34,12 @@ public JUnitException(String message, Throwable cause) { super(message, cause); } + /** + * @since 1.13 + */ + @API(status = MAINTAINED, since = "1.13") + protected JUnitException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java index ece10478da6e..1f30697db788 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java @@ -165,7 +165,7 @@ public static Optional findAnnotation(AnnotatedElement * @since 1.8 * @see SearchOption * @see #findAnnotation(AnnotatedElement, Class) - * @deprecated Use {@link #findAnnotation(Class, Class, List)} + * @deprecated Use {@link #findAnnotation(AnnotatedElement, Class)} * (for {@code SearchOption.DEFAULT}) or * {@link #findAnnotation(Class, Class, List)} (for * {@code SearchOption.INCLUDE_ENCLOSING_CLASSES}) instead diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/Resource.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/Resource.java index da3d6df59fd1..4080d12a5bbe 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/Resource.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/Resource.java @@ -20,7 +20,8 @@ import org.apiguardian.api.API; /** - * Represents a resource on the classpath. + * {@code Resource} represents a resource on the classpath. + * * @since 1.11 * @see ReflectionSupport#findAllResourcesInClasspathRoot(URI, Predicate) * @see ReflectionSupport#findAllResourcesInPackage(String, Predicate) @@ -33,24 +34,27 @@ public interface Resource { /** - * Get the resource name. - *

- * The resource name is a {@code /}-separated path. The path is relative to - * the classpath root in which the resource is located. + * Get the name of this resource. + * + *

The resource name is a {@code /}-separated path. The path is relative + * to the classpath root in which the resource is located. * * @return the resource name; never {@code null} */ String getName(); /** - * Get URI to a resource. + * Get the URI of this resource. * * @return the uri of the resource; never {@code null} */ URI getUri(); /** - * Returns an input stream for reading this resource. + * Get an {@link InputStream} for reading this resource. + * + *

The default implementation delegates to {@link java.net.URL#openStream()} + * for this resource's {@link #getUri() URI}. * * @return an input stream for this resource; never {@code null} * @throws IOException if an I/O exception occurs @@ -58,4 +62,5 @@ public interface Resource { default InputStream getInputStream() throws IOException { return getUri().toURL().openStream(); } + } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/scanning/DefaultClasspathScanner.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/scanning/DefaultClasspathScanner.java index de9ec7de7b66..f4ff533b000c 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/scanning/DefaultClasspathScanner.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/scanning/DefaultClasspathScanner.java @@ -243,9 +243,10 @@ private String determineFullyQualifiedClassName(Path baseDir, String basePackage /** * The fully qualified resource name is a {@code /}-separated path. - *

- * The path is relative to the classpath root in which the resource is located. - + * + *

The path is relative to the classpath root in which the resource is + * located. + * * @return the resource name; never {@code null} */ private String determineFullyQualifiedResourceName(Path baseDir, String basePackageName, Path resourceFile) { diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageUtils.java index 884587c2928d..5e56191f4ad1 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageUtils.java @@ -95,17 +95,17 @@ public static Optional getAttribute(Class type, String name) { return Optional.ofNullable(mainAttributes.getValue(name)); } } - catch (Exception e) { + catch (Exception ex) { return Optional.empty(); } } /** * Get the module or implementation version for the supplied {@code type}. - *

- * The former is only available if the type is part of a versioned module on - * the module path; the latter only if the type is part of a JAR file with a - * manifest that contains an {@code Implementation-Version} attribute. + * + *

The former is only available if the type is part of a versioned module + * on the module path; the latter only if the type is part of a JAR file with + * a manifest that contains an {@code Implementation-Version} attribute. * * @since 1.11 */ @@ -117,4 +117,5 @@ public static Optional getModuleOrImplementationVersion(Class type) { } return getAttribute(type, Package::getImplementationVersion); } + } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java index ce006d770099..0a060bee0999 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java @@ -26,9 +26,9 @@ * itself. Any usage by external parties is not supported. * Use at your own risk! * - * @since 5.11 + * @since 1.11 */ -@API(status = API.Status.INTERNAL, since = "5.11") +@API(status = API.Status.INTERNAL, since = "1.11") public class ServiceLoaderUtils { private ServiceLoaderUtils() { diff --git a/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java b/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java index 878c62a2c7e7..db209116b615 100644 --- a/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java +++ b/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java @@ -26,9 +26,9 @@ * itself. Any usage by external parties is not supported. * Use at your own risk! * - * @since 5.11 + * @since 1.11 */ -@API(status = Status.INTERNAL, since = "5.11") +@API(status = Status.INTERNAL, since = "1.11") public class ServiceLoaderUtils { private ServiceLoaderUtils() { diff --git a/junit-platform-console-standalone/junit-platform-console-standalone.gradle.kts b/junit-platform-console-standalone/junit-platform-console-standalone.gradle.kts index a15f463a4d04..99ed213d9837 100644 --- a/junit-platform-console-standalone/junit-platform-console-standalone.gradle.kts +++ b/junit-platform-console-standalone/junit-platform-console-standalone.gradle.kts @@ -27,6 +27,7 @@ val vintageVersion: String by project tasks { jar { manifest { + attributes("Automatic-Module-Name" to "org.junit.platform.console.standalone") attributes("Main-Class" to "org.junit.platform.console.ConsoleLauncher") } } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/AnsiColorOptionMixin.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/AnsiColorOptionMixin.java index dfeb690544fc..4e9ee00ee6ac 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/AnsiColorOptionMixin.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/AnsiColorOptionMixin.java @@ -28,13 +28,13 @@ class AnsiColorOptionMixin { private boolean disableAnsiColors = System.getenv("NO_COLOR") != null; public boolean isDisableAnsiColors() { - return disableAnsiColors; + return this.disableAnsiColors; } @Option(names = "--disable-ansi-colors", description = "Disable ANSI colors in output (not supported by all terminals).") public void setDisableAnsiColors(boolean disableAnsiColors) { if (disableAnsiColors) { - commandSpec.commandLine().setColorScheme(defaultColorScheme(Ansi.OFF)); + this.commandSpec.commandLine().setColorScheme(defaultColorScheme(Ansi.OFF)); } this.disableAnsiColors = disableAnsiColors; } @@ -43,4 +43,5 @@ public void setDisableAnsiColors(boolean disableAnsiColors) { public void setDisableAnsiColors2(boolean disableAnsiColors) { setDisableAnsiColors(disableAnsiColors); } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/BaseCommand.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/BaseCommand.java index 51d29e0c157b..5f904aa2d2f0 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/BaseCommand.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/BaseCommand.java @@ -106,4 +106,5 @@ protected final CommandLine.Help.ColorScheme getColorScheme() { } protected abstract T execute(PrintWriter out); + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/ClasspathEntriesConverter.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/ClasspathEntriesConverter.java index 123edba1bb7a..a0f2fa15774d 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/ClasspathEntriesConverter.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/ClasspathEntriesConverter.java @@ -20,8 +20,10 @@ import picocli.CommandLine; class ClasspathEntriesConverter implements CommandLine.ITypeConverter> { + @Override public List convert(String value) { return Stream.of(value.split(File.pathSeparator)).map(Paths::get).collect(Collectors.toList()); } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandFacade.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandFacade.java index 4409fb6ef762..3fe0844c375b 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandFacade.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandFacade.java @@ -53,4 +53,5 @@ private CommandResult run(String[] args, Optional outputS version.map(it -> it.endsWith("-SNAPSHOT") ? "snapshot" : it).orElse("current")); return new MainCommand(consoleTestExecutorFactory).run(args, outputStreamConfig); } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandResult.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandResult.java index 147029e80f1d..87a202e0560a 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandResult.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandResult.java @@ -23,12 +23,12 @@ public class CommandResult { /** - * Exit code indicating successful execution + * Exit code indicating successful execution. */ public static final int SUCCESS = 0; /** - * Exit code indicating any failure(s) + * Exit code indicating any failure(s). */ protected static final int FAILURE = -1; @@ -53,10 +53,11 @@ private CommandResult(int exitCode, T value) { } public int getExitCode() { - return exitCode; + return this.exitCode; } public Optional getValue() { - return Optional.ofNullable(value); + return Optional.ofNullable(this.value); } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/ConsoleUtils.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/ConsoleUtils.java index ed82e426dca0..680e3381df6a 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/ConsoleUtils.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/ConsoleUtils.java @@ -37,4 +37,5 @@ public class ConsoleUtils { public static Charset charset() { return Charset.defaultCharset(); } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/DiscoverTestsCommand.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/DiscoverTestsCommand.java index 42e476f0a472..a53d98a681e1 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/DiscoverTestsCommand.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/DiscoverTestsCommand.java @@ -43,4 +43,5 @@ protected Void execute(PrintWriter out) { this.consoleTestExecutorFactory.create(discoveryOptions, testOutputOptions).discover(out); return null; } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/ExecuteTestsCommand.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/ExecuteTestsCommand.java index caa0340cb76f..079a1b740b12 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/ExecuteTestsCommand.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/ExecuteTestsCommand.java @@ -106,4 +106,5 @@ Optional getReportsDir() { return reportsDir == null ? Optional.ofNullable(reportsDir2) : Optional.of(reportsDir); } } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/ListTestEnginesCommand.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/ListTestEnginesCommand.java index 42cfde790699..a83ac4530e7c 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/ListTestEnginesCommand.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/ListTestEnginesCommand.java @@ -48,4 +48,5 @@ private void displayEngine(PrintWriter out, TestEngine engine) { engine.getVersion().ifPresent(details::add); out.println(getColorScheme().string(String.format("@|bold %s|@%s", engine.getId(), details))); } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/MainCommand.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/MainCommand.java index cb186813b716..177a17913681 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/MainCommand.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/MainCommand.java @@ -156,4 +156,5 @@ private static CommandLine getLikelyExecutedCommand(final CommandLine commandLin .map(parseResult -> parseResult.commandSpec().commandLine()) // .orElse(commandLine); } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/OutputStreamConfig.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/OutputStreamConfig.java index 8925bddf2f77..560c25ac3cf1 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/OutputStreamConfig.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/OutputStreamConfig.java @@ -29,6 +29,7 @@ class OutputStreamConfig { } void applyTo(CommandLine commandLine) { - commandLine.setOut(out).setErr(err); + commandLine.setOut(this.out).setErr(this.err); } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java index 9f0c139d2c4a..66c7f6f14d89 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java @@ -64,7 +64,6 @@ public FileSelector convert(String value) { FilePosition filePosition = FilePosition.fromQuery(uri.getQuery()).orElse(null); return selectFile(path, filePosition); } - } static class Directory implements ITypeConverter { diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptions.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptions.java index 06067a1022f5..73f4fc4a2416 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptions.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptions.java @@ -32,6 +32,8 @@ public class TestConsoleOutputOptions { private boolean isSingleColorPalette; private Details details = DEFAULT_DETAILS; private Theme theme = DEFAULT_THEME; + private Path stdoutPath; + private Path stderrPath; public boolean isAnsiColorOutputDisabled() { return this.ansiColorOutputDisabled; @@ -73,4 +75,24 @@ public void setTheme(Theme theme) { this.theme = theme; } + @API(status = INTERNAL, since = "1.13") + public Path getStdoutPath() { + return this.stdoutPath; + } + + @API(status = INTERNAL, since = "1.13") + public void setStdoutPath(Path stdoutPath) { + this.stdoutPath = stdoutPath; + } + + @API(status = INTERNAL, since = "1.13") + public Path getStderrPath() { + return this.stderrPath; + } + + @API(status = INTERNAL, since = "1.13") + public void setStderrPath(Path stderrPath) { + this.stderrPath = stderrPath; + } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptionsMixin.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptionsMixin.java index 44ee07588e4a..20103a737d99 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptionsMixin.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptionsMixin.java @@ -51,11 +51,19 @@ static class ConsoleOutputOptions { @Option(names = "-details-theme", hidden = true) private final Theme theme2 = DEFAULT_THEME; + @Option(names = "--redirect-stdout", paramLabel = "FILE", description = "Redirect test output to stdout to a file.") + private Path stdout; + + @Option(names = "--redirect-stderr", paramLabel = "FILE", description = "Redirect test output to stderr to a file.") + private Path stderr; + private void applyTo(TestConsoleOutputOptions result) { result.setColorPalettePath(choose(colorPalette, colorPalette2, null)); result.setSingleColorPalette(singleColorPalette || singleColorPalette2); result.setDetails(choose(details, details2, DEFAULT_DETAILS)); result.setTheme(choose(theme, theme2, DEFAULT_THEME)); + result.setStdoutPath(stdout); + result.setStderrPath(stderr); } } @@ -70,4 +78,5 @@ TestConsoleOutputOptions toTestConsoleOutputOptions() { private static T choose(T left, T right, T defaultValue) { return left == right ? left : (left == defaultValue ? right : left); } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java index 06278aace90f..0345dde65603 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java @@ -311,4 +311,5 @@ public TestDiscoveryOptions setConfigurationParametersResources(List con this.configurationParametersResources = configurationParametersResources; return this; } + } 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 3f91263094b9..1d1404650f23 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 @@ -348,4 +348,5 @@ private static List merge(List list1, List list2) { result.addAll(list2 == null ? Collections.emptyList() : list2); return result; } + } 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 ab64005eded8..ed08f30b5adc 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 @@ -14,6 +14,7 @@ import static org.junit.platform.console.tasks.DiscoveryRequestCreator.toDiscoveryRequestBuilder; import static org.junit.platform.launcher.LauncherConstants.OUTPUT_DIR_PROPERTY_NAME; +import java.io.PrintStream; import java.io.PrintWriter; import java.net.URL; import java.net.URLClassLoader; @@ -101,10 +102,17 @@ private TestExecutionSummary executeTests(PrintWriter out, Optional report Launcher launcher = launcherSupplier.get(); SummaryGeneratingListener summaryListener = registerListeners(out, reportsDir, launcher); - LauncherDiscoveryRequestBuilder discoveryRequestBuilder = toDiscoveryRequestBuilder(discoveryOptions); - reportsDir.ifPresent(dir -> discoveryRequestBuilder.configurationParameter(OUTPUT_DIR_PROPERTY_NAME, - dir.toAbsolutePath().toString())); - launcher.execute(discoveryRequestBuilder.build()); + PrintStream originalOut = System.out; + PrintStream originalErr = System.err; + try (StandardStreamsHandler standardStreamsHandler = new StandardStreamsHandler()) { + standardStreamsHandler.redirectStandardStreams(outputOptions.getStdoutPath(), + outputOptions.getStderrPath()); + launchTests(launcher, reportsDir); + } + finally { + System.setOut(originalOut); + System.setErr(originalErr); + } TestExecutionSummary summary = summaryListener.getSummary(); if (summary.getTotalFailureCount() > 0 || outputOptions.getDetails() != Details.NONE) { @@ -114,6 +122,13 @@ private TestExecutionSummary executeTests(PrintWriter out, Optional report return summary; } + private void launchTests(Launcher launcher, Optional reportsDir) { + LauncherDiscoveryRequestBuilder discoveryRequestBuilder = toDiscoveryRequestBuilder(discoveryOptions); + reportsDir.ifPresent(dir -> discoveryRequestBuilder.configurationParameter(OUTPUT_DIR_PROPERTY_NAME, + dir.toAbsolutePath().toString())); + launcher.execute(discoveryRequestBuilder.build()); + } + private Optional createCustomClassLoader() { List additionalClasspathEntries = discoveryOptions.getExistingAdditionalClasspathEntries(); if (!additionalClasspathEntries.isEmpty()) { @@ -194,4 +209,5 @@ private void printSummary(TestExecutionSummary summary, PrintWriter out) { public interface Factory { ConsoleTestExecutor create(TestDiscoveryOptions discoveryOptions, TestConsoleOutputOptions outputOptions); } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/StandardStreamsHandler.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/StandardStreamsHandler.java new file mode 100644 index 000000000000..50bcb1309054 --- /dev/null +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/StandardStreamsHandler.java @@ -0,0 +1,102 @@ +/* + * 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 java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.platform.commons.JUnitException; + +class StandardStreamsHandler implements AutoCloseable { + + private PrintStream stdout; + + private PrintStream stderr; + + public StandardStreamsHandler() { + } + + /** + * Redirect standard output (stdout) and standard error (stderr) to the specified + * file paths. + * + *

If the paths are the same, both streams are redirected to the same file. + * + *

The default charset is used for writing to the files. + * + * @param stdoutPath the file path for standard output, or {@code null} to + * indicate no redirection + * @param stderrPath the file path for standard error, or {@code null} to + * indicate no redirection + */ + public void redirectStandardStreams(Path stdoutPath, Path stderrPath) { + if (isSameFile(stdoutPath, stderrPath)) { + try { + PrintStream commonStream = new PrintStream(Files.newOutputStream(stdoutPath), true); + this.stdout = commonStream; + this.stderr = commonStream; + } + catch (IOException ex) { + throw new JUnitException("Error redirecting stdout and stderr to file: " + stdoutPath, ex); + } + } + else { + if (stdoutPath != null) { + try { + this.stdout = new PrintStream(Files.newOutputStream(stdoutPath), true); + } + catch (IOException ex) { + throw new JUnitException("Error redirecting stdout to file: " + stdoutPath, ex); + } + } + + if (stderrPath != null) { + try { + this.stderr = new PrintStream(Files.newOutputStream(stderrPath), true); + } + catch (IOException ex) { + throw new JUnitException("Error redirecting stderr to file: " + stderrPath, ex); + } + } + } + + if (this.stdout != null) { + System.setOut(this.stdout); + } + if (this.stderr != null) { + System.setErr(this.stderr); + } + } + + @Override + public void close() { + try { + if (this.stdout != null) { + this.stdout.close(); + } + } + finally { + if (this.stderr != null) { + this.stderr.close(); + } + } + } + + private static boolean isSameFile(Path path1, Path path2) { + if (path1 == null || path2 == null) { + return false; + } + return path1.normalize().toAbsolutePath().equals(path2.normalize().toAbsolutePath()); + } + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/CompositeTestDescriptorVisitor.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/CompositeTestDescriptorVisitor.java new file mode 100644 index 000000000000..28de4e46b881 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/CompositeTestDescriptorVisitor.java @@ -0,0 +1,41 @@ +/* + * 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; + +import java.util.Arrays; + +import org.junit.platform.commons.util.Preconditions; + +/** + * @since 1.13 + */ +final class CompositeTestDescriptorVisitor implements TestDescriptor.Visitor { + + private final TestDescriptor.Visitor[] visitors; + + static TestDescriptor.Visitor from(TestDescriptor.Visitor... visitors) { + Preconditions.notNull(visitors, "visitors must not be null"); + Preconditions.notEmpty(visitors, "visitors must not be empty"); + Preconditions.containsNoNullElements(visitors, "visitors must not contain any null elements"); + return visitors.length == 1 ? visitors[0] : new CompositeTestDescriptorVisitor(visitors); + } + + private CompositeTestDescriptorVisitor(TestDescriptor.Visitor[] visitors) { + this.visitors = Arrays.copyOf(visitors, visitors.length); + } + + @Override + public void visit(TestDescriptor descriptor) { + for (TestDescriptor.Visitor visitor : visitors) { + visitor.visit(descriptor); + } + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/DefaultDiscoveryIssue.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/DefaultDiscoveryIssue.java new file mode 100644 index 000000000000..055aa4029191 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/DefaultDiscoveryIssue.java @@ -0,0 +1,114 @@ +/* + * 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; + +import java.util.Objects; +import java.util.Optional; + +import org.junit.platform.commons.util.ToStringBuilder; + +/** + * @since 1.13 + */ +final class DefaultDiscoveryIssue implements DiscoveryIssue { + + private final Severity severity; + private final String message; + private final TestSource source; + private final Throwable cause; + + DefaultDiscoveryIssue(Builder builder) { + this.severity = builder.severity; + this.message = builder.message; + this.source = builder.source; + this.cause = builder.cause; + } + + @Override + public Severity severity() { + return this.severity; + } + + @Override + public String message() { + return this.message; + } + + @Override + public Optional source() { + return Optional.ofNullable(this.source); + } + + @Override + public Optional cause() { + return Optional.ofNullable(this.cause); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) + return false; + DefaultDiscoveryIssue that = (DefaultDiscoveryIssue) o; + return this.severity == that.severity // + && Objects.equals(this.message, that.message) // + && Objects.equals(this.source, that.source) // + && Objects.equals(this.cause, that.cause); + } + + @Override + public int hashCode() { + return Objects.hash(this.severity, this.message, this.source, this.cause); + } + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(DiscoveryIssue.class.getSimpleName()) // + .append("severity", this.severity) // + .append("message", this.message); + if (this.source != null) { + builder.append("source", this.source); + } + if (this.cause != null) { + builder.append("cause", this.cause); + } + return builder.toString(); + } + + static class Builder implements DiscoveryIssue.Builder { + + private final Severity severity; + private final String message; + private TestSource source; + public Throwable cause; + + Builder(Severity severity, String message) { + this.severity = severity; + this.message = message; + } + + @Override + public Builder source(TestSource source) { + this.source = source; + return this; + } + + @Override + public Builder cause(Throwable cause) { + this.cause = cause; + return this; + } + + @Override + public DiscoveryIssue build() { + return new DefaultDiscoveryIssue(this); + } + } +} 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 new file mode 100644 index 000000000000..4524b291a1b3 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoveryIssue.java @@ -0,0 +1,137 @@ +/* + * 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; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.Optional; + +import org.apiguardian.api.API; + +/** + * {@code DiscoveryIssue} represents an issue that was encountered during test + * discovery by a {@link TestEngine}. + * + * @since 1.13 + */ +@API(status = EXPERIMENTAL, since = "1.13") +public interface DiscoveryIssue { + + /** + * Create a new {@code DiscoveryIssue} with the supplied {@link Severity} and + * message. + * + * @see #builder(Severity, String) + */ + static DiscoveryIssue create(Severity severity, String message) { + return builder(severity, message).build(); + } + + /** + * Create a new {@link Builder} for creating a {@code DiscoveryIssue} with + * the supplied {@link Severity} and message. + * + * @see Builder + * @see #create(Severity, String) + */ + static Builder builder(Severity severity, String message) { + return new DefaultDiscoveryIssue.Builder(severity, message); + } + + /** + * {@return the severity of this issue} + */ + Severity severity(); + + /** + * {@return the message of this issue} + */ + String message(); + + /** + * {@return the source of this issue} + */ + Optional source(); + + /** + * {@return the cause of this issue} + */ + Optional cause(); + + /** + * The severity of a {@code DiscoveryIssue}. + */ + enum Severity { + + /** + * Indicates that the engine encountered something that could be + * 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, + + /** + * Indicates that the engine encountered something that is problematic + * and might lead to unexpected behavior. + */ + WARNING, + + /** + * Indicates that the engine encountered something that is definitely + * problematic and will lead to unexpected behavior. + */ + ERROR + } + + /** + * Builder for creating a {@code DiscoveryIssue}. + */ + interface Builder { + + /** + * Set the {@link TestSource} for the {@code DiscoveryIssue}. + */ + default Builder source(Optional source) { + source.ifPresent(this::source); + return this; + } + + /** + * Set the {@link TestSource} for the {@code DiscoveryIssue}. + */ + Builder source(TestSource source); + + /** + * Set the {@link Throwable} that caused the {@code DiscoveryIssue}. + */ + default Builder cause(Optional cause) { + cause.ifPresent(this::cause); + return this; + } + + /** + * Set the {@link Throwable} that caused the {@code DiscoveryIssue}. + */ + Builder cause(Throwable cause); + + /** + * Build the {@code DiscoveryIssue}. + */ + DiscoveryIssue build(); + + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryListener.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryListener.java index e6af6982e72f..5fad4a0b5dd3 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryListener.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/EngineDiscoveryListener.java @@ -10,6 +10,7 @@ package org.junit.platform.engine; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import org.apiguardian.api.API; @@ -53,4 +54,17 @@ public interface EngineDiscoveryListener { default void selectorProcessed(UniqueId engineId, DiscoverySelector selector, SelectorResolutionResult result) { } + /** + * Called when the engine with the supplied {@code engineId} encountered an + * issue during test discovery. + * + * @param engineId the unique ID of the engine descriptor + * @param issue the encountered issue + * @since 1.13 + * @see DiscoveryIssue + */ + @API(status = EXPERIMENTAL, since = "1.13") + default void issueEncountered(UniqueId engineId, DiscoveryIssue issue) { + } + } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java index 6dfd169ab9f4..b23d2d79ed09 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java @@ -177,18 +177,18 @@ default Set getDescendants() { void removeFromHierarchy(); /** - * Order the children from this descriptor. + * Order the children of this descriptor. * *

The {@code orderer} is provided a modifiable list of child test - * descriptors in this test descriptor; never {@code null}. The + * descriptors of this test descriptor; never {@code null}. The * {@code orderer} must return a list containing the same descriptors in any * order; potentially the same list, but never {@code null}. If descriptors - * were added or removed, an exception is thrown. + * are added or removed, an exception is thrown. * - * @param orderer a unary operator to order the children of this test - * descriptor. + * @param orderer a unary operator to order the children of this test descriptor + * @since 1.12 */ - @API(since = "5.12", status = EXPERIMENTAL) + @API(since = "1.12", status = EXPERIMENTAL) default void orderChildren(UnaryOperator> orderer) { Preconditions.notNull(orderer, "orderer must not be null"); Set originalChildren = getChildren(); @@ -319,6 +319,25 @@ default void accept(Visitor visitor) { @FunctionalInterface interface Visitor { + /** + * Combine the supplied {@code visitors} into a single {@code Visitor}. + * + *

If the supplied array contains only a single {@code Visitor}, that + * {@code Visitor} is returned as is. + * + * @param visitors the {@code Visitor}s to combine; never {@code null} + * or empty + * @return the combined {@code Visitor} + * @throws org.junit.platform.commons.PreconditionViolationException if + * {@code visitors} is {@code null}, contains {@code null} elements, or + * is empty + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + static Visitor composite(Visitor... visitors) { + return CompositeTestDescriptorVisitor.from(visitors); + } + /** * Visit a {@link TestDescriptor}. * diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java index bdea0b7a9568..0af74b590121 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java @@ -39,7 +39,7 @@ public final class ReportEntry { /** * @deprecated Use {@link #from(String, String)} or {@link #from(Map)} */ - @API(status = DEPRECATED, since = "5.8") + @API(status = DEPRECATED, since = "1.8") @Deprecated public ReportEntry() { } 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 new file mode 100644 index 000000000000..1f17d4128b5c --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/DiscoveryIssueReporter.java @@ -0,0 +1,136 @@ +/* + * 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.discovery; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.EngineDiscoveryListener; +import org.junit.platform.engine.UniqueId; + +/** + * {@code DiscoveryIssueReporter} defines the API for reporting + * {@link DiscoveryIssue DiscoveryIssues}. + * + * @since 1.13 + * @see SelectorResolver.Context + */ +@API(status = EXPERIMENTAL, since = "1.13") +@FunctionalInterface +public interface DiscoveryIssueReporter { + + /** + * Create a new {@code DiscoveryIssueReporter} that reports issues to the + * supplied {@link EngineDiscoveryListener} for the specified engine. + * + * @param engineDiscoveryListener the listener to report issues to; never + * {@code null} + * @param engineId the unique identifier of the engine; never {@code null} + */ + static DiscoveryIssueReporter create(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); + } + + /** + * Build the supplied {@link DiscoveryIssue.Builder Builder} and report the + * resulting {@link DiscoveryIssue}. + */ + default void reportIssue(DiscoveryIssue.Builder builder) { + reportIssue(builder.build()); + } + + /** + * Report the supplied {@link DiscoveryIssue}. + */ + void reportIssue(DiscoveryIssue issue); + + /** + * Create a {@link Condition} that reports a {@link DiscoveryIssue} when the + * supplied {@link Predicate} is not met. + * + * @param predicate the predicate to test; never {@code null} + * @param issueCreator the function to create the issue with; never {@code null} + * @return a new {@code Condition}; never {@code null} + */ + default Condition createReportingCondition(Predicate predicate, + Function issueCreator) { + Preconditions.notNull(predicate, "predicate must not be null"); + Preconditions.notNull(issueCreator, "issueCreator must not be null"); + return value -> { + if (predicate.test(value)) { + return true; + } + else { + reportIssue(issueCreator.apply(value)); + return false; + } + }; + } + + /** + * A {@code Condition} is a union of {@link Predicate} and {@link Consumer}. + * + *

Instances of this type may be used as {@link Predicate Predicates} or + * {@link Consumer Consumers}. For example, a {@code Condition} may be + * passed to {@link java.util.stream.Stream#filter(Predicate)} if it is used + * for filtering, or to {@link java.util.stream.Stream#peek(Consumer)} if it + * is only used for reporting or other side effects. + * + * @see #createReportingCondition(Predicate, Function) + */ + @FunctionalInterface + interface Condition extends Predicate, Consumer { + + /** + * Return a composed condition that represents a logical AND of the + * supplied conditions without short-circuiting. + * + *

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. + * + * @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; + }; + } + + /** + * Evaluate the {@code #test(Object)} method of this condition to + * potentially report an issue. + */ + @Override + default void accept(T value) { + test(value); + } + } +} 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 36a939414a7d..46670c941fc3 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,6 +61,7 @@ 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<>(); @@ -68,11 +69,13 @@ class EngineDiscoveryRequestResolution { private final Map contextBySelector = new HashMap<>(); EngineDiscoveryRequestResolution(EngineDiscoveryRequest request, TestDescriptor engineDescriptor, - List resolvers, List visitors) { + List resolvers, List visitors, + DiscoveryIssueReporter issueReporter) { 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 5ec9b333b206..28e24d09a705 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 @@ -110,10 +110,13 @@ 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"); - InitializationContext initializationContext = new DefaultInitializationContext<>(request, engineDescriptor); + DiscoveryIssueReporter issueReporter = DiscoveryIssueReporter.create(request.getDiscoveryListener(), + engineDescriptor.getUniqueId()); + InitializationContext initializationContext = new DefaultInitializationContext<>(request, engineDescriptor, + issueReporter); List resolvers = instantiate(resolverCreators, initializationContext); List visitors = instantiate(visitorCreators, initializationContext); - new EngineDiscoveryRequestResolution(request, engineDescriptor, resolvers, visitors).run(); + new EngineDiscoveryRequestResolution(request, engineDescriptor, resolvers, visitors, issueReporter).run(); } private List instantiate(List, R>> creators, @@ -210,6 +213,12 @@ public Builder addSelectorResolver(Function, Selecto * Add a context sensitive {@link TestDescriptor.Visitor} to this * builder. * + *

If multiple {@linkplain TestDescriptor.Visitor visitors} are registered, + * they will iterate over the test tree separately. To avoid the overhead of + * multiple iterations, consider combining multiple visitors into a single + * visitor using + * {@link TestDescriptor.Visitor#composite(TestDescriptor.Visitor...)}. + * * @param visitorCreator the function that will be called to create the * {@link TestDescriptor.Visitor} to be added. * @return this builder for method chaining @@ -281,6 +290,14 @@ public interface InitializationContext { @API(status = EXPERIMENTAL, since = "1.12") Predicate getPackageFilter(); + /** + * {@return the {@link DiscoveryIssueReporter} for the current + * resolution} + * + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + DiscoveryIssueReporter getIssueReporter(); } private static class DefaultInitializationContext implements InitializationContext { @@ -289,12 +306,15 @@ private static class DefaultInitializationContext impl private final T engineDescriptor; private final Predicate classNameFilter; private final Predicate packageFilter; + private final DiscoveryIssueReporter issueReporter; - DefaultInitializationContext(EngineDiscoveryRequest request, T engineDescriptor) { + DefaultInitializationContext(EngineDiscoveryRequest request, T engineDescriptor, + DiscoveryIssueReporter issueReporter) { this.request = request; this.engineDescriptor = engineDescriptor; this.classNameFilter = buildClassNamePredicate(request); this.packageFilter = buildPackagePredicate(request); + this.issueReporter = issueReporter; } /** @@ -335,6 +355,11 @@ public Predicate getClassNameFilter() { public Predicate getPackageFilter() { return packageFilter; } + + @Override + public DiscoveryIssueReporter getIssueReporter() { + return issueReporter; + } } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java index 39011dab7162..928b6d21e55a 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java @@ -76,7 +76,8 @@ public ResourceLock getResourceLock() { @Override public ExecutionMode getExecutionMode() { - return taskContext.getExecutionAdvisor().getForcedExecutionMode(testDescriptor).orElse(node.getExecutionMode()); + return taskContext.getExecutionAdvisor().getForcedExecutionMode(testDescriptor) // + .orElseGet(node::getExecutionMode); } @Override diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/AbstractLauncherDiscoveryListenerTests.java b/junit-platform-engine/src/testFixtures/java/org/junit/platform/fakes/FaultyTestEngines.java similarity index 60% rename from platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/AbstractLauncherDiscoveryListenerTests.java rename to junit-platform-engine/src/testFixtures/java/org/junit/platform/fakes/FaultyTestEngines.java index f2ffad3215d4..9199a1388af3 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/AbstractLauncherDiscoveryListenerTests.java +++ b/junit-platform-engine/src/testFixtures/java/org/junit/platform/fakes/FaultyTestEngines.java @@ -8,19 +8,20 @@ * https://www.eclipse.org/legal/epl-v20.html */ -package org.junit.platform.launcher.listeners.discovery; +package org.junit.platform.fakes; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.SelectorResolutionResult; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.descriptor.EngineDescriptor; -import org.junit.platform.fakes.TestEngineStub; -abstract class AbstractLauncherDiscoveryListenerTests { +public class FaultyTestEngines { - protected TestEngineStub createEngineThatCannotResolveAnything(String engineId) { + public static TestEngineStub createEngineThatCannotResolveAnything(String engineId) { return new TestEngineStub(engineId) { @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { @@ -29,10 +30,18 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId selector, SelectorResolutionResult.unresolved())); return new EngineDescriptor(uniqueId, "Some Engine"); } + + @Override + public void execute(ExecutionRequest request) { + var listener = request.getEngineExecutionListener(); + var rootTestDescriptor = request.getRootTestDescriptor(); + listener.executionStarted(rootTestDescriptor); + listener.executionFinished(rootTestDescriptor, TestExecutionResult.successful()); + } }; } - protected TestEngineStub createEngineThatFailsToResolveAnything(String engineId, RuntimeException rootCause) { + public static TestEngineStub createEngineThatFailsToResolveAnything(String engineId, Throwable rootCause) { return new TestEngineStub(engineId) { @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { @@ -41,7 +50,14 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId selector, SelectorResolutionResult.failed(rootCause))); return new EngineDescriptor(uniqueId, "Some Engine"); } + + @Override + public void execute(ExecutionRequest request) { + var listener = request.getEngineExecutionListener(); + var rootTestDescriptor = request.getRootTestDescriptor(); + listener.executionStarted(rootTestDescriptor); + listener.executionFinished(rootTestDescriptor, TestExecutionResult.successful()); + } }; } - } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java index f397eb0b9bc0..80e7cd3b76ec 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java @@ -122,9 +122,9 @@ public String getUniqueId() { * behind the scenes. * * @return the unique ID for this identifier; never {@code null} - * @since 5.8 + * @since 1.8 */ - @API(status = STABLE, since = "5.8") + @API(status = STABLE, since = "1.8") public UniqueId getUniqueIdObject() { return this.uniqueId; } @@ -150,9 +150,9 @@ public Optional getParentId() { * * @return a container for the unique ID for this identifier's parent; * never {@code null} though potentially empty - * @since 5.8 + * @since 1.8 */ - @API(status = STABLE, since = "5.8") + @API(status = STABLE, since = "1.8") public Optional getParentIdObject() { return Optional.ofNullable(this.parentId); } @@ -291,7 +291,7 @@ private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOEx /** * Represents the serialized output of {@code TestIdentifier}. The fields on this - * class match the fields that {@code TestIdentifier} had prior to 5.8. + * class match the fields that {@code TestIdentifier} had prior to 1.8. */ private static class SerializedForm implements Serializable { diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestPlan.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestPlan.java index ca8361bb3757..522c715a1efe 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestPlan.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestPlan.java @@ -63,13 +63,10 @@ public class TestPlan { private final Set roots = synchronizedSet(new LinkedHashSet<>(4)); - private final Map> children = new ConcurrentHashMap<>(32); - private final Map allIdentifiers = new ConcurrentHashMap<>(32); private final boolean containsTests; - private final ConfigurationParameters configurationParameters; private final OutputDirectoryProvider outputDirectoryProvider; @@ -80,6 +77,7 @@ public class TestPlan { *

Each supplied {@code TestDescriptor} is expected to be a descriptor * for a {@link org.junit.platform.engine.TestEngine TestEngine}. * + * @param containsTests whether the test plan contains tests * @param engineDescriptors the engine test descriptors from which the test * plan should be created; never {@code null} * @param configurationParameters the {@code ConfigurationParameters} for @@ -88,13 +86,12 @@ public class TestPlan { * this test plan; never {@code null} * @return a new test plan */ - @API(status = INTERNAL, since = "1.12") - public static TestPlan from(Collection engineDescriptors, + @API(status = INTERNAL, since = "1.13") + public static TestPlan from(boolean containsTests, Collection engineDescriptors, ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider) { Preconditions.notNull(engineDescriptors, "Cannot create TestPlan from a null collection of TestDescriptors"); Preconditions.notNull(configurationParameters, "Cannot create TestPlan from null ConfigurationParameters"); - TestPlan testPlan = new TestPlan(engineDescriptors.stream().anyMatch(TestDescriptor::containsTests), - configurationParameters, outputDirectoryProvider); + TestPlan testPlan = new TestPlan(containsTests, configurationParameters, outputDirectoryProvider); TestDescriptor.Visitor visitor = descriptor -> testPlan.addInternal(TestIdentifier.from(descriptor)); engineDescriptors.forEach(engineDescriptor -> engineDescriptor.accept(visitor)); return testPlan; diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncherDiscoveryRequest.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncherDiscoveryRequest.java new file mode 100644 index 000000000000..4a8c8d5cad97 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncherDiscoveryRequest.java @@ -0,0 +1,62 @@ +/* + * 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 java.util.List; + +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.DiscoveryFilter; +import org.junit.platform.engine.DiscoverySelector; +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.PostDiscoveryFilter; + +/** + * @since 5.13 + */ +class DelegatingLauncherDiscoveryRequest implements LauncherDiscoveryRequest { + + private final LauncherDiscoveryRequest request; + + DelegatingLauncherDiscoveryRequest(LauncherDiscoveryRequest request) { + this.request = request; + } + + public List getEngineFilters() { + return this.request.getEngineFilters(); + } + + public List getPostDiscoveryFilters() { + return this.request.getPostDiscoveryFilters(); + } + + public LauncherDiscoveryListener getDiscoveryListener() { + return this.request.getDiscoveryListener(); + } + + public List getSelectorsByType(Class selectorType) { + return this.request.getSelectorsByType(selectorType); + } + + public > List getFiltersByType(Class filterType) { + return this.request.getFiltersByType(filterType); + } + + public ConfigurationParameters getConfigurationParameters() { + return this.request.getConfigurationParameters(); + } + + public OutputDirectoryProvider getOutputDirectoryProvider() { + return this.request.getOutputDirectoryProvider(); + } +} 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 new file mode 100644 index 000000000000..4c7fa3538aa3 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueCollector.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.platform.engine.SelectorResolutionResult.Status.FAILED; +import static org.junit.platform.engine.SelectorResolutionResult.Status.UNRESOLVED; + +import java.util.ArrayList; +import java.util.List; + +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.UniqueId; +import org.junit.platform.engine.discovery.UniqueIdSelector; +import org.junit.platform.launcher.LauncherDiscoveryListener; + +class DiscoveryIssueCollector implements LauncherDiscoveryListener { + + final List issues = new ArrayList<>(); + + @Override + public void engineDiscoveryStarted(UniqueId engineId) { + this.issues.clear(); + } + + @Override + public void selectorProcessed(UniqueId engineId, DiscoverySelector selector, SelectorResolutionResult result) { + if (result.getStatus() == FAILED) { + this.issues.add(DiscoveryIssue.builder(Severity.ERROR, selector + " resolution failed") // + .cause(result.getThrowable()) // + .build()); + } + else if (result.getStatus() == UNRESOLVED && selector instanceof UniqueIdSelector) { + UniqueId uniqueId = ((UniqueIdSelector) selector).getUniqueId(); + if (uniqueId.hasPrefix(engineId)) { + this.issues.add(DiscoveryIssue.create(Severity.ERROR, selector + " could not be resolved")); + } + } + } + + @Override + public void issueEncountered(UniqueId engineId, DiscoveryIssue issue) { + this.issues.add(issue); + } + + DiscoveryIssueNotifier toNotifier() { + if (issues.isEmpty()) { + return DiscoveryIssueNotifier.NO_ISSUES; + } + Severity criticalSeverity = Severity.ERROR; // TODO #242 - make this configurable + return DiscoveryIssueNotifier.from(criticalSeverity, issues); + } +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueException.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueException.java new file mode 100644 index 000000000000..563e3a33629b --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueException.java @@ -0,0 +1,32 @@ +/* + * 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.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; + +/** + * {@code DiscoveryIssueException} is an exception that is thrown if an engine + * reports critical issues during test discovery. + * + * @since 1.13 + */ +@API(status = EXPERIMENTAL, since = "1.13") +public class DiscoveryIssueException extends JUnitException { + + private static final long serialVersionUID = 1L; + + DiscoveryIssueException(String message) { + super(message, null, false, false); + } +} 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 new file mode 100644 index 000000000000..8bb14f68dcdd --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DiscoveryIssueNotifier.java @@ -0,0 +1,153 @@ +/* + * 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 java.util.Collections.emptyList; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.partitioningBy; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; + +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.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.MethodSource; + +/** + * @since 1.13 + */ +class DiscoveryIssueNotifier { + + static final DiscoveryIssueNotifier NO_ISSUES = new DiscoveryIssueNotifier(emptyList(), emptyList(), emptyList()); + private static final Logger logger = LoggerFactory.getLogger(DiscoveryIssueNotifier.class); + + private final List allIssues; + private final List criticalIssues; + private final List nonCriticalIssues; + + static DiscoveryIssueNotifier from(Severity criticalSeverity, List issues) { + Map> issuesByCriticality = issues.stream() // + .sorted(comparing(DiscoveryIssue::severity).reversed()) // + .collect(partitioningBy(issue -> issue.severity().compareTo(criticalSeverity) >= 0)); + List criticalIssues = issuesByCriticality.get(true); + List nonCriticalIssues = issuesByCriticality.get(false); + return new DiscoveryIssueNotifier(new ArrayList<>(issues), criticalIssues, nonCriticalIssues); + } + + private DiscoveryIssueNotifier(List allIssues, List criticalIssues, + List nonCriticalIssues) { + this.allIssues = allIssues; + this.criticalIssues = criticalIssues; + this.nonCriticalIssues = nonCriticalIssues; + } + + List getAllIssues() { + return allIssues; + } + + boolean hasCriticalIssues() { + return !criticalIssues.isEmpty(); + } + + void logCriticalIssues(TestEngine testEngine) { + logIssues(testEngine, criticalIssues, "critical"); + } + + void logNonCriticalIssues(TestEngine testEngine) { + logIssues(testEngine, nonCriticalIssues, "non-critical"); + } + + DiscoveryIssueException createExceptionForCriticalIssues(TestEngine testEngine) { + if (criticalIssues.isEmpty()) { + return null; + } + String message = formatMessage(testEngine, criticalIssues, "critical"); + return new DiscoveryIssueException(message); + } + + private void logIssues(TestEngine testEngine, List issues, String adjective) { + if (!issues.isEmpty()) { + Severity maxSeverity = issues.get(0).severity(); + logger(maxSeverity).accept(() -> formatMessage(testEngine, issues, adjective)); + } + } + + private static Consumer> logger(Severity severity) { + switch (severity) { + case NOTICE: + return logger::info; + case DEPRECATION: + case WARNING: + return logger::warn; + case ERROR: + return logger::error; + default: + throw new IllegalArgumentException("Unknown severity: " + severity); + } + } + + private static String formatMessage(TestEngine testEngine, List issues, String adjective) { + Preconditions.notNull(testEngine, "testEngine must not be null"); + Preconditions.notNull(issues, "issues must not be null"); + Preconditions.notEmpty(issues, "issues must not be empty"); + String engineId = testEngine.getId(); + StringBuilder message = new StringBuilder(); + message.append("TestEngine with ID '").append(engineId).append("' encountered "); + if (issues.size() == 1) { + message.append("a ").append(adjective).append(" issue"); + } + else { + message.append(issues.size()).append(' ').append(adjective).append(" issues"); + } + message.append(" during test discovery:"); + for (int i = 0; i < issues.size(); i++) { + DiscoveryIssue issue = issues.get(i); + message.append("\n\n(").append(i + 1).append(") [").append(issue.severity()).append("] ").append( + issue.message()); + issue.source().ifPresent(source -> { + message.append("\n Source: ").append(source); + if (source instanceof MethodSource) { + MethodSource methodSource = (MethodSource) source; + appendIdeCompatibleLink(message, methodSource.getClassName(), methodSource.getMethodName()); + } + else if (source instanceof ClassSource) { + ClassSource classSource = (ClassSource) source; + appendIdeCompatibleLink(message, classSource.getClassName(), ""); + } + }); + issue.cause().ifPresent(t -> message.append("\n Cause: ").append(getStackTrace(t))); + } + return message.toString(); + } + + 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/EngineDiscoveryErrorDescriptor.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryErrorDescriptor.java deleted file mode 100644 index 018de8fb1b39..000000000000 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryErrorDescriptor.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.platform.launcher.core; - -import org.junit.platform.engine.TestEngine; -import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; -import org.junit.platform.engine.support.descriptor.ClassSource; - -/** - * Represents an error thrown by a {@link org.junit.platform.engine.TestEngine} - * during discovery. - * - *

The contained {@link Throwable} will be reported as the cause of a test - * failure by the {@link DefaultLauncher} when execution is started for this - * engine. - * - * @since 1.6 - */ -class EngineDiscoveryErrorDescriptor extends AbstractTestDescriptor { - - private final Throwable cause; - - EngineDiscoveryErrorDescriptor(UniqueId uniqueId, TestEngine testEngine, Throwable cause) { - super(uniqueId, testEngine.getId(), ClassSource.from(testEngine.getClass())); - this.cause = cause; - } - - Throwable getCause() { - return cause; - } - - @Override - public Type getType() { - return Type.TEST; - } - - @Override - public void prune() { - // prevent pruning - } - -} 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 09c895a69684..87057a6c1f4d 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 @@ -34,10 +34,12 @@ 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.launcher.EngineDiscoveryResult; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.PostDiscoveryFilter; +import org.junit.platform.launcher.core.LauncherDiscoveryResult.EngineResultInfo; /** * Orchestrates test discovery using the configured test engines. @@ -86,8 +88,8 @@ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase * filters} and {@linkplain PostDiscoveryFilter post-discovery filters} and * {@linkplain TestDescriptor#prune() prunes} the resulting test tree. * - * Note: The test descriptors in the discovery result can safely be used as - * non-root descriptors. Engine-test descriptor entries are pruned from + *

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. @@ -99,11 +101,19 @@ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase private LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase phase, Function uniqueIdCreator) { - LauncherDiscoveryListener listener = getLauncherDiscoveryListener(request); + DiscoveryIssueCollector issueCollector = new DiscoveryIssueCollector(); + LauncherDiscoveryListener listener = getLauncherDiscoveryListener(request, issueCollector); + LauncherDiscoveryRequest delegatingRequest = new DelegatingLauncherDiscoveryRequest(request) { + @Override + public LauncherDiscoveryListener getDiscoveryListener() { + return listener; + } + }; listener.launcherDiscoveryStarted(request); try { - Map testEngines = discoverSafely(request, phase, listener, uniqueIdCreator); - return new LauncherDiscoveryResult(testEngines, request.getConfigurationParameters(), + Map testEngineResults = discoverSafely(delegatingRequest, phase, + issueCollector, uniqueIdCreator); + return new LauncherDiscoveryResult(testEngineResults, request.getConfigurationParameters(), request.getOutputDirectoryProvider()); } finally { @@ -111,9 +121,9 @@ private LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase } } - private Map discoverSafely(LauncherDiscoveryRequest request, Phase phase, - LauncherDiscoveryListener listener, Function uniqueIdCreator) { - Map testEngineDescriptors = new LinkedHashMap<>(); + private Map discoverSafely(LauncherDiscoveryRequest request, Phase phase, + DiscoveryIssueCollector issueCollector, Function uniqueIdCreator) { + Map testEngineDescriptors = new LinkedHashMap<>(); EngineFilterer engineFilterer = new EngineFilterer(request.getEngineFilters()); for (TestEngine testEngine : this.testEngines) { @@ -129,8 +139,8 @@ private Map discoverSafely(LauncherDiscoveryRequest logger.debug(() -> String.format("Discovering tests during Launcher %s phase in engine '%s'.", phase, testEngine.getId())); - TestDescriptor rootDescriptor = discoverEngineRoot(testEngine, request, listener, uniqueIdCreator); - testEngineDescriptors.put(testEngine, rootDescriptor); + EngineResultInfo engineResult = discoverEngineRoot(testEngine, request, issueCollector, uniqueIdCreator); + testEngineDescriptors.put(testEngine, engineResult); } engineFilterer.performSanityChecks(); @@ -144,15 +154,16 @@ private Map discoverSafely(LauncherDiscoveryRequest return testEngineDescriptors; } - private TestDescriptor discoverEngineRoot(TestEngine testEngine, LauncherDiscoveryRequest request, - LauncherDiscoveryListener listener, Function uniqueIdCreator) { + private EngineResultInfo discoverEngineRoot(TestEngine testEngine, LauncherDiscoveryRequest request, + DiscoveryIssueCollector issueCollector, Function uniqueIdCreator) { UniqueId uniqueEngineId = uniqueIdCreator.apply(testEngine.getId()); + LauncherDiscoveryListener listener = request.getDiscoveryListener(); try { listener.engineDiscoveryStarted(uniqueEngineId); TestDescriptor engineRoot = testEngine.discover(request, uniqueEngineId); discoveryResultValidator.validate(testEngine, engineRoot); listener.engineDiscoveryFinished(uniqueEngineId, EngineDiscoveryResult.successful()); - return engineRoot; + return EngineResultInfo.completed(engineRoot, issueCollector.toNotifier()); } catch (Throwable throwable) { UnrecoverableExceptions.rethrowIfUnrecoverable(throwable); @@ -165,17 +176,20 @@ private TestDescriptor discoverEngineRoot(TestEngine testEngine, LauncherDiscove cause = new JUnitException(message, throwable); } listener.engineDiscoveryFinished(uniqueEngineId, EngineDiscoveryResult.failed(cause)); - return new EngineDiscoveryErrorDescriptor(uniqueEngineId, testEngine, cause); + return EngineResultInfo.errored(new EngineDescriptor(uniqueEngineId, testEngine.getId()), + issueCollector.toNotifier(), cause); } } - LauncherDiscoveryListener getLauncherDiscoveryListener(LauncherDiscoveryRequest discoveryRequest) { + LauncherDiscoveryListener getLauncherDiscoveryListener(LauncherDiscoveryRequest discoveryRequest, + DiscoveryIssueCollector issueCollector) { return ListenerRegistry.copyOf(launcherDiscoveryListenerRegistry) // .add(discoveryRequest.getDiscoveryListener()) // + .add(issueCollector) // .getCompositeListener(); } - private void applyPostDiscoveryFilters(Map testEngineDescriptors, + private void applyPostDiscoveryFilters(Map testEngineDescriptors, List filters) { Filter postDiscoveryFilter = composeFilters(filters); Map> excludedTestDescriptorsByReason = new LinkedHashMap<>(); @@ -215,17 +229,17 @@ private void logTestDescriptorExclusionReasons(Map> *

If a {@link TestEngine} ends up with no {@code TestDescriptors} after * pruning, it will not be removed. */ - private void prune(Map testEngineDescriptors) { - acceptInAllTestEngines(testEngineDescriptors, TestDescriptor::prune); + private void prune(Map testEngineResults) { + acceptInAllTestEngines(testEngineResults, TestDescriptor::prune); } private boolean isExcluded(TestDescriptor descriptor, FilterResult filterResult) { return descriptor.getChildren().isEmpty() && filterResult.excluded(); } - private void acceptInAllTestEngines(Map testEngineDescriptors, + private void acceptInAllTestEngines(Map testEngineResults, TestDescriptor.Visitor visitor) { - testEngineDescriptors.values().forEach(descriptor -> descriptor.accept(visitor)); + testEngineResults.values().forEach(result -> result.getRootDescriptor().accept(visitor)); } public enum Phase { 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 aedf22950cc1..9c5e1b3eaaf1 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 @@ -32,6 +32,7 @@ import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherDiscoveryResult.EngineResultInfo; /** * Orchestrates test execution using the configured test engines. @@ -160,16 +161,7 @@ public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionList EngineExecutionListener listener = selectExecutionListener(engineExecutionListener, configurationParameters); for (TestEngine testEngine : discoveryResult.getTestEngines()) { - TestDescriptor engineDescriptor = discoveryResult.getEngineTestDescriptor(testEngine); - if (engineDescriptor instanceof EngineDiscoveryErrorDescriptor) { - listener.executionStarted(engineDescriptor); - listener.executionFinished(engineDescriptor, - TestExecutionResult.failed(((EngineDiscoveryErrorDescriptor) engineDescriptor).getCause())); - } - else { - execute(engineDescriptor, listener, configurationParameters, testEngine, - discoveryResult.getOutputDirectoryProvider()); - } + failOrExecuteEngine(discoveryResult, listener, testEngine); } } @@ -183,6 +175,28 @@ private static EngineExecutionListener selectExecutionListener(EngineExecutionLi return engineExecutionListener; } + private void failOrExecuteEngine(LauncherDiscoveryResult discoveryResult, EngineExecutionListener listener, + TestEngine testEngine) { + + EngineResultInfo engineDiscoveryResult = discoveryResult.getEngineResult(testEngine); + DiscoveryIssueNotifier discoveryIssueNotifier = engineDiscoveryResult.getDiscoveryIssueNotifier(); + TestDescriptor engineDescriptor = engineDiscoveryResult.getRootDescriptor(); + Throwable failure = engineDiscoveryResult.getCause() // + .orElseGet(() -> discoveryIssueNotifier.createExceptionForCriticalIssues(testEngine)); + if (failure != null) { + listener.executionStarted(engineDescriptor); + if (engineDiscoveryResult.getCause().isPresent()) { + discoveryIssueNotifier.logCriticalIssues(testEngine); + } + discoveryIssueNotifier.logNonCriticalIssues(testEngine); + listener.executionFinished(engineDescriptor, TestExecutionResult.failed(failure)); + } + else { + executeEngine(engineDescriptor, listener, discoveryResult.getConfigurationParameters(), testEngine, + discoveryResult.getOutputDirectoryProvider(), discoveryIssueNotifier); + } + } + private ListenerRegistry buildListenerRegistryForExecution( TestExecutionListener... listeners) { if (listeners.length == 0) { @@ -191,15 +205,16 @@ private ListenerRegistry buildListenerRegistryForExecutio return ListenerRegistry.copyOf(this.listenerRegistry).addAll(listeners); } - private void execute(TestDescriptor engineDescriptor, EngineExecutionListener listener, + private void executeEngine(TestDescriptor engineDescriptor, EngineExecutionListener listener, ConfigurationParameters configurationParameters, TestEngine testEngine, - OutputDirectoryProvider outputDirectoryProvider) { + OutputDirectoryProvider outputDirectoryProvider, DiscoveryIssueNotifier discoveryIssueNotifier) { OutcomeDelayingEngineExecutionListener delayingListener = new OutcomeDelayingEngineExecutionListener(listener, engineDescriptor); try { testEngine.execute(ExecutionRequest.create(engineDescriptor, delayingListener, configurationParameters, outputDirectoryProvider)); + discoveryIssueNotifier.logNonCriticalIssues(testEngine); delayingListener.reportEngineOutcome(); } catch (Throwable throwable) { @@ -212,6 +227,8 @@ private void execute(TestDescriptor engineDescriptor, EngineExecutionListener li String message = String.format("TestEngine with ID '%s' failed to execute tests", testEngine.getId()); cause = new JUnitException(message, throwable); } + delayingListener.reportEngineStartIfNecessary(); + discoveryIssueNotifier.logNonCriticalIssues(testEngine); delayingListener.reportEngineFailure(cause); } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java index 315652f960a1..4440a6209801 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/InternalTestPlan.java @@ -30,8 +30,9 @@ class InternalTestPlan extends TestPlan { private final TestPlan delegate; static InternalTestPlan from(LauncherDiscoveryResult discoveryResult) { - TestPlan delegate = TestPlan.from(discoveryResult.getEngineTestDescriptors(), - discoveryResult.getConfigurationParameters(), discoveryResult.getOutputDirectoryProvider()); + TestPlan delegate = TestPlan.from(discoveryResult.containsCriticalIssuesOrContainsTests(), + discoveryResult.getEngineTestDescriptors(), discoveryResult.getConfigurationParameters(), + discoveryResult.getOutputDirectoryProvider()); return new InternalTestPlan(discoveryResult, delegate); } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java index 64adb2f55670..7b3db9368915 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherDiscoveryResult.java @@ -16,11 +16,15 @@ import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.apiguardian.api.API; import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.reporting.OutputDirectoryProvider; @@ -34,19 +38,28 @@ @API(status = INTERNAL, since = "1.7", consumers = { "org.junit.platform.testkit", "org.junit.platform.suite.engine" }) public class LauncherDiscoveryResult { - private final Map testEngineDescriptors; + private final Map testEngineResults; private final ConfigurationParameters configurationParameters; private final OutputDirectoryProvider outputDirectoryProvider; - LauncherDiscoveryResult(Map testEngineDescriptors, + LauncherDiscoveryResult(Map testEngineResults, ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider) { - this.testEngineDescriptors = unmodifiableMap(new LinkedHashMap<>(testEngineDescriptors)); + this.testEngineResults = unmodifiableMap(new LinkedHashMap<>(testEngineResults)); this.configurationParameters = configurationParameters; this.outputDirectoryProvider = outputDirectoryProvider; } public TestDescriptor getEngineTestDescriptor(TestEngine testEngine) { - return this.testEngineDescriptors.get(testEngine); + return getEngineResult(testEngine).getRootDescriptor(); + } + + @API(status = INTERNAL, since = "1.13") + public List getDiscoveryIssues(TestEngine testEngine) { + return getEngineResult(testEngine).getDiscoveryIssueNotifier().getAllIssues(); + } + + EngineResultInfo getEngineResult(TestEngine testEngine) { + return this.testEngineResults.get(testEngine); } ConfigurationParameters getConfigurationParameters() { @@ -58,29 +71,78 @@ OutputDirectoryProvider getOutputDirectoryProvider() { } public Collection getTestEngines() { - return this.testEngineDescriptors.keySet(); + return this.testEngineResults.keySet(); + } + + boolean containsCriticalIssuesOrContainsTests() { + return this.testEngineResults.values().stream() // + .anyMatch(EngineResultInfo::containsCriticalIssuesOrContainsTests); } Collection getEngineTestDescriptors() { - return this.testEngineDescriptors.values(); + return this.testEngineResults.values().stream() // + .map(EngineResultInfo::getRootDescriptor) // + .collect(Collectors.toList()); } public LauncherDiscoveryResult withRetainedEngines(Predicate predicate) { - Map prunedTestEngineDescriptors = retainEngines(predicate); - if (prunedTestEngineDescriptors.size() < this.testEngineDescriptors.size()) { - return new LauncherDiscoveryResult(prunedTestEngineDescriptors, this.configurationParameters, + Map prunedTestEngineResults = retainEngines(predicate); + if (prunedTestEngineResults.size() < this.testEngineResults.size()) { + return new LauncherDiscoveryResult(prunedTestEngineResults, this.configurationParameters, this.outputDirectoryProvider); } return this; } - private Map retainEngines(Predicate predicate) { + private Map retainEngines(Predicate predicate) { // @formatter:off - return this.testEngineDescriptors.entrySet() + return this.testEngineResults.entrySet() .stream() - .filter(entry -> predicate.test(entry.getValue())) + .filter(entry -> predicate.test(entry.getValue().getRootDescriptor())) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); // @formatter:on } + static class EngineResultInfo { + + static EngineResultInfo completed(TestDescriptor rootDescriptor, + DiscoveryIssueNotifier discoveryIssueNotifier) { + return new EngineResultInfo(rootDescriptor, discoveryIssueNotifier, null); + } + + static EngineResultInfo errored(TestDescriptor rootDescriptor, DiscoveryIssueNotifier discoveryIssueNotifier, + Throwable cause) { + return new EngineResultInfo(rootDescriptor, discoveryIssueNotifier, cause); + } + + private final TestDescriptor rootDescriptor; + private final Throwable cause; + private final DiscoveryIssueNotifier discoveryIssueNotifier; + + EngineResultInfo(TestDescriptor rootDescriptor, DiscoveryIssueNotifier discoveryIssueNotifier, + Throwable cause) { + this.rootDescriptor = rootDescriptor; + this.discoveryIssueNotifier = discoveryIssueNotifier; + this.cause = cause; + } + + TestDescriptor getRootDescriptor() { + return this.rootDescriptor; + } + + DiscoveryIssueNotifier getDiscoveryIssueNotifier() { + return discoveryIssueNotifier; + } + + Optional getCause() { + return Optional.ofNullable(this.cause); + } + + boolean containsCriticalIssuesOrContainsTests() { + return cause != null // + || discoveryIssueNotifier.hasCriticalIssues() // + || TestDescriptor.containsTests(rootDescriptor); + } + } + } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/OutcomeDelayingEngineExecutionListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/OutcomeDelayingEngineExecutionListener.java index 1afb4460d5b3..7bdfe458e03f 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/OutcomeDelayingEngineExecutionListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/OutcomeDelayingEngineExecutionListener.java @@ -73,10 +73,13 @@ else if (outcome == Outcome.SKIPPED) { } } - void reportEngineFailure(Throwable throwable) { + void reportEngineStartIfNecessary() { if (!engineStarted) { super.executionStarted(engineDescriptor); } + } + + void reportEngineFailure(Throwable throwable) { if (executionResult != null && executionResult.getThrowable().isPresent()) { throwable.addSuppressed(executionResult.getThrowable().get()); } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/AbortOnFailureLauncherDiscoveryListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/AbortOnFailureLauncherDiscoveryListener.java index 11a7429219c9..3892a8ae565c 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/AbortOnFailureLauncherDiscoveryListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/AbortOnFailureLauncherDiscoveryListener.java @@ -10,15 +10,8 @@ package org.junit.platform.launcher.listeners.discovery; -import static org.junit.platform.engine.SelectorResolutionResult.Status.FAILED; -import static org.junit.platform.engine.SelectorResolutionResult.Status.UNRESOLVED; - -import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.ExceptionUtils; -import org.junit.platform.engine.DiscoverySelector; -import org.junit.platform.engine.SelectorResolutionResult; import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.discovery.UniqueIdSelector; import org.junit.platform.launcher.EngineDiscoveryResult; import org.junit.platform.launcher.LauncherDiscoveryListener; @@ -33,19 +26,6 @@ public void engineDiscoveryFinished(UniqueId engineId, EngineDiscoveryResult res result.getThrowable().ifPresent(ExceptionUtils::throwAsUncheckedException); } - @Override - public void selectorProcessed(UniqueId engineId, DiscoverySelector selector, SelectorResolutionResult result) { - if (result.getStatus() == FAILED) { - throw new JUnitException(selector + " resolution failed", result.getThrowable().orElse(null)); - } - if (result.getStatus() == UNRESOLVED && selector instanceof UniqueIdSelector) { - UniqueId uniqueId = ((UniqueIdSelector) selector).getUniqueId(); - if (uniqueId.hasPrefix(engineId)) { - throw new JUnitException(selector + " could not be resolved"); - } - } - } - @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/CompositeLauncherDiscoveryListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/CompositeLauncherDiscoveryListener.java index 0f4153ce5b00..a9ba957786e4 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/CompositeLauncherDiscoveryListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/CompositeLauncherDiscoveryListener.java @@ -16,6 +16,7 @@ import java.util.Collections; import java.util.List; +import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.SelectorResolutionResult; import org.junit.platform.engine.UniqueId; @@ -59,4 +60,10 @@ public void engineDiscoveryFinished(UniqueId engineId, EngineDiscoveryResult res public void selectorProcessed(UniqueId engineId, DiscoverySelector selector, SelectorResolutionResult result) { listeners.forEach(delegate -> delegate.selectorProcessed(engineId, selector, result)); } + + @Override + public void issueEncountered(UniqueId engineId, DiscoveryIssue issue) { + listeners.forEach(delegate -> delegate.issueEncountered(engineId, issue)); + } + } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/LoggingLauncherDiscoveryListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/LoggingLauncherDiscoveryListener.java index e5c31298abdd..b57289f8dab0 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/LoggingLauncherDiscoveryListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/discovery/LoggingLauncherDiscoveryListener.java @@ -18,6 +18,7 @@ import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; +import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.SelectorResolutionResult; import org.junit.platform.engine.UniqueId; @@ -88,6 +89,11 @@ public void selectorProcessed(UniqueId engineId, DiscoverySelector selector, Sel } } + @Override + public void issueEncountered(UniqueId engineId, DiscoveryIssue issue) { + logger.trace(() -> "Issue encountered during discovery by TestEngine with ID '" + engineId + "': " + issue); + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/junit-platform-launcher/src/nativeImage/initialize-at-build-time b/junit-platform-launcher/src/nativeImage/initialize-at-build-time index 4b3770ab5fb1..66e181d6e90d 100644 --- a/junit-platform-launcher/src/nativeImage/initialize-at-build-time +++ b/junit-platform-launcher/src/nativeImage/initialize-at-build-time @@ -2,6 +2,7 @@ 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 @@ -13,6 +14,7 @@ org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvid 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 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 1aea480e9569..20289f336e5c 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 @@ -26,6 +26,7 @@ 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.discovery.DiscoveryIssueReporter; import org.junit.platform.engine.support.discovery.SelectorResolver; /** @@ -41,13 +42,16 @@ final class ClassSelectorResolver implements SelectorResolver { private final SuiteEngineDescriptor suiteEngineDescriptor; private final ConfigurationParameters configurationParameters; private final OutputDirectoryProvider outputDirectoryProvider; + private final DiscoveryIssueReporter issueReporter; ClassSelectorResolver(Predicate classNameFilter, SuiteEngineDescriptor suiteEngineDescriptor, - ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider) { + ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider, + DiscoveryIssueReporter issueReporter) { this.classNameFilter = classNameFilter; this.suiteEngineDescriptor = suiteEngineDescriptor; this.configurationParameters = configurationParameters; this.outputDirectoryProvider = outputDirectoryProvider; + this.issueReporter = issueReporter; } @Override @@ -106,7 +110,8 @@ private Optional newSuiteDescriptor(Class suiteClass, Te return Optional.empty(); } - return Optional.of(new SuiteTestDescriptor(id, suiteClass, configurationParameters, outputDirectoryProvider)); + return Optional.of( + new SuiteTestDescriptor(id, suiteClass, configurationParameters, outputDirectoryProvider, 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 3a9775ea5e9a..0c7aead14921 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 @@ -26,7 +26,8 @@ final class DiscoverySelectorResolver { context.getClassNameFilter(), context.getEngineDescriptor(), context.getDiscoveryRequest().getConfigurationParameters(), - context.getDiscoveryRequest().getOutputDirectoryProvider())) + context.getDiscoveryRequest().getOutputDirectoryProvider(), + context.getIssueReporter())) .build(); // @formatter:on 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 dcf971ce86ad..87ce1ae662a4 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 @@ -11,16 +11,20 @@ package org.junit.platform.suite.engine; import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods; -import static org.junit.platform.commons.util.ReflectionUtils.returnsPrimitiveVoid; +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; import java.util.List; -import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.support.HierarchyTraversalMode; import org.junit.platform.commons.support.ModifierSupport; -import org.junit.platform.engine.support.hierarchical.ThrowableCollector; +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.suite.api.AfterSuite; import org.junit.platform.suite.api.BeforeSuite; @@ -35,56 +39,68 @@ private LifecycleMethodUtils() { /* no-op */ } - static List findBeforeSuiteMethods(Class testClass, ThrowableCollector throwableCollector) { - return findMethodsAndAssertStaticAndNonPrivate(testClass, BeforeSuite.class, HierarchyTraversalMode.TOP_DOWN, - throwableCollector); + static List findBeforeSuiteMethods(Class testClass, DiscoveryIssueReporter issueReporter) { + return findMethodsAndCheckStaticAndNonPrivate(testClass, BeforeSuite.class, HierarchyTraversalMode.TOP_DOWN, + issueReporter); } - static List findAfterSuiteMethods(Class testClass, ThrowableCollector throwableCollector) { - return findMethodsAndAssertStaticAndNonPrivate(testClass, AfterSuite.class, HierarchyTraversalMode.BOTTOM_UP, - throwableCollector); + static List findAfterSuiteMethods(Class testClass, DiscoveryIssueReporter issueReporter) { + return findMethodsAndCheckStaticAndNonPrivate(testClass, AfterSuite.class, HierarchyTraversalMode.BOTTOM_UP, + issueReporter); } - private static List findMethodsAndAssertStaticAndNonPrivate(Class testClass, + private static List findMethodsAndCheckStaticAndNonPrivate(Class testClass, Class annotationType, HierarchyTraversalMode traversalMode, - ThrowableCollector throwableCollector) { - - List methods = findAnnotatedMethods(testClass, annotationType, traversalMode); - throwableCollector.execute(() -> methods.forEach(method -> { - assertVoid(annotationType, method); - assertStatic(annotationType, method); - assertNonPrivate(annotationType, method); - assertNoParameters(annotationType, method); - })); - return methods; + DiscoveryIssueReporter issueReporter) { + + return findAnnotatedMethods(testClass, annotationType, traversalMode).stream() // + .filter(allOf( // + returnsPrimitiveVoid(annotationType, issueReporter), // + isStatic(annotationType, issueReporter), // + isNotPrivate(annotationType, issueReporter), // + hasNoParameters(annotationType, issueReporter) // + )) // + .collect(toUnmodifiableList()); + } + + private static DiscoveryIssueReporter.Condition isStatic(Class annotationType, + DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(ModifierSupport::isStatic, method -> { + String message = String.format("@%s method '%s' must be static.", annotationType.getSimpleName(), + method.toGenericString()); + return createError(message, method); + }); } - private static void assertStatic(Class annotationType, Method method) { - if (ModifierSupport.isNotStatic(method)) { - throw new JUnitException(String.format("@%s method '%s' must be static.", annotationType.getSimpleName(), - method.toGenericString())); - } + private static DiscoveryIssueReporter.Condition isNotPrivate(Class annotationType, + DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(ModifierSupport::isNotPrivate, method -> { + String message = String.format("@%s method '%s' must not be private.", annotationType.getSimpleName(), + method.toGenericString()); + return createError(message, method); + }); } - private static void assertNonPrivate(Class annotationType, Method method) { - if (ModifierSupport.isPrivate(method)) { - throw new JUnitException(String.format("@%s method '%s' must not be private.", - annotationType.getSimpleName(), method.toGenericString())); - } + private static DiscoveryIssueReporter.Condition returnsPrimitiveVoid( + Class annotationType, DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(ReflectionUtils::returnsPrimitiveVoid, method -> { + String message = String.format("@%s method '%s' must not return a value.", annotationType.getSimpleName(), + method.toGenericString()); + return createError(message, method); + }); } - private static void assertVoid(Class annotationType, Method method) { - if (!returnsPrimitiveVoid(method)) { - throw new JUnitException(String.format("@%s method '%s' must not return a value.", - annotationType.getSimpleName(), method.toGenericString())); - } + private static DiscoveryIssueReporter.Condition hasNoParameters(Class annotationType, + DiscoveryIssueReporter issueReporter) { + return issueReporter.createReportingCondition(method -> method.getParameterCount() == 0, method -> { + String message = String.format("@%s method '%s' must not accept parameters.", + annotationType.getSimpleName(), method.toGenericString()); + return createError(message, method); + }); } - private static void assertNoParameters(Class annotationType, Method method) { - if (method.getParameterCount() > 0) { - throw new JUnitException(String.format("@%s method '%s' must not accept parameters.", - annotationType.getSimpleName(), method.toGenericString())); - } + private static DiscoveryIssue createError(String message, Method method) { + return DiscoveryIssue.builder(Severity.ERROR, message).source(MethodSource.from(method)).build(); } } 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 bfcd2f3b541f..9a4257d8a0cc 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 @@ -29,6 +29,7 @@ import org.junit.platform.engine.reporting.OutputDirectoryProvider; import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; import org.junit.platform.engine.support.descriptor.ClassSource; +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.launcher.LauncherDiscoveryRequest; @@ -57,17 +58,19 @@ final class SuiteTestDescriptor extends AbstractTestDescriptor { private final OutputDirectoryProvider outputDirectoryProvider; private final Boolean failIfNoTests; private final Class suiteClass; + private final LifecycleMethods lifecycleMethods; private LauncherDiscoveryResult launcherDiscoveryResult; private SuiteLauncher launcher; SuiteTestDescriptor(UniqueId id, Class suiteClass, ConfigurationParameters configurationParameters, - OutputDirectoryProvider outputDirectoryProvider) { + OutputDirectoryProvider outputDirectoryProvider, 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); } private static Boolean getFailIfNoTests(Class suiteClass) { @@ -134,24 +137,21 @@ void execute(EngineExecutionListener parentEngineExecutionListener) { parentEngineExecutionListener.executionStarted(this); ThrowableCollector throwableCollector = new OpenTest4JAwareThrowableCollector(); - List beforeSuiteMethods = LifecycleMethodUtils.findBeforeSuiteMethods(suiteClass, throwableCollector); - List afterSuiteMethods = LifecycleMethodUtils.findAfterSuiteMethods(suiteClass, throwableCollector); - - executeBeforeSuiteMethods(beforeSuiteMethods, throwableCollector); + executeBeforeSuiteMethods(throwableCollector); TestExecutionSummary summary = executeTests(parentEngineExecutionListener, throwableCollector); - executeAfterSuiteMethods(afterSuiteMethods, throwableCollector); + executeAfterSuiteMethods(throwableCollector); TestExecutionResult testExecutionResult = computeTestExecutionResult(summary, throwableCollector); parentEngineExecutionListener.executionFinished(this, testExecutionResult); } - private void executeBeforeSuiteMethods(List beforeSuiteMethods, ThrowableCollector throwableCollector) { + private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) { if (throwableCollector.isNotEmpty()) { return; } - for (Method beforeSuiteMethod : beforeSuiteMethods) { + for (Method beforeSuiteMethod : lifecycleMethods.beforeSuite) { throwableCollector.execute(() -> ReflectionSupport.invokeMethod(beforeSuiteMethod, null)); if (throwableCollector.isNotEmpty()) { return; @@ -173,8 +173,8 @@ private TestExecutionSummary executeTests(EngineExecutionListener parentEngineEx return launcher.execute(discoveryResult, parentEngineExecutionListener); } - private void executeAfterSuiteMethods(List afterSuiteMethods, ThrowableCollector throwableCollector) { - for (Method afterSuiteMethod : afterSuiteMethods) { + private void executeAfterSuiteMethods(ThrowableCollector throwableCollector) { + for (Method afterSuiteMethod : lifecycleMethods.afterSuite) { throwableCollector.execute(() -> ReflectionSupport.invokeMethod(afterSuiteMethod, null)); } } @@ -198,4 +198,15 @@ public boolean mayRegisterTests() { return true; } + private static class LifecycleMethods { + + final List beforeSuite; + final List afterSuite; + + LifecycleMethods(Class suiteClass, DiscoveryIssueReporter issueReporter) { + beforeSuite = LifecycleMethodUtils.findBeforeSuiteMethods(suiteClass, issueReporter); + afterSuite = LifecycleMethodUtils.findAfterSuiteMethods(suiteClass, issueReporter); + } + } + } diff --git a/junit-platform-suite-engine/src/nativeImage/initialize-at-build-time b/junit-platform-suite-engine/src/nativeImage/initialize-at-build-time index a6d7d06046b1..5313fc54879d 100644 --- a/junit-platform-suite-engine/src/nativeImage/initialize-at-build-time +++ b/junit-platform-suite-engine/src/nativeImage/initialize-at-build-time @@ -1,4 +1,5 @@ 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/EngineDiscoveryResults.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineDiscoveryResults.java new file mode 100644 index 000000000000..901462794d31 --- /dev/null +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineDiscoveryResults.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.platform.testkit.engine; + +import static java.util.Collections.unmodifiableList; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.List; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.TestDescriptor; + +/** + * {@code EngineDiscoveryResults} represents the results of test discovery + * by a {@link org.junit.platform.engine.TestEngine TestEngine} on the JUnit + * Platform and provides access to the {@link TestDescriptor} of the engine + * and any {@link DiscoveryIssue DiscoveryIssues} that were encountered. + * + * @since 1.13 + */ +@API(status = EXPERIMENTAL, since = "1.13") +public class EngineDiscoveryResults { + + private final TestDescriptor engineDescriptor; + private final List discoveryIssues; + + EngineDiscoveryResults(TestDescriptor engineDescriptor, List discoveryIssues) { + this.engineDescriptor = Preconditions.notNull(engineDescriptor, "Engine descriptor must not be null"); + this.discoveryIssues = unmodifiableList( + Preconditions.notNull(discoveryIssues, "Discovery issues list must not be null")); + Preconditions.containsNoNullElements(discoveryIssues, "Discovery issues list must not contain null elements"); + } + + /** + * {@return the root {@link TestDescriptor} of the engine} + */ + public TestDescriptor getEngineDescriptor() { + return engineDescriptor; + } + + /** + * {@return the issues that were encountered during discovery} + */ + public List getDiscoveryIssues() { + return discoveryIssues; + } + +} 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 2813765250e0..df1505b1c4d8 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,9 +16,11 @@ 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.launcher.core.EngineDiscoveryOrchestrator.Phase.DISCOVERY; import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.EXECUTION; import java.nio.file.Path; +import java.util.List; import java.util.Map; import java.util.ServiceLoader; import java.util.stream.Stream; @@ -29,6 +31,7 @@ import org.junit.platform.commons.util.CollectionUtils; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.DiscoveryFilter; +import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.EngineExecutionListener; @@ -46,15 +49,24 @@ import org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry; /** - * {@code EngineTestKit} provides support for executing a test plan for a given - * {@link TestEngine} and then accessing the results via - * {@linkplain EngineExecutionResults a fluent API} to verify the expected results. + * {@code EngineTestKit} provides support for discovering and executing tests + * for a given {@link TestEngine} and provides convenient access to the results. + * + *

For discovery, {@link EngineDiscoveryResults} provides access to + * the {@link TestDescriptor} of the engine and any {@link DiscoveryIssue + * DiscoveryIssues} that were encountered. + * + *

For execution, {@link EngineExecutionResults} provides a fluent + * API to verify the expected results. * * @since 1.4 * @see #engine(String) * @see #engine(TestEngine) + * @see #discover(String, LauncherDiscoveryRequest) + * @see #discover(TestEngine, LauncherDiscoveryRequest) * @see #execute(String, LauncherDiscoveryRequest) * @see #execute(TestEngine, LauncherDiscoveryRequest) + * @see EngineDiscoveryResults * @see EngineExecutionResults */ @API(status = MAINTAINED, since = "1.7") @@ -121,6 +133,65 @@ public static Builder engine(TestEngine testEngine) { return new Builder(testEngine); } + /** + * Discover tests for the given {@link LauncherDiscoveryRequest} using the + * {@link TestEngine} with the supplied ID. + * + *

The {@code TestEngine} will be loaded via Java's {@link ServiceLoader} + * mechanism, analogous to the manner in which test engines are loaded in + * the JUnit Platform Launcher API. + * + *

{@link org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder} + * provides a convenient way to build an appropriate discovery request to + * supply to this method. As an alternative, consider using + * {@link #engine(TestEngine)} for a more fluent API. + * + * @param engineId the ID of the {@code TestEngine} to use; must not be + * {@code null} or blank + * @param discoveryRequest the {@code LauncherDiscoveryRequest} to use + * @return the results of the discovery + * @throws PreconditionViolationException for invalid arguments or if the + * {@code TestEngine} with the supplied ID cannot be loaded + * @since 1.13 + * @see #discover(TestEngine, LauncherDiscoveryRequest) + * @see #engine(String) + * @see #engine(TestEngine) + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static EngineDiscoveryResults discover(String engineId, LauncherDiscoveryRequest discoveryRequest) { + Preconditions.notBlank(engineId, "TestEngine ID must not be null or blank"); + return discover(loadTestEngine(engineId.trim()), discoveryRequest); + } + + /** + * Discover tests for the given {@link LauncherDiscoveryRequest} using the + * supplied {@link TestEngine}. + * + *

{@link org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder} + * provides a convenient way to build an appropriate discovery request to + * supply to this method. As an alternative, consider using + * {@link #engine(TestEngine)} for a more fluent API. + * + * @param testEngine the {@code TestEngine} to use; must not be {@code null} + * @param discoveryRequest the {@code EngineDiscoveryResults} to use; must + * not be {@code null} + * @return the recorded {@code EngineExecutionResults} + * @throws PreconditionViolationException for invalid arguments + * @since 1.13 + * @see #discover(String, LauncherDiscoveryRequest) + * @see #engine(String) + * @see #engine(TestEngine) + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static EngineDiscoveryResults discover(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest) { + Preconditions.notNull(testEngine, "TestEngine must not be null"); + Preconditions.notNull(discoveryRequest, "EngineDiscoveryRequest must not be null"); + LauncherDiscoveryResult discoveryResult = discover(testEngine, discoveryRequest, DISCOVERY); + TestDescriptor engineDescriptor = discoveryResult.getEngineTestDescriptor(testEngine); + List discoveryIssues = discoveryResult.getDiscoveryIssues(testEngine); + return new EngineDiscoveryResults(engineDescriptor, discoveryIssues); + } + /** * Execute tests for the given {@link EngineDiscoveryRequest} using the * {@link TestEngine} with the supplied ID. @@ -260,13 +331,16 @@ private static void executeDirectly(TestEngine testEngine, EngineDiscoveryReques private static void executeUsingLauncherOrchestration(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest, EngineExecutionListener listener) { - LauncherDiscoveryResult discoveryResult = new EngineDiscoveryOrchestrator(singleton(testEngine), - emptySet()).discover(discoveryRequest, EXECUTION); - TestDescriptor engineTestDescriptor = discoveryResult.getEngineTestDescriptor(testEngine); - Preconditions.notNull(engineTestDescriptor, "TestEngine did not yield a TestDescriptor"); + LauncherDiscoveryResult discoveryResult = discover(testEngine, discoveryRequest, EXECUTION); new EngineExecutionOrchestrator().execute(discoveryResult, listener); } + private static LauncherDiscoveryResult discover(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest, + EngineDiscoveryOrchestrator.Phase phase) { + return new EngineDiscoveryOrchestrator(singleton(testEngine), emptySet()) // + .discover(discoveryRequest, phase); + } + @SuppressWarnings("unchecked") private static TestEngine loadTestEngine(String engineId) { Iterable testEngines = new ServiceLoaderTestEngineRegistry().loadTestEngines(); @@ -446,6 +520,25 @@ public Builder outputDirectoryProvider(OutputDirectoryProvider outputDirectoryPr return this; } + /** + * Discover tests for the configured {@link TestEngine}, + * {@linkplain DiscoverySelector discovery selectors}, + * {@linkplain DiscoveryFilter discovery filters}, and + * configuration parameters. + * + * @return the recorded {@code EngineDiscoveryResults} + * @since 1.13 + * @see #selectors(DiscoverySelector...) + * @see #filters(Filter...) + * @see #configurationParameter(String, String) + * @see #configurationParameters(Map) + */ + @API(status = EXPERIMENTAL, since = "1.13") + public EngineDiscoveryResults discover() { + LauncherDiscoveryRequest request = this.requestBuilder.build(); + return EngineTestKit.discover(this.testEngine, request); + } + /** * Execute tests for the configured {@link TestEngine}, * {@linkplain DiscoverySelector discovery selectors}, diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java index 5f260fe3dc14..4be237c109e0 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java @@ -249,6 +249,32 @@ public static Condition dynamicTestRegistered(Condition condition) return allOf(type(DYNAMIC_TEST_REGISTERED), condition); } + /** + * Create a new {@link Condition} that matches if and only if the + * {@linkplain TestDescriptor#getUniqueId() unique id} of an {@link Event}'s + * {@linkplain Event#getTestDescriptor() test descriptor} is equal to the + * {@link UniqueId} parsed from the supplied {@link String}. + * + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static Condition uniqueId(String uniqueId) { + return uniqueId(UniqueId.parse(uniqueId)); + } + + /** + * Create a new {@link Condition} that matches if and only if the + * {@linkplain TestDescriptor#getUniqueId() unique id} of an {@link Event}'s + * {@linkplain Event#getTestDescriptor() test descriptor} is equal to the + * supplied {@link UniqueId}. + * + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static Condition uniqueId(UniqueId uniqueId) { + return uniqueId(new Condition<>(isEqual(uniqueId), "equal to '%s'", uniqueId)); + } + /** * Create a new {@link Condition} that matches if and only if the * {@linkplain TestDescriptor#getUniqueId() unique id} of an @@ -260,11 +286,22 @@ public static Condition uniqueIdSubstring(String uniqueIdSubstring) { String text = segment.getType() + ":" + segment.getValue(); return text.contains(uniqueIdSubstring); }; + return uniqueId(new Condition<>(uniqueId -> uniqueId.getSegments().stream().anyMatch(predicate), + "substring '%s'", uniqueIdSubstring)); + } - return new Condition<>( - byTestDescriptor( - where(TestDescriptor::getUniqueId, uniqueId -> uniqueId.getSegments().stream().anyMatch(predicate))), - "descriptor with uniqueId substring '%s'", uniqueIdSubstring); + /** + * Create a new {@link Condition} that matches if and only if the + * {@linkplain TestDescriptor#getUniqueId() unique id} of an {@link Event}'s + * {@linkplain Event#getTestDescriptor() test descriptor} matches the + * supplied {@link Condition}. + * + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static Condition uniqueId(Condition condition) { + return new Condition<>(byTestDescriptor(where(TestDescriptor::getUniqueId, condition::matches)), + "descriptor with uniqueId %s", condition.description().value()); } /** @@ -315,6 +352,21 @@ public static Condition displayName(String displayName) { "descriptor with display name '%s'", displayName); } + /** + * Create a new {@link Condition} that matches if and only if the + * {@linkplain TestDescriptor#getLegacyReportingName()} () legacy reporting name} + * of an {@link Event}'s {@linkplain Event#getTestDescriptor() test descriptor} + * is equal to the supplied {@link String}. + * + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static Condition legacyReportingName(String legacyReportingName) { + return new Condition<>( + byTestDescriptor(where(TestDescriptor::getLegacyReportingName, isEqual(legacyReportingName))), + "descriptor with legacy reporting name '%s'", legacyReportingName); + } + /** * Create a new {@link Condition} that matches if and only if an * {@link Event}'s {@linkplain Event#getType() type} is diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java index 78e48d641bd2..3d97979d2276 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java @@ -47,10 +47,11 @@ public final class Constants { public static final String PARALLEL_POOL_SIZE = "junit.vintage.execution.parallel.pool-size"; /** - * Indicates whether parallel execution is enabled for test classes in the JUnit Vintage engine. + * Indicates whether parallel execution is enabled for test classes in the + * JUnit Vintage engine. * - *

Set this property to {@code true} to enable parallel execution of test classes. - * Defaults to {@code false}. + *

Set this property to {@code true} to enable parallel execution of test + * classes. Defaults to {@code false}. * * @since 5.12 */ @@ -58,10 +59,11 @@ public final class Constants { public static final String PARALLEL_CLASS_EXECUTION = "junit.vintage.execution.parallel.classes"; /** - * Indicates whether parallel execution is enabled for test methods in the JUnit Vintage engine. + * Indicates whether parallel execution is enabled for test methods in the + * JUnit Vintage engine. * - *

Set this property to {@code true} to enable parallel execution of test methods. - * Defaults to {@code false}. + *

Set this property to {@code true} to enable parallel execution of test + * methods. Defaults to {@code false}. * * @since 5.12 */ diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/VintageExecutor.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/VintageExecutor.java index f20446863d2a..0ca61c6655fe 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/VintageExecutor.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/VintageExecutor.java @@ -186,4 +186,5 @@ private void shutdownExecutorService(ExecutorService executorService) { Thread.currentThread().interrupt(); } } + } diff --git a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineDiscoveryTests.java b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineDiscoveryTests.java index 7425889bccb7..8947aeae6212 100644 --- a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineDiscoveryTests.java +++ b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineDiscoveryTests.java @@ -212,7 +212,7 @@ void resolvesJUnit4TestCaseWithIndistinguishableOverloadedMethod() { List testMethodDescriptors = new ArrayList<>(runnerDescriptor.getChildren()); assertThat(testMethodDescriptors).hasSize(2); - var testMethodDescriptor = testMethodDescriptors.get(0); + var testMethodDescriptor = testMethodDescriptors.getFirst(); assertEquals("theory", testMethodDescriptor.getDisplayName()); assertEquals(VintageUniqueIdBuilder.uniqueIdForMethod(testClass, "theory", "0"), testMethodDescriptor.getUniqueId()); 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 4d47f228f241..2fd7a4b4080b 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 @@ -182,7 +182,7 @@ void executesEnclosedWithParameterizedChildrenJUnit4TestCase() { String commonNestedClassPrefix = EnclosedWithParameterizedChildrenJUnit4TestCase.class.getName() + "$NestedTestCase"; - execute(testClass).allEvents().debug().assertEventsMatchExactly( // + execute(testClass).allEvents().assertEventsMatchExactly( // event(engine(), started()), // event(container(testClass), started()), // event(container(commonNestedClassPrefix), started()), // @@ -498,7 +498,7 @@ void executesParameterizedTimingTestCase() { Class testClass = ParameterizedTimingTestCase.class; - var events = execute(testClass).allEvents().debug(); + var events = execute(testClass).allEvents(); var firstParamStartedEvent = events.filter(event(container("[foo]"), started())::matches).findFirst() // .orElseThrow(() -> new AssertionError("No start event for [foo]")); @@ -639,7 +639,7 @@ public Description getDescription() { @Override public void run(RunNotifier notifier) { - var staticDescription = getDescription().getChildren().get(0); + var staticDescription = getDescription().getChildren().getFirst(); notifier.fireTestStarted(staticDescription); notifier.fireTestFinished(staticDescription); var dynamicDescription = createTestDescription(testClass, "dynamicTest"); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssumptionsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssumptionsTests.java index a63534493de9..7aa35c56b634 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssumptionsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssumptionsTests.java @@ -165,7 +165,7 @@ void assumingThatWithBooleanTrue() { List list = new ArrayList<>(); assumingThat(true, () -> list.add("test")); assertEquals(1, list.size()); - assertEquals("test", list.get(0)); + assertEquals("test", list.getFirst()); } @Test @@ -173,7 +173,7 @@ void assumingThatWithBooleanSupplierTrue() { List list = new ArrayList<>(); assumingThat(() -> true, () -> list.add("test")); assertEquals(1, list.size()); - assertEquals("test", list.get(0)); + assertEquals("test", list.getFirst()); } @Test 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 898dfc1ea9e8..27578670dab7 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 @@ -22,9 +22,16 @@ import java.util.EmptyStackException; import java.util.List; import java.util.Stack; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayNameGenerator.IndicativeSentences.SentenceFragment; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContextProvider; +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.testkit.engine.Event; /** * Check generated display names. @@ -147,10 +154,10 @@ void checkDisplayNameGeneratedForTestingAStackDemo() { } @Test - void checkDisplayNameGeneratedForIndicativeGeneratorTestCase() { + void checkDisplayNameGeneratedForIndicativeGenerator() { check(IndicativeGeneratorTestCase.class, // "CONTAINER: A stack", // - "TEST: A stack, is instantiated with new constructor", // + "TEST: A stack, is instantiated with its constructor", // "CONTAINER: A stack, when new", // "TEST: A stack, when new, throws EmptyStackException when peeked", // "CONTAINER: A stack, when new, after pushing an element to an empty stack", // @@ -159,10 +166,10 @@ void checkDisplayNameGeneratedForIndicativeGeneratorTestCase() { } @Test - void checkDisplayNameGeneratedForIndicativeGeneratorWithCustomSeparatorTestCase() { + void checkDisplayNameGeneratedForIndicativeGeneratorWithCustomSeparator() { check(IndicativeGeneratorWithCustomSeparatorTestCase.class, // "CONTAINER: A stack", // - "TEST: A stack >> is instantiated with new constructor", // + "TEST: A stack >> is instantiated with its constructor", // "CONTAINER: A stack >> when new", // "TEST: A stack >> when new >> throws EmptyStackException when peeked", // "CONTAINER: A stack >> when new >> after pushing an element to an empty stack", // @@ -170,6 +177,18 @@ void checkDisplayNameGeneratedForIndicativeGeneratorWithCustomSeparatorTestCase( ); } + @Test + void checkDisplayNameGeneratedForIndicativeGeneratorWithCustomSentenceFragments() { + check(IndicativeGeneratorWithCustomSentenceFragmentsTestCase.class, // + "CONTAINER: A stack", // + "TEST: A stack, is instantiated with its constructor", // + "CONTAINER: A stack, when new", // + "TEST: A stack, when new, throws EmptyStackException when peeked", // + "CONTAINER: A stack, when new, after pushing an element to an empty stack", // + "TEST: A stack, when new, after pushing an element to an empty stack, is no longer empty" // + ); + } + @Test void displayNameGenerationInheritance() { check(DisplayNameGenerationInheritanceTestCase.InnerNestedTestCase.class, // @@ -225,9 +244,39 @@ void indicativeSentencesOnSubClass() { ); } + @Test + void indicativeSentencesOnClassTemplate() { + check(ClassTemplateTestCase.class, // + "CONTAINER: Class template", // + "CONTAINER: [1] Class template", // + "TEST: Class template, some test", // + "CONTAINER: Class template, Regular Nested Test Case", // + "TEST: Class template, Regular Nested Test Case, some nested test", // + "CONTAINER: Class template, Nested Class Template", // + "CONTAINER: [1] Class template, Nested Class Template", // + "TEST: Class template, Nested Class Template, some nested test" // + ); + + assertThat(executeTestsForClass(ClassTemplateTestCase.class).allEvents().started().stream()) // + .map(event -> event.getTestDescriptor().getDisplayName()) // + .containsExactly( // + "JUnit Jupiter", // + "Class template", // + "[1] Class template", // + "Class template, some test", // + "Class template, Regular Nested Test Case", // + "Class template, Regular Nested Test Case, some nested test", // + "Class template, Nested Class Template", // + "[1] Class template, Nested Class Template", // + "Class template, Nested Class Template, some nested test" // + ); + } + private void check(Class testClass, String... expectedDisplayNames) { var request = request().selectors(selectClass(testClass)).build(); - var descriptors = discoverTests(request).getDescendants(); + var descriptors = executeTests(request).allEvents().started().stream() // + .map(Event::getTestDescriptor) // + .skip(1); // Skip engine descriptor assertThat(descriptors).map(this::describe).containsExactlyInAnyOrder(expectedDisplayNames); } @@ -235,7 +284,7 @@ private String describe(TestDescriptor descriptor) { return descriptor.getType() + ": " + descriptor.getDisplayName(); } - // ------------------------------------------------------------------- + // ------------------------------------------------------------------------- static class NoNameGenerator implements DisplayNameGenerator { @@ -314,7 +363,7 @@ static class NoNameStyleTestCase extends AbstractTestCase { static class UnderscoreStyleInheritedFromSuperClassTestCase extends UnderscoreStyleTestCase { } - // ------------------------------------------------------------------- + // ------------------------------------------------------------------------- @SuppressWarnings("JUnitMalformedDeclaration") @DisplayName("A stack") @@ -381,7 +430,7 @@ void peek_returns_that_element_without_removing_it_from_the_stack() { } } - // ------------------------------------------------------------------- + // ------------------------------------------------------------------------- @SuppressWarnings("JUnitMalformedDeclaration") @DisplayName("A stack") @@ -391,7 +440,7 @@ static class IndicativeGeneratorTestCase { Stack stack; @Test - void is_instantiated_with_new_constructor() { + void is_instantiated_with_its_constructor() { new Stack<>(); } @@ -426,7 +475,7 @@ void is_no_longer_empty() { } } - // ------------------------------------------------------------------- + // ------------------------------------------------------------------------- @SuppressWarnings("JUnitMalformedDeclaration") @DisplayName("A stack") @@ -436,7 +485,7 @@ static class IndicativeGeneratorWithCustomSeparatorTestCase { Stack stack; @Test - void is_instantiated_with_new_constructor() { + void is_instantiated_with_its_constructor() { new Stack<>(); } @@ -470,4 +519,107 @@ void is_no_longer_empty() { } } } + + // ------------------------------------------------------------------------- + + @SuppressWarnings("JUnitMalformedDeclaration") + @SentenceFragment("A stack") + @IndicativeSentencesGeneration + static class IndicativeGeneratorWithCustomSentenceFragmentsTestCase { + + Stack stack; + + @SentenceFragment("is instantiated with its constructor") + @Test + void instantiateViaConstructor() { + new Stack<>(); + } + + @SentenceFragment("when new") + @Nested + class NewStackTestCase { + + @BeforeEach + void createNewStack() { + stack = new Stack<>(); + } + + @SentenceFragment("throws EmptyStackException when peeked") + @Test + void throwsExceptionWhenPeeked() { + assertThrows(EmptyStackException.class, () -> stack.peek()); + } + + @SentenceFragment("after pushing an element to an empty stack") + @Nested + class ElementPushedOntoStackTestCase { + + String anElement = "an element"; + + @BeforeEach + void pushElementOntoStack() { + stack.push(anElement); + } + + @SentenceFragment("is no longer empty") + @Test + void nonEmptyStack() { + assertFalse(stack.isEmpty()); + } + } + } + } + + // ------------------------------------------------------------------------- + + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ExtendWith(ClassTemplateTestCase.Once.class) + @DisplayName("Class template") + @IndicativeSentencesGeneration(generator = DisplayNameGenerator.ReplaceUnderscores.class) + @TestClassOrder(ClassOrderer.OrderAnnotation.class) + static class ClassTemplateTestCase { + + @Test + void some_test() { + } + + @Nested + @Order(1) + class Regular_Nested_Test_Case { + @Test + void some_nested_test() { + } + } + + @Nested + @Order(2) + @ClassTemplate + class Nested_Class_Template { + @Test + void some_nested_test() { + } + } + + private static class Once implements ClassTemplateInvocationContextProvider { + + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideClassTemplateInvocationContexts( + ExtensionContext context) { + return Stream.of(new ClassTemplateInvocationContext() { + @Override + public String getDisplayName(int invocationIndex) { + return "%s %s".formatted(ClassTemplateInvocationContext.super.getDisplayName(invocationIndex), + context.getDisplayName()); + } + }); + } + } + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java index 0d77cac0882e..45dfd821661a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java @@ -35,11 +35,13 @@ public class KitchenSinkExtension implements // Lifecycle Callbacks BeforeAllCallback, - BeforeEachCallback, - BeforeTestExecutionCallback, - TestExecutionExceptionHandler, - AfterTestExecutionCallback, - AfterEachCallback, + BeforeClassTemplateInvocationCallback, + BeforeEachCallback, + BeforeTestExecutionCallback, + TestExecutionExceptionHandler, + AfterTestExecutionCallback, + AfterEachCallback, + AfterClassTemplateInvocationCallback, AfterAllCallback, // Lifecycle methods exception handling @@ -55,8 +57,9 @@ public class KitchenSinkExtension implements // Conditional Test Execution ExecutionCondition, - // @TestTemplate + // @TestTemplate and @ClassTemplate TestTemplateInvocationContextProvider, + ClassTemplateInvocationContextProvider, // Miscellaneous TestWatcher, @@ -79,6 +82,10 @@ public ExtensionContextScope getTestInstantiationExtensionContextScope(Extension public void beforeAll(ExtensionContext context) { } + @Override + public void beforeClassTemplateInvocation(ExtensionContext context) { + } + @Override public void beforeEach(ExtensionContext context) { } @@ -99,6 +106,10 @@ public void afterTestExecution(ExtensionContext context) { public void afterEach(ExtensionContext context) { } + @Override + public void afterClassTemplateInvocation(ExtensionContext context) { + } + @Override public void afterAll(ExtensionContext context) { } @@ -174,6 +185,23 @@ public boolean mayReturnZeroTestTemplateInvocationContexts(ExtensionContext cont return false; } + // --- @ClassTemplate ------------------------------------------------------- + + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return false; + } + + @Override + public Stream provideClassTemplateInvocationContexts(ExtensionContext context) { + return null; + } + + @Override + public boolean mayReturnZeroClassTemplateInvocationContexts(ExtensionContext context) { + return false; + } + // --- TestWatcher --------------------------------------------------------- @Override diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java index 60e7c36b808f..cc300beee64e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java @@ -12,6 +12,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.util.Throwables.getRootCause; +import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; +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.support.hierarchical.ExclusiveResource.LockMode; import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; @@ -25,6 +29,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.ClassTemplate; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Nested; @@ -34,12 +39,14 @@ import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.JupiterTestDescriptor; 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.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.hierarchical.ExclusiveResource; @@ -180,6 +187,77 @@ void addSharedResourcesViaAnnotationValueAndProviders() { // @formatter:on } + @Test + void addSharedResourcesViaAnnotationValueAndProvidersForClassTemplate() { + var selector = selectClass(SharedResourcesViaAnnotationValueAndProvidersClassTemplateTestCase.class); + var engineDescriptor = discoverTests(selector).getEngineDescriptor(); + engineDescriptor.accept(TestDescriptor::prune); + + var classTemplateTestDescriptor = (JupiterTestDescriptor) getOnlyElement(engineDescriptor.getChildren()); + + var expectedResources = List.of( // + new ExclusiveResource("a1", LockMode.READ_WRITE), // + new ExclusiveResource("a2", LockMode.READ_WRITE), // + new ExclusiveResource("a3", LockMode.READ), // + new ExclusiveResource("b1", LockMode.READ), // + new ExclusiveResource("b2", LockMode.READ), // + new ExclusiveResource("c1", LockMode.READ_WRITE), // + new ExclusiveResource("c2", LockMode.READ_WRITE), // + new ExclusiveResource("c3", LockMode.READ_WRITE), // + new ExclusiveResource("d1", LockMode.READ_WRITE), // + new ExclusiveResource("d2", LockMode.READ) // + ); + + assertThat(classTemplateTestDescriptor.getExclusiveResources()) // + .containsExactlyInAnyOrderElementsOf(expectedResources); + } + + @Test + void addSharedResourcesViaAnnotationValueAndProvidersForClassTemplateInvocation() { + var selector = selectIteration( + selectClass(SharedResourcesViaAnnotationValueAndProvidersClassTemplateTestCase.class), 0); + var engineDescriptor = discoverTests(selector).getEngineDescriptor(); + engineDescriptor.accept(TestDescriptor::prune); + + var classTemplateTestDescriptor = (JupiterTestDescriptor) getOnlyElement(engineDescriptor.getChildren()); + + var expectedResources = List.of( // + new ExclusiveResource("a1", LockMode.READ_WRITE), // + new ExclusiveResource("a2", LockMode.READ_WRITE), // + new ExclusiveResource("a3", LockMode.READ), // + new ExclusiveResource("b1", LockMode.READ), // + new ExclusiveResource("b2", LockMode.READ), // + new ExclusiveResource("c1", LockMode.READ_WRITE), // + new ExclusiveResource("c2", LockMode.READ_WRITE), // + new ExclusiveResource("c3", LockMode.READ_WRITE), // + new ExclusiveResource("d1", LockMode.READ_WRITE), // + new ExclusiveResource("d2", LockMode.READ) // + ); + + assertThat(classTemplateTestDescriptor.getExclusiveResources()) // + .containsExactlyInAnyOrderElementsOf(expectedResources); + } + + @Test + void addSharedResourcesViaAnnotationValueAndProvidersForMethodInClassTemplate() { + var selector = selectMethod(SharedResourcesViaAnnotationValueAndProvidersClassTemplateTestCase.class, "test"); + var engineDescriptor = discoverTests(selector).getEngineDescriptor(); + engineDescriptor.accept(TestDescriptor::prune); + + var classTemplateTestDescriptor = (JupiterTestDescriptor) getOnlyElement(engineDescriptor.getChildren()); + + var expectedResources = List.of( // + new ExclusiveResource("a1", LockMode.READ_WRITE), // + new ExclusiveResource("a2", LockMode.READ_WRITE), // + new ExclusiveResource("a3", LockMode.READ), // + new ExclusiveResource("b1", LockMode.READ), // + new ExclusiveResource("b2", LockMode.READ) // + ); + + assertThat(classTemplateTestDescriptor.getExclusiveResources()) // + .containsExactlyInAnyOrderElementsOf(expectedResources); + } + @Test void sharedResourcesHavingTheSameValueAndModeAreDeduplicated() { // @formatter:off @@ -523,4 +601,71 @@ class NestedClass { } } + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ResourceLock( // + value = "a1", // + providers = SharedResourcesViaAnnotationValueAndProvidersClassTemplateTestCase.FirstClassLevelProvider.class // + ) + @ResourceLock( // + value = "a2", // + target = ResourceLockTarget.CHILDREN, // + providers = SharedResourcesViaAnnotationValueAndProvidersClassTemplateTestCase.SecondClassLevelProvider.class // + ) + static class SharedResourcesViaAnnotationValueAndProvidersClassTemplateTestCase { + + @Test + @ResourceLock(value = "b1", mode = ResourceAccessMode.READ) + void test() { + } + + @Nested + @ResourceLock(providers = NestedClassLevelProvider.class) + class NestedClass { + @Test + @ResourceLock("c1") + void test() { + } + } + + @Nested + @ClassTemplate + @ResourceLock(value = "d1", target = ResourceLockTarget.CHILDREN) + class NestedClassTemplate { + @Test + @ResourceLock(value = "d2", mode = ResourceAccessMode.READ) + void test() { + } + } + + static class FirstClassLevelProvider implements ResourceLocksProvider { + + @Override + public Set provideForClass(Class testClass) { + return Set.of(new Lock("a3", ResourceAccessMode.READ)); + } + } + + static class SecondClassLevelProvider implements ResourceLocksProvider { + + @Override + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { + return Set.of(new Lock("b2", ResourceAccessMode.READ)); + } + + @Override + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { + return Set.of(new Lock("c2")); + } + } + + static class NestedClassLevelProvider implements ResourceLocksProvider { + + @Override + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { + return Set.of(new Lock("c3")); + } + } + } } 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 3ec1035ebb5c..c8b93723188c 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,16 +13,17 @@ 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.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.Set; +import java.util.function.Consumer; import org.junit.platform.engine.DiscoverySelector; -import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.testkit.engine.EngineDiscoveryResults; import org.junit.platform.testkit.engine.EngineExecutionResults; import org.junit.platform.testkit.engine.EngineTestKit; @@ -40,7 +41,13 @@ protected EngineExecutionResults executeTestsForClass(Class testClass) { } protected EngineExecutionResults executeTests(DiscoverySelector... selectors) { - return executeTests(request().selectors(selectors).outputDirectoryProvider(dummyOutputDirectoryProvider())); + return executeTests(request -> request.selectors(selectors)); + } + + protected EngineExecutionResults executeTests(Consumer configurer) { + var builder = defaultRequest(); + configurer.accept(builder); + return executeTests(builder); } protected EngineExecutionResults executeTests(LauncherDiscoveryRequestBuilder builder) { @@ -51,20 +58,30 @@ protected EngineExecutionResults executeTests(LauncherDiscoveryRequest request) return EngineTestKit.execute(this.engine, request); } - protected TestDescriptor discoverTests(DiscoverySelector... selectors) { - return discoverTests( - request().selectors(selectors).outputDirectoryProvider(dummyOutputDirectoryProvider()).build()); + protected EngineDiscoveryResults discoverTestsForClass(Class testClass) { + return discoverTests(selectClass(testClass)); + } + + protected EngineDiscoveryResults discoverTests(DiscoverySelector... selectors) { + return discoverTests(defaultRequest().selectors(selectors).build()); + } + + private static LauncherDiscoveryRequestBuilder defaultRequest() { + return request() // + .outputDirectoryProvider(dummyOutputDirectoryProvider()) // + .configurationParameter(STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME, String.valueOf(false)); } - protected TestDescriptor discoverTests(LauncherDiscoveryRequest request) { - return engine.discover(request, UniqueId.forEngine(engine.getId())); + protected EngineDiscoveryResults discoverTests(LauncherDiscoveryRequest request) { + return EngineTestKit.discover(this.engine, request); } protected UniqueId discoverUniqueId(Class clazz, String methodName) { - TestDescriptor engineDescriptor = discoverTests(selectMethod(clazz, methodName)); - Set descendants = engineDescriptor.getDescendants(); + var results = discoverTests(selectMethod(clazz, methodName)); + var engineDescriptor = results.getEngineDescriptor(); + var descendants = engineDescriptor.getDescendants(); // @formatter:off - TestDescriptor testDescriptor = descendants.stream() + var testDescriptor = descendants.stream() .skip(descendants.size() - 1) .findFirst() .orElseGet(() -> fail("no descendants")); 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 new file mode 100644 index 000000000000..364f78a7306d --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ClassTemplateInvocationTests.java @@ -0,0 +1,1555 @@ +/* + * 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.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; +import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException; +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.selectNestedClass; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectNestedMethod; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.launcher.TagFilter.excludeTags; +import static org.junit.platform.launcher.TagFilter.includeTags; +import static org.junit.platform.testkit.engine.EventConditions.container; +import static org.junit.platform.testkit.engine.EventConditions.displayName; +import static org.junit.platform.testkit.engine.EventConditions.dynamicTestRegistered; +import static org.junit.platform.testkit.engine.EventConditions.engine; +import static org.junit.platform.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.EventConditions.legacyReportingName; +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.message; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.ClassTemplate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestReporter; +import org.junit.jupiter.api.extension.AfterClassTemplateInvocationCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeClassTemplateInvocationCallback; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtendWith; +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; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor; +import org.junit.jupiter.engine.descriptor.ClassTemplateTestDescriptor; +import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; +import org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor; +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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.testkit.engine.EngineExecutionResults; +import org.opentest4j.AssertionFailedError; +import org.opentest4j.TestAbortedException; + +/** + * @since 5.13 + */ +public class ClassTemplateInvocationTests extends AbstractJupiterTestEngineTests { + + @ParameterizedTest + @ValueSource(strings = { // + "class:%s", // + "uid:[engine:junit-jupiter]/[class-template:%s]" // + }) + void executesClassTemplateClassTwice(String selectorIdentifierTemplate) { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId1 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var invocation1MethodAId = invocationId1.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + var invocation1NestedClassId = invocationId1.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); + var invocation1NestedMethodBId = invocation1NestedClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var invocation2MethodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + var invocation2NestedClassId = invocationId2.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); + var invocation2NestedMethodBId = invocation2NestedClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTests(DiscoverySelectors.parse( + selectorIdentifierTemplate.formatted(TwoInvocationsTestCase.class.getName())).orElseThrow()); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId1)), displayName("[1] A of TwoInvocationsTestCase"), + legacyReportingName("%s[1]".formatted(TwoInvocationsTestCase.class.getName()))), // + event(container(uniqueId(invocationId1)), started()), // + event(dynamicTestRegistered(uniqueId(invocation1MethodAId))), // + event(dynamicTestRegistered(uniqueId(invocation1NestedClassId))), // + event(dynamicTestRegistered(uniqueId(invocation1NestedMethodBId))), // + event(test(uniqueId(invocation1MethodAId)), started()), // + event(test(uniqueId(invocation1MethodAId)), finishedSuccessfully()), // + event(container(uniqueId(invocation1NestedClassId)), started()), // + event(test(uniqueId(invocation1NestedMethodBId)), started()), // + event(test(uniqueId(invocation1NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocation1NestedClassId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId1)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of TwoInvocationsTestCase"), + legacyReportingName("%s[2]".formatted(TwoInvocationsTestCase.class.getName()))), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(invocation2MethodAId))), // + event(dynamicTestRegistered(uniqueId(invocation2NestedClassId))), // + event(dynamicTestRegistered(uniqueId(invocation2NestedMethodBId))), // + event(test(uniqueId(invocation2MethodAId)), started()), // + event(test(uniqueId(invocation2MethodAId)), finishedSuccessfully()), // + event(container(uniqueId(invocation2NestedClassId)), started()), // + event(test(uniqueId(invocation2NestedMethodBId)), started()), // + event(test(uniqueId(invocation2NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocation2NestedClassId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(classTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void classTemplateAnnotationIsInherited() { + var results = executeTestsForClass(InheritedTwoInvocationsTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(12).succeeded(12)); + } + + @Test + void executesOnlySelectedMethodsDeclaredInClassTemplate() { + var results = executeTests(selectMethod(TwoInvocationsTestCase.class, "a")); + + results.testEvents() // + .assertStatistics(stats -> stats.started(2).succeeded(2)) // + .assertEventsMatchLoosely(event(test(displayName("a()")), finishedSuccessfully())); + } + + @Test + void executesOnlySelectedMethodsDeclaredInNestedClassOfClassTemplate() { + var results = executeTests(selectNestedMethod(List.of(TwoInvocationsTestCase.class), + TwoInvocationsTestCase.NestedTestCase.class, "b")); + + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)) // + .assertEventsMatchLoosely(event(test(displayName("b()")), finishedSuccessfully())); + } + + @Test + void executesOnlyTestsPassingPostDiscoveryFilter() { + var results = executeTests(request -> request // + .selectors(selectClass(TwoInvocationsTestCase.class)) // + .filters(includeTags("nested"))); + + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)) // + .assertEventsMatchLoosely(event(test(displayName("b()")), finishedSuccessfully())); + } + + @Test + void prunesEmptyNestedTestClasses() { + var results = executeTests(request -> request // + .selectors(selectClass(TwoInvocationsTestCase.class)) // + .filters(excludeTags("nested"))); + + results.containerEvents().assertThatEvents() // + .noneMatch(container(TwoInvocationsTestCase.NestedTestCase.class.getSimpleName())::matches); + + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)) // + .assertEventsMatchLoosely(event(test(displayName("a()")), finishedSuccessfully())); + } + + @Test + void executesNestedClassTemplateClassTwiceWithClassSelectorForEnclosingClass() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classId = engineId.append(ClassTestDescriptor.SEGMENT_TYPE, + NestedClassTemplateWithTwoInvocationsTestCase.class.getName()); + var methodAId = classId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + var nestedClassTemplateId = classId.append(ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, + "NestedTestCase"); + var invocationId1 = nestedClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var invocation1NestedMethodBId = invocationId1.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + var invocationId2 = nestedClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var invocation2NestedMethodBId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTestsForClass(NestedClassTemplateWithTwoInvocationsTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classId)), started()), // + + event(test(uniqueId(methodAId)), started()), // + event(test(uniqueId(methodAId)), finishedSuccessfully()), // + + event(container(uniqueId(nestedClassTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId1)), displayName("[1] A of NestedTestCase"), + legacyReportingName( + "%s[1]".formatted(NestedClassTemplateWithTwoInvocationsTestCase.NestedTestCase.class.getName()))), // + event(container(uniqueId(invocationId1)), started()), // + event(dynamicTestRegistered(uniqueId(invocation1NestedMethodBId))), // + event(test(uniqueId(invocation1NestedMethodBId)), started()), // + event(test(uniqueId(invocation1NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId1)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of NestedTestCase"), + legacyReportingName( + "%s[2]".formatted(NestedClassTemplateWithTwoInvocationsTestCase.NestedTestCase.class.getName()))), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(invocation2NestedMethodBId))), // + event(test(uniqueId(invocation2NestedMethodBId)), started()), // + event(test(uniqueId(invocation2NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(nestedClassTemplateId)), finishedSuccessfully()), // + + event(container(uniqueId(classId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void executesNestedClassTemplateClassTwiceWithNestedClassSelector() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classId = engineId.append(ClassTestDescriptor.SEGMENT_TYPE, + NestedClassTemplateWithTwoInvocationsTestCase.class.getName()); + var nestedClassTemplateId = classId.append(ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, + "NestedTestCase"); + var invocationId1 = nestedClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var invocation1NestedMethodBId = invocationId1.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + var invocationId2 = nestedClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var invocation2NestedMethodBId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTestsForClass(NestedClassTemplateWithTwoInvocationsTestCase.NestedTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classId)), started()), // + + event(container(uniqueId(nestedClassTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId1)), displayName("[1] A of NestedTestCase")), // + event(container(uniqueId(invocationId1)), started()), // + event(dynamicTestRegistered(uniqueId(invocation1NestedMethodBId))), // + event(test(uniqueId(invocation1NestedMethodBId)), started()), // + event(test(uniqueId(invocation1NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId1)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(invocation2NestedMethodBId))), // + event(test(uniqueId(invocation2NestedMethodBId)), started()), // + event(test(uniqueId(invocation2NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(nestedClassTemplateId)), finishedSuccessfully()), // + + event(container(uniqueId(classId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void executesNestedClassTemplatesTwiceEach() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var outerClassTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoTimesTwoInvocationsTestCase.class.getName()); + + var outerInvocation1Id = outerClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var outerInvocation1NestedClassTemplateId = outerInvocation1Id.append( + ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, "NestedTestCase"); + var outerInvocation1InnerInvocation1Id = outerInvocation1NestedClassTemplateId.append( + ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var outerInvocation1InnerInvocation1NestedMethodId = outerInvocation1InnerInvocation1Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + var outerInvocation1InnerInvocation2Id = outerInvocation1NestedClassTemplateId.append( + ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation1InnerInvocation2NestedMethodId = outerInvocation1InnerInvocation2Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + + var outerInvocation2Id = outerClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation2NestedClassTemplateId = outerInvocation2Id.append( + ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, "NestedTestCase"); + var outerInvocation2InnerInvocation1Id = outerInvocation2NestedClassTemplateId.append( + ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var outerInvocation2InnerInvocation1NestedMethodId = outerInvocation2InnerInvocation1Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + var outerInvocation2InnerInvocation2Id = outerInvocation2NestedClassTemplateId.append( + ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation2InnerInvocation2NestedMethodId = outerInvocation2InnerInvocation2Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + + var results = executeTestsForClass(TwoTimesTwoInvocationsTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(outerClassTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation1Id)), + displayName("[1] A of TwoTimesTwoInvocationsTestCase")), // + event(container(uniqueId(outerInvocation1Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation1NestedClassTemplateId))), // + event(container(uniqueId(outerInvocation1NestedClassTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation1Id)), + displayName("[1] A of NestedTestCase")), // + event(container(uniqueId(outerInvocation1InnerInvocation1Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation1NestedMethodId))), // + event(test(uniqueId(outerInvocation1InnerInvocation1NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation1InnerInvocation1NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation1InnerInvocation1Id)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation2Id)), + displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(outerInvocation1InnerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation2NestedMethodId))), // + event(test(uniqueId(outerInvocation1InnerInvocation2NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation1InnerInvocation2NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation1InnerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerInvocation1NestedClassTemplateId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation1Id)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2Id)), + displayName("[2] B of TwoTimesTwoInvocationsTestCase")), // + event(container(uniqueId(outerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2NestedClassTemplateId))), // + event(container(uniqueId(outerInvocation2NestedClassTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation1Id)), + displayName("[1] A of NestedTestCase")), // + event(container(uniqueId(outerInvocation2InnerInvocation1Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation1NestedMethodId))), // + event(test(uniqueId(outerInvocation2InnerInvocation1NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation2InnerInvocation1NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2InnerInvocation1Id)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2Id)), + displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2NestedMethodId))), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerInvocation2NestedClassTemplateId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerClassTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void invocationContextProviderCanRegisterAdditionalExtensions() { + var results = executeTestsForClass(AdditionalExtensionRegistrationTestCase.class); + + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)); + } + + @Test + void eachInvocationHasSeparateExtensionContext() { + var results = executeTestsForClass(SeparateExtensionContextTestCase.class); + + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)); + } + + @Test + void supportsTestTemplateMethodsInsideClassTemplateClasses() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + CombinationWithTestTemplateTestCase.class.getName()); + var invocationId1 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var testTemplateId1 = invocationId1.append(TestTemplateTestDescriptor.SEGMENT_TYPE, "test(int)"); + var testTemplate1InvocationId1 = testTemplateId1.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#1"); + var testTemplate1InvocationId2 = testTemplateId1.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var testTemplateId2 = invocationId2.append(TestTemplateTestDescriptor.SEGMENT_TYPE, "test(int)"); + var testTemplate2InvocationId1 = testTemplateId2.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#1"); + var testTemplate2InvocationId2 = testTemplateId2.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + + var results = executeTestsForClass(CombinationWithTestTemplateTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId1)), + displayName("[1] A of CombinationWithTestTemplateTestCase")), // + event(container(uniqueId(invocationId1)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplateId1))), // + event(container(uniqueId(testTemplateId1)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplate1InvocationId1))), // + event(test(uniqueId(testTemplate1InvocationId1)), started()), // + event(test(uniqueId(testTemplate1InvocationId1)), finishedSuccessfully()), // + event(dynamicTestRegistered(uniqueId(testTemplate1InvocationId2))), // + event(test(uniqueId(testTemplate1InvocationId2)), started()), // + event(test(uniqueId(testTemplate1InvocationId2)), finishedSuccessfully()), // + event(container(uniqueId(testTemplateId1)), finishedSuccessfully()), // + event(container(uniqueId(invocationId1)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), + displayName("[2] B of CombinationWithTestTemplateTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplateId2))), // + event(container(uniqueId(testTemplateId2)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplate2InvocationId1))), // + event(test(uniqueId(testTemplate2InvocationId1)), started()), // + event(test(uniqueId(testTemplate2InvocationId1)), finishedSuccessfully()), // + event(dynamicTestRegistered(uniqueId(testTemplate2InvocationId2))), // + event(test(uniqueId(testTemplate2InvocationId2)), started()), // + event(test(uniqueId(testTemplate2InvocationId2)), finishedSuccessfully()), // + event(container(uniqueId(testTemplateId2)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(classTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void testTemplateInvocationInsideClassTemplateClassCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + CombinationWithTestTemplateTestCase.class.getName()); + var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var testTemplateId2 = invocationId2.append(TestTemplateTestDescriptor.SEGMENT_TYPE, "test(int)"); + var testTemplate2InvocationId2 = testTemplateId2.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + + var results = executeTests(selectUniqueId(testTemplate2InvocationId2)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), + displayName("[2] B of CombinationWithTestTemplateTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplateId2))), // + event(container(uniqueId(testTemplateId2)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplate2InvocationId2))), // + event(test(uniqueId(testTemplate2InvocationId2)), started()), // + event(test(uniqueId(testTemplate2InvocationId2)), finishedSuccessfully()), // + event(container(uniqueId(testTemplateId2)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(classTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void supportsTestFactoryMethodsInsideClassTemplateClasses() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + CombinationWithTestFactoryTestCase.class.getName()); + var invocationId1 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var testFactoryId1 = invocationId1.append(TestFactoryTestDescriptor.SEGMENT_TYPE, "test()"); + var testFactory1DynamicTestId1 = testFactoryId1.append(TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE, + "#1"); + var testFactory1DynamicTestId2 = testFactoryId1.append(TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE, + "#2"); + var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var testFactoryId2 = invocationId2.append(TestFactoryTestDescriptor.SEGMENT_TYPE, "test()"); + var testFactory2DynamicTestId1 = testFactoryId2.append(TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE, + "#1"); + var testFactory2DynamicTestId2 = testFactoryId2.append(TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE, + "#2"); + + var results = executeTestsForClass(CombinationWithTestFactoryTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId1)), + displayName("[1] A of CombinationWithTestFactoryTestCase")), // + event(container(uniqueId(invocationId1)), started()), // + event(dynamicTestRegistered(uniqueId(testFactoryId1))), // + event(container(uniqueId(testFactoryId1)), started()), // + event(dynamicTestRegistered(uniqueId(testFactory1DynamicTestId1))), // + event(test(uniqueId(testFactory1DynamicTestId1)), started()), // + event(test(uniqueId(testFactory1DynamicTestId1)), finishedSuccessfully()), // + event(dynamicTestRegistered(uniqueId(testFactory1DynamicTestId2))), // + event(test(uniqueId(testFactory1DynamicTestId2)), started()), // + event(test(uniqueId(testFactory1DynamicTestId2)), finishedSuccessfully()), // + event(container(uniqueId(testFactoryId1)), finishedSuccessfully()), // + event(container(uniqueId(invocationId1)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), + displayName("[2] B of CombinationWithTestFactoryTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(testFactoryId2))), // + event(container(uniqueId(testFactoryId2)), started()), // + event(dynamicTestRegistered(uniqueId(testFactory2DynamicTestId1))), // + event(test(uniqueId(testFactory2DynamicTestId1)), started()), // + event(test(uniqueId(testFactory2DynamicTestId1)), finishedSuccessfully()), // + event(dynamicTestRegistered(uniqueId(testFactory2DynamicTestId2))), // + event(test(uniqueId(testFactory2DynamicTestId2)), started()), // + event(test(uniqueId(testFactory2DynamicTestId2)), finishedSuccessfully()), // + event(container(uniqueId(testFactoryId2)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(classTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void specificDynamicTestInsideClassTemplateClassCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + CombinationWithTestFactoryTestCase.class.getName()); + var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var testFactoryId2 = invocationId2.append(TestFactoryTestDescriptor.SEGMENT_TYPE, "test()"); + var testFactory2DynamicTestId2 = testFactoryId2.append(TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE, + "#2"); + + var results = executeTests(selectUniqueId(testFactory2DynamicTestId2)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), + displayName("[2] B of CombinationWithTestFactoryTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(testFactoryId2))), // + event(container(uniqueId(testFactoryId2)), started()), // + event(dynamicTestRegistered(uniqueId(testFactory2DynamicTestId2))), // + event(test(uniqueId(testFactory2DynamicTestId2)), started()), // + event(test(uniqueId(testFactory2DynamicTestId2)), finishedSuccessfully()), // + event(container(uniqueId(testFactoryId2)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(classTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void failsIfProviderReturnsZeroInvocationContextWithoutOptIn() { + var results = executeTestsForClass(InvalidZeroInvocationTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(InvalidZeroInvocationTestCase.class), started()), // + event(container(InvalidZeroInvocationTestCase.class), + finishedWithFailure( + message("Provider [Ext] did not provide any invocation contexts, but was expected to do so. " + + "You may override mayReturnZeroClassTemplateInvocationContexts() to allow this."))), // + event(engine(), finishedSuccessfully())); + } + + @Test + void succeedsIfProviderReturnsZeroInvocationContextWithOptIn() { + var results = executeTestsForClass(ValidZeroInvocationTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(ValidZeroInvocationTestCase.class), started()), // + event(container(ValidZeroInvocationTestCase.class), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @ParameterizedTest + @ValueSource(classes = { NoProviderRegisteredTestCase.class, NoSupportingProviderRegisteredTestCase.class }) + void failsIfNoSupportingProviderIsRegistered(Class testClass) { + var results = executeTestsForClass(testClass); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(testClass), started()), // + event(container(testClass), + finishedWithFailure( + message("You must register at least one ClassTemplateInvocationContextProvider that supports " + + "@ClassTemplate class [" + testClass.getName() + "]"))), // + event(engine(), finishedSuccessfully())); + } + + @Test + void classTemplateInvocationCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var methodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + var nestedClassId = invocationId2.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); + var nestedMethodBId = nestedClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTests(selectUniqueId(invocationId2)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of TwoInvocationsTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(methodAId))), // + event(dynamicTestRegistered(uniqueId(nestedClassId))), // + event(dynamicTestRegistered(uniqueId(nestedMethodBId))), // + event(test(uniqueId(methodAId)), started()), // + event(test(uniqueId(methodAId)), finishedSuccessfully()), // + event(container(uniqueId(nestedClassId)), started()), // + event(test(uniqueId(nestedMethodBId)), started()), // + event(test(uniqueId(nestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(nestedClassId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(classTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void classTemplateInvocationCanBeSelectedByIteration() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var methodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + var nestedClassId = invocationId2.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); + var nestedMethodBId = nestedClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTests(selectIteration(selectClass(TwoInvocationsTestCase.class), 1)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of TwoInvocationsTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(methodAId))), // + event(dynamicTestRegistered(uniqueId(nestedClassId))), // + event(dynamicTestRegistered(uniqueId(nestedMethodBId))), // + event(test(uniqueId(methodAId)), started()), // + event(test(uniqueId(methodAId)), finishedSuccessfully()), // + event(container(uniqueId(nestedClassId)), started()), // + event(test(uniqueId(nestedMethodBId)), started()), // + event(test(uniqueId(nestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(nestedClassId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(classTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @ParameterizedTest + @ValueSource(strings = { // + "class:org.junit.jupiter.engine.ClassTemplateInvocationTests$TwoInvocationsTestCase", // + "uid:[engine:junit-jupiter]/[class-template:org.junit.jupiter.engine.ClassTemplateInvocationTests$TwoInvocationsTestCase]" // + }) + void executesAllInvocationsForRedundantSelectors(String classTemplateSelectorIdentifier) { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + + var results = executeTests(selectUniqueId(invocationId2), + DiscoverySelectors.parse(classTemplateSelectorIdentifier).orElseThrow()); + + results.testEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + + @Test + void methodInClassTemplateInvocationCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var methodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + + var results = executeTests(selectUniqueId(methodAId)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of TwoInvocationsTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(methodAId))), // + event(test(uniqueId(methodAId)), started()), // + event(test(uniqueId(methodAId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(classTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void nestedMethodInClassTemplateInvocationCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId2 = classTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var nestedClassId = invocationId2.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); + var nestedMethodBId = nestedClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTests(selectUniqueId(nestedMethodBId)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of TwoInvocationsTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(nestedClassId))), // + event(dynamicTestRegistered(uniqueId(nestedMethodBId))), // + event(container(uniqueId(nestedClassId)), started()), // + event(test(uniqueId(nestedMethodBId)), started()), // + event(test(uniqueId(nestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(nestedClassId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(classTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void nestedClassTemplateInvocationCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var outerClassTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoTimesTwoInvocationsWithMultipleMethodsTestCase.class.getName()); + var outerInvocation2Id = outerClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation2NestedClassTemplateId = outerInvocation2Id.append( + ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, "NestedTestCase"); + var outerInvocation2InnerInvocation2Id = outerInvocation2NestedClassTemplateId.append( + ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation2InnerInvocation2NestedMethodId = outerInvocation2InnerInvocation2Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTests(selectUniqueId(outerInvocation2InnerInvocation2NestedMethodId)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(outerClassTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2Id)), + displayName("[2] B of TwoTimesTwoInvocationsWithMultipleMethodsTestCase")), // + event(container(uniqueId(outerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2NestedClassTemplateId))), // + event(container(uniqueId(outerInvocation2NestedClassTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2Id)), + displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2NestedMethodId))), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerInvocation2NestedClassTemplateId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerClassTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void nestedClassTemplateInvocationCanBeSelectedByIteration() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var outerClassTemplateId = engineId.append(ClassTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoTimesTwoInvocationsTestCase.class.getName()); + var outerInvocation1Id = outerClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var outerInvocation1NestedClassTemplateId = outerInvocation1Id.append( + ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, "NestedTestCase"); + var outerInvocation1InnerInvocation2Id = outerInvocation1NestedClassTemplateId.append( + ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation1InnerInvocation2NestedMethodId = outerInvocation1InnerInvocation2Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + var outerInvocation2Id = outerClassTemplateId.append(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation2NestedClassTemplateId = outerInvocation2Id.append( + ClassTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, "NestedTestCase"); + var outerInvocation2InnerInvocation2Id = outerInvocation2NestedClassTemplateId.append( + ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation2InnerInvocation2NestedMethodId = outerInvocation2InnerInvocation2Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + + var results = executeTests(selectIteration(selectNestedClass(List.of(TwoTimesTwoInvocationsTestCase.class), + TwoTimesTwoInvocationsTestCase.NestedTestCase.class), 1)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(outerClassTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation1Id)), + displayName("[1] A of TwoTimesTwoInvocationsTestCase")), // + event(container(uniqueId(outerInvocation1Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation1NestedClassTemplateId))), // + event(container(uniqueId(outerInvocation1NestedClassTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation2Id)), + displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(outerInvocation1InnerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation2NestedMethodId))), // + event(test(uniqueId(outerInvocation1InnerInvocation2NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation1InnerInvocation2NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation1InnerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerInvocation1NestedClassTemplateId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation1Id)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2Id)), + displayName("[2] B of TwoTimesTwoInvocationsTestCase")), // + event(container(uniqueId(outerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2NestedClassTemplateId))), // + event(container(uniqueId(outerInvocation2NestedClassTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2Id)), + displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2NestedMethodId))), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerInvocation2NestedClassTemplateId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerClassTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void executesLifecycleCallbacksInNestedClassTemplates() { + var results = executeTestsForClass(TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase.class); + + results.containerEvents().assertStatistics(stats -> stats.started(10).succeeded(10)); + results.testEvents().assertStatistics(stats -> stats.started(8).succeeded(8)); + + // @formatter:off + assertThat(allReportEntryValues(results)).containsExactly( + "beforeAll: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", + "beforeClassTemplateInvocation: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", + "beforeAll: NestedTestCase", + "beforeClassTemplateInvocation: NestedTestCase", + "beforeEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test1 [NestedTestCase]", + "test1", + "afterEach: test1 [NestedTestCase]", + "afterEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [NestedTestCase]", + "test2", + "afterEach: test2 [NestedTestCase]", + "afterEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "afterClassTemplateInvocation: NestedTestCase", + "beforeClassTemplateInvocation: NestedTestCase", + "beforeEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test1 [NestedTestCase]", + "test1", + "afterEach: test1 [NestedTestCase]", + "afterEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [NestedTestCase]", + "test2", + "afterEach: test2 [NestedTestCase]", + "afterEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "afterClassTemplateInvocation: NestedTestCase", + "afterAll: NestedTestCase", + "afterClassTemplateInvocation: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", + "beforeClassTemplateInvocation: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", + "beforeAll: NestedTestCase", + "beforeClassTemplateInvocation: NestedTestCase", + "beforeEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test1 [NestedTestCase]", + "test1", + "afterEach: test1 [NestedTestCase]", + "afterEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [NestedTestCase]", + "test2", + "afterEach: test2 [NestedTestCase]", + "afterEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "afterClassTemplateInvocation: NestedTestCase", + "beforeClassTemplateInvocation: NestedTestCase", + "beforeEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test1 [NestedTestCase]", + "test1", + "afterEach: test1 [NestedTestCase]", + "afterEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [NestedTestCase]", + "test2", + "afterEach: test2 [NestedTestCase]", + "afterEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "afterClassTemplateInvocation: NestedTestCase", + "afterAll: NestedTestCase", + "afterClassTemplateInvocation: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", + "afterAll: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase" + ); + // @formatter:on + } + + @Test + void guaranteesWrappingBehaviorForCallbacks() { + var results = executeTestsForClass(CallbackWrappingBehaviorTestCase.class); + + results.containerEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)); + + // @formatter:off + assertThat(allReportEntryValues(results)).containsExactly( + "1st -> beforeClassTemplateInvocation: CallbackWrappingBehaviorTestCase", + "2nd -> beforeClassTemplateInvocation: CallbackWrappingBehaviorTestCase", + "test", + "2nd -> afterClassTemplateInvocation: CallbackWrappingBehaviorTestCase", + "1st -> afterClassTemplateInvocation: CallbackWrappingBehaviorTestCase", + "1st -> beforeClassTemplateInvocation: CallbackWrappingBehaviorTestCase", + "2nd -> beforeClassTemplateInvocation: CallbackWrappingBehaviorTestCase", + "test", + "2nd -> afterClassTemplateInvocation: CallbackWrappingBehaviorTestCase", + "1st -> afterClassTemplateInvocation: CallbackWrappingBehaviorTestCase" + ); + // @formatter:on + } + + @Test + void propagatesExceptionsFromCallbacks() { + + var results = executeTestsForClass(CallbackExceptionBehaviorTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).failed(2).succeeded(2)); + + results.containerEvents().assertThatEvents() // + .haveExactly(2, finishedWithFailure( // + message("2nd -> afterClassTemplateInvocation: CallbackExceptionBehaviorTestCase"), // + suppressed(0, message("1st -> beforeClassTemplateInvocation: CallbackExceptionBehaviorTestCase")), // + suppressed(1, message("1st -> afterClassTemplateInvocation: CallbackExceptionBehaviorTestCase")))); + + assertThat(allReportEntryValues(results).distinct()) // + .containsExactly("1st -> beforeClassTemplateInvocation: CallbackExceptionBehaviorTestCase", // + "2nd -> afterClassTemplateInvocation: CallbackExceptionBehaviorTestCase", // + "1st -> afterClassTemplateInvocation: CallbackExceptionBehaviorTestCase"); + } + + @Test + void templateWithPreparations() { + var results = executeTestsForClass(ClassTemplateWithPreparationsTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertTrue(CustomCloseableResource.closed, "resource in store was closed"); + } + + @Test + void propagatesTagsFromEnclosingClassesToNestedClassTemplates() { + var engineDescriptor = discoverTestsForClass( + NestedClassTemplateWithTagOnEnclosingClassTestCase.class).getEngineDescriptor(); + var classDescriptor = getOnlyElement(engineDescriptor.getChildren()); + var nestedClassTemplateDescriptor = getOnlyElement(classDescriptor.getChildren()); + + assertThat(classDescriptor.getTags()).extracting(TestTag::getName) // + .containsExactly("top-level"); + assertThat(nestedClassTemplateDescriptor.getTags()).extracting(TestTag::getName) // + .containsExactlyInAnyOrder("top-level", "nested"); + } + + // ------------------------------------------------------------------- + + private static Stream allReportEntryValues(EngineExecutionResults results) { + return results.allEvents().reportingEntryPublished() // + .map(event -> event.getRequiredPayload(ReportEntry.class)) // + .map(ReportEntry::getKeyValuePairs) // + .map(Map::values) // + .flatMap(Collection::stream); + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + static class TwoInvocationsTestCase { + @Test + void a() { + } + + @Nested + class NestedTestCase { + @Test + @Tag("nested") + void b() { + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + static class NestedClassTemplateWithTwoInvocationsTestCase { + @Test + void a() { + } + + @Nested + @ClassTemplate + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + class NestedTestCase { + @Test + void b() { + } + } + } + + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + @ClassTemplate + static class TwoTimesTwoInvocationsTestCase { + @Nested + @ClassTemplate + class NestedTestCase { + @Test + void test() { + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + static class TwoInvocationsWithExtensionTestCase { + @Test + void a() { + } + + @Nested + class NestedTestCase { + @Test + @Tag("nested") + void b() { + } + } + } + + static class TwoInvocationsClassTemplateInvocationContextProvider + implements ClassTemplateInvocationContextProvider { + + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideClassTemplateInvocationContexts(ExtensionContext context) { + var suffix = " of %s".formatted(context.getRequiredTestClass().getSimpleName()); + return Stream.of(new Ctx("A" + suffix), new Ctx("B" + suffix)); + } + + record Ctx(String displayName) implements ClassTemplateInvocationContext { + @Override + public String getDisplayName(int invocationIndex) { + var defaultDisplayName = ClassTemplateInvocationContext.super.getDisplayName(invocationIndex); + return "%s %s".formatted(defaultDisplayName, displayName); + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ExtendWith(AdditionalExtensionRegistrationTestCase.Ext.class) + static class AdditionalExtensionRegistrationTestCase { + + @Test + void test(Data data) { + assertNotNull(data); + assertNotNull(data.value()); + } + + static class Ext implements ClassTemplateInvocationContextProvider { + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideClassTemplateInvocationContexts(ExtensionContext context) { + return Stream.of(new Data("A"), new Data("B")).map(Ctx::new); + } + } + + record Ctx(Data data) implements ClassTemplateInvocationContext { + @Override + public String getDisplayName(int invocationIndex) { + return this.data.value(); + } + + @Override + public List getAdditionalExtensions() { + return List.of(new ParameterResolver() { + @Override + public boolean supportsParameter(ParameterContext parameterContext, + ExtensionContext extensionContext) throws ParameterResolutionException { + return Data.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return Ctx.this.data; + } + }); + } + } + + record Data(String value) { + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + @ExtendWith(SeparateExtensionContextTestCase.SomeResourceExtension.class) + static class SeparateExtensionContextTestCase { + + @Test + void test(SomeResource someResource) { + assertFalse(someResource.closed); + } + + static class SomeResourceExtension implements BeforeAllCallback, ParameterResolver { + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + context.getStore(Namespace.GLOBAL).put("someResource", new SomeResource()); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + var parentContext = extensionContext.getParent().orElseThrow(); + assertAll( // + () -> assertEquals(SeparateExtensionContextTestCase.class, parentContext.getRequiredTestClass()), // + () -> assertEquals(SeparateExtensionContextTestCase.class, + parentContext.getElement().orElseThrow()), // + () -> assertEquals(TestInstance.Lifecycle.PER_METHOD, + parentContext.getTestInstanceLifecycle().orElseThrow()) // + ); + return SomeResource.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return extensionContext.getStore(Namespace.GLOBAL).get("someResource"); + } + } + + static class SomeResource implements CloseableResource { + private boolean closed; + + @Override + public void close() { + this.closed = true; + } + } + } + + @ClassTemplate + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + static class CombinationWithTestTemplateTestCase { + + @ParameterizedTest + @ValueSource(ints = { 1, 2 }) + void test(int i) { + assertNotEquals(0, i); + } + } + + @ClassTemplate + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + static class CombinationWithTestFactoryTestCase { + + @TestFactory + Stream test() { + return IntStream.of(1, 2) // + .mapToObj(i -> dynamicTest("test" + i, () -> assertNotEquals(0, i))); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ExtendWith(InvalidZeroInvocationTestCase.Ext.class) + static class InvalidZeroInvocationTestCase { + + @Test + void test() { + fail("should not be called"); + } + + static class Ext implements ClassTemplateInvocationContextProvider { + + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideClassTemplateInvocationContexts( + ExtensionContext context) { + return Stream.empty(); + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ExtendWith(ValidZeroInvocationTestCase.Ext.class) + static class ValidZeroInvocationTestCase { + + @Test + void test() { + fail("should not be called"); + } + + static class Ext implements ClassTemplateInvocationContextProvider { + + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideClassTemplateInvocationContexts( + ExtensionContext context) { + return Stream.empty(); + } + + @Override + public boolean mayReturnZeroClassTemplateInvocationContexts(ExtensionContext context) { + return true; + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + static class NoProviderRegisteredTestCase { + + @Test + void test() { + fail("should not be called"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ExtendWith(NoSupportingProviderRegisteredTestCase.Ext.class) + static class NoSupportingProviderRegisteredTestCase { + + @Test + void test() { + fail("should not be called"); + } + + static class Ext implements ClassTemplateInvocationContextProvider { + + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return false; + } + + @Override + public Stream provideClassTemplateInvocationContexts( + ExtensionContext context) { + throw new RuntimeException("should not be called"); + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + @ClassTemplate + static class TwoTimesTwoInvocationsWithMultipleMethodsTestCase { + + @Test + void test() { + } + + @Nested + @ClassTemplate + class NestedTestCase { + @Test + void a() { + } + + @Test + void b() { + } + } + + @Nested + @ClassTemplate + class AnotherNestedTestCase { + @Test + void test() { + } + } + } + + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + @ExtendWith(ClassTemplateInvocationCallbacks.class) + @ClassTemplate + static class TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase extends LifecycleCallbacks { + + @Nested + @ClassTemplate + class NestedTestCase extends LifecycleCallbacks { + + @Test + @DisplayName("test1") + void test1(TestReporter testReporter) { + testReporter.publishEntry("test1"); + } + + @Test + @DisplayName("test2") + void test2(TestReporter testReporter) { + testReporter.publishEntry("test2"); + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + static class LifecycleCallbacks { + @BeforeAll + static void beforeAll(TestReporter testReporter, TestInfo testInfo) { + testReporter.publishEntry("beforeAll: " + testInfo.getTestClass().orElseThrow().getSimpleName()); + } + + @BeforeEach + void beforeEach(TestReporter testReporter, TestInfo testInfo) { + testReporter.publishEntry( + "beforeEach: " + testInfo.getDisplayName() + " [" + getClass().getSimpleName() + "]"); + } + + @AfterEach + void afterEach(TestReporter testReporter, TestInfo testInfo) { + testReporter.publishEntry( + "afterEach: " + testInfo.getDisplayName() + " [" + getClass().getSimpleName() + "]"); + } + + @AfterAll + static void afterAll(TestReporter testReporter, TestInfo testInfo) { + testReporter.publishEntry("afterAll: " + testInfo.getTestClass().orElseThrow().getSimpleName()); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ExtendWith({ PreparingClassTemplateInvocationContextProvider.class, CompanionExtension.class }) + static class ClassTemplateWithPreparationsTestCase { + + @Test + void test(CustomCloseableResource resource) { + assertNotNull(resource); + assertFalse(CustomCloseableResource.closed, "should not be closed yet"); + } + + } + + private static class PreparingClassTemplateInvocationContextProvider + implements ClassTemplateInvocationContextProvider { + + static final Namespace NAMESPACE = Namespace.create(PreparingClassTemplateInvocationContextProvider.class); + + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideClassTemplateInvocationContexts( + ExtensionContext context) { + var invocationContext = new PreparingClassTemplateInvocationContext(); + return Stream.of(invocationContext, invocationContext); + } + + } + + private static class PreparingClassTemplateInvocationContext implements ClassTemplateInvocationContext { + + @Override + public void prepareInvocation(ExtensionContext context) { + CustomCloseableResource.closed = false; + context.getStore(PreparingClassTemplateInvocationContextProvider.NAMESPACE) // + .put("resource", new CustomCloseableResource()); + } + + } + + private static class CompanionExtension implements ParameterResolver { + + @Override + public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { + return ExtensionContextScope.TEST_METHOD; + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return CustomCloseableResource.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return extensionContext.getStore(PreparingClassTemplateInvocationContextProvider.NAMESPACE).get("resource"); + } + + } + + private static class CustomCloseableResource implements CloseableResource { + + static boolean closed; + + @Override + public void close() { + closed = true; + } + + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + @ClassTemplate + static class CallbackWrappingBehaviorTestCase { + + @RegisterExtension + @Order(1) + static Extension first = new ClassTemplateInvocationCallbacks("1st -> "); + + @RegisterExtension + @Order(2) + static Extension second = new ClassTemplateInvocationCallbacks("2nd -> "); + + @Test + void test(TestReporter testReporter) { + testReporter.publishEntry("test"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + @ClassTemplate + static class CallbackExceptionBehaviorTestCase { + + @RegisterExtension + @Order(1) + static Extension first = new ClassTemplateInvocationCallbacks("1st -> ", TestAbortedException::new); + + @RegisterExtension + @Order(2) + static Extension second = new ClassTemplateInvocationCallbacks("2nd -> ", AssertionFailedError::new); + + @Test + void test() { + fail("should not be called"); + } + } + + static class ClassTemplateInvocationCallbacks + implements BeforeClassTemplateInvocationCallback, AfterClassTemplateInvocationCallback { + + private final String prefix; + private final Function exceptionFactory; + + @SuppressWarnings("unused") + ClassTemplateInvocationCallbacks() { + this(""); + } + + ClassTemplateInvocationCallbacks(String prefix) { + this(prefix, __ -> null); + } + + ClassTemplateInvocationCallbacks(String prefix, Function exceptionFactory) { + this.prefix = prefix; + this.exceptionFactory = exceptionFactory; + } + + @Override + public void beforeClassTemplateInvocation(ExtensionContext context) { + handle("beforeClassTemplateInvocation", context); + } + + @Override + public void afterClassTemplateInvocation(ExtensionContext context) { + handle("afterClassTemplateInvocation", context); + } + + private void handle(String methodName, ExtensionContext context) { + var message = format(methodName, context); + context.publishReportEntry(message); + var throwable = exceptionFactory.apply(message); + if (throwable != null) { + throw throwAsUncheckedException(throwable); + } + } + + private String format(String methodName, ExtensionContext context) { + return "%s%s: %s".formatted(prefix, methodName, context.getRequiredTestClass().getSimpleName()); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + static class InheritedTwoInvocationsTestCase extends TwoInvocationsTestCase { + @Test + void c() { + } + } + + @Tag("top-level") + static class NestedClassTemplateWithTagOnEnclosingClassTestCase { + @Nested + @ClassTemplate + @Tag("nested") + @ExtendWith(TwoInvocationsClassTemplateInvocationContextProvider.class) + class NestedTestCase { + @Test + void test() { + } + } + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/DefaultExecutionModeTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/DefaultExecutionModeTests.java index 57fdb111c7a7..c67182cd7283 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/DefaultExecutionModeTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/DefaultExecutionModeTests.java @@ -109,7 +109,7 @@ private JupiterEngineDescriptor discoverTestsWithDefaultExecutionMode(Class t if (executionMode != null) { request.configurationParameter(Constants.DEFAULT_PARALLEL_EXECUTION_MODE, executionMode.name()); } - return (JupiterEngineDescriptor) discoverTests(request.build()); + return (JupiterEngineDescriptor) discoverTests(request.build()).getEngineDescriptor(); } private static void assertExecutionMode(TestDescriptor testDescriptor, ExecutionMode expectedExecutionMode) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/DynamicNodeGenerationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/DynamicNodeGenerationTests.java index 30dcea34760c..c1284f58931a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/DynamicNodeGenerationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/DynamicNodeGenerationTests.java @@ -67,7 +67,7 @@ class DynamicNodeGenerationTests extends AbstractJupiterTestEngineTests { @Test void testFactoryMethodsAreCorrectlyDiscoveredForClassSelector() { LauncherDiscoveryRequest request = request().selectors(selectClass(MyDynamicTestCase.class)).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertThat(engineDescriptor.getDescendants()).as("# resolved test descriptors").hasSize(13); } @@ -75,7 +75,7 @@ void testFactoryMethodsAreCorrectlyDiscoveredForClassSelector() { void testFactoryMethodIsCorrectlyDiscoveredForMethodSelector() { LauncherDiscoveryRequest request = request().selectors( selectMethod(MyDynamicTestCase.class, "dynamicStream")).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertThat(engineDescriptor.getDescendants()).as("# resolved test descriptors").hasSize(2); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/InvalidLifecycleMethodConfigurationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/InvalidLifecycleMethodConfigurationTests.java index e1d7a47cfce6..a17b1fb8d4a6 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/InvalidLifecycleMethodConfigurationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/InvalidLifecycleMethodConfigurationTests.java @@ -10,82 +10,64 @@ package org.junit.jupiter.engine; -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 java.util.function.Predicate.isEqual; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.commons.util.FunctionUtils.where; + +import java.lang.annotation.Annotation; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.platform.testkit.engine.EngineExecutionResults; -import org.junit.platform.testkit.engine.Events; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; /** * Integration tests that verify proper handling of invalid configuration for * lifecycle methods in conjunction with the {@link JupiterTestEngine}. * - *

In general, configuration errors should not be thrown until the - * execution phase, thereby giving all containers a chance to execute. - * * @since 5.0 */ class InvalidLifecycleMethodConfigurationTests extends AbstractJupiterTestEngineTests { @Test void executeValidTestCaseAlongsideTestCaseWithInvalidNonStaticBeforeAllDeclaration() { - assertContainerFailed(TestCaseWithInvalidNonStaticBeforeAllMethod.class); + assertReportsError(TestCaseWithInvalidNonStaticBeforeAllMethod.class, BeforeAll.class); } @Test void executeValidTestCaseAlongsideTestCaseWithInvalidNonStaticAfterAllDeclaration() { - assertContainerFailed(TestCaseWithInvalidNonStaticAfterAllMethod.class); + assertReportsError(TestCaseWithInvalidNonStaticAfterAllMethod.class, AfterAll.class); } @Test void executeValidTestCaseAlongsideTestCaseWithInvalidStaticBeforeEachDeclaration() { - assertContainerFailed(TestCaseWithInvalidStaticBeforeEachMethod.class); + assertReportsError(TestCaseWithInvalidStaticBeforeEachMethod.class, BeforeEach.class); } @Test void executeValidTestCaseAlongsideTestCaseWithInvalidStaticAfterEachDeclaration() { - assertContainerFailed(TestCaseWithInvalidStaticAfterEachMethod.class); + assertReportsError(TestCaseWithInvalidStaticAfterEachMethod.class, AfterEach.class); } - private void assertContainerFailed(Class invalidTestClass) { - EngineExecutionResults executionResults = executeTests(selectClass(TestCase.class), - selectClass(invalidTestClass)); - Events containers = executionResults.containerEvents(); - Events tests = executionResults.testEvents(); - - // @formatter:off - assertAll( - () -> assertEquals(3, containers.started().count(), "# containers started"), - () -> assertEquals(1, tests.started().count(), "# tests started"), - () -> assertEquals(1, tests.succeeded().count(), "# tests succeeded"), - () -> assertEquals(0, tests.failed().count(), "# tests failed"), - () -> assertEquals(3, containers.finished().count(), "# containers finished"), - () -> assertEquals(1, containers.failed().count(), "# containers failed") - ); - // @formatter:on + private void assertReportsError(Class invalidTestClass, Class annotationType) { + var results = discoverTestsForClass(invalidTestClass); + + assertThat(results.getDiscoveryIssues()) // + .filteredOn(where(DiscoveryIssue::severity, isEqual(Severity.ERROR))) // + .extracting(DiscoveryIssue::message) // + .asString().contains("@%s method".formatted(annotationType.getSimpleName())); } // ------------------------------------------------------------------------- - @SuppressWarnings("JUnitMalformedDeclaration") - static class TestCase { - - @Test - void test() { - } - } - @SuppressWarnings("JUnitMalformedDeclaration") static class TestCaseWithInvalidNonStaticBeforeAllMethod { // must be static - @SuppressWarnings("JUnitMalformedDeclaration") + @SuppressWarnings("unused") @BeforeAll void beforeAll() { } @@ -99,7 +81,7 @@ void test() { static class TestCaseWithInvalidNonStaticAfterAllMethod { // must be static - @SuppressWarnings("JUnitMalformedDeclaration") + @SuppressWarnings("unused") @AfterAll void afterAll() { } @@ -113,7 +95,7 @@ void test() { static class TestCaseWithInvalidStaticBeforeEachMethod { // must NOT be static - @SuppressWarnings("JUnitMalformedDeclaration") + @SuppressWarnings("unused") @BeforeEach static void beforeEach() { } @@ -127,7 +109,7 @@ void test() { static class TestCaseWithInvalidStaticAfterEachMethod { // must NOT be static - @SuppressWarnings("JUnitMalformedDeclaration") + @SuppressWarnings("unused") @AfterEach static void afterEach() { } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/MultipleTestableAnnotationsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/MultipleTestableAnnotationsTests.java index 6aa1fc0e1c84..a03a569a0c76 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/MultipleTestableAnnotationsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/MultipleTestableAnnotationsTests.java @@ -10,18 +10,15 @@ package org.junit.jupiter.engine; -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 java.util.logging.Level; -import java.util.logging.LogRecord; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.RepetitionInfo; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.fixtures.TrackLogRecords; -import org.junit.platform.commons.logging.LogRecordListener; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.support.descriptor.MethodSource; /** * Integration tests that verify the correct behavior for methods annotated @@ -32,23 +29,27 @@ class MultipleTestableAnnotationsTests extends AbstractJupiterTestEngineTests { @Test - void testAndRepeatedTest(@TrackLogRecords LogRecordListener listener) { - discoverTests(request().selectors(selectClass(TestCase.class)).build()); - - // @formatter:off - assertTrue(listener.stream(Level.WARNING) - .map(LogRecord::getMessage) - .anyMatch(m -> m.matches("Possible configuration error: method .+ resulted in multiple TestDescriptors .+"))); - // @formatter:on + void testAndRepeatedTest() throws Exception { + var results = discoverTestsForClass(TestCase.class); + + var discoveryIssue = getOnlyElement(results.getDiscoveryIssues()); + + assertThat(discoveryIssue.severity()) // + .isEqualTo(Severity.WARNING); + assertThat(discoveryIssue.message()) // + .matches("Possible configuration error: method .+ resulted in multiple TestDescriptors .+"); + assertThat(discoveryIssue.source()) // + .contains( + MethodSource.from(TestCase.class.getDeclaredMethod("testAndRepeatedTest", RepetitionInfo.class))); } @SuppressWarnings("JUnitMalformedDeclaration") static class TestCase { - @SuppressWarnings("JUnitMalformedDeclaration") @Test @RepeatedTest(1) void testAndRepeatedTest(RepetitionInfo repetitionInfo) { + assertNotNull(repetitionInfo); } } 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 634aa46c34de..328ef26a73db 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,14 @@ package org.junit.jupiter.engine; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.util.Throwables.getRootCause; 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.core.LauncherDiscoveryRequestBuilder.request; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -28,7 +27,6 @@ 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.commons.JUnitException; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.testkit.engine.EngineExecutionResults; @@ -45,7 +43,7 @@ class NestedTestClassesTests extends AbstractJupiterTestEngineTests { @Test void nestedTestsAreCorrectlyDiscovered() { LauncherDiscoveryRequest request = request().selectors(selectClass(TestCaseWithNesting.class)).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertEquals(5, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -66,7 +64,7 @@ void nestedTestsAreExecuted() { @Test void doublyNestedTestsAreCorrectlyDiscovered() { LauncherDiscoveryRequest request = request().selectors(selectClass(TestCaseWithDoubleNesting.class)).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertEquals(8, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -111,8 +109,7 @@ void inheritedNestedTestsAreExecuted() { @Test void extendedNestedTestsAreExecuted() { - EngineExecutionResults executionResults = executeTestsForClass(TestCaseWithExtendedNested.class); - executionResults.allEvents().debug(); + var executionResults = executeTestsForClass(TestCaseWithExtendedNested.class); Events containers = executionResults.containerEvents(); Events tests = executionResults.testEvents(); @@ -126,11 +123,10 @@ void extendedNestedTestsAreExecuted() { @Test void deeplyNestedInheritedMethodsAreExecutedWhenSelectedViaUniqueId() { - EngineExecutionResults executionResults = executeTests(selectUniqueId( + 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()]"), selectUniqueId( "[engine:junit-jupiter]/[class:org.junit.jupiter.engine.NestedTestClassesTests$TestCaseWithExtendedNested]/[nested-class:ConcreteInner2]/[nested-class:NestedInAbstractClass]/[nested-class:SecondLevelInherited]/[method:test()]")); - executionResults.allEvents().debug(); Events containers = executionResults.containerEvents(); Events tests = executionResults.testEvents(); @@ -180,12 +176,12 @@ void individualMethodsWithinRecursiveNestedTestClassHierarchiesAreExecuted() { } private void assertNestedCycle(Class start, Class from, Class to) { - assertThatExceptionOfType(JUnitException.class)// - .isThrownBy(() -> executeTestsForClass(start))// - .withCauseExactlyInstanceOf(JUnitException.class)// - .satisfies(ex -> assertThat(getRootCause(ex)).hasMessageMatching( - String.format("Detected cycle in inner class hierarchy between .+%s and .+%s", from.getSimpleName(), - to.getSimpleName()))); + var results = executeTestsForClass(start); + var expectedMessage = String.format( + "Cause: org.junit.platform.commons.JUnitException: Detected cycle in inner class hierarchy between %s and %s", + from.getName(), to.getName()); + results.containerEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message(it -> it.contains(expectedMessage)))); } // ------------------------------------------------------------------- diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/StandardTestClassTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/StandardTestClassTests.java index 18459ff98194..1d458e174f32 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/StandardTestClassTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/StandardTestClassTests.java @@ -37,7 +37,7 @@ class StandardTestClassTests extends AbstractJupiterTestEngineTests { @Test void standardTestClassIsCorrectlyDiscovered() { LauncherDiscoveryRequest request = request().selectors(selectClass(MyStandardTestCase.class)).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertEquals(1 /*class*/ + 6 /*methods*/, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -45,7 +45,7 @@ void standardTestClassIsCorrectlyDiscovered() { @Test void moreThanOneTestClassIsCorrectlyDiscovered() { LauncherDiscoveryRequest request = request().selectors(selectClass(SecondOfTwoTestCases.class)).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertEquals(1 /*class*/ + 3 /*methods*/, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleConfigurationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleConfigurationTests.java index fdb7e512ce75..8090690f32f6 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleConfigurationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleConfigurationTests.java @@ -73,7 +73,7 @@ void instancePerClassConfiguredViaSystemProperty() { Class testClass = AssumedInstancePerClassTestCase.class; // Should fail by default... - performAssertions(testClass, 2, 1, 0); + performAssertions(testClass, 1, 1, 0); // Should pass with the system property set System.setProperty(KEY, PER_CLASS.name()); @@ -85,7 +85,7 @@ void instancePerClassConfiguredViaConfigParam() { Class testClass = AssumedInstancePerClassTestCase.class; // Should fail by default... - performAssertions(testClass, 2, 1, 0); + performAssertions(testClass, 1, 1, 0); // Should pass with the config param performAssertions(testClass, singletonMap(KEY, PER_CLASS.name()), 2, 0, 1, "beforeAll", "test", "afterAll"); @@ -97,7 +97,7 @@ void instancePerClassConfiguredViaConfigParamThatOverridesSystemProperty() { // Should fail with system property System.setProperty(KEY, PER_METHOD.name()); - performAssertions(testClass, 2, 1, 0); + performAssertions(testClass, 1, 1, 0); // Should pass with the config param performAssertions(testClass, singletonMap(KEY, PER_CLASS.name()), 2, 0, 1, "beforeAll", "test", "afterAll"); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java index 91676086da86..a360f0c8306e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -30,12 +31,14 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Stream; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.ClassTemplate; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @@ -46,6 +49,8 @@ import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContextProvider; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtendWith; @@ -56,6 +61,8 @@ import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; import org.junit.jupiter.engine.execution.DefaultTestInstances; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.platform.testkit.engine.EngineExecutionResults; /** @@ -106,7 +113,7 @@ void instancePerMethod() { String beforeAllCallbackKey = beforeAllCallbackKey(testClass); String afterAllCallbackKey = afterAllCallbackKey(testClass); String testTemplateKey = testTemplateKey(testClass, "singletonTest"); - String testExecutionConditionKey1 = executionConditionKey(testClass, testsInvoked.get(0)); + String testExecutionConditionKey1 = executionConditionKey(testClass, testsInvoked.getFirst()); String beforeEachCallbackKey1 = beforeEachCallbackKey(testClass, testsInvoked.get(0)); String afterEachCallbackKey1 = afterEachCallbackKey(testClass, testsInvoked.get(0)); String testExecutionConditionKey2 = executionConditionKey(testClass, testsInvoked.get(1)); @@ -188,7 +195,7 @@ private void instancePerClass(Class testClass, Map.Entry, Integer>[] String preDestroyCallbackTestInstanceKey = preDestroyCallbackTestInstanceKey(testClass); String beforeAllCallbackKey = beforeAllCallbackKey(testClass); String afterAllCallbackKey = afterAllCallbackKey(testClass); - String testExecutionConditionKey1 = executionConditionKey(testClass, testsInvoked.get(0)); + String testExecutionConditionKey1 = executionConditionKey(testClass, testsInvoked.getFirst()); String beforeEachCallbackKey1 = beforeEachCallbackKey(testClass, testsInvoked.get(0)); String afterEachCallbackKey1 = afterEachCallbackKey(testClass, testsInvoked.get(0)); String testExecutionConditionKey2 = executionConditionKey(testClass, testsInvoked.get(1)); @@ -267,7 +274,7 @@ void instancePerMethodWithNestedTestClass() { String afterEachCallbackKey = afterEachCallbackKey(testClass, "outerTest"); String nestedBeforeAllCallbackKey = beforeAllCallbackKey(nestedTestClass); String nestedAfterAllCallbackKey = afterAllCallbackKey(nestedTestClass); - String nestedExecutionConditionKey1 = executionConditionKey(nestedTestClass, testsInvoked.get(0)); + String nestedExecutionConditionKey1 = executionConditionKey(nestedTestClass, testsInvoked.getFirst()); String nestedBeforeEachCallbackKey1 = beforeEachCallbackKey(nestedTestClass, testsInvoked.get(0)); String nestedAfterEachCallbackKey1 = afterEachCallbackKey(nestedTestClass, testsInvoked.get(0)); String nestedExecutionConditionKey2 = executionConditionKey(nestedTestClass, testsInvoked.get(1)); @@ -389,7 +396,7 @@ void instancePerClassWithNestedTestClass() { String afterEachCallbackKey = afterEachCallbackKey(testClass, "outerTest"); String nestedBeforeAllCallbackKey = beforeAllCallbackKey(nestedTestClass); String nestedAfterAllCallbackKey = afterAllCallbackKey(nestedTestClass); - String nestedExecutionConditionKey1 = executionConditionKey(nestedTestClass, testsInvoked.get(0)); + String nestedExecutionConditionKey1 = executionConditionKey(nestedTestClass, testsInvoked.getFirst()); String nestedBeforeEachCallbackKey1 = beforeEachCallbackKey(nestedTestClass, testsInvoked.get(0)); String nestedAfterEachCallbackKey1 = afterEachCallbackKey(nestedTestClass, testsInvoked.get(0)); String nestedExecutionConditionKey2 = executionConditionKey(nestedTestClass, testsInvoked.get(1)); @@ -509,7 +516,7 @@ void instancePerMethodOnOuterTestClassWithInstancePerClassOnNestedTestClass() { String afterEachCallbackKey = afterEachCallbackKey(testClass, "outerTest"); String nestedBeforeAllCallbackKey = beforeAllCallbackKey(nestedTestClass); String nestedAfterAllCallbackKey = afterAllCallbackKey(nestedTestClass); - String nestedExecutionConditionKey1 = executionConditionKey(nestedTestClass, testsInvoked.get(0)); + String nestedExecutionConditionKey1 = executionConditionKey(nestedTestClass, testsInvoked.getFirst()); String nestedBeforeEachCallbackKey1 = beforeEachCallbackKey(nestedTestClass, testsInvoked.get(0)); String nestedAfterEachCallbackKey1 = afterEachCallbackKey(nestedTestClass, testsInvoked.get(0)); String nestedExecutionConditionKey2 = executionConditionKey(nestedTestClass, testsInvoked.get(1)); @@ -601,6 +608,44 @@ void instancePerMethodOnOuterTestClassWithInstancePerClassOnNestedTestClass() { assertThat(lifecyclesMap.get(nestedTestClass).stream()).allMatch(Lifecycle.PER_CLASS::equals); } + @ParameterizedTest + @EnumSource(Lifecycle.class) + void classTemplate(Lifecycle lifecycle) { + var classTemplate = ClassTemplateWithDefaultLifecycleTestCase.class; + + var results = executeTests(r -> r // + .selectors(selectClass(classTemplate)) // + .configurationParameter(Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME, lifecycle.name())); + + results.allEvents().assertStatistics(stats -> stats.failed(0)); + results.testEvents().assertStatistics(stats -> stats.succeeded(4)); + + assertThat(instanceCount).containsExactly(entry(classTemplate, lifecycle == Lifecycle.PER_CLASS ? 1 : 4)); + assertThat(lifecyclesMap.keySet()).containsExactly(classTemplate); + assertThat(lifecyclesMap.get(classTemplate)).filteredOn(Objects::nonNull).containsOnly(lifecycle); + } + + @ParameterizedTest + @EnumSource(Lifecycle.class) + void classTemplateWithNestedClass(Lifecycle lifecycle) { + var classTemplate = ClassTemplateWithDefaultLifecycleAndNestedClassTestCase.class; + var nestedClass = ClassTemplateWithDefaultLifecycleAndNestedClassTestCase.InnerTestCase.class; + + var results = executeTests(r -> r // + .selectors(selectClass(classTemplate)) // + .configurationParameter(Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME, lifecycle.name())); + + results.allEvents().assertStatistics(stats -> stats.failed(0)); + results.testEvents().assertStatistics(stats -> stats.succeeded(4)); + + assertThat(instanceCount).containsExactly( // + entry(classTemplate, lifecycle == Lifecycle.PER_CLASS ? 1 : 4), // + entry(nestedClass, lifecycle == Lifecycle.PER_CLASS ? 2 : 4)); + assertThat(lifecyclesMap.keySet()).containsExactlyInAnyOrder(classTemplate, nestedClass); + assertThat(lifecyclesMap.get(classTemplate)).filteredOn(Objects::nonNull).containsOnly(lifecycle); + assertThat(lifecyclesMap.get(nestedClass)).filteredOn(Objects::nonNull).containsOnly(lifecycle); + } + private void performAssertions(Class testClass, int numContainers, int numTests, Map.Entry, Integer>[] instanceCountEntries, int allMethods, int eachMethods) { @@ -623,7 +668,7 @@ private void performAssertions(Class testClass, int numContainers, int numTes @SafeVarargs @SuppressWarnings("varargs") - private final Map.Entry, Integer>[] instanceCounts(Map.Entry, Integer>... entries) { + private Map.Entry, Integer>[] instanceCounts(Map.Entry, Integer>... entries) { return entries; } @@ -983,7 +1028,9 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con String testMethod = context.getTestMethod().map(Method::getName).orElse(null); if (testMethod == null) { assertThat(context.getTestInstance()).isNotPresent(); - assertThat(instanceCount.getOrDefault(context.getRequiredTestClass(), 0)).isEqualTo(0); + if (!isAnnotated(context.getRequiredTestClass().getEnclosingClass(), ClassTemplate.class)) { + assertThat(instanceCount.getOrDefault(context.getRequiredTestClass(), 0)).isEqualTo(0); + } } instanceMap.put(executionConditionKey(context.getRequiredTestClass(), testMethod), context.getTestInstances().orElse(null)); @@ -1067,4 +1114,65 @@ private static void trackLifecycle(ExtensionContext context) { @interface SingletonTest { } + @SuppressWarnings("JUnitMalformedDeclaration") + @ClassTemplate + @ExtendWith(Twice.class) + @ExtendWith(InstanceTrackingExtension.class) + static class ClassTemplateWithDefaultLifecycleTestCase { + + ClassTemplateWithDefaultLifecycleTestCase() { + incrementInstanceCount(ClassTemplateWithDefaultLifecycleTestCase.class); + } + + @Test + void test1() { + } + + @Test + void test2() { + } + } + + @ClassTemplate + @ExtendWith(Twice.class) + @ExtendWith(InstanceTrackingExtension.class) + static class ClassTemplateWithDefaultLifecycleAndNestedClassTestCase { + + ClassTemplateWithDefaultLifecycleAndNestedClassTestCase() { + incrementInstanceCount(ClassTemplateWithDefaultLifecycleAndNestedClassTestCase.class); + } + + @Nested + class InnerTestCase { + + public InnerTestCase() { + incrementInstanceCount(InnerTestCase.class); + } + + @Test + void test1() { + } + + @Test + void test2() { + } + } + } + + private static class Twice implements ClassTemplateInvocationContextProvider { + + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideClassTemplateInvocationContexts(ExtensionContext context) { + return Stream.of(new Ctx(), new Ctx()); + } + + private record Ctx() implements ClassTemplateInvocationContext { + } + } + } 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 b9427e78e6e6..677df08911b0 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 @@ -14,6 +14,9 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; @@ -57,6 +60,8 @@ import org.junit.jupiter.api.extension.ExtendWith; 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; @@ -408,6 +413,14 @@ void templateWithCloseableStream() { event(container("templateWithCloseableStream"), finishedSuccessfully()))); } + @Test + void templateWithPreparations() { + var results = executeTestsForClass(TestTemplateWithPreparationsTestCase.class); + + assertTrue(CustomCloseableResource.closed, "resource in store was closed"); + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + private TestDescriptor findTestDescriptor(EngineExecutionResults executionResults, Condition condition) { // @formatter:off return executionResults.allEvents() @@ -852,4 +865,74 @@ private static TestTemplateInvocationContext emptyTestTemplateInvocationContext( }; } + static class TestTemplateWithPreparationsTestCase { + + @TestTemplate + @ExtendWith({ PreparingTestTemplateInvocationContextProvider.class, CompanionExtension.class }) + void test(CustomCloseableResource resource) { + assertNotNull(resource); + assertFalse(CustomCloseableResource.closed, "should not be closed yet"); + } + + } + + private static class PreparingTestTemplateInvocationContextProvider + implements TestTemplateInvocationContextProvider { + + static final Namespace NAMESPACE = Namespace.create(PreparingTestTemplateInvocationContextProvider.class); + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + return Stream.of(new PreparingTestTemplateInvocationContext()); + } + + } + + private static class PreparingTestTemplateInvocationContext implements TestTemplateInvocationContext { + + @Override + public void prepareInvocation(ExtensionContext context) { + context.getStore(PreparingTestTemplateInvocationContextProvider.NAMESPACE) // + .put("resource", new CustomCloseableResource()); + } + + } + + private static class CompanionExtension implements ParameterResolver { + + @Override + public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { + return ExtensionContextScope.TEST_METHOD; + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return CustomCloseableResource.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return extensionContext.getStore(PreparingTestTemplateInvocationContextProvider.NAMESPACE).get("resource"); + } + + } + + private static class CustomCloseableResource implements CloseableResource { + + static boolean closed; + + @Override + public void close() { + closed = true; + } + + } + } 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 7a9ca9a4a165..eb7b16f74964 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 @@ -56,7 +56,7 @@ void shouldGetDisplayNameFromSupplierIfNoDisplayNameAnnotationWithBlankStringPre 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-empty value."); + "Configuration error: @DisplayName on [class org.junit.jupiter.engine.descriptor.DisplayNameUtilsTests$BlankDisplayNameTestCase] must be declared with a non-blank value."); } @Test 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 48682ff17e7a..ee9716201092 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 @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.junit.platform.launcher.core.OutputDirectoryProviders.hierarchicalOutputDirectoryProvider; import static org.mockito.ArgumentMatchers.eq; @@ -39,6 +40,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGenerator; 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.extension.Extension; @@ -111,40 +113,70 @@ void fromJupiterEngineDescriptor() { } @Test - @SuppressWarnings("resource") void fromClassTestDescriptor() { var nestedClassDescriptor = nestedClassDescriptor(); var outerClassDescriptor = outerClassDescriptor(nestedClassDescriptor); + var doublyNestedClassDescriptor = doublyNestedClassDescriptor(); + var methodTestDescriptor = nestedMethodDescriptor(); + nestedClassDescriptor.addChild(doublyNestedClassDescriptor); + nestedClassDescriptor.addChild(methodTestDescriptor); - var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, configuration, - extensionRegistry, null); + var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD, + configuration, extensionRegistry, null); // @formatter:off assertAll("outerContext", - () -> assertThat(outerExtensionContext.getElement()).contains(OuterClass.class), - () -> assertThat(outerExtensionContext.getTestClass()).contains(OuterClass.class), + () -> assertThat(outerExtensionContext.getElement()).contains(OuterClassTestCase.class), + () -> assertThat(outerExtensionContext.getTestClass()).contains(OuterClassTestCase.class), () -> assertThat(outerExtensionContext.getTestInstance()).isEmpty(), () -> assertThat(outerExtensionContext.getTestMethod()).isEmpty(), - () -> assertThat(outerExtensionContext.getRequiredTestClass()).isEqualTo(OuterClass.class), + () -> assertThat(outerExtensionContext.getRequiredTestClass()).isEqualTo(OuterClassTestCase.class), () -> assertThrows(PreconditionViolationException.class, outerExtensionContext::getRequiredTestInstance), () -> assertThrows(PreconditionViolationException.class, outerExtensionContext::getRequiredTestMethod), () -> assertThat(outerExtensionContext.getDisplayName()).isEqualTo(outerClassDescriptor.getDisplayName()), () -> assertThat(outerExtensionContext.getParent()).isEmpty(), () -> assertThat(outerExtensionContext.getExecutionMode()).isEqualTo(ExecutionMode.SAME_THREAD), - () -> assertThat(outerExtensionContext.getExtensions(PreInterruptCallback.class)).isEmpty() + () -> assertThat(outerExtensionContext.getExtensions(PreInterruptCallback.class)).isEmpty(), + () -> assertThat(outerExtensionContext.getEnclosingTestClasses()).isEmpty() ); // @formatter:on var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor, - configuration, extensionRegistry, null); - assertThat(nestedExtensionContext.getParent()).containsSame(outerExtensionContext); + PER_METHOD, configuration, extensionRegistry, null); + // @formatter:off + assertAll("nestedContext", + () -> assertThat(nestedExtensionContext.getParent()).containsSame(outerExtensionContext), + () -> assertThat(nestedExtensionContext.getTestClass()).contains(OuterClassTestCase.NestedClass.class), + () -> assertThat(nestedExtensionContext.getEnclosingTestClasses()).containsExactly(OuterClassTestCase.class) + ); + // @formatter:on + + var doublyNestedExtensionContext = new ClassExtensionContext(nestedExtensionContext, null, + doublyNestedClassDescriptor, PER_METHOD, configuration, extensionRegistry, null); + // @formatter:off + assertAll("doublyNestedContext", + () -> assertThat(doublyNestedExtensionContext.getParent()).containsSame(nestedExtensionContext), + () -> assertThat(doublyNestedExtensionContext.getTestClass()).contains(OuterClassTestCase.NestedClass.DoublyNestedClass.class), + () -> assertThat(doublyNestedExtensionContext.getEnclosingTestClasses()).containsExactly(OuterClassTestCase.class, OuterClassTestCase.NestedClass.class) + ); + // @formatter:on + + var methodExtensionContext = new MethodExtensionContext(nestedExtensionContext, null, methodTestDescriptor, + configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); + // @formatter:off + assertAll("methodContext", + () -> assertThat(methodExtensionContext.getParent()).containsSame(nestedExtensionContext), + () -> assertThat(methodExtensionContext.getTestClass()).contains(OuterClassTestCase.NestedClass.class), + () -> assertThat(methodExtensionContext.getEnclosingTestClasses()).containsExactly(OuterClassTestCase.class) + ); + // @formatter:on } @Test void ExtensionContext_With_ExtensionRegistry_getExtensions() { var classTestDescriptor = nestedClassDescriptor(); - try (var ctx = new ClassExtensionContext(null, null, classTestDescriptor, configuration, extensionRegistry, - null)) { + try (var ctx = new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, configuration, + extensionRegistry, null)) { Extension ext = mock(); when(extensionRegistry.getExtensions(Extension.class)).thenReturn(List.of(ext)); @@ -154,46 +186,44 @@ void ExtensionContext_With_ExtensionRegistry_getExtensions() { } @Test - @SuppressWarnings("resource") void tagsCanBeRetrievedInExtensionContext() { var nestedClassDescriptor = nestedClassDescriptor(); var outerClassDescriptor = outerClassDescriptor(nestedClassDescriptor); var methodTestDescriptor = methodDescriptor(); outerClassDescriptor.addChild(methodTestDescriptor); - var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, configuration, - extensionRegistry, null); + var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD, + configuration, extensionRegistry, null); assertThat(outerExtensionContext.getTags()).containsExactly("outer-tag"); assertThat(outerExtensionContext.getRoot()).isSameAs(outerExtensionContext); var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor, - configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, 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()); - methodExtensionContext.setTestInstances(DefaultTestInstances.of(new OuterClass())); + 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); var engineDescriptor = new JupiterEngineDescriptor(UniqueId.forEngine("junit-jupiter"), configuration); engineDescriptor.addChild(classTestDescriptor); - Object testInstance = new OuterClass(); + Object testInstance = new OuterClassTestCase(); var testMethod = methodTestDescriptor.getTestMethod(); var engineExtensionContext = new JupiterEngineExtensionContext(null, engineDescriptor, configuration, extensionRegistry); var classExtensionContext = new ClassExtensionContext(engineExtensionContext, null, classTestDescriptor, - configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, null); var methodExtensionContext = new MethodExtensionContext(classExtensionContext, null, methodTestDescriptor, configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); methodExtensionContext.setTestInstances(DefaultTestInstances.of(testInstance)); @@ -201,10 +231,11 @@ void fromMethodTestDescriptor() { // @formatter:off assertAll("methodContext", () -> assertThat(methodExtensionContext.getElement()).contains(testMethod), - () -> assertThat(methodExtensionContext.getTestClass()).contains(OuterClass.class), + () -> assertThat(methodExtensionContext.getTestClass()).contains(OuterClassTestCase.class), + () -> assertThat(methodExtensionContext.getEnclosingTestClasses()).isEmpty(), () -> assertThat(methodExtensionContext.getTestInstance()).contains(testInstance), () -> assertThat(methodExtensionContext.getTestMethod()).contains(testMethod), - () -> assertThat(methodExtensionContext.getRequiredTestClass()).isEqualTo(OuterClass.class), + () -> assertThat(methodExtensionContext.getRequiredTestClass()).isEqualTo(OuterClassTestCase.class), () -> assertThat(methodExtensionContext.getRequiredTestInstance()).isEqualTo(testInstance), () -> assertThat(methodExtensionContext.getRequiredTestMethod()).isEqualTo(testMethod), () -> assertThat(methodExtensionContext.getDisplayName()).isEqualTo(methodTestDescriptor.getDisplayName()), @@ -221,7 +252,7 @@ void reportEntriesArePublishedToExecutionListener() { var classTestDescriptor = outerClassDescriptor(null); var engineExecutionListener = spy(EngineExecutionListener.class); ExtensionContext extensionContext = new ClassExtensionContext(null, engineExecutionListener, - classTestDescriptor, configuration, extensionRegistry, null); + classTestDescriptor, PER_METHOD, configuration, extensionRegistry, null); var map1 = Collections.singletonMap("key", "value"); var map2 = Collections.singletonMap("other key", "other value"); @@ -346,7 +377,7 @@ private ExtensionContext createExtensionContextForFilePublishing(Path tempDir, EngineExecutionListener engineExecutionListener, ClassTestDescriptor classTestDescriptor) { when(configuration.getOutputDirectoryProvider()) // .thenReturn(hierarchicalOutputDirectoryProvider(tempDir)); - return new ClassExtensionContext(null, engineExecutionListener, classTestDescriptor, configuration, + return new ClassExtensionContext(null, engineExecutionListener, classTestDescriptor, PER_METHOD, configuration, extensionRegistry, null); } @@ -355,11 +386,11 @@ private ExtensionContext createExtensionContextForFilePublishing(Path tempDir, void usingStore() { var methodTestDescriptor = methodDescriptor(); var classTestDescriptor = outerClassDescriptor(methodTestDescriptor); - ExtensionContext parentContext = new ClassExtensionContext(null, null, classTestDescriptor, configuration, - extensionRegistry, null); + ExtensionContext parentContext = new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, + configuration, extensionRegistry, null); var childContext = new MethodExtensionContext(parentContext, null, methodTestDescriptor, configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); - childContext.setTestInstances(DefaultTestInstances.of(new OuterClass())); + childContext.setTestInstances(DefaultTestInstances.of(new OuterClassTestCase())); var childStore = childContext.getStore(Namespace.GLOBAL); var parentStore = parentContext.getStore(Namespace.GLOBAL); @@ -415,8 +446,8 @@ void configurationParameter(Function { var classUniqueId = UniqueId.parse("[engine:junit-jupiter]/[class:MyClass]"); var classTestDescriptor = new ClassTestDescriptor(classUniqueId, testClass, configuration); - return new ClassExtensionContext(null, null, classTestDescriptor, configuration, extensionRegistry, - null); + return new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, configuration, + extensionRegistry, null); }), // named("method", (JupiterConfiguration configuration) -> { var method = ReflectionSupport.findMethod(testClass, "extensionContextFactories").orElseThrow(); @@ -430,13 +461,18 @@ void configurationParameter(Function discoveryIssues = new ArrayList<>(); + @Test - void findNonVoidBeforeAllMethodsWithStandardLifecycle() { - JUnitException exception = assertThrows(JUnitException.class, - () -> findBeforeAllMethods(TestCaseWithNonVoidLifecyleMethods.class, true)); - assertEquals( - "@BeforeAll method 'java.lang.Double org.junit.jupiter.engine.descriptor.TestCaseWithNonVoidLifecyleMethods.cc()' must not return a value.", - exception.getMessage()); + void findNonVoidBeforeAllMethodsWithStandardLifecycle() throws Exception { + var methods = findBeforeAllMethods(TestCaseWithInvalidLifecycleMethods.class, true, discoveryIssues::add); + assertThat(methods).isEmpty(); + + var methodSource = MethodSource.from(TestCaseWithInvalidLifecycleMethods.class.getDeclaredMethod("cc")); + var notVoidIssue = DiscoveryIssue.builder(Severity.ERROR, + "@BeforeAll method 'private java.lang.Double org.junit.jupiter.engine.descriptor.TestCaseWithInvalidLifecycleMethods.cc()' must not return a value.") // + .source(methodSource) // + .build(); + var notStaticIssue = DiscoveryIssue.builder(Severity.ERROR, + "@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, + "@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(); + assertThat(discoveryIssues).containsExactlyInAnyOrder(notVoidIssue, notStaticIssue, privateIssue); } @Test - void findNonVoidAfterAllMethodsWithStandardLifecycle() { - JUnitException exception = assertThrows(JUnitException.class, - () -> findAfterAllMethods(TestCaseWithNonVoidLifecyleMethods.class, true)); - assertEquals( - "@AfterAll method 'java.lang.String org.junit.jupiter.engine.descriptor.TestCaseWithNonVoidLifecyleMethods.dd()' must not return a value.", - exception.getMessage()); + void findNonVoidAfterAllMethodsWithStandardLifecycle() throws Exception { + var methods = findAfterAllMethods(TestCaseWithInvalidLifecycleMethods.class, true, discoveryIssues::add); + assertThat(methods).isEmpty(); + + var methodSource = MethodSource.from(TestCaseWithInvalidLifecycleMethods.class.getDeclaredMethod("dd")); + var notVoidIssue = DiscoveryIssue.builder(Severity.ERROR, + "@AfterAll method 'private java.lang.String org.junit.jupiter.engine.descriptor.TestCaseWithInvalidLifecycleMethods.dd()' must not return a value.") // + .source(methodSource) // + .build(); + var notStaticIssue = DiscoveryIssue.builder(Severity.ERROR, + "@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, + "@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(); + assertThat(discoveryIssues).containsExactlyInAnyOrder(notVoidIssue, notStaticIssue, privateIssue); } @Test - void findNonVoidBeforeEachMethodsWithStandardLifecycle() { - JUnitException exception = assertThrows(JUnitException.class, - () -> findBeforeEachMethods(TestCaseWithNonVoidLifecyleMethods.class)); - assertEquals( - "@BeforeEach method 'java.lang.String org.junit.jupiter.engine.descriptor.TestCaseWithNonVoidLifecyleMethods.aa()' must not return a value.", - exception.getMessage()); + void findNonVoidBeforeEachMethodsWithStandardLifecycle() throws Exception { + var methods = findBeforeEachMethods(TestCaseWithInvalidLifecycleMethods.class, discoveryIssues::add); + assertThat(methods).isEmpty(); + + var methodSource = MethodSource.from(TestCaseWithInvalidLifecycleMethods.class.getDeclaredMethod("aa")); + var notVoidIssue = DiscoveryIssue.builder(Severity.ERROR, + "@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, + "@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(); + assertThat(discoveryIssues).containsExactlyInAnyOrder(notVoidIssue, privateIssue); } @Test - void findNonVoidAfterEachMethodsWithStandardLifecycle() { - JUnitException exception = assertThrows(JUnitException.class, - () -> findAfterEachMethods(TestCaseWithNonVoidLifecyleMethods.class)); - assertEquals( - "@AfterEach method 'int org.junit.jupiter.engine.descriptor.TestCaseWithNonVoidLifecyleMethods.bb()' must not return a value.", - exception.getMessage()); + void findNonVoidAfterEachMethodsWithStandardLifecycle() throws Exception { + var methods = findAfterEachMethods(TestCaseWithInvalidLifecycleMethods.class, discoveryIssues::add); + assertThat(methods).isEmpty(); + + var methodSource = MethodSource.from(TestCaseWithInvalidLifecycleMethods.class.getDeclaredMethod("bb")); + var notVoidIssue = DiscoveryIssue.builder(Severity.ERROR, + "@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, + "@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(); + assertThat(discoveryIssues).containsExactlyInAnyOrder(notVoidIssue, privateIssue); } @Test void findBeforeEachMethodsWithStandardLifecycle() { - List methods = findBeforeEachMethods(TestCaseWithStandardLifecycle.class); + List methods = findBeforeEachMethods(TestCaseWithStandardLifecycle.class, discoveryIssues::add); assertThat(namesOf(methods)).containsExactlyInAnyOrder("nine", "ten"); + assertThat(discoveryIssues).isEmpty(); } @Test void findAfterEachMethodsWithStandardLifecycle() { - List methods = findAfterEachMethods(TestCaseWithStandardLifecycle.class); + List methods = findAfterEachMethods(TestCaseWithStandardLifecycle.class, discoveryIssues::add); assertThat(namesOf(methods)).containsExactlyInAnyOrder("eleven", "twelve"); } @Test void findBeforeAllMethodsWithStandardLifecycleAndWithoutRequiringStatic() { - List methods = findBeforeAllMethods(TestCaseWithStandardLifecycle.class, false); + List methods = findBeforeAllMethods(TestCaseWithStandardLifecycle.class, false, discoveryIssues::add); assertThat(namesOf(methods)).containsExactly("one"); + assertThat(discoveryIssues).isEmpty(); } @Test - void findBeforeAllMethodsWithStandardLifecycleAndRequiringStatic() { - JUnitException exception = assertThrows(JUnitException.class, - () -> findBeforeAllMethods(TestCaseWithStandardLifecycle.class, true)); - assertEquals( - "@BeforeAll method 'void org.junit.jupiter.engine.descriptor.TestCaseWithStandardLifecycle.one()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).", - exception.getMessage()); + void findBeforeAllMethodsWithStandardLifecycleAndRequiringStatic() throws Exception { + var methods = findBeforeAllMethods(TestCaseWithStandardLifecycle.class, true, discoveryIssues::add); + assertThat(methods).isEmpty(); + + var expectedIssue = DiscoveryIssue.builder(Severity.ERROR, + "@BeforeAll method 'void org.junit.jupiter.engine.descriptor.TestCaseWithStandardLifecycle.one()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).") // + .source(MethodSource.from(TestCaseWithStandardLifecycle.class.getDeclaredMethod("one"))) // + .build(); + assertThat(discoveryIssues).containsExactly(expectedIssue); } @Test void findBeforeAllMethodsWithLifeCyclePerClassAndRequiringStatic() { - List methods = findBeforeAllMethods(TestCaseWithLifecyclePerClass.class, false); + List methods = findBeforeAllMethods(TestCaseWithLifecyclePerClass.class, false, discoveryIssues::add); assertThat(namesOf(methods)).containsExactlyInAnyOrder("three", "four"); + assertThat(discoveryIssues).isEmpty(); } @Test void findAfterAllMethodsWithStandardLifecycleAndWithoutRequiringStatic() { - List methods = findAfterAllMethods(TestCaseWithStandardLifecycle.class, false); + List methods = findAfterAllMethods(TestCaseWithStandardLifecycle.class, false, discoveryIssues::add); assertThat(namesOf(methods)).containsExactlyInAnyOrder("five", "six"); + assertThat(discoveryIssues).isEmpty(); } @Test void findAfterAllMethodsWithStandardLifecycleAndRequiringStatic() { - assertThrows(JUnitException.class, () -> findAfterAllMethods(TestCaseWithStandardLifecycle.class, true)); + var methods = findAfterAllMethods(TestCaseWithStandardLifecycle.class, true, discoveryIssues::add); + assertThat(methods).isEmpty(); + + assertThat(discoveryIssues) // + .filteredOn(where(DiscoveryIssue::severity, isEqual(Severity.ERROR))) // + .isNotEmpty(); } @Test void findAfterAllMethodsWithLifeCyclePerClassAndRequiringStatic() { - List methods = findAfterAllMethods(TestCaseWithLifecyclePerClass.class, false); + List methods = findAfterAllMethods(TestCaseWithLifecyclePerClass.class, false, discoveryIssues::add); assertThat(namesOf(methods)).containsExactlyInAnyOrder("seven", "eight"); } @@ -191,29 +248,26 @@ void eight() { } -class TestCaseWithNonVoidLifecyleMethods { +@SuppressWarnings("JUnitMalformedDeclaration") +class TestCaseWithInvalidLifecycleMethods { - @SuppressWarnings("JUnitMalformedDeclaration") @BeforeEach - String aa() { + private String aa() { return null; } - @SuppressWarnings("JUnitMalformedDeclaration") @AfterEach - int bb() { + private int bb() { return 1; } - @SuppressWarnings("JUnitMalformedDeclaration") @BeforeAll - Double cc() { + private Double cc() { return null; } - @SuppressWarnings("JUnitMalformedDeclaration") @AfterAll - String dd() { + private String dd() { return ""; } 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 6ad584b728df..ad1ef49fc983 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 @@ -48,6 +48,31 @@ */ class TestFactoryTestDescriptorTests { + @Test + void copyIncludesTransformedDynamicDescendantFilter() throws Exception { + var rootUniqueId = UniqueId.forEngine("engine"); + var parentUniqueId = rootUniqueId.append("class", "myClass"); + var originalUniqueId = parentUniqueId.append("old", "testFactory()"); + + var configuration = mock(JupiterConfiguration.class); + when(configuration.getDefaultDisplayNameGenerator()).thenReturn(new CustomDisplayNameGenerator()); + Method testMethod = CustomStreamTestCase.class.getDeclaredMethod("customStream"); + var original = new TestFactoryTestDescriptor(originalUniqueId, CustomStreamTestCase.class, testMethod, List::of, + configuration); + + original.getDynamicDescendantFilter().allowUniqueIdPrefix(originalUniqueId.append("foo", "bar")); + original.getDynamicDescendantFilter().allowIndex(42); + + var newUniqueId = parentUniqueId.append("new", "testFactory()"); + + var copy = original.withUniqueId(new UniqueIdPrefixTransformer(originalUniqueId, newUniqueId)); + + assertThat(copy.getUniqueId()).isEqualTo(newUniqueId); + assertThat(copy.getDynamicDescendantFilter().test(newUniqueId, 0)).isTrue(); + assertThat(copy.getDynamicDescendantFilter().test(newUniqueId, 42)).isTrue(); + assertThat(copy.getDynamicDescendantFilter().test(originalUniqueId, 1)).isFalse(); + } + /** * @since 5.3 */ diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptorTests.java index c0dab4d6a66e..59a5ec415b7a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptorTests.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Set; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -33,20 +34,22 @@ * @since 5.0 */ class TestTemplateTestDescriptorTests { + private JupiterConfiguration jupiterConfiguration = mock(); + @BeforeEach + void prepareJupiterConfiguration() { + when(jupiterConfiguration.getDefaultDisplayNameGenerator()).thenReturn(new DisplayNameGenerator.Standard()); + } + @Test void inheritsTagsFromParent() throws Exception { - UniqueId rootUniqueId = UniqueId.root("segment", "template"); - UniqueId parentUniqueId = rootUniqueId.append("class", "myClass"); - AbstractTestDescriptor parent = containerTestDescriptorWithTags(parentUniqueId, - singleton(TestTag.create("foo"))); - - when(jupiterConfiguration.getDefaultDisplayNameGenerator()).thenReturn(new DisplayNameGenerator.Standard()); + var rootUniqueId = UniqueId.root("segment", "template"); + var parentUniqueId = rootUniqueId.append("class", "myClass"); + var parent = containerTestDescriptorWithTags(parentUniqueId, singleton(TestTag.create("foo"))); - TestTemplateTestDescriptor testDescriptor = new TestTemplateTestDescriptor( - parentUniqueId.append("tmp", "testTemplate()"), MyTestCase.class, - MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); + var testDescriptor = new TestTemplateTestDescriptor(parentUniqueId.append("tmp", "testTemplate()"), + MyTestCase.class, MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); parent.addChild(testDescriptor); assertThat(testDescriptor.getTags()).containsExactlyInAnyOrder(TestTag.create("foo"), TestTag.create("bar"), @@ -55,16 +58,14 @@ void inheritsTagsFromParent() throws Exception { @Test void shouldUseCustomDisplayNameGeneratorIfPresentFromConfiguration() throws Exception { - UniqueId rootUniqueId = UniqueId.root("segment", "template"); - UniqueId parentUniqueId = rootUniqueId.append("class", "myClass"); - AbstractTestDescriptor parent = containerTestDescriptorWithTags(parentUniqueId, - singleton(TestTag.create("foo"))); + var rootUniqueId = UniqueId.root("segment", "template"); + var parentUniqueId = rootUniqueId.append("class", "myClass"); + var parent = containerTestDescriptorWithTags(parentUniqueId, singleton(TestTag.create("foo"))); when(jupiterConfiguration.getDefaultDisplayNameGenerator()).thenReturn(new CustomDisplayNameGenerator()); - TestTemplateTestDescriptor testDescriptor = new TestTemplateTestDescriptor( - parentUniqueId.append("tmp", "testTemplate()"), MyTestCase.class, - MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); + var testDescriptor = new TestTemplateTestDescriptor(parentUniqueId.append("tmp", "testTemplate()"), + MyTestCase.class, MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); parent.addChild(testDescriptor); assertThat(testDescriptor.getDisplayName()).isEqualTo("method-display-name"); @@ -72,21 +73,39 @@ void shouldUseCustomDisplayNameGeneratorIfPresentFromConfiguration() throws Exce @Test void shouldUseStandardDisplayNameGeneratorIfConfigurationNotPresent() throws Exception { - UniqueId rootUniqueId = UniqueId.root("segment", "template"); - UniqueId parentUniqueId = rootUniqueId.append("class", "myClass"); - AbstractTestDescriptor parent = containerTestDescriptorWithTags(parentUniqueId, - singleton(TestTag.create("foo"))); - - when(jupiterConfiguration.getDefaultDisplayNameGenerator()).thenReturn(new DisplayNameGenerator.Standard()); + var rootUniqueId = UniqueId.root("segment", "template"); + var parentUniqueId = rootUniqueId.append("class", "myClass"); + var parent = containerTestDescriptorWithTags(parentUniqueId, singleton(TestTag.create("foo"))); - TestTemplateTestDescriptor testDescriptor = new TestTemplateTestDescriptor( - parentUniqueId.append("tmp", "testTemplate()"), MyTestCase.class, - MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); + var testDescriptor = new TestTemplateTestDescriptor(parentUniqueId.append("tmp", "testTemplate()"), + MyTestCase.class, MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); parent.addChild(testDescriptor); assertThat(testDescriptor.getDisplayName()).isEqualTo("testTemplate()"); } + @Test + void copyIncludesTransformedDynamicDescendantFilter() throws Exception { + var rootUniqueId = UniqueId.root("segment", "template"); + var parentUniqueId = rootUniqueId.append("class", "myClass"); + var originalUniqueId = parentUniqueId.append("old", "testTemplate()"); + + var original = new TestTemplateTestDescriptor(originalUniqueId, MyTestCase.class, + MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); + + original.getDynamicDescendantFilter().allowUniqueIdPrefix(originalUniqueId.append("foo", "bar")); + original.getDynamicDescendantFilter().allowIndex(42); + + var newUniqueId = parentUniqueId.append("new", "testTemplate()"); + + var copy = original.withUniqueId(new UniqueIdPrefixTransformer(originalUniqueId, newUniqueId)); + + assertThat(copy.getUniqueId()).isEqualTo(newUniqueId); + assertThat(copy.getDynamicDescendantFilter().test(newUniqueId, 0)).isTrue(); + assertThat(copy.getDynamicDescendantFilter().test(newUniqueId, 42)).isTrue(); + assertThat(copy.getDynamicDescendantFilter().test(originalUniqueId, 1)).isFalse(); + } + private AbstractTestDescriptor containerTestDescriptorWithTags(UniqueId uniqueId, Set tags) { return new AbstractTestDescriptor(uniqueId, "testDescriptor with tags") { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolverTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolverTests.java index 6c74b09dbbb4..697efda838a4 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolverTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolverTests.java @@ -11,18 +11,21 @@ package org.junit.jupiter.engine.discovery; import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.DisplayNameGenerator.getDisplayNameGenerator; import static org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor.DYNAMIC_CONTAINER_SEGMENT_TYPE; import static org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE; +import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.appendClassTemplateInvocationSegment; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.engineId; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForClass; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForMethod; +import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForStaticClass; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestFactoryMethod; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestTemplateMethod; -import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTopLevelClass; import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; import static org.junit.platform.engine.SelectorResolutionResult.Status.FAILED; import static org.junit.platform.engine.SelectorResolutionResult.Status.RESOLVED; @@ -47,12 +50,13 @@ import java.net.URLClassLoader; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.ClassTemplate; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Nested; @@ -60,10 +64,11 @@ import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.descriptor.ClassTemplateTestDescriptor; import org.junit.jupiter.engine.descriptor.DynamicDescendantFilter; import org.junit.jupiter.engine.descriptor.Filterable; -import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; import org.junit.jupiter.engine.descriptor.JupiterTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateInvocationTestDescriptor; import org.junit.jupiter.engine.descriptor.subpackage.Class1WithTestCases; @@ -73,6 +78,7 @@ import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.FilterResult; import org.junit.platform.engine.SelectorResolutionResult; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; @@ -82,21 +88,23 @@ import org.junit.platform.engine.discovery.PackageSelector; import org.junit.platform.engine.discovery.UniqueIdSelector; import org.junit.platform.launcher.LauncherDiscoveryListener; +import org.junit.platform.launcher.PostDiscoveryFilter; import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; import org.mockito.ArgumentCaptor; /** * @since 5.0 */ -class DiscoverySelectorResolverTests { +class DiscoverySelectorResolverTests extends AbstractJupiterTestEngineTests { private final JupiterConfiguration configuration = mock(); private final LauncherDiscoveryListener discoveryListener = mock(); - private final JupiterEngineDescriptor engineDescriptor = new JupiterEngineDescriptor(engineId(), configuration); + private TestDescriptor engineDescriptor; @BeforeEach void setUp() { - when(configuration.getDefaultDisplayNameGenerator()).thenReturn(new DisplayNameGenerator.Standard()); + when(configuration.getDefaultDisplayNameGenerator()) // + .thenReturn(getDisplayNameGenerator(DisplayNameGenerator.Standard.class)); when(configuration.getDefaultExecutionMode()).thenReturn(ExecutionMode.SAME_THREAD); } @@ -143,7 +151,7 @@ void classResolutionForNonexistentClass() { assertTrue(engineDescriptor.getDescendants().isEmpty()); var result = verifySelectorProcessed(selector); assertThat(result.getStatus()).isEqualTo(FAILED); - assertThat(result.getThrowable().get()).hasMessageContaining("Could not load class with name"); + assertThat(result.getThrowable().orElseThrow()).hasMessageContaining("Could not load class with name"); } @Test @@ -192,6 +200,55 @@ void classResolutionOfStaticNestedClass() { assertThat(uniqueIds).contains(uniqueIdForMethod(OtherTestClass.NestedTestClass.class, "test6()")); } + @Test + void classResolutionOfClassTemplate() { + var selector = selectClass(ClassTemplateTestCase.class); + + AtomicBoolean verified = new AtomicBoolean(); + PostDiscoveryFilter filter = descriptor -> { + if (descriptor instanceof ClassTemplateTestDescriptor) { + assertThat(descriptor.mayRegisterTests()).isFalse(); + assertThat(descriptor.getDescendants()).hasSize(1); + verified.set(true); + } + return FilterResult.included("included"); + }; + + resolve(request().selectors(selector).filters(filter)); + + assertThat(verified.get()).describedAs("filter can see descendants").isTrue(); + + TestDescriptor classTemplateDescriptor = getOnlyElement(engineDescriptor.getChildren()); + assertThat(classTemplateDescriptor.mayRegisterTests()).isTrue(); + assertThat(classTemplateDescriptor.getDescendants()).isEmpty(); + + var classTemplateSegment = classTemplateDescriptor.getUniqueId().getLastSegment(); + assertThat(classTemplateSegment.getType()).isEqualTo("class-template"); + assertThat(classTemplateSegment.getValue()).isEqualTo(ClassTemplateTestCase.class.getName()); + } + + @Test + void uniqueIdResolutionOfClassTemplateInvocation() { + var selector = selectUniqueId( + appendClassTemplateInvocationSegment(uniqueIdForClass(ClassTemplateTestCase.class), 1)); + + resolve(request().selectors(selector)); + + assertThat(engineDescriptor.getChildren()).hasSize(1); + + TestDescriptor classTemplateDescriptor = getOnlyElement(engineDescriptor.getChildren()); + + classTemplateDescriptor.prune(); + assertThat(engineDescriptor.getChildren()).hasSize(1); + assertThat(classTemplateDescriptor.mayRegisterTests()).isTrue(); + assertThat(classTemplateDescriptor.getDescendants()).isEmpty(); + + classTemplateDescriptor.prune(); + assertThat(engineDescriptor.getChildren()).hasSize(1); + assertThat(classTemplateDescriptor.mayRegisterTests()).isTrue(); + assertThat(classTemplateDescriptor.getDescendants()).isEmpty(); + } + @Test void methodResolution() throws NoSuchMethodException { Method test1 = MyTestClass.class.getDeclaredMethod("test1"); @@ -238,7 +295,7 @@ void methodResolutionForNonexistentClass() { assertTrue(engineDescriptor.getDescendants().isEmpty()); var result = verifySelectorProcessed(selector); assertThat(result.getStatus()).isEqualTo(FAILED); - assertThat(result.getThrowable().get())// + assertThat(result.getThrowable().orElseThrow())// .isInstanceOf(PreconditionViolationException.class)// .hasMessageStartingWith("Could not load class with name: " + className); } @@ -252,7 +309,7 @@ void methodResolutionForNonexistentMethod() { assertTrue(engineDescriptor.getDescendants().isEmpty()); var result = verifySelectorProcessed(selector); assertThat(result.getStatus()).isEqualTo(FAILED); - assertThat(result.getThrowable().get()).hasMessageContaining("Could not find method"); + assertThat(result.getThrowable().orElseThrow()).hasMessageContaining("Could not find method"); } @Test @@ -322,7 +379,7 @@ void methodResolutionByUniqueIdWithMissingMethodName() { assertTrue(engineDescriptor.getDescendants().isEmpty()); var result = verifySelectorProcessed(selectUniqueId(uniqueId)); assertThat(result.getStatus()).isEqualTo(FAILED); - assertThat(result.getThrowable().get())// + assertThat(result.getThrowable().orElseThrow())// .isInstanceOf(PreconditionViolationException.class)// .hasMessageStartingWith("Method [()] does not match pattern"); } @@ -336,7 +393,7 @@ void methodResolutionByUniqueIdWithMissingParameters() { assertThat(engineDescriptor.getDescendants()).isEmpty(); var result = verifySelectorProcessed(selectUniqueId(uniqueId)); assertThat(result.getStatus()).isEqualTo(FAILED); - assertThat(result.getThrowable().get())// + assertThat(result.getThrowable().orElseThrow())// .isInstanceOf(PreconditionViolationException.class)// .hasMessageStartingWith("Method [methodName] does not match pattern"); } @@ -350,7 +407,7 @@ void methodResolutionByUniqueIdWithBogusParameters() { assertTrue(engineDescriptor.getDescendants().isEmpty()); var result = verifySelectorProcessed(selectUniqueId(uniqueId)); assertThat(result.getStatus()).isEqualTo(FAILED); - assertThat(result.getThrowable().get())// + assertThat(result.getThrowable().orElseThrow())// .isInstanceOf(JUnitException.class)// .hasMessage("Failed to load parameter type [%s] for method [%s] in class [%s].", "junit.foo.Enigma", "methodName", getClass().getName()); @@ -420,9 +477,9 @@ void twoMethodResolutionsByUniqueId() { assertThat(uniqueIds).contains(uniqueIdForMethod(MyTestClass.class, "test2()")); TestDescriptor classFromMethod1 = descriptorByUniqueId( - uniqueIdForMethod(MyTestClass.class, "test1()")).getParent().get(); + uniqueIdForMethod(MyTestClass.class, "test1()")).getParent().orElseThrow(); TestDescriptor classFromMethod2 = descriptorByUniqueId( - uniqueIdForMethod(MyTestClass.class, "test2()")).getParent().get(); + uniqueIdForMethod(MyTestClass.class, "test2()")).getParent().orElseThrow(); assertEquals(classFromMethod1, classFromMethod2); assertSame(classFromMethod1, classFromMethod2); @@ -491,7 +548,7 @@ void classpathResolution() throws Exception { @Test void classpathResolutionForJarFiles() throws Exception { - URL jarUrl = getClass().getResource("/jupiter-testjar.jar"); + URL jarUrl = requireNonNull(getClass().getResource("/jupiter-testjar.jar")); Path path = Paths.get(jarUrl.toURI()); List selectors = selectClasspathRoots(singleton(path)); @@ -502,8 +559,8 @@ void classpathResolutionForJarFiles() throws Exception { resolve(request().selectors(selectors)); assertThat(uniqueIds()) // - .contains(uniqueIdForTopLevelClass("com.example.project.FirstTest")) // - .contains(uniqueIdForTopLevelClass("com.example.project.SecondTest")); + .contains(uniqueIdForStaticClass("com.example.project.FirstTest")) // + .contains(uniqueIdForStaticClass("com.example.project.SecondTest")); } finally { Thread.currentThread().setContextClassLoader(originalClassLoader); @@ -749,12 +806,12 @@ void classNamePatternFilterExcludesNonMatchingClasses() { } private void resolve(LauncherDiscoveryRequestBuilder builder) { - new DiscoverySelectorResolver().resolveSelectors(builder.build(), engineDescriptor); + engineDescriptor = discoverTests(builder.build()).getEngineDescriptor(); } private TestDescriptor descriptorByUniqueId(UniqueId uniqueId) { return engineDescriptor.getDescendants().stream().filter( - d -> d.getUniqueId().equals(uniqueId)).findFirst().get(); + d -> d.getUniqueId().equals(uniqueId)).findFirst().orElseThrow(); } private List uniqueIds() { @@ -797,11 +854,13 @@ class NonTestClass { abstract class AbstractTestClass { + @SuppressWarnings("unused") @Test void test() { } } +@SuppressWarnings("NewClassNamingConvention") class MyTestClass { @Test @@ -817,10 +876,11 @@ void notATest() { @TestFactory Stream dynamicTest() { - return new ArrayList().stream(); + return Stream.empty(); } } +@SuppressWarnings("NewClassNamingConvention") class YourTestClass { @Test @@ -832,17 +892,18 @@ void test4() { } } +@SuppressWarnings("NewClassNamingConvention") class HerTestClass extends MyTestClass { @SuppressWarnings("JUnitMalformedDeclaration") @Test - void test7(String param) { + void test7(@SuppressWarnings("unused") String param) { } } class OtherTestClass { - @SuppressWarnings("JUnitMalformedDeclaration") + @SuppressWarnings({ "JUnitMalformedDeclaration", "NewClassNamingConvention" }) static class NestedTestClass { @Test @@ -885,6 +946,7 @@ void testTemplate() { } } +@SuppressWarnings("NewClassNamingConvention") class MatchingClass { @Nested class NestedClass { @@ -894,8 +956,16 @@ void test() { } } +@SuppressWarnings("NewClassNamingConvention") class OtherClass { @Test void test() { } } + +@ClassTemplate +class ClassTemplateTestCase { + @Test + void test() { + } +} 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 7d5a91951bc3..002980399131 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 @@ -47,14 +47,14 @@ class DiscoveryTests extends AbstractJupiterTestEngineTests { @Test void discoverTestClass() { LauncherDiscoveryRequest request = request().selectors(selectClass(LocalTestCase.class)).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertEquals(7, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @Test void doNotDiscoverAbstractTestClass() { LauncherDiscoveryRequest request = request().selectors(selectClass(AbstractTestCase.class)).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertEquals(0, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -62,7 +62,7 @@ void doNotDiscoverAbstractTestClass() { void discoverMethodByUniqueId() { LauncherDiscoveryRequest request = request().selectors( selectUniqueId(JupiterUniqueIdBuilder.uniqueIdForMethod(LocalTestCase.class, "test1()"))).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertEquals(2, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -70,7 +70,7 @@ void discoverMethodByUniqueId() { void discoverMethodByUniqueIdForOverloadedMethod() { LauncherDiscoveryRequest request = request().selectors( selectUniqueId(JupiterUniqueIdBuilder.uniqueIdForMethod(LocalTestCase.class, "test4()"))).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertEquals(2, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -78,7 +78,7 @@ void discoverMethodByUniqueIdForOverloadedMethod() { void discoverMethodByUniqueIdForOverloadedMethodVariantThatAcceptsArguments() { LauncherDiscoveryRequest request = request().selectors(selectUniqueId(JupiterUniqueIdBuilder.uniqueIdForMethod( LocalTestCase.class, "test4(" + TestInfo.class.getName() + ")"))).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertEquals(2, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -87,7 +87,7 @@ void discoverMethodByMethodReference() throws NoSuchMethodException { Method testMethod = LocalTestCase.class.getDeclaredMethod("test3"); LauncherDiscoveryRequest request = request().selectors(selectMethod(LocalTestCase.class, testMethod)).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertEquals(2, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -96,7 +96,7 @@ void discoverMultipleMethodsOfSameClass() { LauncherDiscoveryRequest request = request().selectors(selectMethod(LocalTestCase.class, "test1"), selectMethod(LocalTestCase.class, "test2")).build(); - TestDescriptor engineDescriptor = discoverTests(request); + TestDescriptor engineDescriptor = discoverTests(request).getEngineDescriptor(); assertThat(engineDescriptor.getChildren()).hasSize(1); TestDescriptor classDescriptor = getOnlyElement(engineDescriptor.getChildren()); @@ -109,7 +109,7 @@ void discoverCompositeSpec() { selectUniqueId(JupiterUniqueIdBuilder.uniqueIdForMethod(LocalTestCase.class, "test2()")), selectClass(LocalTestCase.class)).build(); - TestDescriptor engineDescriptor = discoverTests(spec); + TestDescriptor engineDescriptor = discoverTests(spec).getEngineDescriptor(); assertEquals(7, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -118,7 +118,7 @@ void discoverTestTemplateMethodByUniqueId() { LauncherDiscoveryRequest spec = request().selectors( selectUniqueId(uniqueIdForTestTemplateMethod(TestTemplateClass.class, "testTemplate()"))).build(); - TestDescriptor engineDescriptor = discoverTests(spec); + TestDescriptor engineDescriptor = discoverTests(spec).getEngineDescriptor(); assertEquals(2, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -127,7 +127,7 @@ void discoverTestTemplateMethodByMethodSelector() { LauncherDiscoveryRequest spec = request().selectors( selectMethod(TestTemplateClass.class, "testTemplate")).build(); - TestDescriptor engineDescriptor = discoverTests(spec); + TestDescriptor engineDescriptor = discoverTests(spec).getEngineDescriptor(); assertEquals(2, engineDescriptor.getDescendants().size(), "# resolved test descriptors"); } @@ -139,7 +139,7 @@ void discoverDeeplyNestedTestMethodByNestedMethodSelector() throws Exception { AbstractSuperClass.NestedInAbstractClass.class.getDeclaredMethod("test")); LauncherDiscoveryRequest spec = request().selectors(selector).build(); - TestDescriptor engineDescriptor = discoverTests(spec); + TestDescriptor engineDescriptor = discoverTests(spec).getEngineDescriptor(); ClassTestDescriptor topLevelClassDescriptor = (ClassTestDescriptor) getOnlyElement( engineDescriptor.getChildren()); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/BeforeAndAfterEachTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/BeforeAndAfterEachTests.java index e6856ea36e0e..0b320af3dbb4 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/BeforeAndAfterEachTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/BeforeAndAfterEachTests.java @@ -229,7 +229,7 @@ void beforeEachMethodThrowsAnException() { ); // @formatter:on - List expected = beforeEachMethodCallSequence.get(0).equals("beforeEachMethod1") ? list1 : list2; + List expected = beforeEachMethodCallSequence.getFirst().equals("beforeEachMethod1") ? list1 : list2; assertEquals(expected, callSequence, "wrong call sequence"); 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 ff8560830382..f0e232eb7363 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 @@ -20,16 +20,22 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.LogRecord; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.ClassTemplate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestClassOrder; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContext; +import org.junit.jupiter.api.extension.ClassTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtendWith; +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.DiscoverySelector; @@ -128,6 +134,42 @@ void random() { .assertStatistics(stats -> stats.succeeded(callSequence.size())); } + @Test + void classTemplateWithLocalConfig() { + var classTemplate = ClassTemplateWithLocalConfigTestCase.class; + var inner0 = ClassTemplateWithLocalConfigTestCase.Inner0.class; + var inner1 = ClassTemplateWithLocalConfigTestCase.Inner1.class; + var inner1Inner1 = ClassTemplateWithLocalConfigTestCase.Inner1.Inner1Inner1.class; + var inner1Inner0 = ClassTemplateWithLocalConfigTestCase.Inner1.Inner1Inner0.class; + + executeTests(ClassOrderer.Random.class, selectClass(classTemplate))// + .assertStatistics(stats -> stats.succeeded(callSequence.size())); + + var inner1InvocationCallSequence = Stream.of(inner1, inner1Inner1, inner1Inner0, inner1Inner0).toList(); + var inner1CallSequence = twice(inner1InvocationCallSequence).toList(); + var outerCallSequence = Stream.concat(Stream.of(classTemplate), + Stream.concat(inner1CallSequence.stream(), Stream.of(inner0))).toList(); + var expectedCallSequence = twice(outerCallSequence).map(Class::getSimpleName).toList(); + + assertThat(callSequence).containsExactlyElementsOf(expectedCallSequence); + } + + private static Stream twice(List values) { + return Stream.concat(values.stream(), values.stream()); + } + + @Test + void classTemplateWithGlobalConfig() { + var classTemplate = ClassTemplateWithLocalConfigTestCase.class; + var otherClass = A_TestCase.class; + + executeTests(ClassOrderer.OrderAnnotation.class, selectClass(otherClass), selectClass(classTemplate))// + .assertStatistics(stats -> stats.succeeded(callSequence.size())); + + assertThat(callSequence)// + .containsSubsequence(classTemplate.getSimpleName(), otherClass.getSimpleName()); + } + private Events executeTests(Class classOrderer) { return executeTests(classOrderer, selectClass(A_TestCase.class), selectClass(B_TestCase.class), selectClass(C_TestCase.class)); @@ -266,4 +308,73 @@ void test() { } } + @SuppressWarnings("JUnitMalformedDeclaration") + @Order(1) + @TestClassOrder(ClassOrderer.OrderAnnotation.class) + @ClassTemplate + @ExtendWith(ClassTemplateWithLocalConfigTestCase.Twice.class) + static class ClassTemplateWithLocalConfigTestCase { + + @Test + void test() { + callSequence.add(ClassTemplateWithLocalConfigTestCase.class.getSimpleName()); + } + + @Nested + @Order(1) + class Inner0 { + @Test + void test() { + callSequence.add(getClass().getSimpleName()); + } + } + + @Nested + @ClassTemplate + @Order(0) + class Inner1 { + + @Test + void test() { + callSequence.add(getClass().getSimpleName()); + } + + @Nested + @ClassTemplate + @Order(2) + class Inner1Inner0 { + @Test + void test() { + callSequence.add(getClass().getSimpleName()); + } + } + + @Nested + @Order(1) + class Inner1Inner1 { + @Test + void test() { + callSequence.add(getClass().getSimpleName()); + } + } + } + + private static class Twice implements ClassTemplateInvocationContextProvider { + + @Override + public boolean supportsClassTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideClassTemplateInvocationContexts( + ExtensionContext context) { + return Stream.of(new Ctx(), new Ctx()); + } + + private record Ctx() implements ClassTemplateInvocationContext { + } + } + } + } 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 0e245db1a2a1..90f34e291e63 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 @@ -52,6 +52,8 @@ import org.junit.jupiter.api.TestReporter; import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.engine.JupiterTestEngine; +import org.junit.jupiter.params.ParameterizedTest; +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.testkit.engine.EngineTestKit; @@ -173,9 +175,10 @@ void random() { assertThat(threadNames).hasSize(1); } - @Test - void defaultOrderer() { - var tests = executeTestsInParallel(WithoutTestMethodOrderTestCase.class, OrderAnnotation.class); + @ParameterizedTest + @ValueSource(classes = { WithoutTestMethodOrderTestCase.class, ClassTemplateTestCase.class }) + void defaultOrderer(Class testClass) { + var tests = executeTestsInParallel(testClass, OrderAnnotation.class); tests.assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -615,7 +618,11 @@ void ___() { static class OuterTestCase { @Nested - class NestedOrderAnnotationTestCase extends OrderAnnotationTestCase { + class InnerTestCase { + + @Nested + class NestedOrderAnnotationTestCase extends OrderAnnotationTestCase { + } } } @@ -814,8 +821,8 @@ static class MisbehavingByRemoving implements MethodOrderer { @Override public void orderMethods(MethodOrdererContext context) { context.getMethodDescriptors().sort(comparing(MethodDescriptor::getDisplayName)); - context.getMethodDescriptors().remove(0); - context.getMethodDescriptors().remove(0); + context.getMethodDescriptors().removeFirst(); + context.getMethodDescriptors().removeFirst(); } } @@ -845,4 +852,7 @@ void test1() { } + static class ClassTemplateTestCase extends WithoutTestMethodOrderTestCase { + } + } 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 13ea7eff61d1..90333d84631e 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 @@ -14,9 +14,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +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; import static org.junit.platform.testkit.engine.EventConditions.test; @@ -33,6 +35,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import org.assertj.core.api.Condition; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -45,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.testkit.engine.EngineExecutionResults; import org.junit.platform.testkit.engine.Events; /** @@ -52,6 +56,7 @@ */ @Isolated 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; @@ -87,7 +92,9 @@ void testCaseWithDefaultInterruptCallbackEnabled() { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); PrintStream outStream = new PrintStream(buffer); System.setOut(outStream); - tests = executeTestsForClass(DefaultPreInterruptCallbackTimeoutOnMethodTestCase.class).testEvents(); + // 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(); output = buffer.toString(StandardCharsets.UTF_8); } finally { @@ -100,7 +107,7 @@ void testCaseWithDefaultInterruptCallbackEnabled() { } } - assertTestHasTimedOut(tests); + assertTestHasTimedOut(tests, message(it -> it.startsWith(TC + "() timed out after"))); assertTrue(interruptedTest.get()); Thread thread = Thread.currentThread(); @@ -109,21 +116,29 @@ void testCaseWithDefaultInterruptCallbackEnabled() { "Thread \"%s\" prio=%d Id=%d %s will be interrupted.".formatted(thread.getName(), thread.getPriority(), thread.threadId(), Thread.State.TIMED_WAITING), // "java.lang.Thread.sleep", // - "org.junit.jupiter.engine.extension.PreInterruptCallbackTests$DefaultPreInterruptCallbackTimeoutOnMethodTestCase.test(PreInterruptCallbackTests.java"); + "%s.test(PreInterruptCallbackTests.java".formatted( + DefaultPreInterruptCallbackTimeoutOnMethodTestCase.class.getName())); assertThat(output) // .containsSubsequence( // "junit-jupiter-timeout-watcher", // - "org.junit.jupiter.engine.extension.PreInterruptThreadDumpPrinter.beforeThreadInterrupt"); + "%s.beforeThreadInterrupt".formatted(PreInterruptThreadDumpPrinter.class.getName())); } @Test void testCaseWithNoInterruptCallbackEnabled() { - Events tests = executeTestsForClass(DefaultPreInterruptCallbackTimeoutOnMethodTestCase.class).testEvents(); + Events tests = executeDefaultPreInterruptCallbackTimeoutOnMethodTestCase("1 μs") // + .testEvents(); assertTestHasTimedOut(tests); assertTrue(interruptedTest.get()); } + private EngineExecutionResults executeDefaultPreInterruptCallbackTimeoutOnMethodTestCase(String timeout) { + return executeTests(request -> request // + .selectors(selectClass(DefaultPreInterruptCallbackTimeoutOnMethodTestCase.class)) // + .configurationParameter(Constants.DEFAULT_TEST_METHOD_TIMEOUT_PROPERTY_NAME, timeout)); + } + @Test void testCaseWithDeclaredInterruptCallbackEnabled() { Events tests = executeTestsForClass(DefaultPreInterruptCallbackWithExplicitCallbackTestCase.class).testEvents(); @@ -169,9 +184,13 @@ void testCaseWithDeclaredInterruptCallbackThrowsException() { } private static void assertTestHasTimedOut(Events tests) { + assertTestHasTimedOut(tests, message(TIMEOUT_ERROR_MSG)); + } + + private static void assertTestHasTimedOut(Events tests, Condition messageCondition) { assertOneFailedTest(tests); tests.failed().assertEventsMatchExactly( - event(test(TC), finishedWithFailure(instanceOf(TimeoutException.class), message(TIMEOUT_ERROR_MSG), // + event(test(TC), finishedWithFailure(instanceOf(TimeoutException.class), messageCondition, // suppressed(0, instanceOf(InterruptedException.class))// ))); } @@ -193,12 +212,12 @@ public void beforeThreadInterrupt(PreInterruptContext preInterruptContext, Exten } } + @SuppressWarnings("JUnitMalformedDeclaration") static class DefaultPreInterruptCallbackTimeoutOnMethodTestCase { @Test - @Timeout(value = 1, unit = TimeUnit.MICROSECONDS) void test() throws InterruptedException { try { - Thread.sleep(1000); + Thread.sleep(5_000); } catch (InterruptedException ex) { interruptedTest.set(true); @@ -208,6 +227,7 @@ void test() throws InterruptedException { } } + @SuppressWarnings("JUnitMalformedDeclaration") @ExtendWith(TestPreInterruptCallback.class) static class DefaultPreInterruptCallbackWithExplicitCallbackTestCase { @Test @@ -224,6 +244,7 @@ void test() throws InterruptedException { } } + @SuppressWarnings("JUnitMalformedDeclaration") @ExtendWith(TestPreInterruptCallback.class) static class DefaultPreInterruptCallbackWithExplicitCallbackWithSeparateThreadTestCase { @Test diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreDestroyCallbackUtilityMethodTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreDestroyCallbackUtilityMethodTests.java index 1a96f9639b98..9f3f25c0f67a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreDestroyCallbackUtilityMethodTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreDestroyCallbackUtilityMethodTests.java @@ -35,7 +35,7 @@ public class TestInstancePreDestroyCallbackUtilityMethodTests extends AbstractJu @ValueSource(classes = { PerMethodLifecycleOnAllLevels.class, PerMethodWithinPerClassLifecycle.class, PerClassWithinPerMethodLifecycle.class, PerClassLifecycleOnAllLevels.class }) void destroysWhatWasPostProcessed(Class testClass) { - executeTestsForClass(testClass).allEvents().debug() // + executeTestsForClass(testClass).allEvents() // .assertStatistics(stats -> stats.reportingEntryPublished(4)) // .assertEventsMatchLooselyInOrder( // reportEntry(Map.of("post-process", testClass.getSimpleName())), 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 new file mode 100644 index 000000000000..5fa99e90480b --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java @@ -0,0 +1,2224 @@ +/* + * 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; + +import static java.util.Comparator.comparing; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.junit.jupiter.params.ArgumentCountValidationMode.NONE; +import static org.junit.jupiter.params.ArgumentCountValidationMode.STRICT; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENTS_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DISPLAY_NAME_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.INDEX_PLACEHOLDER; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.testkit.engine.EventConditions.container; +import static org.junit.platform.testkit.engine.EventConditions.displayName; +import static org.junit.platform.testkit.engine.EventConditions.dynamicTestRegistered; +import static org.junit.platform.testkit.engine.EventConditions.engine; +import static org.junit.platform.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +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.message; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestReporter; +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.engine.AbstractJupiterTestEngineTests; +import org.junit.jupiter.engine.Constants; +import org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor; +import org.junit.jupiter.params.aggregator.AggregateWith; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.aggregator.ArgumentsAggregationException; +import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.ArgumentConverter; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.converter.SimpleArgumentConverter; +import org.junit.jupiter.params.converter.TypedArgumentConverter; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.CsvFileSource; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.FieldSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.support.FieldContext; +import org.junit.jupiter.params.support.ParameterDeclarations; +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.TestDescriptor; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.testkit.engine.EngineExecutionResults; +import org.junit.platform.testkit.engine.Event; +import org.junit.platform.testkit.engine.Events; + +@SuppressWarnings("JUnitMalformedDeclaration") +public class ParameterizedClassIntegrationTests extends AbstractJupiterTestEngineTests { + + @ParameterizedTest + @ValueSource(classes = { ConstructorInjectionTestCase.class, RecordTestCase.class, + RecordWithParameterAnnotationOnComponentTestCase.class, FieldInjectionTestCase.class, + RecordWithBuiltInConverterTestCase.class, RecordWithRegisteredConversionTestCase.class, + FieldInjectionWithRegisteredConversionTestCase.class, RecordWithBuiltInAggregatorTestCase.class, + FieldInjectionWithBuiltInAggregatorTestCase.class, RecordWithCustomAggregatorTestCase.class, + FieldInjectionWithCustomAggregatorTestCase.class }) + void injectsParametersIntoClass(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + String parameterNamePrefix = classTemplateClass.getSimpleName().contains("Aggregator") ? "" : "value="; + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(classTemplateClass), started()), // + + event(dynamicTestRegistered("#1"), displayName("[1] %s-1".formatted(parameterNamePrefix))), // + event(container("#1"), started()), // + event(dynamicTestRegistered("test1")), // + event(dynamicTestRegistered("test2")), // + event(test("test1"), started()), // + event(test("test1"), finishedSuccessfully()), // + event(test("test2"), started()), // + event(test("test2"), finishedSuccessfully()), // + event(container("#1"), finishedSuccessfully()), // + + event(dynamicTestRegistered("#2"), displayName("[2] %s1".formatted(parameterNamePrefix))), // + event(container("#2"), started()), // + event(dynamicTestRegistered("test1")), // + event(dynamicTestRegistered("test2")), // + event(test("test1"), started()), // + event(test("test1"), finishedWithFailure(message(it -> it.contains("negative")))), // + event(test("test2"), started()), // + event(test("test2"), finishedWithFailure(message(it -> it.contains("negative")))), // + event(container("#2"), finishedSuccessfully()), // + + event(container(classTemplateClass), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @ParameterizedTest + @ValueSource(classes = { ArgumentConversionPerInvocationConstructorInjectionTestCase.class, + ArgumentConversionPerInvocationFieldInjectionTestCase.class }) + void argumentConverterIsOnlyCalledOncePerInvocation(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(5).succeeded(5)); + } + + @Nested + class Sources { + + @ParameterizedTest + @ValueSource(classes = { NullAndEmptySourceConstructorInjectionTestCase.class, + NullAndEmptySourceConstructorFieldInjectionTestCase.class }) + void supportsNullAndEmptySource(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=null", "[2] value="); + } + + @ParameterizedTest + @ValueSource(classes = { CsvFileSourceConstructorInjectionTestCase.class, + CsvFileSourceFieldInjectionTestCase.class }) + void supportsCsvFileSource(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(10).succeeded(10)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] name=foo, value=1", "[2] name=bar, value=2", "[3] name=baz, value=3", + "[4] name=qux, value=4"); + } + + @ParameterizedTest + @ValueSource(classes = { SingleEnumSourceConstructorInjectionTestCase.class, + SingleEnumSourceFieldInjectionTestCase.class }) + void supportsSingleEnumSource(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=FOO"); + } + + @ParameterizedTest + @ValueSource(classes = { RepeatedEnumSourceConstructorInjectionTestCase.class, + RepeatedEnumSourceFieldInjectionTestCase.class }) + void supportsRepeatedEnumSource(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=FOO", "[2] value=BAR"); + } + + @ParameterizedTest + @ValueSource(classes = { MethodSourceConstructorInjectionTestCase.class, + MethodSourceFieldInjectionTestCase.class }) + void supportsMethodSource(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=foo", "[2] value=bar"); + } + + @Test + void doesNotSupportDerivingMethodName() { + + var results = executeTestsForClass(MethodSourceWithoutMethodNameTestCase.class); + + results.allEvents().failed() // + .assertEventsMatchExactly(finishedWithFailure( + message("You must specify a method name when using @MethodSource with @ParameterizedClass"))); + } + + @ParameterizedTest + @ValueSource(classes = { FieldSourceConstructorInjectionTestCase.class, + FieldSourceFieldInjectionTestCase.class }) + void supportsFieldSource(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=foo", "[2] value=bar"); + } + + @Test + void doesNotSupportDerivingFieldName() { + + var results = executeTestsForClass(FieldSourceWithoutFieldNameTestCase.class); + + results.allEvents().failed() // + .assertEventsMatchExactly(finishedWithFailure( + message("You must specify a field name when using @FieldSource with @ParameterizedClass"))); + } + + @ParameterizedTest + @ValueSource(classes = { ArgumentsSourceConstructorInjectionTestCase.class, + ArgumentsSourceFieldInjectionTestCase.class }) + void supportsArgumentsSource(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=foo", "[2] value=bar"); + } + + @Test + void failsWhenNoArgumentsSourceIsDeclared() { + var results = executeTestsForClass(NoArgumentSourceTestCase.class); + + results.containerEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message( + "Configuration error: You must configure at least one arguments source for this @ParameterizedClass")))); + } + + @Test + void annotationsAreInherited() { + var results = executeTestsForClass(ConcreteInheritanceTestCase.class); + + int numArgumentSets = 13; + var numContainers = numArgumentSets * 3; // once for outer invocation, once for nested class, once for inner invocation + var numTests = numArgumentSets * 2; // once for outer test, once for inner test + results.containerEvents() // + .assertStatistics(stats -> stats.started(numContainers + 2).succeeded(numContainers + 2)); + results.testEvents() // + .assertStatistics(stats -> stats.started(numTests).succeeded(numTests)); + } + } + + @Nested + class AnnotationAttributes { + + @Test + void supportsCustomNamePatterns() { + + var results = executeTestsForClass(CustomNamePatternTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("1 | TesT | 1, foo | set", "2 | TesT | 2, bar | number=2, name=bar"); + } + + @Test + void closesAutoCloseableArguments() { + AutoCloseableArgument.closeCounter = 0; + + var results = executeTestsForClass(AutoCloseableArgumentTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + assertThat(AutoCloseableArgument.closeCounter).isEqualTo(2); + } + + @Test + void doesNotCloseAutoCloseableArgumentsWhenDisabled() { + AutoCloseableArgument.closeCounter = 0; + + var results = executeTestsForClass(AutoCloseableArgumentWithDisabledCleanupTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + assertThat(AutoCloseableArgument.closeCounter).isEqualTo(0); + } + + @Test + void failsOnStrictArgumentCountValidationMode() { + var results = executeTestsForClass(StrictArgumentCountValidationModeTestCase.class); + + results.allEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message( + "Configuration error: @ParameterizedClass consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused]".formatted())))); + } + + @ParameterizedTest + @ValueSource(classes = { NoneArgumentCountValidationModeTestCase.class, + DefaultArgumentCountValidationModeTestCase.class }) + void doesNotFailOnNoneOrDefaultArgumentCountValidationMode(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + + @Test + void failsOnStrictArgumentCountValidationModeSetViaConfigurationParameter() { + var results = executeTests(request -> request // + .selectors(selectClass(DefaultArgumentCountValidationModeTestCase.class)).configurationParameter( + ArgumentCountValidator.ARGUMENT_COUNT_VALIDATION_KEY, STRICT.name())); + + results.allEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message( + "Configuration error: @ParameterizedClass consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused]".formatted())))); + } + + @Test + void failsForSkippedParameters() { + var results = executeTestsForClass(InvalidUnusedParameterIndexesTestCase.class); + + results.allEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message( + "2 configuration errors:%n- no field annotated with @Parameter(0) declared%n- no field annotated with @Parameter(2) declared".formatted())))); + } + + @Test + 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")))); + } + + @Test + void doesNotFailWhenInvocationIsNotRequiredAndNoArgumentSetsAreProvided() { + var results = executeTestsForClass(AllowedZeroInvocationsTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(2).succeeded(2)); + } + } + + @Nested + class Nesting { + + @ParameterizedTest + @ValueSource(classes = { NestedFieldInjectionTestCase.class, NestedConstructorInjectionTestCase.class }) + void supportsNestedParameterizedClass(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.containerEvents().assertStatistics(stats -> stats.started(14).succeeded(14)); + results.testEvents().assertStatistics(stats -> stats.started(8).succeeded(8)); + assertThat(invocationDisplayNames(results)) // + .containsExactly( // + "[1] number=1", "[1] text=foo", "[2] text=bar", // + "[2] number=2", "[1] text=foo", "[2] text=bar" // + ); + assertThat(allReportEntries(results)).map(it -> it.get("value")).containsExactly( + // @formatter:off + "beforeAll: %s".formatted(classTemplateClass.getSimpleName()), + "beforeParameterizedClassInvocation: %s".formatted(classTemplateClass.getSimpleName()), + "beforeAll: InnerTestCase", + "beforeParameterizedClassInvocation: InnerTestCase", + "beforeEach: [1] flag=true [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [1] flag=true [InnerTestCase]", + "test(1, foo, true)", + "afterEach: [1] flag=true [InnerTestCase]", + "afterEach: [1] flag=true [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [2] flag=false [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [2] flag=false [InnerTestCase]", + "test(1, foo, false)", + "afterEach: [2] flag=false [InnerTestCase]", + "afterEach: [2] flag=false [%s]".formatted(classTemplateClass.getSimpleName()), + "afterParameterizedClassInvocation: InnerTestCase", + "beforeParameterizedClassInvocation: InnerTestCase", + "beforeEach: [1] flag=true [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [1] flag=true [InnerTestCase]", + "test(1, bar, true)", + "afterEach: [1] flag=true [InnerTestCase]", + "afterEach: [1] flag=true [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [2] flag=false [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [2] flag=false [InnerTestCase]", + "test(1, bar, false)", + "afterEach: [2] flag=false [InnerTestCase]", + "afterEach: [2] flag=false [%s]".formatted(classTemplateClass.getSimpleName()), + "afterParameterizedClassInvocation: InnerTestCase", + "afterAll: InnerTestCase", + "afterParameterizedClassInvocation: %s".formatted(classTemplateClass.getSimpleName()), + "beforeParameterizedClassInvocation: %s".formatted(classTemplateClass.getSimpleName()), + "beforeAll: InnerTestCase", + "beforeParameterizedClassInvocation: InnerTestCase", + "beforeEach: [1] flag=true [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [1] flag=true [InnerTestCase]", + "test(2, foo, true)", + "afterEach: [1] flag=true [InnerTestCase]", + "afterEach: [1] flag=true [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [2] flag=false [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [2] flag=false [InnerTestCase]", + "test(2, foo, false)", + "afterEach: [2] flag=false [InnerTestCase]", + "afterEach: [2] flag=false [%s]".formatted(classTemplateClass.getSimpleName()), + "afterParameterizedClassInvocation: InnerTestCase", + "beforeParameterizedClassInvocation: InnerTestCase", + "beforeEach: [1] flag=true [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [1] flag=true [InnerTestCase]", + "test(2, bar, true)", + "afterEach: [1] flag=true [InnerTestCase]", + "afterEach: [1] flag=true [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [2] flag=false [%s]".formatted(classTemplateClass.getSimpleName()), + "beforeEach: [2] flag=false [InnerTestCase]", + "test(2, bar, false)", + "afterEach: [2] flag=false [InnerTestCase]", + "afterEach: [2] flag=false [%s]".formatted(classTemplateClass.getSimpleName()), + "afterParameterizedClassInvocation: InnerTestCase", + "afterAll: InnerTestCase", + "afterParameterizedClassInvocation: %s".formatted(classTemplateClass.getSimpleName()), + "afterAll: %s".formatted(classTemplateClass.getSimpleName()) + // @formatter:on + ); + } + + @ParameterizedTest + @ValueSource(classes = { ConstructorInjectionWithRegularNestedTestCase.class, + FieldInjectionWithRegularNestedTestCase.class }) + void supportsRegularNestedTestClassesInsideParameterizedClass(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.containerEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)); + } + } + + @Nested + class FieldInjection { + + @Test + void supportsMultipleAggregatorFields() { + + var results = executeTestsForClass(MultiAggregatorFieldInjectionTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + } + + @Test + void supportsInjectionOfInheritedFields() { + + var results = executeTestsForClass(InheritedHiddenParameterFieldTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + + assertThat(allReportEntries(results)) // + .extracting(it -> tuple(it.get("super.value"), it.get("this.value"))) // + .containsExactly(tuple("foo", "1"), tuple("bar", "2")); + } + + @Test + void doesNotSupportInjectionForFinalFields() { + + var classTemplateClass = InvalidFinalFieldTestCase.class; + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message( + "Configuration error: @Parameter field [final int %s.i] must not be declared as final.".formatted( + classTemplateClass.getName())))); + } + + @Test + void aggregatorFieldsMustNotDeclareIndex() { + + var classTemplateClass = InvalidAggregatorFieldWithIndexTestCase.class; + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message( + "Configuration error: no index may be declared in @Parameter(0) annotation on aggregator field [%s %s.accessor].".formatted( + ArgumentsAccessor.class.getName(), classTemplateClass.getName())))); + } + + @Test + void declaredIndexMustNotBeNegative() { + + var classTemplateClass = InvalidParameterIndexTestCase.class; + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message( + "Configuration error: index must be greater than or equal to zero in @Parameter(-42) annotation on field [int %s.i].".formatted( + classTemplateClass.getName())))); + } + + @Test + void declaredIndexMustBeUnique() { + + var classTemplateClass = InvalidDuplicateParameterDeclarationTestCase.class; + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message( + "Configuration error: duplicate index declared in @Parameter(0) annotation on fields [int %s.i, long %s.l].".formatted( + classTemplateClass.getName(), classTemplateClass.getName())))); + } + } + + @Nested + class PerClassLifecycle { + + @Test + void supportsFieldInjectionForTestInstanceLifecyclePerClass() { + + var results = executeTestsForClass(FieldInjectionWithPerClassTestInstanceLifecycleTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(8).succeeded(8)); + + Supplier>> valueTrackingReportEntries = () -> allReportEntries(results) // + .filter(it -> it.containsKey("instanceHashCode")); + Supplier>> lifecycleReportEntries = () -> allReportEntries(results) // + .filter(it -> !it.containsKey("instanceHashCode")); + + assertThat(valueTrackingReportEntries.get().map(it -> it.get("value"))) // + .containsExactly("foo", "foo", "bar", "bar"); + assertThat(valueTrackingReportEntries.get().map(it -> it.get("instanceHashCode")).distinct()) // + .hasSize(1); + assertThat(lifecycleReportEntries.get().map(it -> it.get("value"))) // + .containsExactly( + //@formatter:off + "beforeParameterizedClassInvocation1", + "beforeParameterizedClassInvocation2", + "test1", + "test2", + "afterParameterizedClassInvocation1", + "afterParameterizedClassInvocation2", + "beforeParameterizedClassInvocation1", + "beforeParameterizedClassInvocation2", + "test1", + "test2", + "afterParameterizedClassInvocation1", + "afterParameterizedClassInvocation2" + //@formatter:on + ); + } + + @Test + void doesNotSupportConstructorInjectionForTestInstanceLifecyclePerClass() { + + var results = executeTests(request -> request // + .selectors(selectClass(ConstructorInjectionTestCase.class)) // + .configurationParameter(Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME, PER_CLASS.name())); + + results.allEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message(it -> it.contains( + "Constructor injection is not supported for @ParameterizedClass classes with @TestInstance(Lifecycle.PER_CLASS)")))); + } + } + + @Nested + class LifecycleMethods { + + @ParameterizedTest + @CsvSource(textBlock = """ + NonStaticBeforeLifecycleMethodTestCase, @BeforeParameterizedClassInvocation, beforeParameterizedClassInvocation + NonStaticAfterLifecycleMethodTestCase, @AfterParameterizedClassInvocation, afterParameterizedClassInvocation + """) + void lifecycleMethodsNeedToBeStaticByDefault(String simpleClassName, String annotationName, + String lifecycleMethodName) throws Exception { + + var className = ParameterizedClassIntegrationTests.class.getName() + "$" + simpleClassName; + + var results = discoverTestsForClass(Class.forName(className)); + + var issue = getOnlyElement(results.getDiscoveryIssues()); + assertThat(issue.severity()) // + .isEqualTo(Severity.ERROR); + assertThat(issue.message()) // + .isEqualTo( + "%s method 'void %s.%s()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).", + annotationName, className, lifecycleMethodName); + assertThat(issue.source()) // + .containsInstanceOf(org.junit.platform.engine.support.descriptor.MethodSource.class); + } + + @Test + void lifecycleMethodsMustNotBePrivate() { + + var results = discoverTestsForClass(PrivateLifecycleMethodTestCase.class); + + var issue = getOnlyElement(results.getDiscoveryIssues()); + assertThat(issue.severity()) // + .isEqualTo(Severity.ERROR); + assertThat(issue.message()) // + .isEqualTo( + "@BeforeParameterizedClassInvocation method 'private static void %s.beforeParameterizedClassInvocation()' must not be private.", + PrivateLifecycleMethodTestCase.class.getName()); + assertThat(issue.source()) // + .containsInstanceOf(org.junit.platform.engine.support.descriptor.MethodSource.class); + } + + @Test + void lifecycleMethodsMustNotDeclareReturnType() { + + var results = discoverTestsForClass(NonVoidLifecycleMethodTestCase.class); + + var issue = getOnlyElement(results.getDiscoveryIssues()); + assertThat(issue.severity()) // + .isEqualTo(Severity.ERROR); + assertThat(issue.message()) // + .isEqualTo( + "@BeforeParameterizedClassInvocation method 'static int %s.beforeParameterizedClassInvocation()' must not return a value.", + NonVoidLifecycleMethodTestCase.class.getName()); + assertThat(issue.source()) // + .containsInstanceOf(org.junit.platform.engine.support.descriptor.MethodSource.class); + } + + @Test + void lifecycleMethodsFromSuperclassAreWrappedAroundLifecycleMethodsFromTestClass() { + + var results = executeTestsForClass(LifecycleMethodsFromSuperclassTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + + assertThat(allReportEntries(results).map(it -> it.get("value"))) // + .containsExactly("zzz_before", "aaa_before", "test", "aaa_after", "zzz_after"); + } + + @Test + void exceptionsInLifecycleMethodsArePropagated() { + + var results = executeTestsForClass(LifecycleMethodsErrorHandlingTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(3).failed(1).succeeded(2)); + + results.containerEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure( // + message("zzz_before"), // + suppressed(0, message("aaa_after")), // + suppressed(1, message("zzz_after")))); + + assertThat(allReportEntries(results).map(it -> it.get("value"))) // + .containsExactly("zzz_before", "aaa_after", "zzz_after"); + } + + @ParameterizedTest + @ValueSource(classes = { LifecycleMethodArgumentInjectionWithConstructorInjectionTestCase.class, + LifecycleMethodArgumentInjectionWithFieldInjectionTestCase.class }) + void supportsInjectingArgumentsIntoLifecycleMethods(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(5).succeeded(5)); + } + + @ParameterizedTest + @ValueSource(classes = { CustomConverterAnnotationsWithLifecycleMethodsAndConstructorInjectionTestCase.class, + CustomConverterAnnotationsWithLifecycleMethodsAndFieldInjectionTestCase.class }) + void convertersHaveAccessToTheirAnnotations(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + + @ParameterizedTest + @ValueSource(classes = { ValidLifecycleMethodInjectionWithConstructorInjectionTestCase.class, + ValidLifecycleMethodInjectionWithFieldInjectionTestCase.class }) + void supportsMixedInjectionsForLifecycleMethods(Class classTemplateClass) { + + var results = executeTestsForClass(classTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + + @Test + void failsForLifecycleMethodWithInvalidParameters() { + + var results = executeTestsForClass(LifecycleMethodWithInvalidParametersTestCase.class); + + var expectedMessage = """ + 2 configuration errors: + - parameter 'value' with index 0 is incompatible with the parameter declared on the parameterized class: expected type 'int' but found 'long' + - parameter 'anotherValue' with index 1 must not be annotated with @ConvertWith + """; + expectedMessage = expectedMessage.trim() // + .replace("\n", System.lineSeparator()); // use platform-specific line separators + + var failedResult = getFirstTestExecutionResult(results.containerEvents().failed()); + assertThat(failedResult.getThrowable().orElseThrow()) // + .hasMessage( + "Invalid @BeforeParameterizedClassInvocation lifecycle method declaration: static void %s.before(long,int)".formatted( + LifecycleMethodWithInvalidParametersTestCase.class.getName())) // + .cause().hasMessage(expectedMessage); + } + + @Test + void failsForLifecycleMethodWithInvalidParameterOrder() { + + var results = executeTestsForClass(LifecycleMethodWithInvalidParameterOrderTestCase.class); + + results.containerEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message( + ("@BeforeParameterizedClassInvocation method [static void %s.before(%s,int,%s)] declares formal parameters in an invalid order: " + + "argument aggregators must be declared after any indexed arguments and before any arguments resolved by another ParameterResolver.").formatted( + LifecycleMethodWithInvalidParameterOrderTestCase.class.getName(), + ArgumentsAccessor.class.getName(), ArgumentsAccessor.class.getName())))); + } + + @Test + void failsForLifecycleMethodWithParameterAfterAggregator() { + + var results = executeTestsForClass(LifecycleMethodWithParameterAfterAggregatorTestCase.class); + + results.containerEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure( + message(it -> it.contains("No ParameterResolver registered for parameter [int value]")))); + } + + @Test + void lifecycleMethodsMustNotBeDeclaredInRegularTestClasses() { + var testClassName = RegularClassWithLifecycleMethodsTestCase.class.getName(); + + var results = discoverTestsForClass(RegularClassWithLifecycleMethodsTestCase.class); + + assertThat(results.getDiscoveryIssues()).hasSize(2); + + var issues = results.getDiscoveryIssues().stream() // + .sorted(comparing(DiscoveryIssue::message)) // + .toList(); + + assertThat(issues) // + .extracting(DiscoveryIssue::severity) // + .containsOnly(Severity.ERROR); + assertThat(issues) // + .extracting(DiscoveryIssue::source) // + .extracting(Optional::orElseThrow) // + .allMatch(org.junit.platform.engine.support.descriptor.MethodSource.class::isInstance); + assertThat(issues.getFirst().message()) // + .isEqualTo( + "@AfterParameterizedClassInvocation method 'static void %s.after()' must not be declared in test class '%s' because it is not annotated with @ParameterizedClass.", + testClassName, testClassName); + assertThat(issues.getLast().message()) // + .isEqualTo( + "@BeforeParameterizedClassInvocation method 'static void %s.before()' must not be declared in test class '%s' because it is not annotated with @ParameterizedClass.", + testClassName, testClassName); + } + } + + // ------------------------------------------------------------------- + + private static Stream invocationDisplayNames(EngineExecutionResults results) { + return results.containerEvents() // + .started() // + .filter(uniqueId(lastSegmentType(ClassTemplateInvocationTestDescriptor.SEGMENT_TYPE))::matches) // + .map(Event::getTestDescriptor) // + .map(TestDescriptor::getDisplayName); + } + + private static Stream> allReportEntries(EngineExecutionResults results) { + return results.allEvents().reportingEntryPublished() // + .map(e -> e.getRequiredPayload(ReportEntry.class)) // + .map(ReportEntry::getKeyValuePairs); + } + + private static Condition lastSegmentType(@SuppressWarnings("SameParameterValue") String segmentType) { + return new Condition<>(it -> segmentType.equals(it.getLastSegment().getType()), "last segment type is '%s'", + segmentType); + } + + private static TestExecutionResult getFirstTestExecutionResult(Events events) { + return events.stream() // + .findFirst() // + .flatMap(Event::getPayload) // + .map(TestExecutionResult.class::cast) // + .orElseThrow(); + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClassWithNegativeAndPositiveValue + static class ConstructorInjectionTestCase { + + private int value; + private final TestInfo testInfo; + + public ConstructorInjectionTestCase(int value, TestInfo testInfo) { + this.value = value; + this.testInfo = testInfo; + } + + @Test + void test1() { + assertEquals("test1()", testInfo.getDisplayName()); + assertTrue(value < 0, "negative"); + value *= -1; + } + + @Test + void test2() { + assertEquals("test2()", testInfo.getDisplayName()); + assertTrue(value < 0, "negative"); + value *= -1; + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClassWithNegativeAndPositiveValue + record RecordTestCase(int value, TestInfo testInfo) { + + @Test + void test1() { + assertEquals("test1()", testInfo.getDisplayName()); + assertTrue(value < 0, "negative"); + } + + @Test + void test2() { + assertEquals("test2()", testInfo.getDisplayName()); + assertTrue(value < 0, "negative"); + } + } + + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + record RecordWithParameterAnnotationOnComponentTestCase(@Parameter int value) { + + @Test + void test1() { + assertTrue(value < 0, "negative"); + } + + @Test + void test2() { + assertTrue(value < 0, "negative"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + static class FieldInjectionTestCase { + + @Parameter + private int value; + + @Test + void test1() { + assertTrue(value < 0, "negative"); + value *= -1; + } + + @Test + void test2() { + assertTrue(value < 0, "negative"); + value *= -1; + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @CsvSource({ "-1", "1" }) + record RecordWithBuiltInConverterTestCase(int value) { + + @Test + void test1() { + assertTrue(value < 0, "negative"); + } + + @Test + void test2() { + assertTrue(value < 0, "negative"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + record RecordWithRegisteredConversionTestCase(@ConvertWith(CustomIntegerToStringConverter.class) String value) { + + @Test + void test1() { + assertTrue(value.startsWith("minus"), "negative"); + } + + @Test + void test2() { + assertTrue(value.startsWith("minus"), "negative"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + static class FieldInjectionWithRegisteredConversionTestCase { + + @Parameter + @ConvertWith(CustomIntegerToStringConverter.class) + private String value; + + @Test + void test1() { + assertTrue(value.startsWith("minus"), "negative"); + } + + @Test + void test2() { + assertTrue(value.startsWith("minus"), "negative"); + } + } + + private static class CustomIntegerToStringConverter extends TypedArgumentConverter { + + CustomIntegerToStringConverter() { + super(Integer.class, String.class); + } + + @Override + protected String convert(Integer source) throws ArgumentConversionException { + return switch (source) { + case -1 -> "minus one"; + case +1 -> "plus one"; + default -> throw new IllegalArgumentException("Unsupported value: " + source); + }; + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + record RecordWithBuiltInAggregatorTestCase(ArgumentsAccessor accessor) { + + @Test + void test1() { + assertTrue(accessor.getInteger(0) < 0, "negative"); + } + + @Test + void test2() { + assertTrue(accessor.getInteger(0) < 0, "negative"); + } + + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + static class FieldInjectionWithBuiltInAggregatorTestCase { + + @Parameter + private ArgumentsAccessor accessor; + + @Test + void test1() { + assertTrue(accessor.getInteger(0) < 0, "negative"); + } + + @Test + void test2() { + assertTrue(accessor.getInteger(0) < 0, "negative"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + record RecordWithCustomAggregatorTestCase(@AggregateWith(TimesTwoAggregator.class) int value) { + + @Test + void test1() { + assertTrue(value <= -2, "negative"); + } + + @Test + void test2() { + assertTrue(value <= -2, "negative"); + } + + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + static class FieldInjectionWithCustomAggregatorTestCase { + + @TimesTwo + private int value; + + @Test + void test1() { + assertTrue(value <= -2, "negative"); + } + + @Test + void test2() { + assertTrue(value <= -2, "negative"); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + @interface ParameterizedClassWithNegativeAndPositiveValue { + } + + private static class TimesTwoAggregator extends SimpleArgumentsAggregator { + + @Override + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { + + return accessor.getInteger(0) * 2; + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @NullAndEmptySource + record NullAndEmptySourceConstructorInjectionTestCase(String value) { + @Test + void test() { + assertTrue(StringUtils.isBlank(value)); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @NullAndEmptySource + static class NullAndEmptySourceConstructorFieldInjectionTestCase { + + @Parameter + String value; + + @Test + void test() { + assertTrue(StringUtils.isBlank(value)); + } + } + + @ParameterizedClass + @CsvFileSource(resources = "two-column.csv") + record CsvFileSourceConstructorInjectionTestCase(String name, int value) { + @Test + void test() { + assertNotNull(name); + assertTrue(value > 0 && value < 5); + } + } + + @ParameterizedClass + @CsvFileSource(resources = "two-column.csv") + static class CsvFileSourceFieldInjectionTestCase { + + @Parameter(0) + String name; + + @Parameter(1) + int value; + + @Test + void test() { + assertNotNull(name); + assertTrue(value > 0 && value < 5); + } + } + + @ParameterizedClass + @EnumSource + record SingleEnumSourceConstructorInjectionTestCase(EnumOne value) { + @Test + void test() { + assertEquals(EnumOne.FOO, value); + } + } + + @ParameterizedClass + @EnumSource + static class SingleEnumSourceFieldInjectionTestCase { + + @Parameter + EnumOne value; + + @Test + void test() { + assertEquals(EnumOne.FOO, value); + } + } + + @ParameterizedClass + @EnumSource(EnumOne.class) + @EnumSource(EnumTwo.class) + record RepeatedEnumSourceConstructorInjectionTestCase(Object value) { + @Test + void test() { + assertTrue(value == EnumOne.FOO || value == EnumTwo.BAR); + } + } + + @ParameterizedClass + @EnumSource(EnumOne.class) + @EnumSource(EnumTwo.class) + static class RepeatedEnumSourceFieldInjectionTestCase { + + @Parameter + Object value; + + @Test + void test() { + assertTrue(value == EnumOne.FOO || value == EnumTwo.BAR); + } + } + + private enum EnumOne { + FOO + } + + private enum EnumTwo { + BAR + } + + @ParameterizedClass + @MethodSource("parameters") + record MethodSourceConstructorInjectionTestCase(String value) { + + static Stream parameters() { + return Stream.of("foo", "bar"); + } + + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + @ParameterizedClass + @MethodSource("parameters") + static class MethodSourceFieldInjectionTestCase { + + static Stream parameters() { + return Stream.of("foo", "bar"); + } + + @Parameter + String value; + + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + @ParameterizedClass + @MethodSource + record MethodSourceWithoutMethodNameTestCase(String value) { + + @Test + void test() { + fail("should not be executed"); + } + } + + @ParameterizedClass + @FieldSource("parameters") + record FieldSourceConstructorInjectionTestCase(String value) { + + static final List parameters = List.of("foo", "bar"); + + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + @ParameterizedClass + @FieldSource("parameters") + static class FieldSourceFieldInjectionTestCase { + + static final List parameters = List.of("foo", "bar"); + + @Parameter + String value; + + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + @ParameterizedClass + @FieldSource + record FieldSourceWithoutFieldNameTestCase(String value) { + + @Test + void test() { + fail("should not be executed"); + } + } + + @ParameterizedClass + @ArgumentsSource(CustomArgumentsProvider.class) + record ArgumentsSourceConstructorInjectionTestCase(String value) { + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + @ParameterizedClass + @ArgumentsSource(CustomArgumentsProvider.class) + static class ArgumentsSourceFieldInjectionTestCase { + + @Parameter + String value; + + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + static class CustomArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) + throws Exception { + return Stream.of("foo", "bar").map(Arguments::of); + } + } + + @ParameterizedClass(name = INDEX_PLACEHOLDER + " | " // + + DISPLAY_NAME_PLACEHOLDER + " | " // + + ARGUMENTS_PLACEHOLDER + " | " // + + ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER) + @MethodSource("arguments") + @DisplayName("TesT") + record CustomNamePatternTestCase(int number, String name) { + + static Stream arguments() { + return Stream.of(argumentSet("set", 1, "foo"), Arguments.of(2, "bar")); + } + + @Test + void test() { + assertTrue(number > 0); + assertFalse(name.isBlank()); + } + } + + @ParameterizedClass + @ArgumentsSource(AutoCloseableArgumentProvider.class) + record AutoCloseableArgumentTestCase(AutoCloseableArgument argument) { + @Test + void test() { + assertNotNull(argument); + assertEquals(0, AutoCloseableArgument.closeCounter); + } + } + + @ParameterizedClass(autoCloseArguments = false) + @ArgumentsSource(AutoCloseableArgumentProvider.class) + record AutoCloseableArgumentWithDisabledCleanupTestCase(AutoCloseableArgument argument) { + @Test + void test() { + assertNotNull(argument); + assertEquals(0, AutoCloseableArgument.closeCounter); + } + } + + private static class AutoCloseableArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { + return Stream.of(arguments(new AutoCloseableArgument(), Named.of("unused", new AutoCloseableArgument()))); + } + } + + static class AutoCloseableArgument implements AutoCloseable { + + static int closeCounter = 0; + + @Override + public void close() { + closeCounter++; + } + } + + @ParameterizedClass(argumentCountValidation = STRICT) + @CsvSource("foo, unused") + record StrictArgumentCountValidationModeTestCase(String value) { + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass(argumentCountValidation = NONE) + @CsvSource("foo, unused") + record NoneArgumentCountValidationModeTestCase(String value) { + @Test + void test() { + assertEquals("foo", value); + } + } + + @ParameterizedClass + @CsvSource("foo, unused") + record DefaultArgumentCountValidationModeTestCase(String value) { + @Test + void test() { + assertEquals("foo", value); + } + } + + @ParameterizedClass + @MethodSource("org.junit.jupiter.params.ParameterizedClassIntegrationTests#zeroArguments") + record ForbiddenZeroInvocationsTestCase(String value) { + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass(allowZeroInvocations = true) + @MethodSource("org.junit.jupiter.params.ParameterizedClassIntegrationTests#zeroArguments") + record AllowedZeroInvocationsTestCase(String value) { + @Test + void test() { + fail("should not be called"); + } + } + + static Stream zeroArguments() { + return Stream.empty(); + } + + @ParameterizedClass + record NoArgumentSourceTestCase(String value) { + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = { 1, 2 }) + static class NestedFieldInjectionTestCase extends LifecycleCallbacks { + + @Parameter + int number; + + @Nested + @ParameterizedClass + @ValueSource(strings = { "foo", "bar" }) + class InnerTestCase extends LifecycleCallbacks { + + @Parameter + String text; + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void test(boolean flag, TestReporter reporter) { + reporter.publishEntry("test(" + number + ", " + text + ", " + flag + ")"); + assertTrue(number > 0); + assertTrue(List.of("foo", "bar").contains(text)); + } + } + } + + @ParameterizedClass + @ValueSource(ints = { 1, 2 }) + static class NestedConstructorInjectionTestCase extends LifecycleCallbacks { + + final int number; + + NestedConstructorInjectionTestCase(int number) { + this.number = number; + } + + @Nested + @ParameterizedClass + @ValueSource(strings = { "foo", "bar" }) + class InnerTestCase extends LifecycleCallbacks { + + final String text; + + InnerTestCase(String text) { + this.text = text; + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void test(boolean flag, TestReporter reporter) { + reporter.publishEntry("test(" + number + ", " + text + ", " + flag + ")"); + assertTrue(number > 0); + assertTrue(List.of("foo", "bar").contains(text)); + } + } + } + + static class LifecycleCallbacks { + + @BeforeAll + static void beforeAll(TestReporter reporter, TestInfo testInfo) { + reporter.publishEntry("beforeAll: " + testInfo.getTestClass().orElseThrow().getSimpleName()); + } + + @BeforeParameterizedClassInvocation(injectArguments = false) + static void beforeParameterizedClassInvocation(TestReporter reporter, TestInfo testInfo) { + reporter.publishEntry( + "beforeParameterizedClassInvocation: " + testInfo.getTestClass().orElseThrow().getSimpleName()); + } + + @BeforeEach + void beforeEach(TestReporter reporter, TestInfo testInfo) { + reporter.publishEntry( + "beforeEach: " + testInfo.getDisplayName() + " [" + this.getClass().getSimpleName() + "]"); + } + + @AfterEach + void afterEach(TestReporter reporter, TestInfo testInfo) { + reporter.publishEntry( + "afterEach: " + testInfo.getDisplayName() + " [" + this.getClass().getSimpleName() + "]"); + } + + @AfterParameterizedClassInvocation(injectArguments = false) + static void afterParameterizedClassInvocation(TestReporter reporter, TestInfo testInfo) { + reporter.publishEntry( + "afterParameterizedClassInvocation: " + testInfo.getTestClass().orElseThrow().getSimpleName()); + } + + @AfterAll + static void afterAll(TestReporter reporter, TestInfo testInfo) { + reporter.publishEntry("afterAll: " + testInfo.getTestClass().orElseThrow().getSimpleName()); + } + } + + @ParameterizedClass + @ValueSource(ints = { 1, 2 }) + record ConstructorInjectionWithRegularNestedTestCase(int number) { + + @Nested + @TestInstance(PER_CLASS) + class InnerTestCase { + + InnerTestCase(TestInfo testInfo) { + assertThat(testInfo.getTestClass()).contains(InnerTestCase.class); + assertThat(testInfo.getTestMethod()).isEmpty(); + } + + @Test + void test() { + assertTrue(number >= 0); + } + } + } + + @ParameterizedClass + @ValueSource(ints = { 1, 2 }) + static class FieldInjectionWithRegularNestedTestCase { + + @Parameter + int number; + + @Nested + @TestInstance(PER_CLASS) + class InnerTestCase { + + InnerTestCase(TestInfo testInfo) { + assertThat(testInfo.getTestClass()).contains(InnerTestCase.class); + assertThat(testInfo.getTestMethod()).isEmpty(); + } + + @Test + void test() { + assertTrue(number >= 0); + } + } + } + + @ParameterizedClass + @CsvSource({ "1, foo", "2, bar" }) + static class MultiAggregatorFieldInjectionTestCase { + + @Parameter + ArgumentsAccessor accessor; + + @TimesTwo + int numberTimesTwo; + + @Parameter(0) + int number; + + @Parameter(1) + String text; + + @Test + void test() { + assertEquals(2, accessor.size()); + assertEquals(number, accessor.getInteger(0)); + assertEquals(number * 2, numberTimesTwo); + assertEquals(text, accessor.getString(1)); + } + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.PARAMETER }) + @Parameter + @AggregateWith(TimesTwoAggregator.class) + @interface TimesTwo { + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @MethodSource("methodSource") + @FieldSource("fieldSource") + @TestInstance(PER_CLASS) + static class FieldInjectionWithPerClassTestInstanceLifecycleTestCase { + + List methodSource() { + return List.of("foo"); + } + + final List fieldSource = List.of("bar"); + + @BeforeParameterizedClassInvocation(injectArguments = false) + void beforeParameterizedClassInvocation1(TestReporter reporter) { + reporter.publishEntry("beforeParameterizedClassInvocation1"); + } + + @BeforeParameterizedClassInvocation(injectArguments = false) + void beforeParameterizedClassInvocation2(TestReporter reporter) { + reporter.publishEntry("beforeParameterizedClassInvocation2"); + } + + @AfterParameterizedClassInvocation(injectArguments = false) + void afterParameterizedClassInvocation1(TestReporter reporter) { + reporter.publishEntry("afterParameterizedClassInvocation1"); + } + + @AfterParameterizedClassInvocation(injectArguments = false) + void afterParameterizedClassInvocation2(TestReporter reporter) { + reporter.publishEntry("afterParameterizedClassInvocation2"); + } + + @Parameter + private String value; + + @Test + void test1(TestReporter reporter, TestInfo testInfo) { + publishReportEntry(reporter, testInfo); + } + + @Test + void test2(TestReporter reporter, TestInfo testInfo) { + publishReportEntry(reporter, testInfo); + } + + private void publishReportEntry(TestReporter reporter, TestInfo testInfo) { + assertNotNull(value); + reporter.publishEntry(testInfo.getTestMethod().orElseThrow().getName()); + reporter.publishEntry(Map.of( // + "instanceHashCode", Integer.toHexString(hashCode()), // + "value", value // + )); + } + } + + abstract static class BaseTestCase { + @Parameter(0) + String value; + } + + @ParameterizedClass + @CsvSource({ "foo, 1", "bar, 2" }) + static class InheritedHiddenParameterFieldTestCase extends BaseTestCase { + @Parameter(1) + String value; + + @Test + void test(TestReporter reporter) { + reporter.publishEntry(Map.of( // + "super.value", super.value, // + "this.value", this.value // + )); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class InvalidFinalFieldTestCase { + + @Parameter + final int i = -1; + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class InvalidAggregatorFieldWithIndexTestCase { + + @Parameter(0) + ArgumentsAccessor accessor; + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class InvalidParameterIndexTestCase { + + @Parameter(-42) + int i; + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class InvalidDuplicateParameterDeclarationTestCase { + + @Parameter(0) + int i; + + @Parameter(0) + long l; + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @CsvSource({ "unused1, foo, unused2, bar", "unused4, baz, unused5, qux" }) + static class InvalidUnusedParameterIndexesTestCase { + + @Parameter(1) + String second; + + @Parameter(3) + String fourth; + + @Test + void test(TestReporter reporter) { + reporter.publishEntry(Map.of( // + "second", second, // + "fourth", fourth // + )); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + record ArgumentConversionPerInvocationConstructorInjectionTestCase( + @ConvertWith(Wrapper.Converter.class) Wrapper wrapper) { + + static Wrapper instance; + + @BeforeAll + @AfterAll + static void clearWrapper() { + instance = null; + } + + @Test + void test1() { + setOrCheckWrapper(); + } + + @Test + void test2() { + setOrCheckWrapper(); + } + + private void setOrCheckWrapper() { + if (instance == null) { + instance = wrapper; + } + else { + assertSame(instance, wrapper); + } + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class ArgumentConversionPerInvocationFieldInjectionTestCase { + + static Wrapper instance; + + @BeforeAll + @AfterAll + static void clearWrapper() { + instance = null; + } + + @Parameter + @ConvertWith(Wrapper.Converter.class) + Wrapper wrapper; + + @Test + void test1() { + setOrCheckWrapper(); + } + + @Test + void test2() { + setOrCheckWrapper(); + } + + private void setOrCheckWrapper() { + if (instance == null) { + instance = wrapper; + } + else { + assertSame(instance, wrapper); + } + } + } + + record Wrapper(int value) { + static class Converter extends SimpleArgumentConverter { + @Override + protected Object convert(Object source, Class targetType) { + return new Wrapper((Integer) source); + } + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + record NonStaticBeforeLifecycleMethodTestCase() { + + @BeforeParameterizedClassInvocation + void beforeParameterizedClassInvocation() { + fail("should not be called"); + } + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + record NonStaticAfterLifecycleMethodTestCase() { + + @AfterParameterizedClassInvocation + void afterParameterizedClassInvocation() { + fail("should not be called"); + } + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + record PrivateLifecycleMethodTestCase() { + + @BeforeParameterizedClassInvocation + private static void beforeParameterizedClassInvocation() { + fail("should not be called"); + } + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + record NonVoidLifecycleMethodTestCase() { + + @BeforeParameterizedClassInvocation + static int beforeParameterizedClassInvocation() { + return fail("should not be called"); + } + + @Test + void test() { + fail("should not be called"); + } + } + + static abstract class AbstractBaseLifecycleTestCase { + + @BeforeParameterizedClassInvocation + static void zzz_before(TestReporter reporter) { + reporter.publishEntry("zzz_before"); + } + + @AfterParameterizedClassInvocation + static void zzz_after(TestReporter reporter) { + reporter.publishEntry("zzz_after"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class LifecycleMethodsFromSuperclassTestCase extends AbstractBaseLifecycleTestCase { + + @BeforeParameterizedClassInvocation + static void aaa_before(TestReporter reporter) { + reporter.publishEntry("aaa_before"); + } + + @AfterParameterizedClassInvocation + static void aaa_after(TestReporter reporter) { + reporter.publishEntry("aaa_after"); + } + + @Test + void test(TestReporter reporter) { + reporter.publishEntry("test"); + } + } + + static abstract class AbstractBaseLifecycleWithErrorsTestCase { + + @BeforeParameterizedClassInvocation + static void zzz_before(TestReporter reporter) { + reporter.publishEntry("zzz_before"); + fail("zzz_before"); + } + + @AfterParameterizedClassInvocation + static void zzz_after(TestReporter reporter) { + reporter.publishEntry("zzz_after"); + fail("zzz_after"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class LifecycleMethodsErrorHandlingTestCase extends AbstractBaseLifecycleWithErrorsTestCase { + + @BeforeParameterizedClassInvocation + static void aaa_before(TestReporter reporter) { + fail("should not be called"); + } + + @AfterParameterizedClassInvocation + static void aaa_after(TestReporter reporter) { + reporter.publishEntry("aaa_after"); + fail("aaa_after"); + } + + @Test + void test(TestReporter reporter) { + reporter.publishEntry("test"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + record LifecycleMethodArgumentInjectionWithConstructorInjectionTestCase( + @ConvertWith(AtomicIntegerConverter.class) AtomicInteger counter) { + + @BeforeParameterizedClassInvocation + static void before(AtomicInteger counter) { + assertEquals(2, counter.incrementAndGet()); + } + + @AfterParameterizedClassInvocation + static void after(AtomicInteger counter) { + assertEquals(4, counter.get()); + } + + @Test + void test1() { + this.counter.incrementAndGet(); + } + + @Test + void test2() { + this.counter.incrementAndGet(); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class LifecycleMethodArgumentInjectionWithFieldInjectionTestCase { + + @Parameter + @ConvertWith(AtomicIntegerConverter.class) + AtomicInteger counter; + + @BeforeParameterizedClassInvocation + static void before(AtomicInteger counter) { + assertEquals(2, counter.incrementAndGet()); + } + + @AfterParameterizedClassInvocation + static void after(AtomicInteger counter) { + assertEquals(4, counter.get()); + } + + @Test + void test1() { + this.counter.incrementAndGet(); + } + + @Test + void test2() { + this.counter.incrementAndGet(); + } + } + + static class AtomicIntegerConverter extends SimpleArgumentConverter { + @Override + protected Object convert(Object source, Class targetType) { + return new AtomicInteger((Integer) source); + } + } + + @ParameterizedClass + @ValueSource(strings = "foo") + record CustomConverterAnnotationsWithLifecycleMethodsAndConstructorInjectionTestCase( + @CustomConversion String value) { + + @BeforeParameterizedClassInvocation + static void before(String value) { + assertEquals("foo", value); + } + + @Test + void test() { + assertEquals("foo", this.value); + } + } + + @ParameterizedClass + @ValueSource(strings = "foo") + static class CustomConverterAnnotationsWithLifecycleMethodsAndFieldInjectionTestCase { + + @Parameter + @CustomConversion + String value; + + @BeforeParameterizedClassInvocation + static void before(String value) { + assertEquals("foo", value); + } + + @Test + void test() { + assertEquals("foo", this.value); + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.PARAMETER, ElementType.FIELD }) + @ConvertWith(CustomConversion.Converter.class) + @interface CustomConversion { + + class Converter implements ArgumentConverter { + @Override + public Object convert(Object source, ParameterContext context) throws ArgumentConversionException { + assertNotNull(context.getParameter().getAnnotation(CustomConversion.class)); + return source; + } + + @Override + public Object convert(Object source, FieldContext context) throws ArgumentConversionException { + assertNotNull(context.getField().getAnnotation(CustomConversion.class)); + return source; + } + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class ValidLifecycleMethodInjectionWithConstructorInjectionTestCase + extends AbstractValidLifecycleMethodInjectionTestCase { + + private final AtomicInteger value; + + ValidLifecycleMethodInjectionWithConstructorInjectionTestCase( + @ConvertWith(AtomicIntegerConverter.class) AtomicInteger value) { + this.value = value; + } + + @Test + void test() { + assertEquals(5, this.value.getAndIncrement()); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class ValidLifecycleMethodInjectionWithFieldInjectionTestCase + extends AbstractValidLifecycleMethodInjectionTestCase { + + @Parameter + @ConvertWith(AtomicIntegerConverter.class) + AtomicInteger value; + + @Test + void test() { + assertEquals(5, this.value.getAndIncrement()); + } + } + + abstract static class AbstractValidLifecycleMethodInjectionTestCase { + + @BeforeParameterizedClassInvocation + static void before0() { + } + + @BeforeParameterizedClassInvocation + static void before1(AtomicInteger value) { + value.incrementAndGet(); + } + + @BeforeParameterizedClassInvocation + static void before2(ArgumentsAccessor accessor) { + assertEquals(1, accessor.getInteger(0)); + } + + @BeforeParameterizedClassInvocation + static void before3(AtomicInteger value, TestInfo testInfo) { + assertEquals("[1] value=1", testInfo.getDisplayName()); + value.incrementAndGet(); + } + + @BeforeParameterizedClassInvocation + static void before4(ArgumentsAccessor accessor, TestInfo testInfo) { + assertEquals(1, accessor.getInteger(0)); + assertEquals("[1] value=1", testInfo.getDisplayName()); + } + + @BeforeParameterizedClassInvocation + static void before4(AtomicInteger value, ArgumentsAccessor accessor) { + assertEquals(1, accessor.getInteger(0)); + value.incrementAndGet(); + } + + @BeforeParameterizedClassInvocation + static void before5(AtomicInteger value, ArgumentsAccessor accessor, TestInfo testInfo) { + assertEquals(1, accessor.getInteger(0)); + assertEquals("[1] value=1", testInfo.getDisplayName()); + value.incrementAndGet(); + } + + @BeforeParameterizedClassInvocation + static void before6(@TimesTwo int valueTimesTwo) { + assertEquals(2, valueTimesTwo); + } + + @AfterParameterizedClassInvocation + static void after(AtomicInteger value, ArgumentsAccessor accessor, TestInfo testInfo) { + assertEquals(6, value.get()); + assertEquals(1, accessor.getInteger(0)); + assertEquals("[1] value=1", testInfo.getDisplayName()); + } + } + + @ParameterizedClass + @CsvSource("1, 2") + record LifecycleMethodWithInvalidParametersTestCase(int value, int anotherValue) { + + @BeforeParameterizedClassInvocation + static void before(long value, @ConvertWith(CustomIntegerToStringConverter.class) int anotherValue) { + fail("should not be called"); + } + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + record LifecycleMethodWithInvalidParameterOrderTestCase(int value) { + + @BeforeParameterizedClassInvocation + static void before(ArgumentsAccessor accessor1, int value, ArgumentsAccessor accessor2) { + fail("should not be called"); + } + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + record LifecycleMethodWithParameterAfterAggregatorTestCase(int value) { + + @BeforeParameterizedClassInvocation + static void before(@TimesTwo int valueTimesTwo, int value) { + fail("should not be called"); + } + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass // argument sets: 13 = 2 + 4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + @ArgumentsSource(CustomArgumentsProvider.class) // 2 + @CsvFileSource(resources = "two-column.csv") // 4 + @CsvSource("csv") // 1 + @EmptySource // 1 + @EnumSource(EnumOne.class) // 1 + @FieldSource("field") // 1 + @MethodSource("method") // 1 + @NullSource // 1 + @ValueSource(strings = "value") // 1 + abstract static class BaseInheritanceTestCase { + + static final List field = List.of("field"); + + static List method() { + return List.of("method"); + } + + @Parameter + @ConvertWith(ToStringConverter.class) // For @EnumSource + String value; + + @Test + void test() { + } + + @Nested + @ParameterizedClass + @ValueSource(ints = 1) + class Inner { + @Test + void test() { + } + } + + static class ToStringConverter extends SimpleArgumentConverter { + @Override + protected Object convert(Object source, Class targetType) throws ArgumentConversionException { + return source == null ? null : String.valueOf(source); + } + } + } + + static class ConcreteInheritanceTestCase extends BaseInheritanceTestCase { + } + + static class RegularClassWithLifecycleMethodsTestCase { + + @BeforeParameterizedClassInvocation + static void before() { + } + + @AfterParameterizedClassInvocation + static void after() { + } + + @Test + void test() { + } + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java similarity index 84% rename from jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java rename to jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java index fc07dae767a9..7d48e198dfb2 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java @@ -16,16 +16,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENTS_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENTS_WITH_NAMES_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENT_SET_NAME_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.DEFAULT_DISPLAY_NAME; -import static org.junit.jupiter.params.ParameterizedTest.DISPLAY_NAME_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.INDEX_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENTS_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENTS_WITH_NAMES_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENT_SET_NAME_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DEFAULT_DISPLAY_NAME; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DISPLAY_NAME_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.INDEX_PLACEHOLDER; import static org.junit.jupiter.params.provider.Arguments.argumentSet; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.lang.reflect.Method; import java.math.BigDecimal; @@ -39,11 +40,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.AnnotatedElementContext; import org.junit.jupiter.api.extension.ExtensionConfigurationException; -import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.aggregator.AggregateWith; import org.junit.jupiter.params.aggregator.ArgumentsAccessor; -import org.junit.jupiter.params.aggregator.ArgumentsAggregator; +import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; import org.junit.platform.commons.JUnitException; @@ -53,7 +54,7 @@ * @since 5.0 */ @SuppressWarnings("ALL") -class ParameterizedTestNameFormatterTests { +class ParameterizedInvocationNameFormatterTests { private final Locale originalLocale = Locale.getDefault(); @@ -322,21 +323,27 @@ void mixedTypesOfArgumentsImplementationsAndCustomDisplayNamePattern() { // ------------------------------------------------------------------------- - private static ParameterizedTestNameFormatter formatter(String pattern, String displayName) { + private static ParameterizedInvocationNameFormatter formatter(String pattern, String displayName) { return formatter(pattern, displayName, 512); } - private static ParameterizedTestNameFormatter formatter(String pattern, String displayName, int argumentMaxLength) { - return new ParameterizedTestNameFormatter(pattern, displayName, mock(), argumentMaxLength); + private static ParameterizedInvocationNameFormatter formatter(String pattern, String displayName, + int argumentMaxLength) { + ParameterizedDeclarationContext context = mock(); + when(context.getResolverFacade()).thenReturn(mock()); + when(context.getAnnotationName()).thenReturn(ParameterizedTest.class.getSimpleName()); + return new ParameterizedInvocationNameFormatter(pattern, displayName, context, argumentMaxLength); } - private static ParameterizedTestNameFormatter formatter(String pattern, String displayName, Method method) { - var context = new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class)); - return new ParameterizedTestNameFormatter(pattern, displayName, context, 512); + private static ParameterizedInvocationNameFormatter formatter(String pattern, String displayName, Method method) { + var context = new ParameterizedTestContext(method.getDeclaringClass(), method, + method.getAnnotation(ParameterizedTest.class)); + return new ParameterizedInvocationNameFormatter(pattern, displayName, context, 512); } - private static String format(ParameterizedTestNameFormatter formatter, int invocationIndex, Arguments arguments) { - return formatter.format(invocationIndex, arguments, arguments.get()); + private static String format(ParameterizedInvocationNameFormatter formatter, int invocationIndex, + Arguments arguments) { + return formatter.format(invocationIndex, EvaluatedArgumentSet.allOf(arguments)); } private static class ToStringReturnsNull { @@ -377,9 +384,10 @@ void parameterizedTestWithAggregator(int someNumber, void processFruits(String fruit1, String fruit2) { } - private static class CustomAggregator implements ArgumentsAggregator { + private static class CustomAggregator extends SimpleArgumentsAggregator { @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) { + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) { return accessor.get(0); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestMethodContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java similarity index 77% rename from jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestMethodContextTests.java rename to jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java index 7272b084a52d..0baf170b2e56 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestMethodContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java @@ -10,39 +10,41 @@ package org.junit.jupiter.params; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.params.aggregator.AggregatorIntegrationTests.CsvToPerson; import org.junit.jupiter.params.aggregator.AggregatorIntegrationTests.Person; import org.junit.jupiter.params.aggregator.ArgumentsAccessor; import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.ReflectionUtils; /** - * Unit tests for {@link ParameterizedTestMethodContext}. + * Unit tests for {@link ParameterizedTestContext}. * * @since 5.2 */ -class ParameterizedTestMethodContextTests { +class ParameterizedTestContextTests { @ParameterizedTest @ValueSource(strings = { "onePrimitive", "twoPrimitives", "twoAggregators", "twoAggregatorsWithTestInfoAtTheEnd", "mixedMode" }) void validSignatures(String methodName) { - assertTrue(createMethodContext(ValidTestCase.class, methodName).hasPotentiallyValidSignature()); + assertDoesNotThrow(() -> createMethodContext(ValidTestCase.class, methodName)); } @ParameterizedTest @ValueSource(strings = { "twoAggregatorsWithPrimitiveInTheMiddle", "twoAggregatorsWithTestInfoInTheMiddle" }) void invalidSignatures(String methodName) { - assertFalse(createMethodContext(InvalidTestCase.class, methodName).hasPotentiallyValidSignature()); + assertThrows(PreconditionViolationException.class, + () -> createMethodContext(InvalidTestCase.class, methodName)); } - private ParameterizedTestMethodContext createMethodContext(Class testClass, String methodName) { + private ParameterizedTestContext createMethodContext(Class testClass, String methodName) { var method = ReflectionUtils.findMethods(testClass, m -> m.getName().equals(methodName)).getFirst(); - return new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class)); + return new ParameterizedTestContext(testClass, method, method.getAnnotation(ParameterizedTest.class)); } @SuppressWarnings("JUnitMalformedDeclaration") 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 aa36f907aa25..472429698597 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 @@ -15,14 +15,15 @@ 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 static org.junit.jupiter.params.ParameterizedTestExtension.METHOD_CONTEXT_KEY; -import static org.junit.jupiter.params.ParameterizedTestExtension.arguments; +import static org.junit.jupiter.params.ParameterizedInvocationContextProvider.arguments; +import static org.junit.jupiter.params.ParameterizedTestExtension.DECLARATION_CONTEXT_KEY; import java.io.FileNotFoundException; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.nio.file.Path; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -42,6 +43,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.ReflectionUtils; @@ -105,7 +107,7 @@ void emptyDisplayNameIsIllegal() { void defaultDisplayNameWithEmptyStringInConfigurationIsIllegal() { AtomicInteger invocations = new AtomicInteger(); Function> configurationSupplier = key -> { - if (key.equals(ParameterizedTestExtension.DISPLAY_NAME_PATTERN_KEY)) { + if (key.equals(ParameterizedInvocationNameFormatter.DISPLAY_NAME_PATTERN_KEY)) { invocations.incrementAndGet(); return Optional.of(""); } @@ -122,11 +124,15 @@ void defaultDisplayNameWithEmptyStringInConfigurationIsIllegal() { @Test void argumentsRethrowsOriginalExceptionFromProviderAsUncheckedException() { - ArgumentsProvider failingProvider = (context) -> { - throw new FileNotFoundException("a message"); + ArgumentsProvider failingProvider = new ArgumentsProvider() { + @Override + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) throws Exception { + throw new FileNotFoundException("a message"); + } }; - var exception = assertThrows(FileNotFoundException.class, () -> arguments(failingProvider, null)); + var exception = assertThrows(FileNotFoundException.class, () -> arguments(failingProvider, null, null)); assertEquals("a message", exception.getMessage()); } @@ -204,8 +210,8 @@ private ExtensionContext getExtensionContextReturningSingleMethod(Object testCas private ExtensionContext getExtensionContextReturningSingleMethod(Object testCase, Function> configurationSupplier) { - var method = ReflectionUtils.findMethods(testCase.getClass(), - it -> "method".equals(it.getName())).stream().findFirst(); + Class testClass = testCase.getClass(); + var method = ReflectionUtils.findMethods(testClass, it -> "method".equals(it.getName())).stream().findFirst(); return new ExtensionContext() { @@ -248,7 +254,12 @@ public Optional getElement() { @Override public Optional> getTestClass() { - return Optional.empty(); + return Optional.of(testClass); + } + + @Override + public List> getEnclosingTestClasses() { + return List.of(); } @Override @@ -297,8 +308,9 @@ public void publishDirectory(String name, ThrowingConsumer action) { public Store getStore(Namespace namespace) { var store = new NamespaceAwareStore(this.store, namespace); method // - .map(it -> new ParameterizedTestMethodContext(it, it.getAnnotation(ParameterizedTest.class))) // - .ifPresent(ctx -> store.put(METHOD_CONTEXT_KEY, ctx)); + .map(it -> new ParameterizedTestContext(testClass, it, + it.getAnnotation(ParameterizedTest.class))) // + .ifPresent(ctx -> store.put(DECLARATION_CONTEXT_KEY, ctx)); return store; } @@ -360,7 +372,8 @@ void method() { static class ZeroArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.empty(); } } @@ -376,7 +389,8 @@ void method(String parameter) { static class ArgumentsProviderWithCloseHandler implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { var argumentsStream = Stream.of("foo", "bar").map(Arguments::of); return argumentsStream.onClose(() -> streamWasClosed = true); } @@ -393,7 +407,8 @@ void method() { class NonStaticArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return null; } } @@ -431,7 +446,8 @@ static class AmbiguousConstructorArgumentsProvider implements ArgumentsProvider } @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return null; } } 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 eabe6470ec83..0c0c44842427 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 @@ -14,7 +14,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -31,7 +30,6 @@ import static org.junit.platform.testkit.engine.EventConditions.abortedWithReason; import static org.junit.platform.testkit.engine.EventConditions.container; import static org.junit.platform.testkit.engine.EventConditions.displayName; -import static org.junit.platform.testkit.engine.EventConditions.engine; import static org.junit.platform.testkit.engine.EventConditions.event; import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; @@ -83,6 +81,7 @@ import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestReporter; +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.ParameterResolutionException; @@ -93,7 +92,7 @@ import org.junit.jupiter.params.aggregator.AggregateWith; import org.junit.jupiter.params.aggregator.ArgumentsAccessor; import org.junit.jupiter.params.aggregator.ArgumentsAggregationException; -import org.junit.jupiter.params.aggregator.ArgumentsAggregator; +import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator; import org.junit.jupiter.params.converter.ArgumentConversionException; import org.junit.jupiter.params.converter.ArgumentConverter; import org.junit.jupiter.params.converter.ConvertWith; @@ -109,6 +108,7 @@ import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; +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; @@ -127,8 +127,9 @@ class ParameterizedTestIntegrationTests { private final Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT); @AfterEach - void restoreLocale() { + void reset() { Locale.setDefault(Locale.Category.FORMAT, originalLocale); + AutoCloseableArgument.closeCounter = 0; } @ParameterizedTest @@ -430,7 +431,7 @@ void executesLifecycleMethods() { @Test void truncatesArgumentsThatExceedMaxLength() { var results = EngineTestKit.engine(new JupiterTestEngine()) // - .configurationParameter(ParameterizedTestExtension.ARGUMENT_MAX_LENGTH_KEY, "2") // + .configurationParameter(ParameterizedInvocationNameFormatter.ARGUMENT_MAX_LENGTH_KEY, "2") // .selectors(selectMethod(TestCase.class, "testWithCsvSource", String.class.getName())) // .execute(); results.testEvents().assertThatEvents() // @@ -441,7 +442,7 @@ void truncatesArgumentsThatExceedMaxLength() { @Test void displayNamePatternFromConfiguration() { var results = EngineTestKit.engine(new JupiterTestEngine()) // - .configurationParameter(ParameterizedTestExtension.DISPLAY_NAME_PATTERN_KEY, "{index}") // + .configurationParameter(ParameterizedInvocationNameFormatter.DISPLAY_NAME_PATTERN_KEY, "{index}") // .selectors(selectMethod(TestCase.class, "testWithCsvSource", String.class.getName())) // .execute(); results.testEvents().assertThatEvents() // @@ -450,27 +451,26 @@ void displayNamePatternFromConfiguration() { } @Test - void failsWhenArgumentsRequiredButNoneProvided() { - var result = execute(ZeroArgumentsTestCase.class, "testThatRequiresArguments", String.class); - result.containerEvents().assertThatEvents().haveExactly(1, event(finishedWithFailure(message( - "Configuration error: You must configure at least one set of arguments for this @ParameterizedTest")))); + 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")))); } @Test - void doesNotFailWhenArgumentsAreNotRequiredAndNoneProvided() { - var result = execute(ZeroArgumentsTestCase.class, "testThatDoesNotRequireArguments", String.class); - result.allEvents().assertEventsMatchExactly( // - event(engine(), started()), event(container(ZeroArgumentsTestCase.class), started()), - event(container("testThatDoesNotRequireArguments"), started()), - event(container("testThatDoesNotRequireArguments"), finishedSuccessfully()), - event(container(ZeroArgumentsTestCase.class), finishedSuccessfully()), - event(engine(), finishedSuccessfully())); + void doesNotFailWhenInvocationIsNotRequiredAndNoArgumentSetsAreProvided() { + var results = execute(ZeroInvocationsTestCase.class, "testThatDoesNotRequireInvocations", String.class); + + results.allEvents().assertStatistics(stats -> stats.started(3).succeeded(3)); } @Test void failsWhenNoArgumentsSourceIsDeclared() { - var result = execute(ZeroArgumentsTestCase.class, "testThatHasNoArgumentsSource", String.class); - result.containerEvents().assertThatEvents() // + var results = execute(ZeroInvocationsTestCase.class, "testThatHasNoArgumentsSource", String.class); + + results.containerEvents().assertThatEvents() // .haveExactly(1, // event(displayName("testThatHasNoArgumentsSource(String)"), finishedWithFailure(message( "Configuration error: You must configure at least one arguments source for this @ParameterizedTest")))); @@ -523,7 +523,7 @@ void failsWithNullSourceWithZeroFormalParameters() { finishedWithFailure(// instanceOf(PreconditionViolationException.class), // message(msg -> msg.matches( - "@NullSource cannot provide a null argument to method .+: the method does not declare any formal parameters."))))); + "@NullSource cannot provide a null argument to method .+: no formal parameters declared."))))); } @Test @@ -663,7 +663,7 @@ void failsWithEmptySourceWithZeroFormalParameters() { finishedWithFailure(// instanceOf(PreconditionViolationException.class), // message(msg -> msg.matches( - "@EmptySource cannot provide an empty argument to method .+: the method does not declare any formal parameters."))))); + "@EmptySource cannot provide an empty argument to method .+: no formal parameters declared."))))); } @ParameterizedTest(name = "{1}") @@ -1129,8 +1129,8 @@ void failsWithArgumentsSourceProvidingUnusedArguments() { var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class, "testWithTwoUnusedStringArgumentsProvider", String.class); results.allEvents().assertThatEvents() // - .haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format( - "Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]"))))); + .haveExactly(1, event(finishedWithFailure(message(String.format( + "Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused1]"))))); } @Test @@ -1138,8 +1138,8 @@ void failsWithMethodSourceProvidingUnusedArguments() { var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class, "testWithMethodSourceProvidingUnusedArguments", String.class); results.allEvents().assertThatEvents() // - .haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format( - "Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]"))))); + .haveExactly(1, event(finishedWithFailure(message(String.format( + "Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused1]"))))); } @Test @@ -1147,8 +1147,8 @@ void failsWithCsvSourceUnusedArgumentsAndStrictArgumentCountValidationAnnotation var results = execute(ArgumentCountValidationMode.NONE, UnusedArgumentsTestCase.class, "testWithStrictArgumentCountValidation", String.class); results.allEvents().assertThatEvents() // - .haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format( - "Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]"))))); + .haveExactly(1, event(finishedWithFailure(message(String.format( + "Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused1]"))))); } @Test @@ -1156,8 +1156,8 @@ void failsWithCsvSourceUnusedArgumentsButExecutesRemainingArgumentsWhereThereIsN var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class, "testWithCsvSourceContainingDifferentNumbersOfArguments", String.class); results.allEvents().assertThatEvents() // - .haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format( - "Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]"))))) // + .haveExactly(1, event(finishedWithFailure(message(String.format( + "Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused1]"))))) // .haveExactly(1, event(test(), displayName("[2] argument=bar"), finishedWithFailure(message("bar")))); } @@ -1180,6 +1180,17 @@ void executesWithMethodSourceProvidingUnusedArguments() { .haveExactly(1, event(test(), displayName("[2] argument=b"), finishedWithFailure(message("b")))); } + @Test + void evaluatesArgumentsAtMostOnce() { + var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class, + "testWithEvaluationReportingArgumentsProvider", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message(String.format( + "Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused]"))))); + results.allEvents().reportingEntryPublished().assertThatEvents() // + .haveExactly(1, event(EventConditions.reportEntry(Map.of("evaluated", "true")))); + } + private EngineExecutionResults execute(ArgumentCountValidationMode configurationValue, Class javaClass, String methodName, Class... methodParameterTypes) { return EngineTestKit.engine(new JupiterTestEngine()) // @@ -1299,7 +1310,25 @@ void closeAutoCloseableArgumentsAfterTest() { results.allEvents().assertThatEvents() // .haveExactly(1, event(test(), finishedSuccessfully())); - assertTrue(AutoCloseableArgument.isClosed); + assertEquals(2, AutoCloseableArgument.closeCounter); + } + + @Test + void doNotCloseAutoCloseableArgumentsAfterTestWhenDisabled() { + var results = execute("testWithAutoCloseableArgumentButDisabledCleanup", AutoCloseableArgument.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), finishedSuccessfully())); + + assertEquals(0, AutoCloseableArgument.closeCounter); + } + + @Test + void closeAutoCloseableArgumentsAfterTestDespiteEarlyFailure() { + var results = execute(FailureInBeforeEachTestCase.class, "test", AutoCloseableArgument.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), finishedWithFailure(message("beforeEach")))); + + assertEquals(2, AutoCloseableArgument.closeCounter); } @Test @@ -1430,7 +1459,13 @@ void testWithIgnoreLeadingAndTrailingWhitespaceSetToTrueForCsvFileSource(String @ParameterizedTest @ArgumentsSource(AutoCloseableArgumentProvider.class) void testWithAutoCloseableArgument(AutoCloseableArgument autoCloseable) { - assertFalse(AutoCloseableArgument.isClosed); + assertEquals(0, AutoCloseableArgument.closeCounter); + } + + @ParameterizedTest(autoCloseArguments = false) + @ArgumentsSource(AutoCloseableArgumentProvider.class) + void testWithAutoCloseableArgumentButDisabledCleanup(AutoCloseableArgument autoCloseable) { + assertEquals(0, AutoCloseableArgument.closeCounter); } @ParameterizedTest @@ -2123,6 +2158,24 @@ void testWithNoneArgumentCountValidation(String argument) { void testWithCsvSourceContainingDifferentNumbersOfArguments(String argument) { fail(argument); } + + @ParameterizedTest + @ArgumentsSource(EvaluationReportingArgumentsProvider.class) + void testWithEvaluationReportingArgumentsProvider(String argument) { + fail(argument); + } + + private static class EvaluationReportingArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { + return Stream.of(() -> { + context.publishReportEntry("evaluated", "true"); + return List.of("foo", "unused").toArray(); + }); + } + } } static class LifecycleTestCase { @@ -2400,7 +2453,8 @@ void argumentsAggregatorWithConstructorParameter( record ArgumentsProviderWithConstructorParameter(String value) implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of(arguments(value)); } } @@ -2413,27 +2467,33 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo } } - record ArgumentsAggregatorWithConstructorParameter(String value) implements ArgumentsAggregator { + static class ArgumentsAggregatorWithConstructorParameter extends SimpleArgumentsAggregator { + + private final String value; + + public ArgumentsAggregatorWithConstructorParameter(String value) { + this.value = value; + } @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) - throws ArgumentsAggregationException { - return value; + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { + return this.value; } } } - static class ZeroArgumentsTestCase { + static class ZeroInvocationsTestCase { @ParameterizedTest @MethodSource("zeroArgumentsProvider") - void testThatRequiresArguments(String argument) { + void testThatRequiresInvocations(String argument) { fail("This test should not be executed, because no arguments are provided."); } @ParameterizedTest(allowZeroInvocations = true) @MethodSource("zeroArgumentsProvider") - void testThatDoesNotRequireArguments(String argument) { + void testThatDoesNotRequireInvocations(String argument) { fail("This test should not be executed, because no arguments are provided."); } @@ -2451,7 +2511,8 @@ public static Stream zeroArgumentsProvider() { private static class TwoSingleStringArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of(arguments("foo"), arguments("bar")); } } @@ -2459,7 +2520,8 @@ public Stream provideArguments(ExtensionContext context) { private static class TwoUnusedStringArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of(arguments("foo", "unused1"), arguments("bar", "unused2")); } } @@ -2472,11 +2534,11 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo } } - private static class StringAggregator implements ArgumentsAggregator { + private static class StringAggregator extends SimpleArgumentsAggregator { @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) - throws ArgumentsAggregationException { + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { return accessor.getString(0) + accessor.getString(1); } } @@ -2492,18 +2554,19 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo private static class AutoCloseableArgumentProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(arguments(new AutoCloseableArgument())); + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { + return Stream.of(arguments(new AutoCloseableArgument(), Named.of("unused", new AutoCloseableArgument()))); } } static class AutoCloseableArgument implements AutoCloseable { - static boolean isClosed = false; + static int closeCounter = 0; @Override public void close() { - isClosed = true; + closeCounter++; } } @@ -2520,4 +2583,19 @@ static Book factory(String title) { } } + static class FailureInBeforeEachTestCase { + + @BeforeEach + void beforeEach() { + fail("beforeEach"); + } + + @ParameterizedTest + @ArgumentsSource(AutoCloseableArgumentProvider.class) + void test(AutoCloseableArgument autoCloseable) { + assertNotNull(autoCloseable); + assertEquals(0, AutoCloseableArgument.closeCounter); + } + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/AggregatorIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/AggregatorIntegrationTests.java index 90bfcb7b367b..999a101abd21 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/AggregatorIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/AggregatorIntegrationTests.java @@ -37,6 +37,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.AnnotatedElementContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.parallel.ResourceLock; @@ -278,27 +279,29 @@ static class Address { @interface CsvToAddress { } - static class PersonAggregator implements ArgumentsAggregator { + static class PersonAggregator extends SimpleArgumentsAggregator { @Override - public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { + protected Person aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) { int startIndex = context.findAnnotation(StartIndex.class).map(StartIndex::value).orElse(0); // @formatter:off return new Person( - arguments.getString(startIndex + 0), - arguments.getString(startIndex + 1), - arguments.get(startIndex + 2, LocalDate.class), - arguments.get(startIndex + 3, Gender.class) + accessor.getString(startIndex + 0), + accessor.getString(startIndex + 1), + accessor.get(startIndex + 2, LocalDate.class), + accessor.get(startIndex + 3, Gender.class) ); // @formatter:on } } - static class AddressAggregator implements ArgumentsAggregator { + static class AddressAggregator extends SimpleArgumentsAggregator { @Override - public Address aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { + public Address aggregateArguments(ArgumentsAccessor arguments, Class targetType, + AnnotatedElementContext context, int parameterIndex) { int startIndex = context.findAnnotation(StartIndex.class).map(StartIndex::value).orElse(0); // @formatter:off @@ -314,10 +317,11 @@ public Address aggregateArguments(ArgumentsAccessor arguments, ParameterContext /** * Maps from String to length of String. */ - static class MapAggregator implements ArgumentsAggregator { + static class MapAggregator extends SimpleArgumentsAggregator { @Override - public Map aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { + protected Map aggregateArguments(ArgumentsAccessor arguments, Class targetType, + AnnotatedElementContext context, int parameterIndex) { // @formatter:off return IntStream.range(0, arguments.size()) .mapToObj(arguments::getString) @@ -326,19 +330,19 @@ public Map aggregateArguments(ArgumentsAccessor arguments, Para } } - static class NullAggregator implements ArgumentsAggregator { + static class NullAggregator extends SimpleArgumentsAggregator { @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) { - Preconditions.condition(!context.getParameter().getType().isPrimitive(), - () -> "only supports reference types"); + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) { + Preconditions.condition(!targetType.isPrimitive(), () -> "only supports reference types"); return null; } } - static class ErroneousAggregator implements ArgumentsAggregator { + static class ErroneousAggregator extends SimpleArgumentsAggregator { @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) - throws ArgumentsAggregationException { + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { throw new ArgumentsAggregationException("something went horribly wrong"); } } @@ -392,7 +396,7 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo } } - static class InstanceCountingAggregator implements ArgumentsAggregator { + static class InstanceCountingAggregator extends SimpleArgumentsAggregator { static int instanceCount; InstanceCountingAggregator() { @@ -400,8 +404,8 @@ static class InstanceCountingAggregator implements ArgumentsAggregator { } @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) - throws ArgumentsAggregationException { + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { return "enigma"; } } 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 22d1ce685a67..792bae865f48 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,16 +16,11 @@ 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 static org.mockito.Mockito.when; -import java.lang.reflect.Method; import java.util.Arrays; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ParameterContext; import org.junit.platform.commons.PreconditionViolationException; -import org.junit.platform.commons.support.ReflectionSupport; /** * Unit tests for {@link DefaultArgumentsAccessor}. @@ -169,14 +164,8 @@ void size() { } private static DefaultArgumentsAccessor defaultArgumentsAccessor(int invocationIndex, Object... arguments) { - return new DefaultArgumentsAccessor(parameterContext(), invocationIndex, arguments); - } - - private static ParameterContext parameterContext() { - Method declaringExecutable = ReflectionSupport.findMethod(DefaultArgumentsAccessorTests.class, "foo").get(); - ParameterContext parameterContext = mock(); - when(parameterContext.getDeclaringExecutable()).thenReturn(declaringExecutable); - return parameterContext; + var classLoader = DefaultArgumentsAccessorTests.class.getClassLoader(); + return DefaultArgumentsAccessor.create(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 609f83cd95d3..5336690e1c1e 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,45 +12,21 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.File; -import java.lang.Thread.State; -import java.lang.reflect.Method; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.URI; -import java.net.URL; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.MonthDay; -import java.time.OffsetDateTime; -import java.time.OffsetTime; -import java.time.Period; -import java.time.Year; -import java.time.YearMonth; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.Currency; -import java.util.Locale; -import java.util.UUID; -import java.util.concurrent.TimeUnit; +import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.support.conversion.ConversionException; import org.junit.platform.commons.test.TestClassLoader; +import org.junit.platform.commons.util.ClassLoaderUtils; /** * Unit tests for {@link DefaultArgumentConverter}. @@ -59,6 +35,8 @@ */ class DefaultArgumentConverterTests { + private final DefaultArgumentConverter underTest = spy(DefaultArgumentConverter.INSTANCE); + @Test void isAwareOfNull() { assertConverts(null, Object.class, null); @@ -92,44 +70,6 @@ void isAwareOfWideningConversions() { assertConverts(1.0f, double.class, 1.0f); } - @Test - void convertsStringsToPrimitiveTypes() { - assertConverts("true", boolean.class, true); - assertConverts("false", boolean.class, false); - assertConverts("o", char.class, 'o'); - assertConverts("1", byte.class, (byte) 1); - assertConverts("1_0", byte.class, (byte) 10); - assertConverts("1", short.class, (short) 1); - assertConverts("1_2", short.class, (short) 12); - assertConverts("42", int.class, 42); - assertConverts("700_050_000", int.class, 700_050_000); - assertConverts("42", long.class, 42L); - assertConverts("4_2", long.class, 42L); - assertConverts("42.23", float.class, 42.23f); - assertConverts("42.2_3", float.class, 42.23f); - assertConverts("42.23", double.class, 42.23); - assertConverts("42.2_3", double.class, 42.23); - } - - @Test - void convertsStringsToPrimitiveWrapperTypes() { - assertConverts("true", Boolean.class, true); - assertConverts("false", Boolean.class, false); - assertConverts("o", Character.class, 'o'); - assertConverts("1", Byte.class, (byte) 1); - assertConverts("1_0", Byte.class, (byte) 10); - assertConverts("1", Short.class, (short) 1); - assertConverts("1_2", Short.class, (short) 12); - assertConverts("42", Integer.class, 42); - assertConverts("700_050_000", Integer.class, 700_050_000); - assertConverts("42", Long.class, 42L); - assertConverts("4_2", Long.class, 42L); - assertConverts("42.23", Float.class, 42.23f); - assertConverts("42.2_3", Float.class, 42.23f); - assertConverts("42.23", Double.class, 42.23); - assertConverts("42.2_3", Double.class, 42.23); - } - @ParameterizedTest(name = "[{index}] {0}") @ValueSource(classes = { char.class, boolean.class, short.class, byte.class, int.class, long.class, float.class, double.class, void.class }) @@ -137,223 +77,61 @@ void throwsExceptionForNullToPrimitiveTypeConversion(Class type) { assertThatExceptionOfType(ArgumentConversionException.class) // .isThrownBy(() -> convert(null, type)) // .withMessage("Cannot convert null to primitive value of type " + type.getCanonicalName()); - } - @ParameterizedTest(name = "[{index}] {0}") - @ValueSource(classes = { Boolean.class, Character.class, Short.class, Byte.class, Integer.class, Long.class, - Float.class, Double.class }) - void throwsExceptionWhenConvertingTheWordNullToPrimitiveWrapperType(Class type) { - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("null", type)) // - .withMessage("Failed to convert String \"null\" to type " + type.getCanonicalName()); - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("NULL", type)) // - .withMessage("Failed to convert String \"NULL\" to type " + type.getCanonicalName()); + verify(underTest, never()).convert(any(), any(), any(ClassLoader.class)); } @Test - void throwsExceptionOnInvalidStringForPrimitiveTypes() { - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("ab", char.class)) // - .withMessage("Failed to convert String \"ab\" to type char") // - .havingCause() // - .havingCause() // - .withMessage("String must have length of 1: ab"); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("tru", boolean.class)) // - .withMessage("Failed to convert String \"tru\" to type boolean") // - .havingCause() // - .havingCause() // - .withMessage("String must be 'true' or 'false' (ignoring case): tru"); - + void throwsExceptionForNonStringsConversion() { assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("null", boolean.class)) // - .withMessage("Failed to convert String \"null\" to type boolean") // - .havingCause() // - .havingCause() // - .withMessage("String must be 'true' or 'false' (ignoring case): null"); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("NULL", boolean.class)) // - .withMessage("Failed to convert String \"NULL\" to type boolean") // - .havingCause() // - .havingCause() // - .withMessage("String must be 'true' or 'false' (ignoring case): NULL"); - } - - @Test - void throwsExceptionWhenImplicitConverstionIsUnsupported() { - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("foo", Enigma.class)) // - .withMessage("No built-in converter for source type java.lang.String and target type %s", + .isThrownBy(() -> convert(new Enigma(), String.class)) // + .withMessage("No built-in converter for source type %s and target type java.lang.String", Enigma.class.getName()); - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert(new Enigma(), int[].class)) // - .withMessage("No built-in converter for source type %s and target type int[]", Enigma.class.getName()); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert(new long[] {}, int[].class)) // - .withMessage("No built-in converter for source type long[] and target type int[]"); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert(new String[] {}, boolean.class)) // - .withMessage("No built-in converter for source type java.lang.String[] and target type boolean"); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert(Class.class, int[].class)) // - .withMessage("No built-in converter for source type java.lang.Class and target type int[]"); - } - - /** - * @since 5.4 - */ - @Test - @SuppressWarnings("OctalInteger") // We test parsing octal integers here as well as hex. - void convertsEncodedStringsToIntegralTypes() { - assertConverts("0x1f", byte.class, (byte) 0x1F); - assertConverts("-0x1F", byte.class, (byte) -0x1F); - assertConverts("010", byte.class, (byte) 010); - - assertConverts("0x1f00", short.class, (short) 0x1F00); - assertConverts("-0x1F00", short.class, (short) -0x1F00); - assertConverts("01000", short.class, (short) 01000); - - assertConverts("0x1f000000", int.class, 0x1F000000); - assertConverts("-0x1F000000", int.class, -0x1F000000); - assertConverts("010000000", int.class, 010000000); - - assertConverts("0x1f000000000", long.class, 0x1F000000000L); - assertConverts("-0x1F000000000", long.class, -0x1F000000000L); - assertConverts("0100000000000", long.class, 0100000000000L); + verify(underTest, never()).convert(any(), any(), any(ClassLoader.class)); } @Test - void convertsStringsToEnumConstants() { - assertConverts("DAYS", TimeUnit.class, TimeUnit.DAYS); - } + void delegatesStringsConversion() { + doReturn(null).when(underTest).convert(any(), any(), any(ClassLoader.class)); - // --- java.io and java.nio ------------------------------------------------ + convert("value", int.class); - @Test - void convertsStringToCharset() { - assertConverts("ISO-8859-1", Charset.class, StandardCharsets.ISO_8859_1); - assertConverts("UTF-8", Charset.class, StandardCharsets.UTF_8); + verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class)); } @Test - void convertsStringToFile() { - assertConverts("file", File.class, new File("file")); - assertConverts("/file", File.class, new File("/file")); - assertConverts("/some/file", File.class, new File("/some/file")); - } - - @Test - void convertsStringToPath() { - assertConverts("path", Path.class, Paths.get("path")); - assertConverts("/path", Path.class, Paths.get("/path")); - assertConverts("/some/path", Path.class, Paths.get("/some/path")); - } + void throwsExceptionForDelegatedConversionFailure() { + ConversionException exception = new ConversionException("fail"); + doThrow(exception).when(underTest).convert(any(), any(), any(ClassLoader.class)); - // --- java.lang ----------------------------------------------------------- + assertThatExceptionOfType(ArgumentConversionException.class) // + .isThrownBy(() -> convert("value", int.class)) // + .withCause(exception) // + .withMessage(exception.getMessage()); - @Test - void convertsStringToClass() { - assertConverts("java.lang.Integer", Class.class, Integer.class); - assertConverts("java.lang.Void", Class.class, Void.class); - assertConverts("java.lang.Thread$State", Class.class, State.class); - assertConverts("byte", Class.class, byte.class); - assertConverts("void", Class.class, void.class); - assertConverts("char[]", Class.class, char[].class); - assertConverts("java.lang.Long[][]", Class.class, Long[][].class); - assertConverts("[[[I", Class.class, int[][][].class); - assertConverts("[[Ljava.lang.String;", Class.class, String[][].class); + verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class)); } @Test - void convertsStringToClassWithCustomTypeFromDifferentClassLoader() throws Exception { + void delegatesStringToClassWithCustomTypeFromDifferentClassLoaderConversion() throws Exception { String customTypeName = Enigma.class.getName(); try (var testClassLoader = TestClassLoader.forClasses(Enigma.class)) { var customType = testClassLoader.loadClass(customTypeName); assertThat(customType.getClassLoader()).isSameAs(testClassLoader); - var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").get(); + var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").orElseThrow(); assertThat(declaringExecutable.getDeclaringClass().getClassLoader()).isSameAs(testClassLoader); - var clazz = (Class) convert(customTypeName, Class.class, parameterContext(declaringExecutable)); + doReturn(customType).when(underTest).convert(any(), any(), any(ClassLoader.class)); + + var clazz = (Class) convert(customTypeName, Class.class, testClassLoader); assertThat(clazz).isNotEqualTo(Enigma.class); assertThat(clazz).isEqualTo(customType); assertThat(clazz.getClassLoader()).isSameAs(testClassLoader); - } - } - - // --- java.math ----------------------------------------------------------- - - @Test - void convertsStringToBigDecimal() { - assertConverts("123.456e789", BigDecimal.class, new BigDecimal("123.456e789")); - } - - @Test - void convertsStringToBigInteger() { - assertConverts("1234567890123456789", BigInteger.class, new BigInteger("1234567890123456789")); - } - - // --- java.net ------------------------------------------------------------ - - @Test - void convertsStringToURI() { - assertConverts("https://docs.oracle.com/en/java/javase/12/", URI.class, - URI.create("https://docs.oracle.com/en/java/javase/12/")); - } - - @Test - void convertsStringToURL() throws Exception { - assertConverts("https://junit.org/junit5", URL.class, URI.create("https://junit.org/junit5").toURL()); - } - // --- java.time ----------------------------------------------------------- - - @Test - void convertsStringsToJavaTimeInstances() { - assertConverts("PT1234.5678S", Duration.class, Duration.ofSeconds(1234, 567800000)); - assertConverts("1970-01-01T00:00:00Z", Instant.class, Instant.ofEpochMilli(0)); - assertConverts("2017-03-14", LocalDate.class, LocalDate.of(2017, 3, 14)); - assertConverts("2017-03-14T12:34:56.789", LocalDateTime.class, - LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)); - assertConverts("12:34:56.789", LocalTime.class, LocalTime.of(12, 34, 56, 789_000_000)); - assertConverts("--03-14", MonthDay.class, MonthDay.of(3, 14)); - assertConverts("2017-03-14T12:34:56.789Z", OffsetDateTime.class, - OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)); - assertConverts("12:34:56.789Z", OffsetTime.class, OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC)); - assertConverts("P2M6D", Period.class, Period.of(0, 2, 6)); - assertConverts("2017", Year.class, Year.of(2017)); - assertConverts("2017-03", YearMonth.class, YearMonth.of(2017, 3)); - assertConverts("2017-03-14T12:34:56.789Z", ZonedDateTime.class, - ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)); - assertConverts("Europe/Berlin", ZoneId.class, ZoneId.of("Europe/Berlin")); - assertConverts("+02:30", ZoneOffset.class, ZoneOffset.ofHoursMinutes(2, 30)); - } - - // --- java.util ----------------------------------------------------------- - - @Test - void convertsStringToCurrency() { - assertConverts("JPY", Currency.class, Currency.getInstance("JPY")); - } - - @Test - @SuppressWarnings("deprecation") - void convertsStringToLocale() { - assertConverts("en", Locale.class, Locale.ENGLISH); - assertConverts("en_us", Locale.class, new Locale(Locale.US.toString())); - } - - @Test - void convertsStringToUUID() { - var uuid = "d043e930-7b3b-48e3-bdbe-5a3ccfb833db"; - assertConverts(uuid, UUID.class, UUID.fromString(uuid)); + verify(underTest).convert(customTypeName, Class.class, testClassLoader); + } } // ------------------------------------------------------------------------- @@ -364,25 +142,16 @@ private void assertConverts(Object input, Class targetClass, Object expectedO assertThat(result) // .describedAs(input + " --(" + targetClass.getName() + ")--> " + expectedOutput) // .isEqualTo(expectedOutput); - } - private Object convert(Object input, Class targetClass) { - return convert(input, targetClass, parameterContext()); - } - - private Object convert(Object input, Class targetClass, ParameterContext parameterContext) { - return DefaultArgumentConverter.INSTANCE.convert(input, targetClass, parameterContext); + verify(underTest, never()).convert(any(), any(), any(ClassLoader.class)); } - private static ParameterContext parameterContext() { - Method declaringExecutable = ReflectionSupport.findMethod(DefaultArgumentConverterTests.class, "foo").get(); - return parameterContext(declaringExecutable); + private Object convert(Object input, Class targetClass) { + return convert(input, targetClass, ClassLoaderUtils.getClassLoader(getClass())); } - private static ParameterContext parameterContext(Method declaringExecutable) { - ParameterContext parameterContext = mock(); - when(parameterContext.getDeclaringExecutable()).thenReturn(declaringExecutable); - return parameterContext; + private Object convert(Object input, Class targetClass, ClassLoader classLoader) { + return underTest.convert(input, targetClass, classLoader); } @SuppressWarnings("unused") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java index 0cbbb252204c..885ba7a77591 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java @@ -24,13 +24,16 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; @DisplayName("AnnotationBasedArgumentsProvider") class AnnotationBasedArgumentsProviderTests { private final AnnotationBasedArgumentsProvider annotationBasedArgumentsProvider = new AnnotationBasedArgumentsProvider<>() { @Override - protected Stream provideArguments(ExtensionContext context, CsvSource annotation) { + protected Stream provideArguments( + org.junit.jupiter.params.support.ParameterDeclarations parameters, ExtensionContext context, + CsvSource annotation) { return Stream.of(Arguments.of(annotation)); } }; @@ -46,18 +49,20 @@ void shouldThrowExceptionWhenNullAnnotationIsProvidedToAccept() { @DisplayName("should invoke the provideArguments template method with the accepted annotation") void shouldInvokeTemplateMethodWithTheAnnotationProvidedToAccept() { var spiedProvider = spy(annotationBasedArgumentsProvider); + var parameters = mock(org.junit.jupiter.params.support.ParameterDeclarations.class); var extensionContext = mock(ExtensionContext.class); var annotation = csvSource("0", "1", "2"); annotationBasedArgumentsProvider.accept(annotation); - annotationBasedArgumentsProvider.provideArguments(extensionContext); + annotationBasedArgumentsProvider.provideArguments(parameters, extensionContext); - verify(spiedProvider, atMostOnce()).provideArguments(eq(extensionContext), eq(annotation)); + verify(spiedProvider, atMostOnce()).provideArguments(eq(parameters), eq(extensionContext), eq(annotation)); } @Test @DisplayName("should invoke the provideArguments template method for every accepted annotation") void shouldInvokeTemplateMethodForEachAnnotationProvided() { + var parameters = mock(ParameterDeclarations.class); var extensionContext = mock(ExtensionContext.class); var foo = csvSource("foo"); var bar = csvSource("bar"); @@ -65,7 +70,7 @@ void shouldInvokeTemplateMethodForEachAnnotationProvided() { annotationBasedArgumentsProvider.accept(foo); annotationBasedArgumentsProvider.accept(bar); - var arguments = annotationBasedArgumentsProvider.provideArguments(extensionContext).toList(); + var arguments = annotationBasedArgumentsProvider.provideArguments(parameters, extensionContext).toList(); assertThat(arguments).hasSize(2); assertThat(arguments.getFirst().get()[0]).isEqualTo(foo); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java index 4a1e9722e0a9..9beac70e9616 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java @@ -18,6 +18,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; @@ -382,7 +383,7 @@ void throwsExceptionIfColumnCountExceedsHeaderCount() { private Stream provideArguments(CsvSource annotation) { var provider = new CsvArgumentsProvider(); provider.accept(annotation); - return provider.provideArguments(mock()).map(Arguments::get); + return provider.provideArguments(mock(), mock(ExtensionContext.class)).map(Arguments::get); } @SuppressWarnings("unchecked") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java index 63f5a7aa3a76..3a7269f4ee0f 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java @@ -544,7 +544,7 @@ private Stream provideArguments(CsvFileArgumentsProvider provider, Csv var context = mock(ExtensionContext.class); when(context.getTestClass()).thenReturn(Optional.of(CsvFileArgumentsProviderTests.class)); doCallRealMethod().when(context).getRequiredTestClass(); - return provider.provideArguments(context).map(Arguments::get); + return provider.provideArguments(mock(), context).map(Arguments::get); } @SuppressWarnings("unchecked") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java index 8d2d5cfbd170..e15bc98e7308 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java @@ -20,11 +20,14 @@ import static org.mockito.Mockito.when; import java.util.Arrays; +import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.provider.EnumSource.Mode; +import org.junit.jupiter.params.support.ParameterDeclaration; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.PreconditionViolationException; /** @@ -32,7 +35,8 @@ */ class EnumArgumentsProviderTests { - private ExtensionContext extensionContext = mock(); + final ParameterDeclarations parameters = mock(); + final ExtensionContext extensionContext = mock(); @Test void providesAllEnumConstants() { @@ -78,9 +82,10 @@ void invalidPatternIsDetected() { } @Test - void providesEnumConstantsBasedOnTestMethod() throws Exception { - when(extensionContext.getRequiredTestMethod()).thenReturn( - TestCase.class.getDeclaredMethod("methodWithCorrectParameter", EnumWithFourConstants.class)); + void providesEnumConstantsBasedOnTestMethod() { + org.junit.jupiter.params.support.ParameterDeclaration firstParameterDeclaration = mock(); + when(firstParameterDeclaration.getParameterType()).thenAnswer(__ -> EnumWithFourConstants.class); + when(parameters.getFirst()).thenReturn(Optional.of(firstParameterDeclaration)); var arguments = provideArguments(NullEnum.class); @@ -89,9 +94,10 @@ void providesEnumConstantsBasedOnTestMethod() throws Exception { } @Test - void incorrectParameterTypeIsDetected() throws Exception { - when(extensionContext.getRequiredTestMethod()).thenReturn( - TestCase.class.getDeclaredMethod("methodWithIncorrectParameter", Object.class)); + void incorrectParameterTypeIsDetected() { + ParameterDeclaration firstParameterDeclaration = mock(); + when(firstParameterDeclaration.getParameterType()).thenAnswer(__ -> Object.class); + when(parameters.getFirst()).thenReturn(Optional.of(firstParameterDeclaration)); var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments(NullEnum.class).findAny()); @@ -99,13 +105,12 @@ void incorrectParameterTypeIsDetected() throws Exception { } @Test - void methodsWithoutParametersAreDetected() throws Exception { - when(extensionContext.getRequiredTestMethod()).thenReturn( - TestCase.class.getDeclaredMethod("methodWithoutParameters")); + void methodsWithoutParametersAreDetected() { + when(parameters.getSourceElementDescription()).thenReturn("method"); var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments(NullEnum.class).findAny()); - assertThat(exception).hasMessageStartingWith("Test method must declare at least one parameter"); + assertThat(exception).hasMessageStartingWith("There must be at least one declared parameter for method"); } @Test @@ -179,12 +184,6 @@ void invalidRangeIsDetectedWhenEnumWithNoConstantIsProvided() { } static class TestCase { - void methodWithCorrectParameter(EnumWithFourConstants parameter) { - } - - void methodWithIncorrectParameter(Object parameter) { - } - void methodWithoutParameters() { } } @@ -218,7 +217,7 @@ private > Stream provideArguments(Class enumClass var provider = new EnumArgumentsProvider(); provider.accept(annotation); - return provider.provideArguments(extensionContext).map(Arguments::get); + return provider.provideArguments(parameters, extensionContext).map(Arguments::get); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/FieldArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/FieldArgumentsProviderTests.java index a8a395cea0ac..f8a9a9c6c299 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/FieldArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/FieldArgumentsProviderTests.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.execution.DefaultExecutableInvoker; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.support.ReflectionSupport; @@ -478,6 +479,7 @@ private static Stream provideArguments(Class testClass, Method test when(fieldSource.value()).thenReturn(fieldNames); + var parameters = mock(ParameterDeclarations.class); var extensionContext = mock(ExtensionContext.class); when(extensionContext.getTestClass()).thenReturn(Optional.of(testClass)); when(extensionContext.getTestMethod()).thenReturn(Optional.of(testMethod)); @@ -495,7 +497,7 @@ private static Stream provideArguments(Class testClass, Method test var provider = new FieldArgumentsProvider(); provider.accept(fieldSource); - return provider.provideArguments(extensionContext).map(Arguments::get); + return provider.provideArguments(parameters, extensionContext).map(Arguments::get); } // ------------------------------------------------------------------------- diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java index 50e8bcc83ee2..da3a2994bb8e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java @@ -250,8 +250,8 @@ void providesArgumentsUsingExternalFactoryMethodInTypeFromDifferentClassLoader() var arguments = provideArguments(testClass, false, fullyQualifiedMethodName); assertThat(arguments).containsExactly(array("string1"), array("string2")); - var factoryMethod = MethodArgumentsProvider.findFactoryMethodByFullyQualifiedName(testClass, testMethod, - fullyQualifiedMethodName); + var factoryMethod = MethodArgumentsProvider.findFactoryMethodByFullyQualifiedName(testClass, + Optional.of(testMethod), fullyQualifiedMethodName); assertThat(factoryMethod).isNotNull(); assertThat(factoryMethod.getName()).isEqualTo("stringsProvider"); assertThat(factoryMethod.getParameterTypes()).isEmpty(); @@ -759,7 +759,6 @@ private Stream provideArguments(Class testClass, Method testMethod, when(extensionContext.getExecutableInvoker()).thenReturn( new DefaultExecutableInvoker(extensionContext, extensionRegistry)); - doCallRealMethod().when(extensionContext).getRequiredTestMethod(); doCallRealMethod().when(extensionContext).getRequiredTestClass(); var testInstance = allowNonStaticMethod ? ReflectionUtils.newInstance(testClass) : null; @@ -770,7 +769,7 @@ private Stream provideArguments(Class testClass, Method testMethod, var provider = new MethodArgumentsProvider(); provider.accept(methodSource); - return provider.provideArguments(extensionContext).map(Arguments::get); + return provider.provideArguments(mock(), extensionContext).map(Arguments::get); } // ------------------------------------------------------------------------- diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java index bbfaf94b69f0..a601714565e8 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java @@ -18,6 +18,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.commons.PreconditionViolationException; /** @@ -160,7 +161,7 @@ private static Stream provideArguments(short[] shorts, byte[] bytes, i var provider = new ValueArgumentsProvider(); provider.accept(annotation); - return provider.provideArguments(mock()).map(Arguments::get); + return provider.provideArguments(mock(), mock(ExtensionContext.class)).map(Arguments::get); } private static Object[] array(Object... objects) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java index b304ec8f4baa..16306451bca4 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java @@ -58,7 +58,7 @@ void shouldInitializeAnnotationBasedArgumentsProvider() throws NoSuchMethodExcep var method = SubjectClass.class.getDeclaredMethod("foo"); var initialisedAnnotationConsumer = initialize(method, instance); - initialisedAnnotationConsumer.provideArguments(mock()).findAny(); + initialisedAnnotationConsumer.provideArguments(mock(), mock(ExtensionContext.class)).findAny(); assertThat(initialisedAnnotationConsumer.annotations) // .hasSize(1) // @@ -116,7 +116,8 @@ private static class SomeAnnotationBasedArgumentsProvider extends AnnotationBase List annotations = new ArrayList<>(); @Override - protected Stream provideArguments(ExtensionContext context, CsvSource annotation) { + protected Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context, CsvSource annotation) { annotations.add(annotation); return Stream.empty(); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/support/ParameterInfoIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/ParameterInfoIntegrationTests.java new file mode 100644 index 000000000000..2b62f8d691e3 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/ParameterInfoIntegrationTests.java @@ -0,0 +1,111 @@ +/* + * 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.support; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.BeforeClassTemplateInvocationCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +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; + +/** + * @since 5.13 + */ +class ParameterInfoIntegrationTests extends AbstractJupiterTestEngineTests { + + @Test + void storesParameterInfoInExtensionContextStoreOnDifferentLevels() { + var results = executeTestsForClass(TestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(7).succeeded(7)); + } + + @ParameterizedClass + @ValueSource(ints = 1) + @ExtendWith(ParameterInfoConsumingExtension.class) + record TestCase(int i) { + + @Nested + @ParameterizedClass + @ValueSource(ints = 2) + class Inner { + + @Parameter + int j; + + @ParameterizedTest + @ValueSource(ints = 3) + void test(int k) { + assertEquals(1, i); + assertEquals(2, j); + assertEquals(3, k); + } + } + } + + private static class ParameterInfoConsumingExtension + implements BeforeClassTemplateInvocationCallback, BeforeEachCallback { + + @Override + public void beforeClassTemplateInvocation(ExtensionContext parameterizedClassInvocationContext) { + if (TestCase.Inner.class.equals(parameterizedClassInvocationContext.getRequiredTestClass())) { + assertParameterInfo(parameterizedClassInvocationContext, "j", 2); + + var nestedParameterizedClassContext = parameterizedClassInvocationContext.getParent().orElseThrow(); + assertParameterInfo(nestedParameterizedClassContext, "i", 1); + + parameterizedClassInvocationContext = nestedParameterizedClassContext.getParent().orElseThrow(); + } + + assertParameterInfo(parameterizedClassInvocationContext, "i", 1); + + var outerParameterizedClassContext = parameterizedClassInvocationContext.getParent().orElseThrow(); + assertNull(ParameterInfo.get(outerParameterizedClassContext)); + } + + @Override + public void beforeEach(ExtensionContext parameterizedTestInvocationContext) { + assertParameterInfo(parameterizedTestInvocationContext, "k", 3); + + var parameterizedTestContext = parameterizedTestInvocationContext.getParent().orElseThrow(); + assertParameterInfo(parameterizedTestContext, "j", 2); + + var nestedParameterizedClassInvocationContext = parameterizedTestContext.getParent().orElseThrow(); + assertParameterInfo(nestedParameterizedClassInvocationContext, "j", 2); + + var nestedParameterizedClassContext = nestedParameterizedClassInvocationContext.getParent().orElseThrow(); + assertParameterInfo(nestedParameterizedClassContext, "i", 1); + + var outerParameterizedClassInvocationContext = nestedParameterizedClassContext.getParent().orElseThrow(); + assertParameterInfo(outerParameterizedClassInvocationContext, "i", 1); + + var outerParameterizedClassContext = outerParameterizedClassInvocationContext.getParent().orElseThrow(); + assertNull(ParameterInfo.get(outerParameterizedClassContext)); + } + + private static void assertParameterInfo(ExtensionContext context, String parameterName, int argumentValue) { + var parameterInfo = ParameterInfo.get(context); + var declaration = parameterInfo.getDeclarations().get(0).orElseThrow(); + assertEquals(parameterName, declaration.getParameterName().orElseThrow()); + assertEquals(int.class, declaration.getParameterType()); + assertEquals(argumentValue, parameterInfo.getArguments().getInteger(0)); + } + } +} diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedClassKotlinIntegrationTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedClassKotlinIntegrationTests.kt new file mode 100644 index 000000000000..2ff29de978e5 --- /dev/null +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedClassKotlinIntegrationTests.kt @@ -0,0 +1,56 @@ +/* + * 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 + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInfo +import org.junit.jupiter.params.provider.ValueSource +import org.junit.platform.engine.discovery.DiscoverySelectors.selectClass +import org.junit.platform.testkit.engine.EngineTestKit + +class ParameterizedClassKotlinIntegrationTests { + @Test + fun supportsDataClasses() { + val results = + EngineTestKit + .engine("junit-jupiter") + .selectors(selectClass(TestCase::class.java)) + .execute() + + results.containerEvents().assertStatistics { + it.started(4).succeeded(4) + } + results.testEvents().assertStatistics { + it.started(4).succeeded(2).failed(2) + } + } + + @Suppress("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = [-1, 1]) + data class TestCase( + val value: Int, + val testInfo: TestInfo + ) { + @Test + fun test1() { + assertEquals("test1()", testInfo.displayName) + assertTrue(value < 0, "negative") + } + + @Test + fun test2() { + assertEquals("test2()", testInfo.displayName) + assertTrue(value < 0, "negative") + } + } +} diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedTestNameFormatterIntegrationTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedInvocationNameFormatterIntegrationTests.kt similarity index 92% rename from jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedTestNameFormatterIntegrationTests.kt rename to jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedInvocationNameFormatterIntegrationTests.kt index 5ce7d3fabdae..7da01fb28742 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedTestNameFormatterIntegrationTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedInvocationNameFormatterIntegrationTests.kt @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.TestInfo import org.junit.jupiter.params.provider.ValueSource -class ParameterizedTestNameFormatterIntegrationTests { +class ParameterizedInvocationNameFormatterIntegrationTests { @ValueSource(strings = ["foo", "bar"]) @ParameterizedTest fun defaultDisplayName( @@ -21,9 +21,9 @@ class ParameterizedTestNameFormatterIntegrationTests { info: TestInfo ) { if (param.equals("foo")) { - assertEquals("[1] foo", info.displayName) + assertEquals("[1] param=foo", info.displayName) } else { - assertEquals("[2] bar", info.displayName) + assertEquals("[2] param=bar", info.displayName) } } 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 479a625775cc..eb1aa43ed5fb 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,11 +13,6 @@ 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.ParameterContext -import org.junit.platform.commons.util.ReflectionUtils -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import java.lang.reflect.Method /** * Unit tests for using [ArgumentsAccessor] from Kotlin. @@ -56,13 +51,9 @@ class ArgumentsAccessorKotlinTests { fun defaultArgumentsAccessor( invocationIndex: Int, vararg arguments: Any - ): DefaultArgumentsAccessor = DefaultArgumentsAccessor(parameterContext(), invocationIndex, *arguments) - - fun parameterContext(): ParameterContext { - val declaringExecutable: Method = ReflectionUtils.findMethod(DefaultArgumentsAccessorTests::class.java, "foo").get() - val parameterContext: ParameterContext = mock() - `when`(parameterContext.declaringExecutable).thenReturn(declaringExecutable) - return parameterContext + ): DefaultArgumentsAccessor { + val classLoader = ArgumentsAccessorKotlinTests::class.java.classLoader + return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments) } fun foo() { diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/DisplayNameTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/DisplayNameTests.kt index 12d9b336860f..6a1f91cbb19f 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/DisplayNameTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/DisplayNameTests.kt @@ -35,6 +35,6 @@ object DisplayNameTests { number: Int, info: TestInfo ) { - assertEquals("[$number] $char, $number", info.displayName) + assertEquals("[$number] char=$char, number=$number", info.displayName) } } diff --git a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java index 8c36d36b9929..d5dbca29324e 100644 --- a/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java +++ b/platform-tests/src/test/java/org/junit/platform/StackTracePruningTests.java @@ -174,7 +174,7 @@ private static List extractStackTrace(EngineExecutionResults } private static Throwable getThrowable(EngineExecutionResults results) { - var failedTestEvent = results.testEvents().failed().list().get(0); + var failedTestEvent = results.testEvents().failed().list().getFirst(); var testResult = failedTestEvent.getRequiredPayload(TestExecutionResult.class); return testResult.getThrowable().orElseThrow(); } diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/ConversionSupportTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/ConversionSupportTests.java new file mode 100644 index 000000000000..3a57fbe3b5a3 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/ConversionSupportTests.java @@ -0,0 +1,348 @@ +/* + * 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.commons.support.conversion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.io.File; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Currency; +import java.util.Locale; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.test.TestClassLoader; +import org.junit.platform.commons.util.ClassLoaderUtils; + +/** + * Unit tests for {@link ConversionSupport}. + * + * @since 5.12 + */ +class ConversionSupportTests { + + @Test + void isAwareOfNull() { + assertConverts(null, Object.class, null); + assertConverts(null, String.class, null); + assertConverts(null, Boolean.class, null); + } + + @Test + void convertsStringsToPrimitiveTypes() { + assertConverts("true", boolean.class, true); + assertConverts("false", boolean.class, false); + assertConverts("o", char.class, 'o'); + assertConverts("1", byte.class, (byte) 1); + assertConverts("1_0", byte.class, (byte) 10); + assertConverts("1", short.class, (short) 1); + assertConverts("1_2", short.class, (short) 12); + assertConverts("42", int.class, 42); + assertConverts("700_050_000", int.class, 700_050_000); + assertConverts("42", long.class, 42L); + assertConverts("4_2", long.class, 42L); + assertConverts("42.23", float.class, 42.23f); + assertConverts("42.2_3", float.class, 42.23f); + assertConverts("42.23", double.class, 42.23); + assertConverts("42.2_3", double.class, 42.23); + } + + @Test + void convertsStringsToPrimitiveWrapperTypes() { + assertConverts("true", Boolean.class, true); + assertConverts("false", Boolean.class, false); + assertConverts("o", Character.class, 'o'); + assertConverts("1", Byte.class, (byte) 1); + assertConverts("1_0", Byte.class, (byte) 10); + assertConverts("1", Short.class, (short) 1); + assertConverts("1_2", Short.class, (short) 12); + assertConverts("42", Integer.class, 42); + assertConverts("700_050_000", Integer.class, 700_050_000); + assertConverts("42", Long.class, 42L); + assertConverts("4_2", Long.class, 42L); + assertConverts("42.23", Float.class, 42.23f); + assertConverts("42.2_3", Float.class, 42.23f); + assertConverts("42.23", Double.class, 42.23); + assertConverts("42.2_3", Double.class, 42.23); + } + + @ParameterizedTest(name = "[{index}] {0}") + @ValueSource(classes = { char.class, boolean.class, short.class, byte.class, int.class, long.class, float.class, + double.class, void.class }) + void throwsExceptionForNullToPrimitiveTypeConversion(Class type) { + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert(null, type)) // + .withMessage("Cannot convert null to primitive value of type " + type.getCanonicalName()); + } + + @ParameterizedTest(name = "[{index}] {0}") + @ValueSource(classes = { Boolean.class, Character.class, Short.class, Byte.class, Integer.class, Long.class, + Float.class, Double.class }) + void throwsExceptionWhenConvertingTheWordNullToPrimitiveWrapperType(Class type) { + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("null", type)) // + .withMessage("Failed to convert String \"null\" to type " + type.getCanonicalName()); + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("NULL", type)) // + .withMessage("Failed to convert String \"NULL\" to type " + type.getCanonicalName()); + } + + @Test + void throwsExceptionOnInvalidStringForPrimitiveTypes() { + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("ab", char.class)) // + .withMessage("Failed to convert String \"ab\" to type char") // + .havingCause() // + .withMessage("String must have length of 1: ab"); + + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("tru", boolean.class)) // + .withMessage("Failed to convert String \"tru\" to type boolean") // + .havingCause() // + .withMessage("String must be 'true' or 'false' (ignoring case): tru"); + + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("null", boolean.class)) // + .withMessage("Failed to convert String \"null\" to type boolean") // + .havingCause() // + .withMessage("String must be 'true' or 'false' (ignoring case): null"); + + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("NULL", boolean.class)) // + .withMessage("Failed to convert String \"NULL\" to type boolean") // + .havingCause() // + .withMessage("String must be 'true' or 'false' (ignoring case): NULL"); + } + + @Test + void throwsExceptionWhenImplicitConversionIsUnsupported() { + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("foo", Enigma.class)) // + .withMessage("No built-in converter for source type java.lang.String and target type %s", + Enigma.class.getName()); + } + + /** + * @since 5.4 + */ + @Test + @SuppressWarnings("OctalInteger") // We test parsing octal integers here as well as hex. + void convertsEncodedStringsToIntegralTypes() { + assertConverts("0x1f", byte.class, (byte) 0x1F); + assertConverts("-0x1F", byte.class, (byte) -0x1F); + assertConverts("010", byte.class, (byte) 010); + + assertConverts("0x1f00", short.class, (short) 0x1F00); + assertConverts("-0x1F00", short.class, (short) -0x1F00); + assertConverts("01000", short.class, (short) 01000); + + assertConverts("0x1f000000", int.class, 0x1F000000); + assertConverts("-0x1F000000", int.class, -0x1F000000); + assertConverts("010000000", int.class, 010000000); + + assertConverts("0x1f000000000", long.class, 0x1F000000000L); + assertConverts("-0x1F000000000", long.class, -0x1F000000000L); + assertConverts("0100000000000", long.class, 0100000000000L); + } + + @Test + void convertsStringsToEnumConstants() { + assertConverts("DAYS", TimeUnit.class, TimeUnit.DAYS); + } + + // --- java.io and java.nio ------------------------------------------------ + + @Test + void convertsStringToCharset() { + assertConverts("ISO-8859-1", Charset.class, StandardCharsets.ISO_8859_1); + assertConverts("UTF-8", Charset.class, StandardCharsets.UTF_8); + } + + @Test + void convertsStringToFile() { + assertConverts("file", File.class, new File("file")); + assertConverts("/file", File.class, new File("/file")); + assertConverts("/some/file", File.class, new File("/some/file")); + } + + @Test + void convertsStringToPath() { + assertConverts("path", Path.class, Paths.get("path")); + assertConverts("/path", Path.class, Paths.get("/path")); + assertConverts("/some/path", Path.class, Paths.get("/some/path")); + } + + // --- java.lang ----------------------------------------------------------- + + @Test + void convertsStringToClass() { + assertConverts("java.lang.Integer", Class.class, Integer.class); + assertConverts("java.lang.Void", Class.class, Void.class); + assertConverts("java.lang.Thread$State", Class.class, Thread.State.class); + assertConverts("byte", Class.class, byte.class); + assertConverts("void", Class.class, void.class); + assertConverts("char[]", Class.class, char[].class); + assertConverts("java.lang.Long[][]", Class.class, Long[][].class); + assertConverts("[[[I", Class.class, int[][][].class); + assertConverts("[[Ljava.lang.String;", Class.class, String[][].class); + } + + @Test + void convertsStringToClassWithCustomTypeFromDifferentClassLoader() throws Exception { + String customTypeName = Enigma.class.getName(); + try (var testClassLoader = TestClassLoader.forClasses(Enigma.class)) { + var customType = testClassLoader.loadClass(customTypeName); + assertThat(customType.getClassLoader()).isSameAs(testClassLoader); + + var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").get(); + assertThat(declaringExecutable.getDeclaringClass().getClassLoader()).isSameAs(testClassLoader); + + var clazz = (Class) convert(customTypeName, Class.class, classLoader(declaringExecutable)); + assertThat(clazz).isNotEqualTo(Enigma.class); + assertThat(clazz).isEqualTo(customType); + assertThat(clazz.getClassLoader()).isSameAs(testClassLoader); + } + } + + // --- java.math ----------------------------------------------------------- + + @Test + void convertsStringToBigDecimal() { + assertConverts("123.456e789", BigDecimal.class, new BigDecimal("123.456e789")); + } + + @Test + void convertsStringToBigInteger() { + assertConverts("1234567890123456789", BigInteger.class, new BigInteger("1234567890123456789")); + } + + // --- java.net ------------------------------------------------------------ + + @Test + void convertsStringToURI() { + assertConverts("https://docs.oracle.com/en/java/javase/12/", URI.class, + URI.create("https://docs.oracle.com/en/java/javase/12/")); + } + + @Test + void convertsStringToURL() throws Exception { + assertConverts("https://junit.org/junit5", URL.class, URI.create("https://junit.org/junit5").toURL()); + } + + // --- java.time ----------------------------------------------------------- + + @Test + void convertsStringsToJavaTimeInstances() { + assertConverts("PT1234.5678S", Duration.class, Duration.ofSeconds(1234, 567800000)); + assertConverts("1970-01-01T00:00:00Z", Instant.class, Instant.ofEpochMilli(0)); + assertConverts("2017-03-14", LocalDate.class, LocalDate.of(2017, 3, 14)); + assertConverts("2017-03-14T12:34:56.789", LocalDateTime.class, + LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)); + assertConverts("12:34:56.789", LocalTime.class, LocalTime.of(12, 34, 56, 789_000_000)); + assertConverts("--03-14", MonthDay.class, MonthDay.of(3, 14)); + assertConverts("2017-03-14T12:34:56.789Z", OffsetDateTime.class, + OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)); + assertConverts("12:34:56.789Z", OffsetTime.class, OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC)); + assertConverts("P2M6D", Period.class, Period.of(0, 2, 6)); + assertConverts("2017", Year.class, Year.of(2017)); + assertConverts("2017-03", YearMonth.class, YearMonth.of(2017, 3)); + assertConverts("2017-03-14T12:34:56.789Z", ZonedDateTime.class, + ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)); + assertConverts("Europe/Berlin", ZoneId.class, ZoneId.of("Europe/Berlin")); + assertConverts("+02:30", ZoneOffset.class, ZoneOffset.ofHoursMinutes(2, 30)); + } + + // --- java.util ----------------------------------------------------------- + + @Test + void convertsStringToCurrency() { + assertConverts("JPY", Currency.class, Currency.getInstance("JPY")); + } + + @Test + @SuppressWarnings("deprecation") + void convertsStringToLocale() { + assertConverts("en", Locale.class, Locale.ENGLISH); + assertConverts("en_us", Locale.class, new Locale(Locale.US.toString())); + } + + @Test + void convertsStringToUUID() { + var uuid = "d043e930-7b3b-48e3-bdbe-5a3ccfb833db"; + assertConverts(uuid, UUID.class, UUID.fromString(uuid)); + } + + // ------------------------------------------------------------------------- + + private void assertConverts(String input, Class targetClass, Object expectedOutput) { + var result = convert(input, targetClass); + + assertThat(result) // + .describedAs(input + " --(" + targetClass.getName() + ")--> " + expectedOutput) // + .isEqualTo(expectedOutput); + } + + private Object convert(String input, Class targetClass) { + return convert(input, targetClass, classLoader()); + } + + private Object convert(String input, Class targetClass, ClassLoader classLoader) { + return ConversionSupport.convert(input, targetClass, classLoader); + } + + private static ClassLoader classLoader() { + Method declaringExecutable = ReflectionSupport.findMethod(ConversionSupportTests.class, "foo").get(); + return classLoader(declaringExecutable); + } + + private static ClassLoader classLoader(Method declaringExecutable) { + return ClassLoaderUtils.getClassLoader(declaringExecutable.getDeclaringClass()); + } + + @SuppressWarnings("unused") + private static void foo() { + } + + private static class Enigma { + + @SuppressWarnings("unused") + void foo() { + } + } + +} diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverterTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverterTests.java index 4f4a07da3409..e679f3d4d531 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverterTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverterTests.java @@ -100,7 +100,7 @@ void cannotConvertStringToMagazine() { private static Constructor constructor(Class clazz) { return ReflectionUtils.findConstructors(clazz, - ctr -> ctr.getParameterCount() == 1 && ctr.getParameterTypes()[0] == String.class).get(0); + ctr -> ctr.getParameterCount() == 1 && ctr.getParameterTypes()[0] == String.class).getFirst(); } private static Method bookMethod(String methodName) { diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/scanning/CloseablePathTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/scanning/CloseablePathTests.java index 27081f85e92d..cf511bae0283 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/support/scanning/CloseablePathTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/scanning/CloseablePathTests.java @@ -101,7 +101,7 @@ void createsAndClosesJarFileSystemOnceWhenCalledConcurrently() throws Exception assertDoesNotThrow(() -> FileSystems.getFileSystem(jarUri), "FileSystem should still be open"); // Close last remaining path - paths.get(0).close(); + paths.getFirst().close(); assertThrows(FileSystemNotFoundException.class, () -> FileSystems.getFileSystem(jarUri), "FileSystem should have been closed"); } diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/scanning/DefaultClasspathScannerTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/scanning/DefaultClasspathScannerTests.java index c99ae9b6b51f..26349cb7ce6f 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/support/scanning/DefaultClasspathScannerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/scanning/DefaultClasspathScannerTests.java @@ -415,7 +415,7 @@ void scanForResourcesInDefaultPackage() { void scanForClassesInPackageWithFilter() { var thisClassOnly = ClassFilter.of(clazz -> clazz == DefaultClasspathScannerTests.class); var classes = classpathScanner.scanForClassesInPackage("org.junit.platform.commons", thisClassOnly); - assertSame(DefaultClasspathScannerTests.class, classes.get(0)); + assertSame(DefaultClasspathScannerTests.class, classes.getFirst()); } @Test @@ -432,7 +432,7 @@ void resourcesCanBeRead() throws IOException { Predicate thisResourceOnly = resource -> "org/junit/platform/commons/example.resource".equals( resource.getName()); var resources = classpathScanner.scanForResourcesInPackage("org.junit.platform.commons", thisResourceOnly); - Resource resource = resources.get(0); + Resource resource = resources.getFirst(); assertThat(resource.getName()).isEqualTo("org/junit/platform/commons/example.resource"); assertThat(resource.getUri()).isEqualTo(uriOf("/org/junit/platform/commons/example.resource")); @@ -501,7 +501,7 @@ void findAllClassesInClasspathRoot() throws Exception { var thisClassOnly = ClassFilter.of(clazz -> clazz == DefaultClasspathScannerTests.class); var root = getTestClasspathRoot(); var classes = classpathScanner.scanForClassesInClasspathRoot(root, thisClassOnly); - assertSame(DefaultClasspathScannerTests.class, classes.get(0)); + assertSame(DefaultClasspathScannerTests.class, classes.getFirst()); } @Test @@ -510,7 +510,7 @@ void findAllClassesInDefaultPackageInClasspathRoot() throws Exception { var classes = classpathScanner.scanForClassesInClasspathRoot(getTestClasspathRoot(), classFilter); assertEquals(1, classes.size(), "number of classes found in default package"); - var testClass = classes.get(0); + var testClass = classes.getFirst(); assertTrue(inDefaultPackage(testClass)); assertEquals("DefaultPackageTestCase", testClass.getName()); } diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/AnnotationUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/commons/util/AnnotationUtilsTests.java index e6865357192c..20a282e723bf 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/util/AnnotationUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/AnnotationUtilsTests.java @@ -383,7 +383,7 @@ void findAnnotatedMethodsForAnnotationUsedInClassAndSuperclassHierarchyDown() th var methods = findAnnotatedMethods(ClassWithAnnotatedMethods.class, Annotation1.class, TOP_DOWN); assertEquals(3, methods.size()); - assertEquals(superMethod, methods.get(0)); + assertEquals(superMethod, methods.getFirst()); assertThat(methods.subList(1, 3)).containsOnly(method1, method3); } @@ -475,7 +475,7 @@ void findAnnotatedFieldsForAnnotationUsedInClassAndSuperclassHierarchyDown() thr var fields = findAnnotatedFields(ClassWithAnnotatedFields.class, Annotation1.class, isStringField, TOP_DOWN); assertEquals(3, fields.size()); - assertEquals(superField, fields.get(0)); + assertEquals(superField, fields.getFirst()); assertThat(fields.subList(1, 3)).containsOnly(field1, field3); } 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 6b89d7aaeb1d..5c7fea2a0f1a 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 @@ -1569,7 +1569,7 @@ void findMethodsWithShadowingUsingHierarchyDownMode() throws Exception { var methods = findMethods(MethodShadowingChild.class, method -> true, TOP_DOWN); assertEquals(6, methods.size()); - assertEquals(MethodShadowingInterface.class.getMethod("method2", int.class, int.class), methods.get(0)); + assertEquals(MethodShadowingInterface.class.getMethod("method2", int.class, int.class), methods.getFirst()); assertThat(methods.subList(1, 3)).containsOnly( MethodShadowingParent.class.getMethod("method2", int.class, int.class, int.class), MethodShadowingParent.class.getMethod("method5", String.class)); @@ -1704,7 +1704,7 @@ void findMethodsWithStaticHidingUsingHierarchyDownModeInLegacyMode() throws Exce var methods = findMethods(child, method -> true, TOP_DOWN); assertEquals(6, methods.size()); - assertEquals(ifcMethod2, methods.get(0)); + assertEquals(ifcMethod2, methods.getFirst()); assertThat(methods.subList(1, 3)).containsOnly(parentMethod2, parentMethod5); assertThat(methods.subList(3, 6)).containsOnly(childMethod1, childMethod4, childMethod5); } diff --git a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java index a9f0be73a810..49b4a159e118 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java @@ -14,10 +14,21 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.FieldSource; import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.console.options.StdStreamTestCase; /** * @since 1.0 @@ -40,8 +51,8 @@ void executeWithoutArgumentsFailsAndPrintsHelpInformation() { "execute -e junit-jupiter -p org.junit.platform.console.subpackage" // }) void executeWithoutExcludeClassnameOptionDoesNotExcludeClassesAndMustIncludeAllClassesMatchingTheStandardClassnamePattern( - final String line) { - String[] args = line.split(" "); + String line) { + var args = line.split(" "); assertEquals(9, new ConsoleLauncherWrapper().execute(args).getTestsFoundCount()); } @@ -52,8 +63,8 @@ void executeWithoutExcludeClassnameOptionDoesNotExcludeClassesAndMustIncludeAllC "execute -e junit-jupiter -p org.junit.platform.console.subpackage --exclude-classname" + " ^org\\.junit\\.platform\\.console\\.subpackage\\..*" // }) - void executeWithExcludeClassnameOptionExcludesClasses(final String line) { - String[] args = line.split(" "); + void executeWithExcludeClassnameOptionExcludesClasses(String line) { + var args = line.split(" "); var result = new ConsoleLauncherWrapper().execute(args); assertAll("all subpackage test classes are excluded by the class name filter", // () -> assertArrayEquals(args, result.args), // @@ -80,16 +91,48 @@ void executeWithExcludeMethodNameOptionExcludesMethods() { "-e junit-jupiter -o java.base", "-e junit-jupiter --select-module java.base", // "execute -e junit-jupiter -o java.base", "execute -e junit-jupiter --select-module java.base" // }) - void executeSelectingModuleNames(final String line) { - String[] args1 = line.split(" "); - assertEquals(0, new ConsoleLauncherWrapper().execute(args1).getTestsFoundCount()); + void executeSelectingModuleNames(String line) { + var args = line.split(" "); + assertEquals(0, new ConsoleLauncherWrapper().execute(args).getTestsFoundCount()); } @ParameterizedTest @ValueSource(strings = { "-e junit-jupiter --scan-modules", "execute -e junit-jupiter --scan-modules" }) - void executeScanModules(final String line) { - String[] args1 = line.split(" "); - assertEquals(0, new ConsoleLauncherWrapper().execute(args1).getTestsFoundCount()); + void executeScanModules(String line) { + var args = line.split(" "); + assertEquals(0, new ConsoleLauncherWrapper().execute(args).getTestsFoundCount()); + } + + @ParameterizedTest + @FieldSource("redirectStreamArguments") + void executeWithRedirectedStdStream(String redirectedStream, int outputFileSize, @TempDir Path tempDir) + throws IOException { + + var outputFile = tempDir.resolve("output.txt"); + var line = String.format("execute -e junit-jupiter --select-class %s %s %s", StdStreamTestCase.class.getName(), + redirectedStream, outputFile); + var args = line.split(" "); + new ConsoleLauncherWrapper().execute(args); + + assertTrue(Files.exists(outputFile), "File does not exist."); + assertEquals(outputFileSize, Files.size(outputFile), "Invalid file size."); + } + + static List redirectStreamArguments = List.of( + arguments("--redirect-stdout", StdStreamTestCase.getStdoutOutputFileSize()), + arguments("--redirect-stderr", StdStreamTestCase.getStderrOutputFileSize())); + + @Test + void executeWithRedirectedStdStreamsToSameFile(@TempDir Path tempDir) throws IOException { + var outputFile = tempDir.resolve("output.txt"); + var line = String.format("execute -e junit-jupiter --select-class %s --redirect-stdout %s --redirect-stderr %s", + StdStreamTestCase.class.getName(), outputFile, outputFile); + var args = line.split(" "); + new ConsoleLauncherWrapper().execute(args); + + assertTrue(Files.exists(outputFile), "File does not exist."); + assertEquals(StdStreamTestCase.getStdoutOutputFileSize() + StdStreamTestCase.getStderrOutputFileSize(), + Files.size(outputFile), "Invalid file size."); } } diff --git a/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java b/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java index 0745f27a914a..a10094a023da 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.platform.engine.discovery.ClassNameFilter.STANDARD_INCLUDE_PATTERN; @@ -55,11 +56,13 @@ class CommandLineOptionsParsingTests { @Test void parseNoArguments() { String[] noArguments = {}; - var options = parse(noArguments); + Result options = parse(noArguments); // @formatter:off assertAll( () -> assertFalse(options.output.isAnsiColorOutputDisabled()), + () -> assertNull(options.output.getStdoutPath()), + () -> assertNull(options.output.getStderrPath()), () -> assertEquals(TestConsoleOutputOptions.DEFAULT_DETAILS, options.output.getDetails()), () -> assertFalse(options.discovery.isScanClasspath()), () -> assertEquals(List.of(STANDARD_INCLUDE_PATTERN), options.discovery.getIncludedClassNamePatterns()), @@ -177,12 +180,12 @@ void parseInvalidExcludeClassNamePatterns() { void parseValidIncludedPackages(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of("org.junit.included"), - type.parseArgLine("--include-package org.junit.included").discovery.getIncludedPackages()), - () -> assertEquals(List.of("org.junit.included"), - type.parseArgLine("--include-package=org.junit.included").discovery.getIncludedPackages()), - () -> assertEquals(List.of("org.junit.included1", "org.junit.included2"), - type.parseArgLine("--include-package org.junit.included1 --include-package org.junit.included2").discovery.getIncludedPackages()) + () -> assertEquals(List.of("org.junit.included"), + type.parseArgLine("--include-package org.junit.included").discovery.getIncludedPackages()), + () -> assertEquals(List.of("org.junit.included"), + type.parseArgLine("--include-package=org.junit.included").discovery.getIncludedPackages()), + () -> assertEquals(List.of("org.junit.included1", "org.junit.included2"), + type.parseArgLine("--include-package org.junit.included1 --include-package org.junit.included2").discovery.getIncludedPackages()) ); // @formatter:on } @@ -192,12 +195,12 @@ void parseValidIncludedPackages(ArgsType type) { void parseValidExcludedPackages(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of("org.junit.excluded"), - type.parseArgLine("--exclude-package org.junit.excluded").discovery.getExcludedPackages()), - () -> assertEquals(List.of("org.junit.excluded"), - type.parseArgLine("--exclude-package=org.junit.excluded").discovery.getExcludedPackages()), - () -> assertEquals(List.of("org.junit.excluded1", "org.junit.excluded2"), - type.parseArgLine("--exclude-package org.junit.excluded1 --exclude-package org.junit.excluded2").discovery.getExcludedPackages()) + () -> assertEquals(List.of("org.junit.excluded"), + type.parseArgLine("--exclude-package org.junit.excluded").discovery.getExcludedPackages()), + () -> assertEquals(List.of("org.junit.excluded"), + type.parseArgLine("--exclude-package=org.junit.excluded").discovery.getExcludedPackages()), + () -> assertEquals(List.of("org.junit.excluded1", "org.junit.excluded2"), + type.parseArgLine("--exclude-package org.junit.excluded1 --exclude-package org.junit.excluded2").discovery.getExcludedPackages()) ); // @formatter:on } @@ -207,12 +210,12 @@ void parseValidExcludedPackages(ArgsType type) { void parseValidIncludeMethodNamePatterns(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(".+#method.*"), - type.parseArgLine("--include-methodname .+#method.*").discovery.getIncludedMethodNamePatterns()), - () -> assertEquals(List.of(".+#methodA.*", ".+#methodB.*"), - type.parseArgLine("--include-methodname .+#methodA.* --include-methodname .+#methodB.*").discovery.getIncludedMethodNamePatterns()), - () -> assertEquals(List.of(".+#method.*"), - type.parseArgLine("--include-methodname=.+#method.*").discovery.getIncludedMethodNamePatterns()) + () -> assertEquals(List.of(".+#method.*"), + type.parseArgLine("--include-methodname .+#method.*").discovery.getIncludedMethodNamePatterns()), + () -> assertEquals(List.of(".+#methodA.*", ".+#methodB.*"), + type.parseArgLine("--include-methodname .+#methodA.* --include-methodname .+#methodB.*").discovery.getIncludedMethodNamePatterns()), + () -> assertEquals(List.of(".+#method.*"), + type.parseArgLine("--include-methodname=.+#method.*").discovery.getIncludedMethodNamePatterns()) ); // @formatter:on } @@ -222,12 +225,12 @@ void parseValidIncludeMethodNamePatterns(ArgsType type) { void parseValidExcludeMethodNamePatterns(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(".+#method.*"), - type.parseArgLine("--exclude-methodname .+#method.*").discovery.getExcludedMethodNamePatterns()), - () -> assertEquals(List.of(".+#methodA.*", ".+#methodB.*"), - type.parseArgLine("--exclude-methodname .+#methodA.* --exclude-methodname .+#methodB.*").discovery.getExcludedMethodNamePatterns()), - () -> assertEquals(List.of(".+#method.*"), - type.parseArgLine("--exclude-methodname=.+#method.*").discovery.getExcludedMethodNamePatterns()) + () -> assertEquals(List.of(".+#method.*"), + type.parseArgLine("--exclude-methodname .+#method.*").discovery.getExcludedMethodNamePatterns()), + () -> assertEquals(List.of(".+#methodA.*", ".+#methodB.*"), + type.parseArgLine("--exclude-methodname .+#methodA.* --exclude-methodname .+#methodB.*").discovery.getExcludedMethodNamePatterns()), + () -> assertEquals(List.of(".+#method.*"), + type.parseArgLine("--exclude-methodname=.+#method.*").discovery.getExcludedMethodNamePatterns()) ); // @formatter:on } @@ -357,14 +360,14 @@ void parseInvalidXmlReportsDirs() { void parseValidUriSelectors(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("-u file:///foo.txt").discovery.getSelectedUris()), - () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("--u file:///foo.txt").discovery.getSelectedUris()), - () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("-select-uri file:///foo.txt").discovery.getSelectedUris()), - () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("-select-uri=file:///foo.txt").discovery.getSelectedUris()), - () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("--select-uri file:///foo.txt").discovery.getSelectedUris()), - () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("--select-uri=file:///foo.txt").discovery.getSelectedUris()), - () -> assertEquals(List.of(selectUri("file:///foo.txt"), selectUri("https://example")), type.parseArgLine("-u file:///foo.txt -u https://example").discovery.getSelectedUris()), - () -> assertEquals(List.of(selectUri("file:///foo.txt"), selectUri("https://example")), type.parseArgLine("-u file:///foo.txt https://example").discovery.getSelectedUris()) + () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("-u file:///foo.txt").discovery.getSelectedUris()), + () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("--u file:///foo.txt").discovery.getSelectedUris()), + () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("-select-uri file:///foo.txt").discovery.getSelectedUris()), + () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("-select-uri=file:///foo.txt").discovery.getSelectedUris()), + () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("--select-uri file:///foo.txt").discovery.getSelectedUris()), + () -> assertEquals(List.of(selectUri("file:///foo.txt")), type.parseArgLine("--select-uri=file:///foo.txt").discovery.getSelectedUris()), + () -> assertEquals(List.of(selectUri("file:///foo.txt"), selectUri("https://example")), type.parseArgLine("-u file:///foo.txt -u https://example").discovery.getSelectedUris()), + () -> assertEquals(List.of(selectUri("file:///foo.txt"), selectUri("https://example")), type.parseArgLine("-u file:///foo.txt https://example").discovery.getSelectedUris()) ); // @formatter:on } @@ -379,16 +382,16 @@ void parseInvalidUriSelectors() { void parseValidFileSelectors(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("-f foo.txt").discovery.getSelectedFiles()), - () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("--f foo.txt").discovery.getSelectedFiles()), - () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("-select-file foo.txt").discovery.getSelectedFiles()), - () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("-select-file=foo.txt").discovery.getSelectedFiles()), - () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("--select-file foo.txt").discovery.getSelectedFiles()), - () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("--select-file=foo.txt").discovery.getSelectedFiles()), - () -> assertEquals(List.of(selectFile("foo.txt"), selectFile("bar.csv")), type.parseArgLine("-f foo.txt -f bar.csv").discovery.getSelectedFiles()), - () -> assertEquals(List.of(selectFile("foo.txt"), selectFile("bar.csv")), type.parseArgLine("-f foo.txt bar.csv").discovery.getSelectedFiles()), - () -> assertEquals(List.of(selectFile("foo.txt", FilePosition.from(5))), type.parseArgLine("-f foo.txt?line=5").discovery.getSelectedFiles()), - () -> assertEquals(List.of(selectFile("foo.txt", FilePosition.from(12, 34))), type.parseArgLine("-f foo.txt?line=12&column=34").discovery.getSelectedFiles()) + () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("-f foo.txt").discovery.getSelectedFiles()), + () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("--f foo.txt").discovery.getSelectedFiles()), + () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("-select-file foo.txt").discovery.getSelectedFiles()), + () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("-select-file=foo.txt").discovery.getSelectedFiles()), + () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("--select-file foo.txt").discovery.getSelectedFiles()), + () -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("--select-file=foo.txt").discovery.getSelectedFiles()), + () -> assertEquals(List.of(selectFile("foo.txt"), selectFile("bar.csv")), type.parseArgLine("-f foo.txt -f bar.csv").discovery.getSelectedFiles()), + () -> assertEquals(List.of(selectFile("foo.txt"), selectFile("bar.csv")), type.parseArgLine("-f foo.txt bar.csv").discovery.getSelectedFiles()), + () -> assertEquals(List.of(selectFile("foo.txt", FilePosition.from(5))), type.parseArgLine("-f foo.txt?line=5").discovery.getSelectedFiles()), + () -> assertEquals(List.of(selectFile("foo.txt", FilePosition.from(12, 34))), type.parseArgLine("-f foo.txt?line=12&column=34").discovery.getSelectedFiles()) ); // @formatter:on } @@ -403,14 +406,14 @@ void parseInvalidFileSelectors() { void parseValidDirectorySelectors(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("-d foo/bar").discovery.getSelectedDirectories()), - () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("--d foo/bar").discovery.getSelectedDirectories()), - () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("-select-directory foo/bar").discovery.getSelectedDirectories()), - () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("-select-directory=foo/bar").discovery.getSelectedDirectories()), - () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("--select-directory foo/bar").discovery.getSelectedDirectories()), - () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("--select-directory=foo/bar").discovery.getSelectedDirectories()), - () -> assertEquals(List.of(selectDirectory("foo/bar"), selectDirectory("bar/qux")), type.parseArgLine("-d foo/bar -d bar/qux").discovery.getSelectedDirectories()), - () -> assertEquals(List.of(selectDirectory("foo/bar"), selectDirectory("bar/qux")), type.parseArgLine("-d foo/bar bar/qux").discovery.getSelectedDirectories()) + () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("-d foo/bar").discovery.getSelectedDirectories()), + () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("--d foo/bar").discovery.getSelectedDirectories()), + () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("-select-directory foo/bar").discovery.getSelectedDirectories()), + () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("-select-directory=foo/bar").discovery.getSelectedDirectories()), + () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("--select-directory foo/bar").discovery.getSelectedDirectories()), + () -> assertEquals(List.of(selectDirectory("foo/bar")), type.parseArgLine("--select-directory=foo/bar").discovery.getSelectedDirectories()), + () -> assertEquals(List.of(selectDirectory("foo/bar"), selectDirectory("bar/qux")), type.parseArgLine("-d foo/bar -d bar/qux").discovery.getSelectedDirectories()), + () -> assertEquals(List.of(selectDirectory("foo/bar"), selectDirectory("bar/qux")), type.parseArgLine("-d foo/bar bar/qux").discovery.getSelectedDirectories()) ); // @formatter:on } @@ -425,14 +428,14 @@ void parseInvalidDirectorySelectors() { void parseValidModuleSelectors(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("-o com.acme.foo").discovery.getSelectedModules()), - () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("--o com.acme.foo").discovery.getSelectedModules()), - () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("-select-module com.acme.foo").discovery.getSelectedModules()), - () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("-select-module=com.acme.foo").discovery.getSelectedModules()), - () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("--select-module com.acme.foo").discovery.getSelectedModules()), - () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("--select-module=com.acme.foo").discovery.getSelectedModules()), - () -> assertEquals(List.of(selectModule("com.acme.foo"), selectModule("com.example.bar")), type.parseArgLine("-o com.acme.foo -o com.example.bar").discovery.getSelectedModules()), - () -> assertEquals(List.of(selectModule("com.acme.foo"), selectModule("com.example.bar")), type.parseArgLine("-o com.acme.foo com.example.bar").discovery.getSelectedModules()) + () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("-o com.acme.foo").discovery.getSelectedModules()), + () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("--o com.acme.foo").discovery.getSelectedModules()), + () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("-select-module com.acme.foo").discovery.getSelectedModules()), + () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("-select-module=com.acme.foo").discovery.getSelectedModules()), + () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("--select-module com.acme.foo").discovery.getSelectedModules()), + () -> assertEquals(List.of(selectModule("com.acme.foo")), type.parseArgLine("--select-module=com.acme.foo").discovery.getSelectedModules()), + () -> assertEquals(List.of(selectModule("com.acme.foo"), selectModule("com.example.bar")), type.parseArgLine("-o com.acme.foo -o com.example.bar").discovery.getSelectedModules()), + () -> assertEquals(List.of(selectModule("com.acme.foo"), selectModule("com.example.bar")), type.parseArgLine("-o com.acme.foo com.example.bar").discovery.getSelectedModules()) ); // @formatter:on } @@ -447,14 +450,14 @@ void parseInvalidModuleSelectors() { void parseValidPackageSelectors(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("-p com.acme.foo").discovery.getSelectedPackages()), - () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("--p com.acme.foo").discovery.getSelectedPackages()), - () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("-select-package com.acme.foo").discovery.getSelectedPackages()), - () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("-select-package=com.acme.foo").discovery.getSelectedPackages()), - () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("--select-package com.acme.foo").discovery.getSelectedPackages()), - () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("--select-package=com.acme.foo").discovery.getSelectedPackages()), - () -> assertEquals(List.of(selectPackage("com.acme.foo"), selectPackage("com.example.bar")), type.parseArgLine("-p com.acme.foo -p com.example.bar").discovery.getSelectedPackages()), - () -> assertEquals(List.of(selectPackage("com.acme.foo"), selectPackage("com.example.bar")), type.parseArgLine("-p com.acme.foo com.example.bar").discovery.getSelectedPackages()) + () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("-p com.acme.foo").discovery.getSelectedPackages()), + () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("--p com.acme.foo").discovery.getSelectedPackages()), + () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("-select-package com.acme.foo").discovery.getSelectedPackages()), + () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("-select-package=com.acme.foo").discovery.getSelectedPackages()), + () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("--select-package com.acme.foo").discovery.getSelectedPackages()), + () -> assertEquals(List.of(selectPackage("com.acme.foo")), type.parseArgLine("--select-package=com.acme.foo").discovery.getSelectedPackages()), + () -> assertEquals(List.of(selectPackage("com.acme.foo"), selectPackage("com.example.bar")), type.parseArgLine("-p com.acme.foo -p com.example.bar").discovery.getSelectedPackages()), + () -> assertEquals(List.of(selectPackage("com.acme.foo"), selectPackage("com.example.bar")), type.parseArgLine("-p com.acme.foo com.example.bar").discovery.getSelectedPackages()) ); // @formatter:on } @@ -469,14 +472,14 @@ void parseInvalidPackageSelectors() { void parseValidClassSelectors(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("-c com.acme.Foo").discovery.getSelectedClasses()), - () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("--c com.acme.Foo").discovery.getSelectedClasses()), - () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("-select-class com.acme.Foo").discovery.getSelectedClasses()), - () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("-select-class=com.acme.Foo").discovery.getSelectedClasses()), - () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("--select-class com.acme.Foo").discovery.getSelectedClasses()), - () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("--select-class=com.acme.Foo").discovery.getSelectedClasses()), - () -> assertEquals(List.of(selectClass("com.acme.Foo"), selectClass("com.example.Bar")), type.parseArgLine("-c com.acme.Foo -c com.example.Bar").discovery.getSelectedClasses()), - () -> assertEquals(List.of(selectClass("com.acme.Foo"), selectClass("com.example.Bar")), type.parseArgLine("-c com.acme.Foo com.example.Bar").discovery.getSelectedClasses()) + () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("-c com.acme.Foo").discovery.getSelectedClasses()), + () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("--c com.acme.Foo").discovery.getSelectedClasses()), + () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("-select-class com.acme.Foo").discovery.getSelectedClasses()), + () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("-select-class=com.acme.Foo").discovery.getSelectedClasses()), + () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("--select-class com.acme.Foo").discovery.getSelectedClasses()), + () -> assertEquals(List.of(selectClass("com.acme.Foo")), type.parseArgLine("--select-class=com.acme.Foo").discovery.getSelectedClasses()), + () -> assertEquals(List.of(selectClass("com.acme.Foo"), selectClass("com.example.Bar")), type.parseArgLine("-c com.acme.Foo -c com.example.Bar").discovery.getSelectedClasses()), + () -> assertEquals(List.of(selectClass("com.acme.Foo"), selectClass("com.example.Bar")), type.parseArgLine("-c com.acme.Foo com.example.Bar").discovery.getSelectedClasses()) ); // @formatter:on } @@ -491,16 +494,16 @@ void parseInvalidClassSelectors() { void parseValidMethodSelectors(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("-m com.acme.Foo#m()").discovery.getSelectedMethods()), - () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("--m com.acme.Foo#m()").discovery.getSelectedMethods()), - () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("-select-method com.acme.Foo#m()").discovery.getSelectedMethods()), - () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("-select-method=com.acme.Foo#m()").discovery.getSelectedMethods()), - () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("--select-method com.acme.Foo#m()").discovery.getSelectedMethods()), - () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("--select-method=com.acme.Foo#m()").discovery.getSelectedMethods()), - () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()"), selectMethod("com.example.Bar#method(java.lang.Object)")), - type.parseArgLine("-m com.acme.Foo#m() -m com.example.Bar#method(java.lang.Object)").discovery.getSelectedMethods()), - () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()"), selectMethod("com.example.Bar#method(java.lang.Object)")), - type.parseArgLine("-m com.acme.Foo#m() com.example.Bar#method(java.lang.Object)").discovery.getSelectedMethods()) + () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("-m com.acme.Foo#m()").discovery.getSelectedMethods()), + () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("--m com.acme.Foo#m()").discovery.getSelectedMethods()), + () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("-select-method com.acme.Foo#m()").discovery.getSelectedMethods()), + () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("-select-method=com.acme.Foo#m()").discovery.getSelectedMethods()), + () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("--select-method com.acme.Foo#m()").discovery.getSelectedMethods()), + () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), type.parseArgLine("--select-method=com.acme.Foo#m()").discovery.getSelectedMethods()), + () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()"), selectMethod("com.example.Bar#method(java.lang.Object)")), + type.parseArgLine("-m com.acme.Foo#m() -m com.example.Bar#method(java.lang.Object)").discovery.getSelectedMethods()), + () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()"), selectMethod("com.example.Bar#method(java.lang.Object)")), + type.parseArgLine("-m com.acme.Foo#m() com.example.Bar#method(java.lang.Object)").discovery.getSelectedMethods()) ); // @formatter:on } @@ -515,16 +518,16 @@ void parseInvalidMethodSelectors() { void parseValidClasspathResourceSelectors(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("-r /foo.csv").discovery.getSelectedClasspathResources()), - () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("--r /foo.csv").discovery.getSelectedClasspathResources()), - () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("-select-resource /foo.csv").discovery.getSelectedClasspathResources()), - () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("-select-resource=/foo.csv").discovery.getSelectedClasspathResources()), - () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("--select-resource /foo.csv").discovery.getSelectedClasspathResources()), - () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("--select-resource=/foo.csv").discovery.getSelectedClasspathResources()), - () -> assertEquals(List.of(selectClasspathResource("/foo.csv"), selectClasspathResource("bar.json")), type.parseArgLine("-r /foo.csv -r bar.json").discovery.getSelectedClasspathResources()), - () -> assertEquals(List.of(selectClasspathResource("/foo.csv"), selectClasspathResource("bar.json")), type.parseArgLine("-r /foo.csv bar.json").discovery.getSelectedClasspathResources()), - () -> assertEquals(List.of(selectClasspathResource("/foo.csv", FilePosition.from(5))), type.parseArgLine("-r /foo.csv?line=5").discovery.getSelectedClasspathResources()), - () -> assertEquals(List.of(selectClasspathResource("/foo.csv", FilePosition.from(12, 34))), type.parseArgLine("-r /foo.csv?line=12&column=34").discovery.getSelectedClasspathResources()) + () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("-r /foo.csv").discovery.getSelectedClasspathResources()), + () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("--r /foo.csv").discovery.getSelectedClasspathResources()), + () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("-select-resource /foo.csv").discovery.getSelectedClasspathResources()), + () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("-select-resource=/foo.csv").discovery.getSelectedClasspathResources()), + () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("--select-resource /foo.csv").discovery.getSelectedClasspathResources()), + () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("--select-resource=/foo.csv").discovery.getSelectedClasspathResources()), + () -> assertEquals(List.of(selectClasspathResource("/foo.csv"), selectClasspathResource("bar.json")), type.parseArgLine("-r /foo.csv -r bar.json").discovery.getSelectedClasspathResources()), + () -> assertEquals(List.of(selectClasspathResource("/foo.csv"), selectClasspathResource("bar.json")), type.parseArgLine("-r /foo.csv bar.json").discovery.getSelectedClasspathResources()), + () -> assertEquals(List.of(selectClasspathResource("/foo.csv", FilePosition.from(5))), type.parseArgLine("-r /foo.csv?line=5").discovery.getSelectedClasspathResources()), + () -> assertEquals(List.of(selectClasspathResource("/foo.csv", FilePosition.from(12, 34))), type.parseArgLine("-r /foo.csv?line=12&column=34").discovery.getSelectedClasspathResources()) ); // @formatter:on } @@ -539,14 +542,14 @@ void parseInvalidClasspathResourceSelectors() { void parseValidIterationSelectors(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectIteration(selectClasspathResource("/foo.csv"), 0)), type.parseArgLine("-i resource:/foo.csv[0]").discovery.getSelectedIterations()), - () -> assertEquals(List.of(selectIteration(selectMethod("com.acme.Foo#m()"), 1, 2)), type.parseArgLine("--i method:com.acme.Foo#m()[1..2]").discovery.getSelectedIterations()), - () -> assertEquals(List.of(selectIteration(selectClass("com.acme.Foo"), 0, 2)), type.parseArgLine("-select-iteration class:com.acme.Foo[0,2]").discovery.getSelectedIterations()), - () -> assertEquals(List.of(selectIteration(selectPackage("com.acme.foo"), 3)), type.parseArgLine("-select-iteration=package:com.acme.foo[3]").discovery.getSelectedIterations()), - () -> assertEquals(List.of(selectIteration(selectModule("com.acme.foo"), 0, 1, 2, 4, 5, 6)), type.parseArgLine("--select-iteration module:com.acme.foo[0..2,4..6]").discovery.getSelectedIterations()), - () -> assertEquals(List.of(selectIteration(selectDirectory("foo/bar"), 1, 5)), type.parseArgLine("--select-iteration=directory:foo/bar[1,5]").discovery.getSelectedIterations()), - () -> assertEquals(List.of(selectIteration(selectFile("foo.txt"), 6), selectIteration(selectUri("file:///foo.txt"), 7)), type.parseArgLine("-i file:foo.txt[6] -i uri:file:///foo.txt[7]").discovery.getSelectedIterations()), - () -> assertEquals(List.of(selectIteration(selectFile("foo.txt"), 6), selectIteration(selectUri("file:///foo.txt"), 7)), type.parseArgLine("-i file:foo.txt[6] uri:file:///foo.txt[7]").discovery.getSelectedIterations()) + () -> assertEquals(List.of(selectIteration(selectClasspathResource("/foo.csv"), 0)), type.parseArgLine("-i resource:/foo.csv[0]").discovery.getSelectedIterations()), + () -> assertEquals(List.of(selectIteration(selectMethod("com.acme.Foo#m()"), 1, 2)), type.parseArgLine("--i method:com.acme.Foo#m()[1..2]").discovery.getSelectedIterations()), + () -> assertEquals(List.of(selectIteration(selectClass("com.acme.Foo"), 0, 2)), type.parseArgLine("-select-iteration class:com.acme.Foo[0,2]").discovery.getSelectedIterations()), + () -> assertEquals(List.of(selectIteration(selectPackage("com.acme.foo"), 3)), type.parseArgLine("-select-iteration=package:com.acme.foo[3]").discovery.getSelectedIterations()), + () -> assertEquals(List.of(selectIteration(selectModule("com.acme.foo"), 0, 1, 2, 4, 5, 6)), type.parseArgLine("--select-iteration module:com.acme.foo[0..2,4..6]").discovery.getSelectedIterations()), + () -> assertEquals(List.of(selectIteration(selectDirectory("foo/bar"), 1, 5)), type.parseArgLine("--select-iteration=directory:foo/bar[1,5]").discovery.getSelectedIterations()), + () -> assertEquals(List.of(selectIteration(selectFile("foo.txt"), 6), selectIteration(selectUri("file:///foo.txt"), 7)), type.parseArgLine("-i file:foo.txt[6] -i uri:file:///foo.txt[7]").discovery.getSelectedIterations()), + () -> assertEquals(List.of(selectIteration(selectFile("foo.txt"), 6), selectIteration(selectUri("file:///foo.txt"), 7)), type.parseArgLine("-i file:foo.txt[6] uri:file:///foo.txt[7]").discovery.getSelectedIterations()) ); // @formatter:on } @@ -561,10 +564,10 @@ void parseInvalidIterationSelectors() { void parseValidUniqueIdSelectors(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectUniqueId("[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]")), type.parseArgLine("--uid [engine:junit-jupiter]/[class:MyClass]/[method:myMethod]").discovery.getSelectedUniqueIds()), - () -> assertEquals(List.of(selectUniqueId("[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]")), type.parseArgLine("--select-unique-id [engine:junit-jupiter]/[class:MyClass]/[method:myMethod]").discovery.getSelectedUniqueIds()), - () -> assertEquals(List.of(selectUniqueId("[engine:junit-jupiter]/[class:MyClass1]"), selectUniqueId("[engine:junit-jupiter]/[class:MyClass2]")), type.parseArgLine("--uid [engine:junit-jupiter]/[class:MyClass1] --uid [engine:junit-jupiter]/[class:MyClass2]").discovery.getSelectedUniqueIds()), - () -> assertEquals(List.of(selectUniqueId("[engine:junit-jupiter]/[class:MyClass1]"), selectUniqueId("[engine:junit-jupiter]/[class:MyClass2]")), type.parseArgLine("--uid [engine:junit-jupiter]/[class:MyClass1] [engine:junit-jupiter]/[class:MyClass2]").discovery.getSelectedUniqueIds()) + () -> assertEquals(List.of(selectUniqueId("[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]")), type.parseArgLine("--uid [engine:junit-jupiter]/[class:MyClass]/[method:myMethod]").discovery.getSelectedUniqueIds()), + () -> assertEquals(List.of(selectUniqueId("[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]")), type.parseArgLine("--select-unique-id [engine:junit-jupiter]/[class:MyClass]/[method:myMethod]").discovery.getSelectedUniqueIds()), + () -> assertEquals(List.of(selectUniqueId("[engine:junit-jupiter]/[class:MyClass1]"), selectUniqueId("[engine:junit-jupiter]/[class:MyClass2]")), type.parseArgLine("--uid [engine:junit-jupiter]/[class:MyClass1] --uid [engine:junit-jupiter]/[class:MyClass2]").discovery.getSelectedUniqueIds()), + () -> assertEquals(List.of(selectUniqueId("[engine:junit-jupiter]/[class:MyClass1]"), selectUniqueId("[engine:junit-jupiter]/[class:MyClass2]")), type.parseArgLine("--uid [engine:junit-jupiter]/[class:MyClass1] [engine:junit-jupiter]/[class:MyClass2]").discovery.getSelectedUniqueIds()) ); // @formatter:on } @@ -600,16 +603,16 @@ void parseClasspathScanningEntries(ArgsType type) { void parseValidConfigurationParameters(ArgsType type) { // @formatter:off assertAll( - () -> assertThat(type.parseArgLine("-config foo=bar").discovery.getConfigurationParameters()) - .containsOnly(entry("foo", "bar")), - () -> assertThat(type.parseArgLine("-config=foo=bar").discovery.getConfigurationParameters()) - .containsOnly(entry("foo", "bar")), - () -> assertThat(type.parseArgLine("--config foo=bar").discovery.getConfigurationParameters()) - .containsOnly(entry("foo", "bar")), - () -> assertThat(type.parseArgLine("--config=foo=bar").discovery.getConfigurationParameters()) - .containsOnly(entry("foo", "bar")), - () -> assertThat(type.parseArgLine("--config foo=bar --config baz=qux").discovery.getConfigurationParameters()) - .containsExactly(entry("foo", "bar"), entry("baz", "qux")) + () -> assertThat(type.parseArgLine("-config foo=bar").discovery.getConfigurationParameters()) + .containsOnly(entry("foo", "bar")), + () -> assertThat(type.parseArgLine("-config=foo=bar").discovery.getConfigurationParameters()) + .containsOnly(entry("foo", "bar")), + () -> assertThat(type.parseArgLine("--config foo=bar").discovery.getConfigurationParameters()) + .containsOnly(entry("foo", "bar")), + () -> assertThat(type.parseArgLine("--config=foo=bar").discovery.getConfigurationParameters()) + .containsOnly(entry("foo", "bar")), + () -> assertThat(type.parseArgLine("--config foo=bar --config baz=qux").discovery.getConfigurationParameters()) + .containsExactly(entry("foo", "bar"), entry("baz", "qux")) ); // @formatter:on } @@ -619,10 +622,10 @@ void parseValidConfigurationParameters(ArgsType type) { void parseValidConfigurationParametersResource(ArgsType type) { // @formatter:off assertAll( - () -> assertThat(type.parseArgLine("--config-resource foo.properties").discovery.getConfigurationParametersResources()) - .containsOnly("foo.properties"), - () -> assertThat(type.parseArgLine("--config-resource foo.properties --config-resource bar.properties").discovery.getConfigurationParametersResources()) - .containsExactly("foo.properties", "bar.properties") + () -> assertThat(type.parseArgLine("--config-resource foo.properties").discovery.getConfigurationParametersResources()) + .containsOnly("foo.properties"), + () -> assertThat(type.parseArgLine("--config-resource foo.properties --config-resource bar.properties").discovery.getConfigurationParametersResources()) + .containsExactly("foo.properties", "bar.properties") ); // @formatter:on } @@ -632,6 +635,44 @@ void parseInvalidConfigurationParameters() { assertOptionWithMissingRequiredArgumentThrowsException("-config", "--config"); } + @ParameterizedTest + @EnumSource + void parseValidStdoutRedirectionFile(ArgsType type) { + var file = Paths.get("foo.txt"); + // @formatter:off + assertAll( + () -> assertNull(type.parseArgLine("").output.getStdoutPath()), + () -> assertEquals(file, type.parseArgLine("--redirect-stdout=foo.txt").output.getStdoutPath()), + () -> assertEquals(file, type.parseArgLine("--redirect-stdout foo.txt").output.getStdoutPath()), + () -> assertEquals(file, type.parseArgLine("--redirect-stdout bar.txt --redirect-stdout foo.txt").output.getStdoutPath()) + ); + // @formatter:on + } + + @ParameterizedTest + @EnumSource + void parseValidStderrRedirectionFile(ArgsType type) { + var file = Paths.get("foo.txt"); + // @formatter:off + assertAll( + () -> assertNull(type.parseArgLine("").output.getStderrPath()), + () -> assertEquals(file, type.parseArgLine("--redirect-stderr=foo.txt").output.getStderrPath()), + () -> assertEquals(file, type.parseArgLine("--redirect-stderr foo.txt").output.getStderrPath()), + () -> assertEquals(file, type.parseArgLine("--redirect-stderr bar.txt --redirect-stderr foo.txt").output.getStderrPath()) + ); + // @formatter:on + } + + @Test + void parseInvalidStdoutRedirectionFile() { + assertOptionWithMissingRequiredArgumentThrowsException("--redirect-stdout"); + } + + @Test + void parseInvalidStderrRedirectionFile() { + assertOptionWithMissingRequiredArgumentThrowsException("--redirect-stderr"); + } + @Test void parseInvalidConfigurationParametersResource() { assertOptionWithMissingRequiredArgumentThrowsException("--config-resource"); @@ -650,14 +691,14 @@ void parseInvalidConfigurationParametersWithDuplicateKey(ArgsType type) { void parseValidSelectorIdentifier(ArgsType type) { // @formatter:off assertAll( - () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), parseIdentifiers(type,"--select resource:/foo.csv")), - () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), parseIdentifiers(type,"--select method:com.acme.Foo#m()")), - () -> assertEquals(List.of(selectClass("com.acme.Foo")), parseIdentifiers(type,"--select class:com.acme.Foo")), - () -> assertEquals(List.of(selectPackage("com.acme.foo")), parseIdentifiers(type,"--select package:com.acme.foo")), - () -> assertEquals(List.of(selectModule("com.acme.foo")), parseIdentifiers(type,"--select module:com.acme.foo")), - () -> assertEquals(List.of(selectDirectory("foo/bar")), parseIdentifiers(type,"--select directory:foo/bar")), - () -> assertEquals(List.of(selectFile("foo.txt"), selectUri("file:///foo.txt")), parseIdentifiers(type,"--select file:foo.txt --select uri:file:///foo.txt")), - () -> assertEquals(List.of(selectUniqueId("[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]")), parseIdentifiers(type,"--select uid:[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]")) + () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), parseIdentifiers(type,"--select resource:/foo.csv")), + () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), parseIdentifiers(type,"--select method:com.acme.Foo#m()")), + () -> assertEquals(List.of(selectClass("com.acme.Foo")), parseIdentifiers(type,"--select class:com.acme.Foo")), + () -> assertEquals(List.of(selectPackage("com.acme.foo")), parseIdentifiers(type,"--select package:com.acme.foo")), + () -> assertEquals(List.of(selectModule("com.acme.foo")), parseIdentifiers(type,"--select module:com.acme.foo")), + () -> assertEquals(List.of(selectDirectory("foo/bar")), parseIdentifiers(type,"--select directory:foo/bar")), + () -> assertEquals(List.of(selectFile("foo.txt"), selectUri("file:///foo.txt")), parseIdentifiers(type,"--select file:foo.txt --select uri:file:///foo.txt")), + () -> assertEquals(List.of(selectUniqueId("[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]")), parseIdentifiers(type,"--select uid:[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]")) ); // @formatter:on } diff --git a/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTestCase.java b/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTestCase.java new file mode 100644 index 000000000000..bcf3637a9a22 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTestCase.java @@ -0,0 +1,34 @@ +/* + * 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.options; + +import org.junit.jupiter.api.Test; + +public class StdStreamTestCase { + + private static final String STDOUT_DATA = "Writing to STDOUT..."; + private static final String STDERR_DATA = "Writing to STDERR..."; + + public static int getStdoutOutputFileSize() { + return STDOUT_DATA.length(); + } + + public static int getStderrOutputFileSize() { + return STDERR_DATA.length(); + } + + @Test + void printTest() { + System.out.print(STDOUT_DATA); + System.err.print(STDERR_DATA); + } + +} diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/ColorPaletteTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/ColorPaletteTests.java index 55adbe486218..41073cedcc68 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/ColorPaletteTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/ColorPaletteTests.java @@ -187,7 +187,7 @@ void flat_single_color() { private void demoTestRun(TestExecutionListener listener) { TestDescriptor testDescriptor = new TestDescriptorStub(UniqueId.forEngine("demo-engine"), "My Test"); - TestPlan testPlan = TestPlan.from(List.of(testDescriptor), mock(), dummyOutputDirectoryProvider()); + TestPlan testPlan = TestPlan.from(true, List.of(testDescriptor), mock(), dummyOutputDirectoryProvider()); listener.testPlanExecutionStarted(testPlan); listener.executionStarted(TestIdentifier.from(testDescriptor)); listener.executionFinished(TestIdentifier.from(testDescriptor), TestExecutionResult.successful()); diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java index 014c167a203e..e1ec970f3ecd 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java @@ -116,7 +116,7 @@ void convertsDefaultIncludeClassNamePatternOption() { var filters = request.getFiltersByType(ClassNameFilter.class); assertThat(filters).hasSize(1); - assertExcludes(filters.get(0), STANDARD_INCLUDE_PATTERN); + assertExcludes(filters.getFirst(), STANDARD_INCLUDE_PATTERN); } @Test @@ -128,8 +128,8 @@ void convertsExplicitIncludeClassNamePatternOption() { var filters = request.getFiltersByType(ClassNameFilter.class); assertThat(filters).hasSize(1); - assertIncludes(filters.get(0), "Foo.*Bar"); - assertIncludes(filters.get(0), "Bar.*Foo"); + assertIncludes(filters.getFirst(), "Foo.*Bar"); + assertIncludes(filters.getFirst(), "Bar.*Foo"); } @Test @@ -143,10 +143,10 @@ void includeSelectedClassesAndMethodsRegardlessOfClassNamePatterns() { var filters = request.getFiltersByType(ClassNameFilter.class); assertThat(filters).hasSize(1); - assertIncludes(filters.get(0), "SomeTest"); - assertIncludes(filters.get(0), "com.acme.Foo"); - assertIncludes(filters.get(0), "com.acme.Bar"); - assertIncludes(filters.get(0), "Foo.*Bar"); + assertIncludes(filters.getFirst(), "SomeTest"); + assertIncludes(filters.getFirst(), "com.acme.Foo"); + assertIncludes(filters.getFirst(), "com.acme.Bar"); + assertIncludes(filters.getFirst(), "Foo.*Bar"); } @Test @@ -172,7 +172,7 @@ void convertsPackageOptions() { var packageNameFilters = request.getFiltersByType(PackageNameFilter.class); assertThat(packageNameFilters).hasSize(2); - assertIncludes(packageNameFilters.get(0), "org.junit.included1"); + assertIncludes(packageNameFilters.getFirst(), "org.junit.included1"); assertIncludes(packageNameFilters.get(0), "org.junit.included2"); assertIncludes(packageNameFilters.get(0), "org.junit.included3"); assertExcludes(packageNameFilters.get(1), "org.junit.excluded1"); @@ -279,7 +279,7 @@ void propagatesMethodSelectors() { var methodSelectors = request.getSelectorsByType(MethodSelector.class); assertThat(methodSelectors).hasSize(2); - assertThat(methodSelectors.get(0).getClassName()).isEqualTo("com.acme.Foo"); + assertThat(methodSelectors.getFirst().getClassName()).isEqualTo("com.acme.Foo"); assertThat(methodSelectors.get(0).getMethodName()).isEqualTo("m"); assertThat(methodSelectors.get(0).getParameterTypeNames()).isEmpty(); assertThat(methodSelectors.get(1).getClassName()).isEqualTo("com.example.Bar"); diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/TestFeedPrintingListenerTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/TestFeedPrintingListenerTests.java index 35c37df1c1cc..0c0d188381e8 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/TestFeedPrintingListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/TestFeedPrintingListenerTests.java @@ -45,7 +45,7 @@ void prepareListener() { "%c ool test"); engineDescriptor.addChild(testDescriptor); - testPlan = TestPlan.from(Collections.singleton(engineDescriptor), mock(), dummyOutputDirectoryProvider()); + testPlan = TestPlan.from(true, Collections.singleton(engineDescriptor), mock(), dummyOutputDirectoryProvider()); testIdentifier = testPlan.getTestIdentifier(testDescriptor.getUniqueId()); listener.testPlanExecutionStarted(testPlan); diff --git a/platform-tests/src/test/java/org/junit/platform/engine/CompositeTestDescriptorVisitorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/CompositeTestDescriptorVisitorTests.java new file mode 100644 index 000000000000..88ee3f0ed7aa --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/CompositeTestDescriptorVisitorTests.java @@ -0,0 +1,54 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.engine.TestDescriptor.Visitor; +import org.mockito.InOrder; + +class CompositeTestDescriptorVisitorTests { + + @Test + void checksPreconditions() { + assertThrows(PreconditionViolationException.class, Visitor::composite); + assertThrows(PreconditionViolationException.class, () -> Visitor.composite((Visitor[]) null)); + assertThrows(PreconditionViolationException.class, () -> Visitor.composite((Visitor) null)); + } + + @Test + void optimizesForSingleVisitor() { + Visitor visitor = mock(); + + assertSame(visitor, Visitor.composite(visitor)); + } + + @Test + void callsAllVisitorsInOrder() { + Visitor visitor1 = mock("visitor1"); + Visitor visitor2 = mock("visitor2"); + TestDescriptor testDescriptor = mock(); + + var composite = Visitor.composite(visitor1, visitor2); + composite.visit(testDescriptor); + + InOrder inOrder = inOrder(visitor1, visitor2); + inOrder.verify(visitor1).visit(testDescriptor); + inOrder.verify(visitor2).visit(testDescriptor); + inOrder.verifyNoMoreInteractions(); + } + +} 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 new file mode 100644 index 000000000000..ed23c6099ae1 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/DiscoveryIssueTests.java @@ -0,0 +1,98 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.EqualsAndHashCodeAssertions.assertEqualsAndHashCode; +import static org.mockito.Mockito.mock; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.support.descriptor.ClassSource; + +public class DiscoveryIssueTests { + + @Test + void create() { + var issue = DiscoveryIssue.create(Severity.ERROR, "message"); + + assertThat(issue.severity()).isEqualTo(Severity.ERROR); + assertThat(issue.message()).isEqualTo("message"); + assertThat(issue.source()).isEmpty(); + assertThat(issue.cause()).isEmpty(); + } + + @Test + void builder() { + var source = mock(TestSource.class); + var cause = new RuntimeException("boom"); + + var issue = DiscoveryIssue.builder(Severity.WARNING, "message") // + .source(source) // + .cause(cause) // + .build(); + + assertThat(issue.severity()).isEqualTo(Severity.WARNING); + assertThat(issue.message()).isEqualTo("message"); + assertThat(issue.source()).containsSame(source); + assertThat(issue.cause()).containsSame(cause); + } + + @Test + void equalsAndHashCode() { + assertEqualsAndHashCode( // + DiscoveryIssue.create(Severity.ERROR, "message"), // + DiscoveryIssue.builder(Severity.ERROR, "message").build(), // + DiscoveryIssue.create(Severity.WARNING, "message") // + ); + assertEqualsAndHashCode( // + DiscoveryIssue.create(Severity.ERROR, "message"), // + DiscoveryIssue.builder(Severity.ERROR, "message").build(), // + DiscoveryIssue.create(Severity.ERROR, "anotherMessage") // + ); + assertEqualsAndHashCode( // + DiscoveryIssue.builder(Severity.ERROR, "message") // + .source(ClassSource.from(DiscoveryIssue.class)).build(), // + DiscoveryIssue.builder(Severity.ERROR, "message") // + .source(Optional.of(ClassSource.from(DiscoveryIssue.class))).build(), // + DiscoveryIssue.builder(Severity.ERROR, "message") // + .source(ClassSource.from(DefaultDiscoveryIssue.class)).build() // + ); + var cause = new RuntimeException("boom"); + assertEqualsAndHashCode( // + DiscoveryIssue.builder(Severity.ERROR, "message").cause(cause).build(), // + DiscoveryIssue.builder(Severity.ERROR, "message").cause(Optional.of(cause)).build(), // + DiscoveryIssue.builder(Severity.ERROR, "message").cause(new RuntimeException("boom")).build() // + ); + } + + @Test + void stringRepresentationWithoutAttributes() { + var issue = DiscoveryIssue.create(Severity.WARNING, "message"); + + assertThat(issue.toString()) // + .isEqualTo("DiscoveryIssue [severity = WARNING, message = 'message']"); + } + + @Test + void stringRepresentationWithOptionalAttributes() { + var issue = DiscoveryIssue.builder(Severity.WARNING, "message") // + .source(ClassSource.from(DiscoveryIssue.class)) // + .cause(new RuntimeException("boom")) // + .build(); + + assertThat(issue.toString()) // + .isEqualTo( + "DiscoveryIssue [severity = WARNING, message = 'message', source = ClassSource [className = 'org.junit.platform.engine.DiscoveryIssue', filePosition = null], cause = java.lang.RuntimeException: boom]"); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/engine/UniqueIdFormatTests.java b/platform-tests/src/test/java/org/junit/platform/engine/UniqueIdFormatTests.java index f2901b85557b..47d1913f19e2 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/UniqueIdFormatTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/UniqueIdFormatTests.java @@ -121,7 +121,7 @@ default void parseMalformedUid() { @Test default void parseEngineUid() { var parsedId = getFormat().parse(getEngineUid()); - assertSegment(parsedId.getSegments().get(0), "engine", "junit-jupiter"); + assertSegment(parsedId.getSegments().getFirst(), "engine", "junit-jupiter"); assertEquals(getEngineUid(), getFormat().format(parsedId)); assertEquals(getEngineUid(), parsedId.toString()); } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/UniqueIdTests.java b/platform-tests/src/test/java/org/junit/platform/engine/UniqueIdTests.java index d9dc72a328f9..9001186fa9f3 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/UniqueIdTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/UniqueIdTests.java @@ -46,7 +46,7 @@ void uniqueIdCanBeCreatedFromEngineId() { var uniqueId = UniqueId.forEngine(ENGINE_ID); assertEquals("[engine:junit-jupiter]", uniqueId.toString()); - assertSegment(uniqueId.getSegments().get(0), "engine", "junit-jupiter"); + assertSegment(uniqueId.getSegments().getFirst(), "engine", "junit-jupiter"); } @Test @@ -70,7 +70,7 @@ void uniqueIdCanBeCreatedFromTypeAndValue() { var uniqueId = UniqueId.root("aType", "aValue"); assertEquals("[aType:aValue]", uniqueId.toString()); - assertSegment(uniqueId.getSegments().get(0), "aType", "aValue"); + assertSegment(uniqueId.getSegments().getFirst(), "aType", "aValue"); } @Test @@ -96,7 +96,7 @@ void appendingSegmentLeavesOriginalUnchanged() { uniqueId.append("class", "org.junit.MyClass"); assertThat(uniqueId.getSegments()).hasSize(1); - assertSegment(uniqueId.getSegments().get(0), "engine", ENGINE_ID); + assertSegment(uniqueId.getSegments().getFirst(), "engine", ENGINE_ID); } @Test @@ -153,7 +153,7 @@ void ensureDefaultUniqueIdFormatIsUsedForFormatting() { @Test void ensureDefaultUniqueIdFormatDecodingEncodesSegmentParts() { - var segment = UniqueId.parse("[%5B+%25+%5D):(%3A+%2B+%2F]").getSegments().get(0); + var segment = UniqueId.parse("[%5B+%25+%5D):(%3A+%2B+%2F]").getSegments().getFirst(); assertEquals("[ % ])", segment.getType()); assertEquals("(: + /", segment.getValue()); } @@ -163,7 +163,7 @@ void ensureDefaultUniqueIdFormatCanHandleAllCharacters() { for (char c = 0; c < Character.MAX_VALUE; c++) { var value = "foo " + c + " bar"; var uniqueId = UniqueId.parse(UniqueId.root("type", value).toString()); - var segment = uniqueId.getSegments().get(0); + var segment = uniqueId.getSegments().getFirst(); assertEquals(value, segment.getValue()); } } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java index cba42d58f00b..e6b25b11cb09 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java @@ -19,12 +19,12 @@ import java.util.Optional; import java.util.stream.IntStream; -import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.AnnotatedElementContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.aggregator.AggregateWith; import org.junit.jupiter.params.aggregator.ArgumentsAccessor; import org.junit.jupiter.params.aggregator.ArgumentsAggregationException; -import org.junit.jupiter.params.aggregator.ArgumentsAggregator; +import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator; import org.junit.jupiter.params.provider.CsvSource; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.DiscoverySelector; @@ -61,14 +61,14 @@ private static DiscoverySelector selectorWithIdentifier(String identifier) { return parent; } - private static class VarargsAggregator implements ArgumentsAggregator { + private static class VarargsAggregator extends SimpleArgumentsAggregator { @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) - throws ArgumentsAggregationException { - Class parameterType = context.getParameter().getType(); - Preconditions.condition(parameterType.isArray(), () -> "must be an array type, but was " + parameterType); - Class componentType = parameterType.getComponentType(); - IntStream indices = IntStream.range(context.getIndex(), accessor.size()); + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { + + Preconditions.condition(targetType.isArray(), () -> "must be an array type, but was " + targetType); + Class componentType = targetType.getComponentType(); + IntStream indices = IntStream.range(parameterIndex, accessor.size()); if (componentType == int.class) { return indices.map(accessor::getInteger).toArray(); } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/DemoClassTestDescriptor.java b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/DemoClassTestDescriptor.java index 5687846244dc..eba8b5ea58ac 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/DemoClassTestDescriptor.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/DemoClassTestDescriptor.java @@ -17,8 +17,6 @@ import java.util.Set; import org.junit.jupiter.api.Tag; -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.engine.TestTag; import org.junit.platform.engine.UniqueId; @@ -28,8 +26,6 @@ */ public class DemoClassTestDescriptor extends AbstractTestDescriptor { - private static final Logger logger = LoggerFactory.getLogger(DemoClassTestDescriptor.class); - private final Class testClass; public DemoClassTestDescriptor(UniqueId uniqueId, Class testClass) { @@ -40,27 +36,11 @@ public DemoClassTestDescriptor(UniqueId uniqueId, Class testClass) { @Override public Set getTags() { - // Copied from org.junit.jupiter.engine.descriptor.JupiterTestDescriptor.getTags(AnnotatedElement) - // @formatter:off - return findRepeatableAnnotations(this.testClass, Tag.class).stream() - .map(Tag::value) - .filter(tag -> { - var 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, this.testClass)); - } - return isValid; - }) - .map(TestTag::create) + return findRepeatableAnnotations(this.testClass, Tag.class).stream() // + .map(Tag::value) // + .filter(TestTag::isValid) // + .map(TestTag::create) // .collect(toCollection(LinkedHashSet::new)); - // @formatter:on } @Override diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/DemoMethodTestDescriptor.java b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/DemoMethodTestDescriptor.java index 4e3c5aee0aa0..2c4dcb8b7402 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/DemoMethodTestDescriptor.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/DemoMethodTestDescriptor.java @@ -18,8 +18,6 @@ import java.util.Set; import org.junit.jupiter.api.Tag; -import org.junit.platform.commons.logging.Logger; -import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.TestTag; @@ -30,57 +28,29 @@ */ public class DemoMethodTestDescriptor extends AbstractTestDescriptor { - private static final Logger logger = LoggerFactory.getLogger(DemoMethodTestDescriptor.class); - - private final Class testClass; private final Method testMethod; - public DemoMethodTestDescriptor(UniqueId uniqueId, Class testClass, Method testMethod) { + public DemoMethodTestDescriptor(UniqueId uniqueId, Method testMethod) { super(uniqueId, String.format("%s(%s)", Preconditions.notNull(testMethod, "Method must not be null").getName(), ClassUtils.nullSafeToString(Class::getSimpleName, testMethod.getParameterTypes())), MethodSource.from(testMethod)); - this.testClass = Preconditions.notNull(testClass, "Class must not be null"); this.testMethod = testMethod; } @Override public Set getTags() { - // Copied from org.junit.jupiter.engine.descriptor.JupiterTestDescriptor.getTags(AnnotatedElement) - // @formatter:off - Set methodTags = findRepeatableAnnotations(this.testMethod, Tag.class).stream() - .map(Tag::value) - .filter(tag -> { - var 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, this.testMethod)); - } - return isValid; - }) - .map(TestTag::create) + Set methodTags = findRepeatableAnnotations(this.testMethod, Tag.class).stream() // + .map(Tag::value) // + .filter(TestTag::isValid) // + .map(TestTag::create) // .collect(toCollection(LinkedHashSet::new)); - // @formatter:on getParent().ifPresent(parentDescriptor -> methodTags.addAll(parentDescriptor.getTags())); return methodTags; } - public final Class getTestClass() { - return this.testClass; - } - - public final Method getTestMethod() { - return this.testMethod; - } - @Override public Type getType() { return Type.TEST; 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 new file mode 100644 index 000000000000..e93c016d0521 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolverTests.java @@ -0,0 +1,82 @@ +/* + * 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.discovery; + +import static org.junit.platform.engine.DiscoveryIssue.Severity.NOTICE; +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; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.ClassSelector; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.EngineDescriptor; +import org.junit.platform.launcher.LauncherDiscoveryListener; + +public class EngineDiscoveryRequestResolverTests { + + @Test + void allowsSelectorResolversToReportDiscoveryIssues() { + var resolver = EngineDiscoveryRequestResolver.builder() // + .addSelectorResolver(ctx -> new SelectorResolver() { + @Override + public Resolution resolve(ClassSelector selector, Context context) { + ctx.getIssueReporter() // + .reportIssue(DiscoveryIssue.builder(NOTICE, "test") // + .source(ClassSource.from(selector.getClassName()))); + return unresolved(); + } + }) // + .build(); + + var engineId = UniqueId.forEngine("engine"); + var engineDescriptor = new EngineDescriptor(engineId, "Engine"); + var listener = mock(LauncherDiscoveryListener.class); + var request = request() // + .selectors(selectClass(EngineDiscoveryRequestResolverTests.class)) // + .listeners(listener) // + .build(); + + resolver.resolve(request, engineDescriptor); + + var issue = DiscoveryIssue.builder(NOTICE, "test") // + .source(ClassSource.from(EngineDiscoveryRequestResolverTests.class)) // + .build(); + verify(listener).issueEncountered(engineId, issue); + } + + @Test + void allowsVisitorsToReportDiscoveryIssues() { + var resolver = EngineDiscoveryRequestResolver.builder() // + .addTestDescriptorVisitor(ctx -> // + descriptor -> ctx.getIssueReporter() // + .reportIssue(DiscoveryIssue.create(WARNING, descriptor.getDisplayName()))) // + .build(); + + var engineId = UniqueId.forEngine("engine"); + var engineDescriptor = new EngineDescriptor(engineId, "Engine"); + var listener = mock(LauncherDiscoveryListener.class); + var request = request() // + .selectors(selectClass(EngineDiscoveryRequestResolverTests.class)) // + .listeners(listener) // + .build(); + + resolver.resolve(request, engineDescriptor); + + verify(listener).issueEncountered(engineId, DiscoveryIssue.create(WARNING, "Engine")); + } + +} diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/MethodFilterTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/MethodFilterTests.java index 10a3d7931788..b868986c1d31 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/MethodFilterTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/MethodFilterTests.java @@ -145,11 +145,12 @@ private void assertExcluded(FilterResult filterResult, String excludedPattern) { private static TestDescriptor methodTestDescriptor(String uniqueId, Class testClass, String methodName) { var method = ReflectionUtils.findMethod(testClass, methodName, new Class[0]).orElseThrow(); - return new DemoMethodTestDescriptor(UniqueId.root("method", uniqueId), testClass, method); + return new DemoMethodTestDescriptor(UniqueId.root("method", uniqueId), method); } // ------------------------------------------------------------------------- + @SuppressWarnings("JUnitMalformedDeclaration") private static class Class1 { @Test void test1() { @@ -160,6 +161,7 @@ void test2() { } } + @SuppressWarnings("JUnitMalformedDeclaration") private static class Class2 { @Test void test1() { diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/TestPlanTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/TestPlanTests.java index 22be5ce5d260..c51d0c75ac9c 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/TestPlanTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestPlanTests.java @@ -10,18 +10,15 @@ package org.junit.platform.launcher; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import java.util.List; -import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.UniqueId; -import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; import org.junit.platform.engine.support.descriptor.EngineDescriptor; import org.junit.platform.fakes.TestDescriptorStub; @@ -31,56 +28,6 @@ class TestPlanTests { private final EngineDescriptor engineDescriptor = new EngineDescriptor(UniqueId.forEngine("foo"), "Foo"); - @Test - void doesNotContainTestsForEmptyContainers() { - engineDescriptor.addChild( - new AbstractTestDescriptor(engineDescriptor.getUniqueId().append("test", "bar"), "Bar") { - @Override - public Type getType() { - return Type.CONTAINER; - } - }); - - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); - - assertThat(testPlan.containsTests()).as("contains tests").isFalse(); - } - - @Test - void containsTestsForTests() { - engineDescriptor.addChild( - new AbstractTestDescriptor(engineDescriptor.getUniqueId().append("test", "bar"), "Bar") { - @Override - public Type getType() { - return Type.TEST; - } - }); - - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); - - assertThat(testPlan.containsTests()).as("contains tests").isTrue(); - } - - @Test - void containsTestsForContainersThatMayRegisterTests() { - engineDescriptor.addChild( - new AbstractTestDescriptor(engineDescriptor.getUniqueId().append("test", "bar"), "Bar") { - @Override - public Type getType() { - return Type.CONTAINER; - } - - @Override - public boolean mayRegisterTests() { - return true; - } - }); - - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); - - assertThat(testPlan.containsTests()).as("contains tests").isTrue(); - } - @Test void acceptsVisitorsInDepthFirstOrder() { var container = new TestDescriptorStub(engineDescriptor.getUniqueId().append("container", "bar"), "Bar"); @@ -94,7 +41,7 @@ void acceptsVisitorsInDepthFirstOrder() { engineDescriptor2.addChild(test2); engineDescriptor2.addChild(test3); - var testPlan = TestPlan.from(List.of(engineDescriptor, engineDescriptor2), configParams, + var testPlan = TestPlan.from(true, List.of(engineDescriptor, engineDescriptor2), configParams, dummyOutputDirectoryProvider()); var visitor = mock(TestPlan.Visitor.class); diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeEngineExecutionListenerTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeEngineExecutionListenerTests.java index 575f4407c24d..4db6397c1366 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeEngineExecutionListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeEngineExecutionListenerTests.java @@ -43,30 +43,28 @@ class CompositeEngineExecutionListenerTests { void shouldNotThrowExceptionButLogIfDynamicTestRegisteredListenerMethodFails(LogRecordListener logRecordListener) { compositeEngineExecutionListener().dynamicTestRegistered(anyTestDescriptor()); - assertThatTestListenerErrorLogged(logRecordListener, ThrowingEngineExecutionListener.class, - "dynamicTestRegistered"); + assertThatTestListenerErrorLogged(logRecordListener, "dynamicTestRegistered"); } @Test void shouldNotThrowExceptionButLogIfExecutionStartedListenerMethodFails(LogRecordListener logRecordListener) { compositeEngineExecutionListener().executionStarted(anyTestDescriptor()); - assertThatTestListenerErrorLogged(logRecordListener, ThrowingEngineExecutionListener.class, "executionStarted"); + assertThatTestListenerErrorLogged(logRecordListener, "executionStarted"); } @Test void shouldNotThrowExceptionButLogIfExecutionSkippedListenerMethodFails(LogRecordListener logRecordListener) { compositeEngineExecutionListener().executionSkipped(anyTestDescriptor(), "deliberately skipped container"); - assertThatTestListenerErrorLogged(logRecordListener, ThrowingEngineExecutionListener.class, "executionSkipped"); + assertThatTestListenerErrorLogged(logRecordListener, "executionSkipped"); } @Test void shouldNotThrowExceptionButLogIfExecutionFinishedListenerMethodFails(LogRecordListener logRecordListener) { compositeEngineExecutionListener().executionFinished(anyTestDescriptor(), anyTestExecutionResult()); - assertThatTestListenerErrorLogged(logRecordListener, ThrowingEngineExecutionListener.class, - "executionFinished"); + assertThatTestListenerErrorLogged(logRecordListener, "executionFinished"); } @Test @@ -74,8 +72,7 @@ void shouldNotThrowExceptionButLogIfReportingEntryPublishedListenerMethodFails( LogRecordListener logRecordListener) { compositeEngineExecutionListener().reportingEntryPublished(anyTestDescriptor(), ReportEntry.from("one", "two")); - assertThatTestListenerErrorLogged(logRecordListener, ThrowingEngineExecutionListener.class, - "reportingEntryPublished"); + assertThatTestListenerErrorLogged(logRecordListener, "reportingEntryPublished"); } @Test @@ -134,16 +131,15 @@ private static TestExecutionResult anyTestExecutionResult() { return mock(); } - private void assertThatTestListenerErrorLogged(LogRecordListener logRecordListener, Class listenerClass, - String methodName) { - assertThat(firstWarnLogRecord(logRecordListener).getMessage()).startsWith( - "EngineExecutionListener [" + listenerClass.getName() + "] threw exception for method: " + methodName); + private void assertThatTestListenerErrorLogged(LogRecordListener logRecordListener, String methodName) { + assertThat(firstWarnLogRecord(logRecordListener).getMessage()).startsWith("EngineExecutionListener [" + + ThrowingEngineExecutionListener.class.getName() + "] threw exception for method: " + methodName); } private static TestDescriptor anyTestDescriptor() { var testClass = CompositeEngineExecutionListenerTests.class; var method = ReflectionUtils.findMethod(testClass, "anyTestDescriptor", new Class[0]).orElseThrow(); - return new DemoMethodTestDescriptor(UniqueId.root("method", "unique_id"), testClass, method); + return new DemoMethodTestDescriptor(UniqueId.root("method", "unique_id"), method); } private static class ThrowingEngineExecutionListener implements EngineExecutionListener { diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeTestExecutionListenerTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeTestExecutionListenerTests.java index ebb6357891b8..93b9cfc3c94e 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeTestExecutionListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/CompositeTestExecutionListenerTests.java @@ -207,13 +207,13 @@ private void assertThatTestListenerErrorLogged(LogRecordListener logRecordListen } private static TestPlan anyTestPlan() { - return TestPlan.from(Set.of(anyTestDescriptor()), mock(), dummyOutputDirectoryProvider()); + return TestPlan.from(true, Set.of(anyTestDescriptor()), mock(), dummyOutputDirectoryProvider()); } private static DemoMethodTestDescriptor anyTestDescriptor() { var testClass = CompositeTestExecutionListenerTests.class; var method = ReflectionUtils.findMethod(testClass, "anyTestDescriptor", new Class[0]).orElseThrow(); - return new DemoMethodTestDescriptor(UniqueId.root("method", "unique_id"), testClass, method); + return new DemoMethodTestDescriptor(UniqueId.root("method", "unique_id"), method); } private static class ThrowingEagerTestExecutionListener extends ThrowingTestExecutionListener 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 e1f240617575..e7a0f1a7cc3e 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 @@ -10,14 +10,21 @@ package org.junit.platform.launcher.core; +import static java.util.Objects.requireNonNull; +import static java.util.function.UnaryOperator.identity; 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.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; +import static org.junit.platform.engine.SelectorResolutionResult.unresolved; import static org.junit.platform.engine.TestExecutionResult.successful; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.fakes.FaultyTestEngines.createEngineThatCannotResolveAnything; +import static org.junit.platform.fakes.FaultyTestEngines.createEngineThatFailsToResolveAnything; import static org.junit.platform.launcher.LauncherConstants.DRY_RUN_PROPERTY_NAME; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.DEFAULT_DISCOVERY_LISTENER_CONFIGURATION_PROPERTY_NAME; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; @@ -25,12 +32,17 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.time.Instant; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.UnaryOperator; import java.util.logging.Level; import java.util.logging.LogRecord; @@ -42,12 +54,17 @@ import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.logging.LogRecordListener; import org.junit.platform.commons.util.ExceptionUtils; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.FilterResult; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.TestExecutionResult.Status; +import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.descriptor.EngineDescriptor; import org.junit.platform.engine.support.hierarchical.DemoHierarchicalTestDescriptor; @@ -64,6 +81,7 @@ import org.junit.platform.launcher.TestPlan; import org.junit.platform.launcher.listeners.SummaryGeneratingListener; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; /** * @since 1.0 @@ -121,7 +139,7 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId .configurationParameter(DEFAULT_DISCOVERY_LISTENER_CONFIGURATION_PROPERTY_NAME, "logging") // .build()); assertThat(testPlan.getRoots()).hasSize(1); - assertDiscoveryFailed(engine, discoveryListener); + assertDiscoveryFailed(engine, inOrder(discoveryListener), discoveryListener); } @ParameterizedTest @@ -152,9 +170,11 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId assertThat(testPlan.getRoots()).hasSize(1); var engineIdentifier = getOnlyElement(testPlan.getRoots()); assertThat(getOnlyElement(testPlan.getRoots()).getDisplayName()).isEqualTo("my-engine-id"); - verify(discoveryListener).launcherDiscoveryStarted(request); - verify(discoveryListener).launcherDiscoveryFinished(request); - assertDiscoveryFailed(engine, discoveryListener); + + InOrder inOrder = inOrder(discoveryListener); + inOrder.verify(discoveryListener).launcherDiscoveryStarted(request); + assertDiscoveryFailed(engine, inOrder, discoveryListener); + inOrder.verify(discoveryListener).launcherDiscoveryFinished(request); var listener = mock(TestExecutionListener.class); launcher.execute(testPlan, listener); @@ -167,10 +187,13 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId .hasMessage("TestEngine with ID 'my-engine-id' failed to discover tests"); } - private void assertDiscoveryFailed(TestEngine testEngine, LauncherDiscoveryListener discoveryListener) { + private void assertDiscoveryFailed(TestEngine testEngine, InOrder inOrder, + LauncherDiscoveryListener discoveryListener) { var engineId = testEngine.getId(); var failureCaptor = ArgumentCaptor.forClass(EngineDiscoveryResult.class); - verify(discoveryListener).engineDiscoveryFinished(eq(UniqueId.forEngine(engineId)), failureCaptor.capture()); + inOrder.verify(discoveryListener).engineDiscoveryStarted(UniqueId.forEngine(engineId)); + inOrder.verify(discoveryListener).engineDiscoveryFinished(eq(UniqueId.forEngine(engineId)), + failureCaptor.capture()); var result = failureCaptor.getValue(); assertThat(result.getStatus()).isEqualTo(EngineDiscoveryResult.Status.FAILED); assertThat(result.getThrowable()).isPresent(); @@ -256,8 +279,7 @@ void reportsEngineExecutionFailuresForSuccessfullyFinishedEngine() { public void execute(ExecutionRequest request) { var engineDescriptor = request.getRootTestDescriptor(); request.getEngineExecutionListener().executionStarted(engineDescriptor); - request.getEngineExecutionListener().executionFinished(engineDescriptor, - TestExecutionResult.successful()); + request.getEngineExecutionListener().executionFinished(engineDescriptor, successful()); throw rootCause; } }; @@ -328,7 +350,7 @@ public void execute(ExecutionRequest request) { var engineDescriptor = request.getRootTestDescriptor(); var listener = request.getEngineExecutionListener(); listener.executionStarted(engineDescriptor); - listener.executionFinished(engineDescriptor, TestExecutionResult.successful()); + listener.executionFinished(engineDescriptor, successful()); } }; @@ -336,7 +358,7 @@ public void execute(ExecutionRequest request) { createLauncher(engine).execute(request().build(), listener); verify(listener).executionStarted(any()); - verify(listener).executionFinished(any(), eq(TestExecutionResult.successful())); + verify(listener).executionFinished(any(), eq(successful())); } @Test @@ -574,7 +596,7 @@ void launcherCanExecuteTestPlanExactlyOnce() { verify(engine, times(1)).execute(any()); var e = assertThrows(PreconditionViolationException.class, () -> launcher.execute(testPlan)); - assertEquals(e.getMessage(), "TestPlan must only be executed once"); + assertEquals("TestPlan must only be executed once", e.getMessage()); } @Test @@ -651,4 +673,266 @@ void dryRunModeReportsEventsForAllTestsButDoesNotExecuteThem() { inOrder.verify(listener).testPlanExecutionFinished(any()); inOrder.verifyNoMoreInteractions(); } + + @Test + void notifiesDiscoveryListenersOfProcessedSelectors() { + TestEngine engine = new TestEngineStub("some-engine-id") { + + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + discoveryRequest.getSelectorsByType(DiscoverySelector.class).forEach(selector -> { + discoveryRequest.getDiscoveryListener().selectorProcessed(uniqueId, selector, unresolved()); + }); + return new EngineDescriptor(uniqueId, uniqueId.getLastSegment().getValue()); + } + }; + var engineId = UniqueId.forEngine(engine.getId()); + + var discoveryListenerOnConfig = mock(LauncherDiscoveryListener.class, "discoveryListenerOnConfig"); + var discoveryListenerOnLauncher = mock(LauncherDiscoveryListener.class, "discoveryListenerOnLauncher"); + var discoveryListenerOnRequest = mock(LauncherDiscoveryListener.class, "discoveryListenerOnRequest"); + var selector = mock(DiscoverySelector.class); + + var launcherConfig = LauncherFactoryForTestingPurposesOnly.createLauncherConfigBuilderWithDisabledServiceLoading() // + .addTestEngines(engine) // + .addLauncherDiscoveryListeners(discoveryListenerOnConfig) // + .build(); + + var launcher = LauncherFactory.create(launcherConfig); + launcher.registerLauncherDiscoveryListeners(discoveryListenerOnLauncher); + + launcher.discover(request() // + .selectors(selector) // + .listeners(discoveryListenerOnRequest) // + .build()); + + assertAll( // + () -> verify(discoveryListenerOnConfig).selectorProcessed(engineId, selector, unresolved()), // + () -> verify(discoveryListenerOnLauncher).selectorProcessed(engineId, selector, unresolved()), // + () -> verify(discoveryListenerOnRequest).selectorProcessed(engineId, selector, unresolved()) // + ); + } + + @Test + void reportsEngineExecutionFailureForCriticalDiscoveryIssuesAndLogsRemaining( + @TrackLogRecords LogRecordListener listener) { + + var result = execute(new TestEngineStub("engine-id") { + @Override + 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.WARNING, "warning")); + return new EngineDescriptor(uniqueId, "Engine") { + @Override + public Set getTags() { + return Set.of(TestTag.create("custom-tag")); + } + }; + } + }); + + assertThat(result.testPlan().containsTests()).isTrue(); + + assertThat(result.testIdentifier().getDisplayName()).isEqualTo("Engine"); + assertThat(result.testIdentifier().getTags()).containsExactly(TestTag.create("custom-tag")); + + assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.FAILED); + assertThat(result.testExecutionResult().getThrowable().orElseThrow()) // + .hasMessageStartingWith( + "TestEngine with ID 'engine-id' encountered a critical issue during test discovery") // + .hasMessageContaining("(1) [ERROR] error"); + + var logRecord = findFirstDiscoveryIssueLogRecord(listener, Level.WARNING); + assertThat(logRecord.getMessage()) // + .startsWith("TestEngine with ID 'engine-id' encountered a non-critical issue during test discovery") // + .contains("(1) [WARNING] warning"); + assertThat(logRecord.getInstant()) // + .isBetween(result.startTime(), result.finishTime()); + } + + @Test + void logsNonCriticalIssuesForRegularEngineExecution(@TrackLogRecords LogRecordListener listener) { + + var result = execute(new TestEngineStub("engine-id") { + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + var listener = discoveryRequest.getDiscoveryListener(); + listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.NOTICE, "notice")); + 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()); + } + }); + + assertThat(result.testIdentifier().getDisplayName()).isEqualTo("Engine"); + assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.SUCCESSFUL); + + 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"); + assertThat(logRecord.getInstant()) // + .isBetween(result.startTime(), result.finishTime()); + } + + @Test + void logsAllIssuesForDiscoveryFailure(@TrackLogRecords LogRecordListener listener) { + + var result = execute(new TestEngineStub("engine-id") { + @Override + 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")); + throw new RuntimeException("boom"); + } + }); + + assertThat(result.testPlan().containsTests()).isTrue(); + + assertThat(result.testIdentifier().getDisplayName()).isEqualTo("engine-id"); + assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.FAILED); + assertThat(result.testExecutionResult().getThrowable().orElseThrow()) // + .hasMessage("TestEngine with ID 'engine-id' failed to discover tests") // + .cause().hasMessage("boom"); + + var logRecord = findFirstDiscoveryIssueLogRecord(listener, Level.SEVERE); + assertThat(logRecord.getMessage()) // + .startsWith("TestEngine with ID 'engine-id' encountered a critical issue during test discovery") // + .contains("(1) [ERROR] error"); + assertThat(logRecord.getInstant()) // + .isBetween(result.startTime(), result.finishTime()); + + 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"); + assertThat(logRecord.getInstant()) // + .isBetween(result.startTime(), result.finishTime()); + } + + @Test + void logsNonCriticalIssuesForExecutionFailure(@TrackLogRecords LogRecordListener listener) { + + var result = execute(new TestEngineStub("engine-id") { + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + var listener = discoveryRequest.getDiscoveryListener(); + listener.issueEncountered(uniqueId, DiscoveryIssue.create(Severity.NOTICE, "notice")); + return new EngineDescriptor(uniqueId, "Engine"); + } + + @Override + public void execute(ExecutionRequest request) { + throw new RuntimeException("boom"); + } + }); + + assertThat(result.testIdentifier().getDisplayName()).isEqualTo("Engine"); + + assertThat(result.testExecutionResult().getThrowable().orElseThrow()) // + .hasMessage("TestEngine with ID 'engine-id' failed to execute tests") // + .cause().hasMessage("boom"); + + 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"); + assertThat(logRecord.getInstant()) // + .isBetween(result.startTime(), result.finishTime()); + } + + @Test + void reportsEngineExecutionFailureOnUnresolvedUniqueIdSelectorWithEnginePrefix() { + var engine = createEngineThatCannotResolveAnything("some-engine"); + var selector = selectUniqueId(UniqueId.forEngine(engine.getId())); + var result = execute(engine, request -> request.selectors(selector)); + + assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.FAILED); + assertThat(result.testExecutionResult().getThrowable().orElseThrow()) // + .hasMessageStartingWith( + "TestEngine with ID 'some-engine' encountered a critical issue during test discovery") // + .hasMessageContaining("(1) [ERROR] %s could not be resolved", selector); + } + + @Test + void ignoresUnresolvedUniqueIdSelectorWithoutEnginePrefix() { + var engine = createEngineThatCannotResolveAnything("some-engine"); + var selector = selectUniqueId(UniqueId.forEngine("some-other-engine")); + var result = execute(engine, request -> request.selectors(selector)); + + assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.SUCCESSFUL); + } + + @Test + void reportsEngineExecutionFailureForSelectorResolutionFailure() { + var engine = createEngineThatFailsToResolveAnything("some-engine", new RuntimeException("boom")); + var selector = selectClass(Object.class); + var result = execute(engine, request -> request.selectors(selector)); + + assertThat(result.testExecutionResult().getStatus()).isEqualTo(Status.FAILED); + assertThat(result.testExecutionResult().getThrowable().orElseThrow()) // + .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"); + } + + private static ReportedData execute(TestEngine engine) { + return execute(engine, identity()); + } + + private static ReportedData execute(TestEngine engine, UnaryOperator configurer) { + var executionListener = mock(TestExecutionListener.class); + + AtomicReference startTime = new AtomicReference<>(); + doAnswer(invocation -> { + startTime.set(Instant.now()); + return null; + }).when(executionListener).executionStarted(any()); + + AtomicReference finishTime = new AtomicReference<>(); + doAnswer(invocation -> { + finishTime.set(Instant.now()); + return null; + }).when(executionListener).executionFinished(any(), any()); + + var builder = request() // + .configurationParameter(DEFAULT_DISCOVERY_LISTENER_CONFIGURATION_PROPERTY_NAME, "logging"); + var request = configurer.apply(builder).build(); + var launcher = createLauncher(engine); + + var testPlan = launcher.discover(request); + launcher.execute(testPlan, executionListener); + + var inOrder = inOrder(executionListener); + var testIdentifier = ArgumentCaptor.forClass(TestIdentifier.class); + var testExecutionResult = ArgumentCaptor.forClass(TestExecutionResult.class); + inOrder.verify(executionListener).testPlanExecutionStarted(any()); + inOrder.verify(executionListener).executionStarted(testIdentifier.capture()); + inOrder.verify(executionListener).executionFinished(any(), testExecutionResult.capture()); + inOrder.verify(executionListener).testPlanExecutionFinished(any()); + inOrder.verifyNoMoreInteractions(); + + return new ReportedData(testPlan, testIdentifier.getValue(), testExecutionResult.getValue(), + requireNonNull(startTime.get()), requireNonNull(finishTime.get())); + } + + private static LogRecord findFirstDiscoveryIssueLogRecord(LogRecordListener listener, Level level) { + return listener.stream(DiscoveryIssueNotifier.class, level) // + .findFirst() // + .orElseThrow(); + } + + private record ReportedData(TestPlan testPlan, TestIdentifier testIdentifier, + TestExecutionResult testExecutionResult, Instant startTime, Instant finishTime) { + } + } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/ExecutionListenerAdapterTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/ExecutionListenerAdapterTests.java index 32b2087ed966..d8a29dc6e105 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/ExecutionListenerAdapterTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/ExecutionListenerAdapterTests.java @@ -24,6 +24,7 @@ import org.junit.platform.engine.support.descriptor.DemoMethodTestDescriptor; import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.core.LauncherDiscoveryResult.EngineResultInfo; /** * @since 1.0 @@ -35,7 +36,8 @@ class ExecutionListenerAdapterTests { void testReportingEntryPublished() { var testDescriptor = getSampleMethodTestDescriptor(); - var discoveryResult = new LauncherDiscoveryResult(Map.of(mock(), testDescriptor), mock(), + var discoveryResult = new LauncherDiscoveryResult( + Map.of(mock(), EngineResultInfo.completed(testDescriptor, DiscoveryIssueNotifier.NO_ISSUES)), mock(), dummyOutputDirectoryProvider()); var testPlan = InternalTestPlan.from(discoveryResult); var testIdentifier = testPlan.getTestIdentifier(testDescriptor.getUniqueId()); @@ -52,9 +54,9 @@ void testReportingEntryPublished() { } private TestDescriptor getSampleMethodTestDescriptor() { - var localMethodNamedNothing = ReflectionUtils.findMethod(this.getClass(), "nothing", new Class[0]).get(); - return new DemoMethodTestDescriptor(UniqueId.root("method", "unique_id"), this.getClass(), - localMethodNamedNothing); + var localMethodNamedNothing = ReflectionUtils.findMethod(this.getClass(), "nothing", + new Class[0]).orElseThrow(); + return new DemoMethodTestDescriptor(UniqueId.root("method", "unique_id"), localMethodNamedNothing); } //for reflection purposes only diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/InternalTestPlanTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/InternalTestPlanTests.java new file mode 100644 index 000000000000..9e69aecd254d --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/InternalTestPlanTests.java @@ -0,0 +1,118 @@ +/* + * 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.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; +import static org.mockito.Mockito.mock; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor; +import org.junit.platform.engine.support.descriptor.EngineDescriptor; +import org.junit.platform.launcher.core.LauncherDiscoveryResult.EngineResultInfo; + +public class InternalTestPlanTests { + + private final ConfigurationParameters configParams = mock(); + + private final EngineDescriptor engineDescriptor = new EngineDescriptor(UniqueId.forEngine("foo"), "Foo"); + + @Test + void doesNotContainTestsForEmptyContainers() { + engineDescriptor.addChild( + new AbstractTestDescriptor(engineDescriptor.getUniqueId().append("test", "bar"), "Bar") { + @Override + public Type getType() { + return Type.CONTAINER; + } + }); + + var testPlan = InternalTestPlan.from(createLauncherDiscoveryResult( + EngineResultInfo.completed(engineDescriptor, DiscoveryIssueNotifier.NO_ISSUES))); + + assertThat(testPlan.containsTests()).as("contains tests").isFalse(); + } + + @Test + void containsTestsForTests() { + engineDescriptor.addChild( + new AbstractTestDescriptor(engineDescriptor.getUniqueId().append("test", "bar"), "Bar") { + @Override + public Type getType() { + return Type.TEST; + } + }); + + var testPlan = InternalTestPlan.from(createLauncherDiscoveryResult( + EngineResultInfo.completed(engineDescriptor, DiscoveryIssueNotifier.NO_ISSUES))); + + assertThat(testPlan.containsTests()).as("contains tests").isTrue(); + } + + @Test + void containsTestsForContainersThatMayRegisterTests() { + engineDescriptor.addChild( + new AbstractTestDescriptor(engineDescriptor.getUniqueId().append("test", "bar"), "Bar") { + @Override + public Type getType() { + return Type.CONTAINER; + } + + @Override + public boolean mayRegisterTests() { + return true; + } + }); + + var testPlan = InternalTestPlan.from(createLauncherDiscoveryResult( + EngineResultInfo.completed(engineDescriptor, DiscoveryIssueNotifier.NO_ISSUES))); + + assertThat(testPlan.containsTests()).as("contains tests").isTrue(); + } + + @Test + void containsTestsForEnginesWithDiscoveryError() { + var testPlan = InternalTestPlan.from(createLauncherDiscoveryResult( + EngineResultInfo.errored(engineDescriptor, DiscoveryIssueNotifier.NO_ISSUES, new RuntimeException()))); + + assertThat(testPlan.containsTests()).as("contains tests").isTrue(); + } + + @Test + void containsTestsForEnginesWithCriticalDiscoveryIssues() { + var testPlan = InternalTestPlan.from(createLauncherDiscoveryResult(EngineResultInfo.completed(engineDescriptor, + DiscoveryIssueNotifier.from(Severity.ERROR, List.of(DiscoveryIssue.create(Severity.ERROR, "error")))))); + + assertThat(testPlan.containsTests()).as("contains tests").isTrue(); + } + + @Test + void doesNotContainTestsForEnginesWithNonCriticalDiscoveryIssues() { + var testPlan = InternalTestPlan.from(createLauncherDiscoveryResult(EngineResultInfo.completed(engineDescriptor, + DiscoveryIssueNotifier.from(Severity.ERROR, List.of(DiscoveryIssue.create(Severity.WARNING, "warning")))))); + + assertThat(testPlan.containsTests()).as("contains tests").isFalse(); + } + + private LauncherDiscoveryResult createLauncherDiscoveryResult(EngineResultInfo result) { + var testEngineResults = Map.of(mock(TestEngine.class), result); + return new LauncherDiscoveryResult(testEngineResults, configParams, dummyOutputDirectoryProvider()); + } + +} diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilderTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilderTests.java index 93d9cd4a771f..0d92aee8927a 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilderTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilderTests.java @@ -109,7 +109,7 @@ void methodsByFullyQualifiedNameAreStoredInDiscoveryRequest() { var methodSelectors = discoveryRequest.getSelectorsByType(MethodSelector.class); assertThat(methodSelectors).hasSize(1); - var methodSelector = methodSelectors.get(0); + var methodSelector = methodSelectors.getFirst(); assertThat(methodSelector.getJavaClass()).isEqualTo(LauncherDiscoveryRequestBuilderTests.class); assertThat(methodSelector.getJavaMethod()).isEqualTo(fullyQualifiedMethod()); } @@ -128,7 +128,7 @@ void methodsByNameAreStoredInDiscoveryRequest() throws Exception { var methodSelectors = discoveryRequest.getSelectorsByType(MethodSelector.class); assertThat(methodSelectors).hasSize(1); - var methodSelector = methodSelectors.get(0); + var methodSelector = methodSelectors.getFirst(); assertThat(methodSelector.getJavaClass()).isEqualTo(testClass); assertThat(methodSelector.getJavaMethod()).isEqualTo(testMethod); } @@ -148,7 +148,7 @@ void methodsByClassAreStoredInDiscoveryRequest() throws Exception { var methodSelectors = discoveryRequest.getSelectorsByType(MethodSelector.class); assertThat(methodSelectors).hasSize(1); - var methodSelector = methodSelectors.get(0); + var methodSelector = methodSelectors.getFirst(); assertThat(methodSelector.getJavaClass()).isEqualTo(testClass); assertThat(methodSelector.getJavaMethod()).isEqualTo(testMethod); } @@ -190,7 +190,7 @@ void engineFiltersAreStoredInDiscoveryRequest() { var filters = discoveryRequest.getEngineFilters(); assertThat(filters).hasSize(1); - var engineFilter = filters.get(0); + var engineFilter = filters.getFirst(); assertTrue(engineFilter.apply(engine1).included()); assertTrue(engineFilter.apply(engine2).included()); assertTrue(engineFilter.apply(engine3).excluded()); diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/ListenerRegistryTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/ListenerRegistryTests.java index c95b2ea1fd2f..1c3a08783764 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/ListenerRegistryTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/ListenerRegistryTests.java @@ -20,7 +20,7 @@ public class ListenerRegistryTests { @Test void registerWithNullArray() { - var registry = ListenerRegistry.create(l -> l.get(0)); + var registry = ListenerRegistry.create(l -> l.getFirst()); var exception = assertThrows(PreconditionViolationException.class, () -> registry.addAll((Object[]) null)); @@ -29,7 +29,7 @@ void registerWithNullArray() { @Test void registerWithEmptyArray() { - var registry = ListenerRegistry.create(l -> l.get(0)); + var registry = ListenerRegistry.create(l -> l.getFirst()); var exception = assertThrows(PreconditionViolationException.class, registry::addAll); @@ -38,7 +38,7 @@ void registerWithEmptyArray() { @Test void registerWithArrayContainingNullElements() { - var registry = ListenerRegistry.create(l -> l.get(0)); + var registry = ListenerRegistry.create(l -> l.getFirst()); var exception = assertThrows(PreconditionViolationException.class, () -> registry.addAll(new Object[] { null })); diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/SummaryGenerationTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/SummaryGenerationTests.java index adda779701d0..c8a299835e42 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/SummaryGenerationTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/SummaryGenerationTests.java @@ -40,7 +40,7 @@ class SummaryGenerationTests { private final SummaryGeneratingListener listener = new SummaryGeneratingListener(); - private final TestPlan testPlan = TestPlan.from(List.of(), mock(), dummyOutputDirectoryProvider()); + private final TestPlan testPlan = TestPlan.from(true, List.of(), mock(), dummyOutputDirectoryProvider()); @Test void emptyReport() { @@ -149,8 +149,8 @@ public Optional getSource() { listener.testPlanExecutionFinished(testPlan); final var failures = listener.getSummary().getFailures(); assertThat(failures).hasSize(1); - assertThat(failures.get(0).getException()).isEqualTo(failedException); - assertThat(failures.get(0).getTestIdentifier()).isEqualTo(failingTest); + assertThat(failures.getFirst().getException()).isEqualTo(failedException); + assertThat(failures.getFirst().getTestIdentifier()).isEqualTo(failingTest); } @Test diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/AbortOnFailureLauncherDiscoveryListenerTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/AbortOnFailureLauncherDiscoveryListenerTests.java index 5c855acdaf39..867ba9607f03 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/AbortOnFailureLauncherDiscoveryListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/AbortOnFailureLauncherDiscoveryListenerTests.java @@ -11,9 +11,7 @@ package org.junit.platform.launcher.listeners.discovery; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; import static org.junit.platform.launcher.core.LauncherFactoryForTestingPurposesOnly.createLauncher; @@ -26,51 +24,7 @@ import org.junit.platform.engine.UniqueId; import org.junit.platform.fakes.TestEngineStub; -class AbortOnFailureLauncherDiscoveryListenerTests extends AbstractLauncherDiscoveryListenerTests { - - @Test - void abortsDiscoveryOnUnresolvedUniqueIdSelectorWithEnginePrefix() { - var engine = createEngineThatCannotResolveAnything("some-engine"); - var request = request() // - .listeners(abortOnFailure()) // - .selectors(selectUniqueId(UniqueId.forEngine(engine.getId()))) // - .build(); - var launcher = createLauncher(engine); - - var exception = assertThrows(JUnitException.class, () -> launcher.discover(request)); - assertThat(exception).hasMessage("TestEngine with ID 'some-engine' failed to discover tests"); - assertThat(exception.getCause()).hasMessage( - "UniqueIdSelector [uniqueId = [engine:some-engine]] could not be resolved"); - } - - @Test - void doesNotAbortDiscoveryOnUnresolvedUniqueIdSelectorWithoutEnginePrefix() { - var engine = createEngineThatCannotResolveAnything("some-engine"); - var request = request() // - .listeners(abortOnFailure()) // - .selectors(selectUniqueId(UniqueId.forEngine("some-other-engine"))) // - .build(); - var launcher = createLauncher(engine); - - assertDoesNotThrow(() -> launcher.discover(request)); - } - - @Test - void abortsDiscoveryOnSelectorResolutionFailure() { - var rootCause = new RuntimeException(); - var engine = createEngineThatFailsToResolveAnything("some-engine", rootCause); - var request = request() // - .listeners(abortOnFailure()) // - .selectors(selectClass(Object.class)) // - .build(); - var launcher = createLauncher(engine); - - var exception = assertThrows(JUnitException.class, () -> launcher.discover(request)); - assertThat(exception).hasMessage("TestEngine with ID 'some-engine' failed to discover tests"); - assertThat(exception.getCause()) // - .hasMessageEndingWith("resolution failed") // - .cause().isSameAs(rootCause); - } +class AbortOnFailureLauncherDiscoveryListenerTests { @Test void abortsDiscoveryOnEngineDiscoveryFailure() { diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/CompositeLauncherDiscoveryListenerTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/CompositeLauncherDiscoveryListenerTests.java index b0a1f5bbcf2c..ef94cea1f7e9 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/CompositeLauncherDiscoveryListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/CompositeLauncherDiscoveryListenerTests.java @@ -17,6 +17,8 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.SelectorResolutionResult; import org.junit.platform.engine.UniqueId; import org.junit.platform.launcher.EngineDiscoveryResult; @@ -36,20 +38,28 @@ void callsListenersInReverseOrderForFinishedEvents() { var engineDiscoveryResult = EngineDiscoveryResult.successful(); var selector = selectUniqueId(engineId); var selectorResolutionResult = SelectorResolutionResult.resolved(); + var discoveryIssue = DiscoveryIssue.create(Severity.WARNING, "message"); var composite = new CompositeLauncherDiscoveryListener(List.of(firstListener, secondListener)); composite.launcherDiscoveryStarted(launcherDiscoveryRequest); composite.engineDiscoveryStarted(engineId); composite.selectorProcessed(engineId, selector, selectorResolutionResult); + composite.issueEncountered(engineId, discoveryIssue); composite.engineDiscoveryFinished(engineId, engineDiscoveryResult); composite.launcherDiscoveryFinished(launcherDiscoveryRequest); InOrder inOrder = inOrder(firstListener, secondListener); + inOrder.verify(firstListener).launcherDiscoveryStarted(launcherDiscoveryRequest); inOrder.verify(secondListener).launcherDiscoveryStarted(launcherDiscoveryRequest); inOrder.verify(firstListener).engineDiscoveryStarted(engineId); inOrder.verify(secondListener).engineDiscoveryStarted(engineId); + + inOrder.verify(firstListener).selectorProcessed(engineId, selector, selectorResolutionResult); inOrder.verify(secondListener).selectorProcessed(engineId, selector, selectorResolutionResult); + inOrder.verify(firstListener).issueEncountered(engineId, discoveryIssue); + inOrder.verify(secondListener).issueEncountered(engineId, discoveryIssue); + inOrder.verify(secondListener).engineDiscoveryFinished(engineId, engineDiscoveryResult); inOrder.verify(firstListener).engineDiscoveryFinished(engineId, engineDiscoveryResult); inOrder.verify(secondListener).launcherDiscoveryFinished(launcherDiscoveryRequest); diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/LoggingLauncherDiscoveryListenerTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/LoggingLauncherDiscoveryListenerTests.java index 86c7f5724999..400f4b43dd6a 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/LoggingLauncherDiscoveryListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/listeners/discovery/LoggingLauncherDiscoveryListenerTests.java @@ -13,6 +13,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.fakes.FaultyTestEngines.createEngineThatCannotResolveAnything; +import static org.junit.platform.fakes.FaultyTestEngines.createEngineThatFailsToResolveAnything; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.DEFAULT_DISCOVERY_LISTENER_CONFIGURATION_PROPERTY_NAME; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; import static org.junit.platform.launcher.core.LauncherFactoryForTestingPurposesOnly.createLauncher; @@ -29,7 +31,7 @@ import org.junit.platform.fakes.TestEngineStub; @TrackLogRecords -public class LoggingLauncherDiscoveryListenerTests extends AbstractLauncherDiscoveryListenerTests { +public class LoggingLauncherDiscoveryListenerTests { @Test void logsWarningOnUnresolvedUniqueIdSelectorWithEnginePrefix(LogRecordListener log) { diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/LegacyReportingUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/LegacyReportingUtilsTests.java index b8e5b25d5f41..6f5e67291f2f 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/LegacyReportingUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/LegacyReportingUtilsTests.java @@ -72,13 +72,13 @@ void legacyReportingClassNameForDescendantOfTestIdentifierWithClassSourceIsClass } private String getClassName(UniqueId uniqueId) { - var testPlan = TestPlan.from(Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider()); return LegacyReportingUtils.getClassName(testPlan, testPlan.getTestIdentifier(uniqueId)); } @SuppressWarnings("deprecation") private String getClassNameFromOldLocation(UniqueId uniqueId) { - var testPlan = TestPlan.from(Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider()); return org.junit.platform.launcher.listeners.LegacyReportingUtils.getClassName(testPlan, testPlan.getTestIdentifier(uniqueId)); } diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java index a291ff90b51f..f405a2163637 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/LegacyXmlReportGeneratingListenerTests.java @@ -371,7 +371,7 @@ void printsExceptionWhenReportsDirCannotBeCreated() throws Exception { var out = new StringWriter(); var listener = new LegacyXmlReportGeneratingListener(reportsDir, new PrintWriter(out)); - listener.testPlanExecutionStarted(TestPlan.from(Set.of(), mock(), dummyOutputDirectoryProvider())); + listener.testPlanExecutionStarted(TestPlan.from(true, Set.of(), mock(), dummyOutputDirectoryProvider())); assertThat(out.toString()).containsSubsequence("Could not create reports directory", "FileAlreadyExistsException", "at "); @@ -388,7 +388,7 @@ void printsExceptionWhenReportCouldNotBeWritten() throws Exception { var listener = new LegacyXmlReportGeneratingListener(tempDirectory, new PrintWriter(out)); listener.testPlanExecutionStarted( - TestPlan.from(Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider())); + TestPlan.from(true, Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider())); listener.executionFinished(TestIdentifier.from(engineDescriptor), successful()); assertThat(out.toString()).containsSubsequence("Could not write XML report", "Exception", "at "); @@ -399,7 +399,7 @@ void writesReportEntriesToSystemOutElement() throws Exception { var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("engine"), "Engine"); var childUniqueId = UniqueId.root("child", "test"); engineDescriptor.addChild(new TestDescriptorStub(childUniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), mock(), dummyOutputDirectoryProvider()); var out = new StringWriter(); var listener = new LegacyXmlReportGeneratingListener(tempDirectory, new PrintWriter(out)); diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportDataTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportDataTests.java index c0e84ebdccbe..cfb89cc5ffcb 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportDataTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportDataTests.java @@ -38,7 +38,7 @@ void resultsOfTestIdentifierWithoutAnyReportedEventsAreEmpty() { var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("engine"), "Engine"); var childUniqueId = UniqueId.root("child", "test"); engineDescriptor.addChild(new TestDescriptorStub(childUniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var results = reportData.getResults(testPlan.getTestIdentifier(childUniqueId)); @@ -51,7 +51,7 @@ void resultsOfTestIdentifierWithoutReportedEventsContainsOnlyFailureOfAncestor() var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("engine"), "Engine"); var childUniqueId = UniqueId.root("child", "test"); engineDescriptor.addChild(new TestDescriptorStub(childUniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var failureOfAncestor = failed(new RuntimeException("failed!")); @@ -67,7 +67,7 @@ void resultsOfTestIdentifierWithoutReportedEventsContainsOnlySuccessOfAncestor() var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("engine"), "Engine"); var childUniqueId = UniqueId.root("child", "test"); engineDescriptor.addChild(new TestDescriptorStub(childUniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); reportData.markFinished(testPlan.getTestIdentifier(engineDescriptor.getUniqueId()), successful()); diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportWriterTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportWriterTests.java index 0eebd933ffc4..9dfb2506a38e 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportWriterTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/legacy/xml/XmlReportWriterTests.java @@ -58,7 +58,7 @@ class XmlReportWriterTests { @Test void writesTestsuiteElementsWithoutTestcaseElementsWithoutAnyTests() throws Exception { - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); @@ -76,7 +76,7 @@ void writesReportEntry() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); var testDescriptor = new TestDescriptorStub(uniqueId, "successfulTest"); engineDescriptor.addChild(testDescriptor); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); reportData.addReportEntry(TestIdentifier.from(testDescriptor), ReportEntry.from("myKey", "myValue")); @@ -94,7 +94,7 @@ void writesCapturedOutput() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); var testDescriptor = new TestDescriptorStub(uniqueId, "successfulTest"); engineDescriptor.addChild(testDescriptor); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var reportEntry = ReportEntry.from(Map.of( // @@ -123,7 +123,7 @@ void writesCapturedOutput() throws Exception { void writesEmptySkippedElementForSkippedTestWithoutReason() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); engineDescriptor.addChild(new TestDescriptorStub(uniqueId, "skippedTest")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); reportData.markSkipped(testPlan.getTestIdentifier(uniqueId), null); @@ -153,7 +153,7 @@ public String getLegacyReportingName() { return "failedTest"; } }); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); reportData.markFinished(testPlan.getTestIdentifier(uniqueId), failed(null)); @@ -173,7 +173,7 @@ public String getLegacyReportingName() { void omitsMessageAttributeForFailedTestWithThrowableWithoutMessage() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); engineDescriptor.addChild(new TestDescriptorStub(uniqueId, "failedTest")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); reportData.markFinished(testPlan.getTestIdentifier(uniqueId), failed(new NullPointerException())); @@ -190,7 +190,7 @@ void omitsMessageAttributeForFailedTestWithThrowableWithoutMessage() throws Exce void writesValidXmlEvenIfExceptionMessageContainsCData() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); engineDescriptor.addChild(new TestDescriptorStub(uniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var assertionError = new AssertionError(""); @@ -206,7 +206,7 @@ void writesValidXmlEvenIfExceptionMessageContainsCData() throws Exception { void escapesInvalidCharactersInSystemPropertiesAndExceptionMessages() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); engineDescriptor.addChild(new TestDescriptorStub(uniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); var assertionError = new AssertionError("expected: but was: "); @@ -241,7 +241,7 @@ void replacesIllegalCharacters(String input, String output) { void writesValidXmlForExceptionMessagesContainingLineBreaks() throws Exception { var uniqueId = engineDescriptor.getUniqueId().append("test", "test"); engineDescriptor.addChild(new TestDescriptorStub(uniqueId, "test")); - var testPlan = TestPlan.from(Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, Set.of(engineDescriptor), configParams, dummyOutputDirectoryProvider()); var allWhitespaceCharacters = IntStream.range(0, 0x10000) // .filter(Character::isWhitespace) // diff --git a/platform-tests/src/test/java/org/junit/platform/runner/JUnitPlatformRunnerTests.java b/platform-tests/src/test/java/org/junit/platform/runner/JUnitPlatformRunnerTests.java index 1a0f6190c21d..5dc8bb014f62 100644 --- a/platform-tests/src/test/java/org/junit/platform/runner/JUnitPlatformRunnerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/runner/JUnitPlatformRunnerTests.java @@ -139,7 +139,7 @@ class TestCase { var filters = request.getFiltersByType(ClassNameFilter.class); assertThat(filters).hasSize(1); - var filter = filters.get(0); + var filter = filters.getFirst(); // Excluded by default assertExcludes(filter, "example.MyClass"); @@ -185,7 +185,7 @@ class TestCase { var filters = request.getFiltersByType(PackageNameFilter.class); assertThat(filters).hasSize(1); - var filter = filters.get(0); + var filter = filters.getFirst(); assertIncludes(filter, "includedpackage1.TestClass"); assertIncludes(filter, "includedpackage2.TestClass"); assertExcludes(filter, "excludedpackage1.TestClass"); @@ -203,7 +203,7 @@ class TestCase { var filters = request.getFiltersByType(PackageNameFilter.class); assertThat(filters).hasSize(1); - var filter = filters.get(0); + var filter = filters.getFirst(); assertIncludes(filter, "includedpackage1.TestClass"); assertExcludes(filter, "excludedpackage1.TestClass"); assertExcludes(filter, "excludedpackage2.TestClass"); @@ -221,7 +221,7 @@ class TestCase { var filters = request.getPostDiscoveryFilters(); assertThat(filters).hasSize(1); - var filter = filters.get(0); + var filter = filters.getFirst(); assertIncludes(filter, testDescriptorWithTags("foo")); assertIncludes(filter, testDescriptorWithTags("bar")); assertExcludes(filter, testDescriptorWithTags("baz")); @@ -239,7 +239,7 @@ class TestCase { var filters = request.getPostDiscoveryFilters(); assertThat(filters).hasSize(1); - var filter = filters.get(0); + var filter = filters.getFirst(); assertExcludes(filter, testDescriptorWithTags("foo")); assertExcludes(filter, testDescriptorWithTags("bar")); assertIncludes(filter, testDescriptorWithTags("baz")); @@ -257,7 +257,7 @@ class TestCase { var filters = request.getPostDiscoveryFilters(); assertThat(filters).hasSize(1); - var filter = filters.get(0); + var filter = filters.getFirst(); assertIncludes(filter, testDescriptorWithTags("foo")); assertIncludes(filter, testDescriptorWithTags("foo", "any_other_tag")); assertExcludes(filter, testDescriptorWithTags("foo", "bar")); @@ -277,7 +277,7 @@ class TestCase { var filters = request.getPostDiscoveryFilters(); assertThat(filters).hasSize(1); - var filter = filters.get(0); + var filter = filters.getFirst(); assertExcludes(filter, testDescriptorWithTags("foo")); assertExcludes(filter, testDescriptorWithTags("foo", "any_other_tag")); assertIncludes(filter, testDescriptorWithTags("foo", "bar")); @@ -309,7 +309,7 @@ class TestCase { assertIncludes(includeFilter, bazEngine); assertExcludes(includeFilter, quuxEngine); - var excludeFilter = filters.get(0); + var excludeFilter = filters.getFirst(); assertIncludes(excludeFilter, fooEngine); assertExcludes(excludeFilter, barEngine); assertIncludes(excludeFilter, bazEngine); @@ -466,7 +466,7 @@ void convertsTestIdentifiersIntoDescriptions() { TestDescriptor container2 = new TestDescriptorStub(UniqueId.root("root", "container2"), "container2"); container2.addChild(new TestDescriptorStub(UniqueId.root("root", "test2a"), "test2a")); container2.addChild(new TestDescriptorStub(UniqueId.root("root", "test2b"), "test2b")); - var testPlan = TestPlan.from(List.of(container1, container2), mock(), dummyOutputDirectoryProvider()); + var testPlan = TestPlan.from(true, List.of(container1, container2), mock(), dummyOutputDirectoryProvider()); var launcher = mock(Launcher.class); when(launcher.discover(any())).thenReturn(testPlan); @@ -513,12 +513,13 @@ void appliesFilter() throws Exception { TestDescriptor originalParent2 = new TestDescriptorStub(UniqueId.root("root", "parent2"), "parent2"); originalParent2.addChild(new TestDescriptorStub(UniqueId.root("root", "leaf2a"), "leaf2a")); originalParent2.addChild(new TestDescriptorStub(UniqueId.root("root", "leaf2b"), "leaf2b")); - var fullTestPlan = TestPlan.from(List.of(originalParent1, originalParent2), configParams, + var fullTestPlan = TestPlan.from(true, List.of(originalParent1, originalParent2), configParams, dummyOutputDirectoryProvider()); TestDescriptor filteredParent = new TestDescriptorStub(UniqueId.root("root", "parent2"), "parent2"); filteredParent.addChild(new TestDescriptorStub(UniqueId.root("root", "leaf2b"), "leaf2b")); - var filteredTestPlan = TestPlan.from(Set.of(filteredParent), configParams, dummyOutputDirectoryProvider()); + var filteredTestPlan = TestPlan.from(true, Set.of(filteredParent), configParams, + dummyOutputDirectoryProvider()); var launcher = mock(Launcher.class); var captor = ArgumentCaptor.forClass(LauncherDiscoveryRequest.class); @@ -540,7 +541,7 @@ void appliesFilter() throws Exception { @Test void throwsNoTestsRemainExceptionWhenNoTestIdentifierMatchesFilter() { - var testPlan = TestPlan.from(Set.of(new TestDescriptorStub(UniqueId.root("root", "test"), "test")), + var testPlan = TestPlan.from(true, Set.of(new TestDescriptorStub(UniqueId.root("root", "test"), "test")), configParams, dummyOutputDirectoryProvider()); var launcher = mock(Launcher.class); @@ -696,7 +697,7 @@ void descriptionForJavaMethodAndClassSources() throws Exception { List children = platformRunner.getDescription().getChildren(); assertEquals(1, children.size()); - var engineDescription = children.get(0); + var engineDescription = children.getFirst(); assertEquals("dummy", engineDescription.getDisplayName()); var containerDescription = getOnlyElement(engineDescription.getChildren()); @@ -732,7 +733,7 @@ void descriptionForJavaMethodAndClassSourcesUsingTechnicalNames() throws Excepti List children = platformRunner.getDescription().getChildren(); assertEquals(1, children.size()); - var engineDescription = children.get(0); + var engineDescription = children.getFirst(); assertEquals("dummy", engineDescription.getDisplayName()); var containerDescription = getOnlyElement(engineDescription.getChildren()); @@ -780,7 +781,7 @@ private LauncherDiscoveryRequest instantiateRunnerAndCaptureGeneratedRequest(Cla var launcher = mock(Launcher.class); var captor = ArgumentCaptor.forClass(LauncherDiscoveryRequest.class); when(launcher.discover(captor.capture())).thenReturn( - TestPlan.from(Set.of(), mock(), dummyOutputDirectoryProvider())); + TestPlan.from(true, Set.of(), mock(), dummyOutputDirectoryProvider())); new JUnitPlatform(testClass, launcher); diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/BeforeAndAfterSuiteTests.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/BeforeAndAfterSuiteTests.java index d1032281d826..186cd71304f3 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/engine/BeforeAndAfterSuiteTests.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/BeforeAndAfterSuiteTests.java @@ -13,6 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Named.named; import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.suite.engine.SuiteEngineDescriptor.ENGINE_ID; import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.FailingAfterSuite; @@ -47,7 +48,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.suite.api.AfterSuite; import org.junit.platform.suite.api.BeforeSuite; import org.junit.platform.suite.engine.testcases.StatefulTestCase; @@ -192,14 +193,12 @@ void severalFailingBeforeAndAfterSuite() { @ParameterizedTest(name = "{0}") @MethodSource void invalidBeforeOrAfterSuiteMethod(Class testSuiteClass, Predicate failureMessagePredicate) { - // @formatter:off - executeSuite(testSuiteClass) - .allEvents() - .assertThatEvents() - .haveExactly(1, event( - container(testSuiteClass), - finishedWithFailure(instanceOf(JUnitException.class), message(failureMessagePredicate)))); - // @formatter:on + var results = engineWithSelectedSuite(testSuiteClass).discover(); + + var issue = getOnlyElement(results.getDiscoveryIssues()); + assertThat(issue.severity()).isEqualTo(Severity.ERROR); + assertThat(issue.message()).matches(failureMessagePredicate); + assertThat(issue.source()).containsInstanceOf(org.junit.platform.engine.support.descriptor.MethodSource.class); } private static Stream invalidBeforeOrAfterSuiteMethod() { @@ -219,11 +218,19 @@ private static Stream invalidBeforeOrAfterSuiteMethod() { private static Arguments invalidBeforeOrAfterSuiteCase(Class suiteClass, String failureMessageStart, String failureMessageEnd) { return arguments(named(suiteClass.getSimpleName(), suiteClass), - (Predicate) s -> s.startsWith(failureMessageStart) && s.endsWith(failureMessageEnd)); + expectedMessage(failureMessageStart, failureMessageEnd)); + } + + private static Predicate expectedMessage(String failureMessageStart, String failureMessageEnd) { + return message -> message.startsWith(failureMessageStart) && message.endsWith(failureMessageEnd); } private static EngineExecutionResults executeSuite(Class suiteClass) { - return EngineTestKit.engine(ENGINE_ID).selectors(selectClass(suiteClass)).execute(); + return engineWithSelectedSuite(suiteClass).execute(); + } + + private static EngineTestKit.Builder engineWithSelectedSuite(Class suiteClass) { + return EngineTestKit.engine(ENGINE_ID).selectors(selectClass(suiteClass)); } } 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 0cc23198b237..fac2f2cf97ea 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 @@ -13,6 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.mock; import java.util.Collections; import java.util.Optional; @@ -28,6 +29,7 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.reporting.OutputDirectoryProvider; +import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; import org.junit.platform.launcher.core.OutputDirectoryProviders; import org.junit.platform.suite.api.Suite; import org.junit.platform.suite.engine.testcases.SingleTestTestCase; @@ -48,8 +50,9 @@ class SuiteTestDescriptorTests { final ConfigurationParameters configurationParameters = new EmptyConfigurationParameters(); final OutputDirectoryProvider outputDirectoryProvider = OutputDirectoryProviders.dummyOutputDirectoryProvider(); + final DiscoveryIssueReporter discoveryIssueReporter = DiscoveryIssueReporter.create(mock(), engineId); final SuiteTestDescriptor suite = new SuiteTestDescriptor(suiteId, TestSuite.class, configurationParameters, - outputDirectoryProvider); + outputDirectoryProvider, discoveryIssueReporter); @Test void suiteIsEmptyBeforeDiscovery() { @@ -68,7 +71,7 @@ void suiteDiscoversTestsFromClass() { } @Test - void suitDiscoversTestsFromUniqueId() { + void suiteDiscoversTestsFromUniqueId() { suite.addDiscoveryRequestFrom(methodId); suite.discover(); diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/LifecycleMethodsSuites.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/LifecycleMethodsSuites.java index bfbd4619d35d..310ad8c1e85c 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/LifecycleMethodsSuites.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/LifecycleMethodsSuites.java @@ -29,6 +29,7 @@ * * @since 1.11 */ +@SuppressWarnings("NewClassNamingConvention") public class LifecycleMethodsSuites { @Retention(RetentionPolicy.RUNTIME) diff --git a/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineDiscoveryResultsIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineDiscoveryResultsIntegrationTests.java new file mode 100644 index 000000000000..29945442f388 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineDiscoveryResultsIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * 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.testkit.engine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.platform.engine.DiscoveryIssue; +import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.EngineDiscoveryRequest; +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.ClassSource; +import org.junit.platform.fakes.TestEngineStub; + +@ParameterizedClass +@EnumSource +record EngineDiscoveryResultsIntegrationTests(TestKitApi testKit) { + + @Test + void returnsEngineDescriptor() { + var results = testKit.discover("junit-jupiter", selectClass(TestCase.class)); + + assertThat(results.getEngineDescriptor().getDisplayName()).isEqualTo("JUnit Jupiter"); + assertThat(getOnlyElement(results.getEngineDescriptor().getChildren()).getSource()) // + .contains(ClassSource.from(TestCase.class)); + } + + @Test + void collectsDiscoveryIssues() { + var issue = DiscoveryIssue.create(Severity.WARNING, "warning"); + var testEngine = new TestEngineStub() { + @Override + public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { + var listener = discoveryRequest.getDiscoveryListener(); + listener.issueEncountered(uniqueId, issue); + return super.discover(discoveryRequest, uniqueId); + } + }; + + var results = testKit.discover(testEngine); + + assertThat(results.getDiscoveryIssues()).containsExactly(issue); + } + + @SuppressWarnings("JUnitMalformedDeclaration") + static class TestCase { + @Test + void test() { + } + } + + enum TestKitApi { + + STATIC_METHOD { + @Override + EngineDiscoveryResults discover(String engineId, DiscoverySelector selector) { + return EngineTestKit.discover(engineId, request().selectors(selector).build()); + } + + @Override + EngineDiscoveryResults discover(TestEngine testEngine) { + return EngineTestKit.discover(testEngine, request().build()); + } + }, + + FLUENT_API { + @Override + EngineDiscoveryResults discover(String engineId, DiscoverySelector selector) { + return EngineTestKit.engine(engineId).selectors(selector).discover(); + } + + @Override + EngineDiscoveryResults discover(TestEngine testEngine) { + return EngineTestKit.engine(testEngine).discover(); + } + }; + + @SuppressWarnings("SameParameterValue") + abstract EngineDiscoveryResults discover(String engineId, DiscoverySelector selector); + + abstract EngineDiscoveryResults discover(TestEngine testEngine); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/testkit/engine/EventsTests.java b/platform-tests/src/test/java/org/junit/platform/testkit/engine/EventsTests.java index e490d029a74f..2d519bede3b6 100644 --- a/platform-tests/src/test/java/org/junit/platform/testkit/engine/EventsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/testkit/engine/EventsTests.java @@ -146,7 +146,7 @@ void assertEventsMatchLooselyWithOneMatchingAndOneBadConditionFailsPartly() { var failures = error.getFailures(); assertEquals(1, failures.size()); - assertEquals(AssertionError.class, failures.get(0).getClass()); + assertEquals(AssertionError.class, failures.getFirst().getClass()); } @Test diff --git a/platform-tests/src/test/resources/log4j2-test.xml b/platform-tests/src/test/resources/log4j2-test.xml index 653e2017872e..544a157b1d8f 100644 --- a/platform-tests/src/test/resources/log4j2-test.xml +++ b/platform-tests/src/test/resources/log4j2-test.xml @@ -7,13 +7,16 @@ - + + + + + - + - 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 01826462be5d..1b68a34e8616 100644 --- a/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts +++ b/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts @@ -2,7 +2,6 @@ import com.gradle.develocity.agent.gradle.internal.test.TestDistributionConfigurationInternal import junitbuild.extensions.capitalized import org.gradle.api.tasks.PathSensitivity.RELATIVE -import org.gradle.jvm.toolchain.internal.NoToolchainAvailableException import org.gradle.kotlin.dsl.support.listFilesOrdered import java.time.Duration @@ -56,27 +55,6 @@ dependencies { because("it uses the OS enum to support Windows") } - testImplementation(libs.archunit) { - because("checking the architecture of JUnit 5") - } - testImplementation(libs.apiguardian) { - because("we validate that public classes are annotated") - } - testImplementation(libs.bndlib) { - because("parsing OSGi metadata") - } - testRuntimeOnly(libs.slf4j.julBinding) { - because("provide appropriate SLF4J binding") - } - testImplementation(libs.ant) { - because("we reference Ant's main class") - } - testImplementation(libs.bundles.xmlunit) - testImplementation(testFixtures(projects.junitJupiterApi)) - testImplementation(testFixtures(projects.junitPlatformReporting)) - testImplementation(libs.snapshotTests.junit5) - testImplementation(libs.snapshotTests.xml) - thirdPartyJars(libs.junit4) thirdPartyJars(libs.assertj) thirdPartyJars(libs.apiguardian) @@ -133,58 +111,125 @@ val normalizeMavenRepo by tasks.registering(Sync::class) { into(layout.buildDirectory.dir("normalized-repo")) } -tasks.test { - // Opt-out via system property: '-Dplatform.tooling.support.tests.enabled=false' - enabled = System.getProperty("platform.tooling.support.tests.enabled")?.toBoolean() ?: true +val archUnit by testing.suites.registering(JvmTestSuite::class) { + dependencies { + implementation(libs.archunit) { + because("checking the architecture of JUnit 5") + } + implementation(libs.apiguardian) { + because("we validate that public classes are annotated") + } + runtimeOnly.bundle(libs.bundles.log4j) + val modularProjects: List by rootProject + modularProjects.forEach { + runtimeOnly(project(it.path)) + } + } - // The following if-block is necessary since Gradle will otherwise - // always publish all mavenizedProjects even if this "test" task - // is not executed. - if (enabled) { - dependsOn(normalizeMavenRepo) - jvmArgumentProviders += MavenRepo(project, normalizeMavenRepo.map { it.destinationDir }) + targets { + all { + testTask.configure { + useJUnitPlatform() + (options as JUnitPlatformOptions).apply { + includeEngines("archunit") + excludeEngines("junit-jupiter") + } + develocity { + testRetry.maxRetries = 0 + testDistribution.enabled = false + predictiveTestSelection.enabled = false + } + } + } } - environment.remove("JAVA_TOOL_OPTIONS") +} + +tasks.named("checkstyle${archUnit.name.capitalized()}").configure { + config = resources.text.fromFile(checkstyle.configDirectory.file("checkstyleTest.xml")) +} - jvmArgumentProviders += JarPath(project, thirdPartyJarsClasspath.get(), "thirdPartyJars") - jvmArgumentProviders += JarPath(project, antJarsClasspath.get(), "antJars") - jvmArgumentProviders += MavenDistribution(project, unzipMavenDistribution, mavenDistributionDir) +tasks.check { + dependsOn(archUnit) +} - if (buildParameters.javaToolchain.version.getOrElse(21) < 24) { - (options as JUnitPlatformOptions).apply { - includeEngines("archunit") +val test by testing.suites.getting(JvmTestSuite::class) { + dependencies { + implementation(libs.bndlib) { + because("parsing OSGi metadata") + } + runtimeOnly(libs.slf4j.julBinding) { + because("provide appropriate SLF4J binding") } + implementation(libs.ant) { + because("we reference Ant's main class") + } + implementation.bundle(libs.bundles.xmlunit) + implementation(testFixtures(projects.junitJupiterApi)) + implementation(testFixtures(projects.junitPlatformReporting)) + implementation(libs.snapshotTests.junit5) + implementation(libs.snapshotTests.xml) + } - inputs.apply { - dir("projects").withPathSensitivity(RELATIVE) - file("${rootDir}/gradle.properties").withPathSensitivity(RELATIVE) - file("${rootDir}/settings.gradle.kts").withPathSensitivity(RELATIVE) - file("${rootDir}/gradlew").withPathSensitivity(RELATIVE) - file("${rootDir}/gradlew.bat").withPathSensitivity(RELATIVE) - dir("${rootDir}/gradle/wrapper").withPathSensitivity(RELATIVE) - dir("${rootDir}/documentation/src/main").withPathSensitivity(RELATIVE) - dir("${rootDir}/documentation/src/test").withPathSensitivity(RELATIVE) - } - - // Disable capturing output since parallel execution is enabled and output of - // external processes happens on non-test threads which can't reliably be - // attributed to the test that started the process. - systemProperty("junit.platform.output.capture.stdout", "false") - systemProperty("junit.platform.output.capture.stderr", "false") - - develocity { - testDistribution { - requirements.add("jdk=8") - this as TestDistributionConfigurationInternal - preferredMaxDuration = Duration.ofMillis(500) + targets { + all { + testTask.configure { + shouldRunAfter(archUnit) + + // Opt-out via system property: '-Dplatform.tooling.support.tests.enabled=false' + enabled = System.getProperty("platform.tooling.support.tests.enabled")?.toBoolean() ?: true + + // The following if-block is necessary since Gradle will otherwise + // always publish all mavenizedProjects even if this "test" task + // is not executed. + if (enabled) { + dependsOn(normalizeMavenRepo) + jvmArgumentProviders += MavenRepo(project, normalizeMavenRepo.map { it.destinationDir }) + } + environment.remove("JAVA_TOOL_OPTIONS") + + jvmArgumentProviders += JarPath(project, thirdPartyJarsClasspath.get(), "thirdPartyJars") + 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) + file("${rootDir}/settings.gradle.kts").withPathSensitivity(RELATIVE) + file("${rootDir}/gradlew").withPathSensitivity(RELATIVE) + file("${rootDir}/gradlew.bat").withPathSensitivity(RELATIVE) + dir("${rootDir}/gradle/wrapper").withPathSensitivity(RELATIVE) + dir("${rootDir}/documentation/src/main").withPathSensitivity(RELATIVE) + dir("${rootDir}/documentation/src/test").withPathSensitivity(RELATIVE) + } + + // Disable capturing output since parallel execution is enabled and output of + // external processes happens on non-test threads which can't reliably be + // attributed to the test that started the process. + systemProperty("junit.platform.output.capture.stdout", "false") + systemProperty("junit.platform.output.capture.stderr", "false") + + develocity { + testDistribution { + requirements.add("jdk=8") + this as TestDistributionConfigurationInternal + preferredMaxDuration = Duration.ofMillis(500) + } + } + jvmArgumentProviders += JavaHomeDir(project, 8, develocity.testDistribution.enabled) + + val gradleJavaVersion = JavaVersion.current().majorVersion.toInt() + jvmArgumentProviders += JavaHomeDir(project, gradleJavaVersion, develocity.testDistribution.enabled) + systemProperty("gradle.java.version", gradleJavaVersion) + } } } - jvmArgumentProviders += JavaHomeDir(project, 8, develocity.testDistribution.enabled) - - val gradleJavaVersion = JavaVersion.current().majorVersion.toInt() - jvmArgumentProviders += JavaHomeDir(project, gradleJavaVersion, develocity.testDistribution.enabled) - systemProperty("gradle.java.version", gradleJavaVersion) } class MavenRepo(project: Project, @get:Internal val repoDir: Provider) : CommandLineArgumentProvider { @@ -216,7 +261,7 @@ class JavaHomeDir(project: Project, @Input val version: Int, testDistributionEna project.javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(version) }.get() - } catch (e: NoToolchainAvailableException) { + } catch (e: Exception) { null } }) 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 c6d35e3a0e51..3214a579e059 100644 --- a/platform-tooling-support-tests/projects/graalvm-starter/settings.gradle.kts +++ b/platform-tooling-support-tests/projects/graalvm-starter/settings.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { plugins { - id("org.graalvm.buildtools.native") version "0.10.5" + id("org.graalvm.buildtools.native") version "0.10.6" } repositories { mavenCentral() diff --git a/platform-tooling-support-tests/projects/gradle-kotlin-extensions/build.gradle.kts b/platform-tooling-support-tests/projects/gradle-kotlin-extensions/build.gradle.kts index e813a58e9dc9..0fef24865e1f 100644 --- a/platform-tooling-support-tests/projects/gradle-kotlin-extensions/build.gradle.kts +++ b/platform-tooling-support-tests/projects/gradle-kotlin-extensions/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "2.1.10" + kotlin("jvm") version "2.1.20" } repositories { diff --git a/platform-tooling-support-tests/projects/java-versions/pom.xml b/platform-tooling-support-tests/projects/java-versions/pom.xml index d9d22bbec6b6..cdbb8cf6aaf0 100644 --- a/platform-tooling-support-tests/projects/java-versions/pom.xml +++ b/platform-tooling-support-tests/projects/java-versions/pom.xml @@ -31,7 +31,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 1.8 1.8 diff --git a/platform-tooling-support-tests/projects/jupiter-starter/pom.xml b/platform-tooling-support-tests/projects/jupiter-starter/pom.xml index d03ec1ff3367..a0b01281e086 100644 --- a/platform-tooling-support-tests/projects/jupiter-starter/pom.xml +++ b/platform-tooling-support-tests/projects/jupiter-starter/pom.xml @@ -49,7 +49,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 maven-surefire-plugin diff --git a/platform-tooling-support-tests/projects/jupiter-starter/src/test/java/com/example/project/CalculatorParameterizedClassTests.java b/platform-tooling-support-tests/projects/jupiter-starter/src/test/java/com/example/project/CalculatorParameterizedClassTests.java new file mode 100644 index 000000000000..b726db1d9c24 --- /dev/null +++ b/platform-tooling-support-tests/projects/jupiter-starter/src/test/java/com/example/project/CalculatorParameterizedClassTests.java @@ -0,0 +1,40 @@ +/* + * 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.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; + + @Test + void regularTest() { + Calculator calculator = new Calculator(); + assertEquals(2 * i, calculator.add(i, i), () -> i + " + " + i + " should equal 2 * " + i); + } + + @ParameterizedTest + @ValueSource(ints = { 1, 2 }) + void parameterizedTest(int j) { + Calculator calculator = new Calculator(); + assertEquals(i + j, calculator.add(i, j)); + } +} diff --git a/platform-tooling-support-tests/projects/jupiter-starter/src/test/resources/junit-platform.properties b/platform-tooling-support-tests/projects/jupiter-starter/src/test/resources/junit-platform.properties new file mode 100644 index 000000000000..daf7418ffd20 --- /dev/null +++ b/platform-tooling-support-tests/projects/jupiter-starter/src/test/resources/junit-platform.properties @@ -0,0 +1,2 @@ +junit.jupiter.testclass.order.default = \ + org.junit.jupiter.api.ClassOrderer$ClassName diff --git a/platform-tooling-support-tests/projects/maven-surefire-compatibility/pom.xml b/platform-tooling-support-tests/projects/maven-surefire-compatibility/pom.xml index 3ac0876b2d8b..47aba716ed6a 100644 --- a/platform-tooling-support-tests/projects/maven-surefire-compatibility/pom.xml +++ b/platform-tooling-support-tests/projects/maven-surefire-compatibility/pom.xml @@ -37,7 +37,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 maven-surefire-plugin diff --git a/platform-tooling-support-tests/projects/multi-release-jar/pom.xml b/platform-tooling-support-tests/projects/multi-release-jar/pom.xml index 9c96f86949bf..b9e4b86ed82f 100644 --- a/platform-tooling-support-tests/projects/multi-release-jar/pom.xml +++ b/platform-tooling-support-tests/projects/multi-release-jar/pom.xml @@ -31,7 +31,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 11 diff --git a/platform-tooling-support-tests/projects/vintage/pom.xml b/platform-tooling-support-tests/projects/vintage/pom.xml index 164570994740..3553f799ee3a 100644 --- a/platform-tooling-support-tests/projects/vintage/pom.xml +++ b/platform-tooling-support-tests/projects/vintage/pom.xml @@ -38,7 +38,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 maven-surefire-plugin diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java b/platform-tooling-support-tests/src/archUnit/java/platform/tooling/support/tests/ArchUnitTests.java similarity index 87% rename from platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java rename to platform-tooling-support-tests/src/archUnit/java/platform/tooling/support/tests/ArchUnitTests.java index 635ea9b5f630..71f04fba2256 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java +++ b/platform-tooling-support-tests/src/archUnit/java/platform/tooling/support/tests/ArchUnitTests.java @@ -26,38 +26,27 @@ import static com.tngtech.archunit.lang.conditions.ArchPredicates.have; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; -import static java.util.stream.Collectors.toSet; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; -import java.io.UncheckedIOException; import java.lang.annotation.Annotation; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.Arrays; -import java.util.Set; import java.util.function.BiPredicate; -import java.util.jar.JarFile; -import java.util.stream.Stream; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.importer.Location; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; -import com.tngtech.archunit.junit.LocationProvider; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.library.GeneralCodingRules; import org.apiguardian.api.API; -import platform.tooling.support.Helper; -import platform.tooling.support.MavenRepo; - -@AnalyzeClasses(locations = ArchUnitTests.AllJars.class) +@AnalyzeClasses(packages = { "org.junit.platform", "org.junit.jupiter", "org.junit.vintage" }) class ArchUnitTests { @SuppressWarnings("unused") @@ -115,6 +104,7 @@ void avoidAccessingStandardStreams(JavaClasses classes) { // ConsoleLauncher, StreamInterceptor, Picocli et al... var subset = classes // .that(are(not(name("org.junit.platform.console.ConsoleLauncher")))) // + .that(are(not(name("org.junit.platform.console.tasks.ConsoleTestExecutor")))) // .that(are(not(name("org.junit.platform.launcher.core.StreamInterceptor")))) // .that(are(not(name("org.junit.platform.runner.JUnitPlatformRunnerListener")))) // .that(are(not(name("org.junit.platform.testkit.engine.Events")))) // @@ -135,28 +125,6 @@ private static ArchCondition haveContainerAnnotationWithSameT (expectedTarget, actualTarget) -> Arrays.equals(expectedTarget.value(), actualTarget.value()))); } - static class AllJars implements LocationProvider { - - @Override - public Set get(Class testClass) { - return loadJarFiles().map(Location::of).collect(toSet()); - } - - private static Stream loadJarFiles() { - return Helper.loadModuleDirectoryNames().stream().map(AllJars::createJarFile); - } - - private static JarFile createJarFile(String module) { - var path = MavenRepo.jar(module); - try { - return new JarFile(path.toFile()); - } - catch (IOException e) { - throw new UncheckedIOException("Creating JarFile for '" + path + "' failed.", e); - } - } - } - private static class RepeatableAnnotationPredicate extends DescribedPredicate { private final Class annotationType; @@ -180,4 +148,5 @@ public boolean test(JavaClass annotationClass) { .orElse(actualAnnotation.isEmpty()); } } + } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java index 171548801968..95bcfa28a4ae 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java @@ -33,6 +33,7 @@ * @since 1.3 */ @EnableSnapshotTests +//@SnapshotTestOptions(alwaysPersistActualResult = true) class AntStarterTests { @Test @@ -51,13 +52,15 @@ void ant_starter(@TempDir Path workspace, @FilePrefix("ant") OutputFiles outputF assertLinesMatch(List.of(">> HEAD >>", // "test.junit.launcher:", // ">>>>", // + "\\[junitlauncher\\] Tests run: 6, Failures: 0, Aborted: 0, Skipped: 0, Time elapsed: .+ sec", // + "\\[junitlauncher\\] Running com.example.project.CalculatorTests", // "\\[junitlauncher\\] Tests run: 5, Failures: 0, Aborted: 0, Skipped: 0, Time elapsed: .+ sec", // ">>>>", // "test.console.launcher:", // ">>>>", // " \\[java\\] Test run finished after [\\d]+ ms", // ">>>>", // - " \\[java\\] \\[ 5 tests successful \\]", // + " \\[java\\] \\[ 11 tests successful \\]", // " \\[java\\] \\[ 0 tests failed \\]", // ">> TAIL >>"), // result.stdOutLines()); 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 b25ca6589027..e60e667a358f 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 @@ -21,9 +21,11 @@ import de.skuzzle.test.snapshots.Snapshot; import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.platform.tests.process.OutputFiles; +import org.junit.platform.tests.process.ProcessResult; import org.opentest4j.TestAbortedException; import platform.tooling.support.Helper; @@ -34,25 +36,74 @@ * @since 1.3 */ @EnableSnapshotTests +//@SnapshotTestOptions(alwaysPersistActualResult = true) class GradleStarterTests { + @TempDir + Path workspace; + + @BeforeEach + void prepareWorkspace() throws Exception { + copyToWorkspace(Projects.JUPITER_STARTER, workspace); + } + @Test - void gradle_wrapper(@TempDir Path workspace, @FilePrefix("gradle") OutputFiles outputFiles, Snapshot snapshot) - throws Exception { + void buildJupiterStarterProject(@FilePrefix("gradle") OutputFiles outputFiles, Snapshot snapshot) throws Exception { + + var result = runGradle(outputFiles, "build"); + + assertThat(result.stdOut()) // + .contains( // + "CalculatorParameterizedClassTests > [1] i=1 > regularTest() PASSED", // + "CalculatorParameterizedClassTests > [2] i=2 > regularTest() PASSED", // + "CalculatorParameterizedClassTests > [1] i=1 > parameterizedTest(int)", // + "CalculatorParameterizedClassTests > [2] i=2 > parameterizedTest(int)", // + "Using Java version: 1.8", // + "CalculatorTests > 1 + 1 = 2 PASSED", // + "CalculatorTests > add(int, int, int) > 0 + 1 = 1 PASSED", // + "CalculatorTests > add(int, int, int) > 1 + 2 = 3 PASSED", // + "CalculatorTests > add(int, int, int) > 49 + 51 = 100 PASSED", // + "CalculatorTests > add(int, int, int) > 1 + 100 = 101 PASSED" // + ); + var testResultsDir = workspace.resolve("build/test-results/test"); + verifyContainsExpectedStartedOpenTestReport(testResultsDir, snapshot); + } + + @Test + void runOnlyOneMethodInClassTemplate(@FilePrefix("gradle") OutputFiles outputFiles) throws Exception { + + var result = runGradle(outputFiles, "test", "--tests", "CalculatorParameterized*.regular*"); + + assertThat(result.stdOut()) // + .contains( // + "CalculatorParameterizedClassTests > [1] i=1 > regularTest() PASSED", // + "CalculatorParameterizedClassTests > [2] i=2 > regularTest() PASSED" // + ) // + .doesNotContain("parameterizedTest(int)", "CalculatorTests"); + + result = runGradle(outputFiles, "test", "--tests", "*ParameterizedClassTests.parameterized*"); + + assertThat(result.stdOut()) // + .contains( // + "CalculatorParameterizedClassTests > [1] i=1 > parameterizedTest(int)", // + "CalculatorParameterizedClassTests > [2] i=2 > parameterizedTest(int)" // + ) // + .doesNotContain("regularTest()", "CalculatorTests"); + } + + private ProcessResult runGradle(OutputFiles outputFiles, String... extraArgs) throws InterruptedException { var result = ProcessStarters.gradlew() // - .workingDir(copyToWorkspace(Projects.JUPITER_STARTER, workspace)) // + .workingDir(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()) // + .addArguments("--stacktrace", "--no-build-cache", "--warning-mode=fail") // + .addArguments(extraArgs).putEnvironment("JDK8", + Helper.getJavaHome("8").orElseThrow(TestAbortedException::new).toString()) // .redirectOutput(outputFiles) // .startAndWait(); assertEquals(0, result.exitCode()); assertTrue(result.stdOut().lines().anyMatch(line -> line.contains("BUILD SUCCESSFUL"))); - assertThat(result.stdOut()).contains("Using Java version: 1.8"); - - var testResultsDir = workspace.resolve("build/test-results/test"); - verifyContainsExpectedStartedOpenTestReport(testResultsDir, snapshot); + return result; } } 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 11ef756bd352..0cf4eee24483 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 @@ -12,7 +12,6 @@ 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 platform.tooling.support.tests.Projects.copyToWorkspace; import static platform.tooling.support.tests.XmlAssertions.verifyContainsExpectedStartedOpenTestReport; @@ -21,9 +20,11 @@ import de.skuzzle.test.snapshots.Snapshot; import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.platform.tests.process.OutputFiles; +import org.junit.platform.tests.process.ProcessResult; import org.opentest4j.TestAbortedException; import platform.tooling.support.Helper; @@ -34,6 +35,7 @@ * @since 1.3 */ @EnableSnapshotTests +//@SnapshotTestOptions(alwaysPersistActualResult = true) class MavenStarterTests { @ManagedResource @@ -42,25 +44,54 @@ class MavenStarterTests { @ManagedResource MavenRepoProxy mavenRepoProxy; + @TempDir + Path workspace; + + @BeforeEach + void prepareWorkspace() throws Exception { + copyToWorkspace(Projects.JUPITER_STARTER, workspace); + } + @Test - void verifyJupiterStarterProject(@TempDir Path workspace, @FilePrefix("maven") OutputFiles outputFiles, - Snapshot snapshot) throws Exception { + void verifyJupiterStarterProject(@FilePrefix("maven") OutputFiles outputFiles, Snapshot snapshot) throws Exception { + + var result = runMaven(outputFiles, "verify"); + + assertThat(result.stdOutLines()).contains("[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0"); + assertThat(result.stdOut()).contains("Using Java version: 1.8"); + var testResultsDir = workspace.resolve("target/surefire-reports"); + verifyContainsExpectedStartedOpenTestReport(testResultsDir, snapshot); + } + + @Test + void runOnlyOneMethodInClassTemplate(@FilePrefix("maven") OutputFiles outputFiles) throws Exception { + + var result = runMaven(outputFiles, "test", "-Dtest=CalculatorParameterizedClassTests#regularTest"); + + assertThat(result.stdOutLines()) // + .doesNotContain("CalculatorTests") // + .contains("[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0"); + + result = runMaven(outputFiles, "test", "-Dtest=CalculatorParameterizedClassTests#parameterizedTest"); + + assertThat(result.stdOutLines()) // + .doesNotContain("CalculatorTests") // + .contains("[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0"); + } + + private ProcessResult runMaven(OutputFiles outputFiles, String... extraArgs) throws InterruptedException { var result = ProcessStarters.maven(Helper.getJavaHome("8").orElseThrow(TestAbortedException::new)) // - .workingDir(copyToWorkspace(Projects.JUPITER_STARTER, workspace)) // + .workingDir(workspace) // .addArguments(localMavenRepo.toCliArgument(), "-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("-Dsnapshot.repo.url=" + mavenRepoProxy.getBaseUri()) // - .addArguments("--update-snapshots", "--batch-mode", "verify") // - .redirectOutput(outputFiles) // + .addArguments("--update-snapshots", "--batch-mode") // + .addArguments(extraArgs).redirectOutput(outputFiles) // .startAndWait(); assertEquals(0, result.exitCode()); assertEquals("", result.stdErr()); - assertTrue(result.stdOutLines().contains("[INFO] BUILD SUCCESS")); - assertTrue(result.stdOutLines().contains("[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0")); - assertThat(result.stdOut()).contains("Using Java version: 1.8"); - - var testResultsDir = workspace.resolve("target/surefire-reports"); - verifyContainsExpectedStartedOpenTestReport(testResultsDir, snapshot); + assertThat(result.stdOutLines()).contains("[INFO] BUILD SUCCESS"); + return result; } } 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 80028afebcd4..6ce670bc5ecf 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 @@ -12,7 +12,6 @@ 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.jupiter.api.parallel.ExecutionMode.SAME_THREAD; import static platform.tooling.support.ProcessStarters.currentJdkHome; import static platform.tooling.support.tests.Projects.copyToWorkspace; @@ -60,7 +59,7 @@ void verifyErrorMessageForUnalignedClasspath(JRE jre, Path javaHome, @TempDir Pa assertEquals(1, result.exitCode()); assertEquals("", result.stdErr()); - assertTrue(result.stdOutLines().contains("[INFO] BUILD FAILURE")); + assertThat(result.stdOutLines()).contains("[INFO] BUILD FAILURE"); assertThat(result.stdOut()) // .contains("The wrapped NoClassDefFoundError is likely caused by the versions of JUnit jars " + "on the classpath/module path not being properly aligned"); diff --git a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/AntStarterTests_snapshots/open-test-report.xml.snapshot b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/AntStarterTests_snapshots/open-test-report.xml.snapshot index a947aa10f920..1c453c46976d 100644 --- a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/AntStarterTests_snapshots/open-test-report.xml.snapshot +++ b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/AntStarterTests_snapshots/open-test-report.xml.snapshot @@ -16,12 +16,12 @@ test-method: ant_starter obfuscated Linux 16 - 21.0.5 + 21.0.6 UTF-8 - + [engine:junit-jupiter] JUnit Jupiter @@ -29,7 +29,172 @@ test-method: ant_starter - + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests] + com.example.project.CalculatorParameterizedClassTests + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1] + com.example.project.CalculatorParameterizedClassTests[1] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2] + com.example.project.CalculatorParameterizedClassTests[2] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests] com.example.project.CalculatorTests @@ -40,7 +205,7 @@ test-method: ant_starter - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[method:addsTwoNumbers()] addsTwoNumbers() @@ -51,11 +216,11 @@ test-method: ant_starter - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)] add(int, int, int) @@ -66,7 +231,7 @@ test-method: ant_starter - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#1] add(int, int, int)[1] @@ -77,11 +242,11 @@ test-method: ant_starter - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#2] add(int, int, int)[2] @@ -92,11 +257,11 @@ test-method: ant_starter - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#3] add(int, int, int)[3] @@ -107,11 +272,11 @@ test-method: ant_starter - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#4] add(int, int, int)[4] @@ -122,19 +287,19 @@ test-method: ant_starter - + - + - + - + diff --git a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/GradleStarterTests_snapshots/open-test-report.xml.snapshot b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/GradleStarterTests_snapshots/open-test-report.xml.snapshot index 3be1b286c9bb..563fce54c260 100644 --- a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/GradleStarterTests_snapshots/open-test-report.xml.snapshot +++ b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/GradleStarterTests_snapshots/open-test-report.xml.snapshot @@ -2,7 +2,7 @@ dynamic-directory: false snapshot-name: open-test-report.xml snapshot-number: 0 test-class: platform.tooling.support.tests.GradleStarterTests -test-method: gradle_wrapper +test-method: buildJupiterStarterProject obfuscated Linux 16 - 1.8.0_422 + 1.8.0_442 UTF-8 - + [engine:junit-jupiter] JUnit Jupiter @@ -29,7 +29,172 @@ test-method: gradle_wrapper - + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests] + com.example.project.CalculatorParameterizedClassTests + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1] + com.example.project.CalculatorParameterizedClassTests[1] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2] + com.example.project.CalculatorParameterizedClassTests[2] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests] com.example.project.CalculatorTests @@ -40,7 +205,7 @@ test-method: gradle_wrapper - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[method:addsTwoNumbers()] addsTwoNumbers() @@ -51,11 +216,11 @@ test-method: gradle_wrapper - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)] add(int, int, int) @@ -66,7 +231,7 @@ test-method: gradle_wrapper - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#1] add(int, int, int)[1] @@ -77,11 +242,11 @@ test-method: gradle_wrapper - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#2] add(int, int, int)[2] @@ -92,11 +257,11 @@ test-method: gradle_wrapper - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#3] add(int, int, int)[3] @@ -107,11 +272,11 @@ test-method: gradle_wrapper - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#4] add(int, int, int)[4] @@ -122,19 +287,19 @@ test-method: gradle_wrapper - + - + - + - + diff --git a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/MavenStarterTests_snapshots/open-test-report.xml.snapshot b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/MavenStarterTests_snapshots/open-test-report.xml.snapshot index 385bb5aff6a5..c6641484760a 100644 --- a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/MavenStarterTests_snapshots/open-test-report.xml.snapshot +++ b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/MavenStarterTests_snapshots/open-test-report.xml.snapshot @@ -16,12 +16,12 @@ test-method: verifyJupiterStarterProject obfuscated Linux 16 - 1.8.0_422 + 1.8.0_442 UTF-8 - + [engine:junit-jupiter] JUnit Jupiter @@ -29,7 +29,172 @@ test-method: verifyJupiterStarterProject - + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests] + com.example.project.CalculatorParameterizedClassTests + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1] + com.example.project.CalculatorParameterizedClassTests[1] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2] + com.example.project.CalculatorParameterizedClassTests[2] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[class-template:com.example.project.CalculatorParameterizedClassTests]/[class-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests] com.example.project.CalculatorTests @@ -40,7 +205,7 @@ test-method: verifyJupiterStarterProject - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[method:addsTwoNumbers()] addsTwoNumbers() @@ -51,11 +216,11 @@ test-method: verifyJupiterStarterProject - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)] add(int, int, int) @@ -66,7 +231,7 @@ test-method: verifyJupiterStarterProject - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#1] add(int, int, int)[1] @@ -77,11 +242,11 @@ test-method: verifyJupiterStarterProject - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#2] add(int, int, int)[2] @@ -92,11 +257,11 @@ test-method: verifyJupiterStarterProject - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#3] add(int, int, int)[3] @@ -107,11 +272,11 @@ test-method: verifyJupiterStarterProject - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#4] add(int, int, int)[4] @@ -122,19 +287,19 @@ test-method: verifyJupiterStarterProject - + - + - + - +