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