001/*
002 * Copyright (C) 2012 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.testing;
018
019import static com.google.common.base.Preconditions.checkArgument;
020import static com.google.common.base.Preconditions.checkNotNull;
021import static com.google.common.base.Throwables.throwIfUnchecked;
022import static junit.framework.Assert.assertEquals;
023import static junit.framework.Assert.fail;
024
025import com.google.common.annotations.GwtIncompatible;
026import com.google.common.base.Function;
027import com.google.common.base.Throwables;
028import com.google.common.collect.Lists;
029import com.google.common.reflect.AbstractInvocationHandler;
030import com.google.common.reflect.Reflection;
031import java.lang.reflect.AccessibleObject;
032import java.lang.reflect.InvocationTargetException;
033import java.lang.reflect.Method;
034import java.lang.reflect.Modifier;
035import java.util.List;
036import java.util.concurrent.atomic.AtomicInteger;
037
038/**
039 * Tester to ensure forwarding wrapper works by delegating calls to the corresponding method with
040 * the same parameters forwarded and return value forwarded back or exception propagated as is.
041 *
042 * <p>For example:
043 *
044 * <pre>{@code
045 * new ForwardingWrapperTester().testForwarding(Foo.class, new Function<Foo, Foo>() {
046 *   public Foo apply(Foo foo) {
047 *     return new ForwardingFoo(foo);
048 *   }
049 * });
050 * }</pre>
051 *
052 * @author Ben Yu
053 * @since 14.0
054 */
055@GwtIncompatible
056public final class ForwardingWrapperTester {
057
058  private boolean testsEquals = false;
059
060  /**
061   * Asks for {@link Object#equals} and {@link Object#hashCode} to be tested. That is, forwarding
062   * wrappers of equal instances should be equal.
063   */
064  public ForwardingWrapperTester includingEquals() {
065    this.testsEquals = true;
066    return this;
067  }
068
069  /**
070   * Tests that the forwarding wrapper returned by {@code wrapperFunction} properly forwards method
071   * calls with parameters passed as is, return value returned as is, and exceptions propagated as
072   * is.
073   */
074  public <T> void testForwarding(
075      Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
076    checkNotNull(wrapperFunction);
077    checkArgument(interfaceType.isInterface(), "%s isn't an interface", interfaceType);
078    Method[] methods = getMostConcreteMethods(interfaceType);
079    AccessibleObject.setAccessible(methods, true);
080    for (Method method : methods) {
081      // Under java 8, interfaces can have default methods that aren't abstract.
082      // No need to verify them.
083      // Can't check isDefault() for JDK 7 compatibility.
084      if (!Modifier.isAbstract(method.getModifiers())) {
085        continue;
086      }
087      // The interface could be package-private or private.
088      // filter out equals/hashCode/toString
089      if (method.getName().equals("equals")
090          && method.getParameterTypes().length == 1
091          && method.getParameterTypes()[0] == Object.class) {
092        continue;
093      }
094      if (method.getName().equals("hashCode") && method.getParameterTypes().length == 0) {
095        continue;
096      }
097      if (method.getName().equals("toString") && method.getParameterTypes().length == 0) {
098        continue;
099      }
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}. */
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
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
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
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
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
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
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
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
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
233    public String toString() {
234      return "dummy " + interfaceType.getSimpleName();
235    }
236  }
237}