From 1cc2b092d82319cc34afe054c33a1d11d1735f1c Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 5 Mar 2025 12:53:45 +0100 Subject: [PATCH] Introduce `ParameterInfo` support for extensions `Extensions` can get it from `ExtensionContext.Store` and access all indexed parameter declarations as well as the arguments for the current invocation. Resolves #1139. --- ...izedInvocationNameFormatterBenchmarks.java | 2 +- .../jupiter/params/DefaultParameterInfo.java | 44 +++++++ .../params/ParameterizedClassContext.java | 25 ++-- .../ParameterizedDeclarationContext.java | 2 + .../ParameterizedInvocationContext.java | 20 +++- .../params/ParameterizedTestContext.java | 9 +- .../params/ParameterizedTestExtension.java | 4 +- .../junit/jupiter/params/ResolverFacade.java | 34 +++--- .../aggregator/DefaultArgumentsAccessor.java | 17 +-- .../converter/DefaultArgumentConverter.java | 21 +--- .../jupiter/params/support/ParameterInfo.java | 85 ++++++++++++++ ...meterizedInvocationNameFormatterTests.java | 3 +- .../params/ParameterizedTestContextTests.java | 2 +- .../ParameterizedTestExtensionTests.java | 9 +- .../DefaultArgumentsAccessorTests.java | 15 +-- .../DefaultArgumentConverterTests.java | 27 +---- .../ParameterInfoIntegrationTests.java | 111 ++++++++++++++++++ .../ArgumentsAccessorKotlinTests.kt | 15 +-- 18 files changed, 333 insertions(+), 112 deletions(-) create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/DefaultParameterInfo.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterInfo.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/params/support/ParameterInfoIntegrationTests.java diff --git a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java index 38bd5694b3d6..592be9f94445 100644 --- a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java +++ b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java @@ -50,7 +50,7 @@ public void formatTestNames(Blackhole blackhole) throws Exception { var method = TestCase.class.getDeclaredMethod("parameterizedTest", int.class); var formatter = new ParameterizedInvocationNameFormatter( DISPLAY_NAME_PLACEHOLDER + " " + DEFAULT_DISPLAY_NAME + " ({0})", "displayName", - new ParameterizedTestContext(method, method.getAnnotation(ParameterizedTest.class)), 512); + new ParameterizedTestContext(TestCase.class, method, method.getAnnotation(ParameterizedTest.class)), 512); for (int i = 0; i < argumentsList.size(); i++) { Arguments arguments = argumentsList.get(i); blackhole.consume(formatter.format(i, EvaluatedArgumentSet.allOf(arguments))); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/DefaultParameterInfo.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/DefaultParameterInfo.java new file mode 100644 index 000000000000..96fffa5039d5 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/DefaultParameterInfo.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.jupiter.params.support.ParameterInfo; + +/** + * @since 5.13 + */ +class DefaultParameterInfo implements ParameterInfo { + + private final ParameterDeclarations declarations; + private final ArgumentsAccessor arguments; + + DefaultParameterInfo(ParameterDeclarations declarations, ArgumentsAccessor arguments) { + this.declarations = declarations; + this.arguments = arguments; + } + + @Override + public ParameterDeclarations getDeclarations() { + return this.declarations; + } + + @Override + public ArgumentsAccessor getArguments() { + return this.arguments; + } + + void store(ExtensionContext context) { + context.getStore(NAMESPACE).put(KEY, this); + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java index a13c583126b5..c6f8d7eaef58 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java @@ -40,7 +40,7 @@ class ParameterizedClassContext implements ParameterizedDeclarationContext { - private final Class clazz; + private final Class testClass; private final ParameterizedClass annotation; private final TestInstance.Lifecycle testInstanceLifecycle; private final ResolverFacade resolverFacade; @@ -48,28 +48,28 @@ class ParameterizedClassContext implements ParameterizedDeclarationContext beforeMethods; private final List afterMethods; - ParameterizedClassContext(Class clazz, ParameterizedClass annotation, + ParameterizedClassContext(Class testClass, ParameterizedClass annotation, TestInstance.Lifecycle testInstanceLifecycle) { - this.clazz = clazz; + this.testClass = testClass; this.annotation = annotation; this.testInstanceLifecycle = testInstanceLifecycle; - List fields = findParameterAnnotatedFields(clazz); + List fields = findParameterAnnotatedFields(testClass); if (fields.isEmpty()) { - this.resolverFacade = ResolverFacade.create(ReflectionUtils.getDeclaredConstructor(clazz), annotation); + this.resolverFacade = ResolverFacade.create(ReflectionUtils.getDeclaredConstructor(testClass), annotation); this.injectionType = InjectionType.CONSTRUCTOR; } else { - this.resolverFacade = ResolverFacade.create(clazz, fields); + this.resolverFacade = ResolverFacade.create(testClass, fields); this.injectionType = InjectionType.FIELDS; } - this.beforeMethods = findLifecycleMethodsAndAssertStaticAndNonPrivate(clazz, testInstanceLifecycle, TOP_DOWN, - BeforeArgumentSet.class, BeforeArgumentSet::injectArguments, this.resolverFacade); + this.beforeMethods = findLifecycleMethodsAndAssertStaticAndNonPrivate(testClass, testInstanceLifecycle, + TOP_DOWN, BeforeArgumentSet.class, BeforeArgumentSet::injectArguments, this.resolverFacade); // Make a local copy since findAnnotatedMethods() returns an immutable list. this.afterMethods = new ArrayList<>( - findLifecycleMethodsAndAssertStaticAndNonPrivate(clazz, testInstanceLifecycle, BOTTOM_UP, + findLifecycleMethodsAndAssertStaticAndNonPrivate(testClass, testInstanceLifecycle, BOTTOM_UP, AfterArgumentSet.class, AfterArgumentSet::injectArguments, this.resolverFacade)); // Since the bottom-up ordering of afterMethods will later be reversed when the @@ -86,6 +86,11 @@ private static List findParameterAnnotatedFields(Class clazz) { return findFields(clazz, it -> isAnnotated(it, Parameter.class), BOTTOM_UP); } + @Override + public Class getTestClass() { + return this.testClass; + } + @Override public ParameterizedClass getAnnotation() { return this.annotation; @@ -93,7 +98,7 @@ public ParameterizedClass getAnnotation() { @Override public Class getAnnotatedElement() { - return this.clazz; + return this.testClass; } @Override diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java index aa532646db6f..11dad62adc72 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java @@ -20,6 +20,8 @@ */ interface ParameterizedDeclarationContext { + Class getTestClass(); + Annotation getAnnotation(); AnnotatedElement getAnnotatedElement(); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java index 88285f29f9f6..207817c69ebb 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java @@ -10,12 +10,17 @@ package org.junit.jupiter.params; +import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader; + import java.util.Arrays; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.aggregator.DefaultArgumentsAccessor; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.support.ParameterDeclarations; class ParameterizedInvocationContext> { @@ -44,7 +49,8 @@ public void prepareInvocation(ExtensionContext context) { if (this.declarationContext.isAutoClosingArguments()) { registerAutoCloseableArgumentsInStoreForClosing(context); } - new ArgumentCountValidator(this.declarationContext, this.arguments).validate(context); + validateArgumentCount(context); + storeParameterInfo(context); } private void registerAutoCloseableArgumentsInStoreForClosing(ExtensionContext context) { @@ -58,6 +64,18 @@ private void registerAutoCloseableArgumentsInStoreForClosing(ExtensionContext co .forEach(closeable -> store.put(argumentIndex.incrementAndGet(), closeable)); } + private void validateArgumentCount(ExtensionContext context) { + new ArgumentCountValidator(this.declarationContext, this.arguments).validate(context); + } + + private void storeParameterInfo(ExtensionContext context) { + ParameterDeclarations declarations = this.declarationContext.getResolverFacade().getIndexedParameterDeclarations(); + ClassLoader classLoader = getClassLoader(this.declarationContext.getTestClass()); + Object[] arguments = this.arguments.getConsumedPayloads(); + ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments); + new DefaultParameterInfo(declarations, accessor).store(context); + } + private static class CloseableArgument implements ExtensionContext.Store.CloseableResource { private final AutoCloseable autoCloseable; diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java index b716d84543a9..50a9417b3a11 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java @@ -24,16 +24,23 @@ */ class ParameterizedTestContext implements ParameterizedDeclarationContext { + private final Class testClass; private final Method method; private final ParameterizedTest annotation; private final ResolverFacade resolverFacade; - ParameterizedTestContext(Method method, ParameterizedTest annotation) { + ParameterizedTestContext(Class testClass, Method method, ParameterizedTest annotation) { + this.testClass = testClass; this.method = Preconditions.notNull(method, "method must not be null"); this.annotation = Preconditions.notNull(annotation, "annotation must not be null"); this.resolverFacade = ResolverFacade.create(method, annotation); } + @Override + public Class getTestClass() { + return this.testClass; + } + @Override public ParameterizedTest getAnnotation() { return this.annotation; 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 56b29245dec0..5097d9f02322 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 @@ -35,8 +35,8 @@ public boolean supportsTestTemplate(ExtensionContext context) { return false; } - ParameterizedTestContext methodContext = new ParameterizedTestContext(context.getRequiredTestMethod(), - annotation.get()); + ParameterizedTestContext methodContext = new ParameterizedTestContext(context.getRequiredTestClass(), + context.getRequiredTestMethod(), annotation.get()); getStore(context).put(DECLARATION_CONTEXT_KEY, methodContext); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java index 7ea70fadf834..a2660bafdf6a 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java @@ -47,7 +47,6 @@ import org.junit.jupiter.params.aggregator.ArgumentsAccessor; import org.junit.jupiter.params.aggregator.ArgumentsAggregationException; import org.junit.jupiter.params.aggregator.ArgumentsAggregator; -import org.junit.jupiter.params.aggregator.DefaultArgumentsAccessor; import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator; import org.junit.jupiter.params.converter.ArgumentConverter; import org.junit.jupiter.params.converter.ConvertWith; @@ -56,6 +55,7 @@ import org.junit.jupiter.params.support.FieldContext; import org.junit.jupiter.params.support.ParameterDeclaration; import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.jupiter.params.support.ParameterInfo; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.function.Try; @@ -457,10 +457,11 @@ private static ParameterResolutionException parameterResolutionException(String private interface Resolver { - Object resolve(ParameterContext parameterContext, int parameterIndex, EvaluatedArgumentSet arguments, - int invocationIndex); + Object resolve(ParameterContext parameterContext, int parameterIndex, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex); - Object resolve(FieldContext fieldContext, EvaluatedArgumentSet arguments, int invocationIndex); + Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, + int invocationIndex); } @@ -475,8 +476,8 @@ private static class Converter implements Resolver { } @Override - public Object resolve(ParameterContext parameterContext, int parameterIndex, EvaluatedArgumentSet arguments, - int invocationIndex) { + public Object resolve(ParameterContext parameterContext, int parameterIndex, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex) { Object argument = arguments.getConsumedPayload(parameterIndex); try { return this.argumentConverter.convert(argument, parameterContext); @@ -487,7 +488,8 @@ public Object resolve(ParameterContext parameterContext, int parameterIndex, Eva } @Override - public Object resolve(FieldContext fieldContext, EvaluatedArgumentSet arguments, int invocationIndex) { + public Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex) { Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex()); try { return this.argumentConverter.convert(argument, fieldContext); @@ -515,10 +517,9 @@ protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetT } @Override - public Object resolve(ParameterContext parameterContext, int parameterIndex, EvaluatedArgumentSet arguments, - int invocationIndex) { - ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(parameterContext, invocationIndex, - arguments.getConsumedPayloads()); + public Object resolve(ParameterContext parameterContext, int parameterIndex, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex) { + ArgumentsAccessor accessor = ParameterInfo.get(extensionContext).getArguments(); try { return this.argumentsAggregator.aggregateArguments(accessor, parameterContext); } @@ -529,9 +530,9 @@ public Object resolve(ParameterContext parameterContext, int parameterIndex, Eva } @Override - public Object resolve(FieldContext fieldContext, EvaluatedArgumentSet arguments, int invocationIndex) { - ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(fieldContext, invocationIndex, - arguments.getConsumedPayloads()); + public Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex) { + ArgumentsAccessor accessor = ParameterInfo.get(extensionContext).getArguments(); try { return this.argumentsAggregator.aggregateArguments(accessor, fieldContext); } @@ -650,7 +651,7 @@ public Optional getParameterName() { @Override public Object resolve(Resolver resolver, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, int invocationIndex, Optional originalParameterContext) { - return resolver.resolve(this, arguments, invocationIndex); + return resolver.resolve(this, extensionContext, arguments, invocationIndex); } } @@ -692,7 +693,8 @@ public Object resolve(Resolver resolver, ExtensionContext extensionContext, Eval ParameterContext parameterContext = originalParameterContext // .filter(it -> it.getParameter().equals(this.parameter)) // .orElseGet(() -> toParameterContext(extensionContext, originalParameterContext)); - return resolver.resolve(parameterContext, getParameterIndex(), arguments, invocationIndex); + return resolver.resolve(parameterContext, getParameterIndex(), extensionContext, arguments, + invocationIndex); } private ParameterContext toParameterContext(ExtensionContext extensionContext, diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java index cdb5c9d0806e..811a8abd0518 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java @@ -19,9 +19,7 @@ import java.util.function.BiFunction; import org.apiguardian.api.API; -import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.converter.DefaultArgumentConverter; -import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.commons.util.Preconditions; @@ -42,20 +40,11 @@ public class DefaultArgumentsAccessor implements ArgumentsAccessor { private final Object[] arguments; private final BiFunction, Object> converter; - public static DefaultArgumentsAccessor create(ParameterContext parameterContext, int invocationIndex, - Object... arguments) { - - Preconditions.notNull(parameterContext, "ParameterContext must not be null"); - BiFunction, Object> converter = (source, targetType) -> DefaultArgumentConverter.INSTANCE // - .convert(source, targetType, parameterContext); - return new DefaultArgumentsAccessor(converter, invocationIndex, arguments); - } - - public static DefaultArgumentsAccessor create(FieldContext fieldContext, int invocationIndex, Object... arguments) { + public static DefaultArgumentsAccessor create(int invocationIndex, ClassLoader classLoader, Object[] arguments) { + Preconditions.notNull(classLoader, "ClassLoader must not be null"); - Preconditions.notNull(fieldContext, "FieldContext must not be null"); BiFunction, Object> converter = (source, targetType) -> DefaultArgumentConverter.INSTANCE // - .convert(source, targetType, fieldContext); + .convert(source, targetType, classLoader); return new DefaultArgumentsAccessor(converter, invocationIndex, arguments); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java index af84d5da36f2..8544019c1894 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java @@ -11,9 +11,9 @@ package org.junit.jupiter.params.converter; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader; import java.io.File; -import java.lang.reflect.Member; import java.math.BigDecimal; import java.math.BigInteger; import java.net.URI; @@ -27,7 +27,6 @@ import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.support.conversion.ConversionException; import org.junit.platform.commons.support.conversion.ConversionSupport; -import org.junit.platform.commons.util.ClassLoaderUtils; import org.junit.platform.commons.util.ReflectionUtils; /** @@ -60,24 +59,18 @@ private DefaultArgumentConverter() { @Override public final Object convert(Object source, ParameterContext context) { Class targetType = context.getParameter().getType(); - return convert(source, targetType, context); + ClassLoader classLoader = getClassLoader(context.getDeclaringExecutable().getDeclaringClass()); + return convert(source, targetType, classLoader); } @Override public final Object convert(Object source, FieldContext context) throws ArgumentConversionException { Class targetType = context.getField().getType(); - return convert(source, targetType, context); + ClassLoader classLoader = getClassLoader(context.getField().getDeclaringClass()); + return convert(source, targetType, classLoader); } - public final Object convert(Object source, Class targetType, ParameterContext context) { - return convert(source, targetType, context.getDeclaringExecutable()); - } - - public final Object convert(Object source, Class targetType, FieldContext context) { - return convert(source, targetType, context.getField()); - } - - private Object convert(Object source, Class targetType, Member member) { + public final Object convert(Object source, Class targetType, ClassLoader classLoader) { if (source == null) { if (targetType.isPrimitive()) { throw new ArgumentConversionException( @@ -91,8 +84,6 @@ private Object convert(Object source, Class targetType, Member member) { } if (source instanceof String) { - Class declaringClass = member.getDeclaringClass(); - ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass); try { return convert((String) source, targetType, classLoader); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterInfo.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterInfo.java new file mode 100644 index 000000000000..03fd6b2a811d --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterInfo.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params.support; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; + +/** + * {@code ParameterInfo} is used to provide information about the current + * invocation of a parameterized class or test. + * + *

