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