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 com.android.dx.stock.ProxyBuilder; 20 import org.mockito.exceptions.base.MockitoException; 21 import org.mockito.exceptions.stacktrace.StackTraceCleaner; 22 import org.mockito.internal.util.reflection.LenientCopyTool; 23 import org.mockito.invocation.MockHandler; 24 import org.mockito.mock.MockCreationSettings; 25 import org.mockito.plugins.MockMaker; 26 import org.mockito.plugins.StackTraceCleanerProvider; 27 28 import java.lang.reflect.InvocationHandler; 29 import java.lang.reflect.InvocationTargetException; 30 import java.lang.reflect.Method; 31 import java.lang.reflect.Modifier; 32 import java.lang.reflect.Proxy; 33 import java.util.Set; 34 35 /** 36 * Generates mock instances on Android's runtime. 37 */ 38 public final class DexmakerMockMaker implements MockMaker, StackTraceCleanerProvider { 39 private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); 40 private boolean isApi28; 41 DexmakerMockMaker()42 public DexmakerMockMaker() throws Exception { 43 try { 44 Class buildVersion = Class.forName("android.os.Build$VERSION"); 45 isApi28 = buildVersion.getDeclaredField("SDK_INT").getInt(null) >= 28; 46 } catch (ClassNotFoundException e) { 47 System.err.println("Could not determine platform API level, assuming >= 28: " + e); 48 isApi28 = true; 49 } 50 51 if (isApi28) { 52 // Blacklisted APIs were introduced in Android P: 53 // 54 // https://android-developers.googleblog.com/2018/02/ 55 // improving-stability-by-reducing-usage.html 56 // 57 // This feature prevents access to blacklisted fields and calling of blacklisted APIs 58 // if the calling class is not trusted. 59 Method allowHiddenApiReflectionFromMethod; 60 try { 61 Class vmDebug = Class.forName("dalvik.system.VMDebug"); 62 allowHiddenApiReflectionFromMethod = vmDebug.getDeclaredMethod( 63 "allowHiddenApiReflectionFrom", Class.class); 64 } catch (ClassNotFoundException | NoSuchMethodException e) { 65 throw new IllegalStateException( 66 "Cannot find VMDebug#allowHiddenApiReflectionFrom. Method is needed to " 67 + "allow spies to copy blacklisted fields."); 68 } 69 70 // The LenientCopyTool copies the fields to a spy when creating the copy from an 71 // existing object. Some of the fields might be blacklisted. Marking the LenientCopyTool 72 // as trusted allows the tool to copy all fields, including the blacklisted ones. 73 try { 74 allowHiddenApiReflectionFromMethod.invoke(null, LenientCopyTool.class); 75 } catch (InvocationTargetException | IllegalAccessException e) { 76 System.err.println("Cannot allow LenientCopyTool to copy spies of blacklisted " 77 + "fields. This might break spying on system classes."); 78 } 79 } 80 } 81 82 @Override createMock(MockCreationSettings<T> settings, MockHandler handler)83 public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) { 84 Class<T> typeToMock = settings.getTypeToMock(); 85 Set<Class<?>> interfacesSet = settings.getExtraInterfaces(); 86 Class<?>[] extraInterfaces = interfacesSet.toArray(new Class[interfacesSet.size()]); 87 InvocationHandler invocationHandler = new InvocationHandlerAdapter(handler); 88 89 if (typeToMock.isInterface()) { 90 // support interfaces via java.lang.reflect.Proxy 91 Class[] classesToMock = new Class[extraInterfaces.length + 1]; 92 classesToMock[0] = typeToMock; 93 System.arraycopy(extraInterfaces, 0, classesToMock, 1, extraInterfaces.length); 94 // newProxyInstance returns the type of typeToMock 95 @SuppressWarnings("unchecked") 96 T mock = (T) Proxy.newProxyInstance(typeToMock.getClassLoader(), classesToMock, invocationHandler); 97 return mock; 98 99 } else { 100 // support concrete classes via dexmaker's ProxyBuilder 101 try { 102 ProxyBuilder builder = ProxyBuilder.forClass(typeToMock) 103 .implementing(extraInterfaces); 104 105 if (isApi28) { 106 builder.markTrusted(); 107 } 108 109 if (Boolean.parseBoolean( 110 System.getProperty("dexmaker.share_classloader", "false"))) { 111 builder.withSharedClassLoader(); 112 } 113 114 Class<? extends T> proxyClass = builder.buildProxyClass(); 115 T mock = unsafeAllocator.newInstance(proxyClass); 116 ProxyBuilder.setInvocationHandler(mock, invocationHandler); 117 return mock; 118 } catch (RuntimeException e) { 119 throw e; 120 } catch (Exception e) { 121 throw new MockitoException("Failed to mock " + typeToMock, e); 122 } 123 } 124 } 125 126 @Override resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings)127 public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) { 128 InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock); 129 adapter.setHandler(newHandler); 130 } 131 132 @Override isTypeMockable(final Class<?> type)133 public TypeMockability isTypeMockable(final Class<?> type) { 134 return new TypeMockability() { 135 @Override 136 public boolean mockable() { 137 return !type.isPrimitive() && !Modifier.isFinal(type.getModifiers()); 138 } 139 140 @Override 141 public String nonMockableReason() { 142 if (type.isPrimitive()) { 143 return "primitive type"; 144 } 145 146 if (Modifier.isFinal(type.getModifiers())) { 147 return "final or anonymous class"; 148 } 149 150 return "not handled type"; 151 } 152 }; 153 } 154 155 @Override 156 public MockHandler getHandler(Object mock) { 157 InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock); 158 return adapter != null ? adapter.getHandler() : null; 159 } 160 161 @Override 162 public StackTraceCleaner getStackTraceCleaner(final StackTraceCleaner defaultCleaner) { 163 return new StackTraceCleaner() { 164 @Override 165 public boolean isIn(StackTraceElement candidate) { 166 String className = candidate.getClassName(); 167 168 return defaultCleaner.isIn(candidate) 169 && !className.endsWith("_Proxy") // dexmaker class proxies 170 && !className.startsWith("$Proxy") // dalvik interface proxies 171 && !className.startsWith("java.lang.reflect.Proxy") 172 && !(className.startsWith("com.android.dx.mockito.") 173 // Do not clean unit tests 174 && !className.startsWith("com.android.dx.mockito.tests")); 175 } 176 }; 177 } 178 179 private InvocationHandlerAdapter getInvocationHandlerAdapter(Object mock) { 180 if (mock == null) { 181 return null; 182 } 183 if (Proxy.isProxyClass(mock.getClass())) { 184 InvocationHandler invocationHandler = Proxy.getInvocationHandler(mock); 185 return invocationHandler instanceof InvocationHandlerAdapter 186 ? (InvocationHandlerAdapter) invocationHandler 187 : null; 188 } 189 190 if (ProxyBuilder.isProxyClass(mock.getClass())) { 191 InvocationHandler invocationHandler = ProxyBuilder.getInvocationHandler(mock); 192 return invocationHandler instanceof InvocationHandlerAdapter 193 ? (InvocationHandlerAdapter) invocationHandler 194 : null; 195 } 196 197 return null; 198 } 199 } 200