All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.common.testing.ForwardingWrapperTester Maven / Gradle / Ivy

/*
 * Copyright (C) 2012 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.testing;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.fail;

import com.google.common.annotations.Beta;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.reflect.AbstractInvocationHandler;
import com.google.common.reflect.Reflection;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Tester to ensure forwarding wrapper works by delegating calls to the corresponding method with
 * the same parameters forwarded and return value forwarded back or exception propagated as is.
 *
 * 

For example: * *

{@code
 * new ForwardingWrapperTester().testForwarding(Foo.class, new Function() {
 *   public Foo apply(Foo foo) {
 *     return new ForwardingFoo(foo);
 *   }
 * });
 * }
* * @author Ben Yu * @since 14.0 */ @Beta @GwtIncompatible public final class ForwardingWrapperTester { private boolean testsEquals = false; /** * Asks for {@link Object#equals} and {@link Object#hashCode} to be tested. That is, forwarding * wrappers of equal instances should be equal. */ public ForwardingWrapperTester includingEquals() { this.testsEquals = true; return this; } /** * Tests that the forwarding wrapper returned by {@code wrapperFunction} properly forwards method * calls with parameters passed as is, return value returned as is, and exceptions propagated as * is. */ public void testForwarding( Class interfaceType, Function wrapperFunction) { checkNotNull(wrapperFunction); checkArgument(interfaceType.isInterface(), "%s isn't an interface", interfaceType); Method[] methods = getMostConcreteMethods(interfaceType); AccessibleObject.setAccessible(methods, true); for (Method method : methods) { // Under java 8, interfaces can have default methods that aren't abstract. // No need to verify them. // Can't check isDefault() for JDK 7 compatibility. if (!Modifier.isAbstract(method.getModifiers())) { continue; } // The interface could be package-private or private. // filter out equals/hashCode/toString if (method.getName().equals("equals") && method.getParameterTypes().length == 1 && method.getParameterTypes()[0] == Object.class) { continue; } if (method.getName().equals("hashCode") && method.getParameterTypes().length == 0) { continue; } if (method.getName().equals("toString") && method.getParameterTypes().length == 0) { continue; } testSuccessfulForwarding(interfaceType, method, wrapperFunction); testExceptionPropagation(interfaceType, method, wrapperFunction); } if (testsEquals) { testEquals(interfaceType, wrapperFunction); } testToString(interfaceType, wrapperFunction); } /** Returns the most concrete public methods from {@code type}. */ private static Method[] getMostConcreteMethods(Class type) { Method[] methods = type.getMethods(); for (int i = 0; i < methods.length; i++) { try { methods[i] = type.getMethod(methods[i].getName(), methods[i].getParameterTypes()); } catch (Exception e) { throwIfUnchecked(e); throw new RuntimeException(e); } } return methods; } private static void testSuccessfulForwarding( Class interfaceType, Method method, Function wrapperFunction) { new InteractionTester(interfaceType, method).testInteraction(wrapperFunction); } private static void testExceptionPropagation( Class interfaceType, Method method, Function wrapperFunction) { final RuntimeException exception = new RuntimeException(); T proxy = Reflection.newProxy( interfaceType, new AbstractInvocationHandler() { @Override protected Object handleInvocation(Object p, Method m, Object[] args) throws Throwable { throw exception; } }); T wrapper = wrapperFunction.apply(proxy); try { method.invoke(wrapper, getParameterValues(method)); fail(method + " failed to throw exception as is."); } catch (InvocationTargetException e) { if (exception != e.getCause()) { throw new RuntimeException(e); } } catch (IllegalAccessException e) { throw new AssertionError(e); } } private static void testEquals( Class interfaceType, Function wrapperFunction) { FreshValueGenerator generator = new FreshValueGenerator(); T instance = generator.newFreshProxy(interfaceType); new EqualsTester() .addEqualityGroup(wrapperFunction.apply(instance), wrapperFunction.apply(instance)) .addEqualityGroup(wrapperFunction.apply(generator.newFreshProxy(interfaceType))) // TODO: add an overload to EqualsTester to print custom error message? .testEquals(); } private static void testToString( Class interfaceType, Function wrapperFunction) { T proxy = new FreshValueGenerator().newFreshProxy(interfaceType); assertEquals( "toString() isn't properly forwarded", proxy.toString(), wrapperFunction.apply(proxy).toString()); } private static Object[] getParameterValues(Method method) { FreshValueGenerator paramValues = new FreshValueGenerator(); final List passedArgs = Lists.newArrayList(); for (Class paramType : method.getParameterTypes()) { passedArgs.add(paramValues.generateFresh(paramType)); } return passedArgs.toArray(); } /** Tests a single interaction against a method. */ private static final class InteractionTester extends AbstractInvocationHandler { private final Class interfaceType; private final Method method; private final Object[] passedArgs; private final Object returnValue; private final AtomicInteger called = new AtomicInteger(); InteractionTester(Class interfaceType, Method method) { this.interfaceType = interfaceType; this.method = method; this.passedArgs = getParameterValues(method); this.returnValue = new FreshValueGenerator().generateFresh(method.getReturnType()); } @Override protected Object handleInvocation(Object p, Method calledMethod, Object[] args) throws Throwable { assertEquals(method, calledMethod); assertEquals(method + " invoked more than once.", 0, called.get()); for (int i = 0; i < passedArgs.length; i++) { assertEquals( "Parameter #" + i + " of " + method + " not forwarded", passedArgs[i], args[i]); } called.getAndIncrement(); return returnValue; } void testInteraction(Function wrapperFunction) { T proxy = Reflection.newProxy(interfaceType, this); T wrapper = wrapperFunction.apply(proxy); boolean isPossibleChainingCall = interfaceType.isAssignableFrom(method.getReturnType()); try { Object actualReturnValue = method.invoke(wrapper, passedArgs); // If we think this might be a 'chaining' call then we allow the return value to either // be the wrapper or the returnValue. if (!isPossibleChainingCall || wrapper != actualReturnValue) { assertEquals( "Return value of " + method + " not forwarded", returnValue, actualReturnValue); } } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw Throwables.propagate(e.getCause()); } assertEquals("Failed to forward to " + method, 1, called.get()); } @Override public String toString() { return "dummy " + interfaceType.getSimpleName(); } } }