1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.dx.mockito; 18 19 import android.os.Build; 20 import android.util.Log; 21 22 import com.android.dx.stock.ProxyBuilder; 23 import org.mockito.exceptions.base.MockitoException; 24 import org.mockito.exceptions.stacktrace.StackTraceCleaner; 25 import org.mockito.internal.util.reflection.LenientCopyTool; 26 import org.mockito.invocation.MockHandler; 27 import org.mockito.mock.MockCreationSettings; 28 import org.mockito.plugins.MockMaker; 29 import org.mockito.plugins.StackTraceCleanerProvider; 30 31 import java.lang.reflect.InvocationHandler; 32 import java.lang.reflect.InvocationTargetException; 33 import java.lang.reflect.Method; 34 import java.lang.reflect.Modifier; 35 import java.lang.reflect.Proxy; 36 import java.util.Set; 37 38 /** 39 * Generates mock instances on Android's runtime. 40 */ 41 public final class DexmakerMockMaker implements MockMaker, StackTraceCleanerProvider { 42 private static final String LOG_TAG = DexmakerMockMaker.class.getSimpleName(); 43 44 private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); 45 DexmakerMockMaker()46 public DexmakerMockMaker() { 47 if (Build.VERSION.SDK_INT >= 28) { 48 // Blacklisted APIs were introduced in Android P: 49 // 50 // https://android-developers.googleblog.com/2018/02/ 51 // improving-stability-by-reducing-usage.html 52 // 53 // This feature prevents access to blacklisted fields and calling of blacklisted APIs 54 // if the calling class is not trusted. 55 Method allowHiddenApiReflectionFromMethod; 56 try { 57 Class vmDebug = Class.forName("dalvik.system.VMDebug"); 58 allowHiddenApiReflectionFromMethod = vmDebug.getDeclaredMethod( 59 "allowHiddenApiReflectionFrom", Class.class); 60 } catch (ClassNotFoundException | NoSuchMethodException e) { 61 throw new IllegalStateException( 62 "Cannot find VMDebug#allowHiddenApiReflectionFrom. Method is needed to " 63 + "allow spies to copy blacklisted fields."); 64 } 65 66 // The LenientCopyTool copies the fields to a spy when creating the copy from an 67 // existing object. Some of the fields might be blacklisted. Marking the LenientCopyTool 68 // as trusted allows the tool to copy all fields, including the blacklisted ones. 69 try { 70 allowHiddenApiReflectionFromMethod.invoke(null, LenientCopyTool.class); 71 } catch (InvocationTargetException | IllegalAccessException e) { 72 Log.w(LOG_TAG, "Cannot allow LenientCopyTool to copy spies of blacklisted fields. " 73 + "This might break spying on system classes."); 74 } 75 } 76 } 77 78 @Override createMock(MockCreationSettings<T> settings, MockHandler handler)79 public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) { 80 Class<T> typeToMock = settings.getTypeToMock(); 81 Set<Class<?>> interfacesSet = settings.getExtraInterfaces(); 82 Class<?>[] extraInterfaces = interfacesSet.toArray(new Class[interfacesSet.size()]); 83 InvocationHandler invocationHandler = new InvocationHandlerAdapter(handler); 84 85 if (typeToMock.isInterface()) { 86 // support interfaces via java.lang.reflect.Proxy 87 Class[] classesToMock = new Class[extraInterfaces.length + 1]; 88 classesToMock[0] = typeToMock; 89 System.arraycopy(extraInterfaces, 0, classesToMock, 1, extraInterfaces.length); 90 // newProxyInstance returns the type of typeToMock 91 @SuppressWarnings("unchecked") 92 T mock = (T) Proxy.newProxyInstance(typeToMock.getClassLoader(), classesToMock, invocationHandler); 93 return mock; 94 95 } else { 96 // support concrete classes via dexmaker's ProxyBuilder 97 try { 98 ProxyBuilder b = ProxyBuilder.forClass(typeToMock) 99 .implementing(extraInterfaces); 100 101 if (Boolean.parseBoolean( 102 System.getProperty("dexmaker.share_classloader", "false"))) { 103 b.withSharedClassLoader(); 104 } 105 106 Class<? extends T> proxyClass = b.buildProxyClass(); 107 T mock = unsafeAllocator.newInstance(proxyClass); 108 ProxyBuilder.setInvocationHandler(mock, invocationHandler); 109 return mock; 110 } catch (RuntimeException e) { 111 throw e; 112 } catch (Exception e) { 113 throw new MockitoException("Failed to mock " + typeToMock, e); 114 } 115 } 116 } 117 118 @Override resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings)119 public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) { 120 InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock); 121 adapter.setHandler(newHandler); 122 } 123 124 @Override isTypeMockable(final Class<?> type)125 public TypeMockability isTypeMockable(final Class<?> type) { 126 return new TypeMockability() { 127 @Override 128 public boolean mockable() { 129 return !type.isPrimitive() && !Modifier.isFinal(type.getModifiers()); 130 } 131 132 @Override 133 public String nonMockableReason() { 134 if (type.isPrimitive()) { 135 return "primitive type"; 136 } 137 138 if (Modifier.isFinal(type.getModifiers())) { 139 return "final or anonymous class"; 140 } 141 142 return "not handled type"; 143 } 144 }; 145 } 146 147 @Override 148 public MockHandler getHandler(Object mock) { 149 InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock); 150 return adapter != null ? adapter.getHandler() : null; 151 } 152 153 @Override 154 public StackTraceCleaner getStackTraceCleaner(final StackTraceCleaner defaultCleaner) { 155 return new StackTraceCleaner() { 156 @Override 157 public boolean isIn(StackTraceElement candidate) { 158 String className = candidate.getClassName(); 159 160 return defaultCleaner.isIn(candidate) 161 && !className.endsWith("_Proxy") // dexmaker class proxies 162 && !className.startsWith("$Proxy") // dalvik interface proxies 163 && !className.startsWith("java.lang.reflect.Proxy") 164 && !(className.startsWith("com.android.dx.mockito.") 165 // Do not clean unit tests 166 && !className.startsWith("com.android.dx.mockito.tests")); 167 } 168 }; 169 } 170 171 private InvocationHandlerAdapter getInvocationHandlerAdapter(Object mock) { 172 if (mock == null) { 173 return null; 174 } 175 if (Proxy.isProxyClass(mock.getClass())) { 176 InvocationHandler invocationHandler = Proxy.getInvocationHandler(mock); 177 return invocationHandler instanceof InvocationHandlerAdapter 178 ? (InvocationHandlerAdapter) invocationHandler 179 : null; 180 } 181 182 if (ProxyBuilder.isProxyClass(mock.getClass())) { 183 InvocationHandler invocationHandler = ProxyBuilder.getInvocationHandler(mock); 184 return invocationHandler instanceof InvocationHandlerAdapter 185 ? (InvocationHandlerAdapter) invocationHandler 186 : null; 187 } 188 189 return null; 190 } 191 } 192