aboutsummaryrefslogtreecommitdiff
path: root/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java
diff options
context:
space:
mode:
Diffstat (limited to 'junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java')
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java1369
1 files changed, 1369 insertions, 0 deletions
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java
new file mode 100644
index 0000000..7fd2336
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java
@@ -0,0 +1,1369 @@
+/*
+ * 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.junit5;
+
+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 com.google.common.collect.Lists.newArrayList;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+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.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Range;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.testing.junit.testparameterinjector.junit5.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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import javax.annotation.Nullable;
+
+/**
+ * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with
+ * {@link TestParameterAnnotation}.
+ *
+ * @see TestParameterAnnotation
+ */
+final 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 TestParameterValueHolder 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).
+ */
+ abstract TestParameterValue wrappedValue();
+
+ /** 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 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 {@link #wrappedValue()} without the {@link TestParameterValue} wrapper if it exists.
+ */
+ @Nullable
+ Object unwrappedValue() {
+ return wrappedValue().getWrappedValue();
+ }
+
+ /**
+ * Returns a String that represents this value and is fit for use in a test name (between
+ * brackets).
+ */
+ String toTestNameString() {
+ return ParameterValueParsing.formatTestNameString(paramName(), wrappedValue());
+ }
+
+ public static ImmutableList<TestParameterValueHolder> create(
+ AnnotationWithMetadata annotationWithMetadata, Origin origin) {
+ List<TestParameterValue> 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 FluentIterable.from(
+ ContiguousSet.create(
+ Range.closedOpen(0, specifiedValues.size()), DiscreteDomain.integers()))
+ .transform(
+ valueIndex ->
+ (TestParameterValueHolder)
+ new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValueHolder(
+ AnnotationTypeOrigin.create(
+ annotationWithMetadata.annotation().annotationType(), origin),
+ specifiedValues.get(valueIndex),
+ valueIndex,
+ newArrayList(
+ FluentIterable.from(specifiedValues)
+ .transform(TestParameterValue::getWrappedValue)),
+ annotationWithMetadata.paramName()))
+ .toList();
+ }
+ }
+
+ /**
+ * 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 ->
+ FluentIterable.from(
+ new TestParameterAnnotationMethodProcessor(
+ /* onlyForFieldsAndParameters= */ false)
+ .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()))
+ .filter(
+ testParameterValue ->
+ testParameterValue
+ .annotationTypeOrigin()
+ .annotationType()
+ .equals(annotationType))
+ .transform(TestParameterValueHolder::unwrappedValue)
+ .first();
+ }
+ }
+
+ /**
+ * 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 ImmutableList<TestParameterValue> getParametersAnnotationValues(
+ AnnotationWithMetadata annotationWithMetadata) {
+ Annotation annotation = annotationWithMetadata.annotation();
+ TestParameterAnnotation testParameter =
+ annotation.annotationType().getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider();
+ try {
+ return FluentIterable.from(
+ valueProvider
+ .getConstructor()
+ .newInstance()
+ .provideValues(
+ annotation,
+ annotationWithMetadata.paramClass(),
+ annotationWithMetadata.context()))
+ .transform(
+ value ->
+ (value instanceof TestParameterValue)
+ ? (TestParameterValue) value
+ : TestParameterValue.wrap(value))
+ .toList();
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(
+ "Unexpected exception while invoking value provider " + valueProvider, e);
+ }
+ }
+
+ /** 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();
+
+ /**
+ * A value class that contains extra information about the context of this parameter.
+ *
+ * <p>In case the annotation is annotating a method, constructor or class (deprecated
+ * functionality), the annotations in the context will be empty.
+ */
+ abstract GenericParameterContext context();
+
+ public static AnnotationWithMetadata withMetadata(
+ Annotation annotation,
+ Class<?> paramClass,
+ String paramName,
+ GenericParameterContext context) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.of(paramClass), Optional.of(paramName), context);
+ }
+
+ public static AnnotationWithMetadata withMetadata(
+ Annotation annotation, Class<?> paramClass, GenericParameterContext context) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.of(paramClass), Optional.absent(), context);
+ }
+
+ public static AnnotationWithMetadata withoutMetadata(
+ Annotation annotation, GenericParameterContext context) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation,
+ /* paramClass= */ Optional.absent(),
+ /* paramName= */ Optional.absent(),
+ context);
+ }
+
+ // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields
+ // to this class without breaking existing code.
+ @Override
+ public final boolean equals(Object other) {
+ throw new UnsupportedOperationException("Equality is not supported");
+ }
+
+ @Override
+ public final int hashCode() {
+ throw new UnsupportedOperationException("hashCode() is not supported");
+ }
+ }
+
+ private final boolean onlyForFieldsAndParameters;
+ private final LoadingCache<Class<?>, ImmutableList<AnnotationTypeOrigin>>
+ annotationTypeOriginsCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(1000)
+ .build(CacheLoader.from(this::calculateAnnotationTypeOrigins));
+ private final Cache<Method, List<List<TestParameterValueHolder>>> parameterValuesCache =
+ CacheBuilder.newBuilder().maximumSize(1000).build();
+
+ private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) {
+ 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() {
+ return new TestParameterAnnotationMethodProcessor(/* 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() {
+ return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true);
+ }
+
+ private ImmutableList<AnnotationTypeOrigin> calculateAnnotationTypeOrigins(Class<?> testClass) {
+ // Collect all annotations used in declared fields and methods that have themselves a
+ // @TestParameterAnnotation annotation.
+ List<AnnotationTypeOrigin> fieldAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(listWithParents(testClass))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields()))
+ .transformAndConcat(field -> Arrays.asList(field.getAnnotations()))
+ .toList(),
+ Origin.FIELD);
+ List<AnnotationTypeOrigin> methodAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(testClass.getMethods())
+ .transformAndConcat(method -> Arrays.asList(method.getAnnotations()))
+ .toList(),
+ Origin.METHOD);
+ List<AnnotationTypeOrigin> parameterAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(listWithParents(testClass))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredMethods()))
+ .transformAndConcat(method -> Arrays.asList(method.getParameterAnnotations()))
+ .transformAndConcat(Arrays::asList)
+ .toList(),
+ Origin.METHOD_PARAMETER);
+ List<AnnotationTypeOrigin> classAnnotations =
+ extractTestParameterAnnotations(Arrays.asList(testClass.getAnnotations()), Origin.CLASS);
+ List<AnnotationTypeOrigin> constructorAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(testClass.getDeclaredConstructors())
+ .transformAndConcat(constructor -> Arrays.asList(constructor.getAnnotations()))
+ .toList(),
+ Origin.CONSTRUCTOR);
+ List<AnnotationTypeOrigin> constructorParameterAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(testClass.getDeclaredConstructors())
+ .transformAndConcat(
+ constructor ->
+ FluentIterable.from(Arrays.asList(constructor.getParameterAnnotations()))
+ .transformAndConcat(Arrays::asList))
+ .toList(),
+ Origin.CONSTRUCTOR_PARAMETER);
+
+ checkDuplicatedClassAndFieldAnnotations(
+ constructorAnnotations, classAnnotations, fieldAnnotations);
+
+ checkDuplicatedFieldsAnnotations(methodAnnotations, fieldAnnotations);
+
+ checkState(
+ FluentIterable.from(constructorAnnotations).toSet().size() == constructorAnnotations.size(),
+ "Annotations should not be duplicated on the constructor.");
+
+ checkState(
+ FluentIterable.from(classAnnotations).toSet().size() == 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);
+ }
+
+ // The order matters, since it will determine which annotation processor is
+ // called first.
+ return FluentIterable.from(classAnnotations)
+ .append(fieldAnnotations)
+ .append(constructorAnnotations)
+ .append(constructorParameterAnnotations)
+ .append(methodAnnotations)
+ .append(parameterAnnotations)
+ .toSet()
+ .asList();
+ }
+
+ private ImmutableList<AnnotationTypeOrigin> getAnnotationTypeOrigins(
+ Class<?> testClass, Origin firstOrigin, Origin... otherOrigins) {
+ Set<Origin> originsToFilterBy =
+ ImmutableSet.<Origin>builder().add(firstOrigin).add(otherOrigins).build();
+ try {
+ return FluentIterable.from(annotationTypeOriginsCache.getUnchecked(testClass))
+ .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin()))
+ .toList();
+ } catch (UncheckedExecutionException e) {
+ Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class);
+ throw e;
+ }
+ }
+
+ 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 (FluentIterable.from(fieldAnnotations).toSet().size() != fieldAnnotations.size()) {
+ List<Class<? extends Annotation>> methodOrFieldAnnotations =
+ new ArrayList<>(
+ FluentIterable.from(methodAnnotations)
+ .append(new HashSet<>(fieldAnnotations))
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toList());
+
+ checkState(
+ FluentIterable.from(methodOrFieldAnnotations).toSet().size()
+ == 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 =
+ FluentIterable.from(classAnnotations)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toSet();
+
+ ImmutableSet<? extends Class<? extends Annotation>> uniqueFieldAnnotations =
+ FluentIterable.from(fieldAnnotations)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toSet();
+ ImmutableSet<? extends Class<? extends Annotation>> uniqueConstructorAnnotations =
+ FluentIterable.from(constructorAnnotations)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toSet();
+
+ 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");
+ }
+
+ private List<AnnotationTypeOrigin> extractTestParameterAnnotations(
+ List<Annotation> annotations, Origin origin) {
+ return new ArrayList<>(
+ FluentIterable.from(annotations)
+ .transform(Annotation::annotationType)
+ .filter(
+ annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class))
+ .transform(annotationType -> AnnotationTypeOrigin.create(annotationType, origin))
+ .toList());
+ }
+
+ @Override
+ public ExecutableValidationResult validateConstructor(Constructor<?> constructor) {
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length == 0) {
+ return ExecutableValidationResult.notValidated();
+ }
+ // The constructor has parameters, they must be injected by a TestParameterAnnotation
+ // annotation.
+ Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
+ Class<?> testClass = constructor.getDeclaringClass();
+ return ExecutableValidationResult.validated(
+ validateMethodOrConstructorParameters(
+ removeOverrides(
+ getAnnotationTypeOrigins(
+ testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER),
+ testClass),
+ testClass,
+ constructor,
+ parameterTypes,
+ parameterAnnotations));
+ }
+
+ @Override
+ public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) {
+ Class<?>[] methodParameterTypes = testMethod.getParameterTypes();
+ if (methodParameterTypes.length == 0) {
+ return ExecutableValidationResult.notValidated();
+ } else {
+ // The method has parameters, they must be injected by a TestParameterAnnotation annotation.
+ return ExecutableValidationResult.validated(
+ validateMethodOrConstructorParameters(
+ getAnnotationTypeOrigins(
+ testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER),
+ testClass,
+ testMethod,
+ methodParameterTypes,
+ testMethod.getParameterAnnotations()));
+ }
+ }
+
+ private List<Throwable> validateMethodOrConstructorParameters(
+ List<AnnotationTypeOrigin> annotationTypeOrigins,
+ Class<?> testClass,
+ AnnotatedElement methodOrConstructor,
+ Class<?>[] parameterTypes,
+ Annotation[][] parametersAnnotations) {
+ List<Throwable> errors = new ArrayList<>();
+
+ 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) {
+ ImmutableList<? extends 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,
+ 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)));
+ }
+ }
+ return errors;
+ }
+
+ @Override
+ public Optional<List<Object>> maybeGetConstructorParameters(
+ Constructor<?> constructor, TestInfo testInfo) {
+ if (testInfo.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, this class would try to find parameters 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).
+ || constructor.isAnnotationPresent(TestParameters.class)) {
+ return Optional.absent();
+ } else {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ List<TestParameterValueHolder> testParameterValues =
+ getParameterValuesForTest(testIndexHolder, testInfo.getTestClass());
+
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
+ List<Object> parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length);
+ List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>();
+ List<TestParameterValueHolder> parameterValuesForConstructor =
+ filterByOrigin(
+ testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER);
+ for (int i = 0; i < parameterTypes.length; i++) {
+ // Initialize each parameter value from the corresponding TestParameterAnnotation value.
+ parameterValues.add(
+ getParameterValue(
+ parameterValuesForConstructor,
+ parameterTypes[i],
+ parameterAnnotations[i],
+ processedAnnotationTypes));
+ }
+ return Optional.of(parameterValues);
+ }
+ }
+
+ @Override
+ public Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo) {
+ Method testMethod = testInfo.getMethod();
+ if (testInfo.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, this class would try to find parameters 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).
+ || testMethod.isAnnotationPresent(TestParameters.class)) {
+ return Optional.absent();
+ } else {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ checkState(testIndexHolder != null);
+ List<TestParameterValueHolder> testParameterValues =
+ filterByOrigin(
+ getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()),
+ Origin.CLASS,
+ Origin.METHOD,
+ Origin.METHOD_PARAMETER);
+
+ Class<?>[] parameterTypes = testMethod.getParameterTypes();
+ Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations();
+ ArrayList<Object> parameterValues =
+ new ArrayList<>(/* initialCapacity= */ parameterTypes.length);
+
+ List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>();
+ for (int i = 0; i < parameterTypes.length; i++) {
+ parameterValues.add(
+ getParameterValue(
+ testParameterValues,
+ parameterTypes[i],
+ parametersAnnotations[i],
+ processedAnnotationTypes));
+ }
+
+ return Optional.of(parameterValues);
+ }
+ }
+
+ /**
+ * 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> calculateTestInfos(TestInfo originalTest) {
+ List<List<TestParameterValueHolder>> parameterValuesForMethod =
+ getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass());
+
+ 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<TestParameterValueHolder> testParameterValues =
+ parameterValuesForMethod.get(parametersIndex);
+ testInfos.add(
+ originalTest
+ .withExtraParameters(
+ FluentIterable.from(testParameterValues)
+ .transform(
+ param ->
+ TestInfoParameter.create(
+ param.toTestNameString(),
+ param.unwrappedValue(),
+ param.valueIndex()))
+ .toList())
+ .withExtraAnnotation(
+ TestIndexHolderFactory.create(
+ /* methodIndex= */ strictIndexOf(
+ getMethodsIncludingParentsSorted(originalTest.getTestClass()),
+ originalTest.getMethod()),
+ parametersIndex,
+ originalTest.getTestClass().getName())));
+ }
+
+ return testInfos.build();
+ }
+
+ private List<List<TestParameterValueHolder>> getParameterValuesForMethod(
+ Method method, Class<?> testClass) {
+ try {
+ return parameterValuesCache.get(
+ method,
+ () -> {
+ List<List<TestParameterValueHolder>> testParameterValuesList =
+ getAnnotationValuesForUsedAnnotationTypes(method, testClass);
+
+ return FluentIterable.from(Lists.cartesianProduct(testParameterValuesList))
+ .filter(
+ // Skip tests based on the annotations' {@link Validator#shouldSkip} return
+ // value.
+ testParameterValues ->
+ FluentIterable.from(testParameterValues)
+ .filter(
+ testParameterValue ->
+ callShouldSkip(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ testParameterValues))
+ .isEmpty())
+ .toList();
+ });
+ } catch (ExecutionException | UncheckedExecutionException e) {
+ Throwables.throwIfUnchecked(e.getCause());
+ throw new RuntimeException(e);
+ }
+ }
+
+ private List<TestParameterValueHolder> getParameterValuesForTest(
+ TestIndexHolder testIndexHolder, Class<?> testClass) {
+ 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 =
+ getMethodsIncludingParentsSorted(testClass).get(testIndexHolder.methodIndex());
+ return getParameterValuesForMethod(testMethod, testClass)
+ .get(testIndexHolder.parametersIndex());
+ }
+
+ /**
+ * Returns the list of annotation index for all annotations defined in a given test method and its
+ * class.
+ */
+ private ImmutableList<List<TestParameterValueHolder>> getAnnotationValuesForUsedAnnotationTypes(
+ Method method, Class<?> testClass) {
+ ImmutableList<AnnotationTypeOrigin> annotationTypes =
+ FluentIterable.from(getAnnotationTypeOrigins(testClass, Origin.CLASS))
+ .append(getAnnotationTypeOrigins(testClass, Origin.FIELD))
+ .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR))
+ .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER))
+ .append(getAnnotationTypeOrigins(testClass, Origin.METHOD))
+ .append(
+ ImmutableList.sortedCopyOf(
+ annotationComparator(method.getParameterAnnotations()),
+ getAnnotationTypeOrigins(testClass, Origin.METHOD_PARAMETER)))
+ .toList();
+
+ return FluentIterable.from(removeOverrides(annotationTypes, testClass, method))
+ .transform(
+ annotationTypeOrigin ->
+ getAnnotationFromParametersOrTestOrClass(annotationTypeOrigin, method, testClass))
+ .filter(l -> !l.isEmpty())
+ .transformAndConcat(i -> i)
+ .toList();
+ }
+
+ private Comparator<AnnotationTypeOrigin> annotationComparator(
+ Annotation[][] parameterAnnotations) {
+ ImmutableList<String> annotationOrdering =
+ FluentIterable.from(parameterAnnotations)
+ .transformAndConcat(Arrays::asList)
+ .transform(Annotation::annotationType)
+ .transform(Class::getName)
+ .toList();
+ return (annotationTypeOrigin, t1) ->
+ Integer.compare(
+ annotationOrdering.indexOf(annotationTypeOrigin.annotationType().getName()),
+ annotationOrdering.indexOf(t1.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(
+ new ArrayList<>(
+ FluentIterable.from(annotationTypeOrigins)
+ .filter(
+ annotationTypeOrigin -> {
+ switch (annotationTypeOrigin.origin()) {
+ case FIELD: // Fall through.
+ case CLASS:
+ return getAnnotationListWithType(
+ method.getAnnotations(), annotationTypeOrigin.annotationType())
+ .isEmpty();
+ default:
+ return true;
+ }
+ })
+ .toList()),
+ testClass);
+ }
+
+ /**
+ * @see #removeOverrides(List, Class)
+ */
+ private List<AnnotationTypeOrigin> removeOverrides(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass) {
+ return new ArrayList<>(
+ FluentIterable.from(annotationTypeOrigins)
+ .filter(
+ annotationTypeOrigin -> {
+ switch (annotationTypeOrigin.origin()) {
+ case FIELD: // Fall through.
+ case CLASS:
+ return getAnnotationListWithType(
+ TestParameterInjectorUtils.getOnlyConstructor(testClass)
+ .getAnnotations(),
+ annotationTypeOrigin.annotationType())
+ .isEmpty();
+ default:
+ return true;
+ }
+ })
+ .toList());
+ }
+
+ /**
+ * 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<TestParameterValueHolder>> getAnnotationFromParametersOrTestOrClass(
+ AnnotationTypeOrigin annotationTypeOrigin, Method method, Class<?> testClass) {
+ Origin origin = annotationTypeOrigin.origin();
+ Class<? extends Annotation> annotationType = annotationTypeOrigin.annotationType();
+ if (origin == Origin.CONSTRUCTOR_PARAMETER) {
+ Constructor<?> constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass);
+ List<AnnotationWithMetadata> annotations =
+ getAnnotationWithMetadataListWithType(constructor, annotationType, testClass);
+
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.CONSTRUCTOR) {
+ Annotation annotation =
+ TestParameterInjectorUtils.getOnlyConstructor(testClass).getAnnotation(annotationType);
+ if (annotation != null) {
+ return ImmutableList.of(
+ TestParameterValueHolder.create(
+ AnnotationWithMetadata.withoutMetadata(
+ annotation,
+ GenericParameterContext.createWithoutParameterAnnotations(testClass)),
+ origin));
+ }
+
+ } else if (origin == Origin.METHOD_PARAMETER) {
+ List<AnnotationWithMetadata> annotations =
+ getAnnotationWithMetadataListWithType(method, annotationType, testClass);
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.METHOD) {
+ if (method.isAnnotationPresent(annotationType)) {
+ return ImmutableList.of(
+ TestParameterValueHolder.create(
+ AnnotationWithMetadata.withoutMetadata(
+ method.getAnnotation(annotationType),
+ GenericParameterContext.createWithoutParameterAnnotations(testClass)),
+ origin));
+ }
+ } else if (origin == Origin.FIELD) {
+ List<AnnotationWithMetadata> annotations =
+ new ArrayList<>(
+ FluentIterable.from(listWithParents(testClass))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields()))
+ .transformAndConcat(
+ field ->
+ FluentIterable.from(
+ getAnnotationListWithType(field.getAnnotations(), annotationType))
+ .transform(
+ annotation ->
+ AnnotationWithMetadata.withMetadata(
+ annotation,
+ field.getType(),
+ field.getName(),
+ GenericParameterContext.create(field, testClass))))
+ .toList());
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.CLASS) {
+ Annotation annotation = testClass.getAnnotation(annotationType);
+ if (annotation != null) {
+ return ImmutableList.of(
+ TestParameterValueHolder.create(
+ AnnotationWithMetadata.withoutMetadata(
+ annotation,
+ GenericParameterContext.createWithoutParameterAnnotations(testClass)),
+ origin));
+ }
+ }
+ return ImmutableList.of();
+ }
+
+ private static ImmutableList<List<TestParameterValueHolder>> toTestParameterValueList(
+ List<AnnotationWithMetadata> annotationWithMetadatas, Origin origin) {
+ return FluentIterable.from(annotationWithMetadatas)
+ .transform(
+ annotationWithMetadata ->
+ (List<TestParameterValueHolder>)
+ new ArrayList<>(
+ TestParameterValueHolder.create(annotationWithMetadata, origin)))
+ .toList();
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Method callable, Class<? extends Annotation> annotationType, Class<?> testClass) {
+ try {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameters(), annotationType, testClass);
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameterTypes(),
+ callable.getParameterAnnotations(),
+ annotationType,
+ testClass);
+ }
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Constructor<?> callable, Class<? extends Annotation> annotationType, Class<?> testClass) {
+ try {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameters(), annotationType, testClass);
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameterTypes(),
+ callable.getParameterAnnotations(),
+ annotationType,
+ testClass);
+ }
+ }
+
+ // 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, Class<?> testClass) {
+ return FluentIterable.from(parameters)
+ .transform(
+ parameter -> {
+ Annotation annotation = parameter.getAnnotation(annotationType);
+ return annotation == null
+ ? null
+ : parameter.isNamePresent()
+ ? AnnotationWithMetadata.withMetadata(
+ annotation,
+ parameter.getType(),
+ parameter.getName(),
+ GenericParameterContext.create(parameter, testClass))
+ : AnnotationWithMetadata.withMetadata(
+ annotation,
+ parameter.getType(),
+ GenericParameterContext.create(parameter, testClass));
+ })
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Class<?>[] parameterTypes,
+ Annotation[][] annotations,
+ Class<? extends Annotation> annotationType,
+ Class<?> testClass) {
+ 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],
+ GenericParameterContext.createWithRepeatableAnnotationsFallback(
+ annotations[i], testClass)));
+ }
+ }
+ }
+ return resultBuilder.build();
+ }
+
+ private ImmutableList<Annotation> getAnnotationListWithType(
+ Annotation[] annotations, Class<? extends Annotation> annotationType) {
+ return FluentIterable.from(annotations)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .toList();
+ }
+
+ @Override
+ public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ try {
+ if (testIndexHolder != null) {
+ List<TestParameterValueHolder> testParameterValues =
+ getParameterValuesForTest(testIndexHolder, testInfo.getTestClass());
+
+ // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER}
+ // annotations.
+ List<TestParameterValueHolder> testParameterValuesForFieldInjection =
+ filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD);
+ // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class
+ // in the example above.
+ List<TestParameterValueHolder> remainingTestParameterValuesForFieldInjection =
+ new ArrayList<>(testParameterValuesForFieldInjection);
+ for (Field declaredField :
+ FluentIterable.from(listWithParents(testInstance.getClass()))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields()))
+ .toList()) {
+ for (TestParameterValueHolder testParameterValue :
+ remainingTestParameterValuesForFieldInjection) {
+ if (declaredField.isAnnotationPresent(
+ testParameterValue.annotationTypeOrigin().annotationType())) {
+ if (testParameterValue.paramName().isPresent()
+ && !declaredField.getName().equals(testParameterValue.paramName().get())) {
+ // names don't match
+ continue;
+ }
+ declaredField.setAccessible(true);
+ declaredField.set(testInstance, testParameterValue.unwrappedValue());
+ remainingTestParameterValuesForFieldInjection.remove(testParameterValue);
+ break;
+ }
+ }
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns an {@link TestParameterValueHolder} list that contains only the values originating from
+ * one of the {@code origins}.
+ */
+ private static ImmutableList<TestParameterValueHolder> filterByOrigin(
+ List<TestParameterValueHolder> testParameterValues, Origin... origins) {
+ Set<Origin> originsToFilterBy = ImmutableSet.copyOf(origins);
+ return FluentIterable.from(testParameterValues)
+ .filter(
+ testParameterValue ->
+ originsToFilterBy.contains(testParameterValue.annotationTypeOrigin().origin()))
+ .toList();
+ }
+
+ /**
+ * 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 FluentIterable.from(annotationTypeOrigins)
+ .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin()))
+ .toList();
+ }
+
+ /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */
+ private Object getParameterValue(
+ List<TestParameterValueHolder> testParameterValues,
+ Class<?> methodParameterType,
+ Annotation[] parameterAnnotations,
+ List<Class<? extends Annotation>> processedAnnotationTypes) {
+ List<Class<? extends Annotation>> iteratedAnnotationTypes = new ArrayList<>();
+ for (TestParameterValueHolder 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.unwrappedValue();
+ }
+ iteratedAnnotationTypes.add(annotationType);
+ }
+ }
+ }
+ // If no annotation matches, use the method parameter type.
+ for (TestParameterValueHolder 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.unwrappedValue();
+ }
+ }
+ 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 getMethodsIncludingParentsSorted(testClass)} */
+ int methodIndex();
+
+ /**
+ * The index of the set of parameters to run the test method with in the list produced by {@link
+ * #getParameterValuesForMethod}.
+ */
+ 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() {}
+ }
+
+ /**
+ * Returns whether the test should be skipped according to the {@code annotationType}'s {@link
+ * TestParameterValidator} and the current list of {@link TestParameterValueHolder}.
+ */
+ private static boolean callShouldSkip(
+ Class<? extends Annotation> annotationType,
+ List<TestParameterValueHolder> 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<TestParameterValueHolder> testParameterValues;
+ private final Set<Object> valueList;
+
+ public ValidatorContext(List<TestParameterValueHolder> testParameterValues) {
+ this.testParameterValues = testParameterValues;
+ this.valueList =
+ FluentIterable.from(testParameterValues)
+ .transform(TestParameterValueHolder::unwrappedValue)
+ .filter(Objects::nonNull)
+ .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(TestParameterValueHolder::unwrappedValue);
+ }
+
+ @Override
+ public List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter) {
+ return getParameter(testParameter)
+ .transform(TestParameterValueHolder::specifiedValues)
+ .or(ImmutableList.of());
+ }
+
+ private Optional<TestParameterValueHolder> getParameter(
+ Class<? extends Annotation> testParameter) {
+ return FluentIterable.from(testParameterValues)
+ .firstMatch(value -> value.annotationTypeOrigin().annotationType().equals(testParameter));
+ }
+ }
+
+ /**
+ * 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, paramClass);
+ } 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<? extends Class<? extends Annotation>> getTestParameterAnnotations(
+ List<AnnotationTypeOrigin> annotationTypeOrigins,
+ final Class<?> testClass,
+ AnnotatedElement methodOrConstructor) {
+ return FluentIterable.from(annotationTypeOrigins)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .filter(
+ annotationType ->
+ testClass.isAnnotationPresent(annotationType)
+ || methodOrConstructor.isAnnotationPresent(annotationType))
+ .toList();
+ }
+
+ 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> getMethodsIncludingParentsSorted(Class<?> clazz) {
+ ImmutableList.Builder<Method> resultBuilder = ImmutableList.builder();
+ while (clazz != null) {
+ resultBuilder.add(clazz.getDeclaredMethods());
+ clazz = clazz.getSuperclass();
+ }
+ // Because getDeclaredMethods()'s order is not specified, there is the theoretical possibility
+ // that the order of methods is unstable. To partly fix this, we sort the result based on method
+ // name. This is still not perfect because of method overloading, but that should be
+ // sufficiently rare for test names.
+ return ImmutableList.sortedCopyOf(
+ Ordering.natural().onResultOf(Method::getName), resultBuilder.build());
+ }
+
+ private static ImmutableList<Class<?>> listWithParents(Class<?> clazz) {
+ ImmutableList.Builder<Class<?>> resultBuilder = ImmutableList.builder();
+
+ Class<?> currentClass = clazz;
+ while (currentClass != null) {
+ resultBuilder.add(currentClass);
+ currentClass = currentClass.getSuperclass();
+ }
+
+ return resultBuilder.build();
+ }
+}