Skip to content

Commit 83b4b31

Browse files
committed
Replace ISO control 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 ISO control 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 190716b commit 83b4b31

File tree

7 files changed

+145
-6
lines changed

7 files changed

+145
-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
@@ -32,6 +32,11 @@ repository on GitHub.
3232
* Convention-based conversion in `ConversionSupport` now supports factory methods and
3333
factory constructors that accept a single `CharSequence` argument in addition to the
3434
existing support for factories that accept a single `String` argument.
35+
* Non-printable control characters in display names are now replaced with alternative
36+
representations. For example, `\n` is replaced with `<LF>`. This applies to all display
37+
names in JUnit Jupiter, `@SuiteDisplayName`, and any other test engines that subclass
38+
`AbstractTestDescriptor`. Please refer to the
39+
<<../user-guide/index.adoc#writing-tests-display-names, User Guide>> for details.
3540

3641

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

6876

6977
[[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: 39 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,8 @@ 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 = replaceControlCharacters(
115+
Preconditions.notBlank(displayName, "displayName must not be null or blank"));
94116
this.source = source;
95117
}
96118

@@ -207,4 +229,20 @@ public String toString() {
207229
return getClass().getSimpleName() + ": " + getUniqueId();
208230
}
209231

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

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)