aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java')
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java426
1 files changed, 426 insertions, 0 deletions
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() {}
+ }
+}