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