• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2018 Mockito contributors
3  * This program is made available under the terms of the MIT License.
4  */
5 
6 package com.android.dx.mockito.inline;
7 
8 import java.lang.ref.WeakReference;
9 import java.lang.reflect.InvocationTargetException;
10 import java.lang.reflect.Method;
11 import java.lang.reflect.Modifier;
12 import java.util.ArrayList;
13 import java.util.Collection;
14 import java.util.List;
15 import java.util.Map;
16 import java.util.concurrent.Callable;
17 import java.util.function.BiConsumer;
18 import java.util.regex.Matcher;
19 import java.util.regex.Pattern;
20 
21 import static com.android.dx.mockito.inline.InlineStaticMockMaker.onMethodCallDuringStubbing;
22 import static com.android.dx.mockito.inline.InlineStaticMockMaker.onMethodCallDuringVerification;
23 
24 /**
25  * Backend for the method entry hooks. Checks if the hooks should cause an interception or should
26  * be ignored.
27  */
28 class StaticMockMethodAdvice {
29     /**
30      * Pattern to decompose a instrumentedMethodWithTypeAndSignature
31      */
32     private final static Pattern methodPattern = Pattern.compile("(.*)#(.*)\\((.*)\\)");
33     private final Map<Object, InvocationHandlerAdapter> markersToHandler;
34     private final Map<Class, Object> classToMarker;
35     @SuppressWarnings("ThreadLocalUsage")
36     private final SelfCallInfo selfCallInfo = new SelfCallInfo();
37 
StaticMockMethodAdvice(Map<Object, InvocationHandlerAdapter> markerToHandler, Map<Class, Object> classToMarker)38     StaticMockMethodAdvice(Map<Object, InvocationHandlerAdapter> markerToHandler, Map<Class, Object>
39             classToMarker) {
40         this.markersToHandler = markerToHandler;
41         this.classToMarker = classToMarker;
42     }
43 
44     /**
45      * Try to invoke the method {@code origin}.
46      *
47      * @param origin    method to invoke
48      * @param arguments arguments to the method
49      * @return result of the method
50      * @throws Throwable Exception if thrown by the method
51      */
tryInvoke(Method origin, Object[] arguments)52     private static Object tryInvoke(Method origin, Object[] arguments)
53             throws Throwable {
54         try {
55             return origin.invoke(null, arguments);
56         } catch (InvocationTargetException exception) {
57             throw exception.getCause();
58         }
59     }
60 
classForTypeName(String name)61     private static Class<?> classForTypeName(String name) throws ClassNotFoundException {
62         if (name.endsWith("[]")) {
63             return Class.forName("[L" + name.substring(0, name.length() - 2) + ";");
64         } else {
65             return Class.forName(name);
66         }
67     }
68 
nameToType(String name)69     private static Class nameToType(String name) throws ClassNotFoundException {
70         switch (name) {
71             case "byte":
72                 return Byte.TYPE;
73             case "short":
74                 return Short.TYPE;
75             case "int":
76                 return Integer.TYPE;
77             case "long":
78                 return Long.TYPE;
79             case "char":
80                 return Character.TYPE;
81             case "float":
82                 return Float.TYPE;
83             case "double":
84                 return Double.TYPE;
85             case "boolean":
86                 return Boolean.TYPE;
87             case "byte[]":
88                 return byte[].class;
89             case "short[]":
90                 return short[].class;
91             case "int[]":
92                 return int[].class;
93             case "long[]":
94                 return long[].class;
95             case "char[]":
96                 return char[].class;
97             case "float[]":
98                 return float[].class;
99             case "double[]":
100                 return double[].class;
101             case "boolean[]":
102                 return boolean[].class;
103             default:
104                 return classForTypeName(name);
105         }
106     }
107 
108     /**
109      * Would a call to SubClass.method handled by SuperClass.method ?
110      * <p>This is the case when subclass or any intermediate parent does not override method.
111      *
112      * @param subclass         Class that might have been called
113      * @param superClass       Class defining the method
114      * @param methodName       Name of method
115      * @param methodParameters Parameter of method
116      * @return {code true} iff the method would have be handled by superClass
117      */
isMethodDefinedBySuperClass(Class<?> subclass, Class<?> superClass, String methodName, Class<?>[] methodParameters)118     private static boolean isMethodDefinedBySuperClass(Class<?> subclass, Class<?> superClass,
119                                                        String methodName,
120                                                        Class<?>[] methodParameters) {
121         do {
122             if (subclass == superClass) {
123                 // The method is not overridden in the subclass or any class in between subClass
124                 // and superClass.
125                 return true;
126             }
127 
128             try {
129                 subclass.getDeclaredMethod(methodName, methodParameters);
130 
131                 // method is overridden is sub-class. hence the call could not have handled by
132                 // the super-class.
133                 return false;
134             } catch (NoSuchMethodException e) {
135                 subclass = subclass.getSuperclass();
136             }
137         } while (subclass != null);
138 
139         // Subclass is not a sub class of superClass
140         return false;
141     }
142 
getAllSubclasses(Class<?> superClass, Collection<Class> possibleSubClasses)143     private static List<Class<?>> getAllSubclasses(Class<?> superClass, Collection<Class>
144             possibleSubClasses) {
145         ArrayList<Class<?>> subclasses = new ArrayList<>();
146         for (Class<?> possibleSubClass : possibleSubClasses) {
147             if (superClass.isAssignableFrom(possibleSubClass)) {
148                 subclasses.add(possibleSubClass);
149             }
150         }
151 
152         return subclasses;
153     }
154 
nativeGetCalledClassName(Thread currentThread)155     private synchronized static native String nativeGetCalledClassName(Thread currentThread);
156 
getClassMethodWasCalledOn(MethodDesc methodDesc)157     private Class<?> getClassMethodWasCalledOn(MethodDesc methodDesc) throws ClassNotFoundException,
158             NoSuchMethodException {
159         Class<?> classDeclaringMethod = classForTypeName(methodDesc.className);
160 
161         /* If a sub-class does not override a static method, the super-classes method is called
162          * directly. Hence 'classDeclaringMethod' will be the super class. As the mocking of
163          * this and the class actually called might be different we need to find the class that
164          * was actually called.
165          */
166         if (Modifier.isFinal(classDeclaringMethod.getModifiers())
167                 || Modifier.isFinal(classDeclaringMethod.getDeclaredMethod(methodDesc.methodName,
168                 methodDesc.methodParamTypes).getModifiers())) {
169             return classDeclaringMethod;
170         } else {
171             boolean mightBeMocked = false;
172             // if neither the defining class nor any subclass of it is mocked, no point of
173             // trying to figure out the called class as isMocked will soon be checked.
174             for (Class<?> subClass : getAllSubclasses(classDeclaringMethod, classToMarker.keySet())) {
175                 if (isMethodDefinedBySuperClass(subClass, classDeclaringMethod,
176                         methodDesc.methodName, methodDesc.methodParamTypes)) {
177                     mightBeMocked = true;
178                     break;
179                 }
180             }
181 
182             if (!mightBeMocked) {
183                 return null;
184             }
185 
186             String calledClassName = nativeGetCalledClassName(Thread.currentThread());
187             return Class.forName(calledClassName);
188         }
189     }
190 
191     /**
192      * Get the method specified by {@code methodWithTypeAndSignature}.
193      *
194      * @param ignored
195      * @param methodWithTypeAndSignature the description of the method
196      * @return method {@code methodWithTypeAndSignature} refer to
197      */
198     @SuppressWarnings("unused")
getOrigin(Object ignored, String methodWithTypeAndSignature)199     public Method getOrigin(Object ignored, String methodWithTypeAndSignature) throws Throwable {
200         MethodDesc methodDesc = new MethodDesc(methodWithTypeAndSignature);
201 
202         Class clazz = getClassMethodWasCalledOn(methodDesc);
203         if (clazz == null) {
204             return null;
205         }
206 
207         Object marker = classToMarker.get(clazz);
208         if (!isMocked(marker)) {
209             return null;
210         }
211 
212         return Class.forName(methodDesc.className).getDeclaredMethod(methodDesc.methodName,
213                 methodDesc.methodParamTypes);
214     }
215 
216     /**
217      * Handle a method entry hook.
218      *
219      * @param origin    method that contains the hook
220      * @param arguments arguments to the method
221      * @return A callable that can be called to get the mocked result or null if the method is not
222      * mocked.
223      */
224     @SuppressWarnings("unused")
handle(Object methodDescStr, Method origin, Object[] arguments)225     public Callable<?> handle(Object methodDescStr, Method origin, Object[] arguments) throws
226             Throwable {
227         MethodDesc methodDesc = new MethodDesc((String) methodDescStr);
228         Class clazz = getClassMethodWasCalledOn(methodDesc);
229 
230         Object marker = classToMarker.get(clazz);
231         InvocationHandlerAdapter interceptor = markersToHandler.get(marker);
232         if (interceptor == null) {
233             return null;
234         }
235 
236         // extended.StaticCapableStubber#whenInt
237         BiConsumer<Class<?>, Method> onStub = onMethodCallDuringStubbing.get();
238         if (onStub != null) {
239             onStub.accept(clazz, origin);
240         }
241 
242         // extended.ExtendedMockito#verifyInt
243         BiConsumer<Class<?>, Method> onVerify = onMethodCallDuringVerification.get();
244         if (onVerify != null) {
245             onVerify.accept(clazz, origin);
246         }
247 
248         return new ReturnValueWrapper(interceptor.interceptEntryHook(marker, origin, arguments,
249                 new SuperMethodCall(selfCallInfo, origin, marker, arguments)));
250     }
251 
252     /**
253      * Checks if an {@code marker} is a mock marker.
254      *
255      * @return {@code true} iff the marker is a mock marker
256      */
isMarker(Object marker)257     public boolean isMarker(Object marker) {
258         return markersToHandler.containsKey(marker);
259     }
260 
261     /**
262      * Check if this method call should be mocked. Usually the same as {@link #isMarker(Object)} but
263      * takes into account the state of {@link #selfCallInfo} that allows to temporary disable
264      * mocking for a single method call.
265      */
isMocked(Object marker)266     public boolean isMocked(Object marker) {
267         return selfCallInfo.shouldMockMethod(marker) && isMarker(marker);
268     }
269 
270     private static class MethodDesc {
271         final String className;
272         final String methodName;
273         final Class<?>[] methodParamTypes;
274 
MethodDesc(String methodWithTypeAndSignature)275         private MethodDesc(String methodWithTypeAndSignature) throws ClassNotFoundException {
276             Matcher methodComponents = methodPattern.matcher(methodWithTypeAndSignature);
277             boolean wasFound = methodComponents.find();
278             if (!wasFound) {
279                 throw new IllegalArgumentException();
280             }
281 
282             className = methodComponents.group(1);
283             methodName = methodComponents.group(2);
284             String methodParamTypeNames[] = methodComponents.group(3).split(",");
285 
286             ArrayList<Class<?>> methodParamTypesList = new ArrayList<>(methodParamTypeNames.length);
287             for (String methodParamName : methodParamTypeNames) {
288                 if (!methodParamName.equals("")) {
289                     methodParamTypesList.add(nameToType(methodParamName));
290                 }
291             }
292             methodParamTypes = methodParamTypesList.toArray(new Class<?>[]{});
293         }
294 
295         @Override
toString()296         public String toString() {
297             return className + "#" + methodName;
298         }
299     }
300 
301     /**
302      * Used to call the real (non mocked) method.
303      */
304     private static class SuperMethodCall implements InvocationHandlerAdapter.SuperMethod {
305         private final SelfCallInfo selfCallInfo;
306         private final Method origin;
307         private final WeakReference<Object> marker;
308         private final Object[] arguments;
309 
SuperMethodCall(SelfCallInfo selfCallInfo, Method origin, Object marker, Object[] arguments)310         private SuperMethodCall(SelfCallInfo selfCallInfo, Method origin, Object marker,
311                                 Object[] arguments) {
312             this.selfCallInfo = selfCallInfo;
313             this.origin = origin;
314             this.marker = new WeakReference(marker);
315             this.arguments = arguments;
316         }
317 
318         /**
319          * Call the read (non mocked) method.
320          *
321          * @return Result of read method
322          * @throws Throwable thrown by the read method
323          */
324         @Override
invoke()325         public Object invoke() throws Throwable {
326             if (!Modifier.isPublic(origin.getDeclaringClass().getModifiers()
327                     & origin.getModifiers())) {
328                 origin.setAccessible(true);
329             }
330 
331             // By setting instance in the the selfCallInfo, once single method call on this instance
332             // and thread will call the read method as isMocked will return false.
333             selfCallInfo.set(marker.get());
334             return tryInvoke(origin, arguments);
335         }
336 
337     }
338 
339     /**
340      * Stores a return value of {@link #handle(Object, Method, Object[])} and returns in on
341      * {@link #call()}.
342      */
343     private static class ReturnValueWrapper implements Callable<Object> {
344         private final Object returned;
345 
ReturnValueWrapper(Object returned)346         private ReturnValueWrapper(Object returned) {
347             this.returned = returned;
348         }
349 
350         @Override
call()351         public Object call() {
352             return returned;
353         }
354     }
355 
356     /**
357      * Used to call the original method. If a instance is {@link #set(Object)}
358      * {@link #shouldMockMethod(Object)} returns false for this instance once.
359      * <p>This is {@link ThreadLocal}, so a thread can {@link #set(Object)} and instance and then
360      * call {@link #shouldMockMethod(Object)} without interference.
361      *
362      * @see SuperMethodCall#invoke()
363      * @see #isMocked(Object)
364      */
365     private static class SelfCallInfo extends ThreadLocal<Object> {
shouldMockMethod(Object value)366         boolean shouldMockMethod(Object value) {
367             Object current = get();
368 
369             if (current == value) {
370                 set(null);
371                 return false;
372             } else {
373                 return true;
374             }
375         }
376     }
377 }
378