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