Home | History | Annotate | Download | only in mockito
      1 /*
      2  * Copyright (C) 2012 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;
     18 
     19 import com.android.dx.stock.ProxyBuilder;
     20 import org.mockito.exceptions.base.MockitoException;
     21 import org.mockito.exceptions.stacktrace.StackTraceCleaner;
     22 import org.mockito.internal.util.reflection.LenientCopyTool;
     23 import org.mockito.invocation.MockHandler;
     24 import org.mockito.mock.MockCreationSettings;
     25 import org.mockito.plugins.MockMaker;
     26 import org.mockito.plugins.StackTraceCleanerProvider;
     27 
     28 import java.lang.reflect.InvocationHandler;
     29 import java.lang.reflect.InvocationTargetException;
     30 import java.lang.reflect.Method;
     31 import java.lang.reflect.Modifier;
     32 import java.lang.reflect.Proxy;
     33 import java.util.Set;
     34 
     35 /**
     36  * Generates mock instances on Android's runtime.
     37  */
     38 public final class DexmakerMockMaker implements MockMaker, StackTraceCleanerProvider {
     39     private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
     40     private boolean isApi28;
     41 
     42     public DexmakerMockMaker() throws Exception {
     43         try {
     44             Class buildVersion = Class.forName("android.os.Build$VERSION");
     45             isApi28 = buildVersion.getDeclaredField("SDK_INT").getInt(null) >= 28;
     46         } catch (ClassNotFoundException e) {
     47             System.err.println("Could not determine platform API level, assuming >= 28: " + e);
     48             isApi28 = true;
     49         }
     50 
     51         if (isApi28) {
     52             // Blacklisted APIs were introduced in Android P:
     53             //
     54             // https://android-developers.googleblog.com/2018/02/
     55             // improving-stability-by-reducing-usage.html
     56             //
     57             // This feature prevents access to blacklisted fields and calling of blacklisted APIs
     58             // if the calling class is not trusted.
     59             Method allowHiddenApiReflectionFromMethod;
     60             try {
     61                 Class vmDebug = Class.forName("dalvik.system.VMDebug");
     62                 allowHiddenApiReflectionFromMethod = vmDebug.getDeclaredMethod(
     63                         "allowHiddenApiReflectionFrom", Class.class);
     64             } catch (ClassNotFoundException | NoSuchMethodException e) {
     65                 throw new IllegalStateException(
     66                         "Cannot find VMDebug#allowHiddenApiReflectionFrom. Method is needed to "
     67                                 + "allow spies to copy blacklisted fields.");
     68             }
     69 
     70             // The LenientCopyTool copies the fields to a spy when creating the copy from an
     71             // existing object. Some of the fields might be blacklisted. Marking the LenientCopyTool
     72             // as trusted allows the tool to copy all fields, including the blacklisted ones.
     73             try {
     74                 allowHiddenApiReflectionFromMethod.invoke(null, LenientCopyTool.class);
     75             } catch (InvocationTargetException | IllegalAccessException e) {
     76                 System.err.println("Cannot allow LenientCopyTool to copy spies of blacklisted "
     77                         + "fields. This might break spying on system classes.");
     78             }
     79         }
     80     }
     81 
     82     @Override
     83     public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
     84         Class<T> typeToMock = settings.getTypeToMock();
     85         Set<Class<?>> interfacesSet = settings.getExtraInterfaces();
     86         Class<?>[] extraInterfaces = interfacesSet.toArray(new Class[interfacesSet.size()]);
     87         InvocationHandler invocationHandler = new InvocationHandlerAdapter(handler);
     88 
     89         if (typeToMock.isInterface()) {
     90             // support interfaces via java.lang.reflect.Proxy
     91             Class[] classesToMock = new Class[extraInterfaces.length + 1];
     92             classesToMock[0] = typeToMock;
     93             System.arraycopy(extraInterfaces, 0, classesToMock, 1, extraInterfaces.length);
     94             // newProxyInstance returns the type of typeToMock
     95             @SuppressWarnings("unchecked")
     96             T mock = (T) Proxy.newProxyInstance(typeToMock.getClassLoader(), classesToMock, invocationHandler);
     97             return mock;
     98 
     99         } else {
    100             // support concrete classes via dexmaker's ProxyBuilder
    101             try {
    102                 ProxyBuilder builder = ProxyBuilder.forClass(typeToMock)
    103                         .implementing(extraInterfaces);
    104 
    105                 if (isApi28) {
    106                     builder.markTrusted();
    107                 }
    108 
    109                 if (Boolean.parseBoolean(
    110                         System.getProperty("dexmaker.share_classloader", "false"))) {
    111                     builder.withSharedClassLoader();
    112                 }
    113 
    114                 Class<? extends T> proxyClass = builder.buildProxyClass();
    115                 T mock = unsafeAllocator.newInstance(proxyClass);
    116                 ProxyBuilder.setInvocationHandler(mock, invocationHandler);
    117                 return mock;
    118             } catch (RuntimeException e) {
    119                 throw e;
    120             } catch (Exception e) {
    121                 throw new MockitoException("Failed to mock " + typeToMock, e);
    122             }
    123         }
    124     }
    125 
    126     @Override
    127     public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) {
    128         InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
    129         adapter.setHandler(newHandler);
    130     }
    131 
    132     @Override
    133     public TypeMockability isTypeMockable(final Class<?> type) {
    134         return new TypeMockability() {
    135             @Override
    136             public boolean mockable() {
    137                 return !type.isPrimitive() && !Modifier.isFinal(type.getModifiers());
    138             }
    139 
    140             @Override
    141             public String nonMockableReason() {
    142                 if (type.isPrimitive()) {
    143                     return "primitive type";
    144                 }
    145 
    146                 if (Modifier.isFinal(type.getModifiers())) {
    147                     return "final or anonymous class";
    148                 }
    149 
    150                 return "not handled type";
    151             }
    152         };
    153     }
    154 
    155     @Override
    156     public MockHandler getHandler(Object mock) {
    157         InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock);
    158         return adapter != null ? adapter.getHandler() : null;
    159     }
    160 
    161     @Override
    162     public StackTraceCleaner getStackTraceCleaner(final StackTraceCleaner defaultCleaner) {
    163         return new StackTraceCleaner() {
    164             @Override
    165             public boolean isIn(StackTraceElement candidate) {
    166                 String className = candidate.getClassName();
    167 
    168                 return defaultCleaner.isIn(candidate)
    169                         && !className.endsWith("_Proxy") // dexmaker class proxies
    170                         && !className.startsWith("$Proxy") // dalvik interface proxies
    171                         && !className.startsWith("java.lang.reflect.Proxy")
    172                         && !(className.startsWith("com.android.dx.mockito.")
    173                              // Do not clean unit tests
    174                              && !className.startsWith("com.android.dx.mockito.tests"));
    175             }
    176         };
    177     }
    178 
    179     private InvocationHandlerAdapter getInvocationHandlerAdapter(Object mock) {
    180         if (mock == null) {
    181             return null;
    182         }
    183         if (Proxy.isProxyClass(mock.getClass())) {
    184             InvocationHandler invocationHandler = Proxy.getInvocationHandler(mock);
    185             return invocationHandler instanceof InvocationHandlerAdapter
    186                     ? (InvocationHandlerAdapter) invocationHandler
    187                     : null;
    188         }
    189 
    190         if (ProxyBuilder.isProxyClass(mock.getClass())) {
    191             InvocationHandler invocationHandler = ProxyBuilder.getInvocationHandler(mock);
    192             return invocationHandler instanceof InvocationHandlerAdapter
    193                     ? (InvocationHandlerAdapter) invocationHandler
    194                     : null;
    195         }
    196 
    197         return null;
    198     }
    199 }
    200