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