blob: abcf4f5c081c1497d2d9e9a8aeecb5b32fda8c31 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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.android.dx.mockito.inline;
import android.os.AsyncTask;
import android.os.Build;
import android.util.ArraySet;
import com.android.dx.stock.ProxyBuilder;
import com.android.dx.stock.ProxyBuilder.MethodSetEntry;
import org.mockito.Mockito;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.creation.instance.Instantiator;
import org.mockito.internal.util.reflection.LenientCopyTool;
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;
import org.mockito.plugins.InstantiatorProvider;
import org.mockito.plugins.MockMaker;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Generates mock instances on Android's runtime that can mock final methods.
*
* <p>This is done by transforming the byte code of the classes to add method entry hooks.
*/
public final class InlineDexmakerMockMaker implements MockMaker {
private static final String DISPATCHER_CLASS_NAME =
"com.android.dx.mockito.inline.MockMethodDispatcher";
private static final String DISPATCHER_JAR = "dispatcher.jar";
/** {@link com.android.dx.mockito.inline.JvmtiAgent} set up during one time init */
private static final JvmtiAgent AGENT;
/** Error during one time init or {@code null} if init was successful*/
private static final Throwable INITIALIZATION_ERROR;
/**
* Class injected into the bootstrap classloader. All entry hooks added to methods will call
* this class.
*/
private static final Class DISPATCHER_CLASS;
/*
* One time setup to allow the system to mocking via this mock maker.
*/
static {
JvmtiAgent agent;
Throwable initializationError = null;
Class dispatcherClass = null;
try {
try {
agent = new JvmtiAgent();
try (InputStream is = InlineDexmakerMockMaker.class.getClassLoader()
.getResource(DISPATCHER_JAR).openStream()) {
agent.appendToBootstrapClassLoaderSearch(is);
}
try {
dispatcherClass = Class.forName(DISPATCHER_CLASS_NAME, true,
Object.class.getClassLoader());
if (dispatcherClass == null) {
throw new IllegalStateException(DISPATCHER_CLASS_NAME
+ " could not be loaded");
}
} catch (ClassNotFoundException cnfe) {
throw new IllegalStateException(
"Mockito failed to inject the MockMethodDispatcher class into the "
+ "bootstrap class loader\n\nIt seems like your current VM does not "
+ "support the jvmti API correctly.", cnfe);
}
} catch (IOException ioe) {
throw new IllegalStateException(
"Mockito could not self-attach a jvmti agent to the current VM. This "
+ "feature is required for inline mocking.\nThis error occured due to an "
+ "I/O error during the creation of this agent: " + ioe + "\n\n"
+ "Potentially, the current VM does not support the jvmti API correctly",
ioe);
}
// Blacklisted APIs were introduced in Android P:
//
// https://android-developers.googleblog.com/2018/02/
// improving-stability-by-reducing-usage.html
//
// This feature prevents access to blacklisted fields and calling of blacklisted APIs
// if the calling class is not trusted.
Method allowHiddenApiReflectionFrom;
try {
Class vmDebug = Class.forName("dalvik.system.VMDebug");
allowHiddenApiReflectionFrom = vmDebug.getDeclaredMethod(
"allowHiddenApiReflectionFrom", Class.class);
} catch (ClassNotFoundException | NoSuchMethodException e) {
throw new IllegalStateException("Cannot find "
+ "VMDebug#allowHiddenApiReflectionFrom.");
}
// The LenientCopyTool copies the fields to a spy when creating the copy from an
// existing object. Some of the fields might be blacklisted. Marking the LenientCopyTool
// as trusted allows the tool to copy all fields, including the blacklisted ones.
try {
allowHiddenApiReflectionFrom.invoke(null, LenientCopyTool.class);
} catch (InvocationTargetException e) {
throw e.getCause();
}
// The MockMethodAdvice is used by methods of spies to call the real methods. As the
// real methods might be blacklisted, this class needs to be marked as trusted.
try {
allowHiddenApiReflectionFrom.invoke(null, MockMethodAdvice.class);
} catch (InvocationTargetException e) {
throw e.getCause();
}
} catch (Throwable throwable) {
agent = null;
initializationError = throwable;
}
AGENT = agent;
INITIALIZATION_ERROR = initializationError;
DISPATCHER_CLASS = dispatcherClass;
}
/**
* All currently active mocks. We modify the class's byte code. Some objects of the class are
* modified, some are not. This list helps the {@link MockMethodAdvice} help figure out if a
* object's method calls should be intercepted.
*/
private final Map<Object, InvocationHandlerAdapter> mocks;
/**
* Class doing the actual byte code transformation.
*/
private final ClassTransformer classTransformer;
/**
* Create a new mock maker.
*/
public InlineDexmakerMockMaker() {
if (INITIALIZATION_ERROR != null) {
throw new RuntimeException(
"Could not initialize inline mock maker.\n"
+ "\n"
+ "Release: Android " + Build.VERSION.RELEASE + " " + Build.VERSION.INCREMENTAL
+ "Device: " + Build.BRAND + " " + Build.MODEL, INITIALIZATION_ERROR);
}
mocks = new MockMap();
classTransformer = new ClassTransformer(AGENT, DISPATCHER_CLASS, mocks);
}
/**
* Get methods to proxy.
*
* <p>Only abstract methods will need to get proxied as all other methods will get an entry
* hook.
*
* @param settings description of the current mocking process.
*
* @return methods to proxy.
*/
private <T> Method[] getMethodsToProxy(MockCreationSettings<T> settings) {
Set<MethodSetEntry> abstractMethods = new HashSet<>();
Set<MethodSetEntry> nonAbstractMethods = new HashSet<>();
Class<?> superClass = settings.getTypeToMock();
while (superClass != null) {
for (Method method : superClass.getDeclaredMethods()) {
if (Modifier.isAbstract(method.getModifiers())
&& !nonAbstractMethods.contains(new MethodSetEntry(method))) {
abstractMethods.add(new MethodSetEntry(method));
} else {
nonAbstractMethods.add(new MethodSetEntry(method));
}
}
superClass = superClass.getSuperclass();
}
for (Class<?> i : settings.getTypeToMock().getInterfaces()) {
for (Method method : i.getMethods()) {
if (!nonAbstractMethods.contains(new MethodSetEntry(method))) {
abstractMethods.add(new MethodSetEntry(method));
}
}
}
for (Class<?> i : settings.getExtraInterfaces()) {
for (Method method : i.getMethods()) {
if (!nonAbstractMethods.contains(new MethodSetEntry(method))) {
abstractMethods.add(new MethodSetEntry(method));
}
}
}
Method[] methodsToProxy = new Method[abstractMethods.size()];
int i = 0;
for (MethodSetEntry entry : abstractMethods) {
methodsToProxy[i++] = entry.originalMethod;
}
return methodsToProxy;
}
@Override
public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
Class<T> typeToMock = settings.getTypeToMock();
Set<Class<?>> interfacesSet = settings.getExtraInterfaces();
Class<?>[] extraInterfaces = interfacesSet.toArray(new Class[interfacesSet.size()]);
InvocationHandlerAdapter handlerAdapter = new InvocationHandlerAdapter(handler);
T mock;
if (typeToMock.isInterface()) {
// support interfaces via java.lang.reflect.Proxy
Class[] classesToMock = new Class[extraInterfaces.length + 1];
classesToMock[0] = typeToMock;
System.arraycopy(extraInterfaces, 0, classesToMock, 1, extraInterfaces.length);
// newProxyInstance returns the type of typeToMock
mock = (T) Proxy.newProxyInstance(typeToMock.getClassLoader(), classesToMock,
handlerAdapter);
} else {
boolean subclassingRequired = !interfacesSet.isEmpty()
|| Modifier.isAbstract(typeToMock.getModifiers());
// Add entry hooks to non-abstract methods.
classTransformer.mockClass(MockFeatures.withMockFeatures(typeToMock, interfacesSet));
Class<? extends T> proxyClass;
Instantiator instantiator = Mockito.framework().getPlugins()
.getDefaultPlugin(InstantiatorProvider.class).getInstantiator(settings);
if (subclassingRequired) {
try {
// support abstract methods via dexmaker's ProxyBuilder
proxyClass = ProxyBuilder.forClass(typeToMock).implementing(extraInterfaces)
.onlyMethods(getMethodsToProxy(settings)).withSharedClassLoader()
.buildProxyClass();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new MockitoException("Failed to mock " + typeToMock, e);
}
try {
mock = instantiator.newInstance(proxyClass);
} catch (org.mockito.internal.creation.instance.InstantiationException e) {
throw new MockitoException("Unable to create mock instance of type '"
+ proxyClass.getSuperclass().getSimpleName() + "'", e);
}
ProxyBuilder.setInvocationHandler(mock, handlerAdapter);
} else {
try {
mock = instantiator.newInstance(typeToMock);
} catch (org.mockito.internal.creation.instance.InstantiationException e) {
throw new MockitoException("Unable to create mock instance of type '"
+ typeToMock.getSimpleName() + "'", e);
}
}
}
mocks.put(mock, handlerAdapter);
return mock;
}
@Override
public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) {
InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
if (adapter != null) {
adapter.setHandler(newHandler);
}
}
@Override
public TypeMockability isTypeMockable(final Class<?> type) {
return new TypeMockability() {
@Override
public boolean mockable() {
return !type.isPrimitive() && type != String.class;
}
@Override
public String nonMockableReason() {
if (type.isPrimitive()) {
return "primitive type";
}
if (type == String.class) {
return "string";
}
return "not handled type";
}
};
}
@Override
public MockHandler getHandler(Object mock) {
InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
return adapter != null ? adapter.getHandler() : null;
}
/**
* Get the {@link InvocationHandlerAdapter} registered for a mock.
*
* @param instance instance that might be mocked
*
* @return adapter for this mock, or {@code null} if instance is not mocked
*/
private InvocationHandlerAdapter getInvocationHandlerAdapter(Object instance) {
if (instance == null) {
return null;
}
return mocks.get(instance);
}
/**
* A map mock -> adapter that holds weak references to the mocks and cleans them up when a
* stale reference is found.
*/
private static class MockMap extends ReferenceQueue<Object>
implements Map<Object, InvocationHandlerAdapter> {
private static final int MIN_CLEAN_INTERVAL_MILLIS = 16000;
private static final int MAX_GET_WITHOUT_CLEAN = 16384;
private final Object lock = new Object();
private static StrongKey cachedKey;
private HashMap<WeakKey, InvocationHandlerAdapter> adapters = new HashMap<>();
/**
* The time we issues the last cleanup
*/
long mLastCleanup = 0;
/**
* If {@link #cleanStaleReferences} is currently cleaning stale references out of
* {@link #adapters}
*/
private boolean isCleaning = false;
/**
* The number of time {@link #get} was called without cleaning up stale references.
* {@link #get} is a method that is called often.
*
* We need to do periodic cleanups as we might never look at mocks at higher indexes and
* hence never realize that their references are stale.
*/
private int getCount = 0;
/**
* Try to get a recycled cached key.
*
* @param obj the reference the key wraps
*
* @return The recycled cached key or a new one
*/
private StrongKey createStrongKey(Object obj) {
synchronized (lock) {
if (cachedKey == null) {
cachedKey = new StrongKey();
}
cachedKey.obj = obj;
StrongKey newKey = cachedKey;
cachedKey = null;
return newKey;
}
}
/**
* Recycle a key. The key should not be used afterwards
*
* @param key The key to recycle
*/
private void recycleStrongKey(StrongKey key) {
synchronized (lock) {
cachedKey = key;
}
}
@Override
public int size() {
return adapters.size();
}
@Override
public boolean isEmpty() {
return adapters.isEmpty();
}
@Override
public boolean containsKey(Object mock) {
synchronized (lock) {
StrongKey key = createStrongKey(mock);
boolean containsKey = adapters.containsKey(key);
recycleStrongKey(key);
return containsKey;
}
}
@Override
public boolean containsValue(Object adapter) {
synchronized (lock) {
return adapters.containsValue(adapter);
}
}
@Override
public InvocationHandlerAdapter get(Object mock) {
synchronized (lock) {
if (getCount > MAX_GET_WITHOUT_CLEAN) {
cleanStaleReferences();
getCount = 0;
} else {
getCount++;
}
StrongKey key = createStrongKey(mock);
InvocationHandlerAdapter adapter = adapters.get(key);
recycleStrongKey(key);
return adapter;
}
}
/**
* Remove entries that reference a stale mock from {@link #adapters}.
*/
private void cleanStaleReferences() {
synchronized (lock) {
if (!isCleaning) {
if (System.currentTimeMillis() - MIN_CLEAN_INTERVAL_MILLIS < mLastCleanup) {
return;
}
isCleaning = true;
AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
synchronized (lock) {
while (true) {
Reference<?> ref = MockMap.this.poll();
if (ref == null) {
break;
}
adapters.remove(ref);
}
mLastCleanup = System.currentTimeMillis();
isCleaning = false;
}
}
});
}
}
}
@Override
public InvocationHandlerAdapter put(Object mock, InvocationHandlerAdapter adapter) {
synchronized (lock) {
InvocationHandlerAdapter oldValue = remove(mock);
adapters.put(new WeakKey(mock), adapter);
return oldValue;
}
}
@Override
public InvocationHandlerAdapter remove(Object mock) {
synchronized (lock) {
StrongKey key = createStrongKey(mock);
InvocationHandlerAdapter adapter = adapters.remove(key);
recycleStrongKey(key);
return adapter;
}
}
@Override
public void putAll(Map<?, ? extends InvocationHandlerAdapter> map) {
synchronized (lock) {
for (Entry<?, ? extends InvocationHandlerAdapter> entry : map.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
}
@Override
public void clear() {
synchronized (lock) {
adapters.clear();
}
}
@Override
public Set<Object> keySet() {
synchronized (lock) {
Set<Object> mocks = new ArraySet<>(adapters.size());
boolean hasStaleReferences = false;
for (WeakKey key : adapters.keySet()) {
Object mock = key.get();
if (mock == null) {
hasStaleReferences = true;
} else {
mocks.add(mock);
}
}
if (hasStaleReferences) {
cleanStaleReferences();
}
return mocks;
}
}
@Override
public Collection<InvocationHandlerAdapter> values() {
synchronized (lock) {
return adapters.values();
}
}
@Override
public Set<Entry<Object, InvocationHandlerAdapter>> entrySet() {
synchronized (lock) {
Set<Entry<Object, InvocationHandlerAdapter>> entries = new ArraySet<>(
adapters.size());
boolean hasStaleReferences = false;
for (Entry<WeakKey, InvocationHandlerAdapter> entry : adapters.entrySet()) {
Object mock = entry.getKey().get();
if (mock == null) {
hasStaleReferences = true;
} else {
entries.add(new AbstractMap.SimpleEntry<>(mock, entry.getValue()));
}
}
if (hasStaleReferences) {
cleanStaleReferences();
}
return entries;
}
}
/**
* A weakly referencing wrapper to a mock.
*
* Only equals other weak or strong keys where the mock is the same.
*/
private class WeakKey extends WeakReference<Object> {
private final int hashCode;
private WeakKey(/*@NonNull*/ Object obj) {
super(obj, MockMap.this);
// Cache the hashcode as the referenced object might disappear
hashCode = System.identityHashCode(obj);
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (other == null) {
return false;
}
// Checking hashcode is cheap
if (other.hashCode() != hashCode) {
return false;
}
Object obj = get();
if (obj == null) {
cleanStaleReferences();
return false;
}
if (other instanceof WeakKey) {
Object otherObj = ((WeakKey) other).get();
if (otherObj == null) {
cleanStaleReferences();
return false;
}
return obj == otherObj;
} else if (other instanceof StrongKey) {
Object otherObj = ((StrongKey) other).obj;
return obj == otherObj;
} else {
return false;
}
}
@Override
public int hashCode() {
return hashCode;
}
}
/**
* A strongly referencing wrapper to a mock.
*
* Only equals other weak or strong keys where the mock is the same.
*/
private class StrongKey {
/*@NonNull*/ private Object obj;
@Override
public boolean equals(Object other) {
if (other instanceof WeakKey) {
Object otherObj = ((WeakKey) other).get();
if (otherObj == null) {
cleanStaleReferences();
return false;
}
return obj == otherObj;
} else if (other instanceof StrongKey) {
return this.obj == ((StrongKey)other).obj;
} else {
return false;
}
}
@Override
public int hashCode() {
return System.identityHashCode(obj);
}
}
}
}