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