Skip to content

Commit 87eb2f6

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 c613755 commit 87eb2f6

File tree

7 files changed

+142
-7
lines changed

7 files changed

+142
-7
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ repository on GitHub.
2626
[[release-notes-6.0.0-RC1-junit-platform-new-features-and-improvements]]
2727
==== New Features and Improvements
2828

29-
* ❓
29+
* Non-printable control characters in display names are now replaced with alternative
30+
representations. For example, `\n` is replaced with `<LF>`. This applies to all display
31+
names in JUnit Jupiter, `@SuiteDisplayName`, and any other test engines that subclass
32+
`AbstractTestDescriptor`. Please refer to the
33+
<<../user-guide/index.adoc#writing-tests-display-names, User Guide>> for details.
3034

3135

3236
[[release-notes-6.0.0-RC1-junit-jupiter]]
@@ -53,6 +57,9 @@ repository on GitHub.
5357
In addition, special characters are escaped within quoted text. Please refer to the
5458
<<../user-guide/index.adoc#writing-tests-parameterized-tests-display-names-quoted-text,
5559
User Guide>> for details.
60+
* Non-printable control characters in display names are now replaced with alternative
61+
representations. Please refer to the
62+
<<../user-guide/index.adoc#writing-tests-display-names, User Guide>> for details.
5663

5764

5865
[[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
@@ -312,6 +312,32 @@ by test runners and IDEs.
312312
include::{testDir}/example/DisplayNameDemo.java[tags=user_guide]
313313
----
314314

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

@@ -2793,8 +2819,7 @@ is considered text. A `CharSequence` is wrapped in double quotes (`"`), and a `C
27932819
is wrapped in single quotes (`'`).
27942820

27952821
Special characters will be escaped in the quoted text. For example, carriage returns and
2796-
line feeds will be escaped as `\\r` and `\\n`, respectively. In addition, any ISO control
2797-
character will be represented as a question mark (`?`) in the quoted text.
2822+
line feeds will be escaped as `\\r` and `\\n`, respectively.
27982823

27992824
[TIP]
28002825
====

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);
@@ -1442,6 +1466,11 @@ void testWithCsvSource(String argument) {
14421466
fail(argument);
14431467
}
14441468

1469+
@ParameterizedTest(name = "{0}")
1470+
@CsvSource({ "'üñåé'", "'\n'", "'\r'", "'\u0007'", "😱", "'Zero\u200BWidth\u200BSpaces'" })
1471+
void testWithCsvSourceAndSpecialCharacters(String argument) {
1472+
}
1473+
14451474
@ParameterizedTest(quoteTextArguments = false, name = "{0} and {1}")
14461475
@CsvSource({ "foo, 23", "bar, 42" })
14471476
void testWithCustomName(String argument, int i) {

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

Lines changed: 37 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,27 @@ private List<UniqueId> getAncestorsUniqueIds(TestDescriptor descriptor) {
166169
return descriptor.getAncestors().stream().map(TestDescriptor::getUniqueId).toList();
167170
}
168171

172+
@ParameterizedTest
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+
# bell sound/character
185+
'ding \u0007 dong' -> 'ding \uFFFD dong'
186+
'Munch 😱 emoji' -> 'Munch 😱 emoji'
187+
'Zero\u200BWidth\u200BSpaces' -> 'Zero\u200BWidth\u200BSpaces'
188+
""")
189+
void specialCharactersInDisplayNamesAreEscaped(String input, String expected) {
190+
assertThat(new DemoDescriptor(input).getDisplayName()).isEqualTo(expected);
191+
}
192+
169193
}
170194

171195
class GroupDescriptor extends AbstractTestDescriptor {
@@ -193,3 +217,16 @@ public Type getType() {
193217
}
194218

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

0 commit comments

Comments
 (0)