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