• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.AsyncTask;
20 import android.os.Build;
21 import android.util.ArraySet;
22 
23 import com.android.dx.stock.ProxyBuilder;
24 import com.android.dx.stock.ProxyBuilder.MethodSetEntry;
25 
26 import org.mockito.Mockito;
27 import org.mockito.creation.instance.Instantiator;
28 import org.mockito.exceptions.base.MockitoException;
29 import org.mockito.internal.util.reflection.LenientCopyTool;
30 import org.mockito.invocation.MockHandler;
31 import org.mockito.mock.MockCreationSettings;
32 import org.mockito.plugins.InlineMockMaker;
33 import org.mockito.plugins.InstantiatorProvider2;
34 import org.mockito.plugins.MockMaker;
35 
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.lang.ref.Reference;
39 import java.lang.ref.ReferenceQueue;
40 import java.lang.ref.WeakReference;
41 import java.lang.reflect.InvocationTargetException;
42 import java.lang.reflect.Method;
43 import java.lang.reflect.Modifier;
44 import java.lang.reflect.Proxy;
45 import java.util.AbstractMap;
46 import java.util.Collection;
47 import java.util.HashMap;
48 import java.util.HashSet;
49 import java.util.Map;
50 import java.util.Set;
51 
52 /**
53  * Generates mock instances on Android's runtime that can mock final methods.
54  *
55  * <p>This is done by transforming the byte code of the classes to add method entry hooks.
56  */
57 
58 public final class InlineDexmakerMockMaker implements InlineMockMaker {
59     private static final String DISPATCHER_CLASS_NAME =
60             "com.android.dx.mockito.inline.MockMethodDispatcher";
61     private static final String DISPATCHER_JAR = "dispatcher.jar";
62 
63     /** {@link com.android.dx.mockito.inline.JvmtiAgent} set up during one time init */
64     private static final JvmtiAgent AGENT;
65 
66     /** Error  during one time init or {@code null} if init was successful*/
67     private static final Throwable INITIALIZATION_ERROR;
68 
69     /**
70      * Class injected into the bootstrap classloader. All entry hooks added to methods will call
71      * this class.
72      */
73     public static final Class DISPATCHER_CLASS;
74 
75     /**
76      * {@code ExtendedMockito#spyOn} allows to turn an existing object into a spy. If this operation
77      * is running this field is set to the object that should become a spy.
78      */
79     public static ThreadLocal<Object> onSpyInProgressInstance = new ThreadLocal<>();
80 
81     /*
82      * One time setup to allow the system to mocking via this mock maker.
83      */
84     static {
85         JvmtiAgent agent;
86         Throwable initializationError = null;
87         Class dispatcherClass = null;
88         try {
89             try {
90                 agent = new JvmtiAgent();
91 
try(InputStream is = InlineDexmakerMockMaker.class.getClassLoader() .getResource(DISPATCHER_JAR).openStream())92                 try (InputStream is = InlineDexmakerMockMaker.class.getClassLoader()
93                         .getResource(DISPATCHER_JAR).openStream()) {
94                     agent.appendToBootstrapClassLoaderSearch(is);
95                 }
96 
97                 try {
98                     dispatcherClass = Class.forName(DISPATCHER_CLASS_NAME, true,
99                             Object.class.getClassLoader());
100 
101                     if (dispatcherClass == null) {
102                         throw new IllegalStateException(DISPATCHER_CLASS_NAME
103                                 + " could not be loaded");
104                     }
105                 } catch (ClassNotFoundException cnfe) {
106                     throw new IllegalStateException(
107                             "Mockito failed to inject the MockMethodDispatcher class into the "
108                             + "bootstrap class loader\n\nIt seems like your current VM does not "
109                             + "support the jvmti API correctly.", cnfe);
110                 }
111             } catch (IOException ioe) {
112                 throw new IllegalStateException(
113                         "Mockito could not self-attach a jvmti agent to the current VM. This "
114                         + "feature is required for inline mocking.\nThis error occured due to an "
115                         + "I/O error during the creation of this agent: " + ioe + "\n\n"
116                         + "Potentially, the current VM does not support the jvmti API correctly",
117                         ioe);
118             }
119 
120             // Blacklisted APIs were introduced in Android P:
121             //
122             // https://android-developers.googleblog.com/2018/02/
123             // improving-stability-by-reducing-usage.html
124             //
125             // This feature prevents access to blacklisted fields and calling of blacklisted APIs
126             // if the calling class is not trusted.
127             Method allowHiddenApiReflectionFrom;
128             try {
129                 Class vmDebug = Class.forName("dalvik.system.VMDebug");
130                 allowHiddenApiReflectionFrom = vmDebug.getDeclaredMethod(
131                         "allowHiddenApiReflectionFrom", Class.class);
132             } catch (ClassNotFoundException | NoSuchMethodException e) {
133                 throw new IllegalStateException("Cannot find "
134                         + "VMDebug#allowHiddenApiReflectionFrom.");
135             }
136 
137             // The LenientCopyTool copies the fields to a spy when creating the copy from an
138             // existing object. Some of the fields might be blacklisted. Marking the LenientCopyTool
139             // as trusted allows the tool to copy all fields, including the blacklisted ones.
140             try {
allowHiddenApiReflectionFrom.invoke(null, LenientCopyTool.class)141                 allowHiddenApiReflectionFrom.invoke(null, LenientCopyTool.class);
142             } catch (InvocationTargetException e) {
143                 throw e.getCause();
144             }
145 
146             // The MockMethodAdvice is used by methods of spies to call the real methods. As the
147             // real methods might be blacklisted, this class needs to be marked as trusted.
148             try {
allowHiddenApiReflectionFrom.invoke(null, MockMethodAdvice.class)149                 allowHiddenApiReflectionFrom.invoke(null, MockMethodAdvice.class);
150             } catch (InvocationTargetException e) {
151                 throw e.getCause();
152             }
153         } catch (Throwable throwable) {
154             agent = null;
155             initializationError = throwable;
156         }
157 
158         AGENT = agent;
159         INITIALIZATION_ERROR = initializationError;
160         DISPATCHER_CLASS = dispatcherClass;
161     }
162 
163     /**
164      * All currently active mocks. We modify the class's byte code. Some objects of the class are
165      * modified, some are not. This list helps the {@link MockMethodAdvice} help figure out if a
166      * object's method calls should be intercepted.
167      */
168     private final Map<Object, InvocationHandlerAdapter> mocks;
169 
170     /**
171      * Class doing the actual byte code transformation.
172      */
173     private final ClassTransformer classTransformer;
174 
175     /**
176      * Create a new mock maker.
177      */
InlineDexmakerMockMaker()178     public InlineDexmakerMockMaker() {
179         if (INITIALIZATION_ERROR != null) {
180             throw new RuntimeException(
181                     "Could not initialize inline mock maker.\n"
182                     + "\n"
183                     + "Release: Android " + Build.VERSION.RELEASE_OR_CODENAME + " "
184                     + Build.VERSION.INCREMENTAL
185                     + "Device: " + Build.BRAND + " " + Build.MODEL, INITIALIZATION_ERROR);
186         }
187 
188         mocks = new MockMap();
189         classTransformer = new ClassTransformer(AGENT, DISPATCHER_CLASS, mocks);
190     }
191 
192     /**
193      * Get methods to proxy.
194      *
195      * <p>Only abstract methods will need to get proxied as all other methods will get an entry
196      * hook.
197      *
198      * @param settings description of the current mocking process.
199      *
200      * @return methods to proxy.
201      */
getMethodsToProxy(MockCreationSettings<T> settings)202     private <T> Method[] getMethodsToProxy(MockCreationSettings<T> settings) {
203         Set<MethodSetEntry> abstractMethods = new HashSet<>();
204         Set<MethodSetEntry> nonAbstractMethods = new HashSet<>();
205 
206         Class<?> superClass = settings.getTypeToMock();
207         while (superClass != null) {
208             for (Method method : superClass.getDeclaredMethods()) {
209                 if (Modifier.isAbstract(method.getModifiers())
210                         && !nonAbstractMethods.contains(new MethodSetEntry(method))) {
211                     abstractMethods.add(new MethodSetEntry(method));
212                 } else {
213                     nonAbstractMethods.add(new MethodSetEntry(method));
214                 }
215             }
216 
217             superClass = superClass.getSuperclass();
218         }
219 
220         for (Class<?> i : settings.getTypeToMock().getInterfaces()) {
221             for (Method method : i.getMethods()) {
222                 if (!nonAbstractMethods.contains(new MethodSetEntry(method))) {
223                     abstractMethods.add(new MethodSetEntry(method));
224                 }
225             }
226         }
227 
228         for (Class<?> i : settings.getExtraInterfaces()) {
229             for (Method method : i.getMethods()) {
230                 if (!nonAbstractMethods.contains(new MethodSetEntry(method))) {
231                     abstractMethods.add(new MethodSetEntry(method));
232                 }
233             }
234         }
235 
236         Method[] methodsToProxy = new Method[abstractMethods.size()];
237         int i = 0;
238         for (MethodSetEntry entry : abstractMethods) {
239             methodsToProxy[i++] = entry.originalMethod;
240         }
241 
242         return methodsToProxy;
243     }
244 
245     @Override
createMock(MockCreationSettings<T> settings, MockHandler handler)246     public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
247         Class<T> typeToMock = settings.getTypeToMock();
248         Set<Class<?>> interfacesSet = settings.getExtraInterfaces();
249         Class<?>[] extraInterfaces = interfacesSet.toArray(new Class[interfacesSet.size()]);
250         InvocationHandlerAdapter handlerAdapter = new InvocationHandlerAdapter(handler);
251 
252         T mock;
253         if (typeToMock.isInterface()) {
254             // support interfaces via java.lang.reflect.Proxy
255             Class[] classesToMock = new Class[extraInterfaces.length + 1];
256             classesToMock[0] = typeToMock;
257             System.arraycopy(extraInterfaces, 0, classesToMock, 1, extraInterfaces.length);
258 
259             // newProxyInstance returns the type of typeToMock
260             mock = (T) Proxy.newProxyInstance(typeToMock.getClassLoader(), classesToMock,
261                     handlerAdapter);
262         } else {
263             boolean subclassingRequired = !interfacesSet.isEmpty()
264                     || Modifier.isAbstract(typeToMock.getModifiers());
265 
266             // Add entry hooks to non-abstract methods.
267             classTransformer.mockClass(MockFeatures.withMockFeatures(typeToMock, interfacesSet));
268 
269             Class<? extends T> proxyClass;
270 
271             Instantiator instantiator = Mockito.framework().getPlugins()
272                     .getDefaultPlugin(InstantiatorProvider2.class).getInstantiator(settings);
273 
274             if (subclassingRequired) {
275                 try {
276                     // support abstract methods via dexmaker's ProxyBuilder
277                     ProxyBuilder builder = ProxyBuilder.forClass(typeToMock).implementing
278                             (extraInterfaces)
279                             .onlyMethods(getMethodsToProxy(settings)).withSharedClassLoader();
280 
281                     if (Build.VERSION.SDK_INT >= 28) {
282                         builder.markTrusted();
283                     }
284 
285                     proxyClass = builder.buildProxyClass();
286                 } catch (RuntimeException e) {
287                     throw e;
288                 } catch (Exception e) {
289                     throw new MockitoException("Failed to mock " + typeToMock, e);
290                 }
291 
292                 try {
293                     mock = instantiator.newInstance(proxyClass);
294                 } catch (org.mockito.creation.instance.InstantiationException e) {
295                     throw new MockitoException("Unable to create mock instance of type '"
296                             + proxyClass.getSuperclass().getSimpleName() + "'", e);
297                 }
298 
299                 ProxyBuilder.setInvocationHandler(mock, handlerAdapter);
300             } else {
301                 if (settings.getSpiedInstance() != null
302                         && onSpyInProgressInstance.get() == settings.getSpiedInstance()) {
303                     mock = (T) onSpyInProgressInstance.get();
304                 } else {
305                     try {
306                         mock = instantiator.newInstance(typeToMock);
307                     } catch (org.mockito.creation.instance.InstantiationException e) {
308                         throw new MockitoException("Unable to create mock instance of type '"
309                                 + typeToMock.getSimpleName() + "'", e);
310                     }
311                 }
312             }
313         }
314 
315         mocks.put(mock, handlerAdapter);
316         return mock;
317     }
318 
319     @Override
resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings)320     public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) {
321         InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
322         if (adapter != null) {
323             adapter.setHandler(newHandler);
324         }
325     }
326 
327     @Override
isTypeMockable(final Class<?> type)328     public TypeMockability isTypeMockable(final Class<?> type) {
329         return new TypeMockability() {
330             @Override
331             public boolean mockable() {
332                 return !type.isPrimitive() && type != String.class;
333             }
334 
335             @Override
336             public String nonMockableReason() {
337                 if (type.isPrimitive()) {
338                     return "primitive type";
339                 }
340 
341                 if (type == String.class) {
342                     return "string";
343                 }
344 
345                 return "not handled type";
346             }
347         };
348     }
349 
350     @Override
351     public void clearMock(Object mock) {
352         mocks.remove(mock);
353     }
354 
355     @Override
356     public void clearAllMocks() {
357         mocks.clear();
358     }
359 
360     @Override
361     public MockHandler getHandler(Object mock) {
362         InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
363         return adapter != null ? adapter.getHandler() : null;
364     }
365 
366     /**
367      * Get the {@link InvocationHandlerAdapter} registered for a mock.
368      *
369      * @param instance instance that might be mocked
370      *
371      * @return adapter for this mock, or {@code null} if instance is not mocked
372      */
373     private InvocationHandlerAdapter getInvocationHandlerAdapter(Object instance) {
374         if (instance == null) {
375             return null;
376         }
377 
378         return mocks.get(instance);
379     }
380 
381     /**
382      * A map mock -> adapter that holds weak references to the mocks and cleans them up when a
383      * stale reference is found.
384      */
385     private static class MockMap extends ReferenceQueue<Object>
386             implements Map<Object, InvocationHandlerAdapter> {
387         private static final int MIN_CLEAN_INTERVAL_MILLIS = 16000;
388         private static final int MAX_GET_WITHOUT_CLEAN = 16384;
389 
390         private final Object lock = new Object();
391         private StrongKey cachedKey;
392 
393         private HashMap<WeakKey, InvocationHandlerAdapter> adapters = new HashMap<>();
394 
395         /**
396          * The time we issues the last cleanup
397          */
398         long mLastCleanup = 0;
399 
400         /**
401          * If {@link #cleanStaleReferences} is currently cleaning stale references out of
402          * {@link #adapters}
403          */
404         private boolean isCleaning = false;
405 
406         /**
407          * The number of time {@link #get} was called without cleaning up stale references.
408          * {@link #get} is a method that is called often.
409          *
410          * We need to do periodic cleanups as we might never look at mocks at higher indexes and
411          * hence never realize that their references are stale.
412          */
413         private int getCount = 0;
414 
415         /**
416          * Try to get a recycled cached key.
417          *
418          * @param obj the reference the key wraps
419          *
420          * @return The recycled cached key or a new one
421          */
422         private StrongKey createStrongKey(Object obj) {
423             synchronized (lock) {
424                 if (cachedKey == null) {
425                     cachedKey = new StrongKey();
426                 }
427 
428                 cachedKey.obj = obj;
429                 StrongKey newKey = cachedKey;
430                 cachedKey = null;
431 
432                 return newKey;
433             }
434         }
435 
436         /**
437          * Recycle a key. The key should not be used afterwards
438          *
439          * @param key The key to recycle
440          */
441         private void recycleStrongKey(StrongKey key) {
442             synchronized (lock) {
443                 cachedKey = key;
444             }
445         }
446 
447         @Override
448         public int size() {
449             return adapters.size();
450         }
451 
452         @Override
453         public boolean isEmpty() {
454             return adapters.isEmpty();
455         }
456 
457         @SuppressWarnings("CollectionIncompatibleType")
458         @Override
459         public boolean containsKey(Object mock) {
460             synchronized (lock) {
461                 StrongKey key = createStrongKey(mock);
462                 boolean containsKey = adapters.containsKey(key);
463                 recycleStrongKey(key);
464 
465                 return containsKey;
466             }
467         }
468 
469         @Override
470         public boolean containsValue(Object adapter) {
471             synchronized (lock) {
472                 return adapters.containsValue(adapter);
473             }
474         }
475 
476         @SuppressWarnings("CollectionIncompatibleType")
477         @Override
478         public InvocationHandlerAdapter get(Object mock) {
479             synchronized (lock) {
480                 if (getCount > MAX_GET_WITHOUT_CLEAN) {
481                     cleanStaleReferences();
482                     getCount = 0;
483                 } else {
484                     getCount++;
485                 }
486 
487                 StrongKey key = createStrongKey(mock);
488                 InvocationHandlerAdapter adapter = adapters.get(key);
489                 recycleStrongKey(key);
490 
491                 return adapter;
492             }
493         }
494 
495         /**
496          * Remove entries that reference a stale mock from {@link #adapters}.
497          */
498         private void cleanStaleReferences() {
499             synchronized (lock) {
500                 if (!isCleaning) {
501                     if (System.currentTimeMillis() - MIN_CLEAN_INTERVAL_MILLIS < mLastCleanup) {
502                         return;
503                     }
504 
505                     isCleaning = true;
506 
507                     AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
508                         @Override
509                         public void run() {
510                             synchronized (lock) {
511                                 while (true) {
512                                     Reference<?> ref = MockMap.this.poll();
513                                     if (ref == null) {
514                                         break;
515                                     }
516 
517                                     adapters.remove(ref);
518                                 }
519 
520                                 mLastCleanup = System.currentTimeMillis();
521                                 isCleaning = false;
522                             }
523                         }
524                     });
525                 }
526             }
527         }
528 
529         @Override
530         public InvocationHandlerAdapter put(Object mock, InvocationHandlerAdapter adapter) {
531             synchronized (lock) {
532                 InvocationHandlerAdapter oldValue = remove(mock);
533                 adapters.put(new WeakKey(mock), adapter);
534 
535                 return oldValue;
536             }
537         }
538 
539         @SuppressWarnings("CollectionIncompatibleType")
540         @Override
541         public InvocationHandlerAdapter remove(Object mock) {
542             synchronized (lock) {
543                 StrongKey key = createStrongKey(mock);
544                 InvocationHandlerAdapter adapter = adapters.remove(key);
545                 recycleStrongKey(key);
546 
547                 return adapter;
548             }
549         }
550 
551         @Override
552         public void putAll(Map<?, ? extends InvocationHandlerAdapter> map) {
553             synchronized (lock) {
554                 for (Entry<?, ? extends InvocationHandlerAdapter> entry : map.entrySet()) {
555                     put(entry.getKey(), entry.getValue());
556                 }
557             }
558         }
559 
560         @Override
561         public void clear() {
562             synchronized (lock) {
563                 adapters.clear();
564             }
565         }
566 
567         @Override
568         public Set<Object> keySet() {
569             synchronized (lock) {
570                 Set<Object> mocks = new ArraySet<>(adapters.size());
571 
572                 boolean hasStaleReferences = false;
573                 for (WeakKey key : adapters.keySet()) {
574                     Object mock = key.get();
575 
576                     if (mock == null) {
577                         hasStaleReferences = true;
578                     } else {
579                         mocks.add(mock);
580                     }
581                 }
582 
583                 if (hasStaleReferences) {
584                     cleanStaleReferences();
585                 }
586 
587                 return mocks;
588             }
589         }
590 
591         @Override
592         public Collection<InvocationHandlerAdapter> values() {
593             synchronized (lock) {
594                 return adapters.values();
595             }
596         }
597 
598         @Override
599         public Set<Entry<Object, InvocationHandlerAdapter>> entrySet() {
600             synchronized (lock) {
601                 Set<Entry<Object, InvocationHandlerAdapter>> entries = new ArraySet<>(
602                         adapters.size());
603 
604                 boolean hasStaleReferences = false;
605                 for (Entry<WeakKey, InvocationHandlerAdapter> entry : adapters.entrySet()) {
606                     Object mock = entry.getKey().get();
607 
608                     if (mock == null) {
609                         hasStaleReferences = true;
610                     } else {
611                         entries.add(new AbstractMap.SimpleEntry<>(mock, entry.getValue()));
612                     }
613                 }
614 
615                 if (hasStaleReferences) {
616                     cleanStaleReferences();
617                 }
618 
619                 return entries;
620             }
621         }
622 
623         /**
624          * A weakly referencing wrapper to a mock.
625          *
626          * Only equals other weak or strong keys where the mock is the same.
627          */
628         private class WeakKey extends WeakReference<Object> {
629             private final int hashCode;
630 
631             private WeakKey(/*@NonNull*/ Object obj) {
632                 super(obj, MockMap.this);
633 
634                 // Cache the hashcode as the referenced object might disappear
635                 hashCode = System.identityHashCode(obj);
636             }
637 
638             @Override
639             public boolean equals(Object other) {
640                 if (other == this) {
641                     return true;
642                 }
643 
644                 if (other == null) {
645                     return false;
646                 }
647 
648                 // Checking hashcode is cheap
649                 if (other.hashCode() != hashCode) {
650                     return false;
651                 }
652 
653                 Object obj = get();
654 
655                 if (obj == null) {
656                     cleanStaleReferences();
657                     return false;
658                 }
659 
660                 if (other instanceof WeakKey) {
661                     Object otherObj = ((WeakKey) other).get();
662 
663                     if (otherObj == null) {
664                         cleanStaleReferences();
665                         return false;
666                     }
667 
668                     return obj == otherObj;
669                 } else if (other instanceof StrongKey) {
670                     Object otherObj = ((StrongKey) other).obj;
671                     return obj == otherObj;
672                 } else {
673                     return false;
674                 }
675             }
676 
677             @Override
678             public int hashCode() {
679                 return hashCode;
680             }
681         }
682 
683         /**
684          * A strongly referencing wrapper to a mock.
685          *
686          * Only equals other weak or strong keys where the mock is the same.
687          */
688         private class StrongKey {
689             /*@NonNull*/ private Object obj;
690 
691             @Override
692             public boolean equals(Object other) {
693                 if (other instanceof WeakKey) {
694                     Object otherObj = ((WeakKey) other).get();
695 
696                     if (otherObj == null) {
697                         cleanStaleReferences();
698                         return false;
699                     }
700 
701                     return obj == otherObj;
702                 } else if (other instanceof StrongKey) {
703                     return this.obj == ((StrongKey)other).obj;
704                 } else {
705                     return false;
706                 }
707             }
708 
709             @Override
710             public int hashCode() {
711                 return System.identityHashCode(obj);
712             }
713         }
714     }
715 }
716