blob: dfe242ffc79d1152c000fa85d2015a5222763166 [file] [log] [blame]
/*
* Copyright (c) 2016 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package com.android.dx.mockito.inline;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Backend for the method entry hooks. Checks if the hooks should cause an interception or should
* be ignored.
*/
class MockMethodAdvice {
private final Map<Object, InvocationHandlerAdapter> interceptors;
/** Pattern to decompose a instrumentedMethodWithTypeAndSignature */
private final Pattern methodPattern = Pattern.compile("(.*)#(.*)\\((.*)\\)");
private final SelfCallInfo selfCallInfo = new SelfCallInfo();
MockMethodAdvice(Map<Object, InvocationHandlerAdapter> interceptors) {
this.interceptors = interceptors;
}
/**
* Try to invoke the method {@code origin} on {@code instance}.
*
* @param origin method to invoke
* @param instance instance to invoke the method on.
* @param arguments arguments to the method
*
* @return result of the method
*
* @throws Throwable Exception if thrown by the method
*/
private static Object tryInvoke(Method origin, Object instance, Object[] arguments)
throws Throwable {
try {
return origin.invoke(instance, arguments);
} catch (InvocationTargetException exception) {
throw exception.getCause();
}
}
/**
* Remove calls to a class from a throwable's stack.
*
* @param throwable throwable to clean
* @param current stack frame number to start cleaning from (upwards)
* @param targetType class to remove from the stack
*
* @return throwable with the cleaned stack
*/
private static Throwable hideRecursiveCall(Throwable throwable, int current,
Class<?> targetType) {
try {
StackTraceElement[] stack = throwable.getStackTrace();
int skip = 0;
StackTraceElement next;
do {
next = stack[stack.length - current - ++skip];
} while (!next.getClassName().equals(targetType.getName()));
int top = stack.length - current - skip;
StackTraceElement[] cleared = new StackTraceElement[stack.length - skip];
System.arraycopy(stack, 0, cleared, 0, top);
System.arraycopy(stack, top + skip, cleared, top, current);
throwable.setStackTrace(cleared);
return throwable;
} catch (RuntimeException ignored) {
// This should not happen unless someone instrumented or manipulated exception stack
// traces.
return throwable;
}
}
/**
* Get the method of {@code instance} specified by {@code methodWithTypeAndSignature}.
*
* @param instance instance the method belongs to
* @param methodWithTypeAndSignature the description of the method
*
* @return method {@code methodWithTypeAndSignature} refer to
*/
@SuppressWarnings("unused")
public Method getOrigin(Object instance, String methodWithTypeAndSignature) throws Throwable {
if (!isMocked(instance)) {
return null;
}
Matcher methodComponents = methodPattern.matcher(methodWithTypeAndSignature);
boolean wasFound = methodComponents.find();
if (!wasFound) {
throw new IllegalArgumentException();
}
String argTypeNames[] = methodComponents.group(3).split(",");
ArrayList<Class<?>> argTypes = new ArrayList<>(argTypeNames.length);
for (String argTypeName : argTypeNames) {
if (!argTypeName.equals("")) {
switch (argTypeName) {
case "byte":
argTypes.add(Byte.TYPE);
break;
case "short":
argTypes.add(Short.TYPE);
break;
case "int":
argTypes.add(Integer.TYPE);
break;
case "long":
argTypes.add(Long.TYPE);
break;
case "char":
argTypes.add(Character.TYPE);
break;
case "float":
argTypes.add(Float.TYPE);
break;
case "double":
argTypes.add(Double.TYPE);
break;
case "boolean":
argTypes.add(Boolean.TYPE);
break;
case "byte[]":
argTypes.add(byte[].class);
break;
case "short[]":
argTypes.add(short[].class);
break;
case "int[]":
argTypes.add(int[].class);
break;
case "long[]":
argTypes.add(long[].class);
break;
case "char[]":
argTypes.add(char[].class);
break;
case "float[]":
argTypes.add(float[].class);
break;
case "double[]":
argTypes.add(double[].class);
break;
case "boolean[]":
argTypes.add(boolean[].class);
break;
default:
if (argTypeName.endsWith("[]")) {
argTypes.add(Class.forName("[L" + argTypeName.substring(0,
argTypeName.length() - 2) + ";"));
} else {
argTypes.add(Class.forName(argTypeName));
}
break;
}
}
}
Method origin = Class.forName(methodComponents.group(1)).getDeclaredMethod(
methodComponents.group(2), argTypes.toArray(new Class<?>[]{}));
if (isOverridden(instance, origin)) {
return null;
} else {
return origin;
}
}
/**
* Handle a method entry hook.
*
* @param instance instance that is mocked
* @param origin method that contains the hook
* @param arguments arguments to the method
*
* @return A callable that can be called to get the mocked result or null if the method is not
* mocked.
*/
@SuppressWarnings("unused")
public Callable<?> handle(Object instance, Method origin, Object[] arguments) throws Throwable {
InvocationHandlerAdapter interceptor = interceptors.get(instance);
if (interceptor == null) {
return null;
}
return new ReturnValueWrapper(interceptor.interceptEntryHook(instance, origin, arguments,
new SuperMethodCall(selfCallInfo, origin, instance, arguments)));
}
/**
* Checks if an {@code instance} is a mock.
*
* @param instance instance that might be a mock
*
* @return {@code true} iff the instance is a mock
*/
public boolean isMock(Object instance) {
return interceptors.containsKey(instance);
}
/**
* Check if this method call should be mocked. Usually the same as {@link #isMock(Object)} but
* takes into account the state of {@link #selfCallInfo} that allows to temporary disable
* mocking for a single method call.
*
* @param instance instance that might be mocked
*
* @return {@code true} iff the a method call should be mocked
*
* @see SelfCallInfo
*/
public boolean isMocked(Object instance) {
return selfCallInfo.shouldMockMethod(instance) && isMock(instance);
}
/**
* Check if a method is overridden.
*
* @param instance mocked instance
* @param origin method that might be overridden
*
* @return {@code true} iff the method is overridden
*/
public boolean isOverridden(Object instance, Method origin) {
Class<?> currentType = instance.getClass();
do {
try {
return !origin.equals(currentType.getDeclaredMethod(origin.getName(),
origin.getParameterTypes()));
} catch (NoSuchMethodException ignored) {
currentType = currentType.getSuperclass();
}
} while (currentType != null);
return true;
}
/**
* Used to call the read (non mocked) method.
*/
private static class SuperMethodCall implements InvocationHandlerAdapter.SuperMethod {
private final SelfCallInfo selfCallInfo;
private final Method origin;
private final Object instance;
private final Object[] arguments;
private SuperMethodCall(SelfCallInfo selfCallInfo, Method origin, Object instance,
Object[] arguments) {
this.selfCallInfo = selfCallInfo;
this.origin = origin;
this.instance = instance;
this.arguments = arguments;
}
/**
* Call the read (non mocked) method.
*
* @return Result of read method
* @throws Throwable thrown by the read method
*/
@Override
public Object invoke() throws Throwable {
if (!Modifier.isPublic(origin.getDeclaringClass().getModifiers()
& origin.getModifiers())) {
origin.setAccessible(true);
}
// By setting instance in the the selfCallInfo, once single method call on this instance
// and thread will call the read method as isMocked will return false.
selfCallInfo.set(instance);
return tryInvoke(origin, instance, arguments);
}
}
/**
* Stores a return value of {@link #handle(Object, Method, Object[])} and returns in on
* {@link #call()}.
*/
private static class ReturnValueWrapper implements Callable<Object> {
private final Object returned;
private ReturnValueWrapper(Object returned) {
this.returned = returned;
}
@Override
public Object call() {
return returned;
}
}
/**
* Used to call the original method. If a instance is {@link #set(Object)}
* {@link #shouldMockMethod(Object)} returns false for this instance once.
*
* <p>This is {@link ThreadLocal}, so a thread can {@link #set(Object)} and instance and then
* call {@link #shouldMockMethod(Object)} without interference.
*
* @see SuperMethodCall#invoke()
* @see #isMocked(Object)
*/
private static class SelfCallInfo extends ThreadLocal<Object> {
boolean shouldMockMethod(Object value) {
Object current = get();
if (current == value) {
set(null);
return false;
} else {
return true;
}
}
}
}