diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index f1ce8b0c8472..000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,23 +0,0 @@ -## Overview - -_Replace the following bullet points with your issue description, -after answering yourself: **"What kind of issue is this?"**_ - -- ( ) **Question.** This issue tracker is not the place for questions. -If you want to ask how to do something, or to understand why -something isn't working the way you expect it to, please first use Stack -Overflow or Gitter. -https://stackoverflow.com/questions/tagged/junit5 - -- ( ) **Bug report.** Please provide us the version of JUnit 5 you are -using and, if possible, a failing unit test with your bug report. Don't -forget to describe the rationale for this issue (e.g. expected vs. -actual behavior). - -- ( ) **Feature request.** Start by telling us what problem you’re trying -to solve. Often a solution already exists! Please, don’t send pull requests -to implement new features without first getting our support. - -## Deliverables - -- [ ] ... diff --git a/.github/actions/main-build/action.yml b/.github/actions/main-build/action.yml index 4c6c611412d7..b9d087000352 100644 --- a/.github/actions/main-build/action.yml +++ b/.github/actions/main-build/action.yml @@ -5,6 +5,9 @@ inputs: required: true description: Gradle arguments default: :platform-tooling-support-tests:test build --configuration-cache + encryptionKey: + required: true + description: Gradle cache encryption key runs: using: "composite" steps: @@ -12,3 +15,4 @@ runs: - uses: ./.github/actions/run-gradle with: arguments: ${{ inputs.arguments }} + encryptionKey: ${{ inputs.encryptionKey }} diff --git a/.github/actions/run-gradle/action.yml b/.github/actions/run-gradle/action.yml index e825551ce6f5..5b4ab3d092ec 100644 --- a/.github/actions/run-gradle/action.yml +++ b/.github/actions/run-gradle/action.yml @@ -5,6 +5,9 @@ inputs: required: true description: Gradle arguments default: build + encryptionKey: + required: true + description: Gradle cache encryption key runs: using: "composite" steps: @@ -14,7 +17,9 @@ runs: distribution: temurin java-version: 21 check-latest: true - - uses: gradle/actions/setup-gradle@16bf8bc8fe830fa669c3c9f914d3eb147c629707 # v4 + - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4 + with: + cache-encryption-key: ${{ inputs.encryptionKey }} - shell: bash env: JAVA_HOME: ${{ steps.setup-gradle-jdk.outputs.path }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 133179e59c25..4507189142e1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,6 +39,7 @@ jobs: - name: Build uses: ./.github/actions/run-gradle with: + encryptionKey: ${{ secrets.GRADLE_ENCRYPTION_KEY }} arguments: | --no-build-cache \ -Dscan.tag.CodeQL \ diff --git a/.github/workflows/cross-version.yml b/.github/workflows/cross-version.yml index 9df3eeea19d7..789f7aa9a088 100644 --- a/.github/workflows/cross-version.yml +++ b/.github/workflows/cross-version.yml @@ -29,6 +29,8 @@ jobs: - version: 24 type: ea release: leyden + - version: 25 + type: ea name: "OpenJDK ${{ matrix.jdk.version }} (${{ matrix.jdk.release || matrix.jdk.type }})" runs-on: ubuntu-latest steps: @@ -58,6 +60,7 @@ jobs: - name: Build uses: ./.github/actions/run-gradle with: + encryptionKey: ${{ secrets.GRADLE_ENCRYPTION_KEY }} arguments: | -PjavaToolchain.version=${{ matrix.jdk.version }} \ -Dscan.tag.JDK_${{ matrix.jdk.version }} \ @@ -91,6 +94,7 @@ jobs: - name: Build uses: ./.github/actions/run-gradle with: + encryptionKey: ${{ secrets.GRADLE_ENCRYPTION_KEY }} arguments: | -PjavaToolchain.version=${{ matrix.jdk }} \ -PjavaToolchain.implementation=j9 \ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9297eb720a2..357ed0d1cf21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,7 @@ jobs: - name: Build uses: ./.github/actions/main-build with: + encryptionKey: ${{ secrets.GRADLE_ENCRYPTION_KEY }} arguments: | -Ptesting.enableJaCoCo \ :platform-tooling-support-tests:test \ @@ -50,6 +51,8 @@ jobs: fetch-depth: 1 - name: Build uses: ./.github/actions/main-build + with: + encryptionKey: ${{ secrets.GRADLE_ENCRYPTION_KEY }} macOS: runs-on: macos-latest @@ -60,10 +63,12 @@ jobs: fetch-depth: 1 - name: Build uses: ./.github/actions/main-build + with: + encryptionKey: ${{ secrets.GRADLE_ENCRYPTION_KEY }} publish_artifacts: name: Publish Snapshot Artifacts - needs: linux + needs: Linux runs-on: ubuntu-latest permissions: attestations: write # required for build provenance attestation @@ -80,6 +85,7 @@ jobs: ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USERNAME }} ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }} with: + encryptionKey: ${{ secrets.GRADLE_ENCRYPTION_KEY }} arguments: | publish -x check \ prepareGitHubAttestation @@ -107,15 +113,23 @@ jobs: - name: Build Documentation uses: ./.github/actions/run-gradle with: + encryptionKey: ${{ secrets.GRADLE_ENCRYPTION_KEY }} arguments: | prepareDocsForUploadToGhPages \ -Dscan.tag.Documentation + - name: Configure Git + shell: bash + run: | + git config --global user.name "JUnit Team" + git config --global user.email "team@junit.org" - name: Upload Documentation if: github.event_name == 'push' && github.repository == 'junit-team/junit5' && github.ref == 'refs/heads/main' uses: ./.github/actions/run-gradle with: + encryptionKey: ${{ secrets.GRADLE_ENCRYPTION_KEY }} arguments: | gitPublishPush \ -Dscan.tag.Documentation env: - GRGIT_USER: ${{ secrets.GH_TOKEN }} + GIT_USERNAME: git + GIT_PASSWORD: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/reproducible-build.yml b/.github/workflows/reproducible-build.yml index 39eb9160dbdc..37d6eee1b526 100644 --- a/.github/workflows/reproducible-build.yml +++ b/.github/workflows/reproducible-build.yml @@ -24,6 +24,7 @@ jobs: - name: Restore Gradle cache and display toolchains uses: ./.github/actions/run-gradle with: + encryptionKey: ${{ secrets.GRADLE_ENCRYPTION_KEY }} arguments: | --quiet \ --configuration-cache diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index e6094d409c45..9f10a217b5b9 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -9,10 +9,10 @@ - - + + diff --git a/README.md b/README.md index c01e40b25947..4ba73add4258 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This repository is the home of _JUnit 5_. ## Latest Releases -- General Availability (GA): [JUnit 5.11.2](https://github.com/junit-team/junit5/releases/tag/r5.11.2) (October 4, 2024) +- 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): N/A ## Documentation diff --git a/documentation/documentation.gradle.kts b/documentation/documentation.gradle.kts index f5f6e1b6391a..d092724e92d8 100644 --- a/documentation/documentation.gradle.kts +++ b/documentation/documentation.gradle.kts @@ -104,10 +104,15 @@ val apiGuardianDocVersion = if (libs.versions.apiguardian.get().contains("SNAPSH gitPublish { repoUri = "https://github.com/junit-team/junit5.git" + referenceRepoUri = rootDir.toURI().toString() + branch = "gh-pages" sign = false fetchDepth = 1 + username = providers.environmentVariable("GIT_USERNAME") + password = providers.environmentVariable("GIT_PASSWORD") + contents { from(docsDir) into("docs") diff --git a/documentation/src/docs/asciidoc/release-notes/index.adoc b/documentation/src/docs/asciidoc/release-notes/index.adoc index 8ed0f41bf6f1..814ac7aab543 100644 --- a/documentation/src/docs/asciidoc/release-notes/index.adoc +++ b/documentation/src/docs/asciidoc/release-notes/index.adoc @@ -17,6 +17,10 @@ authors as well as build tool and IDE vendors. include::{includedir}/link-attributes.adoc[] +include::{basedir}/release-notes-5.11.4.adoc[] + +include::{basedir}/release-notes-5.11.3.adoc[] + include::{basedir}/release-notes-5.11.2.adoc[] include::{basedir}/release-notes-5.11.1.adoc[] 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 new file mode 100644 index 000000000000..0588af0f1cc7 --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.3.adoc @@ -0,0 +1,40 @@ +[[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 new file mode 100644 index 000000000000..e9ed32bc772a --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.4.adoc @@ -0,0 +1,39 @@ +[[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/gradle.properties b/gradle.properties index d91e522a244f..b8ef9af98e63 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,13 @@ group = org.junit -version = 5.11.2 +version = 5.11.5-SNAPSHOT jupiterGroup = org.junit.jupiter platformGroup = org.junit.platform -platformVersion = 1.11.2 +platformVersion = 1.11.5-SNAPSHOT vintageGroup = org.junit.vintage -vintageVersion = 5.11.2 +vintageVersion = 5.11.5-SNAPSHOT # 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 diff --git a/gradle/base/code-generator-model/src/main/resources/jre.yaml b/gradle/base/code-generator-model/src/main/resources/jre.yaml index ed1bdd3d59fc..1747ffa12dd6 100644 --- a/gradle/base/code-generator-model/src/main/resources/jre.yaml +++ b/gradle/base/code-generator-model/src/main/resources/jre.yaml @@ -28,3 +28,5 @@ since: '5.11' - version: 24 since: '5.11' +- version: 25 + since: '5.11.4' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a54336afebdc..8a234a8b19f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,6 +61,7 @@ univocity-parsers = { module = "com.univocity:univocity-parsers", version = "2.9 xmlunit-assertj = { module = "org.xmlunit:xmlunit-assertj3", version.ref = "xmlunit" } xmlunit-placeholders = { module = "org.xmlunit:xmlunit-placeholders", version.ref = "xmlunit" } testingAnnotations = { module = "com.gradle:develocity-testing-annotations", version = "2.0.1" } +woodstox = { module = "com.fasterxml.woodstox:woodstox-core", version = "7.1.0" } # Only declared here so Dependabot knows when to update the referenced versions asciidoctorj-pdf = { module = "org.asciidoctor:asciidoctorj-pdf", version.ref = "asciidoctorj-pdf" } @@ -83,7 +84,7 @@ buildParameters = { id = "org.gradlex.build-parameters", version = "1.4.4" } commonCustomUserData = { id = "com.gradle.common-custom-user-data-gradle-plugin", version = "2.0.2" } develocity = { id = "com.gradle.develocity", version = "3.17.6" } foojayResolver = { id = "org.gradle.toolchains.foojay-resolver", version = "0.8.0" } -gitPublish = { id = "org.ajoberstar.git-publish", version = "4.2.2" } +gitPublish = { id = "org.ajoberstar.git-publish", version = "5.1.0" } jmh = { id = "me.champeau.jmh", version = "0.7.2" } nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } nohttp = { id = "io.spring.nohttp", version = "0.0.11" } diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild/exec/RunConsoleLauncher.kt b/gradle/plugins/common/src/main/kotlin/junitbuild/exec/RunConsoleLauncher.kt index a92f31195ad2..896e76090c6f 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild/exec/RunConsoleLauncher.kt +++ b/gradle/plugins/common/src/main/kotlin/junitbuild/exec/RunConsoleLauncher.kt @@ -6,7 +6,13 @@ import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property -import org.gradle.api.tasks.* +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.options.Option import org.gradle.jvm.toolchain.JavaLauncher import org.gradle.jvm.toolchain.JavaToolchainService @@ -16,7 +22,6 @@ import org.gradle.process.CommandLineArgumentProvider import org.gradle.process.ExecOperations import trackOperationSystemAsInput import java.io.ByteArrayOutputStream -import java.util.* import javax.inject.Inject @CacheableTask @@ -97,4 +102,13 @@ abstract class RunConsoleLauncher @Inject constructor(private val execOperations debugging.set(enabled) } + @Suppress("unused") + @Option( + option = "show-output", + description = "Show output" + ) + fun setShowOutput(showOutput: Boolean) { + hideOutput.set(!showOutput) + } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/Extensions.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/Extensions.java index 652c606d63cb..3607def6d0aa 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/Extensions.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/Extensions.java @@ -33,7 +33,7 @@ * @see ExtendWith * @see java.lang.annotation.Repeatable */ -@Target({ ElementType.TYPE, ElementType.METHOD }) +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java index 9cc9c64cf849..ad503e0e81dd 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java @@ -201,9 +201,7 @@ static void registerExtensionsFromExecutableParameters(ExtensionRegistrar regist * @since 5.11 */ private static Stream streamExtensionRegisteringFields(Class clazz, Predicate predicate) { - Predicate composedPredicate = predicate.and( - field -> isAnnotated(field, ExtendWith.class) || isAnnotated(field, RegisterExtension.class)); - return streamFields(clazz, composedPredicate, TOP_DOWN)// + return streamFields(clazz, predicate.and(registersExtension), TOP_DOWN)// .sorted(orderComparator); } @@ -236,4 +234,14 @@ private static int getOrder(Field field) { return findAnnotation(field, Order.class).map(Order::value).orElse(Order.DEFAULT); } + /** + * {@link Predicate} which determines if a {@link Field} registers an extension via + * {@link RegisterExtension @RegisterExtension} or {@link ExtendWith @ExtendWith}. + * + * @since 5.11.3 + */ + private static final Predicate registersExtension = // + field -> isAnnotated(field, RegisterExtension.class) + || !findRepeatableAnnotations(field, ExtendWith.class).isEmpty(); + } 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 a5673bae9d87..ea7dd0494da6 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 @@ -31,7 +31,7 @@ * @since 5.0 * @see org.junit.jupiter.params.provider.ArgumentsSource */ -@Target(ElementType.METHOD) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.7") 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 bc6bf3503fc9..92928d0ff715 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 @@ -32,7 +32,7 @@ * @see CsvFileSource * @see java.lang.annotation.Repeatable */ -@Target(ElementType.METHOD) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.11") 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 6c6951a75beb..297a4a8ddda5 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 @@ -32,7 +32,7 @@ * @see CsvSource * @see java.lang.annotation.Repeatable */ -@Target(ElementType.METHOD) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.11") 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 22feb5aa46d6..6b1a30a68ed5 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 @@ -32,7 +32,7 @@ * @see EnumSource * @see java.lang.annotation.Repeatable */ -@Target(ElementType.METHOD) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.11") 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 0b46746db5e4..36e9cd57d604 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 @@ -32,7 +32,7 @@ * @see FieldSource * @see java.lang.annotation.Repeatable */ -@Target(ElementType.METHOD) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = EXPERIMENTAL, since = "5.11") 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 056453f29820..33fa077567ee 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 @@ -32,7 +32,7 @@ * @see MethodSource * @see java.lang.annotation.Repeatable */ -@Target(ElementType.METHOD) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.11") 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 8db4dcc5b01f..9b3a489c6a66 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 @@ -32,7 +32,7 @@ * @see ValueSource * @see java.lang.annotation.Repeatable */ -@Target(ElementType.METHOD) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.11") diff --git a/junit-platform-commons/junit-platform-commons.gradle.kts b/junit-platform-commons/junit-platform-commons.gradle.kts index c0d1404a6f60..ef52ce77ab56 100644 --- a/junit-platform-commons/junit-platform-commons.gradle.kts +++ b/junit-platform-commons/junit-platform-commons.gradle.kts @@ -29,6 +29,7 @@ tasks.jar { tasks.codeCoverageClassesJar { exclude("org/junit/platform/commons/util/ModuleUtils.class") + exclude("org/junit/platform/commons/util/PackageNameUtils.class") } eclipse { diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageNameUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageNameUtils.java new file mode 100644 index 000000000000..9a018363a4a1 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageNameUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015-2024 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.util; + +import static org.junit.platform.commons.util.PackageUtils.DEFAULT_PACKAGE_NAME; + +/** + * Collection of utilities for working with package names. + * + *

