aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/testing/junit/testparameterinjector
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/testing/junit/testparameterinjector')
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java83
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java233
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java226
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java412
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java25
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java308
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java99
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java54
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java224
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java266
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java1382
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java36
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java31
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java68
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java52
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java27
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java208
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java426
18 files changed, 4160 insertions, 0 deletions
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java b/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java
new file mode 100644
index 0000000..ab5003e
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java
@@ -0,0 +1,83 @@
+/*
+ * 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 java.lang.Math.min;
+
+import java.lang.annotation.Annotation;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Default base class for {@link TestParameterValidator}, simplifying how validators can exclude
+ * variable independent test parameters annotations.
+ */
+abstract class BaseTestParameterValidator implements TestParameterValidator {
+
+ @Override
+ public boolean shouldSkip(Context context) {
+ for (List<Class<? extends Annotation>> parameters : getIndependentParameters(context)) {
+ checkArgument(!parameters.isEmpty());
+ // For independent test parameters, the only allowed tests will be those that use the same
+ // Nth specified parameter, except for parameter values that have less specified values than
+ // others.
+
+ // For example, if parameter A has values a1 and a2, parameter B has values b1 and b2, and
+ // parameter C has values c1, c2 and c3, given that A, B and C are independent, the only
+ // tests that will not be skipped will be {(a1, b1, c1), (a2, b2, c2), (a2, b2, c3)},
+ // instead of 12 tests that would constitute their cartesian product.
+
+ // First, find the largest specified value count (parameter C in the example above),
+ // so that we can easily determine which parameter value should be used for validating the
+ // other parameters (e.g. should this test be for (a1, b1, c1), (a2, b2, c2), or
+ // (a2, b2, c3). The test parameter 'C' will be the 'leadingParameter'.
+ Class<? extends Annotation> leadingParameter =
+ parameters.stream()
+ .max(Comparator.comparing(parameter -> context.getSpecifiedValues(parameter).size()))
+ .get();
+ // Second, determine which index is the current value in the specified value list of
+ // the leading parameter. In the example above, the index of the current value 'c2' of the
+ // leading parameter 'C' would be '1', given the specified values (c1, c2, c3).
+ int leadingParameterValueIndex =
+ getValueIndex(context, leadingParameter, context.getValue(leadingParameter).get());
+ checkState(leadingParameterValueIndex >= 0);
+ // Each independent test parameter should be the same index, or the last available index.
+ // For example, if the parameter is A, and the leading parameter (C) index is 2, the A's index
+ // should be 1, since a2 is the only available value.
+ for (Class<? extends Annotation> parameter : parameters) {
+ List<Object> specifiedValues = context.getSpecifiedValues(parameter);
+ int valueIndex = specifiedValues.indexOf(context.getValue(parameter).get());
+ int requiredValueIndex = min(leadingParameterValueIndex, specifiedValues.size() - 1);
+ if (valueIndex != requiredValueIndex) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private int getValueIndex(Context context, Class<? extends Annotation> annotation, Object value) {
+ return context.getSpecifiedValues(annotation).indexOf(value);
+ }
+
+ /**
+ * Returns a list of TestParameterAnnotation annotated annotation types that are mutually
+ * independent, and therefore the combinations of their values do not need to be tested.
+ */
+ protected abstract List<List<Class<? extends Annotation>>> getIndependentParameters(
+ Context context);
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java
new file mode 100644
index 0000000..624ee9b
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java
@@ -0,0 +1,233 @@
+/*
+ * 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.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.function.Function.identity;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Primitives;
+import com.google.common.reflect.TypeToken;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.MessageLite;
+import java.lang.reflect.ParameterizedType;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+
+/** A helper class for parsing parameter values from strings. */
+final class ParameterValueParsing {
+
+ @SuppressWarnings("unchecked")
+ static <E extends Enum<E>> Enum<?> parseEnum(String str, Class<?> enumType) {
+ return Enum.valueOf((Class<E>) enumType, str);
+ }
+
+ static MessageLite parseTextprotoMessage(String textprotoString, Class<?> javaType) {
+ return getProtoValueParser().parseTextprotoMessage(textprotoString, javaType);
+ }
+
+ static boolean isValidYamlString(String yamlString) {
+ try {
+ new Yaml(new SafeConstructor()).load(yamlString);
+ return true;
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ static Object parseYamlStringToJavaType(String yamlString, Class<?> javaType) {
+ return parseYamlObjectToJavaType(parseYamlStringToObject(yamlString), TypeToken.of(javaType));
+ }
+
+ static Object parseYamlStringToObject(String yamlString) {
+ return new Yaml(new SafeConstructor()).load(yamlString);
+ }
+
+ @SuppressWarnings("unchecked")
+ static Object parseYamlObjectToJavaType(Object parsedYaml, TypeToken<?> javaType) {
+ // Pass along null so we don't have to worry about it below
+ if (parsedYaml == null) {
+ return null;
+ }
+
+ YamlValueTransformer yamlValueTransformer =
+ new YamlValueTransformer(parsedYaml, javaType.getRawType());
+
+ yamlValueTransformer
+ .ifJavaType(String.class)
+ .supportParsedType(String.class, identity())
+ // Also support other primitives because it's easy to accidentally write e.g. a number when
+ // a string was intended in YAML
+ .supportParsedType(Boolean.class, Object::toString)
+ .supportParsedType(Integer.class, Object::toString)
+ .supportParsedType(Long.class, Object::toString)
+ .supportParsedType(Double.class, Object::toString);
+
+ yamlValueTransformer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, identity());
+
+ yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, identity());
+
+ yamlValueTransformer
+ .ifJavaType(Long.class)
+ .supportParsedType(Long.class, identity())
+ .supportParsedType(Integer.class, Integer::longValue);
+
+ yamlValueTransformer
+ .ifJavaType(Float.class)
+ .supportParsedType(Float.class, identity())
+ .supportParsedType(Double.class, Double::floatValue)
+ .supportParsedType(Integer.class, Integer::floatValue);
+
+ yamlValueTransformer
+ .ifJavaType(Double.class)
+ .supportParsedType(Double.class, identity())
+ .supportParsedType(Integer.class, Integer::doubleValue)
+ .supportParsedType(Long.class, Long::doubleValue);
+
+ yamlValueTransformer
+ .ifJavaType(Enum.class)
+ .supportParsedType(
+ String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType()));
+
+ yamlValueTransformer
+ .ifJavaType(MessageLite.class)
+ .supportParsedType(String.class, str -> parseTextprotoMessage(str, javaType.getRawType()))
+ .supportParsedType(
+ Map.class,
+ map ->
+ getProtoValueParser()
+ .parseProtobufMessage((Map<String, Object>) map, javaType.getRawType()));
+
+ yamlValueTransformer
+ .ifJavaType(byte[].class)
+ .supportParsedType(byte[].class, identity())
+ .supportParsedType(String.class, s -> s.getBytes(StandardCharsets.UTF_8));
+
+ yamlValueTransformer
+ .ifJavaType(ByteString.class)
+ .supportParsedType(String.class, ByteString::copyFromUtf8)
+ .supportParsedType(byte[].class, ByteString::copyFrom);
+
+ // Added mainly for protocol buffer parsing
+ yamlValueTransformer
+ .ifJavaType(List.class)
+ .supportParsedType(
+ List.class,
+ list ->
+ Lists.transform(
+ list,
+ e ->
+ parseYamlObjectToJavaType(
+ e, getGenericParameterType(javaType, /* parameterIndex= */ 0))));
+ yamlValueTransformer
+ .ifJavaType(Map.class)
+ .supportParsedType(
+ Map.class,
+ map ->
+ Maps.transformValues(
+ map,
+ v ->
+ parseYamlObjectToJavaType(
+ v, getGenericParameterType(javaType, /* parameterIndex= */ 1))));
+
+ return yamlValueTransformer.transformedJavaValue();
+ }
+
+ private static TypeToken<?> getGenericParameterType(TypeToken<?> typeToken, int parameterIndex) {
+ checkArgument(
+ typeToken.getType() instanceof ParameterizedType,
+ "Could not parse the generic parameter of type %s",
+ typeToken);
+
+ ParameterizedType parameterizedType = (ParameterizedType) typeToken.getType();
+ return TypeToken.of(parameterizedType.getActualTypeArguments()[parameterIndex]);
+ }
+
+ private static final class YamlValueTransformer {
+ private final Object parsedYaml;
+ private final Class<?> javaType;
+ @Nullable private Object transformedJavaValue;
+
+ YamlValueTransformer(Object parsedYaml, Class<?> javaType) {
+ this.parsedYaml = parsedYaml;
+ this.javaType = javaType;
+ }
+
+ <JavaT> SupportedJavaType<JavaT> ifJavaType(Class<JavaT> supportedJavaType) {
+ return new SupportedJavaType<>(supportedJavaType);
+ }
+
+ Object transformedJavaValue() {
+ checkArgument(
+ transformedJavaValue != null,
+ "Could not map YAML value %s (class = %s) to java class %s",
+ parsedYaml,
+ parsedYaml.getClass(),
+ javaType);
+ return transformedJavaValue;
+ }
+
+ final class SupportedJavaType<JavaT> {
+
+ private final Class<JavaT> supportedJavaType;
+
+ private SupportedJavaType(Class<JavaT> supportedJavaType) {
+ this.supportedJavaType = supportedJavaType;
+ }
+
+ @SuppressWarnings("unchecked")
+ <ParsedYamlT> SupportedJavaType<JavaT> supportParsedType(
+ Class<ParsedYamlT> parsedYamlType, Function<ParsedYamlT, JavaT> transformation) {
+ if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) {
+ if (Primitives.wrap(parsedYamlType).isInstance(parsedYaml)) {
+ checkState(
+ transformedJavaValue == null,
+ "This case is already handled. This is a bug in"
+ + " testparameterinjector.TestParametersMethodProcessor.");
+ transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml));
+ }
+ }
+
+ return this;
+ }
+ }
+ }
+
+ static ProtoValueParsing getProtoValueParser() {
+ try {
+ // This is called reflectively so that the android target doesn't have to build in
+ // ProtoValueParsing, which has no Android-compatible target.
+ Class<?> clazz =
+ Class.forName("com.google.testing.junit.testparameterinjector.ProtoValueParsingImpl");
+ return (ProtoValueParsing) clazz.getDeclaredConstructor().newInstance();
+ } catch (ClassNotFoundException unused) {
+ throw new UnsupportedOperationException(
+ "Textproto support is not available when using the Android version of"
+ + " testparameterinjector.");
+ } catch (ReflectiveOperationException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private ParameterValueParsing() {}
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java
new file mode 100644
index 0000000..dbafc6a
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java
@@ -0,0 +1,226 @@
+/*
+ * 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.checkState;
+
+import com.google.auto.value.AutoAnnotation;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.text.MessageFormat;
+import java.util.List;
+import org.junit.runner.Description;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
+
+/**
+ * {@code TestMethodProcessor} implementation for supporting {@link org.junit.runners.Parameterized}
+ * tests.
+ *
+ * <p>Supports parameterized class if a method with the {@link Parameters} annotation is defined. As
+ * opposed to the junit {@link org.junit.runners.Parameterized} class, only one method can have the
+ * {@link Parameters} annotation, and has to be both public and static.
+ *
+ * <p>The {@link Parameters} annotated method can return either a {@code Collection<Object>} or a
+ * {@code Collection<Object[]>}.
+ *
+ * <p>Does not support injected {@link org.junit.runners.Parameterized.Parameter} fields, and
+ * instead requires a single class constructor with one argument for each parameter returned by the
+ * {@link Parameters} method.
+ */
+class ParameterizedTestMethodProcessor implements TestMethodProcessor {
+
+ /**
+ * The parameters as returned by the {@link Parameters} annotated method, or {@link
+ * Optional#absent()} if the class is not parameterized.
+ */
+ private final Optional<Iterable<?>> parametersForAllTests;
+ /**
+ * The test name pattern as defined by the 'name' attribute of the {@link Parameters} annotation,
+ * or {@link Optional#absent()} if the class is not parameterized.
+ */
+ private final Optional<String> testNamePattern;
+
+ ParameterizedTestMethodProcessor(TestClass testClass) {
+ Optional<FrameworkMethod> parametersMethod = getParametersMethod(testClass);
+ if (parametersMethod.isPresent()) {
+ Object parameters;
+ try {
+ parameters = parametersMethod.get().invokeExplosively(null);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ if (parameters instanceof Iterable) {
+ parametersForAllTests = Optional.<Iterable<?>>of((Iterable<?>) parameters);
+ } else if (parameters instanceof Object[]) {
+ parametersForAllTests =
+ Optional.<Iterable<?>>of(ImmutableList.copyOf((Object[]) parameters));
+ } else {
+ throw new IllegalStateException(
+ "Unsupported @Parameters return value type: " + parameters.getClass());
+ }
+ testNamePattern = Optional.of(parametersMethod.get().getAnnotation(Parameters.class).name());
+ } else {
+ parametersForAllTests = Optional.absent();
+ testNamePattern = Optional.absent();
+ }
+ }
+
+ @Override
+ public ValidationResult validateConstructor(TestClass testClass, List<Throwable> list) {
+ if (parametersForAllTests.isPresent()) {
+ if (testClass.getJavaClass().getConstructors().length != 1) {
+ list.add(
+ new IllegalStateException("Test class should have exactly one public constructor"));
+ return ValidationResult.HANDLED;
+ }
+ Constructor<?> constructor = testClass.getOnlyConstructor();
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ Object[] testParameters = getTestParameters(0);
+ if (parameterTypes.length != testParameters.length) {
+ list.add(
+ new IllegalStateException(
+ "Mismatch constructor parameter count with values"
+ + " returned by the @Parameters method"));
+ return ValidationResult.HANDLED;
+ }
+ for (int i = 0; i < testParameters.length; i++) {
+ if (!parameterTypes[i].isAssignableFrom(testParameters[i].getClass())) {
+ list.add(
+ new IllegalStateException(
+ String.format(
+ "Mismatch constructor parameter type %s with value"
+ + " returned by the @Parameters method: %s",
+ parameterTypes[i], testParameters[i])));
+ }
+ }
+ return ValidationResult.HANDLED;
+ }
+ return ValidationResult.NOT_HANDLED;
+ }
+
+ @Override
+ public ValidationResult validateTestMethod(
+ TestClass testClass, FrameworkMethod testMethod, List<Throwable> errorsReturned) {
+ return ValidationResult.NOT_HANDLED;
+ }
+
+ @Override
+ public List<TestInfo> processTest(Class<?> testClass, TestInfo originalTest) {
+ if (parametersForAllTests.isPresent()) {
+ ImmutableList.Builder<TestInfo> tests = ImmutableList.builder();
+ int testIndex = 0;
+ for (Object parameters : parametersForAllTests.get()) {
+ Object[] parametersForOneTest;
+ if (parameters instanceof Object[]) {
+ parametersForOneTest = (Object[]) parameters;
+ } else {
+ parametersForOneTest = new Object[] {parameters};
+ }
+ String namePattern = testNamePattern.get().replace("{index}", Integer.toString(testIndex));
+ String testParametersString = MessageFormat.format(namePattern, parametersForOneTest);
+ tests.add(
+ originalTest
+ .withExtraParameters(
+ ImmutableList.of(
+ TestInfoParameter.create(
+ testParametersString, parametersForOneTest, testIndex)))
+ .withExtraAnnotation(TestIndexHolderFactory.create(testIndex)));
+ testIndex++;
+ }
+ return tests.build();
+ }
+ return ImmutableList.of(originalTest);
+ }
+
+ @Override
+ public Statement processStatement(Statement originalStatement, Description finalTestDescription) {
+ return originalStatement;
+ }
+
+ @Override
+ public Optional<Object> createTest(
+ TestClass testClass, FrameworkMethod method, Optional<Object> test) {
+ if (parametersForAllTests.isPresent()) {
+ Object[] testParameters =
+ getTestParameters(method.getAnnotation(TestIndexHolder.class).testIndex());
+ try {
+ Constructor<?> constructor = testClass.getOnlyConstructor();
+ return Optional.<Object>of(constructor.newInstance(testParameters));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return test;
+ }
+
+ @Override
+ public Optional<Statement> createStatement(
+ TestClass testClass,
+ FrameworkMethod method,
+ Object testObject,
+ Optional<Statement> statement) {
+ return statement;
+ }
+
+ /**
+ * This mechanism is a workaround to be able to store the test index in the annotation list of the
+ * {@link TestInfo}, since we cannot carry other information through the test runner.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface TestIndexHolder {
+ int testIndex();
+ }
+
+ /** Factory for {@link TestIndexHolder}. */
+ static class TestIndexHolderFactory {
+ @AutoAnnotation
+ static TestIndexHolder create(int testIndex) {
+ return new AutoAnnotation_ParameterizedTestMethodProcessor_TestIndexHolderFactory_create(
+ testIndex);
+ }
+
+ private TestIndexHolderFactory() {}
+ }
+
+ private Object[] getTestParameters(int testIndex) {
+ Object parameters = Iterables.get(parametersForAllTests.get(), testIndex);
+ if (parameters instanceof Object[]) {
+ return (Object[]) parameters;
+ } else {
+ return new Object[] {parameters};
+ }
+ }
+
+ private Optional<FrameworkMethod> getParametersMethod(TestClass testClass) {
+ List<FrameworkMethod> methods = testClass.getAnnotatedMethods(Parameters.class);
+ if (methods.isEmpty()) {
+ return Optional.absent();
+ }
+ FrameworkMethod method = Iterables.getOnlyElement(methods);
+ checkState(
+ method.isPublic() && method.isStatic(),
+ "@Parameters method %s should be static and public",
+ method.getName());
+ return Optional.of(method);
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
new file mode 100644
index 0000000..2c9a199
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
@@ -0,0 +1,412 @@
+/*
+ * 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 java.util.Comparator.comparing;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.testing.junit.testparameterinjector.TestMethodProcessor.ValidationResult;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.junit.internal.runners.model.ReflectiveCallable;
+import org.junit.internal.runners.statements.Fail;
+import org.junit.rules.MethodRule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+/**
+ * Class to substitute JUnit4 runner in JUnit4 tests, adding additional functionality.
+ *
+ * <p>See {@link TestParameterInjector} for an example implementation.
+ */
+abstract class PluggableTestRunner extends BlockJUnit4ClassRunner {
+
+ /**
+ * A {@link ThreadLocal} is used to handle cases where multiple tests are executing in the same
+ * java process in different threads.
+ *
+ * <p>A null value indicates that the TestInfo hasn't been set yet, which would typically happen
+ * if the test hasn't yet started, or the {@link PluggableTestRunner} is not the test runner.
+ */
+ private static final ThreadLocal<TestInfo> currentTestInfo = new ThreadLocal<>();
+
+ private ImmutableList<TestRule> testRules;
+ private List<TestMethodProcessor> testMethodProcessors;
+
+ protected PluggableTestRunner(Class<?> klass) throws InitializationError {
+ super(klass);
+ }
+
+ /**
+ * Returns the list of {@link TestMethodProcessor}s to use. This is meant to be overridden by
+ * subclasses.
+ */
+ protected abstract List<TestMethodProcessor> createTestMethodProcessorList();
+
+ /**
+ * This method is run to perform optional additional operations on the test instance, right after
+ * it was created.
+ */
+ protected void finalizeCreatedTestInstance(Object testInstance) {
+ // Do nothing by default
+ }
+
+ /**
+ * If true, all test methods (across different TestMethodProcessors) will be sorted in a
+ * deterministic way by their test name.
+ *
+ * <p>Deterministic means that the order will not change, even when tests are added/removed or
+ * between releases.
+ */
+ protected boolean shouldSortTestMethodsDeterministically() {
+ return false; // Don't sort methods by default
+ }
+
+ /**
+ * {@link TestRule}s that will be executed after the ones defined in the test class (but still
+ * before all {@link MethodRule}s). This is meant to be overridden by subclasses.
+ */
+ protected List<TestRule> getInnerTestRules() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * {@link TestRule}s that will be executed before the ones defined in the test class. This is
+ * meant to be overridden by subclasses.
+ */
+ protected List<TestRule> getOuterTestRules() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * {@link MethodRule}s that will be executed after the ones defined in the test class. This is
+ * meant to be overridden by subclasses.
+ */
+ protected List<MethodRule> getInnerMethodRules() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * {@link MethodRule}s that will be executed before the ones defined in the test class (but still
+ * after all {@link TestRule}s). This is meant to be overridden by subclasses.
+ */
+ protected List<MethodRule> getOuterMethodRules() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Runs a {@code testClass} with the {@link PluggableTestRunner}, and returns a list of test
+ * {@link Failure}, or an empty list if no failure occurred.
+ */
+ @VisibleForTesting
+ public static ImmutableList<Failure> run(PluggableTestRunner testRunner) throws Exception {
+ final ImmutableList.Builder<Failure> failures = ImmutableList.builder();
+ RunNotifier notifier = new RunNotifier();
+ notifier.addFirstListener(
+ new RunListener() {
+ @Override
+ public void testFailure(Failure failure) throws Exception {
+ failures.add(failure);
+ }
+ });
+ testRunner.run(notifier);
+ return failures.build();
+ }
+
+ @Override
+ protected final ImmutableList<FrameworkMethod> computeTestMethods() {
+ Stream<FrameworkMethod> processedMethods =
+ super.computeTestMethods().stream().flatMap(method -> processMethod(method).stream());
+
+ if (shouldSortTestMethodsDeterministically()) {
+ processedMethods =
+ processedMethods.sorted(
+ comparing((FrameworkMethod method) -> method.getName().hashCode())
+ .thenComparing(FrameworkMethod::getName));
+ }
+
+ return processedMethods.collect(toImmutableList());
+ }
+
+ /** Implementation of a JUnit FrameworkMethod where the name and annotation list is overridden. */
+ private static class OverriddenFrameworkMethod extends FrameworkMethod {
+
+ private final TestInfo testInfo;
+
+ public OverriddenFrameworkMethod(Method method, TestInfo testInfo) {
+ super(method);
+ this.testInfo = testInfo;
+ }
+
+ public TestInfo getTestInfo() {
+ return testInfo;
+ }
+
+ @Override
+ public String getName() {
+ return testInfo.getName();
+ }
+
+ @Override
+ public Annotation[] getAnnotations() {
+ List<Annotation> annotations = testInfo.getAnnotations();
+ return annotations.toArray(new Annotation[0]);
+ }
+
+ @Override
+ public <T extends Annotation> T getAnnotation(final Class<T> annotationClass) {
+ return testInfo.getAnnotation(annotationClass);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PluggableTestRunner.OverriddenFrameworkMethod)) {
+ return false;
+ }
+
+ OverriddenFrameworkMethod other = (OverriddenFrameworkMethod) obj;
+ return super.equals(other) && other.testInfo.equals(testInfo);
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode() * 37 + testInfo.hashCode();
+ }
+ }
+
+ private ImmutableList<FrameworkMethod> processMethod(FrameworkMethod initialMethod) {
+ ImmutableList<TestInfo> testInfos =
+ ImmutableList.of(
+ TestInfo.createWithoutParameters(
+ initialMethod.getMethod(), ImmutableList.copyOf(initialMethod.getAnnotations())));
+
+ for (final TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
+ testInfos =
+ testInfos.stream()
+ .flatMap(
+ lastTestInfo ->
+ testMethodProcessor
+ .processTest(getTestClass().getJavaClass(), lastTestInfo)
+ .stream())
+ .collect(toImmutableList());
+ }
+
+ testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos));
+
+ return testInfos.stream()
+ .map(testInfo -> new OverriddenFrameworkMethod(testInfo.getMethod(), testInfo))
+ .collect(toImmutableList());
+ }
+
+ // Note: This is a copy of the parent implementation, except that instead of calling
+ // #createTest(), this method calls #createTestForMethod(method).
+ @Override
+ protected final Statement methodBlock(final FrameworkMethod method) {
+ Object testObject;
+ try {
+ testObject =
+ new ReflectiveCallable() {
+ @Override
+ protected Object runReflectiveCall() throws Throwable {
+ return createTestForMethod(method);
+ }
+ }.run();
+ } catch (Throwable e) {
+ return new Fail(e);
+ }
+
+ Statement statement = methodInvoker(method, testObject);
+ statement = possiblyExpectingExceptions(method, testObject, statement);
+ statement = withPotentialTimeout(method, testObject, statement);
+ statement = withBefores(method, testObject, statement);
+ statement = withAfters(method, testObject, statement);
+ statement = withRules(method, testObject, statement);
+ return statement;
+ }
+
+ @Override
+ protected final Statement methodInvoker(FrameworkMethod frameworkMethod, Object testObject) {
+ Optional<Statement> statement = Optional.absent();
+ for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
+ statement =
+ testMethodProcessor.createStatement(
+ getTestClass(), frameworkMethod, testObject, statement);
+ }
+ if (statement.isPresent()) {
+ return statement.get();
+ }
+ return super.methodInvoker(frameworkMethod, testObject);
+ }
+
+ /** Modifies the statement with each {@link MethodRule} and {@link TestRule} */
+ private Statement withRules(FrameworkMethod method, Object target, Statement statement) {
+ ImmutableList<TestRule> testRules =
+ Stream.of(
+ getTestRulesForProcessors().stream(),
+ getInnerTestRules().stream(),
+ getTestRules(target).stream(),
+ getOuterTestRules().stream())
+ .flatMap(x -> x)
+ .collect(toImmutableList());
+
+ Iterable<MethodRule> methodRules =
+ Iterables.concat(
+ Lists.reverse(getInnerMethodRules()),
+ rules(target),
+ Lists.reverse(getOuterMethodRules()));
+ for (MethodRule methodRule : methodRules) {
+ // For rules that implement both TestRule and MethodRule, only apply the TestRule.
+ if (!testRules.contains(methodRule)) {
+ statement = methodRule.apply(statement, method, target);
+ }
+ }
+ Description testDescription = describeChild(method);
+ for (TestRule testRule : testRules) {
+ statement = testRule.apply(statement, testDescription);
+ }
+ return new ContextMethodRule().apply(statement, method, target);
+ }
+
+ private Object createTestForMethod(FrameworkMethod method) throws Exception {
+ Optional<Object> maybeTestInstance = Optional.absent();
+ for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
+ maybeTestInstance = testMethodProcessor.createTest(getTestClass(), method, maybeTestInstance);
+ }
+ // If no processor created the test instance, fallback on the default implementation.
+ Object testInstance =
+ maybeTestInstance.isPresent() ? maybeTestInstance.get() : super.createTest();
+
+ finalizeCreatedTestInstance(testInstance);
+
+ return testInstance;
+ }
+
+ @Override
+ protected final void validateZeroArgConstructor(List<Throwable> errorsReturned) {
+ for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
+ if (testMethodProcessor.validateConstructor(getTestClass(), errorsReturned)
+ == ValidationResult.HANDLED) {
+ return;
+ }
+ }
+ super.validateZeroArgConstructor(errorsReturned);
+ }
+
+ @Override
+ protected final void validateTestMethods(List<Throwable> list) {
+ List<FrameworkMethod> testMethods = getTestClass().getAnnotatedMethods(Test.class);
+ for (FrameworkMethod testMethod : testMethods) {
+ boolean isHandled = false;
+ for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
+ if (testMethodProcessor.validateTestMethod(getTestClass(), testMethod, list)
+ == ValidationResult.HANDLED) {
+ isHandled = true;
+ break;
+ }
+ }
+ if (!isHandled) {
+ testMethod.validatePublicVoidNoArg(false /* isStatic */, list);
+ }
+ }
+ }
+
+ // Fix for ParentRunner bug:
+ // Overriding this method because a superclass (ParentRunner) is calling this in its constructor
+ // and then throwing an InitializationError that doesn't have any of the causes in the exception
+ // message.
+ @Override
+ protected final void collectInitializationErrors(List<Throwable> errors) {
+ super.collectInitializationErrors(errors);
+ if (!errors.isEmpty()) {
+ throw new RuntimeException(
+ String.format(
+ "Found %s issues while initializing the test runner:\n\n - %s\n\n\n",
+ errors.size(),
+ errors.stream()
+ .map(Throwables::getStackTraceAsString)
+ .collect(joining("\n\n\n - "))));
+ }
+ }
+
+ // Override this test as final because it is not (always) invoked
+ @Override
+ protected final Object createTest() throws Exception {
+ return super.createTest();
+ }
+
+ // Override this test as final because it is not (always) invoked
+ @Override
+ protected final void validatePublicVoidNoArgMethods(
+ Class<? extends Annotation> annotation, boolean isStatic, List<Throwable> errors) {
+ super.validatePublicVoidNoArgMethods(annotation, isStatic, errors);
+ }
+
+ private synchronized List<TestMethodProcessor> getTestMethodProcessors() {
+ if (testMethodProcessors == null) {
+ testMethodProcessors = createTestMethodProcessorList();
+ }
+ return testMethodProcessors;
+ }
+
+ private synchronized ImmutableList<TestRule> getTestRulesForProcessors() {
+ if (testRules == null) {
+ testRules =
+ testMethodProcessors.stream()
+ .map(testMethodProcessor -> (TestRule) testMethodProcessor::processStatement)
+ .collect(toImmutableList());
+ }
+ return testRules;
+ }
+
+ /** {@link MethodRule} that sets up the Context for each test. */
+ private static class ContextMethodRule implements MethodRule {
+ @Override
+ public Statement apply(Statement statement, FrameworkMethod method, Object o) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ currentTestInfo.set(((OverriddenFrameworkMethod) method).getTestInfo());
+ try {
+ statement.evaluate();
+ } finally {
+ currentTestInfo.set(null);
+ }
+ }
+ };
+ }
+ }
+
+ private static <E> Collector<E, ?, ImmutableList<E>> toImmutableList() {
+ return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java b/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java
new file mode 100644
index 0000000..61cf13b
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java
@@ -0,0 +1,25 @@
+/*
+ * 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 com.google.protobuf.MessageLite;
+import java.util.Map;
+
+/** A helper class for parsing proto values from strings. */
+interface ProtoValueParsing {
+ MessageLite parseTextprotoMessage(String textprotoString, Class<?> javaType);
+
+ MessageLite parseProtobufMessage(Map<String, Object> map, Class<?> javaType);
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
new file mode 100644
index 0000000..7d16b6e
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
@@ -0,0 +1,308 @@
+/*
+ * 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.checkNotNull;
+import static java.lang.Math.min;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import javax.annotation.Nullable;
+
+/** A POJO containing information about a test (name and anotations). */
+@AutoValue
+abstract class TestInfo {
+
+ /**
+ * The maximum amount of characters that {@link #getName()} can have.
+ *
+ * <p>See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max
+ * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced
+ * by an additional 24 characters to account for future changes.
+ */
+ static final int MAX_TEST_NAME_LENGTH = 200;
+
+ /** The maximum amount of characters that a single parameter can take up in {@link #getName()}. */
+ static final int MAX_PARAMETER_NAME_LENGTH = 100;
+
+ public abstract Method getMethod();
+
+ public String getName() {
+ if (getParameters().isEmpty()) {
+ return getMethod().getName();
+ } else {
+ return String.format(
+ "%s[%s]",
+ getMethod().getName(),
+ getParameters().stream().map(TestInfoParameter::getName).collect(joining(",")));
+ }
+ }
+
+ abstract ImmutableList<TestInfoParameter> getParameters();
+
+ public abstract ImmutableList<Annotation> getAnnotations();
+
+ @Nullable
+ public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
+ for (Annotation annotation : getAnnotations()) {
+ if (annotationClass.isInstance(annotation)) {
+ return annotationClass.cast(annotation);
+ }
+ }
+ return null;
+ }
+
+ TestInfo withExtraParameters(List<TestInfoParameter> parameters) {
+ return new AutoValue_TestInfo(
+ getMethod(),
+ ImmutableList.<TestInfoParameter>builder()
+ .addAll(this.getParameters())
+ .addAll(parameters)
+ .build(),
+ getAnnotations());
+ }
+
+ TestInfo withExtraAnnotation(Annotation annotation) {
+ ImmutableList<Annotation> newAnnotations =
+ ImmutableList.<Annotation>builder().addAll(this.getAnnotations()).add(annotation).build();
+ return new AutoValue_TestInfo(getMethod(), getParameters(), newAnnotations);
+ }
+
+ /**
+ * Returns a new TestInfo instance with updated parameter names.
+ *
+ * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link
+ * #getParameters()} list to the new name.
+ */
+ private TestInfo withUpdatedParameterNames(
+ BiFunction<TestInfoParameter, Integer, String> parameterWithIndexToNewName) {
+ return new AutoValue_TestInfo(
+ getMethod(),
+ IntStream.range(0, getParameters().size())
+ .mapToObj(
+ parameterIndex -> {
+ TestInfoParameter parameter = getParameters().get(parameterIndex);
+ return parameter.withName(
+ parameterWithIndexToNewName.apply(parameter, parameterIndex));
+ })
+ .collect(toImmutableList()),
+ getAnnotations());
+ }
+
+ public static TestInfo legacyCreate(Method method, String name, List<Annotation> annotations) {
+ return new AutoValue_TestInfo(
+ method, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
+ }
+
+ static TestInfo createWithoutParameters(Method method, List<Annotation> annotations) {
+ return new AutoValue_TestInfo(
+ method, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
+ }
+
+ static ImmutableList<TestInfo> shortenNamesIfNecessary(List<TestInfo> testInfos) {
+ if (testInfos.stream()
+ .anyMatch(
+ info ->
+ info.getName().length() > MAX_TEST_NAME_LENGTH
+ || info.getParameters().stream()
+ .anyMatch(param -> param.getName().length() > MAX_PARAMETER_NAME_LENGTH))) {
+ int numberOfParameters = testInfos.get(0).getParameters().size();
+
+ if (numberOfParameters == 0) {
+ return ImmutableList.copyOf(testInfos);
+ } else {
+ Set<Integer> parameterIndicesThatNeedUpdate =
+ IntStream.range(0, numberOfParameters)
+ .filter(
+ parameterIndex ->
+ testInfos.stream()
+ .anyMatch(
+ info ->
+ info.getParameters().get(parameterIndex).getName().length()
+ > getMaxCharactersPerParameter(info, numberOfParameters)))
+ .boxed()
+ .collect(toSet());
+
+ return testInfos.stream()
+ .map(
+ info ->
+ info.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ parameterIndicesThatNeedUpdate.contains(parameterIndex)
+ ? getShortenedName(
+ parameter,
+ getMaxCharactersPerParameter(info, numberOfParameters))
+ : info.getParameters().get(parameterIndex).getName()))
+ .collect(toImmutableList());
+ }
+ } else {
+ return ImmutableList.copyOf(testInfos);
+ }
+ }
+
+ private static int getMaxCharactersPerParameter(TestInfo testInfo, int numberOfParameters) {
+ int maxLengthOfAllParameters =
+ // Subtract 2 characters for square brackets
+ MAX_TEST_NAME_LENGTH - testInfo.getMethod().getName().length() - 2;
+ return min(
+ // Subtract 4 characters to leave place for joining commas and the parameter index.
+ maxLengthOfAllParameters / numberOfParameters - 4,
+ // Subtract 3 characters to leave place for the parameter index
+ MAX_PARAMETER_NAME_LENGTH - 3);
+ }
+
+ static ImmutableList<TestInfo> deduplicateTestNames(List<TestInfo> testInfos) {
+ long uniqueTestNameCount = testInfos.stream().map(TestInfo::getName).distinct().count();
+ if (testInfos.size() == uniqueTestNameCount) {
+ // Return early if there are no duplicates
+ return ImmutableList.copyOf(testInfos);
+ } else {
+ return deduplicateWithNumberPrefixes(maybeAddTypesIfDuplicate(testInfos));
+ }
+ }
+
+ private static String getShortenedName(
+ TestInfoParameter parameter, int maxCharactersPerParameter) {
+ if (maxCharactersPerParameter < 4) {
+ // Not enough characters for "..." suffix
+ return String.valueOf(parameter.getIndexInValueSource() + 1);
+ } else {
+ String shortenedName =
+ parameter.getName().length() > maxCharactersPerParameter
+ ? parameter.getName().substring(0, maxCharactersPerParameter - 3) + "..."
+ : parameter.getName();
+ return String.format("%s.%s", parameter.getIndexInValueSource() + 1, shortenedName);
+ }
+ }
+
+ private static ImmutableList<TestInfo> maybeAddTypesIfDuplicate(List<TestInfo> testInfos) {
+ Multimap<String, TestInfo> testNameToInfo =
+ MultimapBuilder.linkedHashKeys().arrayListValues().build();
+ for (TestInfo testInfo : testInfos) {
+ testNameToInfo.put(testInfo.getName(), testInfo);
+ }
+
+ return testNameToInfo.keySet().stream()
+ .flatMap(
+ testName -> {
+ Collection<TestInfo> matchedInfos = testNameToInfo.get(testName);
+ if (matchedInfos.size() == 1) {
+ // There was only one method with this name, so no deduplication is necessary
+ return matchedInfos.stream();
+ } else {
+ // Found tests with duplicate test names
+ int numParameters = matchedInfos.iterator().next().getParameters().size();
+ Set<Integer> indicesThatShouldGetSuffix =
+ // Find parameter indices for which a suffix would allow the reader to
+ // differentiate
+ IntStream.range(0, numParameters)
+ .filter(
+ parameterIndex ->
+ matchedInfos.stream()
+ .map(
+ info ->
+ getTypeSuffix(
+ info.getParameters()
+ .get(parameterIndex)
+ .getValue()))
+ .distinct()
+ .count()
+ > 1)
+ .boxed()
+ .collect(toSet());
+
+ return matchedInfos.stream()
+ .map(
+ testInfo ->
+ testInfo.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ indicesThatShouldGetSuffix.contains(parameterIndex)
+ ? parameter.getName() + getTypeSuffix(parameter.getValue())
+ : parameter.getName()));
+ }
+ })
+ .collect(toImmutableList());
+ }
+
+ private static String getTypeSuffix(@Nullable Object value) {
+ if (value == null) {
+ return " (null reference)";
+ } else {
+ return String.format(" (%s)", value.getClass().getSimpleName());
+ }
+ }
+
+ private static ImmutableList<TestInfo> deduplicateWithNumberPrefixes(
+ ImmutableList<TestInfo> testInfos) {
+ long uniqueTestNameCount = testInfos.stream().map(TestInfo::getName).distinct().count();
+ if (testInfos.size() == uniqueTestNameCount) {
+ return ImmutableList.copyOf(testInfos);
+ } else {
+ // There are still duplicates, even after adding type suffixes. As a last resort: add a
+ // counter to all parameters to guarantee that each case is unique.
+ return testInfos.stream()
+ .map(
+ testInfo ->
+ testInfo.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ String.format(
+ "%s.%s", parameter.getIndexInValueSource() + 1, parameter.getName())))
+ .collect(toImmutableList());
+ }
+ }
+
+ private static <E> Collector<E, ?, ImmutableList<E>> toImmutableList() {
+ return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
+ }
+
+ @AutoValue
+ abstract static class TestInfoParameter {
+
+ abstract String getName();
+
+ @Nullable
+ abstract Object getValue();
+
+ /**
+ * The index of this parameter value in the list of all values provided by the provider that
+ * returned this value.
+ */
+ abstract int getIndexInValueSource();
+
+ TestInfoParameter withName(String newName) {
+ return create(newName, getValue(), getIndexInValueSource());
+ }
+
+ static TestInfoParameter create(String name, @Nullable Object value, int indexInValueSource) {
+ checkArgument(indexInValueSource >= 0);
+ return new AutoValue_TestInfo_TestInfoParameter(
+ checkNotNull(name), value, indexInValueSource);
+ }
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java
new file mode 100644
index 0000000..880327f
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java
@@ -0,0 +1,99 @@
+/*
+ * 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 com.google.common.base.Optional;
+import java.util.List;
+import org.junit.runner.Description;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
+
+/**
+ * Interface to change the list of methods used in a test.
+ *
+ * <p>Note: Implementations of this interface are expected to be immutable, i.e. they no longer
+ * change after construction.
+ */
+interface TestMethodProcessor {
+
+ /** Allows to transform the test information (name and annotations). */
+ List<TestInfo> processTest(Class<?> testClass, TestInfo originalTest);
+
+ /**
+ * Allows to change the code executed during the test.
+ *
+ * @param finalTestDescription the final description calculated taking into account this and all
+ * other test processors
+ */
+ Statement processStatement(Statement originalStatement, Description finalTestDescription);
+
+ /**
+ * This method allows to transform the test object used for {@link #processStatement(Statement,
+ * Description)}.
+ *
+ * @param test the value returned by the previous processor, or {@link Optional#absent()} if this
+ * processor is the first.
+ * @return {@link Optional#absent()} if the default test instance will be used from instantiating
+ * the test class with the default constructor.
+ * <p>The default implementation should return {@code test}.
+ */
+ Optional<Object> createTest(TestClass testClass, FrameworkMethod method, Optional<Object> test);
+
+ /**
+ * This method allows to transform the statement object used for {@link
+ * #processStatement(Statement, Description)}.
+ *
+ * @param statement the value returned by the previous processor, or {@link Optional#absent()} if
+ * this processor is the first.
+ * @return {@link Optional#absent()} if the default statement will be used from invoking the test
+ * method with no parameters.
+ * <p>The default implementation should return {@code statement}.
+ */
+ Optional<Statement> createStatement(
+ TestClass testClass,
+ FrameworkMethod method,
+ Object testObject,
+ Optional<Statement> statement);
+
+ /**
+ * Optionally validates the {@code testClass} constructor, and returns whether the validation
+ * should continue or stop.
+ *
+ * @param errorsReturned A mutable list that any validation error should be added to.
+ */
+ ValidationResult validateConstructor(TestClass testClass, List<Throwable> errorsReturned);
+
+ /**
+ * Optionally validates the {@code testClass} methods, and returns whether the validation should
+ * continue or stop.
+ *
+ * @param errorsReturned A mutable list that any validation error should be added to.
+ */
+ ValidationResult validateTestMethod(
+ TestClass testClass, FrameworkMethod testMethod, List<Throwable> errorsReturned);
+
+ /**
+ * Whether the constructor or method validation has been handled or not.
+ *
+ * <p>If the validation is not handled by a processor, it will be handled using the default {@link
+ * BlockJUnit4ClassRunner} validator.
+ */
+ enum ValidationResult {
+ NOT_HANDLED,
+ HANDLED,
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java
new file mode 100644
index 0000000..b6dc4c2
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java
@@ -0,0 +1,54 @@
+/*
+ * 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 com.google.common.collect.ImmutableList;
+import org.junit.runners.model.TestClass;
+
+/** Factory for all {@link TestMethodProcessor} implementations that this package supports. */
+final class TestMethodProcessors {
+
+ /**
+ * Returns a new instance of every {@link TestMethodProcessor} implementation that this package
+ * supports.
+ *
+ * <p>Note that this includes support for {@link org.junit.runners.Parameterized}.
+ */
+ public static ImmutableList<TestMethodProcessor>
+ createNewParameterizedProcessorsWithLegacyFeatures(TestClass testClass) {
+ return ImmutableList.of(
+ new ParameterizedTestMethodProcessor(testClass),
+ new TestParametersMethodProcessor(testClass),
+ TestParameterAnnotationMethodProcessor.forAllAnnotationPlacements(testClass));
+ }
+
+ /**
+ * Returns a new instance of every {@link TestMethodProcessor} implementation that this package
+ * supports, except the following legacy features:
+ *
+ * <ul>
+ * <li>No support for {@link org.junit.runners.Parameterized}
+ * <li>No support for class and method-level parameters, except for @TestParameters
+ * </ul>
+ */
+ public static ImmutableList<TestMethodProcessor> createNewParameterizedProcessors(
+ TestClass testClass) {
+ return ImmutableList.of(
+ new TestParametersMethodProcessor(testClass),
+ TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters(testClass));
+ }
+
+ private TestMethodProcessors() {}
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
new file mode 100644
index 0000000..6725d16
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
@@ -0,0 +1,224 @@
+/*
+ * 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.checkState;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Primitives;
+import com.google.protobuf.MessageLite;
+import com.google.testing.junit.testparameterinjector.TestParameter.InternalImplementationOfThisParameter;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Test parameter annotation that defines the values that a single parameter can have.
+ *
+ * <p>For enums and booleans, the values can be automatically derived as all possible values:
+ *
+ * <pre>
+ * {@literal @}Test
+ * public void test1(@TestParameter MyEnum myEnum, @TestParameter boolean myBoolean) {
+ * // ... will run for [(A,false), (A,true), (B,false), (B,true), (C,false), (C,true)]
+ * }
+ *
+ * enum MyEnum { A, B, C }
+ * </pre>
+ *
+ * <p>The values can be explicitly defined as a parsed string:
+ *
+ * <pre>
+ * public void test1(
+ * {@literal @}TestParameter({"{name: Hermione, age: 18}", "{name: Dumbledore, age: 115}"})
+ * UpdateCharacterRequest request,
+ * {@literal @}TestParameter({"1", "4"}) int bookNumber) {
+ * // ... will run for [(Hermione,1), (Hermione,4), (Dumbledore,1), (Dumbledore,4)]
+ * }
+ * </pre>
+ *
+ * <p>For more flexibility, see {{@link #valuesProvider()}}. If you don't want to test all possible
+ * combinations but instead want to specify sets of parameters explicitly, use @{@link
+ * TestParameters}.
+ */
+@Retention(RUNTIME)
+@Target({FIELD, PARAMETER})
+@TestParameterAnnotation(valueProvider = InternalImplementationOfThisParameter.class)
+public @interface TestParameter {
+
+ /**
+ * Array of stringified values for the annotated type.
+ *
+ * <p>Types that are supported:
+ *
+ * <ul>
+ * <li>String: No parsing happens
+ * <li>boolean: Specified as YAML boolean
+ * <li>long and int: Specified as YAML integer
+ * <li>float and double: Specified as YAML floating point or integer
+ * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()}
+ * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes
+ * (example: "!!binary 'ZGF0YQ=='")
+ * </ul>
+ *
+ * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link
+ * #valuesProvider()} and leave this field empty.
+ *
+ * <p>For examples, see {@link TestParameter}.
+ */
+ String[] value() default {};
+
+ /**
+ * Sets a provider that will return a list of parameter values.
+ *
+ * <p>If this field is set, {@link #value()} must be empty and vice versa.
+ *
+ * <p><b>Example</b>
+ *
+ * <pre>
+ * {@literal @}Test
+ * public void matchesAllOf_throwsOnNull(
+ * {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
+ * CharMatcher charMatcher) {
+ * assertThrows(NullPointerException.class, () -&gt; charMatcher.matchesAllOf(null));
+ * }
+ *
+ * private static final class CharMatcherProvider implements TestParameterValuesProvider {
+ * {@literal @}Override
+ * public {@literal List<CharMatcher>} provideValues() {
+ * return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
+ * }
+ * }
+ * </pre>
+ */
+ Class<? extends TestParameterValuesProvider> valuesProvider() default
+ DefaultTestParameterValuesProvider.class;
+
+ /** Interface for custom providers of test parameter values. */
+ interface TestParameterValuesProvider {
+ List<?> provideValues();
+ }
+
+ /** Default {@link TestParameterValuesProvider} implementation that does nothing. */
+ class DefaultTestParameterValuesProvider implements TestParameterValuesProvider {
+ @Override
+ public List<Object> provideValues() {
+ return ImmutableList.of();
+ }
+ }
+
+ /** Implementation of this parameter annotation. */
+ final class InternalImplementationOfThisParameter implements TestParameterValueProvider {
+ @Override
+ public List<Object> provideValues(
+ Annotation uncastAnnotation, Optional<Class<?>> maybeParameterClass) {
+ TestParameter annotation = (TestParameter) uncastAnnotation;
+ Class<?> parameterClass = getValueType(annotation.annotationType(), maybeParameterClass);
+
+ boolean valueIsSet = annotation.value().length > 0;
+ boolean valuesProviderIsSet =
+ !annotation.valuesProvider().equals(DefaultTestParameterValuesProvider.class);
+ checkState(
+ !(valueIsSet && valuesProviderIsSet),
+ "It is not allowed to specify both value and valuesProvider on annotation %s",
+ annotation);
+
+ if (valueIsSet) {
+ return stream(annotation.value())
+ .map(v -> parseStringValue(v, parameterClass))
+ .collect(toList());
+ } else if (valuesProviderIsSet) {
+ return getValuesFromProvider(annotation.valuesProvider());
+ } else {
+ if (Enum.class.isAssignableFrom(parameterClass)) {
+ return ImmutableList.copyOf(parameterClass.asSubclass(Enum.class).getEnumConstants());
+ } else if (Primitives.wrap(parameterClass).equals(Boolean.class)) {
+ return ImmutableList.of(false, true);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "A @TestParameter without values can only be placed at an enum or a boolean, but"
+ + " was placed by a %s",
+ parameterClass));
+ }
+ }
+ }
+
+ @Override
+ public Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) {
+ return parameterClass.orElseThrow(
+ () ->
+ new AssertionError(
+ String.format(
+ "An empty parameter class should not be possible since"
+ + " @TestParameter can only target FIELD or PARAMETER, both"
+ + " of which are supported for annotation %s.",
+ annotationType)));
+ }
+
+ private static Object parseStringValue(String value, Class<?> parameterClass) {
+ if (parameterClass.equals(String.class)) {
+ return value.equals("null") ? null : value;
+ } else if (Enum.class.isAssignableFrom(parameterClass)) {
+ return value.equals("null") ? null : ParameterValueParsing.parseEnum(value, parameterClass);
+ } else if (MessageLite.class.isAssignableFrom(parameterClass)) {
+ if (ParameterValueParsing.isValidYamlString(value)) {
+ return ParameterValueParsing.parseYamlStringToJavaType(value, parameterClass);
+ } else {
+ return ParameterValueParsing.parseTextprotoMessage(value, parameterClass);
+ }
+ } else {
+ return ParameterValueParsing.parseYamlStringToJavaType(value, parameterClass);
+ }
+ }
+
+ private static List<Object> getValuesFromProvider(
+ Class<? extends TestParameterValuesProvider> valuesProvider) {
+ try {
+ Constructor<? extends TestParameterValuesProvider> constructor =
+ valuesProvider.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return new ArrayList<>(constructor.newInstance().provideValues());
+ } catch (NoSuchMethodException e) {
+ if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s, probably because it is a not-static"
+ + " inner class. You can fix this by making %s static.",
+ valuesProvider.getSimpleName(), valuesProvider.getSimpleName()),
+ e);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()),
+ e);
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java
new file mode 100644
index 0000000..a859a4f
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java
@@ -0,0 +1,266 @@
+/*
+ * 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.checkState;
+import static com.google.common.base.Verify.verify;
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Primitives;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Annotation to define a test annotation used to have parameterized methods, in either a
+ * parameterized or non parameterized test.
+ *
+ * <p>Parameterized tests enabled by defining a annotation (see {@link TestParameter} as an example)
+ * for the type of the parameter, defining a member variable annotated with this annotation, and
+ * specifying the parameter with the same annotation for each test, or for the whole class, for
+ * example:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @ColorParameter({BLUE, WHITE, RED}) private Color color;
+ *
+ * @Test
+ * public void test() {
+ * assertThat(paint(color)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>An alternative is to use a method parameter for injection:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @Test
+ * @ColorParameter({BLUE, WHITE, RED})
+ * public void test(Color color) {
+ * assertThat(paint(color)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Yet another alternative is to use a method parameter for injection, but with the annotation
+ * specified on the parameter itself, which helps when multiple arguments share the
+ * same @TestParameterAnnotation annotation.
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @Test
+ * public void test(@ColorParameter({BLUE, WHITE}) Color color1,
+ * @ColorParameter({WHITE, RED}) Color color2) {
+ * assertThat(paint(color1. color2)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown
+ * below:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * public @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * public ColorTest(@ColorParameter({BLUE, WHITE}) Color color) {
+ * ...
+ * }
+ *
+ * @Test
+ * public void test() {...}
+ * }
+ * }</pre>
+ *
+ * <p>Each field that needs to be injected from a parameter requires its dedicated distinct
+ * annotation.
+ *
+ * <p>If the same annotation is defined both on the class and method, the method parameter values
+ * take precedence.
+ *
+ * <p>If the same annotation is defined both on the class and constructor, the constructor parameter
+ * values take precedence.
+ *
+ * <p>Annotations cannot be duplicated between the constructor or constructor parameters and a
+ * method or method parameter.
+ *
+ * <p>Since the parameter values must be specified in an annotation return value, they are
+ * restricted to the annotation method return type set (primitive, Class, Enum, String, etc...). If
+ * parameters have to be dynamically generated, the conventional Parameterized mechanism with {@code
+ * Parameters} has to be used instead.
+ */
+@Retention(RUNTIME)
+@Target({ANNOTATION_TYPE})
+@interface TestParameterAnnotation {
+ /**
+ * Pattern of the {@link MessageFormat} format to derive the test's name from the parameters.
+ *
+ * @see {@code Parameters#name()}
+ */
+ String name() default "{0}";
+
+ /** Specifies a validator for the parameter to determine whether test should be skipped. */
+ Class<? extends TestParameterValidator> validator() default DefaultValidator.class;
+
+ /**
+ * Specifies a processor for the parameter to invoke arbitrary code before and after the test
+ * statement's execution.
+ */
+ Class<? extends TestParameterProcessor> processor() default DefaultProcessor.class;
+
+ /** Specifies a value provider for the parameter to provide the values to test. */
+ Class<? extends TestParameterValueProvider> valueProvider() default DefaultValueProvider.class;
+
+ /** Default {@link TestParameterValidator} implementation which skips no test. */
+ class DefaultValidator implements TestParameterValidator {
+
+ @Override
+ public boolean shouldSkip(Context context) {
+ return false;
+ }
+ }
+
+ /** Default {@link TestParameterProcessor} implementation which does nothing. */
+ class DefaultProcessor implements TestParameterProcessor {
+ @Override
+ public void before(Object testParameterValue) {}
+
+ @Override
+ public void after(Object testParameterValue) {}
+ }
+
+ /**
+ * Default {@link TestParameterValueProvider} implementation that gets its values from the
+ * annotation's `value` method.
+ */
+ class DefaultValueProvider implements TestParameterValueProvider {
+
+ @Override
+ public List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) {
+ Object parameters = getParametersAnnotationValues(annotation, annotation.annotationType());
+ checkState(
+ parameters.getClass().isArray(),
+ "The return value of the value method should be an array");
+
+ int parameterCount = Array.getLength(parameters);
+ ImmutableList.Builder<Object> resultBuilder = ImmutableList.builder();
+ for (int i = 0; i < parameterCount; i++) {
+ Object value = Array.get(parameters, i);
+ if (parameterClass.isPresent()) {
+ verify(
+ Primitives.wrap(parameterClass.get()).isInstance(value),
+ "Found %s annotation next to a parameter of type %s which doesn't match"
+ + " (annotation = %s)",
+ annotation.annotationType().getSimpleName(),
+ parameterClass.get().getSimpleName(),
+ annotation);
+ }
+ resultBuilder.add(value);
+ }
+ return resultBuilder.build();
+ }
+
+ @Override
+ public Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) {
+ try {
+ Method valueMethod = annotationType.getMethod("value");
+ return valueMethod.getReturnType().getComponentType();
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(
+ "The @TestParameterAnnotation annotation should have a single value() method.", e);
+ }
+ }
+
+ /**
+ * Returns the parameters of the test parameter, by calling the {@code value} method on the
+ * annotation.
+ */
+ private static Object getParametersAnnotationValues(
+ Annotation annotation, Class<? extends Annotation> annotationType) {
+ Method valueMethod;
+ try {
+ valueMethod = annotationType.getMethod("value");
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(
+ "The @TestParameterAnnotation annotation should have a single value() method.", e);
+ }
+ Object parameters;
+ try {
+ parameters = valueMethod.invoke(annotation);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof IllegalAccessError) {
+ // There seems to be a bug or at least something weird with the JVM that causes
+ // IllegalAccessError to be thrown because the return value is not visible when it is a
+ // non-public nested type. See
+ // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more
+ // info.
+ throw new RuntimeException(
+ String.format(
+ "Could not access %s.value(). This is probably because %s is not visible to the"
+ + " annotation proxy. To fix this, make %s public.",
+ annotationType.getSimpleName(),
+ valueMethod.getReturnType().getSimpleName(),
+ valueMethod.getReturnType().getSimpleName()));
+ // Note: Not chaining the exception to reduce the clutter for the reader
+ } else {
+ throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e);
+ }
+ return parameters;
+ }
+ }
+}
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);
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java
new file mode 100644
index 0000000..dd6c63f
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java
@@ -0,0 +1,36 @@
+/*
+ * 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 java.util.List;
+import org.junit.runners.model.InitializationError;
+
+/**
+ * A JUnit test runner which knows how to instantiate and run test classes where each test case may
+ * be parameterized with its own unique set of test parameters (as opposed to {@link
+ * org.junit.runners.Parameterized} where each test case in a test class is invoked with the exact
+ * same set of parameters).
+ */
+public final class TestParameterInjector extends PluggableTestRunner {
+
+ public TestParameterInjector(Class<?> testClass) throws InitializationError {
+ super(testClass);
+ }
+
+ @Override
+ protected List<TestMethodProcessor> createTestMethodProcessorList() {
+ return TestMethodProcessors.createNewParameterizedProcessorsWithLegacyFeatures(getTestClass());
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java
new file mode 100644
index 0000000..efa4951
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Interface which allows {@link TestParameterAnnotation} annotations to run arbitrary code before
+ * and after test execution.
+ *
+ * <p>When multiple TestParameterAnnotation processors exist for a single test, they are executed in
+ * declaration order, starting with annotations defined at the class, field, method, and finally
+ * parameter level.
+ */
+interface TestParameterProcessor {
+ /** Executes code in the context of a running test statement before the statement starts. */
+ void before(Object testParameterValue);
+
+ /** Executes code in the context of a running test statement after the statement completes. */
+ void after(Object testParameterValue);
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java
new file mode 100644
index 0000000..3733833
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java
@@ -0,0 +1,68 @@
+/*
+ * 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 com.google.common.base.Optional;
+import java.lang.annotation.Annotation;
+import java.util.List;
+
+/**
+ * Validator interface which allows {@link TestParameterAnnotation} annotations to validate the set
+ * of annotation values for a given test instance, and to selectively skip the test.
+ */
+interface TestParameterValidator {
+
+ /**
+ * This interface allows to access information on the current testwhen implementing {@link
+ * TestParameterValidator}.
+ */
+ interface Context {
+
+ /** Returns whether the current test has the {@link TestParameterAnnotation} value(s). */
+ boolean has(Class<? extends Annotation> testParameter, Object value);
+
+ /**
+ * Returns whether the current test has the two {@link TestParameterAnnotation} values, granted
+ * that the value is an enum, and each enum corresponds to a unique annotation.
+ */
+ <T extends Enum<T>, U extends Enum<U>> boolean has(T value1, U value2);
+
+ /**
+ * Returns all the current test value for a given {@link TestParameterAnnotation} annotated
+ * annotation.
+ */
+ Optional<Object> getValue(Class<? extends Annotation> testParameter);
+
+ /**
+ * Returns all the values specified for a given {@link TestParameterAnnotation} annotated
+ * annotation in the test.
+ *
+ * <p>For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will
+ * return [a,b,c].
+ */
+ List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter);
+ }
+
+ /**
+ * Returns whether the test should be skipped based on the annotations' values.
+ *
+ * <p>The {@code testParameterValues} list contains all {@link TestParameterAnnotation}
+ * annotations, including those specified at the class, field, method, method parameter,
+ * constructor, and constructor parameter for a given test.
+ *
+ * <p>This method is not invoked in the context of a running test statement.
+ */
+ boolean shouldSkip(Context context);
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java
new file mode 100644
index 0000000..6c398aa
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java
@@ -0,0 +1,52 @@
+/*
+ * 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 java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Interface which allows {@link TestParameterAnnotation} annotations to provide the values to test
+ * in a dynamic way.
+ */
+interface TestParameterValueProvider {
+
+ /**
+ * Returns the parameter values for which the test should run.
+ *
+ * @param annotation The annotation instance that was encountered in the test class. The
+ * definition of this annotation is itself annotated with the {@link TestParameterAnnotation}
+ * annotation.
+ * @param parameterClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty
+ * optional.
+ */
+ List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass);
+
+ /**
+ * Returns the class of the list elements returned by {@link #provideValues(Annotation,
+ * Optional)}.
+ *
+ * @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 parameterClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty
+ * optional.
+ */
+ Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass);
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java
new file mode 100644
index 0000000..5207ec6
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java
@@ -0,0 +1,27 @@
+/*
+ * 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 com.google.common.base.Optional;
+import java.lang.annotation.Annotation;
+
+/** Interface to retrieve the {@link TestParameterAnnotation} values for a test. */
+interface TestParameterValues {
+ /**
+ * 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.
+ */
+ Optional<Object> getValue(Class<? extends Annotation> annotationType);
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
new file mode 100644
index 0000000..b7ee544
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
@@ -0,0 +1,208 @@
+/*
+ * 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.checkState;
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.Collections.unmodifiableMap;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Annotation that can be placed on @Test-methods or a test constructor to indicate the sets of
+ * parameters that it should be invoked with.
+ *
+ * <p>For @Test-methods, the method will be invoked for every set of parameters that is specified.
+ * For constructors, all the tests in the test class will be invoked on a class instance that was
+ * constructed by each set of parameters.
+ *
+ * <p>Note: If this annotation is used in a test class, the other methods in that class can use
+ * other types of parameterization, such as {@linkplain TestParameter @TestParameter}.
+ *
+ * <p>See {@link #value()} for simple examples.
+ */
+@Retention(RUNTIME)
+@Target({CONSTRUCTOR, METHOD})
+public @interface TestParameters {
+
+ /**
+ * Array of stringified set of parameters in YAML format. Each element corresponds to a single
+ * invocation of a test method.
+ *
+ * <p>Each element in this array is a full parameter set, formatted as a YAML mapping. The mapping
+ * keys must match the parameter names and the mapping values will be converted to the parameter
+ * type if possible. See yaml.org for the YAML syntax. Parameter types that are supported:
+ *
+ * <ul>
+ * <li>YAML primitives:
+ * <ul>
+ * <li>String: Specified as YAML string
+ * <li>boolean: Specified as YAML boolean
+ * <li>long and int: Specified as YAML integer
+ * <li>float and double: Specified as YAML floating point or integer
+ * </ul>
+ * <li>
+ * <li>Parsed types:
+ * <ul>
+ * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()}
+ * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML
+ * bytes (example: "!!binary 'ZGF0YQ=='")
+ * </ul>
+ * <li>
+ * </ul>
+ *
+ * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link
+ * #valuesProvider()} and leave this field empty.
+ *
+ * <p><b>Examples</b>
+ *
+ * <pre>
+ * {@literal @}Test
+ * {@literal @}TestParameters({
+ * "{age: 17, expectIsAdult: false}",
+ * "{age: 22, expectIsAdult: true}",
+ * })
+ * public void personIsAdult(int age, boolean expectIsAdult) { ... }
+ *
+ * {@literal @}Test
+ * {@literal @}TestParameters({
+ * "{updateRequest: {name: 'Hermione'}, expectedResultType: SUCCESS}",
+ * "{updateRequest: {name: '---'}, expectedResultType: FAILURE}",
+ * })
+ * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
+ * </pre>
+ */
+ String[] value() default {};
+
+ /**
+ * Sets a provider that will return a list of parameter sets. Each element in the returned list
+ * corresponds to a single invocation of a test method.
+ *
+ * <p>If this field is set, {@link #value()} must be empty and vice versa.
+ *
+ * <p><b>Example</b>
+ *
+ * <pre>
+ * {@literal @}Test
+ * {@literal @}TestParameters(valuesProvider = IsAdultValueProvider.class)
+ * public void personIsAdult(int age, boolean expectIsAdult) { ... }
+ *
+ * private static final class IsAdultValueProvider implements TestParametersValuesProvider {
+ * {@literal @}Override public {@literal List<TestParametersValues>} provideValues() {
+ * return ImmutableList.of(
+ * TestParametersValues.builder()
+ * .name("teenager")
+ * .addParameter("age", 17)
+ * .addParameter("expectIsAdult", false)
+ * .build(),
+ * TestParametersValues.builder()
+ * .name("young adult")
+ * .addParameter("age", 22)
+ * .addParameter("expectIsAdult", true)
+ * .build()
+ * );
+ * }
+ * }
+ * </pre>
+ */
+ Class<? extends TestParametersValuesProvider> valuesProvider() default
+ DefaultTestParametersValuesProvider.class;
+
+ /** Interface for custom providers of test parameter values. */
+ interface TestParametersValuesProvider {
+ List<TestParametersValues> provideValues();
+ }
+
+ /** A set of parameters for a single method invocation. */
+ @AutoValue
+ abstract class TestParametersValues {
+
+ /**
+ * A name for this set of parameters that will be used for describing this test.
+ *
+ * <p>Example: If a test method is called "personIsAdult" and this name is "teenager", the name
+ * of the resulting test will be "personIsAdult[teenager]".
+ */
+ public abstract String name();
+
+ /** A map, mapping parameter names to their values. */
+ @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values
+ public abstract Map<String, Object> parametersMap();
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ // Avoid instantiations other than the AutoValue one.
+ TestParametersValues() {}
+
+ /** Builder for {@link TestParametersValues}. */
+ public static final class Builder {
+ private String name;
+ private final LinkedHashMap<String, Object> parametersMap = new LinkedHashMap<>();
+
+ /**
+ * Sets a name for this set of parameters that will be used for describing this test.
+ *
+ * <p>Example: If a test method is called "personIsAdult" and this name is "teenager", the
+ * name of the resulting test will be "personIsAdult[teenager]".
+ */
+ public Builder name(String name) {
+ this.name = name.replaceAll("\\s+", " ");
+ return this;
+ }
+
+ /**
+ * Adds a parameter by its name.
+ *
+ * @param parameterName The name of the parameter of the test method
+ * @param value A value of the same type as the method parameter
+ */
+ public Builder addParameter(String parameterName, @Nullable Object value) {
+ this.parametersMap.put(parameterName, value);
+ return this;
+ }
+
+ /** Adds parameters by thris names. */
+ public Builder addParameters(Map<String, Object> parameterNameToValueMap) {
+ this.parametersMap.putAll(parameterNameToValueMap);
+ return this;
+ }
+
+ public TestParametersValues build() {
+ checkState(name != null, "This set of parameters needs a name (%s)", parametersMap);
+ return new AutoValue_TestParameters_TestParametersValues(
+ name, unmodifiableMap(new LinkedHashMap<>(parametersMap)));
+ }
+ }
+ }
+
+ /** Default {@link TestParametersValuesProvider} implementation that does nothing. */
+ class DefaultTestParametersValuesProvider implements TestParametersValuesProvider {
+ @Override
+ public List<TestParametersValues> provideValues() {
+ return ImmutableList.of();
+ }
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
new file mode 100644
index 0000000..7796db0
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
@@ -0,0 +1,426 @@
+/*
+ * 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.checkState;
+import static java.util.Arrays.stream;
+
+import com.google.auto.value.AutoAnnotation;
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Primitives;
+import com.google.common.reflect.TypeToken;
+import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter;
+import com.google.testing.junit.testparameterinjector.TestParameters.DefaultTestParametersValuesProvider;
+import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues;
+import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+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 {@link TestParameters}. */
+@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs.
+class TestParametersMethodProcessor implements TestMethodProcessor {
+
+ private final TestClass testClass;
+
+ private final LoadingCache<Object, ImmutableList<TestParametersValues>>
+ parameterValuesByConstructorOrMethodCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(1000)
+ .build(
+ CacheLoader.from(
+ methodOrConstructor ->
+ (methodOrConstructor instanceof Constructor)
+ ? toParameterValuesList(
+ methodOrConstructor,
+ ((Constructor<?>) methodOrConstructor)
+ .getAnnotation(TestParameters.class),
+ ((Constructor<?>) methodOrConstructor).getParameters())
+ : toParameterValuesList(
+ methodOrConstructor,
+ ((Method) methodOrConstructor)
+ .getAnnotation(TestParameters.class),
+ ((Method) methodOrConstructor).getParameters())));
+
+ public TestParametersMethodProcessor(TestClass testClass) {
+ this.testClass = testClass;
+ }
+
+ @Override
+ public ValidationResult validateConstructor(TestClass testClass, List<Throwable> exceptions) {
+ if (testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class)) {
+ try {
+ // This method throws an exception if there is a validation error
+ getConstructorParameters();
+ } catch (Throwable t) {
+ exceptions.add(t);
+ }
+ return ValidationResult.HANDLED;
+ } else {
+ return ValidationResult.NOT_HANDLED;
+ }
+ }
+
+ @Override
+ public ValidationResult validateTestMethod(
+ TestClass testClass, FrameworkMethod testMethod, List<Throwable> exceptions) {
+ if (testMethod.getMethod().isAnnotationPresent(TestParameters.class)) {
+ try {
+ // This method throws an exception if there is a validation error
+ getMethodParameters(testMethod.getMethod());
+ } catch (Throwable t) {
+ exceptions.add(t);
+ }
+ return ValidationResult.HANDLED;
+ } else {
+ return ValidationResult.NOT_HANDLED;
+ }
+ }
+
+ @Override
+ public List<TestInfo> processTest(Class<?> clazz, TestInfo originalTest) {
+ boolean constructorIsParameterized =
+ testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class);
+ boolean methodIsParameterized =
+ originalTest.getMethod().isAnnotationPresent(TestParameters.class);
+
+ if (!constructorIsParameterized && !methodIsParameterized) {
+ return ImmutableList.of(originalTest);
+ }
+
+ ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder();
+
+ ImmutableList<Optional<TestParametersValues>> constructorParametersList =
+ getConstructorParametersOrSingleAbsentElement();
+ ImmutableList<Optional<TestParametersValues>> methodParametersList =
+ getMethodParametersOrSingleAbsentElement(originalTest.getMethod());
+ for (int constructorParametersIndex = 0;
+ constructorParametersIndex < constructorParametersList.size();
+ ++constructorParametersIndex) {
+ Optional<TestParametersValues> constructorParameters =
+ constructorParametersList.get(constructorParametersIndex);
+
+ for (int methodParametersIndex = 0;
+ methodParametersIndex < methodParametersList.size();
+ ++methodParametersIndex) {
+ Optional<TestParametersValues> methodParameters =
+ methodParametersList.get(methodParametersIndex);
+
+ // Making final copies of non-final integers for use in lambda
+ int constructorParametersIndexCopy = constructorParametersIndex;
+ int methodParametersIndexCopy = methodParametersIndex;
+
+ testInfos.add(
+ originalTest
+ .withExtraParameters(
+ Stream.of(
+ constructorParameters
+ .transform(
+ param ->
+ TestInfoParameter.create(
+ param.name(),
+ param.parametersMap(),
+ constructorParametersIndexCopy))
+ .orNull(),
+ methodParameters
+ .transform(
+ param ->
+ TestInfoParameter.create(
+ param.name(),
+ param.parametersMap(),
+ methodParametersIndexCopy))
+ .orNull())
+ .filter(Objects::nonNull)
+ .collect(toImmutableList()))
+ .withExtraAnnotation(
+ TestIndexHolderFactory.create(
+ constructorParametersIndex, methodParametersIndex)));
+ }
+ }
+ return testInfos.build();
+ }
+
+ private ImmutableList<Optional<TestParametersValues>>
+ getConstructorParametersOrSingleAbsentElement() {
+ return testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class)
+ ? getConstructorParameters().stream().map(Optional::of).collect(toImmutableList())
+ : ImmutableList.of(Optional.absent());
+ }
+
+ private ImmutableList<Optional<TestParametersValues>> getMethodParametersOrSingleAbsentElement(
+ Method method) {
+ return method.isAnnotationPresent(TestParameters.class)
+ ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList())
+ : ImmutableList.of(Optional.absent());
+ }
+
+ @Override
+ public Statement processStatement(Statement originalStatement, Description finalTestDescription) {
+ return originalStatement;
+ }
+
+ @Override
+ public Optional<Object> createTest(
+ TestClass testClass, FrameworkMethod method, Optional<Object> test) {
+ if (testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class)) {
+ ImmutableList<TestParametersValues> parameterValuesList = getConstructorParameters();
+ TestParametersValues parametersValues =
+ parameterValuesList.get(
+ method.getAnnotation(TestIndexHolder.class).constructorParametersIndex());
+
+ try {
+ Constructor<?> constructor = testClass.getOnlyConstructor();
+ return Optional.of(
+ constructor.newInstance(
+ toParameterArray(
+ parametersValues, testClass.getOnlyConstructor().getParameters())));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ return test;
+ }
+ }
+
+ @Override
+ public Optional<Statement> createStatement(
+ TestClass testClass,
+ FrameworkMethod method,
+ Object testObject,
+ Optional<Statement> statement) {
+ if (method.getMethod().isAnnotationPresent(TestParameters.class)) {
+ ImmutableList<TestParametersValues> parameterValuesList =
+ getMethodParameters(method.getMethod());
+ TestParametersValues parametersValues =
+ parameterValuesList.get(
+ method.getAnnotation(TestIndexHolder.class).methodParametersIndex());
+
+ return Optional.of(
+ new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ method.invokeExplosively(
+ testObject,
+ toParameterArray(parametersValues, method.getMethod().getParameters()));
+ }
+ });
+ } else {
+ return statement;
+ }
+ }
+
+ private ImmutableList<TestParametersValues> getConstructorParameters() {
+ return parameterValuesByConstructorOrMethodCache.getUnchecked(testClass.getOnlyConstructor());
+ }
+
+ private ImmutableList<TestParametersValues> getMethodParameters(Method method) {
+ return parameterValuesByConstructorOrMethodCache.getUnchecked(method);
+ }
+
+ private static ImmutableList<TestParametersValues> toParameterValuesList(
+ Object methodOrConstructor, TestParameters annotation, Parameter[] invokableParameters) {
+ boolean valueIsSet = annotation.value().length > 0;
+ boolean valuesProviderIsSet =
+ !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class);
+
+ checkState(
+ !(valueIsSet && valuesProviderIsSet),
+ "It is not allowed to specify both value and valuesProvider on annotation %s",
+ annotation);
+ checkState(
+ valueIsSet || valuesProviderIsSet,
+ "Either value or valuesProvider must be set on annotation %s",
+ annotation);
+
+ ImmutableList<Parameter> parametersList = ImmutableList.copyOf(invokableParameters);
+ checkState(
+ parametersList.stream().allMatch(Parameter::isNamePresent),
+ ""
+ + "No parameter name could be found for %s, which likely means that parameter names"
+ + " aren't available at runtime. Please ensure that the this test was built with the"
+ + " -parameters compiler option.\n"
+ + "\n"
+ + "In Maven, you do this by adding <parameters>true</parameters> to the"
+ + " maven-compiler-plugin's configuration. For example:\n"
+ + "\n"
+ + "<build>\n"
+ + " <plugins>\n"
+ + " <plugin>\n"
+ + " <groupId>org.apache.maven.plugins</groupId>\n"
+ + " <artifactId>maven-compiler-plugin</artifactId>\n"
+ + " <version>3.8.1</version>\n"
+ + " <configuration>\n"
+ + " <compilerArgs>\n"
+ + " <arg>-parameters</arg>\n"
+ + " </compilerArgs>\n"
+ + " </configuration>\n"
+ + " </plugin>\n"
+ + " </plugins>\n"
+ + "</build>\n"
+ + "\n"
+ + "Don't forget to run `mvn clean` after making this change.",
+ methodOrConstructor);
+ if (valueIsSet) {
+ return stream(annotation.value())
+ .map(yamlMap -> toParameterValues(yamlMap, parametersList))
+ .collect(toImmutableList());
+ } else {
+ return toParameterValuesList(annotation.valuesProvider(), parametersList);
+ }
+ }
+
+ private static ImmutableList<TestParametersValues> toParameterValuesList(
+ Class<? extends TestParametersValuesProvider> valuesProvider, List<Parameter> parameters) {
+ try {
+ Constructor<? extends TestParametersValuesProvider> constructor =
+ valuesProvider.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return constructor.newInstance().provideValues().stream()
+ .peek(values -> validateThatValuesMatchParameters(values, parameters))
+ .collect(toImmutableList());
+ } catch (NoSuchMethodException e) {
+ if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s, probably because it is a not-static"
+ + " inner class. You can fix this by making %s static.",
+ valuesProvider.getSimpleName(), valuesProvider.getSimpleName()),
+ e);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()),
+ e);
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static void validateThatValuesMatchParameters(
+ TestParametersValues testParametersValues, List<Parameter> parameters) {
+ ImmutableMap<String, Parameter> parametersByName =
+ Maps.uniqueIndex(parameters, Parameter::getName);
+
+ checkState(
+ testParametersValues.parametersMap().keySet().equals(parametersByName.keySet()),
+ "Cannot map the given TestParametersValues to parameters %s (Given TestParametersValues"
+ + " are %s)",
+ parametersByName.keySet(),
+ testParametersValues);
+
+ testParametersValues
+ .parametersMap()
+ .forEach(
+ (paramName, paramValue) -> {
+ Class<?> expectedClass = Primitives.wrap(parametersByName.get(paramName).getType());
+ if (paramValue != null) {
+ checkState(
+ expectedClass.isInstance(paramValue),
+ "Cannot map value '%s' (class = %s) to parameter %s (class = %s) (for"
+ + " TestParametersValues %s)",
+ paramValue,
+ paramValue.getClass(),
+ paramName,
+ expectedClass,
+ testParametersValues);
+ }
+ });
+ }
+
+ private static TestParametersValues toParameterValues(
+ String yamlString, List<Parameter> parameters) {
+ Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString);
+ checkState(
+ yamlMapObject instanceof Map,
+ "Cannot map YAML string '%s' to parameters because it is not a mapping",
+ yamlString);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> yamlMap = (Map<String, Object>) yamlMapObject;
+
+ ImmutableMap<String, Parameter> parametersByName =
+ Maps.uniqueIndex(parameters, Parameter::getName);
+ checkState(
+ yamlMap.keySet().equals(parametersByName.keySet()),
+ "Cannot map YAML string '%s' to parameters %s",
+ yamlString,
+ parametersByName.keySet());
+
+ return TestParametersValues.builder()
+ .name(yamlString)
+ .addParameters(
+ Maps.transformEntries(
+ yamlMap,
+ (parameterName, parsedYaml) ->
+ ParameterValueParsing.parseYamlObjectToJavaType(
+ parsedYaml,
+ TypeToken.of(parametersByName.get(parameterName).getParameterizedType()))))
+ .build();
+ }
+
+ private static Object[] toParameterArray(
+ TestParametersValues parametersValues, Parameter[] parameters) {
+ return stream(parameters)
+ .map(parameter -> parametersValues.parametersMap().get(parameter.getName()))
+ .toArray();
+ }
+
+ // 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);
+ }
+
+ /**
+ * This mechanism is a workaround to be able to store the test index in the annotation list of the
+ * {@link TestInfo}, since we cannot carry other information through the test runner.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface TestIndexHolder {
+ int constructorParametersIndex();
+
+ int methodParametersIndex();
+ }
+
+ /** Factory for {@link TestIndexHolder}. */
+ static class TestIndexHolderFactory {
+ @AutoAnnotation
+ static TestIndexHolder create(int constructorParametersIndex, int methodParametersIndex) {
+ return new AutoAnnotation_TestParametersMethodProcessor_TestIndexHolderFactory_create(
+ constructorParametersIndex, methodParametersIndex);
+ }
+
+ private TestIndexHolderFactory() {}
+ }
+}