Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.junit.jupiter.params;

import static java.lang.Math.min;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENTS_PLACEHOLDER;
Expand All @@ -24,7 +25,6 @@
import java.text.Format;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -58,16 +58,16 @@ static ParameterizedInvocationNameFormatter create(ExtensionContext extensionCon

String name = declarationContext.getDisplayNamePattern();
String pattern = name.equals(DEFAULT_DISPLAY_NAME)
? extensionContext.getConfigurationParameter(DISPLAY_NAME_PATTERN_KEY) //
.orElse(DEFAULT_DISPLAY_NAME_PATTERN)
? extensionContext.getConfigurationParameter(DISPLAY_NAME_PATTERN_KEY).orElse(
DEFAULT_DISPLAY_NAME_PATTERN)
: name;
pattern = Preconditions.notBlank(pattern.strip(),
() -> "Configuration error: @%s on %s must be declared with a non-empty name.".formatted(
declarationContext.getAnnotationName(),
declarationContext.getResolverFacade().getIndexedParameterDeclarations().getSourceElementDescription()));

int argumentMaxLength = extensionContext.getConfigurationParameter(ARGUMENT_MAX_LENGTH_KEY, Integer::parseInt) //
.orElse(512);
int argumentMaxLength = extensionContext.getConfigurationParameter(ARGUMENT_MAX_LENGTH_KEY,
Integer::parseInt).orElse(512);

return new ParameterizedInvocationNameFormatter(pattern, extensionContext.getDisplayName(), declarationContext,
argumentMaxLength);
Expand Down Expand Up @@ -101,7 +101,7 @@ String format(int invocationIndex, EvaluatedArgumentSet arguments) {
private String formatSafely(int invocationIndex, EvaluatedArgumentSet arguments) {
ArgumentsContext context = new ArgumentsContext(invocationIndex, arguments.getConsumedNames(),
arguments.getName());
StringBuffer result = new StringBuffer(); // used instead of StringBuilder so MessageFormat can append directly
StringBuffer result = new StringBuffer();
for (PartialFormatter partialFormatter : this.partialFormatters) {
partialFormatter.append(context, result);
}
Expand Down Expand Up @@ -152,9 +152,8 @@ else if (minimum == null || index < minimum.index) {
}

private static PartialFormatter determineNonPlaceholderFormatter(String segment, int argumentMaxLength) {
return segment.contains("{") //
? new MessageFormatPartialFormatter(segment, argumentMaxLength) //
: (context, result) -> result.append(segment);
return segment.contains("{") ? new MessageFormatPartialFormatter(segment, argumentMaxLength)
: (context, result) -> result.append(replaceNonPrintableCharacters(segment));
}

private PartialFormatters createPartialFormatters(String displayName,
Expand All @@ -169,14 +168,14 @@ private PartialFormatters createPartialFormatters(String displayName,

PartialFormatters formatters = new PartialFormatters();
formatters.put(INDEX_PLACEHOLDER, PartialFormatter.INDEX);
formatters.put(DISPLAY_NAME_PLACEHOLDER, (context, result) -> result.append(displayName));
formatters.put(DISPLAY_NAME_PLACEHOLDER,
(context, result) -> result.append(replaceNonPrintableCharacters(displayName)));
formatters.put(ARGUMENT_SET_NAME_PLACEHOLDER, argumentSetNameFormatter);
formatters.put(ARGUMENTS_WITH_NAMES_PLACEHOLDER, argumentsWithNamesFormatter);
formatters.put(ARGUMENTS_PLACEHOLDER, new CachingByArgumentsLengthPartialFormatter(
length -> new MessageFormatPartialFormatter(argumentsPattern(length), argumentMaxLength)));
formatters.put(ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER, (context, result) -> {
PartialFormatter formatterToUse = context.argumentSetName.isPresent() //
? argumentSetNameFormatter //
PartialFormatter formatterToUse = context.argumentSetName.isPresent() ? argumentSetNameFormatter
: argumentsWithNamesFormatter;
formatterToUse.append(context, result);
});
Expand All @@ -185,16 +184,18 @@ private PartialFormatters createPartialFormatters(String displayName,

private static String argumentsWithNamesPattern(int length, ParameterizedDeclarationContext<?> declarationContext) {
ResolverFacade resolverFacade = declarationContext.getResolverFacade();
return IntStream.range(0, length) //
.mapToObj(index -> resolverFacade.getParameterName(index).map(name -> name + "=").orElse("") + "{"
+ index + "}") //
.collect(joining(", "));
return IntStream.range(0, length).mapToObj(
index -> resolverFacade.getParameterName(index).map(name -> name + "=").orElse("") + "{" + index
+ "}").collect(joining(", "));
}

private static String argumentsPattern(int length) {
return IntStream.range(0, length) //
.mapToObj(index -> "{" + index + "}") //
.collect(joining(", "));
return IntStream.range(0, length).mapToObj(index -> "{" + index + "}").collect(joining(", "));
}

private static String replaceNonPrintableCharacters(String string) {
return string.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t").replaceAll(
"[\\p{Cc}\\p{Cf}\\p{Co}\\p{Cn}]", "?");
}

private record PlaceholderPosition(int index, String placeholder) {
Expand All @@ -206,19 +207,16 @@ private record ArgumentsContext(int invocationIndex, @Nullable Object[] consumed

@FunctionalInterface
private interface PartialFormatter {

PartialFormatter INDEX = (context, result) -> result.append(context.invocationIndex);

void append(ArgumentsContext context, StringBuffer result);

}

private record ArgumentSetNameFormatter(String annotationName) implements PartialFormatter {

@Override
public void append(ArgumentsContext context, StringBuffer result) {
if (context.argumentSetName.isPresent()) {
result.append(context.argumentSetName.get());
result.append(replaceNonPrintableCharacters(context.argumentSetName.get()));
return;
}
throw new ExtensionConfigurationException(
Expand All @@ -228,47 +226,55 @@ public void append(ArgumentsContext context, StringBuffer result) {
}

private static class MessageFormatPartialFormatter implements PartialFormatter {

@SuppressWarnings("UnnecessaryUnicodeEscape")
private static final char ELLIPSIS = '\u2026';

private final MessageFormat messageFormat;
private final int argumentMaxLength;

MessageFormatPartialFormatter(String pattern, int argumentMaxLength) {
this.messageFormat = new MessageFormat(pattern);
this.messageFormat = new MessageFormat(replaceNonPrintableCharacters(pattern));
this.argumentMaxLength = argumentMaxLength;
}

// synchronized because MessageFormat is not thread-safe
@Override
public synchronized void append(ArgumentsContext context, StringBuffer result) {
this.messageFormat.format(makeReadable(context.consumedArguments), result, new FieldPosition(0));
Object[] readableArguments = requireNonNull(makeReadable(context.consumedArguments));
this.messageFormat.format(readableArguments, result, new FieldPosition(0));
}

private @Nullable Object[] makeReadable(@Nullable Object[] arguments) {
@Nullable
Format[] formats = messageFormat.getFormatsByArgumentIndex();
@Nullable
Object[] result = Arrays.copyOf(arguments, Math.min(arguments.length, formats.length), Object[].class);
for (int i = 0; i < result.length; i++) {
if (formats[i] == null) {
result[i] = truncateIfExceedsMaxLength(StringUtils.nullSafeToString(arguments[i]));
}
}
return result;
@Nullable
private Object[] makeReadable(@Nullable Object[] arguments) {
return requireNonNull(makeReadable(arguments, messageFormat.getFormatsByArgumentIndex())).toArray();
}

private @Nullable String truncateIfExceedsMaxLength(@Nullable String argument) {
if (argument != null && argument.length() > this.argumentMaxLength) {
return argument.substring(0, this.argumentMaxLength - 1) + ELLIPSIS;
@Nullable
private List<Object> makeReadable(@Nullable Object[] arguments, Format[] formats) {
return IntStream.range(0, min(arguments.length, formats.length)).mapToObj(
i -> makeReadable(arguments, i, formats[i])).toList();
}

@Nullable
private Object makeReadable(@Nullable Object[] arguments, int index, @Nullable Format format) {
if (arguments == null) {
return "";
}
return argument;
Object arg = arguments[index];
if (format != null) {
return arg;
}
String stringValue = StringUtils.nullSafeToString(arg);
return truncateIfExceedsMaxLength(replaceNonPrintableCharacters(stringValue));
}

private String truncateIfExceedsMaxLength(String argument) {
return argument.length() > this.argumentMaxLength
? argument.substring(0, this.argumentMaxLength - 1) + ELLIPSIS
: argument;
}
}

private static class CachingByArgumentsLengthPartialFormatter implements PartialFormatter {

private final ConcurrentMap<Integer, PartialFormatter> cache = new ConcurrentHashMap<>(1);
private final Function<Integer, PartialFormatter> factory;

Expand All @@ -283,7 +289,6 @@ public void append(ArgumentsContext context, StringBuffer result) {
}

private static class PartialFormatters {

private final Map<String, PartialFormatter> formattersByPlaceholder = new LinkedHashMap<>();
private int minimumPlaceholderLength = Integer.MAX_VALUE;

Expand All @@ -303,5 +308,4 @@ Set<String> placeholders() {
return formattersByPlaceholder.keySet();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,62 @@ void mixedTypesOfArgumentsImplementationsAndCustomDisplayNamePattern() {

}


@Nested
class replacesNonPrintableCharacters {


@Test
void replacesNonPrintableCharactersInArguments() {
var formatter = formatter(ARGUMENTS_PLACEHOLDER, "enigma");

Arguments args = arguments(
"\t", "\r", "\r\n", "\n", "\u200B"
);

assertEquals("\\t, \\r, \\r\\n, \\n, ?", format(formatter, 1, args));
}

@Test
void replacesNonPrintableCharactersInArgumentSetName() {
var formatter = formatter(ARGUMENT_SET_NAME_PLACEHOLDER, "IGNORED");

var formattedName = format(formatter, 1,
argumentSet("Test\tWith\nNewlines", "value"));

assertEquals("Test\\tWith\\nNewlines", formattedName);
}

@Test
void replacesNonPrintableCharactersInDisplayName() {
var formatter = formatter(DISPLAY_NAME_PLACEHOLDER, "Display\tName\nWith\nNewlines");

assertEquals("Display\\tName\\nWith\\nNewlines", format(formatter, 1, arguments()));
}

@Test
void handlesMixedPrintableAndNonPrintableCharacters() {
var formatter = formatter("{0} {1}", "enigma");

Arguments args = arguments(
"Normal\tText", "Another\nLine"
);

assertEquals("Normal\\tText Another\\nLine", format(formatter, 1, args));
}

@Test
void doesNotReplacePrintableUnicodeCharacters() {
var formatter = formatter(ARGUMENTS_PLACEHOLDER, "enigma");

Arguments args = arguments(
"正常", "こんにちは", "안녕하세요"
);

assertEquals("正常, こんにちは, 안녕하세요", format(formatter, 1, args));
}
}

// -------------------------------------------------------------------------

private static ParameterizedInvocationNameFormatter formatter(String pattern, String displayName) {
Expand Down
Loading