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 com.google.common.base.Throwables.throwIfUnchecked; 22 import static junit.framework.Assert.assertEquals; 23 import static junit.framework.Assert.fail; 24 25 import com.google.common.annotations.GwtIncompatible; 26 import com.google.common.base.Function; 27 import com.google.common.base.Throwables; 28 import com.google.common.collect.Lists; 29 import com.google.common.reflect.AbstractInvocationHandler; 30 import com.google.common.reflect.Reflection; 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 with 40 * the same parameters forwarded and return value forwarded back or exception propagated as is. 41 * 42 * <p>For example: 43 * 44 * <pre>{@code 45 * new ForwardingWrapperTester().testForwarding(Foo.class, new Function<Foo, Foo>() { 46 * public Foo apply(Foo foo) { 47 * return new ForwardingFoo(foo); 48 * } 49 * }); 50 * }</pre> 51 * 52 * @author Ben Yu 53 * @since 14.0 54 */ 55 @GwtIncompatible 56 public final class ForwardingWrapperTester { 57 58 private boolean testsEquals = false; 59 60 /** 61 * Asks for {@link Object#equals} and {@link Object#hashCode} to be tested. That is, forwarding 62 * wrappers of equal instances should be equal. 63 */ includingEquals()64 public ForwardingWrapperTester includingEquals() { 65 this.testsEquals = true; 66 return this; 67 } 68 69 /** 70 * Tests that the forwarding wrapper returned by {@code wrapperFunction} properly forwards method 71 * calls with parameters passed as is, return value returned as is, and exceptions propagated as 72 * is. 73 */ testForwarding( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)74 public <T> void testForwarding( 75 Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) { 76 checkNotNull(wrapperFunction); 77 checkArgument(interfaceType.isInterface(), "%s isn't an interface", interfaceType); 78 Method[] methods = getMostConcreteMethods(interfaceType); 79 AccessibleObject.setAccessible(methods, true); 80 for (Method method : methods) { 81 // Under java 8, interfaces can have default methods that aren't abstract. 82 // No need to verify them. 83 // Can't check isDefault() for JDK 7 compatibility. 84 if (!Modifier.isAbstract(method.getModifiers())) { 85 continue; 86 } 87 // The interface could be package-private or private. 88 // filter out equals/hashCode/toString 89 if (method.getName().equals("equals") 90 && method.getParameterTypes().length == 1 91 && method.getParameterTypes()[0] == Object.class) { 92 continue; 93 } 94 if (method.getName().equals("hashCode") && method.getParameterTypes().length == 0) { 95 continue; 96 } 97 if (method.getName().equals("toString") && method.getParameterTypes().length == 0) { 98 continue; 99 } 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}. */ getMostConcreteMethods(Class<?> 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 testSuccessfulForwarding( Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction)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 testExceptionPropagation( Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction)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 testEquals( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)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 testToString( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)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 getParameterValues(Method method)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 InteractionTester(Class<T> interfaceType, Method method)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 handleInvocation(Object p, Method calledMethod, Object[] args)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 testInteraction(Function<? super T, ? extends T> wrapperFunction)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 toString()233 public String toString() { 234 return "dummy " + interfaceType.getSimpleName(); 235 } 236 } 237 } 238