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 + " " + Build.VERSION.INCREMENTAL
184                     + "Device: " + Build.BRAND + " " + Build.MODEL, INITIALIZATION_ERROR);
185         }
186 
187         mocks = new MockMap();
188         classTransformer = new ClassTransformer(AGENT, DISPATCHER_CLASS, mocks);
189     }
190 
191     /**
192      * Get methods to proxy.
193      *
194      * <p>Only abstract methods will need to get proxied as all other methods will get an entry
195      * hook.
196      *
197      * @param settings description of the current mocking process.
198      *
199      * @return methods to proxy.
200      */
getMethodsToProxy(MockCreationSettings<T> settings)201     private <T> Method[] getMethodsToProxy(MockCreationSettings<T> settings) {
202         Set<MethodSetEntry> abstractMethods = new HashSet<>();
203         Set<MethodSetEntry> nonAbstractMethods = new HashSet<>();
204 
205         Class<?> superClass = settings.getTypeToMock();
206         while (superClass != null) {
207             for (Method method : superClass.getDeclaredMethods()) {
208                 if (Modifier.isAbstract(method.getModifiers())
209                         && !nonAbstractMethods.contains(new MethodSetEntry(method))) {
210                     abstractMethods.add(new MethodSetEntry(method));
211                 } else {
212                     nonAbstractMethods.add(new MethodSetEntry(method));
213                 }
214             }
215 
216             superClass = superClass.getSuperclass();
217         }
218 
219         for (Class<?> i : settings.getTypeToMock().getInterfaces()) {
220             for (Method method : i.getMethods()) {
221                 if (!nonAbstractMethods.contains(new MethodSetEntry(method))) {
222                     abstractMethods.add(new MethodSetEntry(method));
223                 }
224             }
225         }
226 
227         for (Class<?> i : settings.getExtraInterfaces()) {
228             for (Method method : i.getMethods()) {
229                 if (!nonAbstractMethods.contains(new MethodSetEntry(method))) {
230                     abstractMethods.add(new MethodSetEntry(method));
231                 }
232             }
233         }
234 
235         Method[] methodsToProxy = new Method[abstractMethods.size()];
236         int i = 0;
237         for (MethodSetEntry entry : abstractMethods) {
238             methodsToProxy[i++] = entry.originalMethod;
239         }
240 
241         return methodsToProxy;
242     }
243 
244     @Override
createMock(MockCreationSettings<T> settings, MockHandler handler)245     public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
246         Class<T> typeToMock = settings.getTypeToMock();
247         Set<Class<?>> interfacesSet = settings.getExtraInterfaces();
248         Class<?>[] extraInterfaces = interfacesSet.toArray(new Class[interfacesSet.size()]);
249         InvocationHandlerAdapter handlerAdapter = new InvocationHandlerAdapter(handler);
250 
251         T mock;
252         if (typeToMock.isInterface()) {
253             // support interfaces via java.lang.reflect.Proxy
254             Class[] classesToMock = new Class[extraInterfaces.length + 1];
255             classesToMock[0] = typeToMock;
256             System.arraycopy(extraInterfaces, 0, classesToMock, 1, extraInterfaces.length);
257 
258             // newProxyInstance returns the type of typeToMock
259             mock = (T) Proxy.newProxyInstance(typeToMock.getClassLoader(), classesToMock,
260                     handlerAdapter);
261         } else {
262             boolean subclassingRequired = !interfacesSet.isEmpty()
263                     || Modifier.isAbstract(typeToMock.getModifiers());
264 
265             // Add entry hooks to non-abstract methods.
266             classTransformer.mockClass(MockFeatures.withMockFeatures(typeToMock, interfacesSet));
267 
268             Class<? extends T> proxyClass;
269 
270             Instantiator instantiator = Mockito.framework().getPlugins()
271                     .getDefaultPlugin(InstantiatorProvider2.class).getInstantiator(settings);
272 
273             if (subclassingRequired) {
274                 try {
275                     // support abstract methods via dexmaker's ProxyBuilder
276                     ProxyBuilder builder = ProxyBuilder.forClass(typeToMock).implementing
277                             (extraInterfaces)
278                             .onlyMethods(getMethodsToProxy(settings)).withSharedClassLoader();
279 
280                     if (Build.VERSION.SDK_INT >= 28) {
281                         builder.markTrusted();
282                     }
283 
284                     proxyClass = builder.buildProxyClass();
285                 } catch (RuntimeException e) {
286                     throw e;
287                 } catch (Exception e) {
288                     throw new MockitoException("Failed to mock " + typeToMock, e);
289                 }
290 
291                 try {
292                     mock = instantiator.newInstance(proxyClass);
293                 } catch (org.mockito.creation.instance.InstantiationException e) {
294                     throw new MockitoException("Unable to create mock instance of type '"
295                             + proxyClass.getSuperclass().getSimpleName() + "'", e);
296                 }
297 
298                 ProxyBuilder.setInvocationHandler(mock, handlerAdapter);
299             } else {
300                 if (settings.getSpiedInstance() != null
301                         && onSpyInProgressInstance.get() == settings.getSpiedInstance()) {
302                     mock = (T) onSpyInProgressInstance.get();
303                 } else {
304                     try {
305                         mock = instantiator.newInstance(typeToMock);
306                     } catch (org.mockito.creation.instance.InstantiationException e) {
307                         throw new MockitoException("Unable to create mock instance of type '"
308                                 + typeToMock.getSimpleName() + "'", e);
309                     }
310                 }
311             }
312         }
313 
314         mocks.put(mock, handlerAdapter);
315         return mock;
316     }
317 
318     @Override
resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings)319     public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) {
320         InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
321         if (adapter != null) {
322             adapter.setHandler(newHandler);
323         }
324     }
325 
326     @Override
isTypeMockable(final Class<?> type)327     public TypeMockability isTypeMockable(final Class<?> type) {
328         return new TypeMockability() {
329             @Override
330             public boolean mockable() {
331                 return !type.isPrimitive() && type != String.class;
332             }
333 
334             @Override
335             public String nonMockableReason() {
336                 if (type.isPrimitive()) {
337                     return "primitive type";
338                 }
339 
340                 if (type == String.class) {
341                     return "string";
342                 }
343 
344                 return "not handled type";
345             }
346         };
347     }
348 
349     @Override
350     public void clearMock(Object mock) {
351         mocks.remove(mock);
352     }
353 
354     @Override
355     public void clearAllMocks() {
356         mocks.clear();
357     }
358 
359     @Override
360     public MockHandler getHandler(Object mock) {
361         InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
362         return adapter != null ? adapter.getHandler() : null;
363     }
364 
365     /**
366      * Get the {@link InvocationHandlerAdapter} registered for a mock.
367      *
368      * @param instance instance that might be mocked
369      *
370      * @return adapter for this mock, or {@code null} if instance is not mocked
371      */
372     private InvocationHandlerAdapter getInvocationHandlerAdapter(Object instance) {
373         if (instance == null) {
374             return null;
375         }
376 
377         return mocks.get(instance);
378     }
379 
380     /**
381      * A map mock -> adapter that holds weak references to the mocks and cleans them up when a
382      * stale reference is found.
383      */
384     private static class MockMap extends ReferenceQueue<Object>
385             implements Map<Object, InvocationHandlerAdapter> {
386         private static final int MIN_CLEAN_INTERVAL_MILLIS = 16000;
387         private static final int MAX_GET_WITHOUT_CLEAN = 16384;
388 
389         private final Object lock = new Object();
390         private StrongKey cachedKey;
391 
392         private HashMap<WeakKey, InvocationHandlerAdapter> adapters = new HashMap<>();
393 
394         /**
395          * The time we issues the last cleanup
396          */
397         long mLastCleanup = 0;
398 
399         /**
400          * If {@link #cleanStaleReferences} is currently cleaning stale references out of
401          * {@link #adapters}
402          */
403         private boolean isCleaning = false;
404 
405         /**
406          * The number of time {@link #get} was called without cleaning up stale references.
407          * {@link #get} is a method that is called often.
408          *
409          * We need to do periodic cleanups as we might never look at mocks at higher indexes and
410          * hence never realize that their references are stale.
411          */
412         private int getCount = 0;
413 
414         /**
415          * Try to get a recycled cached key.
416          *
417          * @param obj the reference the key wraps
418          *
419          * @return The recycled cached key or a new one
420          */
421         private StrongKey createStrongKey(Object obj) {
422             synchronized (lock) {
423                 if (cachedKey == null) {
424                     cachedKey = new StrongKey();
425                 }
426 
427                 cachedKey.obj = obj;
428                 StrongKey newKey = cachedKey;
429                 cachedKey = null;
430 
431                 return newKey;
432             }
433         }
434 
435         /**
436          * Recycle a key. The key should not be used afterwards
437          *
438          * @param key The key to recycle
439          */
440         private void recycleStrongKey(StrongKey key) {
441             synchronized (lock) {
442                 cachedKey = key;
443             }
444         }
445 
446         @Override
447         public int size() {
448             return adapters.size();
449         }
450 
451         @Override
452         public boolean isEmpty() {
453             return adapters.isEmpty();
454         }
455 
456         @SuppressWarnings("CollectionIncompatibleType")
457         @Override
458         public boolean containsKey(Object mock) {
459             synchronized (lock) {
460                 StrongKey key = createStrongKey(mock);
461                 boolean containsKey = adapters.containsKey(key);
462                 recycleStrongKey(key);
463 
464                 return containsKey;
465             }
466         }
467 
468         @Override
469         public boolean containsValue(Object adapter) {
470             synchronized (lock) {
471                 return adapters.containsValue(adapter);
472             }
473         }
474 
475         @SuppressWarnings("CollectionIncompatibleType")
476         @Override
477         public InvocationHandlerAdapter get(Object mock) {
478             synchronized (lock) {
479                 if (getCount > MAX_GET_WITHOUT_CLEAN) {
480                     cleanStaleReferences();
481                     getCount = 0;
482                 } else {
483                     getCount++;
484                 }
485 
486                 StrongKey key = createStrongKey(mock);
487                 InvocationHandlerAdapter adapter = adapters.get(key);
488                 recycleStrongKey(key);
489 
490                 return adapter;
491             }
492         }
493 
494         /**
495          * Remove entries that reference a stale mock from {@link #adapters}.
496          */
497         private void cleanStaleReferences() {
498             synchronized (lock) {
499                 if (!isCleaning) {
500                     if (System.currentTimeMillis() - MIN_CLEAN_INTERVAL_MILLIS < mLastCleanup) {
501                         return;
502                     }
503 
504                     isCleaning = true;
505 
506                     AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
507                         @Override
508                         public void run() {
509                             synchronized (lock) {
510                                 while (true) {
511                                     Reference<?> ref = MockMap.this.poll();
512                                     if (ref == null) {
513                                         break;
514                                     }
515 
516                                     adapters.remove(ref);
517                                 }
518 
519                                 mLastCleanup = System.currentTimeMillis();
520                                 isCleaning = false;
521                             }
522                         }
523                     });
524                 }
525             }
526         }
527 
528         @Override
529         public InvocationHandlerAdapter put(Object mock, InvocationHandlerAdapter adapter) {
530             synchronized (lock) {
531                 InvocationHandlerAdapter oldValue = remove(mock);
532                 adapters.put(new WeakKey(mock), adapter);
533 
534                 return oldValue;
535             }
536         }
537 
538         @SuppressWarnings("CollectionIncompatibleType")
539         @Override
540         public InvocationHandlerAdapter remove(Object mock) {
541             synchronized (lock) {
542                 StrongKey key = createStrongKey(mock);
543                 InvocationHandlerAdapter adapter = adapters.remove(key);
544                 recycleStrongKey(key);
545 
546                 return adapter;
547             }
548         }
549 
550         @Override
551         public void putAll(Map<?, ? extends InvocationHandlerAdapter> map) {
552             synchronized (lock) {
553                 for (Entry<?, ? extends InvocationHandlerAdapter> entry : map.entrySet()) {
554                     put(entry.getKey(), entry.getValue());
555                 }
556             }
557         }
558 
559         @Override
560         public void clear() {
561             synchronized (lock) {
562                 adapters.clear();
563             }
564         }
565 
566         @Override
567         public Set<Object> keySet() {
568             synchronized (lock) {
569                 Set<Object> mocks = new ArraySet<>(adapters.size());
570 
571                 boolean hasStaleReferences = false;
572                 for (WeakKey key : adapters.keySet()) {
573                     Object mock = key.get();
574 
575                     if (mock == null) {
576                         hasStaleReferences = true;
577                     } else {
578                         mocks.add(mock);
579                     }
580                 }
581 
582                 if (hasStaleReferences) {
583                     cleanStaleReferences();
584                 }
585 
586                 return mocks;
587             }
588         }
589 
590         @Override
591         public Collection<InvocationHandlerAdapter> values() {
592             synchronized (lock) {
593                 return adapters.values();
594             }
595         }
596 
597         @Override
598         public Set<Entry<Object, InvocationHandlerAdapter>> entrySet() {
599             synchronized (lock) {
600                 Set<Entry<Object, InvocationHandlerAdapter>> entries = new ArraySet<>(
601                         adapters.size());
602 
603                 boolean hasStaleReferences = false;
604                 for (Entry<WeakKey, InvocationHandlerAdapter> entry : adapters.entrySet()) {
605                     Object mock = entry.getKey().get();
606 
607                     if (mock == null) {
608                         hasStaleReferences = true;
609                     } else {
610                         entries.add(new AbstractMap.SimpleEntry<>(mock, entry.getValue()));
611                     }
612                 }
613 
614                 if (hasStaleReferences) {
615                     cleanStaleReferences();
616                 }
617 
618                 return entries;
619             }
620         }
621 
622         /**
623          * A weakly referencing wrapper to a mock.
624          *
625          * Only equals other weak or strong keys where the mock is the same.
626          */
627         private class WeakKey extends WeakReference<Object> {
628             private final int hashCode;
629 
630             private WeakKey(/*@NonNull*/ Object obj) {
631                 super(obj, MockMap.this);
632 
633                 // Cache the hashcode as the referenced object might disappear
634                 hashCode = System.identityHashCode(obj);
635             }
636 
637             @Override
638             public boolean equals(Object other) {
639                 if (other == this) {
640                     return true;
641                 }
642 
643                 if (other == null) {
644                     return false;
645                 }
646 
647                 // Checking hashcode is cheap
648                 if (other.hashCode() != hashCode) {
649                     return false;
650                 }
651 
652                 Object obj = get();
653 
654                 if (obj == null) {
655                     cleanStaleReferences();
656                     return false;
657                 }
658 
659                 if (other instanceof WeakKey) {
660                     Object otherObj = ((WeakKey) other).get();
661 
662                     if (otherObj == null) {
663                         cleanStaleReferences();
664                         return false;
665                     }
666 
667                     return obj == otherObj;
668                 } else if (other instanceof StrongKey) {
669                     Object otherObj = ((StrongKey) other).obj;
670                     return obj == otherObj;
671                 } else {
672                     return false;
673                 }
674             }
675 
676             @Override
677             public int hashCode() {
678                 return hashCode;
679             }
680         }
681 
682         /**
683          * A strongly referencing wrapper to a mock.
684          *
685          * Only equals other weak or strong keys where the mock is the same.
686          */
687         private class StrongKey {
688             /*@NonNull*/ private Object obj;
689 
690             @Override
691             public boolean equals(Object other) {
692                 if (other instanceof WeakKey) {
693                     Object otherObj = ((WeakKey) other).get();
694 
695                     if (otherObj == null) {
696                         cleanStaleReferences();
697                         return false;
698                     }
699 
700                     return obj == otherObj;
701                 } else if (other instanceof StrongKey) {
702                     return this.obj == ((StrongKey)other).obj;
703                 } else {
704                     return false;
705                 }
706             }
707 
708             @Override
709             public int hashCode() {
710                 return System.identityHashCode(obj);
711             }
712         }
713     }
714 }
715