Skip to content

Commit d118523

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 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 within AbstractTestDescriptor. See #1713 See #4716 Closes #4714
1 parent 99380e0 commit d118523

File tree

7 files changed

+127
-7
lines changed

7 files changed

+127
-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: 23 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;
@@ -90,7 +96,7 @@ protected AbstractTestDescriptor(UniqueId uniqueId, String displayName) {
9096
*/
9197
protected AbstractTestDescriptor(UniqueId uniqueId, String displayName, @Nullable TestSource source) {
9298
this.uniqueId = Preconditions.notNull(uniqueId, "UniqueId must not be null");
93-
this.displayName = Preconditions.notBlank(displayName, "displayName must not be null or blank");
99+
this.displayName = escape(Preconditions.notBlank(displayName, "displayName must not be null or blank"));
94100
this.source = source;
95101
}
96102

@@ -207,4 +213,20 @@ public String toString() {
207213
return getClass().getSimpleName() + ": " + getUniqueId();
208214
}
209215

216+
private static String escape(String text) {
217+
StringBuilder builder = new StringBuilder();
218+
for (int i = 0; i < text.length(); i++) {
219+
builder.append(escape(text.charAt(i)));
220+
}
221+
return builder.toString();
222+
}
223+
224+
private static String escape(char ch) {
225+
return switch (ch) {
226+
case '\r' -> "<CR>";
227+
case '\n' -> "<LF>";
228+
default -> Character.isISOControl(ch) ? UNICODE_REPLACEMENT_CHARACTER : String.valueOf(ch);
229+
};
230+
}
231+
210232
}

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
@@ -348,7 +348,7 @@ void quotedStrings() {
348348
" \\t ", \
349349
"\\b", \
350350
"\\f", \
351-
"?"\
351+
"\u0007"\
352352
""");
353353
}
354354

@@ -370,7 +370,7 @@ void quotedCharacters() {
370370
'\\t', \
371371
'\\b', \
372372
'\\f', \
373-
'?'\
373+
'\u0007'\
374374
""");
375375
}
376376

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+
Stream<String> displayNames = execute("testWithCsvSourceAndSpecialCharacters", String.class, TestInfo.class)//
279+
.testEvents()//
280+
.stream()//
281+
.map(Event::getTestDescriptor)//
282+
.map(TestDescriptor::getDisplayName)//
283+
.distinct()//
284+
// Remove surrounding quotes:
285+
.map(name -> name.substring(1, name.length() - 1));
286+
287+
assertThat(displayNames).containsExactly(//
288+
"üñåé", //
289+
"\\n", //
290+
"\\r", //
291+
"\uFFFD", // Unicode replacement character: �
292+
"😱", //
293+
"Zero\u200BWidth\u200BSpaces"//
294+
);
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, TestInfo testInfo) {
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)