blob: 19f371ec79369f0a7a00ce966a666d94efff35b2 [file] [log] [blame]
/*
* Copyright (C) 2012 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;
import android.os.Build;
import android.util.Log;
import com.android.dx.stock.ProxyBuilder;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.exceptions.stacktrace.StackTraceCleaner;
import org.mockito.internal.util.reflection.LenientCopyTool;
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;
import org.mockito.plugins.MockMaker;
import org.mockito.plugins.StackTraceCleanerProvider;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Set;
/**
* Generates mock instances on Android's runtime.
*/
public final class DexmakerMockMaker implements MockMaker, StackTraceCleanerProvider {
private static final String LOG_TAG = DexmakerMockMaker.class.getSimpleName();
private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
public DexmakerMockMaker() {
if (Build.VERSION.SDK_INT >= 28) {
// 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 allowHiddenApiReflectionFromMethod;
try {
Class vmDebug = Class.forName("dalvik.system.VMDebug");
allowHiddenApiReflectionFromMethod = vmDebug.getDeclaredMethod(
"allowHiddenApiReflectionFrom", Class.class);
} catch (ClassNotFoundException | NoSuchMethodException e) {
throw new IllegalStateException(
"Cannot find VMDebug#allowHiddenApiReflectionFrom. Method is needed to "
+ "allow spies to copy blacklisted fields.");
}
// 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 {
allowHiddenApiReflectionFromMethod.invoke(null, LenientCopyTool.class);
} catch (InvocationTargetException | IllegalAccessException e) {
Log.w(LOG_TAG, "Cannot allow LenientCopyTool to copy spies of blacklisted fields. "
+ "This might break spying on system classes.");
}
}
}
@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()]);
InvocationHandler invocationHandler = new InvocationHandlerAdapter(handler);
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
@SuppressWarnings("unchecked")
T mock = (T) Proxy.newProxyInstance(typeToMock.getClassLoader(), classesToMock, invocationHandler);
return mock;
} else {
// support concrete classes via dexmaker's ProxyBuilder
try {
ProxyBuilder b = ProxyBuilder.forClass(typeToMock)
.implementing(extraInterfaces);
if (Boolean.parseBoolean(
System.getProperty("dexmaker.share_classloader", "false"))) {
b.withSharedClassLoader();
}
Class<? extends T> proxyClass = b.buildProxyClass();
T mock = unsafeAllocator.newInstance(proxyClass);
ProxyBuilder.setInvocationHandler(mock, invocationHandler);
return mock;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new MockitoException("Failed to mock " + typeToMock, e);
}
}
}
@Override
public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) {
InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
adapter.setHandler(newHandler);
}
@Override
public TypeMockability isTypeMockable(final Class<?> type) {
return new TypeMockability() {
@Override
public boolean mockable() {
return !type.isPrimitive() && !Modifier.isFinal(type.getModifiers());
}
@Override
public String nonMockableReason() {
if (type.isPrimitive()) {
return "primitive type";
}
if (Modifier.isFinal(type.getModifiers())) {
return "final or anonymous class";
}
return "not handled type";
}
};
}
@Override
public MockHandler getHandler(Object mock) {
InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
return adapter != null ? adapter.getHandler() : null;
}
@Override
public StackTraceCleaner getStackTraceCleaner(final StackTraceCleaner defaultCleaner) {
return new StackTraceCleaner() {
@Override
public boolean isIn(StackTraceElement candidate) {
String className = candidate.getClassName();
return defaultCleaner.isIn(candidate)
&& !className.endsWith("_Proxy") // dexmaker class proxies
&& !className.startsWith("$Proxy") // dalvik interface proxies
&& !className.startsWith("java.lang.reflect.Proxy")
&& !(className.startsWith("com.android.dx.mockito.")
// Do not clean unit tests
&& !className.startsWith("com.android.dx.mockito.tests"));
}
};
}
private InvocationHandlerAdapter getInvocationHandlerAdapter(Object mock) {
if (mock == null) {
return null;
}
if (Proxy.isProxyClass(mock.getClass())) {
InvocationHandler invocationHandler = Proxy.getInvocationHandler(mock);
return invocationHandler instanceof InvocationHandlerAdapter
? (InvocationHandlerAdapter) invocationHandler
: null;
}
if (ProxyBuilder.isProxyClass(mock.getClass())) {
InvocationHandler invocationHandler = ProxyBuilder.getInvocationHandler(mock);
return invocationHandler instanceof InvocationHandlerAdapter
? (InvocationHandlerAdapter) invocationHandler
: null;
}
return null;
}
}