Skip to content

Commit

Permalink
Add support for repeated annotations to TestParameterValuesProvider.C…
Browse files Browse the repository at this point in the history
…ontext
  • Loading branch information
nymanjens committed Apr 24, 2024
1 parent 12066d2 commit cb5ab94
Show file tree
Hide file tree
Showing 8 changed files with 524 additions and 57 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameter.TestParameterValuesProvider.html)
in favor of its newer version [`TestParameterValuesProvider`](
https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.html).
- Added support for repeated annotations to [`TestParameterValuesProvider.Context`](
https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.Context.html)

## 1.15

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.google.common.collect.Ordering;
import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
Expand Down Expand Up @@ -79,6 +80,17 @@ static GenericParameterContext create(Parameter parameter, Class<?> testClass) {
testClass);
}

// Executable is not available on old Android SDKs, and isn't desugared. This method is only
// called via @TestParameters, wich only supports newer SDKs anyway.
@SuppressWarnings("AndroidJdkLibsChecker")
static GenericParameterContext create(Executable executable, Class<?> testClass) {
return new GenericParameterContext(
ImmutableList.copyOf(executable.getAnnotations()),
/* getAnnotationsFunction= */ annotationType ->
ImmutableList.copyOf(executable.getAnnotationsByType(annotationType)),
testClass);
}

