diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3bfb3f87128..be1a8450ac7e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,6 +55,7 @@ mavenSurefirePlugin = { module = "org.apache.maven.plugins:maven-surefire-plugin memoryfilesystem = { module = "com.github.marschall:memoryfilesystem", version = "2.8.1" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } +mustache = { module = "com.github.spullara.mustache.java:compiler", version = "0.9.10"} nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version = "0.0.11" } opentest4j = { module = "org.opentest4j:opentest4j", version.ref = "opentest4j" } openTestReporting-cli = { module = "org.opentest4j.reporting:open-test-reporting-cli", version.ref = "openTestReporting" } diff --git a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java index 8c6605490ab8..782d183f01ff 100644 --- a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java +++ b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java @@ -11,6 +11,7 @@ package org.junit.jupiter.params; import java.util.List; +import java.util.Optional; import java.util.stream.IntStream; import org.junit.jupiter.params.provider.Arguments; @@ -47,7 +48,7 @@ public void formatTestNames(Blackhole blackhole) throws Exception { var method = TestCase.class.getDeclaredMethod("parameterizedTest", int.class); var formatter = new ParameterizedTestNameFormatter( ParameterizedTest.DISPLAY_NAME_PLACEHOLDER + " " + ParameterizedTest.DEFAULT_DISPLAY_NAME + " ({0})", - "displayName", new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class)), + "displayName", new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class), Optional.ofNullable(method.getAnnotation(ExpressionLanguage.class))), 512); for (int i = 0; i < argumentsList.size(); i++) { Arguments arguments = argumentsList.get(i); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java index 220825d9817a..4d12fa838fa7 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java @@ -69,7 +69,7 @@ private void validateArgumentCount(ExtensionContext extensionContext, Arguments } private ArgumentCountValidationMode getArgumentCountValidationMode(ExtensionContext extensionContext) { - ParameterizedTest parameterizedTest = methodContext.annotation; + ParameterizedTest parameterizedTest = methodContext.parameterizedTestAnnotation; if (parameterizedTest.argumentCountValidation() != ArgumentCountValidationMode.DEFAULT) { return parameterizedTest.argumentCountValidation(); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentsContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentsContext.java new file mode 100644 index 000000000000..a1320684cd88 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentsContext.java @@ -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; + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExpressionLanguage.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExpressionLanguage.java new file mode 100644 index 000000000000..289df8af5d79 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExpressionLanguage.java @@ -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 value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExpressionLanguageAdapter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExpressionLanguageAdapter.java new file mode 100644 index 000000000000..43a3b5caf8c2 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExpressionLanguageAdapter.java @@ -0,0 +1,9 @@ + +package org.junit.jupiter.params; + +public interface ExpressionLanguageAdapter { + + void compile(String template); + + void format(Object scope, StringBuffer result); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java index 8aed7644f2d8..99eb07c448e5 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java @@ -46,13 +46,16 @@ public boolean supportsTestTemplate(ExtensionContext context) { } Method templateMethod = context.getTestMethod().get(); - Optional annotation = findAnnotation(templateMethod, ParameterizedTest.class); - if (!annotation.isPresent()) { + Optional expressionLanguageAnnotation = findAnnotation(templateMethod, + ExpressionLanguage.class); + Optional parameterizedTestAnnotation = findAnnotation(templateMethod, + ParameterizedTest.class); + if (!parameterizedTestAnnotation.isPresent()) { return false; } ParameterizedTestMethodContext methodContext = new ParameterizedTestMethodContext(templateMethod, - annotation.get()); + parameterizedTestAnnotation.get(), expressionLanguageAnnotation); Preconditions.condition(methodContext.hasPotentiallyValidSignature(), () -> String.format( @@ -86,7 +89,7 @@ public Stream provideTestTemplateInvocationContex return createInvocationContext(formatter, methodContext, arguments, invocationCount.intValue()); }) .onClose(() -> - Preconditions.condition(invocationCount.get() > 0 || methodContext.annotation.allowZeroInvocations(), + Preconditions.condition(invocationCount.get() > 0 || methodContext.parameterizedTestAnnotation.allowZeroInvocations(), "Configuration error: You must configure at least one set of arguments for this @ParameterizedTest")); // @formatter:on } @@ -94,7 +97,7 @@ public Stream provideTestTemplateInvocationContex @Override public boolean mayReturnZeroTestTemplateInvocationContexts(ExtensionContext extensionContext) { ParameterizedTestMethodContext methodContext = getMethodContext(extensionContext); - return methodContext.annotation.allowZeroInvocations(); + return methodContext.parameterizedTestAnnotation.allowZeroInvocations(); } private ParameterizedTestMethodContext getMethodContext(ExtensionContext extensionContext) { @@ -115,7 +118,7 @@ private TestTemplateInvocationContext createInvocationContext(ParameterizedTestN private ParameterizedTestNameFormatter createNameFormatter(ExtensionContext extensionContext, ParameterizedTestMethodContext methodContext) { - String name = methodContext.annotation.name(); + String name = methodContext.parameterizedTestAnnotation.name(); String pattern = name.equals(DEFAULT_DISPLAY_NAME) ? extensionContext.getConfigurationParameter(DISPLAY_NAME_PATTERN_KEY) // .orElse(ParameterizedTest.DEFAULT_DISPLAY_NAME) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java index 4c07e01f81ca..04d4bfdc0772 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java @@ -44,15 +44,19 @@ class ParameterizedTestMethodContext { final Method method; - final ParameterizedTest annotation; + final ParameterizedTest parameterizedTestAnnotation; + final Optional expressionLanguageAnnotation; private final Parameter[] parameters; private final Resolver[] resolvers; private final List resolverTypes; - ParameterizedTestMethodContext(Method method, ParameterizedTest annotation) { + ParameterizedTestMethodContext(Method method, ParameterizedTest parameterizedTestAnnotation, + Optional expressionLanguageAnnotation) { this.method = Preconditions.notNull(method, "method must not be null"); - this.annotation = Preconditions.notNull(annotation, "annotation must not be null"); + this.parameterizedTestAnnotation = Preconditions.notNull(parameterizedTestAnnotation, + "parameterizedTestAnnotation must not be null"); + this.expressionLanguageAnnotation = expressionLanguageAnnotation; this.parameters = method.getParameters(); this.resolvers = new Resolver[this.parameters.length]; this.resolverTypes = new ArrayList<>(this.parameters.length); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java index ecbea24fd04c..1b8674be8335 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java @@ -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 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); + } + }); + } + 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 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); + } + } } diff --git a/jupiter-tests/jupiter-tests.gradle.kts b/jupiter-tests/jupiter-tests.gradle.kts index 061920f4d96d..c3ba633a873f 100644 --- a/jupiter-tests/jupiter-tests.gradle.kts +++ b/jupiter-tests/jupiter-tests.gradle.kts @@ -25,6 +25,7 @@ dependencies { testImplementation(testFixtures(projects.junitJupiterEngine)) testImplementation(testFixtures(projects.junitPlatformLauncher)) testImplementation(testFixtures(projects.junitPlatformReporting)) + implementation(libs.mustache) } tasks { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ExpressionLanguageMustacheTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ExpressionLanguageMustacheTests.java new file mode 100644 index 000000000000..bd32d1a63b35 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ExpressionLanguageMustacheTests.java @@ -0,0 +1,223 @@ + +package org.junit.jupiter.params; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.TestExtensionContext.getExtensionContextReturningSingleMethod; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +public class ExpressionLanguageMustacheTests { + + private final ParameterizedTestExtension parameterizedTestExtension = new ParameterizedTestExtension(); + + @Test + void correctlyComputesDisplayNameTemplateWithoutPlaceholders() { + var testTemplateInvocationContexts = testTemplateInvocationContextsFor(TestCaseTemplateWithoutPlaceholders::new); + assertThat(testTemplateInvocationContexts.get(0).getDisplayName(0)).isEqualTo("foo"); + } + + private List testTemplateInvocationContextsFor(Supplier testCaseFactory) { + var extensionContext = getExtensionContextReturningSingleMethod(testCaseFactory.get()); + this.parameterizedTestExtension.supportsTestTemplate(extensionContext); + return this.parameterizedTestExtension.provideTestTemplateInvocationContexts(extensionContext).toList(); + } + + static class TestCaseTemplateWithoutPlaceholders { + + @ExpressionLanguage(MustacheAdapter.class) + @ParameterizedTest(name = "foo") + @ArgumentsSource(FooArgumentsProvider.class) + void method() { + } + + static class FooArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(arguments(new FooArgument("123", "abc"))); + } + } + } + + @Test + void correctlyComputesDisplayNameTemplateSimplePlaceholder() { + var testTemplateInvocationContexts = testTemplateInvocationContextsFor(TestCaseSimplePlaceholder::new); + assertThat(testTemplateInvocationContexts.get(0).getDisplayName(0)).isEqualTo("foo 123"); + assertThat(testTemplateInvocationContexts.get(1).getDisplayName(0)).isEqualTo("foo 456"); + } + + static class TestCaseSimplePlaceholder { + + @ExpressionLanguage(MustacheAdapter.class) + @ParameterizedTest(name = "foo {{bar}}") + @ArgumentsSource(FooArgumentsProvider.class) + void method() { + } + + static class FooArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(arguments(new FooArgument("123", "abc")), + arguments(new FooArgument("456", "def"))); + } + } + } + + @Test + void correctlyComputesDisplayNameTemplateInvalidPlaceholder() { + var testTemplateInvocationContexts = testTemplateInvocationContextsFor(TestCaseInvalidPlaceholder::new); + assertThat(testTemplateInvocationContexts.get(0).getDisplayName(0)).isEqualTo("foo "); + } + + static class TestCaseInvalidPlaceholder { + + @ExpressionLanguage(MustacheAdapter.class) + @ParameterizedTest(name = "foo {{barbaz}}") + @ArgumentsSource(FooArgumentsProvider.class) + void method() { + } + + static class FooArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(arguments(new FooArgument("123", "abc"))); + } + } + } + + @Test + void correctlyComputesDisplayNameTemplateMultiplePlaceholders() { + var testTemplateInvocationContexts = testTemplateInvocationContextsFor(TestCaseMultiplePlaceholders::new); + assertThat(testTemplateInvocationContexts.get(0).getDisplayName(0)).isEqualTo("foo 123 abc foo"); + } + + static class TestCaseMultiplePlaceholders { + + @ExpressionLanguage(MustacheAdapter.class) + @ParameterizedTest(name = "foo {{bar}} {{baz}} foo") + @ArgumentsSource(FooArgumentsProvider.class) + void method() { + } + + static class FooArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(arguments(new FooArgument("123", "abc"))); + } + } + } + + @Test + void correctlyComputesDisplayNameTemplateNestedPlaceholders() { + var testTemplateInvocationContexts = testTemplateInvocationContextsFor(TestCaseNestedPlaceholders::new); + assertThat(testTemplateInvocationContexts.get(0).getDisplayName(0)).isEqualTo("123: abc"); + } + + static class TestCaseNestedPlaceholders { + + @ExpressionLanguage(MustacheAdapter.class) + @ParameterizedTest(name = "{{foo.bar}}: {{foo.baz}}") + @ArgumentsSource(FooArgumentsProvider.class) + void method() { + } + + static class FooArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(arguments(new FooArgumentWrapper(new FooArgument("123", "abc")))); + } + } + } + + @Test + void correctlyComputesDisplayNameTemplatePlaceholderList() { + var testTemplateInvocationContexts = testTemplateInvocationContextsFor(TestCasePlaceholderList::new); + assertThat(testTemplateInvocationContexts.get(0).getDisplayName(0)).isEqualTo("(123, abc)(456, def)"); + } + + static class TestCasePlaceholderList { + + @ExpressionLanguage(MustacheAdapter.class) + @ParameterizedTest(name = "{{#foos}}({{bar}}, {{baz}}){{/foos}}") + @ArgumentsSource(FooArgumentsProvider.class) + void method() { + } + + static class FooArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(arguments(new FooArgumentList(new FooArgument("123", "abc"), new FooArgument("456", "def")))); + } + } + } + + static class FooArgument { + String bar; + String baz; + + public FooArgument(String bar, String baz) { + this.bar = bar; + this.baz = baz; + } + } + + static class FooArgumentWrapper { + FooArgument foo; + + public FooArgumentWrapper(FooArgument foo) { + this.foo = foo; + } + } + + static class FooArgumentList { + List foos; + + public FooArgumentList(FooArgument... foos) { + this.foos = Arrays.asList(foos); + } + } + + static class MustacheAdapter implements ExpressionLanguageAdapter { + + MustacheFactory mustacheFactory; + Mustache mustache; + + MustacheAdapter() { + mustacheFactory = new DefaultMustacheFactory(); + } + + @Override + public void compile(String template) { + mustache = mustacheFactory.compile(new StringReader(template), template); + } + + @Override + public void format(Object scope, StringBuffer result) { + StringWriter stringWriter = new StringWriter(); + mustache.execute(stringWriter, scope); + result.append(stringWriter); + } + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java index 638cdbf95e1f..eaa8c43f4327 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java @@ -15,36 +15,22 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.params.ParameterizedTestExtension.METHOD_CONTEXT_KEY; import static org.junit.jupiter.params.ParameterizedTestExtension.arguments; +import static org.junit.jupiter.params.TestExtensionContext.getExtensionContextReturningSingleMethod; import java.io.FileNotFoundException; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.nio.file.Path; -import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.junit.jupiter.api.extension.ExecutableInvoker; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.TestInstances; -import org.junit.jupiter.api.function.ThrowingConsumer; -import org.junit.jupiter.api.parallel.ExecutionMode; -import org.junit.jupiter.engine.execution.NamespaceAwareStore; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; -import org.junit.platform.commons.util.ReflectionUtils; -import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; /** * Unit tests for {@link ParameterizedTestExtension}. @@ -189,129 +175,6 @@ void throwsExceptionWhenArgumentsProviderDoesNotContainUnambiguousConstructor() className)); } - private ExtensionContext getExtensionContextReturningSingleMethod(Object testCase) { - return getExtensionContextReturningSingleMethod(testCase, ignored -> Optional.empty()); - } - - private ExtensionContext getExtensionContextReturningSingleMethod(Object testCase, - Function> configurationSupplier) { - - var method = ReflectionUtils.findMethods(testCase.getClass(), - it -> "method".equals(it.getName())).stream().findFirst(); - - return new ExtensionContext() { - - private final NamespacedHierarchicalStore store = new NamespacedHierarchicalStore<>(null); - - @Override - public Optional getTestMethod() { - return method; - } - - @Override - public Optional getParent() { - return Optional.empty(); - } - - @Override - public ExtensionContext getRoot() { - return this; - } - - @Override - public String getUniqueId() { - return null; - } - - @Override - public String getDisplayName() { - return null; - } - - @Override - public Set getTags() { - return null; - } - - @Override - public Optional getElement() { - return Optional.empty(); - } - - @Override - public Optional> getTestClass() { - return Optional.empty(); - } - - @Override - public Optional getTestInstanceLifecycle() { - return Optional.empty(); - } - - @Override - public java.util.Optional getTestInstance() { - return Optional.empty(); - } - - @Override - public Optional getTestInstances() { - return Optional.empty(); - } - - @Override - public Optional getExecutionException() { - return Optional.empty(); - } - - @Override - public Optional getConfigurationParameter(String key) { - return configurationSupplier.apply(key); - } - - @Override - public Optional getConfigurationParameter(String key, Function transformer) { - return configurationSupplier.apply(key).map(transformer); - } - - @Override - public void publishReportEntry(Map map) { - } - - @Override - public void publishFile(String fileName, ThrowingConsumer action) { - } - - @Override - public Store getStore(Namespace namespace) { - var store = new NamespaceAwareStore(this.store, namespace); - method // - .map(it -> new ParameterizedTestMethodContext(it, it.getAnnotation(ParameterizedTest.class))) // - .ifPresent(ctx -> store.put(METHOD_CONTEXT_KEY, ctx)); - return store; - } - - @Override - public ExecutionMode getExecutionMode() { - return ExecutionMode.SAME_THREAD; - } - - @Override - public ExecutableInvoker getExecutableInvoker() { - return new ExecutableInvoker() { - @Override - public Object invoke(Method method, Object target) { - return null; - } - - @Override - public T invoke(Constructor constructor, Object outerInstance) { - return ReflectionUtils.newInstance(constructor); - } - }; - } - }; - } - static class TestCaseWithoutMethod { } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestMethodContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestMethodContextTests.java index e2ce097ca4c5..65414d0c0ba1 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestMethodContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestMethodContextTests.java @@ -13,6 +13,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Optional; + import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.params.aggregator.AggregatorIntegrationTests.CsvToPerson; import org.junit.jupiter.params.aggregator.AggregatorIntegrationTests.Person; @@ -42,7 +44,8 @@ void invalidSignatures(String methodName) { private ParameterizedTestMethodContext createMethodContext(Class testClass, String methodName) { var method = ReflectionUtils.findMethods(testClass, m -> m.getName().equals(methodName)).getFirst(); - return new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class)); + return new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class), + Optional.ofNullable(method.getAnnotation(ExpressionLanguage.class))); } @SuppressWarnings("JUnitMalformedDeclaration") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java index c95332c171bb..c3396b99975b 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java @@ -35,6 +35,7 @@ import java.time.ZoneId; import java.util.Arrays; import java.util.Locale; +import java.util.Optional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; @@ -331,7 +332,8 @@ private static ParameterizedTestNameFormatter formatter(String pattern, String d } private static ParameterizedTestNameFormatter formatter(String pattern, String displayName, Method method) { - var context = new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class)); + var context = new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class), + Optional.ofNullable(method.getAnnotation(ExpressionLanguage.class))); return new ParameterizedTestNameFormatter(pattern, displayName, context, 512); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/TestExtensionContext.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/TestExtensionContext.java new file mode 100644 index 000000000000..31a222e77740 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/TestExtensionContext.java @@ -0,0 +1,153 @@ + +package org.junit.jupiter.params; + +import static org.junit.jupiter.params.ParameterizedTestExtension.METHOD_CONTEXT_KEY; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExecutableInvoker; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstances; +import org.junit.jupiter.api.function.ThrowingConsumer; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.engine.execution.NamespaceAwareStore; +import org.junit.platform.commons.util.ReflectionUtils; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; + +public class TestExtensionContext { + + private TestExtensionContext() { + } + + static ExtensionContext getExtensionContextReturningSingleMethod(Object testCase) { + return getExtensionContextReturningSingleMethod(testCase, ignored -> Optional.empty()); + } + + static ExtensionContext getExtensionContextReturningSingleMethod(Object testCase, + Function> configurationSupplier) { + + var method = ReflectionUtils.findMethods(testCase.getClass(), + it -> "method".equals(it.getName())).stream().findFirst(); + + return new ExtensionContext() { + + private final NamespacedHierarchicalStore store = new NamespacedHierarchicalStore<>(null); + + @Override + public Optional getTestMethod() { + return method; + } + + @Override + public Optional getParent() { + return Optional.empty(); + } + + @Override + public ExtensionContext getRoot() { + return this; + } + + @Override + public String getUniqueId() { + return null; + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public Set getTags() { + return null; + } + + @Override + public Optional getElement() { + return Optional.empty(); + } + + @Override + public Optional> getTestClass() { + return Optional.empty(); + } + + @Override + public Optional getTestInstanceLifecycle() { + return Optional.empty(); + } + + @Override + public java.util.Optional getTestInstance() { + return Optional.empty(); + } + + @Override + public Optional getTestInstances() { + return Optional.empty(); + } + + @Override + public Optional getExecutionException() { + return Optional.empty(); + } + + @Override + public Optional getConfigurationParameter(String key) { + return configurationSupplier.apply(key); + } + + @Override + public Optional getConfigurationParameter(String key, Function transformer) { + return configurationSupplier.apply(key).map(transformer); + } + + @Override + public void publishReportEntry(Map map) { + } + + @Override + public void publishFile(String fileName, ThrowingConsumer action) { + } + + @Override + public Store getStore(Namespace namespace) { + var store = new NamespaceAwareStore(this.store, namespace); + method // + .map(it -> new ParameterizedTestMethodContext(it, it.getAnnotation(ParameterizedTest.class), + Optional.ofNullable(it.getAnnotation(ExpressionLanguage.class)))) // + .ifPresent(ctx -> store.put(METHOD_CONTEXT_KEY, ctx)); + return store; + } + + @Override + public ExecutionMode getExecutionMode() { + return ExecutionMode.SAME_THREAD; + } + + @Override + public ExecutableInvoker getExecutableInvoker() { + return new ExecutableInvoker() { + @Override + public Object invoke(Method method, Object target) { + return null; + } + + @Override + public T invoke(Constructor constructor, Object outerInstance) { + return ReflectionUtils.newInstance(constructor); + } + }; + } + }; + } +}