Home | History | Annotate | Download | only in testing
      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    */
     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