• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 The Guava Authors
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 
17 package com.google.common.testing;
18 
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;
24 
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;
37 import java.util.List;
38 import java.util.concurrent.atomic.AtomicInteger;
39 import org.checkerframework.checker.nullness.qual.Nullable;
40 
41 /**
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.
44  *
45  * <p>For example:
46  *
47  * <pre>{@code
48  * new ForwardingWrapperTester().testForwarding(Foo.class, new Function<Foo, Foo>() {
49  *   public Foo apply(Foo foo) {
50  *     return new ForwardingFoo(foo);
51  *   }
52  * });
53  * }</pre>
54  *
55  * @author Ben Yu
56  * @since 14.0
57  */
58 @GwtIncompatible
59 @J2ktIncompatible
60 @ElementTypesAreNonnullByDefault
61 public final class ForwardingWrapperTester {
62 
63   private boolean testsEquals = false;
64 
65   /**
66    * Asks for {@link Object#equals} and {@link Object#hashCode} to be tested. That is, forwarding
67    * wrappers of equal instances should be equal.
68    */
69   @CanIgnoreReturnValue
includingEquals()70   public ForwardingWrapperTester includingEquals() {
71     this.testsEquals = true;
72     return this;
73   }
74 
75   /**
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
78    * is.
79    */
testForwarding( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)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())) {
91         continue;
92       }
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) {
98         continue;
99       }
100       if (method.getName().equals("hashCode") && method.getParameterTypes().length == 0) {
101         continue;
102       }
103       if (method.getName().equals("toString") && method.getParameterTypes().length == 0) {
104         continue;
105       }
106       testSuccessfulForwarding(interfaceType, method, wrapperFunction);
107       testExceptionPropagation(interfaceType, method, wrapperFunction);
108     }
109     if (testsEquals) {
110       testEquals(interfaceType, wrapperFunction);
111     }
112     testToString(interfaceType, wrapperFunction);
113   }
114 
115   /** Returns the most concrete public methods from {@code type}. */
getMostConcreteMethods(Class<?> type)116   private static Method[] getMostConcreteMethods(Class<?> type) {
117     Method[] methods = type.getMethods();
118     for (int i = 0; i < methods.length; i++) {
119       try {
120         methods[i] = type.getMethod(methods[i].getName(), methods[i].getParameterTypes());
121       } catch (Exception e) {
122         throwIfUnchecked(e);
123         throw new RuntimeException(e);
124       }
125     }
126     return methods;
127   }
128 
testSuccessfulForwarding( Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction)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);
132   }
133 
testExceptionPropagation( Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction)134   private static <T> void testExceptionPropagation(
135       Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) {
136     RuntimeException exception = new RuntimeException();
137     T proxy =
138         Reflection.newProxy(
139             interfaceType,
140             new AbstractInvocationHandler() {
141               @Override
142               protected Object handleInvocation(Object p, Method m, @Nullable Object[] args)
143                   throws Throwable {
144                 throw exception;
145               }
146             });
147     T wrapper = wrapperFunction.apply(proxy);
148     try {
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);
154       }
155     } catch (IllegalAccessException e) {
156       throw new AssertionError(e);
157     }
158   }
159 
testEquals( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)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);
164     new EqualsTester()
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?
168         .testEquals();
169   }
170 
testToString( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)171   private static <T> void testToString(
172       Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
173     T proxy = new FreshValueGenerator().newFreshProxy(interfaceType);
174     assertEquals(
175         "toString() isn't properly forwarded",
176         proxy.toString(),
177         wrapperFunction.apply(proxy).toString());
178   }
179 
getParameterValues(Method method)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));
185     }
186     return passedArgs.toArray();
187   }
188 
189   /** Tests a single interaction against a method. */
190   private static final class InteractionTester<T> extends AbstractInvocationHandler {
191 
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();
197 
InteractionTester(Class<T> interfaceType, Method method)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());
203     }
204 
205     @Override
handleInvocation( Object p, Method calledMethod, @Nullable Object[] args)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++) {
211         assertEquals(
212             "Parameter #" + i + " of " + method + " not forwarded", passedArgs[i], args[i]);
213       }
214       called.getAndIncrement();
215       return returnValue;
216     }
217 
testInteraction(Function<? super T, ? extends T> wrapperFunction)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());
222       try {
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) {
227           assertEquals(
228               "Return value of " + method + " not forwarded", returnValue, actualReturnValue);
229         }
230       } catch (IllegalAccessException e) {
231         throw new RuntimeException(e);
232       } catch (InvocationTargetException e) {
233         throw Throwables.propagate(e.getCause());
234       }
235       assertEquals("Failed to forward to " + method, 1, called.get());
236     }
237 
238     @Override
toString()239     public String toString() {
240       return "dummy " + interfaceType.getSimpleName();
241     }
242   }
243 }
244