001/* 002 * Copyright (C) 2012 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.google.common.testing; 018 019import static com.google.common.base.Preconditions.checkArgument; 020import static com.google.common.base.Preconditions.checkNotNull; 021import static com.google.common.base.Throwables.throwIfUnchecked; 022import static junit.framework.Assert.assertEquals; 023import static junit.framework.Assert.fail; 024 025import com.google.common.annotations.GwtIncompatible; 026import com.google.common.base.Function; 027import com.google.common.base.Throwables; 028import com.google.common.collect.Lists; 029import com.google.common.reflect.AbstractInvocationHandler; 030import com.google.common.reflect.Reflection; 031import java.lang.reflect.AccessibleObject; 032import java.lang.reflect.InvocationTargetException; 033import java.lang.reflect.Method; 034import java.lang.reflect.Modifier; 035import java.util.List; 036import java.util.concurrent.atomic.AtomicInteger; 037 038/** 039 * Tester to ensure forwarding wrapper works by delegating calls to the corresponding method with 040 * the same parameters forwarded and return value forwarded back or exception propagated as is. 041 * 042 * <p>For example: 043 * 044 * <pre>{@code 045 * new ForwardingWrapperTester().testForwarding(Foo.class, new Function<Foo, Foo>() { 046 * public Foo apply(Foo foo) { 047 * return new ForwardingFoo(foo); 048 * } 049 * }); 050 * }</pre> 051 * 052 * @author Ben Yu 053 * @since 14.0 054 */ 055@GwtIncompatible 056public final class ForwardingWrapperTester { 057 058 private boolean testsEquals = false; 059 060 /** 061 * Asks for {@link Object#equals} and {@link Object#hashCode} to be tested. That is, forwarding 062 * wrappers of equal instances should be equal. 063 */ 064 public ForwardingWrapperTester includingEquals() { 065 this.testsEquals = true; 066 return this; 067 } 068 069 /** 070 * Tests that the forwarding wrapper returned by {@code wrapperFunction} properly forwards method 071 * calls with parameters passed as is, return value returned as is, and exceptions propagated as 072 * is. 073 */ 074 public <T> void testForwarding( 075 Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) { 076 checkNotNull(wrapperFunction); 077 checkArgument(interfaceType.isInterface(), "%s isn't an interface", interfaceType); 078 Method[] methods = getMostConcreteMethods(interfaceType); 079 AccessibleObject.setAccessible(methods, true); 080 for (Method method : methods) { 081 // Under java 8, interfaces can have default methods that aren't abstract. 082 // No need to verify them. 083 // Can't check isDefault() for JDK 7 compatibility. 084 if (!Modifier.isAbstract(method.getModifiers())) { 085 continue; 086 } 087 // The interface could be package-private or private. 088 // filter out equals/hashCode/toString 089 if (method.getName().equals("equals") 090 && method.getParameterTypes().length == 1 091 && method.getParameterTypes()[0] == Object.class) { 092 continue; 093 } 094 if (method.getName().equals("hashCode") && method.getParameterTypes().length == 0) { 095 continue; 096 } 097 if (method.getName().equals("toString") && method.getParameterTypes().length == 0) { 098 continue; 099 } 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}. */ 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 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 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 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 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 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 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 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 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 233 public String toString() { 234 return "dummy " + interfaceType.getSimpleName(); 235 } 236 } 237}