Skip to content

Commit 3bee7af

Browse files
committed
Replace non-printable characters in display names
Prior to this commit, display names were "sanitized" before they were printed via the ConsoleLauncher (#1713), and text-based arguments in display names for parameterized tests were quoted and escaped (#4716). However, there was still the chance that display names could contain control characters such as CR or LF. To address that, this commit introduces support for automatically replacing non-printable characters in any display name passed to one of the constructors for AbstractTestDescriptor. Doing so automatically covers all display names in JUnit Jupiter, @⁠SuiteDisplayName, and any other test engines that subclass AbstractTestDescriptor, which should cover most common use cases. Specifically, the following replacements are performed. - \r -> <CR> - \n -> <LF> - ISO control character -> � (Unicode replacement character) This commit also removes the special handling of ISO control characters from QuoteUtils, since this is now handled in AbstractTestDescriptor. See #1713 See #4716 Closes #4714
1 parent 8307cd4 commit 3bee7af

File tree

7 files changed

+144
-6
lines changed

7 files changed

+144
-6
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-RC1.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ repository on GitHub.
3030
* Convention-based conversion in `ConversionSupport` now supports factory methods and
3131
factory constructors that accept a single `CharSequence` argument in addition to the
3232
existing support for factories that accept a single `String` argument.
33+
* Non-printable control characters in display names are now replaced with alternative
34+
representations. For example, `\n` is replaced with `<LF>`. This applies to all display
35+
names in JUnit Jupiter, `@SuiteDisplayName`, and any other test engines that subclass
36+
`AbstractTestDescriptor`. Please refer to the
37+
<<../user-guide/index.adoc#writing-tests-display-names, User Guide>> for details.
3338

3439

3540
[[release-notes-6.0.0-RC1-junit-jupiter]]
@@ -60,6 +65,9 @@ repository on GitHub.
6065
Fallback String-to-Object Conversion>> for parameterized tests now supports factory
6166
methods and factory constructors that accept a single `CharSequence` argument in
6267
addition to the existing support for factories that accept a single `String` argument.
68+
* Non-printable control characters in display names are now replaced with alternative
69+
representations. Please refer to the
70+
<<../user-guide/index.adoc#writing-tests-display-names, User Guide>> for details.
6371

6472

6573
[[release-notes-6.0.0-RC1-junit-vintage]]

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,32 @@ by test runners and IDEs.
307307
include::{testDir}/example/DisplayNameDemo.java[tags=user_guide]
308308
----
309309

310+
[NOTE]
311+
====
312+
Control characters in text-based arguments in display names for parameterized tests are
313+
escaped by default. See <<writing-tests-parameterized-tests-display-names-quoted-text>>
314+
for details.
315+
316+
Any remaining ISO control characters in a display name will be replaced as follows.
317+
318+
[cols="25%,15%,60%"]
319+
|===
320+
| Original | Replacement | Description
321+
322+
| ```\r```
323+
| ```<CR>```
324+
| Textual representation of a carriage return
325+
326+
| ```\n```
327+
| ```<LF>```
328+
| Textual representation of a line feed
329+
330+
| Other control character
331+
| ```�```
332+
| Unicode replacement character (U+FFFD)
333+
|===
334+
====
335+
310336
[[writing-tests-display-name-generator]]
311337
==== Display Name Generators
312338

@@ -2778,8 +2804,7 @@ is considered text. A `CharSequence` is wrapped in double quotes (`"`), and a `C
27782804
is wrapped in single quotes (`'`).
27792805

27802806
Special characters will be escaped in the quoted text. For example, carriage returns and
2781-
line feeds will be escaped as `\\r` and `\\n`, respectively. In addition, any ISO control
2782-
character will be represented as a question mark (`?`) in the quoted text.
2807+
line feeds will be escaped as `\\r` and `\\n`, respectively.
27832808

27842809
[TIP]
27852810
====

junit-jupiter-params/src/main/java/org/junit/jupiter/params/QuoteUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ private static String escape(char ch, boolean withinString) {
4848
case '\t' -> "\\t";
4949
case '\r' -> "\\r";
5050
case '\n' -> "\\n";
51-
default -> Character.isISOControl(ch) ? "?" : String.valueOf(ch);
51+
default -> String.valueOf(ch);
5252
};
5353
}
5454

junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/AbstractTestDescriptor.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
@API(status = STABLE, since = "1.0")
4242
public abstract class AbstractTestDescriptor implements TestDescriptor {
4343

44+
/**
45+
* The Unicode replacement character, often displayed as a black diamond with
46+
* a white question mark in it: {@value}
47+
*/
48+
private static final String UNICODE_REPLACEMENT_CHARACTER = "\uFFFD";
49+
4450
private final UniqueId uniqueId;
4551

4652
private final String displayName;
@@ -66,6 +72,10 @@ public abstract class AbstractTestDescriptor implements TestDescriptor {
6672
* Create a new {@code AbstractTestDescriptor} with the supplied
6773
* {@link UniqueId} and display name.
6874
*
75+
* <p>As of JUnit 6.0, ISO control characters in the provided display name
76+
* will be replaced. See {@link #AbstractTestDescriptor(UniqueId, String, TestSource)}
77+
* for details.
78+
*
6979
* @param uniqueId the unique ID of this {@code TestDescriptor}; never
7080
* {@code null}
7181
* @param displayName the display name for this {@code TestDescriptor};
@@ -80,6 +90,17 @@ protected AbstractTestDescriptor(UniqueId uniqueId, String displayName) {
8090
* Create a new {@code AbstractTestDescriptor} with the supplied
8191
* {@link UniqueId}, display name, and source.
8292
*
93+
* <p>As of JUnit 6.0, ISO control characters in the provided display name
94+
* will be replaced according to the following table.
95+
*
96+
* <table class="plain">
97+
* <caption>Control Character Replacement</caption>
98+
* <tr><th> Original </th><th> Replacement </th><th> Description </th></tr>
99+
* <tr><td> {@code \r} </td><td> {@code <CR>} </td><td> Textual representation of a carriage return </td></tr>
100+
* <tr><td> {@code \n} </td><td> {@code <LF>} </td><td> Textual representation of a line feed </td></tr>
101+
* <tr><td> Other control character </td><td> &#xFFFD; </td><td> Unicode replacement character (U+FFFD) </td></tr>
102+
* </table>
103+
*
83104
* @param uniqueId the unique ID of this {@code TestDescriptor}; never
84105
* {@code null}
85106
* @param displayName the display name for this {@code TestDescriptor};
@@ -90,7 +111,7 @@ protected AbstractTestDescriptor(UniqueId uniqueId, String displayName) {
90111
*/
91112
protected AbstractTestDescriptor(UniqueId uniqueId, String displayName, @Nullable TestSource source) {
92113
this.uniqueId = Preconditions.notNull(uniqueId, "UniqueId must not be null");
93-
this.displayName = Preconditions.notBlank(displayName, "displayName must not be null or blank");
114+
this.displayName = escape(Preconditions.notBlank(displayName, "displayName must not be null or blank"));
94115
this.source = source;
95116
}
96117

@@ -207,4 +228,20 @@ public String toString() {
207228
return getClass().getSimpleName() + ": " + getUniqueId();
208229
}
209230

231+
private static String escape(String text) {
232+
StringBuilder builder = new StringBuilder();
233+
for (int i = 0; i < text.length(); i++) {
234+
builder.append(escape(text.charAt(i)));
235+
}
236+
return builder.toString();
237+
}
238+
239+
private static String escape(char ch) {
240+
return switch (ch) {
241+
case '\r' -> "<CR>";
242+
case '\n' -> "<LF>";
243+
default -> Character.isISOControl(ch) ? UNICODE_REPLACEMENT_CHARACTER : String.valueOf(ch);
244+
};
245+
}
246+
210247
}

jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ class QuotedTextTests {
342342
' \t ' -> ' \\t '
343343
'\b' -> \\b
344344
'\f' -> \\f
345-
'\u0007' -> ?
345+
'\u0007' -> '\u0007'
346346
""")
347347
void quotedStrings(String argument, String expected) {
348348
var formatter = formatter(DEFAULT_DISPLAY_NAME, "IGNORED");
@@ -364,7 +364,7 @@ void quotedStrings(String argument, String expected) {
364364
"\t" -> \\t
365365
"\b" -> \\b
366366
"\f" -> \\f
367-
"\u0007" -> ?
367+
"\u0007" -> "\u0007"
368368
""")
369369
void quotedCharacters(char argument, String expected) {
370370
var formatter = formatter(DEFAULT_DISPLAY_NAME, "IGNORED");

jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,30 @@ void executesWithCsvSource() {
270270
.haveExactly(1, event(test(), displayName("[2] argument = bar"), finishedWithFailure(message("bar"))));
271271
}
272272

273+
/**
274+
* @since 6.0
275+
*/
276+
@Test
277+
void executesWithCsvSourceAndSpecialCharacters() {
278+
// @formatter:off
279+
execute("testWithCsvSourceAndSpecialCharacters", String.class)
280+
.testEvents()
281+
.started()
282+
.assertEventsMatchExactly(
283+
displayName(quoted("üñåé")),
284+
displayName(quoted("\\n")),
285+
displayName(quoted("\\r")),
286+
displayName(quoted("\uFFFD")),
287+
displayName(quoted("😱")),
288+
displayName(quoted("Zero\u200BWidth\u200BSpaces"))
289+
);
290+
// @formatter:on
291+
}
292+
293+
private static String quoted(String text) {
294+
return '"' + text + '"';
295+
}
296+
273297
@Test
274298
void executesWithCustomName() {
275299
var results = execute("testWithCustomName", String.class, int.class);
@@ -1453,6 +1477,11 @@ void testWithCsvSource(String argument) {
14531477
fail(argument);
14541478
}
14551479

1480+
@ParameterizedTest(name = "{0}")
1481+
@CsvSource({ "'üñåé'", "'\n'", "'\r'", "'\u0007'", "😱", "'Zero\u200BWidth\u200BSpaces'" })
1482+
void testWithCsvSourceAndSpecialCharacters(String argument) {
1483+
}
1484+
14561485
@ParameterizedTest(quoteTextArguments = false, name = "{0} and {1}")
14571486
@CsvSource({ "foo, 23", "bar, 42" })
14581487
void testWithCustomName(String argument, int i) {

platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/AbstractTestDescriptorTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@
1616
import static org.junit.jupiter.api.Assertions.assertSame;
1717
import static org.junit.jupiter.api.Assertions.assertThrows;
1818
import static org.junit.jupiter.api.Assertions.assertTrue;
19+
import static org.mockito.Mockito.mock;
1920

2021
import java.util.ArrayList;
2122
import java.util.List;
2223
import java.util.concurrent.atomic.AtomicInteger;
2324

2425
import org.junit.jupiter.api.BeforeEach;
2526
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.params.ParameterizedTest;
28+
import org.junit.jupiter.params.provider.CsvSource;
2629
import org.junit.platform.commons.JUnitException;
2730
import org.junit.platform.engine.TestDescriptor;
2831
import org.junit.platform.engine.UniqueId;
@@ -166,6 +169,29 @@ private List<UniqueId> getAncestorsUniqueIds(TestDescriptor descriptor) {
166169
return descriptor.getAncestors().stream().map(TestDescriptor::getUniqueId).toList();
167170
}
168171

172+
@ParameterizedTest(name = "{0} \u27A1 {1}")
173+
// NOTE: "\uFFFD" is the Unicode replacement character: �
174+
@CsvSource(delimiterString = "->", textBlock = """
175+
'carriage \r return' -> 'carriage <CR> return'
176+
'line \n feed' -> 'line <LF> feed'
177+
'form \f feed' -> 'form \uFFFD feed'
178+
'back \b space' -> 'back \uFFFD space'
179+
'tab \t tab' -> 'tab \uFFFD tab'
180+
# Latin-1
181+
'üñåé' -> 'üñåé'
182+
# "hello" in Japanese
183+
'こんにちは' -> 'こんにちは'
184+
# 'hello world' in Thai
185+
'สวัสดีชาวโลก' -> 'สวัสดีชาวโลก'
186+
# bell sound/character
187+
'ding \u0007 dong' -> 'ding \uFFFD dong'
188+
'Munch 😱 emoji' -> 'Munch 😱 emoji'
189+
'Zero\u200BWidth\u200BSpaces' -> 'Zero\u200BWidth\u200BSpaces'
190+
""")
191+
void specialCharactersInDisplayNamesAreEscaped(String input, String expected) {
192+
assertThat(new DemoDescriptor(input).getDisplayName()).isEqualTo(expected);
193+
}
194+
169195
}
170196

171197
class GroupDescriptor extends AbstractTestDescriptor {
@@ -193,3 +219,16 @@ public Type getType() {
193219
}
194220

195221
}
222+
223+
class DemoDescriptor extends AbstractTestDescriptor {
224+
225+
DemoDescriptor(String displayName) {
226+
super(mock(), displayName);
227+
}
228+
229+
@Override
230+
public Type getType() {
231+
return Type.CONTAINER;
232+
}
233+
234+
}

0 commit comments

Comments
 (0)