DISCLAIMER

+ * + *

These utilities are intended solely for usage within the JUnit framework + * itself. Any usage by external parties is not supported. + * Use at your own risk! + * + * @since 1.11.3 + */ +class PackageNameUtils { + + static String getPackageName(Class clazz) { + Package p = clazz.getPackage(); + if (p != null) { + return p.getName(); + } + String className = clazz.getName(); + int index = className.lastIndexOf('.'); + return index == -1 ? DEFAULT_PACKAGE_NAME : className.substring(0, index); + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java index 517ee073f273..fcc44e95843d 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java @@ -19,6 +19,7 @@ import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList; +import static org.junit.platform.commons.util.PackageNameUtils.getPackageName; import static org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode.BOTTOM_UP; import static org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode.TOP_DOWN; @@ -1899,7 +1900,7 @@ private static boolean isPackagePrivate(Member member) { } private static boolean declaredInSamePackage(Method m1, Method m2) { - return m1.getDeclaringClass().getPackage().getName().equals(m2.getDeclaringClass().getPackage().getName()); + return getPackageName(m1.getDeclaringClass()).equals(getPackageName(m2.getDeclaringClass())); } /** diff --git a/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/PackageNameUtils.java b/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/PackageNameUtils.java new file mode 100644 index 000000000000..84afaf736290 --- /dev/null +++ b/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/PackageNameUtils.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015-2024 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.util; + +/** + * Collection of utilities for working with package names. + * + *

DISCLAIMER

+ * + *

These utilities are intended solely for usage within the JUnit framework + * itself. Any usage by external parties is not supported. + * Use at your own risk! + * + * @since 1.11.3 + */ +class PackageNameUtils { + + static String getPackageName(Class clazz) { + return clazz.getPackageName(); + } + +} diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/ConsoleLauncher.java b/junit-platform-console/src/main/java/org/junit/platform/console/ConsoleLauncher.java index 6ffd9ebefa5d..54345f061e2e 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/ConsoleLauncher.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/ConsoleLauncher.java @@ -30,36 +30,17 @@ public class ConsoleLauncher { public static void main(String... args) { - PrintWriter out = new PrintWriter(System.out); - PrintWriter err = new PrintWriter(System.err); - CommandResult result = run(out, err, args); + CommandResult result = newCommandFacade().run(args); System.exit(result.getExitCode()); } @API(status = INTERNAL, since = "1.0") public static CommandResult run(PrintWriter out, PrintWriter err, String... args) { - ConsoleLauncher consoleLauncher = new ConsoleLauncher(ConsoleTestExecutor::new, out, err); - return consoleLauncher.run(args); + return newCommandFacade().run(args, out, err); } - private final ConsoleTestExecutor.Factory consoleTestExecutorFactory; - private final PrintWriter out; - private final PrintWriter err; - - ConsoleLauncher(ConsoleTestExecutor.Factory consoleTestExecutorFactory, PrintWriter out, PrintWriter err) { - this.consoleTestExecutorFactory = consoleTestExecutorFactory; - this.out = out; - this.err = err; - } - - CommandResult run(String... args) { - try { - return new CommandFacade(consoleTestExecutorFactory).run(out, err, args); - } - finally { - out.flush(); - err.flush(); - } + private static CommandFacade newCommandFacade() { + return new CommandFacade(ConsoleTestExecutor::new); } } 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 a591a59abb70..b1690e279c96 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 @@ -33,10 +33,24 @@ public CommandFacade(ConsoleTestExecutor.Factory consoleTestExecutorFactory) { this.consoleTestExecutorFactory = consoleTestExecutorFactory; } - public CommandResult run(PrintWriter out, PrintWriter err, String[] args) { + public CommandResult run(String[] args) { + return run(args, Optional.empty()); + } + + public CommandResult run(String[] args, PrintWriter out, PrintWriter err) { + try { + return run(args, Optional.of(new OutputStreamConfig(out, err))); + } + finally { + out.flush(); + err.flush(); + } + } + + private CommandResult run(String[] args, Optional outputStreamConfig) { Optional version = ManifestVersionProvider.getImplementationVersion(); System.setProperty("junit.docs.version", version.map(it -> it.endsWith("-SNAPSHOT") ? "snapshot" : it).orElse("current")); - return new MainCommand(consoleTestExecutorFactory).run(out, err, args); + return new MainCommand(consoleTestExecutorFactory).run(args, outputStreamConfig); } } 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 d4a86e807785..941a4976ea3e 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 @@ -103,10 +103,11 @@ private Object runCommand(String subcommand, Optional triggeringOption) List args = new ArrayList<>(commandLine.getParseResult().expandedArgs()); triggeringOption.ifPresent(args::remove); - CommandResult result = runCommand(commandLine.getOut(), // - commandLine.getErr(), // + CommandResult result = runCommand( // + new CommandLine(command), // args.toArray(new String[0]), // - command); + Optional.of(new OutputStreamConfig(commandLine)) // + ); this.commandResult = result; printDeprecationWarning(subcommand, triggeringOption, commandLine); @@ -130,24 +131,19 @@ private static void printDeprecationWarning(String subcommand, Optional err.flush(); } - CommandResult run(PrintWriter out, PrintWriter err, String[] args) { + CommandResult run(String[] args, Optional outputStreamConfig) { CommandLine commandLine = new CommandLine(this) // .addSubcommand(new DiscoverTestsCommand(consoleTestExecutorFactory)) // .addSubcommand(new ExecuteTestsCommand(consoleTestExecutorFactory)) // .addSubcommand(new ListTestEnginesCommand()); - return runCommand(out, err, args, commandLine); + return runCommand(commandLine, args, outputStreamConfig); } - private static CommandResult runCommand(PrintWriter out, PrintWriter err, String[] args, Object command) { - return runCommand(out, err, args, new CommandLine(command)); - } - - private static CommandResult runCommand(PrintWriter out, PrintWriter err, String[] args, - CommandLine commandLine) { - int exitCode = BaseCommand.initialize(commandLine) // - .setOut(out) // - .setErr(err) // - .execute(args); + private static CommandResult runCommand(CommandLine commandLine, String[] args, + Optional outputStreamConfig) { + BaseCommand.initialize(commandLine); + outputStreamConfig.ifPresent(it -> it.applyTo(commandLine)); + int exitCode = commandLine.execute(args); return CommandResult.create(exitCode, getLikelyExecutedCommand(commandLine).getExecutionResult()); } 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 new file mode 100644 index 000000000000..dd72ed9e9f0b --- /dev/null +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/OutputStreamConfig.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2024 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 java.io.PrintWriter; + +import picocli.CommandLine; + +class OutputStreamConfig { + + private final PrintWriter out; + private final PrintWriter err; + + OutputStreamConfig(CommandLine commandLine) { + this(commandLine.getOut(), commandLine.getErr()); + } + + OutputStreamConfig(PrintWriter out, PrintWriter err) { + this.out = out; + this.err = err; + } + + void applyTo(CommandLine commandLine) { + commandLine.setOut(out).setErr(err); + } +} diff --git a/junit-platform-reporting/src/main/java/org/junit/platform/reporting/legacy/xml/XmlReportWriter.java b/junit-platform-reporting/src/main/java/org/junit/platform/reporting/legacy/xml/XmlReportWriter.java index e81d3abbd10c..4ea8c2f8f6ac 100644 --- a/junit-platform-reporting/src/main/java/org/junit/platform/reporting/legacy/xml/XmlReportWriter.java +++ b/junit-platform-reporting/src/main/java/org/junit/platform/reporting/legacy/xml/XmlReportWriter.java @@ -13,6 +13,7 @@ import static java.text.MessageFormat.format; import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableMap; import static java.util.Comparator.naturalOrder; import static java.util.function.Function.identity; import static java.util.stream.Collectors.counting; @@ -30,6 +31,7 @@ import static org.junit.platform.reporting.legacy.xml.XmlReportWriter.AggregatedTestResult.Type.SKIPPED; import static org.junit.platform.reporting.legacy.xml.XmlReportWriter.AggregatedTestResult.Type.SUCCESS; +import java.io.IOException; import java.io.Writer; import java.net.InetAddress; import java.net.UnknownHostException; @@ -38,6 +40,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -68,6 +71,17 @@ */ class XmlReportWriter { + static final char ILLEGAL_CHARACTER_REPLACEMENT = '\uFFFD'; + + private static final Map REPLACEMENTS_IN_ATTRIBUTE_VALUES; + static { + Map tmp = new HashMap<>(3); + tmp.put('\n', " "); + tmp.put('\r', " "); + tmp.put('\t', " "); + REPLACEMENTS_IN_ATTRIBUTE_VALUES = unmodifiableMap(tmp); + } + // Using zero-width assertions in the split pattern simplifies the splitting process: All split parts // (including the first and last one) can be used directly, without having to re-add separator characters. private static final Pattern CDATA_SPLIT_PATTERN = Pattern.compile("(?<=]])(?=>)"); @@ -101,243 +115,271 @@ private boolean shouldInclude(TestPlan testPlan, TestIdentifier testIdentifier) private void writeXmlReport(TestIdentifier testIdentifier, Map tests, Writer out) throws XMLStreamException { - XMLOutputFactory factory = XMLOutputFactory.newInstance(); - XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(out); - xmlWriter.writeStartDocument("UTF-8", "1.0"); - newLine(xmlWriter); - writeTestsuite(testIdentifier, tests, xmlWriter); - xmlWriter.writeEndDocument(); - xmlWriter.flush(); - xmlWriter.close(); + try (XmlReport report = new XmlReport(out)) { + report.write(testIdentifier, tests); + } } - private void writeTestsuite(TestIdentifier testIdentifier, Map tests, - XMLStreamWriter writer) throws XMLStreamException { + private class XmlReport implements AutoCloseable { - // NumberFormat is not thread-safe. Thus, we instantiate it here and pass it to - // writeTestcase instead of using a constant - NumberFormat numberFormat = NumberFormat.getInstance(Locale.US); + private final XMLStreamWriter xml; + private final ReplacingWriter out; - writer.writeStartElement("testsuite"); + XmlReport(Writer out) throws XMLStreamException { + this.out = new ReplacingWriter(out); + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + this.xml = factory.createXMLStreamWriter(this.out); + } - writeSuiteAttributes(testIdentifier, tests.values(), numberFormat, writer); + void write(TestIdentifier testIdentifier, Map tests) + throws XMLStreamException { + xml.writeStartDocument("UTF-8", "1.0"); + newLine(); + writeTestsuite(testIdentifier, tests); + xml.writeEndDocument(); + } - newLine(writer); - writeSystemProperties(writer); + private void writeTestsuite(TestIdentifier testIdentifier, Map tests) + throws XMLStreamException { - for (Entry entry : tests.entrySet()) { - writeTestcase(entry.getKey(), entry.getValue(), numberFormat, writer); - } + // NumberFormat is not thread-safe. Thus, we instantiate it here and pass it to + // writeTestcase instead of using a constant + NumberFormat numberFormat = NumberFormat.getInstance(Locale.US); - writeOutputElement("system-out", formatNonStandardAttributesAsString(testIdentifier), writer); + xml.writeStartElement("testsuite"); - writer.writeEndElement(); - newLine(writer); - } + writeSuiteAttributes(testIdentifier, tests.values(), numberFormat); - private void writeSuiteAttributes(TestIdentifier testIdentifier, Collection testResults, - NumberFormat numberFormat, XMLStreamWriter writer) throws XMLStreamException { + newLine(); + writeSystemProperties(); - writeAttributeSafely(writer, "name", testIdentifier.getDisplayName()); - writeTestCounts(testResults, writer); - writeAttributeSafely(writer, "time", getTime(testIdentifier, numberFormat)); - writeAttributeSafely(writer, "hostname", getHostname().orElse("")); - writeAttributeSafely(writer, "timestamp", ISO_LOCAL_DATE_TIME.format(getCurrentDateTime())); - } + for (Entry entry : tests.entrySet()) { + writeTestcase(entry.getKey(), entry.getValue(), numberFormat); + } - private void writeTestCounts(Collection testResults, XMLStreamWriter writer) - throws XMLStreamException { - Map counts = testResults.stream().map(it -> it.type).collect(groupingBy(identity(), counting())); - long total = counts.values().stream().mapToLong(Long::longValue).sum(); - writeAttributeSafely(writer, "tests", String.valueOf(total)); - writeAttributeSafely(writer, "skipped", counts.getOrDefault(SKIPPED, 0L).toString()); - writeAttributeSafely(writer, "failures", counts.getOrDefault(FAILURE, 0L).toString()); - writeAttributeSafely(writer, "errors", counts.getOrDefault(ERROR, 0L).toString()); - } + writeOutputElement("system-out", formatNonStandardAttributesAsString(testIdentifier)); - private void writeSystemProperties(XMLStreamWriter writer) throws XMLStreamException { - writer.writeStartElement("properties"); - newLine(writer); - Properties systemProperties = System.getProperties(); - for (String propertyName : new TreeSet<>(systemProperties.stringPropertyNames())) { - writer.writeEmptyElement("property"); - writeAttributeSafely(writer, "name", propertyName); - writeAttributeSafely(writer, "value", systemProperties.getProperty(propertyName)); - newLine(writer); - } - writer.writeEndElement(); - newLine(writer); - } + xml.writeEndElement(); + newLine(); + } - private void writeTestcase(TestIdentifier testIdentifier, AggregatedTestResult testResult, - NumberFormat numberFormat, XMLStreamWriter writer) throws XMLStreamException { + private void writeSuiteAttributes(TestIdentifier testIdentifier, Collection testResults, + NumberFormat numberFormat) throws XMLStreamException { - writer.writeStartElement("testcase"); + writeAttributeSafely("name", testIdentifier.getDisplayName()); + writeTestCounts(testResults); + writeAttributeSafely("time", getTime(testIdentifier, numberFormat)); + writeAttributeSafely("hostname", getHostname().orElse("")); + writeAttributeSafely("timestamp", ISO_LOCAL_DATE_TIME.format(getCurrentDateTime())); + } - writeAttributeSafely(writer, "name", getName(testIdentifier)); - writeAttributeSafely(writer, "classname", getClassName(testIdentifier)); - writeAttributeSafely(writer, "time", getTime(testIdentifier, numberFormat)); - newLine(writer); + private void writeTestCounts(Collection testResults) throws XMLStreamException { + Map counts = testResults.stream().map(it -> it.type).collect( + groupingBy(identity(), counting())); + long total = counts.values().stream().mapToLong(Long::longValue).sum(); + writeAttributeSafely("tests", String.valueOf(total)); + writeAttributeSafely("skipped", counts.getOrDefault(SKIPPED, 0L).toString()); + writeAttributeSafely("failures", counts.getOrDefault(FAILURE, 0L).toString()); + writeAttributeSafely("errors", counts.getOrDefault(ERROR, 0L).toString()); + } - writeSkippedOrErrorOrFailureElement(testIdentifier, testResult, writer); + private void writeSystemProperties() throws XMLStreamException { + xml.writeStartElement("properties"); + newLine(); + Properties systemProperties = System.getProperties(); + for (String propertyName : new TreeSet<>(systemProperties.stringPropertyNames())) { + xml.writeEmptyElement("property"); + writeAttributeSafely("name", propertyName); + writeAttributeSafely("value", systemProperties.getProperty(propertyName)); + newLine(); + } + xml.writeEndElement(); + newLine(); + } - List systemOutElements = new ArrayList<>(); - List systemErrElements = new ArrayList<>(); - systemOutElements.add(formatNonStandardAttributesAsString(testIdentifier)); - collectReportEntries(testIdentifier, systemOutElements, systemErrElements); - writeOutputElements("system-out", systemOutElements, writer); - writeOutputElements("system-err", systemErrElements, writer); + private void writeTestcase(TestIdentifier testIdentifier, AggregatedTestResult testResult, + NumberFormat numberFormat) throws XMLStreamException { - writer.writeEndElement(); - newLine(writer); - } + xml.writeStartElement("testcase"); - private String getName(TestIdentifier testIdentifier) { - return testIdentifier.getLegacyReportingName(); - } + writeAttributeSafely("name", getName(testIdentifier)); + writeAttributeSafely("classname", getClassName(testIdentifier)); + writeAttributeSafely("time", getTime(testIdentifier, numberFormat)); + newLine(); - private String getClassName(TestIdentifier testIdentifier) { - return LegacyReportingUtils.getClassName(this.reportData.getTestPlan(), testIdentifier); - } + writeSkippedOrErrorOrFailureElement(testIdentifier, testResult); + + List systemOutElements = new ArrayList<>(); + List systemErrElements = new ArrayList<>(); + systemOutElements.add(formatNonStandardAttributesAsString(testIdentifier)); + collectReportEntries(testIdentifier, systemOutElements, systemErrElements); + writeOutputElements("system-out", systemOutElements); + writeOutputElements("system-err", systemErrElements); + + xml.writeEndElement(); + newLine(); + } - private void writeSkippedOrErrorOrFailureElement(TestIdentifier testIdentifier, AggregatedTestResult testResult, - XMLStreamWriter writer) throws XMLStreamException { + private String getName(TestIdentifier testIdentifier) { + return testIdentifier.getLegacyReportingName(); + } - if (testResult.type == SKIPPED) { - writeSkippedElement(this.reportData.getSkipReason(testIdentifier), writer); + private String getClassName(TestIdentifier testIdentifier) { + return LegacyReportingUtils.getClassName(reportData.getTestPlan(), testIdentifier); } - else { - Map>> throwablesByType = testResult.getThrowablesByType(); - for (Type type : EnumSet.of(FAILURE, ERROR)) { - for (Optional throwable : throwablesByType.getOrDefault(type, emptyList())) { - writeErrorOrFailureElement(type, throwable.orElse(null), writer); + + private void writeSkippedOrErrorOrFailureElement(TestIdentifier testIdentifier, AggregatedTestResult testResult) + throws XMLStreamException { + + if (testResult.type == SKIPPED) { + writeSkippedElement(reportData.getSkipReason(testIdentifier), xml); + } + else { + Map>> throwablesByType = testResult.getThrowablesByType(); + for (Type type : EnumSet.of(FAILURE, ERROR)) { + for (Optional throwable : throwablesByType.getOrDefault(type, emptyList())) { + writeErrorOrFailureElement(type, throwable.orElse(null), xml); + } } } } - } - private void writeSkippedElement(String reason, XMLStreamWriter writer) throws XMLStreamException { - if (isNotBlank(reason)) { - writer.writeStartElement("skipped"); - writeCDataSafely(writer, reason); - writer.writeEndElement(); - } - else { - writer.writeEmptyElement("skipped"); + private void writeSkippedElement(String reason, XMLStreamWriter writer) throws XMLStreamException { + if (isNotBlank(reason)) { + writer.writeStartElement("skipped"); + writeCDataSafely(reason); + writer.writeEndElement(); + } + else { + writer.writeEmptyElement("skipped"); + } + newLine(); } - newLine(writer); - } - private void writeErrorOrFailureElement(Type type, Throwable throwable, XMLStreamWriter writer) - throws XMLStreamException { + private void writeErrorOrFailureElement(Type type, Throwable throwable, XMLStreamWriter writer) + throws XMLStreamException { - String elementName = type == FAILURE ? "failure" : "error"; - if (throwable != null) { - writer.writeStartElement(elementName); - writeFailureAttributesAndContent(throwable, writer); - writer.writeEndElement(); - } - else { - writer.writeEmptyElement(elementName); + String elementName = type == FAILURE ? "failure" : "error"; + if (throwable != null) { + writer.writeStartElement(elementName); + writeFailureAttributesAndContent(throwable); + writer.writeEndElement(); + } + else { + writer.writeEmptyElement(elementName); + } + newLine(); } - newLine(writer); - } - private void writeFailureAttributesAndContent(Throwable throwable, XMLStreamWriter writer) - throws XMLStreamException { + private void writeFailureAttributesAndContent(Throwable throwable) throws XMLStreamException { - if (throwable.getMessage() != null) { - writeAttributeSafely(writer, "message", throwable.getMessage()); + if (throwable.getMessage() != null) { + writeAttributeSafely("message", throwable.getMessage()); + } + writeAttributeSafely("type", throwable.getClass().getName()); + writeCDataSafely(readStackTrace(throwable)); } - writeAttributeSafely(writer, "type", throwable.getClass().getName()); - writeCDataSafely(writer, readStackTrace(throwable)); - } - private void collectReportEntries(TestIdentifier testIdentifier, List systemOutElements, - List systemErrElements) { - List entries = this.reportData.getReportEntries(testIdentifier); - if (!entries.isEmpty()) { - List systemOutElementsForCapturedOutput = new ArrayList<>(); - StringBuilder formattedReportEntries = new StringBuilder(); - for (int i = 0; i < entries.size(); i++) { - ReportEntry reportEntry = entries.get(i); - Map keyValuePairs = new LinkedHashMap<>(reportEntry.getKeyValuePairs()); - removeIfPresentAndAddAsSeparateElement(keyValuePairs, STDOUT_REPORT_ENTRY_KEY, - systemOutElementsForCapturedOutput); - removeIfPresentAndAddAsSeparateElement(keyValuePairs, STDERR_REPORT_ENTRY_KEY, systemErrElements); - if (!keyValuePairs.isEmpty()) { - buildReportEntryDescription(reportEntry.getTimestamp(), keyValuePairs, i + 1, - formattedReportEntries); + private void collectReportEntries(TestIdentifier testIdentifier, List systemOutElements, + List systemErrElements) { + List entries = reportData.getReportEntries(testIdentifier); + if (!entries.isEmpty()) { + List systemOutElementsForCapturedOutput = new ArrayList<>(); + StringBuilder formattedReportEntries = new StringBuilder(); + for (int i = 0; i < entries.size(); i++) { + ReportEntry reportEntry = entries.get(i); + Map keyValuePairs = new LinkedHashMap<>(reportEntry.getKeyValuePairs()); + removeIfPresentAndAddAsSeparateElement(keyValuePairs, STDOUT_REPORT_ENTRY_KEY, + systemOutElementsForCapturedOutput); + removeIfPresentAndAddAsSeparateElement(keyValuePairs, STDERR_REPORT_ENTRY_KEY, systemErrElements); + if (!keyValuePairs.isEmpty()) { + buildReportEntryDescription(reportEntry.getTimestamp(), keyValuePairs, i + 1, + formattedReportEntries); + } } + systemOutElements.add(formattedReportEntries.toString().trim()); + systemOutElements.addAll(systemOutElementsForCapturedOutput); } - systemOutElements.add(formattedReportEntries.toString().trim()); - systemOutElements.addAll(systemOutElementsForCapturedOutput); } - } - private void removeIfPresentAndAddAsSeparateElement(Map keyValuePairs, String key, - List elements) { - String value = keyValuePairs.remove(key); - if (value != null) { - elements.add(value); + private void removeIfPresentAndAddAsSeparateElement(Map keyValuePairs, String key, + List elements) { + String value = keyValuePairs.remove(key); + if (value != null) { + elements.add(value); + } } - } - private void buildReportEntryDescription(LocalDateTime timestamp, Map keyValuePairs, - int entryNumber, StringBuilder result) { - result.append( - format("Report Entry #{0} (timestamp: {1})\n", entryNumber, ISO_LOCAL_DATE_TIME.format(timestamp))); - keyValuePairs.forEach((key, value) -> result.append(format("\t- {0}: {1}\n", key, value))); - } + private void buildReportEntryDescription(LocalDateTime timestamp, Map keyValuePairs, + int entryNumber, StringBuilder result) { + result.append( + format("Report Entry #{0} (timestamp: {1})\n", entryNumber, ISO_LOCAL_DATE_TIME.format(timestamp))); + keyValuePairs.forEach((key, value) -> result.append(format("\t- {0}: {1}\n", key, value))); + } - private String getTime(TestIdentifier testIdentifier, NumberFormat numberFormat) { - return numberFormat.format(this.reportData.getDurationInSeconds(testIdentifier)); - } + private String getTime(TestIdentifier testIdentifier, NumberFormat numberFormat) { + return numberFormat.format(reportData.getDurationInSeconds(testIdentifier)); + } - private Optional getHostname() { - try { - return Optional.ofNullable(InetAddress.getLocalHost().getHostName()); + private Optional getHostname() { + try { + return Optional.ofNullable(InetAddress.getLocalHost().getHostName()); + } + catch (UnknownHostException e) { + return Optional.empty(); + } } - catch (UnknownHostException e) { - return Optional.empty(); + + private LocalDateTime getCurrentDateTime() { + return LocalDateTime.now(reportData.getClock()).withNano(0); } - } - private LocalDateTime getCurrentDateTime() { - return LocalDateTime.now(this.reportData.getClock()).withNano(0); - } + private String formatNonStandardAttributesAsString(TestIdentifier testIdentifier) { + return "unique-id: " + testIdentifier.getUniqueId() // + + "\ndisplay-name: " + testIdentifier.getDisplayName(); + } - private String formatNonStandardAttributesAsString(TestIdentifier testIdentifier) { - return "unique-id: " + testIdentifier.getUniqueId() // - + "\ndisplay-name: " + testIdentifier.getDisplayName(); - } + private void writeOutputElements(String elementName, List elements) throws XMLStreamException { + for (String content : elements) { + writeOutputElement(elementName, content); + } + } - private void writeOutputElements(String elementName, List elements, XMLStreamWriter writer) - throws XMLStreamException { - for (String content : elements) { - writeOutputElement(elementName, content, writer); + private void writeOutputElement(String elementName, String content) throws XMLStreamException { + xml.writeStartElement(elementName); + writeCDataSafely("\n" + content + "\n"); + xml.writeEndElement(); + newLine(); } - } - private void writeOutputElement(String elementName, String content, XMLStreamWriter writer) - throws XMLStreamException { - writer.writeStartElement(elementName); - writeCDataSafely(writer, "\n" + content + "\n"); - writer.writeEndElement(); - newLine(writer); - } + private void writeAttributeSafely(String name, String value) throws XMLStreamException { + // Workaround for XMLStreamWriter implementations that don't escape + // '\n', '\r', and '\t' characters in attribute values + xml.flush(); + out.setWhitespaceReplacingEnabled(true); + xml.writeAttribute(name, replaceIllegalCharacters(value)); + xml.flush(); + out.setWhitespaceReplacingEnabled(false); + } - private void writeAttributeSafely(XMLStreamWriter writer, String name, String value) throws XMLStreamException { - writer.writeAttribute(name, escapeIllegalChars(value)); - } + private void writeCDataSafely(String data) throws XMLStreamException { + for (String safeDataPart : CDATA_SPLIT_PATTERN.split(replaceIllegalCharacters(data))) { + xml.writeCData(safeDataPart); + } + } - private void writeCDataSafely(XMLStreamWriter writer, String data) throws XMLStreamException { - for (String safeDataPart : CDATA_SPLIT_PATTERN.split(escapeIllegalChars(data))) { - writer.writeCData(safeDataPart); + private void newLine() throws XMLStreamException { + xml.writeCharacters("\n"); + } + + @Override + public void close() throws XMLStreamException { + xml.flush(); + xml.close(); } } - static String escapeIllegalChars(String text) { + static String replaceIllegalCharacters(String text) { if (text.codePoints().allMatch(XmlReportWriter::isAllowedXmlCharacter)) { return text; } @@ -346,14 +388,14 @@ static String escapeIllegalChars(String text) { if (isAllowedXmlCharacter(codePoint)) { result.appendCodePoint(codePoint); } - else { // use a Character Reference (cf. https://www.w3.org/TR/xml/#NT-CharRef) - result.append("&#").append(codePoint).append(';'); + else { + result.append(ILLEGAL_CHARACTER_REPLACEMENT); } }); return result.toString(); } - private static boolean isAllowedXmlCharacter(int codePoint) { + static boolean isAllowedXmlCharacter(int codePoint) { // source: https://www.w3.org/TR/xml/#charsets return codePoint == 0x9 // || codePoint == 0xA // @@ -363,15 +405,6 @@ private static boolean isAllowedXmlCharacter(int codePoint) { || (codePoint >= 0x10000 && codePoint <= 0x10FFFF); } - private void newLine(XMLStreamWriter xmlWriter) throws XMLStreamException { - xmlWriter.writeCharacters("\n"); - } - - private static boolean isFailure(TestExecutionResult result) { - Optional throwable = result.getThrowable(); - return throwable.isPresent() && throwable.get() instanceof AssertionError; - } - static class AggregatedTestResult { private static final AggregatedTestResult SKIPPED_RESULT = new AggregatedTestResult(SKIPPED, emptyList()); @@ -411,6 +444,125 @@ private static Type from(TestExecutionResult executionResult) { } return SUCCESS; } + + private static boolean isFailure(TestExecutionResult result) { + Optional throwable = result.getThrowable(); + return throwable.isPresent() && throwable.get() instanceof AssertionError; + } + } + } + + private static class ReplacingWriter extends Writer { + + private final Writer delegate; + private boolean whitespaceReplacingEnabled; + + ReplacingWriter(Writer delegate) { + this.delegate = delegate; + } + + void setWhitespaceReplacingEnabled(boolean whitespaceReplacingEnabled) { + this.whitespaceReplacingEnabled = whitespaceReplacingEnabled; + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + if (!whitespaceReplacingEnabled) { + delegate.write(cbuf, off, len); + return; + } + StringBuilder stringBuilder = new StringBuilder(len * 2); + for (int i = off; i < off + len; i++) { + char c = cbuf[i]; + String replacement = REPLACEMENTS_IN_ATTRIBUTE_VALUES.get(c); + if (replacement != null) { + stringBuilder.append(replacement); + } + else { + stringBuilder.append(c); + } + } + delegate.write(stringBuilder.toString()); + } + + @Override + public void write(int c) throws IOException { + if (whitespaceReplacingEnabled) { + super.write(c); + } + else { + delegate.write(c); + } + } + + @Override + public void write(char[] cbuf) throws IOException { + if (whitespaceReplacingEnabled) { + super.write(cbuf); + } + else { + delegate.write(cbuf); + } + } + + @Override + public void write(String str) throws IOException { + if (whitespaceReplacingEnabled) { + super.write(str); + } + else { + delegate.write(str); + } + } + + @Override + public void write(String str, int off, int len) throws IOException { + if (whitespaceReplacingEnabled) { + super.write(str, off, len); + } + else { + delegate.write(str, off, len); + } + } + + @Override + public Writer append(CharSequence csq) throws IOException { + if (whitespaceReplacingEnabled) { + return super.append(csq); + } + else { + return delegate.append(csq); + } + } + + @Override + public Writer append(CharSequence csq, int start, int end) throws IOException { + if (whitespaceReplacingEnabled) { + return super.append(csq, start, end); + } + else { + return delegate.append(csq, start, end); + } + } + + @Override + public Writer append(char c) throws IOException { + if (whitespaceReplacingEnabled) { + return super.append(c); + } + else { + return delegate.append(c); + } + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void close() throws IOException { + delegate.close(); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionsUtilsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionsUtilsTests.java new file mode 100644 index 000000000000..b7afbb0c44b4 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionsUtilsTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015-2024 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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Field; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.engine.extension.ExtensionRegistrar; + +/** + * Tests for {@link ExtensionUtils}. + * + * @since 5.11.3 + */ +class ExtensionsUtilsTests { + + @Test + void registerExtensionsViaStaticFields() throws Exception { + Field field = TestCase.class.getDeclaredField("staticField"); + ExtensionRegistrar registrar = mock(); + ExtensionUtils.registerExtensionsFromStaticFields(registrar, TestCase.class); + verify(registrar).registerExtension(Extension1.class); + verify(registrar).registerExtension(Extension2.class); + verify(registrar).registerExtension(TestCase.staticField, field); + } + + @Test + @SuppressWarnings("unchecked") + void registerExtensionsViaInstanceFields() throws Exception { + Class testClass = TestCase.class; + Field field = testClass.getDeclaredField("instanceField"); + ExtensionRegistrar registrar = mock(); + ExtensionUtils.registerExtensionsFromInstanceFields(registrar, testClass); + verify(registrar).registerExtension(Extension1.class); + verify(registrar).registerExtension(Extension2.class); + verify(registrar).registerUninitializedExtension(eq(testClass), eq(field), any(Function.class)); + } + + static class Extension1 implements Extension { + } + + static class Extension2 implements Extension { + } + + static class Extension3 implements Extension { + } + + static class Extension4 implements Extension { + } + + @Retention(RetentionPolicy.RUNTIME) + @ExtendWith(Extension1.class) + @ExtendWith(Extension2.class) + @interface UseCustomExtensions { + } + + static class TestCase { + + @UseCustomExtensions + @RegisterExtension + static Extension3 staticField = new Extension3(); + + @UseCustomExtensions + @RegisterExtension + Extension4 instanceField = new Extension4(); + + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java index 2a748e280283..d196ec732b58 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistrationViaParametersAndFieldsTests.java @@ -132,6 +132,12 @@ void testTemplateMethodParameter() { assertTestsSucceeded(TestTemplateMethodParameterTestCase.class, 2); } + @Test + void multipleRegistrationsViaParameter(@TrackLogRecords LogRecordListener listener) { + assertOneTestSucceeded(MultipleRegistrationsViaParameterTestCase.class); + assertThat(getRegisteredLocalExtensions(listener)).containsExactly("LongParameterResolver", "DummyExtension"); + } + @Test void staticField() { assertOneTestSucceeded(StaticFieldTestCase.class); @@ -147,9 +153,11 @@ void fieldsWithTestInstancePerClass() { assertOneTestSucceeded(TestInstancePerClassFieldTestCase.class); } - @Test - void multipleRegistrationsViaField(@TrackLogRecords LogRecordListener listener) { - assertOneTestSucceeded(MultipleRegistrationsViaFieldTestCase.class); + @ParameterizedTest + @ValueSource(classes = { MultipleMixedRegistrationsViaFieldTestCase.class, + MultipleExtendWithRegistrationsViaFieldTestCase.class }) + void multipleRegistrationsViaField(Class testClass, @TrackLogRecords LogRecordListener listener) { + assertOneTestSucceeded(testClass); assertThat(getRegisteredLocalExtensions(listener)).containsExactly("LongParameterResolver", "DummyExtension"); } @@ -567,14 +575,37 @@ private static TestTemplateInvocationContext emptyTestTemplateInvocationContext( } } - static class MultipleRegistrationsViaFieldTestCase { + @ExtendWith(LongParameterResolver.class) + static class MultipleRegistrationsViaParameterTestCase { + + @Test + void test(@ExtendWith(DummyExtension.class) @ExtendWith(LongParameterResolver.class) Long number) { + assertThat(number).isEqualTo(42L); + } + } + + static class MultipleMixedRegistrationsViaFieldTestCase { @ExtendWith(LongParameterResolver.class) @RegisterExtension DummyExtension dummy = new DummyExtension(); @Test - void test() { + void test(Long number) { + assertThat(number).isEqualTo(42L); + } + } + + static class MultipleExtendWithRegistrationsViaFieldTestCase { + + @SuppressWarnings("unused") + @ExtendWith(LongParameterResolver.class) + @ExtendWith(DummyExtension.class) + Object field; + + @Test + void test(Long number) { + assertThat(number).isEqualTo(42L); } } 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 9fdd334754ed..7733c8fc5104 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 @@ -10,6 +10,7 @@ package org.junit.jupiter.params; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -40,7 +41,6 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Arrays; @@ -1093,9 +1093,10 @@ private EngineExecutionResults execute(String methodName, Class... methodPara @Nested class RepeatableSourcesIntegrationTests { - @Test - void executesWithRepeatableCsvFileSource() { - var results = execute("testWithRepeatableCsvFileSource", String.class, String.class); + @ParameterizedTest + @ValueSource(strings = { "testWithRepeatableCsvFileSource", "testWithRepeatableCsvFileSourceAsMetaAnnotation" }) + void executesWithRepeatableCsvFileSource(String methodName) { + var results = execute(methodName, String.class, String.class); results.allEvents().assertThatEvents() // .haveExactly(1, event(test(), displayName("[1] column1=foo, column2=1"), finishedWithFailure(message("foo 1")))) // @@ -1103,17 +1104,19 @@ void executesWithRepeatableCsvFileSource() { finishedWithFailure(message("apple 1")))); } - @Test - void executesWithRepeatableCsvSource() { - var results = execute("testWithRepeatableCsvSource", String.class); + @ParameterizedTest + @ValueSource(strings = { "testWithRepeatableCsvSource", "testWithRepeatableCsvSourceAsMetaAnnotation" }) + void executesWithRepeatableCsvSource(String methodName) { + var results = execute(methodName, String.class); results.allEvents().assertThatEvents() // .haveExactly(1, event(test(), displayName("[1] argument=a"), finishedWithFailure(message("a")))) // .haveExactly(1, event(test(), displayName("[2] argument=b"), finishedWithFailure(message("b")))); } - @Test - void executesWithRepeatableMethodSource() { - var results = execute("testWithRepeatableMethodSource", String.class); + @ParameterizedTest + @ValueSource(strings = { "testWithRepeatableMethodSource", "testWithRepeatableMethodSourceAsMetaAnnotation" }) + void executesWithRepeatableMethodSource(String methodName) { + var results = execute(methodName, String.class); results.allEvents().assertThatEvents() // .haveExactly(1, event(test(), displayName("[1] argument=some"), finishedWithFailure(message("some")))) // @@ -1121,27 +1124,30 @@ void executesWithRepeatableMethodSource() { event(test(), displayName("[2] argument=other"), finishedWithFailure(message("other")))); } - @Test - void executesWithRepeatableEnumSource() { - var results = execute("testWithRepeatableEnumSource", Action.class); + @ParameterizedTest + @ValueSource(strings = { "testWithRepeatableEnumSource", "testWithRepeatableEnumSourceAsMetaAnnotation" }) + void executesWithRepeatableEnumSource(String methodName) { + var results = execute(methodName, Action.class); results.allEvents().assertThatEvents() // .haveExactly(1, event(test(), displayName("[1] argument=FOO"), finishedWithFailure(message("FOO")))) // .haveExactly(1, event(test(), displayName("[2] argument=BAR"), finishedWithFailure(message("BAR")))); } - @Test - void executesWithRepeatableValueSource() { - var results = execute("testWithRepeatableValueSource", String.class); + @ParameterizedTest + @ValueSource(strings = { "testWithRepeatableValueSource", "testWithRepeatableValueSourceAsMetaAnnotation" }) + void executesWithRepeatableValueSource(String methodName) { + var results = execute(methodName, String.class); results.allEvents().assertThatEvents() // .haveExactly(1, event(test(), displayName("[1] argument=foo"), finishedWithFailure(message("foo")))) // .haveExactly(1, event(test(), displayName("[2] argument=bar"), finishedWithFailure(message("bar")))); } - @Test - void executesWithRepeatableFieldSource() { - var results = execute("testWithRepeatableFieldSource", String.class); + @ParameterizedTest + @ValueSource(strings = { "testWithRepeatableFieldSource", "testWithRepeatableFieldSourceAsMetaAnnotation" }) + void executesWithRepeatableFieldSource(String methodName) { + var results = execute(methodName, String.class); results.allEvents().assertThatEvents() // .haveExactly(1, event(test(), displayName("[1] argument=some"), finishedWithFailure(message("some")))) // @@ -1149,9 +1155,11 @@ void executesWithRepeatableFieldSource() { event(test(), displayName("[2] argument=other"), finishedWithFailure(message("other")))); } - @Test - void executesWithRepeatableArgumentsSource() { - var results = execute("testWithRepeatableArgumentsSource", String.class); + @ParameterizedTest + @ValueSource(strings = { "testWithRepeatableArgumentsSource", + "testWithRepeatableArgumentsSourceAsMetaAnnotation" }) + void executesWithRepeatableArgumentsSource(String methodName) { + var results = execute(methodName, String.class); results.allEvents().assertThatEvents() // .haveExactly(1, event(test(), displayName("[1] argument=foo"), finishedWithFailure(message("foo")))) // .haveExactly(1, event(test(), displayName("[2] argument=bar"), finishedWithFailure(message("bar")))) // @@ -1542,7 +1550,7 @@ void testWithNullAndEmptySourceForTwoDimensionalStringArray(String[][] argument) static class MethodSourceTestCase { @Target(ElementType.METHOD) - @Retention(RetentionPolicy.RUNTIME) + @Retention(RUNTIME) @ParameterizedTest(name = "{arguments}") @MethodSource @interface MethodSourceTest { @@ -1767,7 +1775,7 @@ private static Stream test() { static class FieldSourceTestCase { @Target(ElementType.METHOD) - @Retention(RetentionPolicy.RUNTIME) + @Retention(RUNTIME) @ParameterizedTest(name = "{arguments}") @FieldSource @interface FieldSourceTest { @@ -2035,6 +2043,18 @@ void testWithRepeatableCsvFileSource(String column1, String column2) { fail("%s %s".formatted(column1, column2)); } + @CsvFileSource(resources = "two-column.csv") + @CsvFileSource(resources = "two-column-with-headers.csv", delimiter = '|', useHeadersInDisplayName = true, nullValues = "NIL") + @Retention(RUNTIME) + @interface TwoCsvFileSources { + } + + @ParameterizedTest + @TwoCsvFileSources + void testWithRepeatableCsvFileSourceAsMetaAnnotation(String column1, String column2) { + fail("%s %s".formatted(column1, column2)); + } + @ParameterizedTest @CsvSource({ "a" }) @CsvSource({ "b" }) @@ -2042,6 +2062,18 @@ void testWithRepeatableCsvSource(String argument) { fail(argument); } + @CsvSource({ "a" }) + @CsvSource({ "b" }) + @Retention(RUNTIME) + @interface TwoCsvSources { + } + + @ParameterizedTest + @TwoCsvSources + void testWithRepeatableCsvSourceAsMetaAnnotation(String argument) { + fail(argument); + } + @ParameterizedTest @EnumSource(SmartAction.class) @EnumSource(QuickAction.class) @@ -2049,6 +2081,18 @@ void testWithRepeatableEnumSource(Action argument) { fail(argument.toString()); } + @EnumSource(SmartAction.class) + @EnumSource(QuickAction.class) + @Retention(RUNTIME) + @interface TwoEnumSources { + } + + @ParameterizedTest + @TwoEnumSources + void testWithRepeatableEnumSourceAsMetaAnnotation(Action argument) { + fail(argument.toString()); + } + interface Action { } @@ -2067,6 +2111,18 @@ void testWithRepeatableMethodSource(String argument) { fail(argument); } + @MethodSource("someArgumentsMethodSource") + @MethodSource("otherArgumentsMethodSource") + @Retention(RUNTIME) + @interface TwoMethodSources { + } + + @ParameterizedTest + @TwoMethodSources + void testWithRepeatableMethodSourceAsMetaAnnotation(String argument) { + fail(argument); + } + public static Stream someArgumentsMethodSource() { return Stream.of(Arguments.of("some")); } @@ -2082,6 +2138,18 @@ void testWithRepeatableFieldSource(String argument) { fail(argument); } + @FieldSource("someArgumentsContainer") + @FieldSource("otherArgumentsContainer") + @Retention(RUNTIME) + @interface TwoFieldSources { + } + + @ParameterizedTest + @TwoFieldSources + void testWithRepeatableFieldSourceAsMetaAnnotation(String argument) { + fail(argument); + } + static List someArgumentsContainer = List.of("some"); static List otherArgumentsContainer = List.of("other"); @@ -2092,6 +2160,18 @@ void testWithRepeatableValueSource(String argument) { fail(argument); } + @ValueSource(strings = "foo") + @ValueSource(strings = "bar") + @Retention(RUNTIME) + @interface TwoValueSources { + } + + @ParameterizedTest + @TwoValueSources + void testWithRepeatableValueSourceAsMetaAnnotation(String argument) { + fail(argument); + } + @ParameterizedTest @ValueSource(strings = "foo") @ValueSource(strings = "foo") @@ -2117,6 +2197,18 @@ void testWithDifferentRepeatableAnnotations(String argument) { void testWithRepeatableArgumentsSource(String argument) { fail(argument); } + + @ArgumentsSource(TwoSingleStringArgumentsProvider.class) + @ArgumentsSource(TwoUnusedStringArgumentsProvider.class) + @Retention(RUNTIME) + @interface TwoArgumentsSources { + } + + @ParameterizedTest + @TwoArgumentsSources + void testWithRepeatableArgumentsSourceAsMetaAnnotation(String argument) { + fail(argument); + } } private static class TwoSingleStringArgumentsProvider implements ArgumentsProvider { diff --git a/platform-tests/platform-tests.gradle.kts b/platform-tests/platform-tests.gradle.kts index 183203351e94..a857b163b81c 100644 --- a/platform-tests/platform-tests.gradle.kts +++ b/platform-tests/platform-tests.gradle.kts @@ -9,6 +9,12 @@ plugins { id("junitbuild.jmh-conventions") } +val woodstox = configurations.dependencyScope("woodstox") +val woodstoxRuntimeClasspath = configurations.resolvable("woodstoxRuntimeClasspath") { + extendsFrom(configurations.testRuntimeClasspath.get()) + extendsFrom(woodstox.get()) +} + dependencies { // --- Things we are testing -------------------------------------------------- testImplementation(projects.junitPlatformCommons) @@ -42,6 +48,7 @@ dependencies { testRuntimeOnly(libs.groovy4) { because("`ReflectionUtilsTests.findNestedClassesWithInvalidNestedClassFile` needs it") } + woodstox(libs.woodstox) // --- https://openjdk.java.net/projects/code-tools/jmh/ ----------------------- jmh(projects.junitJupiterApi) @@ -79,6 +86,16 @@ tasks { includeTags("junit4") } } + val testWoodstox by registering(Test::class) { + val test by testing.suites.existing(JvmTestSuite::class) + testClassesDirs = files(test.map { it.sources.output.classesDirs }) + classpath = files(sourceSets.main.map { it.output }) + files(test.map { it.sources.output }) + woodstoxRuntimeClasspath.get() + group = JavaBasePlugin.VERIFICATION_GROUP + setIncludes(listOf("**/org/junit/platform/reporting/**")) + } + check { + dependsOn(testWoodstox) + } } eclipse { diff --git a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherTests.java b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherTests.java index ef30a6d3db59..f4fb02f4a935 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherTests.java @@ -22,7 +22,6 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EmptySource; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.platform.console.tasks.ConsoleTestExecutor; /** * @since 1.0 @@ -36,8 +35,7 @@ class ConsoleLauncherTests { @EmptySource @MethodSource("commandsWithEmptyOptionExitCodes") void displayHelp(String command) { - var consoleLauncher = new ConsoleLauncher(ConsoleTestExecutor::new, printSink, printSink); - var exitCode = consoleLauncher.run(command, "--help").getExitCode(); + var exitCode = ConsoleLauncher.run(printSink, printSink, command, "--help").getExitCode(); assertEquals(0, exitCode); assertThat(output()).contains("--help"); @@ -47,8 +45,7 @@ void displayHelp(String command) { @EmptySource @MethodSource("commandsWithEmptyOptionExitCodes") void displayVersion(String command) { - var consoleLauncher = new ConsoleLauncher(ConsoleTestExecutor::new, printSink, printSink); - var exitCode = consoleLauncher.run(command, "--version").getExitCode(); + var exitCode = ConsoleLauncher.run(printSink, printSink, command, "--version").getExitCode(); assertEquals(0, exitCode); assertThat(output()).contains("JUnit Platform Console Launcher"); @@ -57,8 +54,7 @@ void displayVersion(String command) { @ParameterizedTest(name = "{0}") @MethodSource("commandsWithEmptyOptionExitCodes") void displayBanner(String command) { - var consoleLauncher = new ConsoleLauncher(ConsoleTestExecutor::new, printSink, printSink); - consoleLauncher.run(command); + ConsoleLauncher.run(printSink, printSink, command); assertThat(output()).contains("Thanks for using JUnit!"); } @@ -66,8 +62,7 @@ void displayBanner(String command) { @ParameterizedTest(name = "{0}") @MethodSource("commandsWithEmptyOptionExitCodes") void disableBanner(String command, int expectedExitCode) { - var consoleLauncher = new ConsoleLauncher(ConsoleTestExecutor::new, printSink, printSink); - var exitCode = consoleLauncher.run(command, "--disable-banner").getExitCode(); + var exitCode = ConsoleLauncher.run(printSink, printSink, command, "--disable-banner").getExitCode(); assertEquals(expectedExitCode, exitCode); assertThat(output()).doesNotContain("Thanks for using JUnit!"); @@ -76,8 +71,7 @@ void disableBanner(String command, int expectedExitCode) { @ParameterizedTest(name = "{0}") @MethodSource("commandsWithEmptyOptionExitCodes") void executeWithUnknownCommandLineOption(String command) { - var consoleLauncher = new ConsoleLauncher(ConsoleTestExecutor::new, printSink, printSink); - var exitCode = consoleLauncher.run(command, "--all").getExitCode(); + var exitCode = ConsoleLauncher.run(printSink, printSink, command, "--all").getExitCode(); assertEquals(-1, exitCode); assertThat(output()).contains("Unknown option: '--all'").contains("Usage:"); @@ -90,8 +84,7 @@ private String output() { @ParameterizedTest(name = "{0}") @MethodSource("commandsWithEmptyOptionExitCodes") void executeWithoutCommandLineOptions(String command, int expectedExitCode) { - var consoleLauncher = new ConsoleLauncher(ConsoleTestExecutor::new, printSink, printSink); - var actualExitCode = consoleLauncher.run(command).getExitCode(); + var actualExitCode = ConsoleLauncher.run(printSink, printSink, command).getExitCode(); assertEquals(expectedExitCode, actualExitCode); } diff --git a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherWrapper.java b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherWrapper.java index 7ae6ba8fd921..0d2c240d8ea3 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherWrapper.java +++ b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherWrapper.java @@ -17,6 +17,7 @@ import java.io.StringWriter; import java.util.Optional; +import org.junit.platform.console.options.CommandFacade; import org.junit.platform.console.tasks.ConsoleTestExecutor; /** @@ -26,16 +27,14 @@ class ConsoleLauncherWrapper { private final StringWriter out = new StringWriter(); private final StringWriter err = new StringWriter(); - private final ConsoleLauncher consoleLauncher; + private final ConsoleTestExecutor.Factory consoleTestExecutorFactory; ConsoleLauncherWrapper() { this(ConsoleTestExecutor::new); } private ConsoleLauncherWrapper(ConsoleTestExecutor.Factory consoleTestExecutorFactory) { - var outWriter = new PrintWriter(out, false); - var errWriter = new PrintWriter(err, false); - this.consoleLauncher = new ConsoleLauncher(consoleTestExecutorFactory, outWriter, errWriter); + this.consoleTestExecutorFactory = consoleTestExecutorFactory; } public ConsoleLauncherWrapperResult execute(String... args) { @@ -47,7 +46,9 @@ public ConsoleLauncherWrapperResult execute(int expectedExitCode, String... args } public ConsoleLauncherWrapperResult execute(Optional expectedCode, String... args) { - var result = consoleLauncher.run(args); + var outWriter = new PrintWriter(out, false); + var errWriter = new PrintWriter(err, false); + var result = new CommandFacade(consoleTestExecutorFactory).run(args, outWriter, errWriter); var code = result.getExitCode(); var outText = out.toString(); var errText = err.toString(); 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 b66b09e50e3c..7a5b49f38c40 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 @@ -10,6 +10,7 @@ package org.junit.platform.reporting.legacy.xml; +import static java.util.stream.Collectors.joining; import static org.assertj.core.api.Assertions.assertThat; import static org.joox.JOOX.$; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -20,6 +21,7 @@ import static org.junit.platform.launcher.LauncherConstants.STDERR_REPORT_ENTRY_KEY; import static org.junit.platform.launcher.LauncherConstants.STDOUT_REPORT_ENTRY_KEY; import static org.junit.platform.reporting.legacy.xml.XmlReportAssertions.assertValidAccordingToJenkinsSchema; +import static org.junit.platform.reporting.legacy.xml.XmlReportWriter.ILLEGAL_CHARACTER_REPLACEMENT; import static org.mockito.Mockito.mock; import java.io.StringReader; @@ -28,6 +30,7 @@ import java.time.Clock; import java.util.Map; import java.util.Set; +import java.util.stream.IntStream; import java.util.stream.Stream; import org.joox.Match; @@ -219,50 +222,55 @@ void escapesInvalidCharactersInSystemPropertiesAndExceptionMessages() throws Exc assertValidAccordingToJenkinsSchema(testsuite.document()); assertThat(testsuite.find("property").matchAttr("name", "foo\\.bar").attr("value")) // - .isEqualTo(""); + .isEqualTo(String.valueOf(ILLEGAL_CHARACTER_REPLACEMENT)); var failure = testsuite.find("failure"); assertThat(failure.attr("message")) // - .isEqualTo("expected: but was: "); + .isEqualTo("expected: but was: "); assertThat(failure.text()) // - .contains("AssertionError: expected: but was: "); + .contains("AssertionError: expected: but was: "); + } + + @ParameterizedTest(name = "[{index}]") + @MethodSource("stringPairs") + void replacesIllegalCharacters(String input, String output) { + assertEquals(output, XmlReportWriter.replaceIllegalCharacters(input)); } @Test - void doesNotReopenCDataWithinCDataContent() throws Exception { + 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); + var allWhitespaceCharacters = IntStream.range(0, 0x10000) // + .filter(Character::isWhitespace) // + .filter(XmlReportWriter::isAllowedXmlCharacter) // + .mapToObj(Character::toString) // + .collect(joining()); + + var message = "a" + allWhitespaceCharacters + " b<&>"; var reportData = new XmlReportData(testPlan, Clock.systemDefaultZone()); - var assertionError = new AssertionError(""); + var assertionError = new AssertionError(message); reportData.markFinished(testPlan.getTestIdentifier(uniqueId), failed(assertionError)); - Writer assertingWriter = new StringWriter() { - @SuppressWarnings("NullableProblems") - @Override - public void write(char[] buffer, int off, int len) { - assertThat(new String(buffer, off, len)).doesNotContain("]]> stringPairs() { return Stream.of( // - arguments("\0", "�"), // - arguments("\1", ""), // + arguments("\0", String.valueOf(ILLEGAL_CHARACTER_REPLACEMENT)), // + arguments("\1", String.valueOf(ILLEGAL_CHARACTER_REPLACEMENT)), // arguments("\t", "\t"), // arguments("\r", "\r"), // arguments("\n", "\n"), // - arguments("\u001f", ""), // - arguments("\u0020", "\u0020"), // + arguments("\u001f", String.valueOf(ILLEGAL_CHARACTER_REPLACEMENT)), // + arguments("✅", "✅"), // + arguments(" ", " "), // arguments("foo!", "foo!"), // arguments("\uD801\uDC00", "\uD801\uDC00") // ); diff --git a/platform-tooling-support-tests/projects/vintage/src/test/java/DefaultPackageTest.java b/platform-tooling-support-tests/projects/vintage/src/test/java/DefaultPackageTest.java new file mode 100644 index 000000000000..beebfdd5fd54 --- /dev/null +++ b/platform-tooling-support-tests/projects/vintage/src/test/java/DefaultPackageTest.java @@ -0,0 +1,22 @@ + +/* + * Copyright 2015-2024 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 + */ +import com.example.vintage.VintageTest; + +import org.junit.Ignore; + +/** + * Reproducer for https://github.com/junit-team/junit5/issues/4076 + */ +@Ignore +public class DefaultPackageTest extends VintageTest { + void packagePrivateMethod() { + } +} diff --git a/platform-tooling-support-tests/projects/vintage/src/test/java/com/example/vintage/VintageTest.java b/platform-tooling-support-tests/projects/vintage/src/test/java/com/example/vintage/VintageTest.java index 1632b1e3537b..a6efb8990791 100644 --- a/platform-tooling-support-tests/projects/vintage/src/test/java/com/example/vintage/VintageTest.java +++ b/platform-tooling-support-tests/projects/vintage/src/test/java/com/example/vintage/VintageTest.java @@ -15,6 +15,9 @@ import org.junit.Test; public class VintageTest { + void packagePrivateMethod() { + } + @Test public void success() { // pass diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java index ff32c0777736..1ecbc4764582 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java @@ -21,6 +21,7 @@ import static com.tngtech.archunit.core.domain.properties.HasModifiers.Predicates.modifier; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.nameContaining; +import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.nameStartingWith; import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; import static com.tngtech.archunit.lang.conditions.ArchPredicates.have; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; @@ -28,24 +29,32 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static platform.tooling.support.Helper.loadJarFiles; +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.stream.Collectors; +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 org.junit.jupiter.api.Order; -@Order(Integer.MAX_VALUE) @AnalyzeClasses(locations = ArchUnitTests.AllJars.class) class ArchUnitTests { + @SuppressWarnings("unused") @ArchTest private final ArchRule allPublicTopLevelTypesHaveApiAnnotations = classes() // .that(have(modifier(PUBLIC))) // @@ -55,6 +64,15 @@ class ArchUnitTests { .and(not(describe("are shadowed", resideInAnyPackage("..shadow..")))) // .should().beAnnotatedWith(API.class); + @SuppressWarnings("unused") + @ArchTest // Consistency of @Documented and @Inherited is checked by the compiler but not for @Retention and @Target + private final ArchRule repeatableAnnotationsShouldHaveMatchingContainerAnnotations = classes() // + .that(nameStartingWith("org.junit.")) // + .and().areAnnotations() // + .and().areAnnotatedWith(Repeatable.class) // + .should(haveContainerAnnotationWithSameRetentionPolicy()) // + .andShould(haveContainerAnnotationWithSameTargetTypes()); + @ArchTest void allAreIn(JavaClasses classes) { // about 928 classes found in all jars @@ -94,6 +112,16 @@ void avoidAccessingStandardStreams(JavaClasses classes) { GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS.check(subset); } + private static ArchCondition haveContainerAnnotationWithSameRetentionPolicy() { + return ArchCondition.from(new RepeatableAnnotationPredicate<>(Retention.class, + (expectedTarget, actualTarget) -> expectedTarget.value() == actualTarget.value())); + } + + private static ArchCondition haveContainerAnnotationWithSameTargetTypes() { + return ArchCondition.from(new RepeatableAnnotationPredicate<>(Target.class, + (expectedTarget, actualTarget) -> Arrays.equals(expectedTarget.value(), actualTarget.value()))); + } + static class AllJars implements LocationProvider { @Override @@ -103,4 +131,27 @@ public Set get(Class testClass) { } + private static class RepeatableAnnotationPredicate extends DescribedPredicate { + + private final Class annotationType; + private final BiPredicate predicate; + + public RepeatableAnnotationPredicate(Class annotationType, BiPredicate predicate) { + super("have identical @%s annotation as container annotation", annotationType.getSimpleName()); + this.annotationType = annotationType; + this.predicate = predicate; + } + + @Override + public boolean test(JavaClass annotationClass) { + var containerAnnotationClass = (JavaClass) annotationClass.getAnnotationOfType( + Repeatable.class.getName()).get("value").orElseThrow(); + var expectedAnnotation = annotationClass.tryGetAnnotationOfType(annotationType); + var actualAnnotation = containerAnnotationClass.tryGetAnnotationOfType(annotationType); + return expectedAnnotation.map(expectedTarget -> actualAnnotation // + .map(actualTarget -> predicate.test(expectedTarget, actualTarget)) // + .orElse(false)) // + .orElse(actualAnnotation.isEmpty()); + } + } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenEnvVars.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenEnvVars.java index bcec609010a0..eebfc4ffd58f 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenEnvVars.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenEnvVars.java @@ -16,9 +16,11 @@ final class MavenEnvVars { - // https://issues.apache.org/jira/browse/MNG-8248 static final Map FOR_JDK24_AND_LATER = JRE.currentVersion().compareTo(JRE.JAVA_24) >= 0 // - ? Map.of("MAVEN_OPTS", "--enable-native-access=ALL-UNNAMED") // + ? Map.of("MAVEN_OPTS", String.join(" ", // + "--enable-native-access=ALL-UNNAMED", // https://issues.apache.org/jira/browse/MNG-8248 + "--sun-misc-unsafe-memory-access=allow" // https://issues.apache.org/jira/browse/MNG-8399 + )) // : Map.of(); private MavenEnvVars() {