Skip to content

Commit

Permalink
Decouple CartesianArgumentsProvider from Jupiter Arguments Provider (#…
Browse files Browse the repository at this point in the history
…520/#519)

The CartesianArgumentsProvider supports the Jupiter AnnotationConsumer interface.
The CartesianArgumentsProvider has a method for providing arguments 
that accepts the ExtensionContext and the Parameter for which it provides arguments.


closes: #520
PR: #519
  • Loading branch information
filiphr committed Oct 7, 2021
1 parent bd6d994 commit 0bee05d
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 76 deletions.
20 changes: 4 additions & 16 deletions docs/cartesian-product.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -345,15 +345,9 @@ import org.junitpioneer.jupiter.cartesian.CartesianArgumentsProvider;
class IntArgumentsProvider implements CartesianArgumentsProvider {
private Ints source;
@Override
void accept(Parameter parameter) {
this.source = Objects.requireNonNull(parameter.getAnnotation(Ints.class));
}
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
public Stream<? extends Arguments> provideArguments(ExtensionContext context, Parameter parameter) {
Ints source = Objects.requireNonNull(parameter.getAnnotation(Ints.class));
return Arrays.stream(source.value()).map(Arguments::of);
}
Expand Down Expand Up @@ -391,15 +385,9 @@ import org.junitpioneer.jupiter.cartesian.CartesianArgumentsProvider;
class PeopleProvider implements CartesianArgumentsProvider {
private People source;
@Override
void accept(Parameter parameter) {
this.source = Objects.requireNonNull(parameter.getAnnotation(People.class));
}
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
public Stream<? extends Arguments> provideArguments(ExtensionContext context, Parameter parameter) {
People source = Objects.requireNonNull(parameter.getAnnotation(People.class));
return IntStream.range(0, source.names().length)
.mapToObj(i -> new Person(source.names()[i], source.ages()[i]))
.map(Arguments::of);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -27,6 +28,7 @@
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junitpioneer.jupiter.cartesian.CartesianArgumentsSource;

/**
* Pioneer-internal utility class to handle annotations.
Expand Down Expand Up @@ -257,12 +259,19 @@ private static <A extends Annotation> Stream<A> findOnOuterClasses(Optional<Clas
}

public static List<? extends Annotation> findParameterArgumentsSources(Method testMethod) {
return Arrays
.stream(testMethod.getParameters())
.map(parameter -> PioneerAnnotationUtils.findAnnotatedAnnotations(parameter, ArgumentsSource.class))
//@formatter:off
return Arrays.stream(testMethod.getParameters())
.map(parameter -> {
List<Annotation> annotations = new ArrayList<>();
AnnotationSupport.findAnnotation(parameter, CartesianArgumentsSource.class)
.ifPresent(annotations::add);
annotations.addAll(AnnotationSupport.findRepeatableAnnotations(parameter, ArgumentsSource.class));
return annotations;
})
.filter(list -> !list.isEmpty())
.map(annotations -> annotations.get(0))
.collect(Collectors.toList());
//@formatter:on
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,29 @@
package org.junitpioneer.jupiter.cartesian;

import java.lang.reflect.Parameter;
import java.util.stream.Stream;

import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;

/**
* If you are implementing an {@link org.junit.jupiter.params.provider.ArgumentsProvider ArgumentsProvider}
* for {@link CartesianTest}, it has to implement this interface <b>instead</b> to know which parameter it provides
* for {@link CartesianTest}, it has to implement this interface <b>as well</b> to know which parameter it provides
* arguments to. For more information, see
* <a href="https://junit-pioneer.org/docs/cartesian-product/" target="_top">the Cartesian product documentation</a>.
*
* @see org.junit.jupiter.params.provider.ArgumentsProvider
* @see CartesianTestExtension
*/
public interface CartesianArgumentsProvider extends ArgumentsProvider {
public interface CartesianArgumentsProvider {

void accept(Parameter parameter);
/**
* Provider a {@link Stream} of {@link Arguments} that needs to be used for the {@code @CartesianTest}.
*
* @param context the current extension context; never {@code null}
* @param parameter the parameter for which the arguments needs to be provided
* @return a stream of arguments; never {@code null}
*/
Stream<? extends Arguments> provideArguments(ExtensionContext context, Parameter parameter) throws Exception;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2016-2021 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
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junitpioneer.jupiter.cartesian;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* {@code @CartesianArgumentsSource} is an annotation
* that is used to register {@linkplain CartesianArgumentsProvider cartesian argument providers}
* for the annotated test parameter.
*
* <p>{@code @CartesianArgumentsSource} may also be used as a meta-annotation in order to
* create a custom <em>composed annotation</em> that inherits the semantics
* of {@code @CartesianArgumentsSource}.
*
* This is similar to {@link org.junit.jupiter.params.provider.ArgumentsSource ArgumentsSource} and is used
* to provide arguments for {@link CartesianTest}.
*
* @see CartesianTest
*/
@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CartesianArgumentsSource {

/**
* The type of {@link CartesianArgumentsProvider} to be used.
*/
Class<? extends CartesianArgumentsProvider> value();

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,28 @@

/**
* This is basically an enhanced copy of Jupiter's {@code EnumArgumentsProvider},
* except it does NOT support {@code @ParameterizedTest} and consumes a {@code Parameter}
* instead of an annotation.
* except it does NOT support {@code @ParameterizedTest} and implements {@link CartesianArgumentsProvider}
* for use with {@code @CartesianTest}.
*
* @implNote This class does not implement {@code ArgumentsProvider} since the Jupiter's {@code EnumSource}
* should be used for that.
*/
class CartesianEnumArgumentsProvider implements CartesianArgumentsProvider {

private CartesianTest.Enum enumSource;
private Class<?> parameterType;

@Override
public void accept(Parameter parameter) {
this.parameterType = parameter.getType();
if (!Enum.class.isAssignableFrom(this.parameterType))
public Stream<? extends Arguments> provideArguments(ExtensionContext context, Parameter parameter) {
Class<?> parameterType = parameter.getType();
if (!Enum.class.isAssignableFrom(parameterType))
throw new PreconditionViolationException(String
.format(
"Parameter of type %s must reference an Enum type (alternatively, use the annotation's 'value' attribute to specify the type explicitly)",
this.parameterType));
this.enumSource = AnnotationSupport
parameterType));
CartesianTest.Enum enumSource = AnnotationSupport
.findAnnotation(parameter, CartesianTest.Enum.class)
.orElseThrow(() -> new PreconditionViolationException(
"Parameter has to be annotated with " + CartesianTest.Enum.class.getName()));
}

@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
Set<? extends Enum<?>> constants = getEnumConstants();
Set<? extends Enum<?>> constants = getEnumConstants(enumSource, parameterType);
CartesianTest.Enum.Mode mode = enumSource.mode();
String[] declaredConstantNames = enumSource.names();
if (declaredConstantNames.length > 0) {
Expand All @@ -63,16 +60,17 @@ public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return constants.stream().map(Arguments::of);
}

private <E extends Enum<E>> Set<? extends E> getEnumConstants() {
Class<E> enumClass = determineEnumClass();
private <E extends Enum<E>> Set<? extends E> getEnumConstants(CartesianTest.Enum enumSource,
Class<?> parameterType) {
Class<E> enumClass = determineEnumClass(enumSource, parameterType);
return EnumSet.allOf(enumClass);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
private <E extends Enum<E>> Class<E> determineEnumClass() {
private <E extends Enum<E>> Class<E> determineEnumClass(CartesianTest.Enum enumSource, Class<?> parameterType) {
Class enumClass = enumSource.value();
if (enumClass.equals(NullEnum.class)) {
enumClass = this.parameterType;
enumClass = parameterType;
}
return enumClass;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.platform.commons.PreconditionViolationException;
import org.junitpioneer.jupiter.cartesian.CartesianEnumArgumentsProvider.NullEnum;

Expand Down Expand Up @@ -85,7 +84,7 @@

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@ArgumentsSource(CartesianValueArgumentsProvider.class)
@CartesianArgumentsSource(CartesianValueArgumentsProvider.class)
@interface Values {

/**
Expand Down Expand Up @@ -142,7 +141,7 @@

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@ArgumentsSource(CartesianEnumArgumentsProvider.class)
@CartesianArgumentsSource(CartesianEnumArgumentsProvider.class)
@interface Enum {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import org.junit.jupiter.api.extension.ExtensionConfigurationException;
Expand All @@ -30,6 +31,7 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.support.AnnotationConsumerInitializer;
import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.support.ReflectionSupport;
Expand Down Expand Up @@ -61,8 +63,8 @@ private CartesianTestNameFormatter createNameFormatter(ExtensionContext context)

private List<List<?>> computeSets(ExtensionContext context) {
Method testMethod = context.getRequiredTestMethod();
List<? extends Annotation> methodArgumentsSources = PioneerAnnotationUtils
.findAnnotatedAnnotations(testMethod, ArgumentsSource.class);
List<? extends Annotation> methodArgumentsSources = AnnotationSupport
.findRepeatableAnnotations(testMethod, ArgumentsSource.class);
List<? extends Annotation> parameterArgumentsSources = PioneerAnnotationUtils
.findParameterArgumentsSources(testMethod);
ensureNoInputConflicts(methodArgumentsSources, parameterArgumentsSources, testMethod);
Expand All @@ -89,33 +91,41 @@ private List<List<?>> getSetsFromArgumentsSources(List<? extends Annotation> arg

private List<Object> getSetFromAnnotation(ExtensionContext context, Annotation source, Parameter parameter) {
try {
CartesianArgumentsProvider provider = initializeArgumentsProvider(source);
CartesianArgumentsProvider provider = initializeArgumentsProvider(source, parameter);
return provideArguments(context, parameter, provider);
}
catch (Exception ex) {
throw new ExtensionConfigurationException("Could not provide arguments because of exception.", ex);
}
}

private CartesianArgumentsProvider initializeArgumentsProvider(Annotation source) {
private CartesianArgumentsProvider initializeArgumentsProvider(Annotation source, Parameter parameter) {
Optional<CartesianArgumentsSource> cartesianProviderAnnotation = AnnotationSupport
.findAnnotation(parameter, CartesianArgumentsSource.class);

if (cartesianProviderAnnotation.isPresent()) {
return AnnotationConsumerInitializer
.initialize(parameter, ReflectionSupport.newInstance(cartesianProviderAnnotation.get().value()));
}

ArgumentsSource providerAnnotation = AnnotationSupport
.findAnnotation(source.annotationType(), ArgumentsSource.class)
.findAnnotation(parameter, ArgumentsSource.class)
// never happens, we already know these annotations are annotated with @ArgumentsSource
.orElseThrow(() -> new PreconditionViolationException(format(
"%s was not annotated with @ArgumentsSource but should have been.", source.annotationType())));
"%s was not annotated with @CartesianArgumentsSource or @ArgumentsSource but should have been.",
source.annotationType())));
ArgumentsProvider provider = ReflectionSupport.newInstance(providerAnnotation.value());
if (provider instanceof CartesianArgumentsProvider)
return (CartesianArgumentsProvider) provider;
return AnnotationConsumerInitializer.initialize(parameter, (CartesianArgumentsProvider) provider);
else
throw new PreconditionViolationException(
format("%s does not implement the CartesianArgumentsProvider interface.", provider.getClass()));
}

private List<Object> provideArguments(ExtensionContext context, Parameter source,
CartesianArgumentsProvider provider) throws Exception {
provider.accept(source);
return provider
.provideArguments(context)
.provideArguments(context, source)
.map(Arguments::get)
.flatMap(Arrays::stream)
.distinct()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,30 @@

import java.lang.reflect.Array;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.support.AnnotationConsumer;
import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.support.AnnotationSupport;

/**
* This is a slightly modified copy of Jupiter's {@code ValueSourceArgumentsProvider},
* except it does NOT support {@code @ParameterizedTest} and can consume a {@code Parameter}
* instead of an annotation.
* This is a slightly modified copy of Jupiter's {@code ValueArgumentsProvider},
* except it does NOT support {@code @ParameterizedTest} and implements {@link CartesianArgumentsProvider}
* for use with {@code @CartesianTest}.
*
* @implNote This class does not implement {@code ArgumentsProvider} since the Jupiter's {@code ValueSource}
* should be used for that.
*/
class CartesianValueArgumentsProvider implements CartesianArgumentsProvider {
class CartesianValueArgumentsProvider implements CartesianArgumentsProvider, AnnotationConsumer<CartesianTest.Values> {

private Object[] arguments;

private void getArgumentsFromAnnotation(CartesianTest.Values source) {
@Override
public void accept(CartesianTest.Values source) {
// @formatter:off
List<Object> arrays =
// Declaration of <Object> is necessary due to a bug in Eclipse Photon.
Expand Down Expand Up @@ -64,17 +69,8 @@ Stream.<Object> of(
}

@Override
public void accept(Parameter parameter) {
CartesianTest.Values source = AnnotationSupport
.findAnnotation(parameter, CartesianTest.Values.class)
.orElseThrow(() -> new PreconditionViolationException(
"Parameter has to be annotated with " + CartesianTest.Values.class.getName()));
getArgumentsFromAnnotation(source);
}

@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(Arguments.of(arguments));
public Stream<? extends Arguments> provideArguments(ExtensionContext context, Parameter parameter) {
return Arrays.stream(arguments).map(Arguments::of);
}

}

0 comments on commit 0bee05d

Please sign in to comment.