• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.inline;
18 
19 import android.os.Build;
20 
21 import org.mockito.Mockito;
22 import org.mockito.creation.instance.Instantiator;
23 import org.mockito.exceptions.base.MockitoException;
24 import org.mockito.invocation.MockHandler;
25 import org.mockito.mock.MockCreationSettings;
26 import org.mockito.plugins.InstantiatorProvider2;
27 import org.mockito.plugins.MockMaker;
28 
29 import java.io.IOException;
30 import java.lang.reflect.InvocationTargetException;
31 import java.lang.reflect.Method;
32 import java.lang.reflect.Modifier;
33 import java.util.HashMap;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.function.BiConsumer;
37 
38 /**
39  * Creates mock markers and adds stubbing hooks to static method
40  *
41  * <p>This is done by transforming the byte code of the classes to add method entry hooks.
42  */
43 public final class InlineStaticMockMaker implements MockMaker {
44     /**
45      * {@link StaticJvmtiAgent} set up during one time init
46      */
47     private static final StaticJvmtiAgent AGENT;
48 
49     /**
50      * Error  during one time init or {@code null} if init was successful
51      */
52     private static final Throwable INITIALIZATION_ERROR;
53     public static ThreadLocal<Class> mockingInProgressClass = new ThreadLocal<>();
54     public static ThreadLocal<BiConsumer<Class<?>, Method>> onMethodCallDuringStubbing
55             = new ThreadLocal<>();
56     public static ThreadLocal<BiConsumer<Class<?>, Method>> onMethodCallDuringVerification
57             = new ThreadLocal<>();
58 
59     /*
60      * One time setup to allow the system to mocking via this mock maker.
61      */
62     static {
63         StaticJvmtiAgent agent;
64         Throwable initializationError = null;
65 
66         try {
67             try {
68                 agent = new StaticJvmtiAgent();
69             } catch (IOException ioe) {
70                 throw new IllegalStateException("Mockito could not self-attach a jvmti agent to " +
71                         "the current VM. This feature is required for inline mocking.\nThis error" +
72                         " occured due to an I/O error during the creation of this agent: " + ioe
73                         + "\n\nPotentially, the current VM does not support the jvmti API " +
74                         "correctly", ioe);
75             }
76 
77             // Blacklisted APIs were introduced in Android P:
78             //
79             // https://android-developers.googleblog.com/2018/02/
80             // improving-stability-by-reducing-usage.html
81             //
82             // This feature prevents access to blacklisted fields and calling of blacklisted APIs
83             // if the calling class is not trusted.
84             Method allowHiddenApiReflectionFrom;
85             try {
86                 Class vmDebug = Class.forName("dalvik.system.VMDebug");
87                 allowHiddenApiReflectionFrom = vmDebug.getDeclaredMethod(
88                         "allowHiddenApiReflectionFrom", Class.class);
89             } catch (ClassNotFoundException | NoSuchMethodException e) {
90                 throw new IllegalStateException("Cannot find "
91                         + "VMDebug#allowHiddenApiReflectionFrom.");
92             }
93 
94             // The StaticMockMethodAdvice is used by methods of spies to call the real methods. As
95             // the real methods might be blacklisted, this class needs to be marked as trusted.
96             try {
allowHiddenApiReflectionFrom.invoke(null, StaticMockMethodAdvice.class)97                 allowHiddenApiReflectionFrom.invoke(null, StaticMockMethodAdvice.class);
98             } catch (InvocationTargetException e) {
99                 throw e.getCause();
100             }
101         } catch (Throwable throwable) {
102             agent = null;
103             initializationError = throwable;
104         }
105 
106         AGENT = agent;
107         INITIALIZATION_ERROR = initializationError;
108     }
109 
110     /**
111      * All currently active mock markers. We modify the class's byte code. Some objects of the class
112      * are modified, some are not. This list helps the {@link MockMethodAdvice} help figure out if a
113      * object's method calls should be intercepted.
114      */
115     private final HashMap<Object, InvocationHandlerAdapter> markerToHandler = new HashMap<>();
116     private final Map<Class, Object> classToMarker = new HashMap<>();
117 
118     /**
119      * Class doing the actual byte code transformation.
120      */
121     private final StaticClassTransformer classTransformer;
122 
123     /**
124      * Create a new mock maker.
125      */
InlineStaticMockMaker()126     public InlineStaticMockMaker() {
127         if (INITIALIZATION_ERROR != null) {
128             throw new RuntimeException("Could not initialize inline mock maker.\n" + "\n" +
129                     "Release: Android " + Build.VERSION.RELEASE_OR_CODENAME + " "
130                     + Build.VERSION.INCREMENTAL
131                     + "Device: " + Build.BRAND + " " + Build.MODEL, INITIALIZATION_ERROR);
132         }
133 
134         classTransformer = new StaticClassTransformer(AGENT, InlineDexmakerMockMaker
135                 .DISPATCHER_CLASS, markerToHandler, classToMarker);
136     }
137 
138     @Override
createMock(MockCreationSettings<T> settings, MockHandler handler)139     public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
140         Class<T> typeToMock = settings.getTypeToMock();
141         if (!typeToMock.equals(mockingInProgressClass.get()) || Modifier.isAbstract(typeToMock
142                 .getModifiers())) {
143             return null;
144         }
145 
146         Set<Class<?>> interfacesSet = settings.getExtraInterfaces();
147         InvocationHandlerAdapter handlerAdapter = new InvocationHandlerAdapter(handler);
148 
149         classTransformer.mockClass(MockFeatures.withMockFeatures(typeToMock, interfacesSet));
150 
151         Instantiator instantiator = Mockito.framework().getPlugins().getDefaultPlugin
152                 (InstantiatorProvider2.class).getInstantiator(settings);
153 
154         T mock;
155         try {
156             mock = instantiator.newInstance(typeToMock);
157         } catch (org.mockito.creation.instance.InstantiationException e) {
158             throw new MockitoException("Unable to create mock instance of type '" + typeToMock
159                     .getSimpleName() + "'", e);
160         }
161 
162         if (classToMarker.containsKey(typeToMock)) {
163             throw new MockitoException(typeToMock + " is already mocked");
164         }
165         classToMarker.put(typeToMock, mock);
166 
167         markerToHandler.put(mock, handlerAdapter);
168         return mock;
169     }
170 
171     @Override
resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings)172     public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) {
173         InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
174         if (adapter != null) {
175             if (mockingInProgressClass.get() == mock.getClass()) {
176                 markerToHandler.remove(mock);
177                 classToMarker.remove(mock.getClass());
178             } else {
179                 adapter.setHandler(newHandler);
180             }
181         }
182     }
183 
184     @Override
isTypeMockable(final Class<?> type)185     public TypeMockability isTypeMockable(final Class<?> type) {
186         if (mockingInProgressClass.get() == type) {
187             return new TypeMockability() {
188                 @Override
189                 public boolean mockable() {
190                     return !Modifier.isAbstract(type.getModifiers()) && !type.isPrimitive() && type
191                             != String.class;
192                 }
193 
194                 @Override
195                 public String nonMockableReason() {
196                     if (Modifier.isAbstract(type.getModifiers())) {
197                         return "abstract type";
198                     }
199 
200                     if (type.isPrimitive()) {
201                         return "primitive type";
202                     }
203 
204                     if (type == String.class) {
205                         return "string";
206                     }
207 
208                     return "not handled type";
209                 }
210             };
211         } else {
212             return null;
213         }
214     }
215 
216     @Override
217     public MockHandler getHandler(Object mock) {
218         InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
219         return adapter != null ? adapter.getHandler() : null;
220     }
221 
222     /**
223      * Get the {@link InvocationHandlerAdapter} registered for a marker.
224      *
225      * @param marker marker of the class that might have mocking set up
226      * @return adapter for this class, or {@code null} if not mocked
227      */
228     private InvocationHandlerAdapter getInvocationHandlerAdapter(Object marker) {
229         if (marker == null) {
230             return null;
231         }
232 
233         return markerToHandler.get(marker);
234     }
235 }
236