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.util.List; 35 import java.util.concurrent.atomic.AtomicInteger; 36 37 /** 38 * Tester to ensure forwarding wrapper works by delegating calls to the corresponding method 39 * with the same parameters forwarded and return value forwarded back or exception propagated as is. 40 * 41 * <p>For example: <pre> {@code 42 * new ForwardingWrapperTester().testForwarding(Foo.class, new Function<Foo, Foo>() { 43 * public Foo apply(Foo foo) { 44 * return new ForwardingFoo(foo); 45 * } 46 * });}</pre> 47 * 48 * @author Ben Yu 49 * @since 14.0 50 */ 51 @Beta 52 public final class ForwardingWrapperTester { 53 54 private boolean testsEquals = false; 55 56 /** 57 * Asks for {@link Object#equals} and {@link Object#hashCode} to be tested. 58 * That is, forwarding wrappers of equal instances should be equal. 59 */ includingEquals()60 public ForwardingWrapperTester includingEquals() { 61 this.testsEquals = true; 62 return this; 63 } 64 65 /** 66 * Tests that the forwarding wrapper returned by {@code wrapperFunction} properly forwards 67 * method calls with parameters passed as is, return value returned as is, and exceptions 68 * propagated as is. 69 */ testForwarding( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)70 public <T> void testForwarding( 71 Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) { 72 checkNotNull(wrapperFunction); 73 checkArgument(interfaceType.isInterface(), "%s isn't an interface", interfaceType); 74 Method[] methods = getMostConcreteMethods(interfaceType); 75 AccessibleObject.setAccessible(methods, true); 76 for (Method method : methods) { 77 // The interface could be package-private or private. 78 // filter out equals/hashCode/toString 79 if (method.getName().equals("equals") 80 && method.getParameterTypes().length == 1 81 && method.getParameterTypes()[0] == Object.class) { 82 continue; 83 } 84 if (method.getName().equals("hashCode") 85 && method.getParameterTypes().length == 0) { 86 continue; 87 } 88 if (method.getName().equals("toString") 89 && method.getParameterTypes().length == 0) { 90 continue; 91 } 92 testSuccessfulForwarding(interfaceType, method, wrapperFunction); 93 testExceptionPropagation(interfaceType, method, wrapperFunction); 94 } 95 if (testsEquals) { 96 testEquals(interfaceType, wrapperFunction); 97 } 98 testToString(interfaceType, wrapperFunction); 99 } 100 101 /** Returns the most concrete public methods from {@code type}. */ getMostConcreteMethods(Class<?> type)102 private static Method[] getMostConcreteMethods(Class<?> type) { 103 Method[] methods = type.getMethods(); 104 for (int i = 0; i < methods.length; i++) { 105 try { 106 methods[i] = type.getMethod(methods[i].getName(), methods[i].getParameterTypes()); 107 } catch (Exception e) { 108 throw Throwables.propagate(e); 109 } 110 } 111 return methods; 112 } 113 testSuccessfulForwarding( Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction)114 private static <T> void testSuccessfulForwarding( 115 Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) { 116 new InteractionTester<T>(interfaceType, method).testInteraction(wrapperFunction); 117 } 118 testExceptionPropagation( Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction)119 private static <T> void testExceptionPropagation( 120 Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) { 121 final RuntimeException exception = new RuntimeException(); 122 T proxy = Reflection.newProxy(interfaceType, new AbstractInvocationHandler() { 123 @Override protected Object handleInvocation(Object p, Method m, Object[] args) 124 throws Throwable { 125 throw exception; 126 } 127 }); 128 T wrapper = wrapperFunction.apply(proxy); 129 try { 130 method.invoke(wrapper, getParameterValues(method)); 131 fail(method + " failed to throw exception as is."); 132 } catch (InvocationTargetException e) { 133 if (exception != e.getCause()) { 134 throw new RuntimeException(e); 135 } 136 } catch (IllegalAccessException e) { 137 throw new AssertionError(e); 138 } 139 } 140 testEquals( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)141 private static <T> void testEquals( 142 Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) { 143 FreshValueGenerator generator = new FreshValueGenerator(); 144 T instance = generator.newProxy(interfaceType); 145 new EqualsTester() 146 .addEqualityGroup(wrapperFunction.apply(instance), wrapperFunction.apply(instance)) 147 .addEqualityGroup(wrapperFunction.apply(generator.newProxy(interfaceType))) 148 // TODO: add an overload to EqualsTester to print custom error message? 149 .testEquals(); 150 } 151 testToString( Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction)152 private static <T> void testToString( 153 Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) { 154 T proxy = new FreshValueGenerator().newProxy(interfaceType); 155 assertEquals("toString() isn't properly forwarded", 156 proxy.toString(), wrapperFunction.apply(proxy).toString()); 157 } 158 getParameterValues(Method method)159 private static Object[] getParameterValues(Method method) { 160 FreshValueGenerator paramValues = new FreshValueGenerator(); 161 final List<Object> passedArgs = Lists.newArrayList(); 162 for (Class<?> paramType : method.getParameterTypes()) { 163 passedArgs.add(paramValues.generate(paramType)); 164 } 165 return passedArgs.toArray(); 166 } 167 168 /** Tests a single interaction against a method. */ 169 private static final class InteractionTester<T> extends AbstractInvocationHandler { 170 171 private final Class<T> interfaceType; 172 private final Method method; 173 private final Object[] passedArgs; 174 private final Object returnValue; 175 private final AtomicInteger called = new AtomicInteger(); 176 InteractionTester(Class<T> interfaceType, Method method)177 InteractionTester(Class<T> interfaceType, Method method) { 178 this.interfaceType = interfaceType; 179 this.method = method; 180 this.passedArgs = getParameterValues(method); 181 this.returnValue = new FreshValueGenerator().generate(method.getReturnType()); 182 } 183 handleInvocation(Object p, Method calledMethod, Object[] args)184 @Override protected Object handleInvocation(Object p, Method calledMethod, Object[] args) 185 throws Throwable { 186 assertEquals(method, calledMethod); 187 assertEquals(method + " invoked more than once.", 0, called.get()); 188 for (int i = 0; i < passedArgs.length; i++) { 189 assertEquals("Parameter #" + i + " of " + method + " not forwarded", 190 passedArgs[i], args[i]); 191 } 192 called.getAndIncrement(); 193 return returnValue; 194 } 195 testInteraction(Function<? super T, ? extends T> wrapperFunction)196 void testInteraction(Function<? super T, ? extends T> wrapperFunction) { 197 T proxy = Reflection.newProxy(interfaceType, this); 198 T wrapper = wrapperFunction.apply(proxy); 199 boolean isPossibleChainingCall = interfaceType.isAssignableFrom(method.getReturnType()); 200 try { 201 Object actualReturnValue = method.invoke(wrapper, passedArgs); 202 // If we think this might be a 'chaining' call then we allow the return value to either 203 // be the wrapper or the returnValue. 204 if (!isPossibleChainingCall || wrapper != actualReturnValue) { 205 assertEquals("Return value of " + method + " not forwarded", returnValue, 206 actualReturnValue); 207 } 208 } catch (IllegalAccessException e) { 209 throw new RuntimeException(e); 210 } catch (InvocationTargetException e) { 211 throw Throwables.propagate(e.getCause()); 212 } 213 assertEquals("Failed to forward to " + method, 1, called.get()); 214 } 215 toString()216 @Override public String toString() { 217 return "dummy " + interfaceType.getSimpleName(); 218 } 219 } 220 } 221