2
* Copyright (C) 2012 The Guava Authors
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
8
* http://www.apache.org/licenses/LICENSE-2.0
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
17
package com.google.common.testing;
19
import static com.google.common.base.Preconditions.checkArgument;
20
import static com.google.common.base.Preconditions.checkNotNull;
21
import static com.google.common.base.Throwables.throwIfUnchecked;
22
import static junit.framework.Assert.assertEquals;
23
import static junit.framework.Assert.fail;
25
import com.google.common.annotations.GwtIncompatible;
26
import com.google.common.annotations.J2ktIncompatible;
27
import com.google.common.base.Function;
28
import com.google.common.base.Throwables;
29
import com.google.common.collect.Lists;
30
import com.google.common.reflect.AbstractInvocationHandler;
31
import com.google.common.reflect.Reflection;
32
import com.google.errorprone.annotations.CanIgnoreReturnValue;
33
import java.lang.reflect.AccessibleObject;
34
import java.lang.reflect.InvocationTargetException;
35
import java.lang.reflect.Method;
36
import java.lang.reflect.Modifier;
38
import java.util.concurrent.atomic.AtomicInteger;
39
import org.checkerframework.checker.nullness.qual.Nullable;
42
* Tester to ensure forwarding wrapper works by delegating calls to the corresponding method with
43
* the same parameters forwarded and return value forwarded back or exception propagated as is.
48
* new ForwardingWrapperTester().testForwarding(Foo.class, new Function<Foo, Foo>() {
49
* public Foo apply(Foo foo) {
50
* return new ForwardingFoo(foo);
60
@ElementTypesAreNonnullByDefault
61
public final class ForwardingWrapperTester {
63
private boolean testsEquals = false;
66
* Asks for {@link Object#equals} and {@link Object#hashCode} to be tested. That is, forwarding
67
* wrappers of equal instances should be equal.
70
public ForwardingWrapperTester includingEquals() {
71
this.testsEquals = true;
76
* Tests that the forwarding wrapper returned by {@code wrapperFunction} properly forwards method
77
* calls with parameters passed as is, return value returned as is, and exceptions propagated as
80
public <T> void testForwarding(
81
Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
82
checkNotNull(wrapperFunction);
83
checkArgument(interfaceType.isInterface(), "%s isn't an interface", interfaceType);
84
Method[] methods = getMostConcreteMethods(interfaceType);
85
AccessibleObject.setAccessible(methods, true);
86
for (Method method : methods) {
87
// Under java 8, interfaces can have default methods that aren't abstract.
88
// No need to verify them.
89
// Can't check isDefault() for JDK 7 compatibility.
90
if (!Modifier.isAbstract(method.getModifiers())) {
93
// The interface could be package-private or private.
94
// filter out equals/hashCode/toString
95
if (method.getName().equals("equals")
96
&& method.getParameterTypes().length == 1
97
&& method.getParameterTypes()[0] == Object.class) {
100
if (method.getName().equals("hashCode") && method.getParameterTypes().length == 0) {
103
if (method.getName().equals("toString") && method.getParameterTypes().length == 0) {
106
testSuccessfulForwarding(interfaceType, method, wrapperFunction);
107
testExceptionPropagation(interfaceType, method, wrapperFunction);
110
testEquals(interfaceType, wrapperFunction);
112
testToString(interfaceType, wrapperFunction);
115
/** Returns the most concrete public methods from {@code type}. */
116
private static Method[] getMostConcreteMethods(Class<?> type) {
117
Method[] methods = type.getMethods();
118
for (int i = 0; i < methods.length; i++) {
120
methods[i] = type.getMethod(methods[i].getName(), methods[i].getParameterTypes());
121
} catch (Exception e) {
123
throw new RuntimeException(e);
129
private static <T> void testSuccessfulForwarding(
130
Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) {
131
new InteractionTester<T>(interfaceType, method).testInteraction(wrapperFunction);
134
private static <T> void testExceptionPropagation(
135
Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) {
136
RuntimeException exception = new RuntimeException();
140
new AbstractInvocationHandler() {
142
protected Object handleInvocation(Object p, Method m, @Nullable Object[] args)
147
T wrapper = wrapperFunction.apply(proxy);
149
method.invoke(wrapper, getParameterValues(method));
150
fail(method + " failed to throw exception as is.");
151
} catch (InvocationTargetException e) {
152
if (exception != e.getCause()) {
153
throw new RuntimeException(e);
155
} catch (IllegalAccessException e) {
156
throw new AssertionError(e);
160
private static <T> void testEquals(
161
Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
162
FreshValueGenerator generator = new FreshValueGenerator();
163
T instance = generator.newFreshProxy(interfaceType);
165
.addEqualityGroup(wrapperFunction.apply(instance), wrapperFunction.apply(instance))
166
.addEqualityGroup(wrapperFunction.apply(generator.newFreshProxy(interfaceType)))
167
// TODO: add an overload to EqualsTester to print custom error message?
171
private static <T> void testToString(
172
Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
173
T proxy = new FreshValueGenerator().newFreshProxy(interfaceType);
175
"toString() isn't properly forwarded",
177
wrapperFunction.apply(proxy).toString());
180
private static @Nullable Object[] getParameterValues(Method method) {
181
FreshValueGenerator paramValues = new FreshValueGenerator();
182
List<@Nullable Object> passedArgs = Lists.newArrayList();
183
for (Class<?> paramType : method.getParameterTypes()) {
184
passedArgs.add(paramValues.generateFresh(paramType));
186
return passedArgs.toArray();
189
/** Tests a single interaction against a method. */
190
private static final class InteractionTester<T> extends AbstractInvocationHandler {
192
private final Class<T> interfaceType;
193
private final Method method;
194
private final @Nullable Object[] passedArgs;
195
private final @Nullable Object returnValue;
196
private final AtomicInteger called = new AtomicInteger();
198
InteractionTester(Class<T> interfaceType, Method method) {
199
this.interfaceType = interfaceType;
200
this.method = method;
201
this.passedArgs = getParameterValues(method);
202
this.returnValue = new FreshValueGenerator().generateFresh(method.getReturnType());
206
protected @Nullable Object handleInvocation(
207
Object p, Method calledMethod, @Nullable Object[] args) throws Throwable {
208
assertEquals(method, calledMethod);
209
assertEquals(method + " invoked more than once.", 0, called.get());
210
for (int i = 0; i < passedArgs.length; i++) {
212
"Parameter #" + i + " of " + method + " not forwarded", passedArgs[i], args[i]);
214
called.getAndIncrement();
218
void testInteraction(Function<? super T, ? extends T> wrapperFunction) {
219
T proxy = Reflection.newProxy(interfaceType, this);
220
T wrapper = wrapperFunction.apply(proxy);
221
boolean isPossibleChainingCall = interfaceType.isAssignableFrom(method.getReturnType());
223
Object actualReturnValue = method.invoke(wrapper, passedArgs);
224
// If we think this might be a 'chaining' call then we allow the return value to either
225
// be the wrapper or the returnValue.
226
if (!isPossibleChainingCall || wrapper != actualReturnValue) {
228
"Return value of " + method + " not forwarded", returnValue, actualReturnValue);
230
} catch (IllegalAccessException e) {
231
throw new RuntimeException(e);
232
} catch (InvocationTargetException e) {
233
throw Throwables.propagate(e.getCause());
235
assertEquals("Failed to forward to " + method, 1, called.get());
239
public String toString() {
240
return "dummy " + interfaceType.getSimpleName();