diff options
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.java | 426 |
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() {} + } +} |