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.Beta; 026import com.google.common.annotations.GwtIncompatible; 027import com.google.common.base.Function; 028import com.google.common.base.Throwables; 029import com.google.common.collect.Lists; 030import com.google.common.reflect.AbstractInvocationHandler; 031import com.google.common.reflect.Reflection; 032import java.lang.reflect.AccessibleObject; 033import java.lang.reflect.InvocationTargetException; 034import java.lang.reflect.Method; 035import java.lang.reflect.Modifier; 036import java.util.List; 037import java.util.concurrent.atomic.AtomicInteger; 038 039/** 040 * Tester to ensure forwarding wrapper works by delegating calls to the corresponding method with 041 * the same parameters forwarded and return value forwarded back or exception propagated as is. 042 * 043 * <p>For example: 044 * 045 * <pre>{@code 046 * new ForwardingWrapperTester().testForwarding(Foo.class, new Function<Foo, Foo>() { 047 * public Foo apply(Foo foo) { 048 * return new ForwardingFoo(foo); 049 * } 050 * }); 051 * }</pre> 052 * 053 * @author Ben Yu 054 * @since 14.0 055 */ 056@Beta 057@GwtIncompatible 058public final class ForwardingWrapperTester { 059 060 private boolean testsEquals = false; 061 062 /** 063 * Asks for {@link Object#equals} and {@link Object#hashCode} to be tested. That is, forwarding 064 * wrappers of equal instances should be equal. 065 */ 066 public ForwardingWrapperTester includingEquals() { 067 this.testsEquals = true; 068 return this; 069 } 070 071 /** 072 * Tests that the forwarding wrapper returned by {@code wrapperFunction} properly forwards method 073 * calls with parameters passed as is, return value returned as is, and exceptions propagated as 074 * is. 075 */ 076 public <T> void testForwarding( 077 Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) { 078 checkNotNull(wrapperFunction); 079 checkArgument(interfaceType.isInterface(), "%s isn't an interface", interfaceType); 080 Method[] methods = getMostConcreteMethods(interfaceType); 081 AccessibleObject.setAccessible(methods, true); 082 for (Method method : methods) { 083 // Under java 8, interfaces can have default methods that aren't abstract. 084 // No need to verify them. 085 // Can't check isDefault() for JDK 7 compatibility. 086 if (!Modifier.isAbstract(method.getModifiers())) { 087 continue; 088 } 089 // The interface could be package-private or private. 090 // filter out equals/hashCode/toString 091 if (method.getName().equals("equals") 092 && method.getParameterTypes().length == 1 093 && method.getParameterTypes()[0] == Object.class) { 094 continue; 095 } 096 if (method.getName().equals("hashCode") && method.getParameterTypes().length == 0) { 097 continue; 098 } 099 if (method.getName().equals("toString") && method.getParameterTypes().length == 0) { 100 continue; 101 } 102 testSuccessfulForwarding(interfaceType, method, wrapperFunction); 103 testExceptionPropagation(interfaceType, method, wrapperFunction); 104 } 105 if (testsEquals) { 106 testEquals(interfaceType, wrapperFunction); 107 } 108 testToString(interfaceType, wrapperFunction); 109 } 110 111 /** Returns the most concrete public methods from {@code type}. */ 112 private static Method[] getMostConcreteMethods(Class<?> type) { 113 Method[] methods = type.getMethods(); 114 for (int i = 0; i < methods.length; i++) { 115 try { 116 methods[i] = type.getMethod(methods[i].getName(), methods[i].getParameterTypes()); 117 } catch (Exception e) { 118 throwIfUnchecked(e); 119 throw new RuntimeException(e); 120 } 121 } 122 return methods; 123 } 124 125 private static <T> void testSuccessfulForwarding( 126 Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) { 127 new InteractionTester<T>(interfaceType, method).testInteraction(wrapperFunction); 128 } 129 130 private static <T> void testExceptionPropagation( 131 Class<T> interfaceType, Method method, Function<? super T, ? extends T> wrapperFunction) { 132 final RuntimeException exception = new RuntimeException(); 133 T proxy = 134 Reflection.newProxy( 135 interfaceType, 136 new AbstractInvocationHandler() { 137 @Override 138 protected Object handleInvocation(Object p, Method m, Object[] args) 139 throws Throwable { 140 throw exception; 141 } 142 }); 143 T wrapper = wrapperFunction.apply(proxy); 144 try { 145 method.invoke(wrapper, getParameterValues(method)); 146 fail(method + " failed to throw exception as is."); 147 } catch (InvocationTargetException e) { 148 if (exception != e.getCause()) { 149 throw new RuntimeException(e); 150 } 151 } catch (IllegalAccessException e) { 152 throw new AssertionError(e); 153 } 154 } 155 156 private static <T> void testEquals( 157 Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) { 158 FreshValueGenerator generator = new FreshValueGenerator(); 159 T instance = generator.newFreshProxy(interfaceType); 160 new EqualsTester() 161 .addEqualityGroup(wrapperFunction.apply(instance), wrapperFunction.apply(instance)) 162 .addEqualityGroup(wrapperFunction.apply(generator.newFreshProxy(interfaceType))) 163 // TODO: add an overload to EqualsTester to print custom error message? 164 .testEquals(); 165 } 166 167 private static <T> void testToString( 168 Class<T> interfaceType, Function<? super T, ? extends T> wrapperFunction) { 169 T proxy = new FreshValueGenerator().newFreshProxy(interfaceType); 170 assertEquals( 171 "toString() isn't properly forwarded", 172 proxy.toString(), 173 wrapperFunction.apply(proxy).toString()); 174 } 175 176 private static Object[] getParameterValues(Method method) { 177 FreshValueGenerator paramValues = new FreshValueGenerator(); 178 final List<Object> passedArgs = Lists.newArrayList(); 179 for (Class<?> paramType : method.getParameterTypes()) { 180 passedArgs.add(paramValues.generateFresh(paramType)); 181 } 182 return passedArgs.toArray(); 183 } 184 185 /** Tests a single interaction against a method. */ 186 private static final class InteractionTester<T> extends AbstractInvocationHandler { 187 188 private final Class<T> interfaceType; 189 private final Method method; 190 private final Object[] passedArgs; 191 private final Object returnValue; 192 private final AtomicInteger called = new AtomicInteger(); 193 194 InteractionTester(Class<T> interfaceType, Method method) { 195 this.interfaceType = interfaceType; 196 this.method = method; 197 this.passedArgs = getParameterValues(method); 198 this.returnValue = new FreshValueGenerator().generateFresh(method.getReturnType()); 199 } 200 201 @Override 202 protected Object handleInvocation(Object p, Method calledMethod, Object[] args) 203 throws Throwable { 204 assertEquals(method, calledMethod); 205 assertEquals(method + " invoked more than once.", 0, called.get()); 206 for (int i = 0; i < passedArgs.length; i++) { 207 assertEquals( 208 "Parameter #" + i + " of " + method + " not forwarded", passedArgs[i], args[i]); 209 } 210 called.getAndIncrement(); 211 return returnValue; 212 } 213 214 void testInteraction(Function<? super T, ? extends T> wrapperFunction) { 215 T proxy = Reflection.newProxy(interfaceType, this); 216 T wrapper = wrapperFunction.apply(proxy); 217 boolean isPossibleChainingCall = interfaceType.isAssignableFrom(method.getReturnType()); 218 try { 219 Object actualReturnValue = method.invoke(wrapper, passedArgs); 220 // If we think this might be a 'chaining' call then we allow the return value to either 221 // be the wrapper or the returnValue. 222 if (!isPossibleChainingCall || wrapper != actualReturnValue) { 223 assertEquals( 224 "Return value of " + method + " not forwarded", returnValue, actualReturnValue); 225 } 226 } catch (IllegalAccessException e) { 227 throw new RuntimeException(e); 228 } catch (InvocationTargetException e) { 229 throw Throwables.propagate(e.getCause()); 230 } 231 assertEquals("Failed to forward to " + method, 1, called.get()); 232 } 233 234 @Override 235 public String toString() { 236 return "dummy " + interfaceType.getSimpleName(); 237 } 238 } 239}