From e575ea3e9c7f3ce95c794accbd81904a1ce92599 Mon Sep 17 00:00:00 2001 From: Ale Mendoza Date: Fri, 21 Apr 2023 13:16:27 -0600 Subject: [PATCH] Do not parse {displayName} for @ParameterizedTest using MessageFormat Prior to this commit, if a @ParameterizedTest used the {displayName} placeholder to generate a display name and the value of the displayName contained an apostrophe (') or something resembling a MessageFormat element (such as {data}), JUnit threw an exception due to failure to parse the display name. This is applicable to method names in Kotlin-based tests or custom display names in general. To fix this bug, instead of replacing the DISPLAY_NAME_PLACEHOLDER before the MessageFormat is evaluated, the DISPLAY_NAME_PLACEHOLDER is now replaced with another temporary placeholder, which is then replaced after the MessageFormat has been evaluated. Closes #3235 Closes #3264 --- .../release-notes-5.10.0-M1.adoc | 102 ++++++++++++++++++ .../ParameterizedTestNameFormatter.java | 6 +- .../ParameterizedTestNameFormatterTests.java | 18 ++++ ...erizedTestNameFormatterIntegrationTests.kt | 62 +++++++++++ 4 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc create mode 100644 junit-jupiter-params/src/test/kotlin/ParameterizedTestNameFormatterIntegrationTests.kt diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc new file mode 100644 index 000000000000..1308b0e792a0 --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc @@ -0,0 +1,102 @@ +[[release-notes-5.10.0-M1]] +== 5.10.0-M1 + +*Date of Release:* ❓ + +*Scope:* ❓ + +For a complete list of all _closed_ issues and pull requests for this release, consult the +link:{junit5-repo}+/milestone/65?closed=1+[5.10.0-M1] milestone page in the JUnit +repository on GitHub. + + +[[release-notes-5.10.0-M1-junit-platform]] +=== JUnit Platform + +==== Bug Fixes + +* ❓ + +==== Deprecations and Breaking Changes + +* Building native images with GraalVM now requires configuring the build arg + `--initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig`. + +==== New Features and Improvements + +* Promote various "experimental" APIs that have matured to "stable" including + `ModuleSelector`, `EngineDiscoveryListener`, `EngineDiscoveryRequestResolver`, + `LauncherSession`, `LauncherSessionListener`, parallel execution support classes, + `@Suite` and related annotations, and others. +* All utility methods in `ReflectionSupport` that return a `List` now have counterparts + which return a `Stream`. +* For consistency with JUnit Jupiter lifecycle callbacks, listener method pairs for + started/finished and opened/closed events are now invoked using "wrapping" semantics. + This means that finished/closed event methods are invoked in reverse order compared to + the corresponding started/opened event methods when multiple listeners are registered. + This affects the following listener interfaces: + `TestExecutionListener`, `EngineExecutionListener`, `LauncherDiscoveryListener`, and + `LauncherSessionListener`. +* New `LauncherInterceptor` SPI for intercepting the creation of instances of `Launcher` + and `LauncherSessionlistener` as well as invocations of the `discover` and `execute` + methods of the former. Please refer to the + <<../user-guide/index.adoc#launcher-api-launcher-interceptors-custom, User Guide>> for + details. +* Support for limiting the `max-pool-size-factor` for parallel execution via a configuration parameter. +* The new `testfeed` details mode for `ConsoleLauncher` prints test execution events as + they occur in a concise format. + + +[[release-notes-5.10.0-M1-junit-jupiter]] +=== JUnit Jupiter + +==== Bug Fixes + +* The `{displayName}` placeholder of `@ParameterizedTest` is no longer parsed during the + evaluation of the `MessageFormat`, now `@DisplayName` and Kotlin method names can contain + single apostrophes and `MessageFormat` elements, such as `{data}`. + +==== Deprecations and Breaking Changes + +* The `dynamic` parallel execution strategy now allows the thread pool to be saturated by +default. + +==== New Features and Improvements + +* Promote various "experimental" APIs that have matured to "stable" including + `MethodOrderer`, `ClassOrderer`, `InvocationInterceptor`, + `LifecycleMethodExecutionExceptionHandler`, `@TempDir`, parallel execution annotations, + and others. +* `@RepeatedTest` can now be configured with a failure threshold which signifies the + number of failures after which remaining repetitions will be automatically skipped. See + the <<../user-guide/index.adoc#writing-tests-repeated-tests, User Guide>> for details. +* New `ArgumentsAccessor.getInvocationIndex()` method that supplies the index of a + `@ParameterizedTest` invocation. +* `@EmptySource` now supports additional types, including `Collection` and `Map` subtypes + with a public no-arg constructor. +* `DisplayNameGenerator` methods are now allowed to return `null`, in order to signal to + fall back to the default display name generator. +* New `AnnotationBasedArgumentsProvider` convenience base class which implements both + `ArgumentsProvider` and `AnnotationConsumer`. +* New `AnnotationBasedArgumentConverter` convenience base class which implements both + `ArgumentConverter` and `AnnotationConsumer`. +* New `junit.jupiter.execution.parallel.config.dynamic.max-pool-size-factor` configuration + parameter to set the maximum pool size factor. +* New `junit.jupiter.execution.parallel.config.dynamic.saturate` configuration + parameter to disable pool saturation. + + +[[release-notes-5.10.0-M1-junit-vintage]] +=== JUnit Vintage + +==== Bug Fixes + +* ❓ + +==== Deprecations and Breaking Changes + +* ❓ + +==== New Features and Improvements + +* ❓ 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/ParameterizedTestNameFormatter.java index 4d8899d89639..800f146b70b0 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/ParameterizedTestNameFormatter.java @@ -31,6 +31,7 @@ class ParameterizedTestNameFormatter { private static final char ELLIPSIS = '\u2026'; + private static final String DISPLAY_NAME_TEMPORARY_PLACEHOLDER = "__DISPLAY_NAME__"; private final String pattern; private final String displayName; @@ -61,7 +62,8 @@ private String formatSafely(int invocationIndex, Object[] arguments) { String pattern = prepareMessageFormatPattern(invocationIndex, namedArguments); MessageFormat format = new MessageFormat(pattern); Object[] humanReadableArguments = makeReadable(format, namedArguments); - return format.format(humanReadableArguments); + String formatted = format.format(humanReadableArguments); + return formatted.replace(DISPLAY_NAME_TEMPORARY_PLACEHOLDER, this.displayName); } private Object[] extractNamedArguments(Object[] arguments) { @@ -72,7 +74,7 @@ private Object[] extractNamedArguments(Object[] arguments) { private String prepareMessageFormatPattern(int invocationIndex, Object[] arguments) { String result = pattern// - .replace(DISPLAY_NAME_PLACEHOLDER, this.displayName)// + .replace(DISPLAY_NAME_PLACEHOLDER, DISPLAY_NAME_TEMPORARY_PLACEHOLDER)// .replace(INDEX_PLACEHOLDER, String.valueOf(invocationIndex)); if (result.contains(ARGUMENTS_WITH_NAMES_PLACEHOLDER)) { diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java index e83f88fc0f62..ad63dbdede43 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java @@ -60,6 +60,24 @@ void formatsDisplayName() { assertEquals("enigma", formatter.format(2)); } + @Test + void formatsDisplayNameWithApostrophe() { + String displayName = "display'Zero"; + var formatter = formatter(DISPLAY_NAME_PLACEHOLDER, "display'Zero"); + + assertEquals(displayName, formatter.format(1)); + assertEquals(displayName, formatter.format(2)); + } + + @Test + void formatsDisplayNameContainingFormatElements() { + String displayName = "{enigma} {0} '{1}'"; + var formatter = formatter(DISPLAY_NAME_PLACEHOLDER, displayName); + + assertEquals(displayName, formatter.format(1)); + assertEquals(displayName, formatter.format(2)); + } + @Test void formatsInvocationIndex() { var formatter = formatter(INDEX_PLACEHOLDER, "enigma"); diff --git a/junit-jupiter-params/src/test/kotlin/ParameterizedTestNameFormatterIntegrationTests.kt b/junit-jupiter-params/src/test/kotlin/ParameterizedTestNameFormatterIntegrationTests.kt new file mode 100644 index 000000000000..afcfb4bde7a2 --- /dev/null +++ b/junit-jupiter-params/src/test/kotlin/ParameterizedTestNameFormatterIntegrationTests.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2023 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 org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.TestInfo +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class ParameterizedTestNameFormatterIntegrationTests { + + @ValueSource(strings = ["foo", "bar"]) + @ParameterizedTest + fun `implicit'Name`(param: String, info: TestInfo) { + if (param.equals("foo")) { + assertEquals("[1] foo", info.displayName) + } else { + assertEquals("[2] bar", info.displayName) + } + } + + @ValueSource(strings = ["foo", "bar"]) + @ParameterizedTest(name = "{0}") + fun `zero'Only`(param: String, info: TestInfo) { + if (param.equals("foo")) { + assertEquals("foo", info.displayName) + } else { + assertEquals("bar", info.displayName) + } + } + + @ValueSource(strings = ["foo", "bar"]) + @ParameterizedTest(name = "{displayName}") + fun `displayName'Only`(param: String, info: TestInfo) { + assertEquals("displayName'Only(String, TestInfo)", info.displayName) + } + + @ValueSource(strings = ["foo", "bar"]) + @ParameterizedTest(name = "{displayName} - {0}") + fun `displayName'Zero`(param: String, info: TestInfo) { + if (param.equals("foo")) { + assertEquals("displayName'Zero(String, TestInfo) - foo", info.displayName) + } else { + assertEquals("displayName'Zero(String, TestInfo) - bar", info.displayName) + } + } + + @ValueSource(strings = ["foo", "bar"]) + @ParameterizedTest(name = "{0} - {displayName}") + fun `zero'DisplayName`(param: String, info: TestInfo) { + if (param.equals("foo")) { + assertEquals("foo - zero'DisplayName(String, TestInfo)", info.displayName) + } else { + assertEquals("bar - zero'DisplayName(String, TestInfo)", info.displayName) + } + } +}