ForwardingWrapperTester.java revision 3ecfa412eddc4b084663f38d562537b86b9734d5
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
17package com.google.common.testing;
18
19import static com.google.common.base.Preconditions.checkArgument;
20import static com.google.common.base.Preconditions.checkNotNull;
21import static junit.framework.Assert.assertEquals;
22import static junit.framework.Assert.fail;
23
24import com.google.common.annotations.Beta;
25import com.google.common.base.Function;
26import com.google.common.base.Throwables;
27import com.google.common.collect.Lists;
28import com.google.common.reflect.AbstractInvocationHandler;
29import com.google.common.reflect.Reflection;
30
31import java.lang.reflect.AccessibleObject;
32import java.lang.reflect.InvocationTargetException;
33import java.lang.reflect.Method;
34import java.lang.reflect.Modifier;
35import java.util.List;
36import 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
53public 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   */
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   */
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}. */
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
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
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
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
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
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
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
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
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
223    @Override public String toString() {
224      return "dummy " + interfaceType.getSimpleName();
225    }
226  }
227}
228