Skip to content

Commit

Permalink
Do not parse {displayName} for @ParameterizedTest using MessageFormat
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alemendoza-v authored and sbrannen committed Apr 24, 2023
1 parent 82f1f4b commit e575ea3
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 2 deletions.
@@ -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

* ❓
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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)) {
Expand Down
Expand Up @@ -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");
Expand Down
@@ -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)
}
}
}

0 comments on commit e575ea3

Please sign in to comment.