static GenericParameterContext createWithRepeatableAnnotationsFallback(
Annotation[] annotationsOnParameter, Class<?> testClass) {
return new GenericParameterContext(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,18 @@
import com.google.auto.value.AutoAnnotation;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.FluentIterable;
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.common.util.concurrent.UncheckedExecutionException;
import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter;
import com.google.testing.junit.testparameterinjector.TestParameters.DefaultTestParametersValuesProvider;
import com.google.testing.junit.testparameterinjector.TestParameters.RepeatedTestParameters;
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;
Expand All @@ -45,23 +42,22 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;

/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */
@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs.
final class TestParametersMethodProcessor implements TestMethodProcessor {

private final LoadingCache<Executable, ImmutableList<TestParametersValues>>
private final Cache<Executable, ImmutableList<TestParametersValues>>
parameterValuesByConstructorOrMethodCache =
CacheBuilder.newBuilder()
.maximumSize(1000)
.build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList));
CacheBuilder.newBuilder().maximumSize(1000).build();

@Override
public ExecutableValidationResult validateConstructor(Constructor<?> constructor) {
if (hasRelevantAnnotation(constructor)) {
try {
// This method throws an exception if there is a validation error
getConstructorParameters(constructor);
ImmutableList<TestParametersValues> unused = getConstructorParameters(constructor);
} catch (Throwable t) {
return ExecutableValidationResult.validated(t);
}
Expand All @@ -76,7 +72,7 @@ public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?>
if (hasRelevantAnnotation(testMethod)) {
try {
// This method throws an exception if there is a validation error
getMethodParameters(testMethod);
ImmutableList<TestParametersValues> unused = getMethodParameters(testMethod, testClass);
} catch (Throwable t) {
return ExecutableValidationResult.validated(t);
}
Expand All @@ -102,7 +98,8 @@ public List<TestInfo> calculateTestInfos(TestInfo originalTest) {
ImmutableList<Optional<TestParametersValues>> constructorParametersList =
getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass());
ImmutableList<Optional<TestParametersValues>> methodParametersList =
getMethodParametersOrSingleAbsentElement(originalTest.getMethod());
getMethodParametersOrSingleAbsentElement(
originalTest.getMethod(), originalTest.getTestClass());
for (int constructorParametersIndex = 0;
constructorParametersIndex < constructorParametersList.size();
++constructorParametersIndex) {
Expand Down Expand Up @@ -157,9 +154,11 @@ public List<TestInfo> calculateTestInfos(TestInfo originalTest) {
}

private ImmutableList<Optional<TestParametersValues>> getMethodParametersOrSingleAbsentElement(
Method method) {
Method method, Class<?> testClass) {
return hasRelevantAnnotation(method)
? FluentIterable.from(getMethodParameters(method)).transform(Optional::of).toList()
? FluentIterable.from(getMethodParameters(method, testClass))
.transform(Optional::of)
.toList()
: ImmutableList.of(Optional.absent());
}

Expand All @@ -183,7 +182,8 @@ public Optional<List<Object>> maybeGetConstructorParameters(
public Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo) {
Method testMethod = testInfo.getMethod();
if (hasRelevantAnnotation(testMethod)) {
ImmutableList<TestParametersValues> parameterValuesList = getMethodParameters(testMethod);
ImmutableList<TestParametersValues> parameterValuesList =
getMethodParameters(testMethod, testInfo.getTestClass());
TestParametersValues parametersValues =
parameterValuesList.get(
testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex());
Expand All @@ -199,27 +199,31 @@ public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {}

private ImmutableList<TestParametersValues> getConstructorParameters(Constructor<?> constructor) {
try {
return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor);
} catch (UncheckedExecutionException e) {
return parameterValuesByConstructorOrMethodCache.get(
constructor, () -> toParameterValuesList(constructor, constructor.getDeclaringClass()));
} catch (ExecutionException e) {
// Rethrow IllegalStateException because they can be caused by user mistakes and the user
// doesn't need to know that the caching layer is in between.
Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class);
throw e;
throw new RuntimeException(e);
}
}

private ImmutableList<TestParametersValues> getMethodParameters(Method method) {
private ImmutableList<TestParametersValues> getMethodParameters(
Method method, Class<?> testClass) {
try {
return parameterValuesByConstructorOrMethodCache.getUnchecked(method);
} catch (UncheckedExecutionException e) {
return parameterValuesByConstructorOrMethodCache.get(
method, () -> toParameterValuesList(method, testClass));
} catch (ExecutionException e) {
// Rethrow IllegalStateException because they can be caused by user mistakes and the user
// doesn't need to know that the caching layer is in between.
Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class);
throw e;
throw new RuntimeException(e);
}
}

private static ImmutableList<TestParametersValues> toParameterValuesList(Executable executable) {
private static ImmutableList<TestParametersValues> toParameterValuesList(
Executable executable, Class<?> testClass) {
checkParameterNamesArePresent(executable);
ImmutableList<Parameter> parametersList = ImmutableList.copyOf(executable.getParameters());

Expand Down Expand Up @@ -258,7 +262,10 @@ private static ImmutableList<TestParametersValues> toParameterValuesList(Executa
yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName()))
.toList();
} else {
return toParameterValuesList(annotation.valuesProvider(), parametersList);
return toParameterValuesList(
annotation.valuesProvider(),
parametersList,
GenericParameterContext.create(executable, testClass));
}
} else { // Not annotated with @TestParameters
verify(
Expand All @@ -278,12 +285,19 @@ private static ImmutableList<TestParametersValues> toParameterValuesList(Executa
}

private static ImmutableList<TestParametersValues> toParameterValuesList(
Class<? extends TestParametersValuesProvider> valuesProvider, List<Parameter> parameters) {
Class<? extends TestParameters.TestParametersValuesProvider> valuesProvider,
List<Parameter> parameters,
GenericParameterContext context) {
try {
Constructor<? extends TestParametersValuesProvider> constructor =
Constructor<? extends TestParameters.TestParametersValuesProvider> constructor =
valuesProvider.getDeclaredConstructor();
constructor.setAccessible(true);
List<TestParametersValues> testParametersValues = constructor.newInstance().provideValues();
TestParameters.TestParametersValuesProvider provider = constructor.newInstance();
List<TestParametersValues> testParametersValues =
provider instanceof TestParametersValuesProvider
? ((TestParametersValuesProvider) provider)
.provideValues(new TestParametersValuesProvider.Context(context))
: provider.provideValues();
for (TestParametersValues testParametersValue : testParametersValues) {
validateThatValuesMatchParameters(testParametersValue, parameters);
}
Expand All @@ -302,7 +316,7 @@ private static ImmutableList<TestParametersValues> toParameterValuesList(
"Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()),
e);
}
} catch (ReflectiveOperationException e) {
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Copyright 2024 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 com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.NoSuchElementException;

/**
* Abstract class for custom providers of @TestParameters values.
*
* <p>This is a replacement for {@link TestParameters.TestParametersValuesProvider}, which will soon
* be deprecated. The difference with the former interface is that this class provides a {@code
* Context} instance when invoking {@link #provideValues}.
*/
public abstract class TestParametersValuesProvider
implements TestParameters.TestParametersValuesProvider {

protected abstract List<TestParametersValues> provideValues(Context context) throws Exception;

/**
* @deprecated This method should never be called as it will simply throw an {@link
* UnsupportedOperationException}.
*/
@Override
@Deprecated
public final List<TestParametersValues> provideValues() {
throw new UnsupportedOperationException(
"The TestParameterInjector framework should never call this method, and instead call"
+ " #provideValues(Context)");
}

/**
* An immutable value class that contains extra information about the context of the parameter for
* which values are being provided.
*/
public static final class Context {

private final GenericParameterContext delegate;

Context(GenericParameterContext delegate) {
this.delegate = delegate;
}

/**
* Returns the only annotation with the given type on the method or constructor that was
* annotated with @TestParameters.
*
* <p>For example, if the test code is as follows:
*
* <pre>
* {@literal @}Test
* {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}")
* {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}")
* {@literal @}CustomAnnotation(123)
* public void update(UpdateRequest updateRequest, ResultType expectedResultType) {
* ...
* }
* </pre>
*
* then {@code context.getOtherAnnotation(CustomAnnotation.class).value()} will equal 123.
*
* @throws NoSuchElementException if this there is no annotation with the given type
* @throws IllegalArgumentException if there are multiple annotations with the given type
* @throws IllegalArgumentException if the argument it TestParameters.class because it is
* already handled by the TestParameterInjector framework.
*/
public <A extends Annotation> A getOtherAnnotation(Class<A> annotationType) {
checkArgument(
!TestParameters.class.equals(annotationType),
"Getting the @TestParameters annotating the method or constructor is not allowed because"
+ " it is already handled by the TestParameterInjector framework.");
return delegate.getAnnotation(annotationType);
}

/**
* Returns the only annotation with the given type on the method or constructor that was
* annotated with @TestParameter.
*
* <pre>
* {@literal @}Test
* {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}")
* {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}")
* {@literal @}CustomAnnotation(123)
* {@literal @}CustomAnnotation(456)
* public void update(UpdateRequest updateRequest, ResultType expectedResultType) {
* ...
* }
* </pre>
*
* then {@code context.getOtherAnnotations(CustomAnnotation.class)} will return the annotations
* with 123 and 456.
*
* <p>Returns an empty list if this there is no annotation with the given type.
*
* @throws IllegalArgumentException if the argument it TestParameters.class because it is
* already handled by the TestParameterInjector framework.
*/
public <A extends Annotation> ImmutableList<A> getOtherAnnotations(Class<A> annotationType) {
checkArgument(
!TestParameters.class.equals(annotationType),
"Getting the @TestParameters annotating the method or constructor is not allowed because"
+ " it is already handled by the TestParameterInjector framework.");
return delegate.getAnnotations(annotationType);
}

/**
* The class that contains the test that is currently being run.
*
* <p>Having this can be useful when sharing providers between tests that have the same base
* class. In those cases, an abstract method can be called as follows:
*
* <pre>
* ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
* </pre>
*/
public Class<?> testClass() {
return delegate.testClass();
}

/** A list of all annotations on the method or constructor. */
@VisibleForTesting
ImmutableList<Annotation> annotationsOnParameter() {
return delegate.annotationsOnParameter();
}

@Override
public String toString() {
return delegate.toString();
}
}
}
Loading

0 comments on commit cb5ab94

Please sign in to comment.