aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java')
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java1382
1 files changed, 1382 insertions, 0 deletions
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
new file mode 100644
index 0000000..4380f57
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
@@ -0,0 +1,1382 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.toCollection;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.auto.value.AutoAnnotation;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Primitives;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter;
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Predicate;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.junit.runner.Description;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
+
+/**
+ * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with
+ * {@link TestParameterAnnotation}.
+ *
+ * @see TestParameterAnnotation
+ */
+class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
+
+ /**
+ * Class to hold an annotation type and origin and one of the values as returned by the {@code
+ * value()} method.
+ */
+ @AutoValue
+ abstract static class TestParameterValue implements Serializable {
+
+ private static final long serialVersionUID = -6491624726743872379L;
+
+ /**
+ * Annotation type and origin of the annotation annotated with {@link TestParameterAnnotation}.
+ */
+ abstract AnnotationTypeOrigin annotationTypeOrigin();
+
+ /**
+ * The value used for the test as returned by the @TestParameterAnnotation annotated
+ * annotation's {@code value()} method (e.g. 'true' or 'false' in the case of a Boolean
+ * parameter).
+ */
+ @Nullable
+ abstract Object value();
+
+ /** The index of this value in {@link #specifiedValues()}. */
+ abstract int valueIndex();
+
+ /**
+ * The list of values specified by the @TestParameterAnnotation annotated annotation's {@code
+ * value()} method (e.g. {true, false} in the case of a boolean parameter).
+ */
+ @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values
+ abstract List<Object> specifiedValues();
+
+ /**
+ * The class of the parameter or field that is being annotated. In case the annotation is
+ * annotating a method, constructor or class, {@code paramClass} is an absent optional.
+ */
+ abstract Optional<Class<?>> paramClass();
+
+ /**
+ * The name of the parameter or field that is being annotated. In case the annotation is
+ * annotating a method, constructor or class, {@code paramName} is an absent optional.
+ */
+ abstract Optional<String> paramName();
+
+ /**
+ * Returns a String that represents this value and is fit for use in a test name (between
+ * brackets).
+ */
+ String toTestNameString() {
+ Class<? extends Annotation> annotationType = annotationTypeOrigin().annotationType();
+ String namePattern = annotationType.getAnnotation(TestParameterAnnotation.class).name();
+
+ if (paramName().isPresent()
+ && paramClass().isPresent()
+ && namePattern.equals("{0}")
+ && Primitives.unwrap(paramClass().get()).isPrimitive()) {
+ // If no custom name pattern was set and this parameter is a primitive (e.g.
+ // boolean
+ // or integer), prefix the parameter value with its field name. This is to avoid
+ // test names such as myMethod_success[true,false,2]. Instead, it'll be
+ // myMethod_success[dryRun=true,experimentFlag=false,retries=2].
+ return String.format("%s=%s", paramName().get(), value()).trim().replaceAll("\\s+", " ");
+ } else {
+ return MessageFormat.format(namePattern, value()).trim().replaceAll("\\s+", " ");
+ }
+ }
+
+ public static ImmutableList<TestParameterValue> create(
+ AnnotationWithMetadata annotationWithMetadata, Origin origin) {
+ List<Object> specifiedValues = getParametersAnnotationValues(annotationWithMetadata);
+ checkState(
+ !specifiedValues.isEmpty(),
+ "The number of parameter values should not be 0"
+ + ", otherwise the parameter would cause the test to be skipped.");
+ return IntStream.range(0, specifiedValues.size())
+ .mapToObj(
+ valueIndex ->
+ new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValue(
+ AnnotationTypeOrigin.create(
+ annotationWithMetadata.annotation().annotationType(), origin),
+ specifiedValues.get(valueIndex),
+ valueIndex,
+ new ArrayList<>(specifiedValues),
+ annotationWithMetadata.paramClass(),
+ annotationWithMetadata.paramName()))
+ .collect(toImmutableList());
+ }
+ }
+ /**
+ * Returns a {@link TestParameterValues} for retrieving the {@link TestParameterAnnotation}
+ * annotation values for a the {@code testInfo}.
+ */
+ public static TestParameterValues getTestParameterValues(TestInfo testInfo) {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ if (testIndexHolder == null) {
+ return annotationType -> Optional.absent();
+ } else {
+ return annotationType ->
+ Optional.fromNullable(
+ new TestParameterAnnotationMethodProcessor(
+ new TestClass(testInfo.getMethod().getDeclaringClass()),
+ /* onlyForFieldsAndParameters= */ false)
+ .getParameterValuesForTest(testIndexHolder).stream()
+ .filter(matches(annotationType))
+ .map(TestParameterValue::value)
+ .findFirst()
+ .orElse(null));
+ }
+ }
+
+ /**
+ * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code
+ * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found.
+ */
+ public static Optional<Object> getTestParameterValue(
+ TestInfo testInfo, Class<? extends Annotation> annotationType) {
+ return getTestParameterValues(testInfo).getValue(annotationType);
+ }
+
+ private static List<Object> getParametersAnnotationValues(
+ AnnotationWithMetadata annotationWithMetadata) {
+ Annotation annotation = annotationWithMetadata.annotation();
+ TestParameterAnnotation testParameter =
+ annotation.annotationType().getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider();
+ try {
+ return valueProvider
+ .getConstructor()
+ .newInstance()
+ .provideValues(
+ annotation,
+ java.util.Optional.ofNullable(annotationWithMetadata.paramClass().orNull()));
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(
+ "Unexpected exception while invoking value provider " + valueProvider, e);
+ }
+ }
+
+ private static Predicate<TestParameterValue> matches(Class<? extends Annotation> annotationType) {
+ return testParameterValue ->
+ testParameterValue.annotationTypeOrigin().annotationType().equals(annotationType);
+ }
+
+ /** The origin of an annotation type. */
+ enum Origin {
+ CLASS,
+ FIELD,
+ METHOD,
+ METHOD_PARAMETER,
+ CONSTRUCTOR,
+ CONSTRUCTOR_PARAMETER,
+ }
+
+ /** Class to hold an annotation type and the element where it was declared. */
+ @AutoValue
+ abstract static class AnnotationTypeOrigin implements Serializable {
+
+ private static final long serialVersionUID = 4909750539931241385L;
+
+ /** Annotation type of the @TestParameterAnnotation annotated annotation. */
+ abstract Class<? extends Annotation> annotationType();
+
+ /** Where the annotation was declared. */
+ abstract Origin origin();
+
+ public static AnnotationTypeOrigin create(
+ Class<? extends Annotation> annotationType, Origin origin) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationTypeOrigin(
+ annotationType, origin);
+ }
+
+ @Override
+ public final String toString() {
+ return annotationType().getSimpleName() + ":" + origin();
+ }
+ }
+
+ /** Class to hold an annotation type and metadata about the annotated parameter. */
+ @AutoValue
+ abstract static class AnnotationWithMetadata implements Serializable {
+
+ /**
+ * The annotation whose interface is itself annotated by the @TestParameterAnnotation
+ * annotation.
+ */
+ abstract Annotation annotation();
+
+ /**
+ * The class of the parameter or field that is being annotated. In case the annotation is
+ * annotating a method, constructor or class, {@code paramClass} is an absent optional.
+ */
+ abstract Optional<Class<?>> paramClass();
+
+ /**
+ * The name of the parameter or field that is being annotated. In case the annotation is
+ * annotating a method, constructor or class, {@code paramName} is an absent optional.
+ */
+ abstract Optional<String> paramName();
+
+ public static AnnotationWithMetadata withMetadata(
+ Annotation annotation, Class<?> paramClass, String paramName) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.of(paramClass), Optional.of(paramName));
+ }
+
+ public static AnnotationWithMetadata withMetadata(Annotation annotation, Class<?> paramClass) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.of(paramClass), Optional.absent());
+ }
+
+ public static AnnotationWithMetadata withoutMetadata(Annotation annotation) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.absent(), Optional.absent());
+ }
+ }
+
+ private final TestClass testClass;
+ private final boolean onlyForFieldsAndParameters;
+ private volatile ImmutableList<AnnotationTypeOrigin> cachedAnnotationTypeOrigins;
+ private final Cache<Method, List<List<TestParameterValue>>> parameterValuesCache =
+ CacheBuilder.newBuilder().maximumSize(1000).build();
+
+ private TestParameterAnnotationMethodProcessor(
+ TestClass testClass, boolean onlyForFieldsAndParameters) {
+ this.testClass = testClass;
+ this.onlyForFieldsAndParameters = onlyForFieldsAndParameters;
+ }
+
+ /**
+ * Constructs a new {@link TestMethodProcessor} that handles {@link
+ * TestParameterAnnotation}-annotated annotations that are placed anywhere:
+ *
+ * <ul>
+ * <li>At a method / constructor parameter
+ * <li>At a field
+ * <li>At a method / constructor on the class
+ * <li>At the test class
+ * </ul>
+ */
+ static TestMethodProcessor forAllAnnotationPlacements(TestClass testClass) {
+ return new TestParameterAnnotationMethodProcessor(
+ testClass, /* onlyForFieldsAndParameters= */ false);
+ }
+
+ /**
+ * Constructs a new {@link TestMethodProcessor} that handles {@link
+ * TestParameterAnnotation}-annotated annotations that are placed at fields or parameters.
+ *
+ * <p>Note that this excludes class and method-level annotations, as is the default (using the
+ * constructor).
+ */
+ static TestMethodProcessor onlyForFieldsAndParameters(TestClass testClass) {
+ return new TestParameterAnnotationMethodProcessor(
+ testClass, /* onlyForFieldsAndParameters= */ true);
+ }
+
+ private ImmutableList<AnnotationTypeOrigin> getAnnotationTypeOrigins(
+ Origin firstOrigin, Origin... otherOrigins) {
+ if (cachedAnnotationTypeOrigins == null) {
+ // Collect all annotations used in declared fields and methods that have themselves a
+ // @TestParameterAnnotation annotation.
+ List<AnnotationTypeOrigin> fieldAnnotations =
+ extractTestParameterAnnotations(
+ streamWithParents(testClass.getJavaClass())
+ .flatMap(c -> stream(c.getDeclaredFields()))
+ .flatMap(field -> stream(field.getAnnotations())),
+ Origin.FIELD);
+ List<AnnotationTypeOrigin> methodAnnotations =
+ extractTestParameterAnnotations(
+ stream(testClass.getJavaClass().getMethods())
+ .flatMap(method -> stream(method.getAnnotations())),
+ Origin.METHOD);
+ List<AnnotationTypeOrigin> parameterAnnotations =
+ extractTestParameterAnnotations(
+ stream(testClass.getJavaClass().getMethods())
+ .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)),
+ Origin.METHOD_PARAMETER);
+ List<AnnotationTypeOrigin> classAnnotations =
+ extractTestParameterAnnotations(
+ stream(testClass.getJavaClass().getAnnotations()), Origin.CLASS);
+ List<AnnotationTypeOrigin> constructorAnnotations =
+ extractTestParameterAnnotations(
+ stream(testClass.getJavaClass().getConstructors())
+ .flatMap(constructor -> stream(constructor.getAnnotations())),
+ Origin.CONSTRUCTOR);
+ List<AnnotationTypeOrigin> constructorParameterAnnotations =
+ extractTestParameterAnnotations(
+ stream(testClass.getJavaClass().getConstructors())
+ .flatMap(
+ constructor ->
+ stream(constructor.getParameterAnnotations()).flatMap(Stream::of)),
+ Origin.CONSTRUCTOR_PARAMETER);
+
+ checkDuplicatedClassAndFieldAnnotations(
+ constructorAnnotations, classAnnotations, fieldAnnotations);
+
+ checkDuplicatedFieldsAnnotations(methodAnnotations, fieldAnnotations);
+
+ checkState(
+ constructorAnnotations.stream().distinct().count() == constructorAnnotations.size(),
+ "Annotations should not be duplicated on the constructor.");
+
+ checkState(
+ classAnnotations.stream().distinct().count() == classAnnotations.size(),
+ "Annotations should not be duplicated on the class.");
+
+ if (onlyForFieldsAndParameters) {
+ checkState(
+ methodAnnotations.isEmpty(),
+ "This test runner (constructed by the testparameterinjector package) was configured"
+ + " to disallow method-level annotations that could be field/parameter"
+ + " annotations, but found %s",
+ methodAnnotations);
+ checkState(
+ classAnnotations.isEmpty(),
+ "This test runner (constructed by the testparameterinjector package) was configured"
+ + " to disallow class-level annotations that could be field/parameter annotations,"
+ + " but found %s",
+ classAnnotations);
+ checkState(
+ constructorAnnotations.isEmpty(),
+ "This test runner (constructed by the testparameterinjector package) was configured"
+ + " to disallow constructor-level annotations that could be field/parameter"
+ + " annotations, but found %s",
+ constructorAnnotations);
+ }
+
+ cachedAnnotationTypeOrigins =
+ Stream.of(
+ // The order matters, since it will determine which annotation processor is
+ // called first.
+ classAnnotations.stream(),
+ fieldAnnotations.stream(),
+ constructorAnnotations.stream(),
+ constructorParameterAnnotations.stream(),
+ methodAnnotations.stream(),
+ parameterAnnotations.stream())
+ .flatMap(x -> x)
+ .distinct()
+ .collect(toImmutableList());
+ }
+
+ Set<Origin> originsToFilterBy =
+ ImmutableSet.<Origin>builder().add(firstOrigin).add(otherOrigins).build();
+ return cachedAnnotationTypeOrigins.stream()
+ .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin()))
+ .collect(toImmutableList());
+ }
+
+ private void checkDuplicatedFieldsAnnotations(
+ List<AnnotationTypeOrigin> methodAnnotations, List<AnnotationTypeOrigin> fieldAnnotations) {
+ // If an annotation is duplicated on two fields, then it becomes specific, and cannot be
+ // overridden by a method.
+ if (fieldAnnotations.stream().distinct().count() != fieldAnnotations.size()) {
+ List<Class<? extends Annotation>> methodOrFieldAnnotations =
+ Stream.concat(methodAnnotations.stream(), fieldAnnotations.stream().distinct())
+ .map(AnnotationTypeOrigin::annotationType)
+ .collect(toCollection(ArrayList::new));
+
+ checkState(
+ methodOrFieldAnnotations.stream().distinct().count() == methodOrFieldAnnotations.size(),
+ "Annotations should not be duplicated on a method and field"
+ + " if they are present on multiple fields");
+ }
+ }
+
+ private void checkDuplicatedClassAndFieldAnnotations(
+ List<AnnotationTypeOrigin> constructorAnnotations,
+ List<AnnotationTypeOrigin> classAnnotations,
+ List<AnnotationTypeOrigin> fieldAnnotations) {
+ ImmutableSet<? extends Class<? extends Annotation>> classAnnotationTypes =
+ classAnnotations.stream()
+ .map(AnnotationTypeOrigin::annotationType)
+ .collect(toImmutableSet());
+
+ ImmutableSet<Class<? extends Annotation>> uniqueFieldAnnotations =
+ fieldAnnotations.stream()
+ .map(AnnotationTypeOrigin::annotationType)
+ .collect(toImmutableSet());
+ ImmutableSet<Class<? extends Annotation>> uniqueConstructorAnnotations =
+ constructorAnnotations.stream()
+ .map(AnnotationTypeOrigin::annotationType)
+ .collect(toImmutableSet());
+
+ checkState(
+ Collections.disjoint(classAnnotationTypes, uniqueFieldAnnotations),
+ "Annotations should not be duplicated on a class and field");
+
+ checkState(
+ Collections.disjoint(classAnnotationTypes, uniqueConstructorAnnotations),
+ "Annotations should not be duplicated on a class and constructor");
+
+ checkState(
+ Collections.disjoint(uniqueConstructorAnnotations, uniqueFieldAnnotations),
+ "Annotations should not be duplicated on a field and constructor");
+ }
+
+ /** Returns a list of annotation types that are a {@link TestParameterAnnotation}. */
+ private List<AnnotationTypeOrigin> extractTestParameterAnnotations(
+ Stream<Annotation> annotations, Origin origin) {
+ return annotations
+ .map(Annotation::annotationType)
+ .filter(annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class))
+ .map(annotationType -> AnnotationTypeOrigin.create(annotationType, origin))
+ .collect(toCollection(ArrayList::new));
+ }
+
+ @Override
+ public ValidationResult validateConstructor(TestClass testClass, List<Throwable> errorsReturned) {
+ if (testClass.getJavaClass().getConstructors().length != 1) {
+ errorsReturned.add(
+ new IllegalStateException("Test class should have exactly one public constructor"));
+ return ValidationResult.HANDLED;
+ }
+ Constructor<?> constructor = testClass.getOnlyConstructor();
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length == 0) {
+ return ValidationResult.NOT_HANDLED;
+ }
+ // The constructor has parameters, they must be injected by a TestParameterAnnotation
+ // annotation.
+ Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
+ validateMethodOrConstructorParameters(
+ removeOverrides(
+ getAnnotationTypeOrigins(
+ Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER),
+ testClass.getJavaClass()),
+ testClass,
+ errorsReturned,
+ constructor,
+ parameterTypes,
+ parameterAnnotations);
+
+ return ValidationResult.HANDLED;
+ }
+
+ @Override
+ public ValidationResult validateTestMethod(
+ TestClass testClass, FrameworkMethod testMethod, List<Throwable> errorsReturned) {
+ Class<?>[] methodParameterTypes = testMethod.getMethod().getParameterTypes();
+ if (methodParameterTypes.length == 0) {
+ return ValidationResult.NOT_HANDLED;
+ } else {
+ Method method = testMethod.getMethod();
+ // The method has parameters, they must be injected by a TestParameterAnnotation annotation.
+ testMethod.validatePublicVoid(false /* isStatic */, errorsReturned);
+ Annotation[][] parametersAnnotations = method.getParameterAnnotations();
+ validateMethodOrConstructorParameters(
+ getAnnotationTypeOrigins(Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER),
+ testClass,
+ errorsReturned,
+ method,
+ methodParameterTypes,
+ parametersAnnotations);
+ return ValidationResult.HANDLED;
+ }
+ }
+
+ private void validateMethodOrConstructorParameters(
+ List<AnnotationTypeOrigin> annotationTypeOrigins,
+ TestClass testClass,
+ List<Throwable> errors,
+ AnnotatedElement methodOrConstructor,
+ Class<?>[] parameterTypes,
+ Annotation[][] parametersAnnotations) {
+ for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) {
+ Class<?> parameterType = parameterTypes[parameterIndex];
+ Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex];
+ boolean matchingTestParameterAnnotationFound = false;
+ // First, handle the case where the method parameter specifies the test parameter explicitly,
+ // e.g. {@code public void test(@ColorParameter({...}) Color c)}.
+ for (AnnotationTypeOrigin testParameterAnnotationType : annotationTypeOrigins) {
+ for (Annotation parameterAnnotation : parameterAnnotations) {
+ if (parameterAnnotation
+ .annotationType()
+ .equals(testParameterAnnotationType.annotationType())) {
+ // Verify that the type is assignable with the return type of the 'value' method.
+ Class<?> valueMethodReturnType =
+ getValueMethodReturnType(
+ testParameterAnnotationType.annotationType(),
+ /* paramClass = */ Optional.of(parameterType));
+ if (!parameterType.isAssignableFrom(valueMethodReturnType)) {
+ errors.add(
+ new IllegalStateException(
+ String.format(
+ "Parameter of type %s annotated with %s does not match"
+ + " expected type %s in method/constructor %s",
+ parameterType.getName(),
+ testParameterAnnotationType.annotationType().getName(),
+ valueMethodReturnType.getName(),
+ methodOrConstructor)));
+ } else {
+ matchingTestParameterAnnotationFound = true;
+ }
+ }
+ }
+ }
+ // Second, handle the case where the method parameter does not specify the test parameter,
+ // and instead relies on the type matching, e.g. {@code public void test(Color c)}.
+ if (!matchingTestParameterAnnotationFound) {
+ List<Class<? extends Annotation>> testParameterAnnotationTypes =
+ getTestParameterAnnotations(
+ // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already
+ // been evaluated.
+ filterAnnotationTypeOriginsByOrigin(
+ annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD),
+ testClass.getJavaClass(),
+ methodOrConstructor);
+ // If no annotation is present, simply compare the type.
+ for (Class<? extends Annotation> testParameterAnnotationType :
+ testParameterAnnotationTypes) {
+ if (parameterType.isAssignableFrom(
+ getValueMethodReturnType(
+ testParameterAnnotationType, /* paramClass = */ Optional.absent()))) {
+ if (matchingTestParameterAnnotationFound) {
+ errors.add(
+ new IllegalStateException(
+ String.format(
+ "Ambiguous method/constructor parameter type, matching multiple"
+ + " annotations for parameter of type %s in method %s",
+ parameterType.getName(), methodOrConstructor)));
+ }
+ matchingTestParameterAnnotationFound = true;
+ }
+ }
+ }
+ if (!matchingTestParameterAnnotationFound) {
+ errors.add(
+ new IllegalStateException(
+ String.format(
+ "No matching test parameter annotation found"
+ + " for parameter of type %s in method/constructor %s",
+ parameterType.getName(), methodOrConstructor)));
+ }
+ }
+ }
+
+ @Override
+ public Optional<Statement> createStatement(
+ TestClass testClass,
+ FrameworkMethod frameworkMethod,
+ Object testObject,
+ Optional<Statement> statement) {
+ if (frameworkMethod.getAnnotation(TestIndexHolder.class) == null
+ // Explicitly skip @TestParameters annotated methods to ensure compatibility.
+ //
+ // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported)
+ // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;).
+ // Without the @TestParameters check below, InvokeParameterizedMethod would be invoked for
+ // these methods. When there are no method parameters, this is a no-op, but when the method
+ // is annotated with @TestParameters, this throws an exception (because there are method
+ // parameters that this processor has no values for - they are provided by the
+ // @TestParameters processor).
+ || frameworkMethod.getAnnotation(TestParameters.class) != null) {
+ return statement;
+ } else {
+ return Optional.of(new InvokeParameterizedMethod(frameworkMethod, testObject));
+ }
+ }
+
+ /**
+ * Returns the {@link TestInfo}, one for each result of the cartesian product of each test
+ * parameter values.
+ *
+ * <p>For example, given the annotation {@code @ColorParameter({BLUE, WHITE, RED})} on a method,
+ * it method will return the TestParameterValues: "(@ColorParameter, BLUE), (@ColorParameter,
+ * WHITE), (@ColorParameter, RED)}).
+ *
+ * <p>For multiple annotations (say, {@code @TestParameter("foo", "bar")} and
+ * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result:
+ *
+ * <ul>
+ * <li>("foo", BLUE)
+ * <li>("foo", WHITE)
+ * <li>("bar", BLUE)
+ * <li>("bar", WHITE)
+ * <li>
+ * </ul>
+ *
+ * corresponding to the cartesian product of both annotations.
+ */
+ @Override
+ public List<TestInfo> processTest(Class<?> testClass, TestInfo originalTest) {
+ List<List<TestParameterValue>> parameterValuesForMethod =
+ getParameterValuesForMethod(originalTest.getMethod());
+
+ if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) {
+ // This test is not parameterized
+ return ImmutableList.of(originalTest);
+ }
+
+ ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder();
+ for (int parametersIndex = 0;
+ parametersIndex < parameterValuesForMethod.size();
+ ++parametersIndex) {
+ List<TestParameterValue> testParameterValues = parameterValuesForMethod.get(parametersIndex);
+ testInfos.add(
+ originalTest
+ .withExtraParameters(
+ testParameterValues.stream()
+ .map(
+ param ->
+ TestInfoParameter.create(
+ param.toTestNameString(), param.value(), param.valueIndex()))
+ .collect(toImmutableList()))
+ .withExtraAnnotation(
+ TestIndexHolderFactory.create(
+ /* methodIndex= */ strictIndexOf(
+ getMethodsIncludingParents(testClass), originalTest.getMethod()),
+ parametersIndex,
+ testClass.getName())));
+ }
+
+ return testInfos.build();
+ }
+
+ private List<List<TestParameterValue>> getParameterValuesForMethod(Method method) {
+ try {
+ return parameterValuesCache.get(
+ method,
+ () -> {
+ List<List<TestParameterValue>> testParameterValuesList =
+ getAnnotationValuesForUsedAnnotationTypes(testClass.getJavaClass(), method);
+
+ return Lists.cartesianProduct(testParameterValuesList).stream()
+ .filter(
+ // Skip tests based on the annotations' {@link Validator#shouldSkip} return
+ // value.
+ testParameterValues ->
+ testParameterValues.stream()
+ .noneMatch(
+ testParameterValue ->
+ callShouldSkip(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ testParameterValues)))
+ .collect(toImmutableList());
+ });
+ } catch (ExecutionException | UncheckedExecutionException e) {
+ Throwables.throwIfUnchecked(e.getCause());
+ throw new RuntimeException(e);
+ }
+ }
+
+ private List<TestParameterValue> getParameterValuesForTest(TestIndexHolder testIndexHolder) {
+ verify(
+ testIndexHolder.testClassName().equals(testClass.getName()),
+ "The class for which the given annotation was created (%s) is not the same as the test"
+ + " class that this runner is handling (%s)",
+ testIndexHolder.testClassName(),
+ testClass.getName());
+ Method testMethod =
+ getMethodsIncludingParents(testClass.getJavaClass()).get(testIndexHolder.methodIndex());
+ return getParameterValuesForMethod(testMethod).get(testIndexHolder.parametersIndex());
+ }
+
+ /**
+ * Returns the list of annotation index for all annotations defined in a given test method and its
+ * class.
+ */
+ private ImmutableList<List<TestParameterValue>> getAnnotationValuesForUsedAnnotationTypes(
+ Class<?> testClass, Method method) {
+ ImmutableList<AnnotationTypeOrigin> annotationTypes =
+ Stream.of(
+ getAnnotationTypeOrigins(Origin.CLASS).stream(),
+ getAnnotationTypeOrigins(Origin.FIELD).stream(),
+ getAnnotationTypeOrigins(Origin.CONSTRUCTOR).stream(),
+ getAnnotationTypeOrigins(Origin.CONSTRUCTOR_PARAMETER).stream(),
+ getAnnotationTypeOrigins(Origin.METHOD).stream(),
+ getAnnotationTypeOrigins(Origin.METHOD_PARAMETER).stream()
+ .sorted(annotationComparator(method.getParameterAnnotations())))
+ .flatMap(x -> x)
+ .collect(toImmutableList());
+
+ return removeOverrides(annotationTypes, testClass, method).stream()
+ .map(
+ annotationTypeOrigin ->
+ getAnnotationFromParametersOrTestOrClass(annotationTypeOrigin, method, testClass))
+ .filter(l -> !l.isEmpty())
+ .flatMap(List::stream)
+ .collect(toImmutableList());
+ }
+
+ private Comparator<AnnotationTypeOrigin> annotationComparator(
+ Annotation[][] parameterAnnotations) {
+ ImmutableList<String> annotationOrdering =
+ stream(parameterAnnotations)
+ .flatMap(Arrays::stream)
+ .map(Annotation::annotationType)
+ .map(Class::getName)
+ .collect(toImmutableList());
+ return Comparator.comparingInt(o -> annotationOrdering.indexOf(o.annotationType().getName()));
+ }
+
+ /**
+ * Returns a list of {@link AnnotationTypeOrigin} where the overridden annotation are removed for
+ * the current {@code originalTest} and {@code testClass}.
+ *
+ * <p>Specifically, annotation defined on CLASS and FIELD elements will be removed if they are
+ * also defined on the method, method parameter, constructor, or constructor parameters.
+ */
+ private List<AnnotationTypeOrigin> removeOverrides(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass, Method method) {
+ return removeOverrides(
+ annotationTypeOrigins.stream()
+ .filter(
+ annotationTypeOrigin -> {
+ switch (annotationTypeOrigin.origin()) {
+ case FIELD: // Fall through.
+ case CLASS:
+ return getAnnotationListWithType(
+ method.getAnnotations(), annotationTypeOrigin.annotationType())
+ .isEmpty();
+ default:
+ return true;
+ }
+ })
+ .collect(toCollection(ArrayList::new)),
+ testClass);
+ }
+
+ /** @see #removeOverrides(List, Class) */
+ private List<AnnotationTypeOrigin> removeOverrides(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass) {
+ return annotationTypeOrigins.stream()
+ .filter(
+ annotationTypeOrigin -> {
+ switch (annotationTypeOrigin.origin()) {
+ case FIELD: // Fall through.
+ case CLASS:
+ return getAnnotationListWithType(
+ getOnlyConstructor(testClass).getAnnotations(),
+ annotationTypeOrigin.annotationType())
+ .isEmpty()
+ && getAnnotationListWithType(
+ getOnlyConstructor(testClass).getParameterAnnotations(),
+ annotationTypeOrigin.annotationType())
+ .isEmpty();
+ default:
+ return true;
+ }
+ })
+ .collect(toCollection(ArrayList::new));
+ }
+
+ /**
+ * Returns the given annotations defined either on the method parameters, method or the test
+ * class.
+ *
+ * <p>The annotation from the parameters takes precedence over the same annotation defined on the
+ * method, and the one defined on the method takes precedence over the same annotation defined on
+ * the class.
+ */
+ private ImmutableList<List<TestParameterValue>> getAnnotationFromParametersOrTestOrClass(
+ AnnotationTypeOrigin annotationTypeOrigin, Method method, Class<?> testClass) {
+ Origin origin = annotationTypeOrigin.origin();
+ Class<? extends Annotation> annotationType = annotationTypeOrigin.annotationType();
+ if (origin == Origin.CONSTRUCTOR_PARAMETER) {
+ Constructor<?> constructor = getOnlyConstructor(testClass);
+ List<AnnotationWithMetadata> annotations =
+ getAnnotationWithMetadataListWithType(constructor, annotationType);
+
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.CONSTRUCTOR) {
+ Annotation annotation = getOnlyConstructor(testClass).getAnnotation(annotationType);
+ if (annotation != null) {
+ return ImmutableList.of(
+ TestParameterValue.create(AnnotationWithMetadata.withoutMetadata(annotation), origin));
+ }
+
+ } else if (origin == Origin.METHOD_PARAMETER) {
+ List<AnnotationWithMetadata> annotations =
+ getAnnotationWithMetadataListWithType(method, annotationType);
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.METHOD) {
+ if (method.isAnnotationPresent(annotationType)) {
+ return ImmutableList.of(
+ TestParameterValue.create(
+ AnnotationWithMetadata.withoutMetadata(method.getAnnotation(annotationType)),
+ origin));
+ }
+ } else if (origin == Origin.FIELD) {
+ List<AnnotationWithMetadata> annotations =
+ streamWithParents(testClass)
+ .flatMap(c -> stream(c.getDeclaredFields()))
+ .flatMap(
+ field ->
+ getAnnotationListWithType(field.getAnnotations(), annotationType).stream()
+ .map(
+ annotation ->
+ AnnotationWithMetadata.withMetadata(
+ annotation, field.getType(), field.getName())))
+ .collect(toCollection(ArrayList::new));
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.CLASS) {
+ Annotation annotation = testClass.getAnnotation(annotationType);
+ if (annotation != null) {
+ return ImmutableList.of(
+ TestParameterValue.create(AnnotationWithMetadata.withoutMetadata(annotation), origin));
+ }
+ }
+ return ImmutableList.of();
+ }
+
+ private static ImmutableList<List<TestParameterValue>> toTestParameterValueList(
+ List<AnnotationWithMetadata> annotationWithMetadatas, Origin origin) {
+ return annotationWithMetadatas.stream()
+ .map(annotationWithMetadata -> TestParameterValue.create(annotationWithMetadata, origin))
+ .collect(toImmutableList());
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Method callable, Class<? extends Annotation> annotationType) {
+ try {
+ return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType);
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType);
+ }
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Constructor<?> callable, Class<? extends Annotation> annotationType) {
+ try {
+ return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType);
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType);
+ }
+ }
+
+ // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method
+ // has a fallback that takes the parameter types and annotations (without the parameter names,
+ // which are optional anyway).
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Parameter[] parameters, Class<? extends Annotation> annotationType) {
+ return stream(parameters)
+ .map(
+ parameter -> {
+ Annotation annotation = parameter.getAnnotation(annotationType);
+ return annotation == null
+ ? null
+ : parameter.isNamePresent()
+ ? AnnotationWithMetadata.withMetadata(
+ annotation, parameter.getType(), parameter.getName())
+ : AnnotationWithMetadata.withMetadata(annotation, parameter.getType());
+ })
+ .filter(Objects::nonNull)
+ .collect(toImmutableList());
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Class<?>[] parameterTypes,
+ Annotation[][] annotations,
+ Class<? extends Annotation> annotationType) {
+ checkArgument(parameterTypes.length == annotations.length);
+
+ ImmutableList.Builder<AnnotationWithMetadata> resultBuilder = ImmutableList.builder();
+ for (int i = 0; i < annotations.length; i++) {
+ for (Annotation annotation : annotations[i]) {
+ if (annotation.annotationType().equals(annotationType)) {
+ resultBuilder.add(AnnotationWithMetadata.withMetadata(annotation, parameterTypes[i]));
+ }
+ }
+ }
+ return resultBuilder.build();
+ }
+
+ private ImmutableList<Annotation> getAnnotationListWithType(
+ Annotation[][] parameterAnnotations, Class<? extends Annotation> annotationType) {
+ return stream(parameterAnnotations)
+ .flatMap(Stream::of)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .collect(toImmutableList());
+ }
+
+ private ImmutableList<Annotation> getAnnotationListWithType(
+ Annotation[] annotations, Class<? extends Annotation> annotationType) {
+ return stream(annotations)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .collect(toImmutableList());
+ }
+
+ private static Constructor<?> getOnlyConstructor(Class<?> testClass) {
+ Constructor<?>[] constructors = testClass.getConstructors();
+ checkState(
+ constructors.length == 1,
+ "a single public constructor is required for class %s",
+ testClass);
+ return constructors[0];
+ }
+
+ @Override
+ public Optional<Object> createTest(
+ TestClass testClass, FrameworkMethod method, Optional<Object> test) {
+ TestIndexHolder testIndexHolder = method.getAnnotation(TestIndexHolder.class);
+ if (testIndexHolder == null) {
+ return test;
+ }
+ try {
+ List<TestParameterValue> testParameterValues = getParameterValuesForTest(testIndexHolder);
+
+ Object testObject;
+ if (test.isPresent()) {
+ testObject = test.get();
+ } else {
+ Constructor<?> constructor = testClass.getOnlyConstructor();
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length == 0) {
+ testObject = constructor.newInstance();
+ } else {
+ // The constructor has parameters, they must be injected by a TestParameterAnnotation
+ // annotation.
+ Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
+ Object[] arguments = new Object[parameterTypes.length];
+ List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>();
+ List<TestParameterValue> parameterValuesForConstructor =
+ filterByOrigin(
+ testParameterValues,
+ Origin.CLASS,
+ Origin.CONSTRUCTOR,
+ Origin.CONSTRUCTOR_PARAMETER);
+ for (int i = 0; i < arguments.length; i++) {
+ // Initialize each parameter value from the corresponding TestParameterAnnotation value.
+ arguments[i] =
+ getParameterValue(
+ parameterValuesForConstructor,
+ parameterTypes[i],
+ parameterAnnotations[i],
+ processedAnnotationTypes);
+ }
+ testObject = constructor.newInstance(arguments);
+ }
+ }
+ // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER}
+ // annotations.
+ List<TestParameterValue> testParameterValuesForFieldInjection =
+ filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD);
+ // The annotationType corresponding to the annotationIndex, e.g ColorParameter.class
+ // in the example above.
+ List<TestParameterValue> remainingTestParameterValuesForFieldInjection =
+ new ArrayList<>(testParameterValuesForFieldInjection);
+ for (Field declaredField :
+ streamWithParents(testObject.getClass())
+ .flatMap(c -> stream(c.getDeclaredFields()))
+ .collect(toImmutableList())) {
+ for (TestParameterValue testParameterValue :
+ remainingTestParameterValuesForFieldInjection) {
+ if (declaredField.isAnnotationPresent(
+ testParameterValue.annotationTypeOrigin().annotationType())) {
+ declaredField.setAccessible(true);
+ declaredField.set(testObject, testParameterValue.value());
+ remainingTestParameterValuesForFieldInjection.remove(testParameterValue);
+ break;
+ }
+ }
+ }
+ return Optional.of(testObject);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns an {@link TestParameterValue} list that contains only the values originating from one
+ * of the {@code origins}.
+ */
+ private static ImmutableList<TestParameterValue> filterByOrigin(
+ List<TestParameterValue> testParameterValues, Origin... origins) {
+ Set<Origin> originsToFilterBy = ImmutableSet.copyOf(origins);
+ return testParameterValues.stream()
+ .filter(
+ testParameterValue ->
+ originsToFilterBy.contains(testParameterValue.annotationTypeOrigin().origin()))
+ .collect(toImmutableList());
+ }
+
+ /**
+ * Returns an {@link AnnotationTypeOrigin} list that contains only the values originating from one
+ * of the {@code origins}.
+ */
+ private static ImmutableList<AnnotationTypeOrigin> filterAnnotationTypeOriginsByOrigin(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Origin... origins) {
+ List<Origin> originList = Arrays.asList(origins);
+ return annotationTypeOrigins.stream()
+ .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin()))
+ .collect(toImmutableList());
+ }
+
+ @Override
+ public Statement processStatement(Statement originalStatement, Description finalTestDescription) {
+ TestIndexHolder testIndexHolder = finalTestDescription.getAnnotation(TestIndexHolder.class);
+ if (testIndexHolder == null) {
+ return originalStatement;
+ }
+ List<TestParameterValue> testParameterValues = getParameterValuesForTest(testIndexHolder);
+
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ for (TestParameterValue testParameterValue : testParameterValues) {
+ callBefore(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ testParameterValue.value());
+ }
+ try {
+ originalStatement.evaluate();
+ } finally {
+ // In reverse order.
+ for (TestParameterValue testParameterValue : Lists.reverse(testParameterValues)) {
+ callAfter(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ testParameterValue.value());
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Class to invoke the test method if it has parameters, and they need to be injected from the
+ * TestParameterAnnotation values.
+ */
+ private class InvokeParameterizedMethod extends Statement {
+
+ private final FrameworkMethod frameworkMethod;
+ private final Object testObject;
+ private final List<TestParameterValue> testParameterValues;
+
+ public InvokeParameterizedMethod(FrameworkMethod frameworkMethod, Object testObject) {
+ this.frameworkMethod = frameworkMethod;
+ this.testObject = testObject;
+ TestIndexHolder testIndexHolder = frameworkMethod.getAnnotation(TestIndexHolder.class);
+ checkState(testIndexHolder != null);
+ testParameterValues =
+ filterByOrigin(
+ getParameterValuesForTest(testIndexHolder),
+ Origin.CLASS,
+ Origin.METHOD,
+ Origin.METHOD_PARAMETER);
+ }
+
+ @Override
+ public void evaluate() throws Throwable {
+ Class<?>[] parameterTypes = frameworkMethod.getMethod().getParameterTypes();
+ Annotation[][] parametersAnnotations = frameworkMethod.getMethod().getParameterAnnotations();
+ Object[] parameterValues = new Object[parameterTypes.length];
+
+ List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>();
+ // Initialize each parameter value from the corresponding TestParameterAnnotation value.
+ for (int i = 0; i < parameterTypes.length; i++) {
+ parameterValues[i] =
+ getParameterValue(
+ testParameterValues,
+ parameterTypes[i],
+ parametersAnnotations[i],
+ processedAnnotationTypes);
+ }
+ frameworkMethod.invokeExplosively(testObject, parameterValues);
+ }
+ }
+
+ /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */
+ private Object getParameterValue(
+ List<TestParameterValue> testParameterValues,
+ Class<?> methodParameterType,
+ Annotation[] parameterAnnotations,
+ List<Class<? extends Annotation>> processedAnnotationTypes) {
+ List<Class<? extends Annotation>> iteratedAnnotationTypes = new ArrayList<>();
+ for (TestParameterValue testParameterValue : testParameterValues) {
+ // The annotationType corresponding to the annotationIndex, e.g ColorParameter.class
+ // in the example above.
+ for (Annotation parameterAnnotation : parameterAnnotations) {
+ Class<? extends Annotation> annotationType =
+ testParameterValue.annotationTypeOrigin().annotationType();
+ if (parameterAnnotation.annotationType().equals(annotationType)) {
+ // If multiple annotations exist, ensure that the proper one is selected.
+ // For instance, for:
+ // <code>
+ // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {}
+ // </code>
+ // Verifies that the correct @FooParameter annotation value will be assigned to the
+ // corresponding variable.
+ if (Collections.frequency(processedAnnotationTypes, annotationType)
+ == Collections.frequency(iteratedAnnotationTypes, annotationType)) {
+ processedAnnotationTypes.add(annotationType);
+ return testParameterValue.value();
+ }
+ iteratedAnnotationTypes.add(annotationType);
+ }
+ }
+ }
+ // If no annotation matches, use the method parameter type.
+ for (TestParameterValue testParameterValue : testParameterValues) {
+ // The annotationType corresponding to the annotationIndex, e.g ColorParameter.class
+ // in the example above.
+ if (methodParameterType.isAssignableFrom(
+ getValueMethodReturnType(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ /* paramClass = */ Optional.absent()))) {
+ return testParameterValue.value();
+ }
+ }
+ throw new IllegalStateException(
+ "The method parameter should have matched a TestParameterAnnotation");
+ }
+
+ /**
+ * This mechanism is a workaround to be able to store the annotation values in the annotation list
+ * of the {@link TestInfo}, since we cannot carry other information through the test runner.
+ */
+ @Retention(RUNTIME)
+ @interface TestIndexHolder {
+
+ /** The index of the test method in {@code getMethodsIncludingParents(testClass)} */
+ int methodIndex();
+
+ /**
+ * The index of the set of parameters to run the test method with in the list produced by {@link
+ * #getParameterValuesForMethod(Method)}.
+ */
+ int parametersIndex();
+
+ /**
+ * The full name of the test class. Only used for verifying that assumptions about the above
+ * indices are valid.
+ */
+ String testClassName();
+ }
+
+ /** Factory for {@link TestIndexHolder}. */
+ static class TestIndexHolderFactory {
+ @AutoAnnotation
+ static TestIndexHolder create(int methodIndex, int parametersIndex, String testClassName) {
+ return new AutoAnnotation_TestParameterAnnotationMethodProcessor_TestIndexHolderFactory_create(
+ methodIndex, parametersIndex, testClassName);
+ }
+
+ private TestIndexHolderFactory() {}
+ }
+
+ /** Invokes the {@link TestParameterProcessor#before} method of an annotation. */
+ private static void callBefore(
+ Class<? extends Annotation> annotationType, Object annotationValue) {
+ TestParameterAnnotation annotation =
+ annotationType.getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterProcessor> processor = annotation.processor();
+ try {
+ processor.getConstructor().newInstance().before(annotationValue);
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking processor " + processor, e);
+ }
+ }
+
+ /** Invokes the {@link TestParameterProcessor#after} method of an annotation. */
+ private static void callAfter(
+ Class<? extends Annotation> annotationType, Object annotationValue) {
+ TestParameterAnnotation annotation =
+ annotationType.getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterProcessor> processor = annotation.processor();
+ try {
+ processor.getConstructor().newInstance().after(annotationValue);
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking processor " + processor, e);
+ }
+ }
+
+ /**
+ * Returns whether the test should be skipped according to the {@code annotationType}'s {@link
+ * TestParameterValidator} and the current list of {@link TestParameterValue}.
+ */
+ private static boolean callShouldSkip(
+ Class<? extends Annotation> annotationType, List<TestParameterValue> testParameterValues) {
+ TestParameterAnnotation annotation =
+ annotationType.getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValidator> validator = annotation.validator();
+ try {
+ return validator
+ .getConstructor()
+ .newInstance()
+ .shouldSkip(new ValidatorContext(testParameterValues));
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking validator " + validator, e);
+ }
+ }
+
+ private static class ValidatorContext implements TestParameterValidator.Context {
+
+ private final List<TestParameterValue> testParameterValues;
+ private final Set<Object> valueList;
+
+ public ValidatorContext(List<TestParameterValue> testParameterValues) {
+ this.testParameterValues = testParameterValues;
+ this.valueList = testParameterValues.stream().map(TestParameterValue::value).collect(toSet());
+ }
+
+ @Override
+ public boolean has(Class<? extends Annotation> testParameter, Object value) {
+ return getValue(testParameter).transform(value::equals).or(false);
+ }
+
+ @Override
+ public <T extends Enum<T>, U extends Enum<U>> boolean has(T value1, U value2) {
+ return valueList.contains(value1) && valueList.contains(value2);
+ }
+
+ @Override
+ public Optional<Object> getValue(Class<? extends Annotation> testParameter) {
+ return getParameter(testParameter).transform(TestParameterValue::value);
+ }
+
+ @Override
+ public List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter) {
+ return getParameter(testParameter)
+ .transform(TestParameterValue::specifiedValues)
+ .or(ImmutableList.of());
+ }
+
+ private Optional<TestParameterValue> getParameter(Class<? extends Annotation> testParameter) {
+ return Optional.fromNullable(
+ testParameterValues.stream()
+ .filter(value -> value.annotationTypeOrigin().annotationType().equals(testParameter))
+ .findAny()
+ .orElse(null));
+ }
+ }
+
+ /**
+ * Returns the class of the list elements returned by {@code provideValues()}.
+ *
+ * @param annotationType The type of the annotation that was encountered in the test class. The
+ * definition of this annotation is itself annotated with the {@link TestParameterAnnotation}
+ * annotation.
+ * @param paramClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code paramClass} is an absent
+ * optional.
+ */
+ private static Class<?> getValueMethodReturnType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> paramClass) {
+ TestParameterAnnotation testParameter =
+ annotationType.getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider();
+ try {
+ return valueProvider
+ .getConstructor()
+ .newInstance()
+ .getValueType(annotationType, java.util.Optional.ofNullable(paramClass.orNull()));
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Unexpected exception while invoking value provider " + valueProvider, e);
+ }
+ }
+
+ /** Returns the TestParameterAnnotation annotation types defined for a method or constructor. */
+ private ImmutableList<Class<? extends Annotation>> getTestParameterAnnotations(
+ List<AnnotationTypeOrigin> annotationTypeOrigins,
+ final Class<?> testClass,
+ AnnotatedElement methodOrConstructor) {
+ return annotationTypeOrigins.stream()
+ .map(AnnotationTypeOrigin::annotationType)
+ .filter(
+ annotationType ->
+ testClass.isAnnotationPresent(annotationType)
+ || methodOrConstructor.isAnnotationPresent(annotationType))
+ .collect(toImmutableList());
+ }
+
+ private <T> int strictIndexOf(List<T> haystack, T needle) {
+ int index = haystack.indexOf(needle);
+ checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack);
+ return index;
+ }
+
+ private ImmutableList<Method> getMethodsIncludingParents(Class<?> clazz) {
+ ImmutableList.Builder<Method> resultBuilder = ImmutableList.builder();
+ while (clazz != null) {
+ resultBuilder.add(clazz.getMethods());
+ clazz = clazz.getSuperclass();
+ }
+ return resultBuilder.build();
+ }
+
+ private static Stream<Class<?>> streamWithParents(Class<?> clazz) {
+ Stream.Builder<Class<?>> resultBuilder = Stream.builder();
+
+ Class<?> currentClass = clazz;
+ while (currentClass != null) {
+ resultBuilder.add(currentClass);
+ currentClass = currentClass.getSuperclass();
+ }
+
+ return resultBuilder.build();
+ }
+
+ // Immutable collectors are re-implemented here because they are missing from the Android
+ // collection library.
+ private static <E> Collector<E, ?, ImmutableList<E>> toImmutableList() {
+ return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
+ }
+
+ private static <E> Collector<E, ?, ImmutableSet<E>> toImmutableSet() {
+ return Collectors.collectingAndThen(Collectors.toList(), ImmutableSet::copyOf);
+ }
+}