-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Spike for expression language evaluation in display names #4293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a40da53
e9c1619
ab3ed37
ba7b30c
22c697b
e3963d9
fdbe111
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package org.junit.jupiter.params; | ||
|
||
import org.junit.jupiter.params.provider.Arguments; | ||
|
||
public class ArgumentsContext { | ||
|
||
final int invocationIndex; | ||
final Arguments arguments; | ||
final Object[] consumedArguments; | ||
|
||
ArgumentsContext(int invocationIndex, Arguments arguments, Object[] consumedArguments) { | ||
this.invocationIndex = invocationIndex; | ||
this.arguments = arguments; | ||
this.consumedArguments = consumedArguments; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
|
||
package org.junit.jupiter.params; | ||
|
||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
|
||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface ExpressionLanguage { | ||
|
||
Class<? extends ExpressionLanguageAdapter> value(); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,9 @@ | ||||||||||
|
||||||||||
package org.junit.jupiter.params; | ||||||||||
|
||||||||||
public interface ExpressionLanguageAdapter { | ||||||||||
|
||||||||||
void compile(String template); | ||||||||||
|
||||||||||
void format(Object scope, StringBuffer result); | ||||||||||
Comment on lines
+6
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should split this into two to allow (not necessarily in this PR) registering a global default expression language that would support instantiating the
Suggested change
interface Expression {
void evaluate(Map<String, Object> context, Appendable result);
} |
||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -19,6 +19,7 @@ | |||||||||||||||||||||||||
import static org.junit.jupiter.params.ParameterizedTest.INDEX_PLACEHOLDER; | ||||||||||||||||||||||||||
import static org.junit.platform.commons.util.StringUtils.isNotBlank; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
import java.lang.reflect.InvocationTargetException; | ||||||||||||||||||||||||||
import java.text.FieldPosition; | ||||||||||||||||||||||||||
import java.text.Format; | ||||||||||||||||||||||||||
import java.text.MessageFormat; | ||||||||||||||||||||||||||
|
@@ -27,12 +28,14 @@ | |||||||||||||||||||||||||
import java.util.LinkedHashMap; | ||||||||||||||||||||||||||
import java.util.List; | ||||||||||||||||||||||||||
import java.util.Map; | ||||||||||||||||||||||||||
import java.util.Optional; | ||||||||||||||||||||||||||
import java.util.Set; | ||||||||||||||||||||||||||
import java.util.concurrent.ConcurrentHashMap; | ||||||||||||||||||||||||||
import java.util.concurrent.ConcurrentMap; | ||||||||||||||||||||||||||
import java.util.function.Function; | ||||||||||||||||||||||||||
import java.util.stream.IntStream; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
import org.jetbrains.annotations.NotNull; | ||||||||||||||||||||||||||
import org.junit.jupiter.api.Named; | ||||||||||||||||||||||||||
import org.junit.jupiter.api.extension.ExtensionConfigurationException; | ||||||||||||||||||||||||||
import org.junit.jupiter.params.provider.Arguments; | ||||||||||||||||||||||||||
|
@@ -96,12 +99,12 @@ private PartialFormatter[] parse(String pattern, String displayName, Parameteriz | |||||||||||||||||||||||||
while (isNotBlank(unparsedSegment)) { | ||||||||||||||||||||||||||
PlaceholderPosition position = findFirstPlaceholder(formatters, unparsedSegment); | ||||||||||||||||||||||||||
if (position == null) { | ||||||||||||||||||||||||||
result.add(determineNonPlaceholderFormatter(unparsedSegment, argumentMaxLength)); | ||||||||||||||||||||||||||
result.add(determineNonPlaceholderFormatter(methodContext, unparsedSegment, argumentMaxLength)); | ||||||||||||||||||||||||||
break; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
if (position.index > 0) { | ||||||||||||||||||||||||||
String before = unparsedSegment.substring(0, position.index); | ||||||||||||||||||||||||||
result.add(determineNonPlaceholderFormatter(before, argumentMaxLength)); | ||||||||||||||||||||||||||
result.add(determineNonPlaceholderFormatter(methodContext, before, argumentMaxLength)); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
result.add(formatters.get(position.placeholder)); | ||||||||||||||||||||||||||
unparsedSegment = unparsedSegment.substring(position.index + position.placeholder.length()); | ||||||||||||||||||||||||||
|
@@ -110,6 +113,24 @@ private PartialFormatter[] parse(String pattern, String displayName, Parameteriz | |||||||||||||||||||||||||
return result.toArray(new PartialFormatter[0]); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@NotNull | ||||||||||||||||||||||||||
private static Optional<ExpressionLanguageAdapter> createExpressionLanguageAdapter( | ||||||||||||||||||||||||||
ParameterizedTestMethodContext methodContext, | ||||||||||||||||||||||||||
String segment | ||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||
return methodContext.expressionLanguageAnnotation.map(ExpressionLanguage::value).map(adapterClass -> { | ||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||
ExpressionLanguageAdapter adapterInstance = adapterClass.getDeclaredConstructor().newInstance(); | ||||||||||||||||||||||||||
adapterInstance.compile(segment); | ||||||||||||||||||||||||||
return adapterInstance; | ||||||||||||||||||||||||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { | ||||||||||||||||||||||||||
String message = "Failed to initialize expression language for parameterized test. " | ||||||||||||||||||||||||||
+ "See nested exception for further details."; | ||||||||||||||||||||||||||
throw new JUnitException(message, ex); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
Comment on lines
+122
to
+130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static PlaceholderPosition findFirstPlaceholder(PartialFormatters formatters, String segment) { | ||||||||||||||||||||||||||
if (segment.length() < formatters.minimumPlaceholderLength) { | ||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||
|
@@ -129,10 +150,11 @@ else if (minimum == null || index < minimum.index) { | |||||||||||||||||||||||||
return minimum; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static PartialFormatter determineNonPlaceholderFormatter(String segment, int argumentMaxLength) { | ||||||||||||||||||||||||||
return segment.contains("{") // | ||||||||||||||||||||||||||
? new MessageFormatPartialFormatter(segment, argumentMaxLength) // | ||||||||||||||||||||||||||
: (context, result) -> result.append(segment); | ||||||||||||||||||||||||||
private NonPlaceholderFormatter determineNonPlaceholderFormatter( | ||||||||||||||||||||||||||
ParameterizedTestMethodContext methodContext, String segment, int argumentMaxLength) { | ||||||||||||||||||||||||||
return createExpressionLanguageAdapter(methodContext, segment) | ||||||||||||||||||||||||||
.map(expressionLanguageAdapter -> (NonPlaceholderFormatter) new ExpressionLanguageNonPlaceholderFormatter(expressionLanguageAdapter)) | ||||||||||||||||||||||||||
.orElseGet(() -> new DefaultNonPlaceholderFormatter(segment, argumentMaxLength)); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private PartialFormatters createPartialFormatters(String displayName, ParameterizedTestMethodContext methodContext, | ||||||||||||||||||||||||||
|
@@ -143,15 +165,15 @@ private PartialFormatters createPartialFormatters(String displayName, Parameteri | |||||||||||||||||||||||||
argumentMaxLength)); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
PartialFormatters formatters = new PartialFormatters(); | ||||||||||||||||||||||||||
formatters.put(INDEX_PLACEHOLDER, PartialFormatter.INDEX); | ||||||||||||||||||||||||||
formatters.put(INDEX_PLACEHOLDER, PlaceholderFormatter.INDEX); | ||||||||||||||||||||||||||
formatters.put(DISPLAY_NAME_PLACEHOLDER, (context, result) -> result.append(displayName)); | ||||||||||||||||||||||||||
formatters.put(ARGUMENT_SET_NAME_PLACEHOLDER, PartialFormatter.ARGUMENT_SET_NAME); | ||||||||||||||||||||||||||
formatters.put(ARGUMENT_SET_NAME_PLACEHOLDER, PlaceholderFormatter.ARGUMENT_SET_NAME); | ||||||||||||||||||||||||||
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.arguments instanceof ArgumentSet // | ||||||||||||||||||||||||||
? PartialFormatter.ARGUMENT_SET_NAME // | ||||||||||||||||||||||||||
? PlaceholderFormatter.ARGUMENT_SET_NAME // | ||||||||||||||||||||||||||
: argumentsWithNamesFormatter; | ||||||||||||||||||||||||||
formatterToUse.append(context, result); | ||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||
|
@@ -183,37 +205,29 @@ private static class PlaceholderPosition { | |||||||||||||||||||||||||
|
||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static class ArgumentsContext { | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private final int invocationIndex; | ||||||||||||||||||||||||||
private final Arguments arguments; | ||||||||||||||||||||||||||
private final Object[] consumedArguments; | ||||||||||||||||||||||||||
@FunctionalInterface | ||||||||||||||||||||||||||
private interface PartialFormatter { | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
ArgumentsContext(int invocationIndex, Arguments arguments, Object[] consumedArguments) { | ||||||||||||||||||||||||||
this.invocationIndex = invocationIndex; | ||||||||||||||||||||||||||
this.arguments = arguments; | ||||||||||||||||||||||||||
this.consumedArguments = consumedArguments; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
void append(ArgumentsContext context, StringBuffer result); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@FunctionalInterface | ||||||||||||||||||||||||||
private interface PartialFormatter { | ||||||||||||||||||||||||||
private interface PlaceholderFormatter extends PartialFormatter { | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
PartialFormatter INDEX = (context, result) -> result.append(context.invocationIndex); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
PartialFormatter ARGUMENT_SET_NAME = (context, result) -> { | ||||||||||||||||||||||||||
if (!(context.arguments instanceof ArgumentSet)) { | ||||||||||||||||||||||||||
throw new ExtensionConfigurationException( | ||||||||||||||||||||||||||
String.format("When the display name pattern for a @ParameterizedTest contains %s, " | ||||||||||||||||||||||||||
+ "the arguments must be supplied as an ArgumentSet.", | ||||||||||||||||||||||||||
ARGUMENT_SET_NAME_PLACEHOLDER)); | ||||||||||||||||||||||||||
String.format("When the display name pattern for a @ParameterizedTest contains %s, " | ||||||||||||||||||||||||||
+ "the arguments must be supplied as an ArgumentSet.", | ||||||||||||||||||||||||||
ARGUMENT_SET_NAME_PLACEHOLDER)); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
result.append(((ArgumentSet) context.arguments).getName()); | ||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
void append(ArgumentsContext context, StringBuffer result); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private interface NonPlaceholderFormatter extends PartialFormatter {} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static class MessageFormatPartialFormatter implements PartialFormatter { | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@SuppressWarnings("UnnecessaryUnicodeEscape") | ||||||||||||||||||||||||||
|
@@ -289,4 +303,34 @@ Set<String> placeholders() { | |||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static class DefaultNonPlaceholderFormatter implements NonPlaceholderFormatter { | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private final PartialFormatter delegate; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
public DefaultNonPlaceholderFormatter(String segment, int argumentMaxLength) { | ||||||||||||||||||||||||||
this.delegate = segment.contains("{") // | ||||||||||||||||||||||||||
? new MessageFormatPartialFormatter(segment, argumentMaxLength) // | ||||||||||||||||||||||||||
: (context, result) -> result.append(segment); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||
public void append(ArgumentsContext context, StringBuffer result) { | ||||||||||||||||||||||||||
delegate.append(context, result); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private static class ExpressionLanguageNonPlaceholderFormatter implements NonPlaceholderFormatter { | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
private final ExpressionLanguageAdapter expressionLanguageAdapter; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
public ExpressionLanguageNonPlaceholderFormatter(ExpressionLanguageAdapter expressionLanguageAdapter) { | ||||||||||||||||||||||||||
this.expressionLanguageAdapter = expressionLanguageAdapter; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||
public void append(ArgumentsContext argumentsContext, StringBuffer result) { | ||||||||||||||||||||||||||
expressionLanguageAdapter.format(argumentsContext.arguments.get()[0], result); | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not correct, but I'm unsure how we should name multiple arguments, so that they can be accessed by the EL? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should provide a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't think I would throw an exception. Rather, I would document the behavior, pointing out that source code parameter names are available when compiled with |
||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -25,6 +25,7 @@ dependencies { | |||||
testImplementation(testFixtures(projects.junitJupiterEngine)) | ||||||
testImplementation(testFixtures(projects.junitPlatformLauncher)) | ||||||
testImplementation(testFixtures(projects.junitPlatformReporting)) | ||||||
implementation(libs.mustache) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
tasks { | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.