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