Registered {@link Extension} implementations may retrieve the current + * {@code ParameterInfo} instance by calling + * {@link ExtensionContext#getStore(Namespace)} with {@link #NAMESPACE} and + * {@link ExtensionContext.Store#get(Object, Class) Store.get(...)} with + * {@link #KEY}. Alternatively, the {@link #get(ExtensionContext)} method may + * be used to retrieve the {@code ParameterInfo} instance for the supplied + * {@code ExtensionContext}. Extensions must not modify any entries in the + * {@link ExtensionContext.Store Store} for {@link #NAMESPACE}. + * + *

When a {@link ParameterizedTest @ParameterizedTest} method is declared + * inside a {@link ParameterizedClass @ParameterizedClass} or a + * {@link Nested @Nested} {@link ParameterizedClass @ParameterizedClass} is + * declared inside an enclosing {@link ParameterizedClass @ParameterizedClass}, + * there will be multiple {@code ParameterInfo} instances available on different + * levels of the {@link ExtensionContext} hierarchy. In such cases, please use + * {@link ExtensionContext#getParent()} to navigate to the right level before + * retrieving the {@code ParameterInfo} instance from the + * {@link ExtensionContext.Store Store}. + * + * + * @since 5.13 + * @see ParameterizedClass + * @see ParameterizedTest + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface ParameterInfo { + + /** + * The {@link Namespace} for accessing the + * {@link ExtensionContext.Store Store} for {@code ParameterInfo}. + */ + Namespace NAMESPACE = Namespace.create(ParameterInfo.class); + + /** + * The key for retrieving the {@code ParameterInfo} instance from the + * {@link ExtensionContext.Store Store}. + */ + Object KEY = ParameterInfo.class; + + /** + * {@return the closest {@code ParameterInfo} instance for the supplied + * {@code ExtensionContext}; potentially {@code null}} + */ + static ParameterInfo get(ExtensionContext context) { + return context.getStore(NAMESPACE).get(KEY, ParameterInfo.class); + } + + /** + * {@return the declarations of all indexed parameters} + */ + ParameterDeclarations getDeclarations(); + + /** + * {@return the accessor to the arguments of the current invocation} + */ + ArgumentsAccessor getArguments(); + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java index 5214e774ec39..7d48e198dfb2 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java @@ -336,7 +336,8 @@ private static ParameterizedInvocationNameFormatter formatter(String pattern, St } private static ParameterizedInvocationNameFormatter formatter(String pattern, String displayName, Method method) { - var context = new ParameterizedTestContext(method, method.getAnnotation(ParameterizedTest.class)); + var context = new ParameterizedTestContext(method.getDeclaringClass(), method, + method.getAnnotation(ParameterizedTest.class)); return new ParameterizedInvocationNameFormatter(pattern, displayName, context, 512); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java index 36f29fb2e55b..0baf170b2e56 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java @@ -44,7 +44,7 @@ void invalidSignatures(String methodName) { private ParameterizedTestContext createMethodContext(Class testClass, String methodName) { var method = ReflectionUtils.findMethods(testClass, m -> m.getName().equals(methodName)).getFirst(); - return new ParameterizedTestContext(method, method.getAnnotation(ParameterizedTest.class)); + return new ParameterizedTestContext(testClass, method, method.getAnnotation(ParameterizedTest.class)); } @SuppressWarnings("JUnitMalformedDeclaration") 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 5550270a5300..bd0fd97330c9 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 @@ -209,8 +209,8 @@ private ExtensionContext getExtensionContextReturningSingleMethod(Object testCas private ExtensionContext getExtensionContextReturningSingleMethod(Object testCase, Function> configurationSupplier) { - var method = ReflectionUtils.findMethods(testCase.getClass(), - it -> "method".equals(it.getName())).stream().findFirst(); + Class testClass = testCase.getClass(); + var method = ReflectionUtils.findMethods(testClass, it -> "method".equals(it.getName())).stream().findFirst(); return new ExtensionContext() { @@ -253,7 +253,7 @@ public Optional getElement() { @Override public Optional> getTestClass() { - return Optional.empty(); + return Optional.of(testClass); } @Override @@ -302,7 +302,8 @@ public void publishDirectory(String name, ThrowingConsumer action) { public Store getStore(Namespace namespace) { var store = new NamespaceAwareStore(this.store, namespace); method // - .map(it -> new ParameterizedTestContext(it, it.getAnnotation(ParameterizedTest.class))) // + .map(it -> new ParameterizedTestContext(testClass, it, + it.getAnnotation(ParameterizedTest.class))) // .ifPresent(ctx -> store.put(DECLARATION_CONTEXT_KEY, ctx)); return store; } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java index 09359a7b3769..792bae865f48 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java @@ -16,16 +16,11 @@ import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import java.lang.reflect.Method; import java.util.Arrays; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ParameterContext; import org.junit.platform.commons.PreconditionViolationException; -import org.junit.platform.commons.support.ReflectionSupport; /** * Unit tests for {@link DefaultArgumentsAccessor}. @@ -169,14 +164,8 @@ void size() { } private static DefaultArgumentsAccessor defaultArgumentsAccessor(int invocationIndex, Object... arguments) { - return DefaultArgumentsAccessor.create(parameterContext(), invocationIndex, arguments); - } - - private static ParameterContext parameterContext() { - Method declaringExecutable = ReflectionSupport.findMethod(DefaultArgumentsAccessorTests.class, "foo").get(); - ParameterContext parameterContext = mock(); - when(parameterContext.getDeclaringExecutable()).thenReturn(declaringExecutable); - return parameterContext; + var classLoader = DefaultArgumentsAccessorTests.class.getClassLoader(); + return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments); } @SuppressWarnings("unused") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java index b7e70fc1141e..5336690e1c1e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java @@ -16,21 +16,17 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Method; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.commons.support.conversion.ConversionException; import org.junit.platform.commons.test.TestClassLoader; +import org.junit.platform.commons.util.ClassLoaderUtils; /** * Unit tests for {@link DefaultArgumentConverter}. @@ -124,12 +120,12 @@ void delegatesStringToClassWithCustomTypeFromDifferentClassLoaderConversion() th var customType = testClassLoader.loadClass(customTypeName); assertThat(customType.getClassLoader()).isSameAs(testClassLoader); - var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").get(); + var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").orElseThrow(); assertThat(declaringExecutable.getDeclaringClass().getClassLoader()).isSameAs(testClassLoader); doReturn(customType).when(underTest).convert(any(), any(), any(ClassLoader.class)); - var clazz = (Class) convert(customTypeName, Class.class, parameterContext(declaringExecutable)); + var clazz = (Class) convert(customTypeName, Class.class, testClassLoader); assertThat(clazz).isNotEqualTo(Enigma.class); assertThat(clazz).isEqualTo(customType); assertThat(clazz.getClassLoader()).isSameAs(testClassLoader); @@ -151,22 +147,11 @@ private void assertConverts(Object input, Class targetClass, Object expectedO } private Object convert(Object input, Class targetClass) { - return convert(input, targetClass, parameterContext()); - } - - private Object convert(Object input, Class targetClass, ParameterContext parameterContext) { - return underTest.convert(input, targetClass, parameterContext); - } - - private static ParameterContext parameterContext() { - Method declaringExecutable = ReflectionSupport.findMethod(DefaultArgumentConverterTests.class, "foo").get(); - return parameterContext(declaringExecutable); + return convert(input, targetClass, ClassLoaderUtils.getClassLoader(getClass())); } - private static ParameterContext parameterContext(Method declaringExecutable) { - ParameterContext parameterContext = mock(); - when(parameterContext.getDeclaringExecutable()).thenReturn(declaringExecutable); - return parameterContext; + private Object convert(Object input, Class targetClass, ClassLoader classLoader) { + return underTest.convert(input, targetClass, classLoader); } @SuppressWarnings("unused") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/support/ParameterInfoIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/ParameterInfoIntegrationTests.java new file mode 100644 index 000000000000..ea10ff6bd68c --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/ParameterInfoIntegrationTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params.support; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.BeforeContainerTemplateInvocationCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * @since 5.13 + */ +class ParameterInfoIntegrationTests extends AbstractJupiterTestEngineTests { + + @Test + void storesParameterInfoInExtensionContextStoreOnDifferentLevels() { + var results = executeTestsForClass(TestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(7).succeeded(7)); + } + + @ParameterizedClass + @ValueSource(ints = 1) + @ExtendWith(ParameterInfoConsumingExtension.class) + record TestCase(int i) { + + @Nested + @ParameterizedClass + @ValueSource(ints = 2) + class Inner { + + @Parameter + int j; + + @ParameterizedTest + @ValueSource(ints = 3) + void test(int k) { + assertEquals(1, i); + assertEquals(2, j); + assertEquals(3, k); + } + } + } + + private static class ParameterInfoConsumingExtension + implements BeforeContainerTemplateInvocationCallback, BeforeEachCallback { + + @Override + public void beforeContainerTemplateInvocation(ExtensionContext parameterizedClassInvocationContext) { + if (TestCase.Inner.class.equals(parameterizedClassInvocationContext.getRequiredTestClass())) { + assertParameterInfo(parameterizedClassInvocationContext, "j", 2); + + var nestedParameterizedClassContext = parameterizedClassInvocationContext.getParent().orElseThrow(); + assertParameterInfo(nestedParameterizedClassContext, "i", 1); + + parameterizedClassInvocationContext = nestedParameterizedClassContext.getParent().orElseThrow(); + } + + assertParameterInfo(parameterizedClassInvocationContext, "i", 1); + + var outerParameterizedClassContext = parameterizedClassInvocationContext.getParent().orElseThrow(); + assertNull(ParameterInfo.get(outerParameterizedClassContext)); + } + + private static void assertParameterInfo(ExtensionContext context, String parameterName, int argumentValue) { + var parameterInfo = ParameterInfo.get(context); + var declaration = parameterInfo.getDeclarations().get(0).orElseThrow(); + assertEquals(parameterName, declaration.getParameterName().orElseThrow()); + assertEquals(int.class, declaration.getParameterType()); + assertEquals(argumentValue, parameterInfo.getArguments().getInteger(0)); + } + + @Override + public void beforeEach(ExtensionContext parameterizedTestInvocationContext) { + assertParameterInfo(parameterizedTestInvocationContext, "k", 3); + + var parameterizedTestContext = parameterizedTestInvocationContext.getParent().orElseThrow(); + assertParameterInfo(parameterizedTestContext, "j", 2); + + var nestedParameterizedClassInvocationContext = parameterizedTestContext.getParent().orElseThrow(); + assertParameterInfo(nestedParameterizedClassInvocationContext, "j", 2); + + var nestedParameterizedClassContext = nestedParameterizedClassInvocationContext.getParent().orElseThrow(); + assertParameterInfo(nestedParameterizedClassContext, "i", 1); + + var outerParameterizedClassInvocationContext = nestedParameterizedClassContext.getParent().orElseThrow(); + assertParameterInfo(outerParameterizedClassInvocationContext, "i", 1); + + var outerParameterizedClassContext = outerParameterizedClassInvocationContext.getParent().orElseThrow(); + assertNull(ParameterInfo.get(outerParameterizedClassContext)); + } + } +} diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt index a83881e4753a..eb1aa43ed5fb 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt @@ -13,11 +13,6 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ParameterContext -import org.junit.platform.commons.util.ReflectionUtils -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import java.lang.reflect.Method /** * Unit tests for using [ArgumentsAccessor] from Kotlin. @@ -56,13 +51,9 @@ class ArgumentsAccessorKotlinTests { fun defaultArgumentsAccessor( invocationIndex: Int, vararg arguments: Any - ): DefaultArgumentsAccessor = DefaultArgumentsAccessor.create(parameterContext(), invocationIndex, *arguments) - - fun parameterContext(): ParameterContext { - val declaringExecutable: Method = ReflectionUtils.findMethod(DefaultArgumentsAccessorTests::class.java, "foo").get() - val parameterContext: ParameterContext = mock() - `when`(parameterContext.declaringExecutable).thenReturn(declaringExecutable) - return parameterContext + ): DefaultArgumentsAccessor { + val classLoader = ArgumentsAccessorKotlinTests::class.java.classLoader + return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments) } fun foo() {