• 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.Beta;
26 import com.google.common.annotations.GwtIncompatible;
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 java.lang.reflect.AccessibleObject;
33 import java.lang.reflect.InvocationTargetException;
34 import java.lang.reflect.Method;
35 import java.lang.reflect.Modifier;
36 import java.util.List;
37 import java.util.concurrent.atomic.AtomicInteger;
38 
39 /**
40  * Tester to ensure forwarding wrapper works by delegating calls to the corresponding method with
41  * the same parameters forwarded and return value forwarded back or exception propagated as is.
42  *
43  * <p>For example:
44  *
45  * <pre>{@code
46  * new ForwardingWrapperTester().testForwarding(Foo.class, new Function<Foo, Foo>() {
47  *   public Foo apply(Foo foo) {
48  *     return new ForwardingFoo(foo);
49  *   }
50  * });
51  * }</pre>
52  *
53  * @author Ben Yu
54  * @since 14.0
55  */
56 @Beta
57 @GwtIncompatible
58 public final class ForwardingWrapperTester {
59 
60   private boolean testsEquals = false;
61 
62   /**
63    * Asks for {@link Object#equals} and {@link Object#hashCode} to be tested. That is, forwarding
64    * wrappers of equal instances should be equal.
65    */
includingEquals()66   public ForwardingWrapperTester includingEquals() {
67     this.testsEquals = true;
68     return this;
69   }
70 
71   /**
72    * Tests that the forwarding wrapper returned by {@code wrapperFunction} properly forwards method
73    * calls with parameters passed as is, return value returned as is, and exceptions propagated as
74    * is.
75    */
testForwarding( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)76   public <T> void testForwarding(
77       Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
78     checkNotNull(wrapperFunction);
79     checkArgument(interfaceType.isInterface(), "%s isn't an interface", interfaceType);
80     Method[] methods = getMostConcreteMethods(interfaceType);
81     AccessibleObject.setAccessible(methods, true);
82     for (Method method : methods) {
83       // Under java 8, interfaces can have default methods that aren't abstract.
84       // No need to verify them.
85       // Can't check isDefault() for JDK 7 compatibility.
86       if (!Modifier.isAbstract(method.getModifiers())) {
87         continue;
88       }
89       // The interface could be package-private or private.
90       // filter out equals/hashCode/toString
91       if (method.getName().equals("equals")
92           && method.getParameterTypes().length == 1
93           && method.getParameterTypes()[0] == Object.class) {
94         continue;
95       }
96       if (method.getName().equals("hashCode") && method.getParameterTypes().length == 0) {
97         continue;
98       }
99       if (method.getName().equals("toString") && method.getParameterTypes().length == 0) {
100         continue;
101       }
102       testSuccessfulForwarding(interfaceType, method, wrapperFunction);
103       testExceptionPropagation(interfaceType, method, wrapperFunction);
104     }
105     if (testsEquals) {
106       testEquals(interfaceType, wrapperFunction);
107     }
108     testToString(interfaceType, wrapperFunction);
109   }
110 
111   /** Returns the most concrete public methods from {@code type}. */
getMostConcreteMethods(Class<?> type)112   private static Method[] getMostConcreteMethods(Class<?> type) {
113     Method[] methods = type.getMethods();
114     for (int i = 0; i < methods.length; i++) {
115       try {
116         methods[i] = type.getMethod(methods[i].getName(), methods[i].getParameterTypes());
117       } catch (Exception e) {
118         throwIfUnchecked(e);
119         throw new RuntimeException(e);
120       }
121     }
122     return methods;
123   }
124 
testSuccessfulForwarding( Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction)125   private static <T> void testSuccessfulForwarding(
126       Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) {
127     new InteractionTester<T>(interfaceType, method).testInteraction(wrapperFunction);
128   }
129 
testExceptionPropagation( Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction)130   private static <T> void testExceptionPropagation(
131       Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) {
132     final RuntimeException exception = new RuntimeException();
133     T proxy =
134         Reflection.newProxy(
135             interfaceType,
136             new AbstractInvocationHandler() {
137               @Override
138               protected Object handleInvocation(Object p, Method m, Object[] args)
139                   throws Throwable {
140                 throw exception;
141               }
142             });
143     T wrapper = wrapperFunction.apply(proxy);
144     try {
145       method.invoke(wrapper, getParameterValues(method));
146       fail(method + " failed to throw exception as is.");
147     } catch (InvocationTargetException e) {
148       if (exception != e.getCause()) {
149         throw new RuntimeException(e);
150       }
151     } catch (IllegalAccessException e) {
152       throw new AssertionError(e);
153     }
154   }
155 
testEquals( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)156   private static <T> void testEquals(
157       Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
158     FreshValueGenerator generator = new FreshValueGenerator();
159     T instance = generator.newFreshProxy(interfaceType);
160     new EqualsTester()
161         .addEqualityGroup(wrapperFunction.apply(instance), wrapperFunction.apply(instance))
162         .addEqualityGroup(wrapperFunction.apply(generator.newFreshProxy(interfaceType)))
163         // TODO: add an overload to EqualsTester to print custom error message?
164         .testEquals();
165   }
166 
testToString( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)167   private static <T> void testToString(
168       Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
169     T proxy = new FreshValueGenerator().newFreshProxy(interfaceType);
170     assertEquals(
171         "toString() isn't properly forwarded",
172         proxy.toString(),
173         wrapperFunction.apply(proxy).toString());
174   }
175 
getParameterValues(Method method)176   private static Object[] getParameterValues(Method method) {
177     FreshValueGenerator paramValues = new FreshValueGenerator();
178     final List<Object> passedArgs = Lists.newArrayList();
179     for (Class<?> paramType : method.getParameterTypes()) {
180       passedArgs.add(paramValues.generateFresh(paramType));
181     }
182     return passedArgs.toArray();
183   }
184 
185   /** Tests a single interaction against a method. */
186   private static final class InteractionTester<T> extends AbstractInvocationHandler {
187 
188     private final Class<T> interfaceType;
189     private final Method method;
190     private final Object[] passedArgs;
191     private final Object returnValue;
192     private final AtomicInteger called = new AtomicInteger();
193 
InteractionTester(Class<T> interfaceType, Method method)194     InteractionTester(Class<T> interfaceType, Method method) {
195       this.interfaceType = interfaceType;
196       this.method = method;
197       this.passedArgs = getParameterValues(method);
198       this.returnValue = new FreshValueGenerator().generateFresh(method.getReturnType());
199     }
200 
201     @Override
handleInvocation(Object p, Method calledMethod, Object[] args)202     protected Object handleInvocation(Object p, Method calledMethod, Object[] args)
203         throws Throwable {
204       assertEquals(method, calledMethod);
205       assertEquals(method + " invoked more than once.", 0, called.get());
206       for (int i = 0; i < passedArgs.length; i++) {
207         assertEquals(
208             "Parameter #" + i + " of " + method + " not forwarded", passedArgs[i], args[i]);
209       }
210       called.getAndIncrement();
211       return returnValue;
212     }
213 
testInteraction(Function<? super T, ? extends T> wrapperFunction)214     void testInteraction(Function<? super T, ? extends T> wrapperFunction) {
215       T proxy = Reflection.newProxy(interfaceType, this);
216       T wrapper = wrapperFunction.apply(proxy);
217       boolean isPossibleChainingCall = interfaceType.isAssignableFrom(method.getReturnType());
218       try {
219         Object actualReturnValue = method.invoke(wrapper, passedArgs);
220         // If we think this might be a 'chaining' call then we allow the return value to either
221         // be the wrapper or the returnValue.
222         if (!isPossibleChainingCall || wrapper != actualReturnValue) {
223           assertEquals(
224               "Return value of " + method + " not forwarded", returnValue, actualReturnValue);
225         }
226       } catch (IllegalAccessException e) {
227         throw new RuntimeException(e);
228       } catch (InvocationTargetException e) {
229         throw Throwables.propagate(e.getCause());
230       }
231       assertEquals("Failed to forward to " + method, 1, called.get());
232     }
233 
234     @Override
toString()235     public String toString() {
236       return "dummy " + interfaceType.getSimpleName();
237     }
238   }
239 }
240