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.Beta;
026import com.google.common.annotations.GwtIncompatible;
027import com.google.common.base.Function;
028import com.google.common.base.Throwables;
029import com.google.common.collect.Lists;
030import com.google.common.reflect.AbstractInvocationHandler;
031import com.google.common.reflect.Reflection;
032import java.lang.reflect.AccessibleObject;
033import java.lang.reflect.InvocationTargetException;
034import java.lang.reflect.Method;
035import java.lang.reflect.Modifier;
036import java.util.List;
037import java.util.concurrent.atomic.AtomicInteger;
038
039/**
040 * Tester to ensure forwarding wrapper works by delegating calls to the corresponding method with
041 * the same parameters forwarded and return value forwarded back or exception propagated as is.
042 *
043 * <p>For example:
044 *
045 * <pre>{@code
046 * new ForwardingWrapperTester().testForwarding(Foo.class, new Function<Foo, Foo>() {
047 *   public Foo apply(Foo foo) {
048 *     return new ForwardingFoo(foo);
049 *   }
050 * });
051 * }</pre>
052 *
053 * @author Ben Yu
054 * @since 14.0
055 */
056@Beta
057@GwtIncompatible
058public final class ForwardingWrapperTester {
059
060  private boolean testsEquals = false;
061
062  /**
063   * Asks for {@link Object#equals} and {@link Object#hashCode} to be tested. That is, forwarding
064   * wrappers of equal instances should be equal.
065   */
066  public ForwardingWrapperTester includingEquals() {
067    this.testsEquals = true;
068    return this;
069  }
070
071  /**
072   * Tests that the forwarding wrapper returned by {@code wrapperFunction} properly forwards method
073   * calls with parameters passed as is, return value returned as is, and exceptions propagated as
074   * is.
075   */
076  public <T> void testForwarding(
077      Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
078    checkNotNull(wrapperFunction);
079    checkArgument(interfaceType.isInterface(), "%s isn't an interface", interfaceType);
080    Method[] methods = getMostConcreteMethods(interfaceType);
081    AccessibleObject.setAccessible(methods, true);
082    for (Method method : methods) {
083      // Under java 8, interfaces can have default methods that aren't abstract.
084      // No need to verify them.
085      // Can't check isDefault() for JDK 7 compatibility.
086      if (!Modifier.isAbstract(method.getModifiers())) {
087        continue;
088      }
089      // The interface could be package-private or private.
090      // filter out equals/hashCode/toString
091      if (method.getName().equals("equals")
092          && method.getParameterTypes().length == 1
093          && method.getParameterTypes()[0] == Object.class) {
094        continue;
095      }
096      if (method.getName().equals("hashCode") && method.getParameterTypes().length == 0) {
097        continue;
098      }
099      if (method.getName().equals("toString") && method.getParameterTypes().length == 0) {
100        continue;
101      }
102      testSuccessfulForwarding(interfaceType, method, wrapperFunction);
103      testExceptionPropagation(interfaceType, method, wrapperFunction);
104    }
105    if (testsEquals) {
106      testEquals(interfaceType, wrapperFunction);
107    }
108    testToString(interfaceType, wrapperFunction);
109  }
110
111  /** Returns the most concrete public methods from {@code type}. */
112  private static Method[] getMostConcreteMethods(Class<?> type) {
113    Method[] methods = type.getMethods();
114    for (int i = 0; i < methods.length; i++) {
115      try {
116        methods[i] = type.getMethod(methods[i].getName(), methods[i].getParameterTypes());
117      } catch (Exception e) {
118        throwIfUnchecked(e);
119        throw new RuntimeException(e);
120      }
121    }
122    return methods;
123  }
124
125  private static <T> void testSuccessfulForwarding(
126      Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) {
127    new InteractionTester<T>(interfaceType, method).testInteraction(wrapperFunction);
128  }
129
130  private static <T> void testExceptionPropagation(
131      Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) {
132    final RuntimeException exception = new RuntimeException();
133    T proxy =
134        Reflection.newProxy(
135            interfaceType,
136            new AbstractInvocationHandler() {
137              @Override
138              protected Object handleInvocation(Object p, Method m, Object[] args)
139                  throws Throwable {
140                throw exception;
141              }
142            });
143    T wrapper = wrapperFunction.apply(proxy);
144    try {
145      method.invoke(wrapper, getParameterValues(method));
146      fail(method + " failed to throw exception as is.");
147    } catch (InvocationTargetException e) {
148      if (exception != e.getCause()) {
149        throw new RuntimeException(e);
150      }
151    } catch (IllegalAccessException e) {
152      throw new AssertionError(e);
153    }
154  }
155
156  private static <T> void testEquals(
157      Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
158    FreshValueGenerator generator = new FreshValueGenerator();
159    T instance = generator.newFreshProxy(interfaceType);
160    new EqualsTester()
161        .addEqualityGroup(wrapperFunction.apply(instance), wrapperFunction.apply(instance))
162        .addEqualityGroup(wrapperFunction.apply(generator.newFreshProxy(interfaceType)))
163        // TODO: add an overload to EqualsTester to print custom error message?
164        .testEquals();
165  }
166
167  private static <T> void testToString(
168      Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) {
169    T proxy = new FreshValueGenerator().newFreshProxy(interfaceType);
170    assertEquals(
171        "toString() isn't properly forwarded",
172        proxy.toString(),
173        wrapperFunction.apply(proxy).toString());
174  }
175
176  private static Object[] getParameterValues(Method method) {
177    FreshValueGenerator paramValues = new FreshValueGenerator();
178    final List<Object> passedArgs = Lists.newArrayList();
179    for (Class<?> paramType : method.getParameterTypes()) {
180      passedArgs.add(paramValues.generateFresh(paramType));
181    }
182    return passedArgs.toArray();
183  }
184
185  /** Tests a single interaction against a method. */
186  private static final class InteractionTester<T> extends AbstractInvocationHandler {
187
188    private final Class<T> interfaceType;
189    private final Method method;
190    private final Object[] passedArgs;
191    private final Object returnValue;
192    private final AtomicInteger called = new AtomicInteger();
193
194    InteractionTester(Class<T> interfaceType, Method method) {
195      this.interfaceType = interfaceType;
196      this.method = method;
197      this.passedArgs = getParameterValues(method);
198      this.returnValue = new FreshValueGenerator().generateFresh(method.getReturnType());
199    }
200
201    @Override
202    protected Object handleInvocation(Object p, Method calledMethod, Object[] args)
203        throws Throwable {
204      assertEquals(method, calledMethod);
205      assertEquals(method + " invoked more than once.", 0, called.get());
206      for (int i = 0; i < passedArgs.length; i++) {
207        assertEquals(
208            "Parameter #" + i + " of " + method + " not forwarded", passedArgs[i], args[i]);
209      }
210      called.getAndIncrement();
211      return returnValue;
212    }
213
214    void testInteraction(Function<? super T, ? extends T> wrapperFunction) {
215      T proxy = Reflection.newProxy(interfaceType, this);
216      T wrapper = wrapperFunction.apply(proxy);
217      boolean isPossibleChainingCall = interfaceType.isAssignableFrom(method.getReturnType());
218      try {
219        Object actualReturnValue = method.invoke(wrapper, passedArgs);
220        // If we think this might be a 'chaining' call then we allow the return value to either
221        // be the wrapper or the returnValue.
222        if (!isPossibleChainingCall || wrapper != actualReturnValue) {
223          assertEquals(
224              "Return value of " + method + " not forwarded", returnValue, actualReturnValue);
225        }
226      } catch (IllegalAccessException e) {
227        throw new RuntimeException(e);
228      } catch (InvocationTargetException e) {
229        throw Throwables.propagate(e.getCause());
230      }
231      assertEquals("Failed to forward to " + method, 1, called.get());
232    }
233
234    @Override
235    public String toString() {
236      return "dummy " + interfaceType.getSimpleName();
237    }
238  }
239}