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