1 /* 2 * Copyright (C) 2018 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.Build; 20 21 import org.mockito.Mockito; 22 import org.mockito.creation.instance.Instantiator; 23 import org.mockito.exceptions.base.MockitoException; 24 import org.mockito.invocation.MockHandler; 25 import org.mockito.mock.MockCreationSettings; 26 import org.mockito.plugins.InstantiatorProvider2; 27 import org.mockito.plugins.MockMaker; 28 29 import java.io.IOException; 30 import java.lang.reflect.InvocationTargetException; 31 import java.lang.reflect.Method; 32 import java.lang.reflect.Modifier; 33 import java.util.HashMap; 34 import java.util.Map; 35 import java.util.Set; 36 import java.util.function.BiConsumer; 37 38 /** 39 * Creates mock markers and adds stubbing hooks to static method 40 * 41 * <p>This is done by transforming the byte code of the classes to add method entry hooks. 42 */ 43 public final class InlineStaticMockMaker implements MockMaker { 44 /** 45 * {@link StaticJvmtiAgent} set up during one time init 46 */ 47 private static final StaticJvmtiAgent AGENT; 48 49 /** 50 * Error during one time init or {@code null} if init was successful 51 */ 52 private static final Throwable INITIALIZATION_ERROR; 53 public static ThreadLocal<Class> mockingInProgressClass = new ThreadLocal<>(); 54 public static ThreadLocal<BiConsumer<Class<?>, Method>> onMethodCallDuringStubbing 55 = new ThreadLocal<>(); 56 public static ThreadLocal<BiConsumer<Class<?>, Method>> onMethodCallDuringVerification 57 = new ThreadLocal<>(); 58 59 /* 60 * One time setup to allow the system to mocking via this mock maker. 61 */ 62 static { 63 StaticJvmtiAgent agent; 64 Throwable initializationError = null; 65 66 try { 67 try { 68 agent = new StaticJvmtiAgent(); 69 } catch (IOException ioe) { 70 throw new IllegalStateException("Mockito could not self-attach a jvmti agent to " + 71 "the current VM. This feature is required for inline mocking.\nThis error" + 72 " occured due to an I/O error during the creation of this agent: " + ioe 73 + "\n\nPotentially, the current VM does not support the jvmti API " + 74 "correctly", ioe); 75 } 76 77 // Blacklisted APIs were introduced in Android P: 78 // 79 // https://android-developers.googleblog.com/2018/02/ 80 // improving-stability-by-reducing-usage.html 81 // 82 // This feature prevents access to blacklisted fields and calling of blacklisted APIs 83 // if the calling class is not trusted. 84 Method allowHiddenApiReflectionFrom; 85 try { 86 Class vmDebug = Class.forName("dalvik.system.VMDebug"); 87 allowHiddenApiReflectionFrom = vmDebug.getDeclaredMethod( 88 "allowHiddenApiReflectionFrom", Class.class); 89 } catch (ClassNotFoundException | NoSuchMethodException e) { 90 throw new IllegalStateException("Cannot find " 91 + "VMDebug#allowHiddenApiReflectionFrom."); 92 } 93 94 // The StaticMockMethodAdvice is used by methods of spies to call the real methods. As 95 // the real methods might be blacklisted, this class needs to be marked as trusted. 96 try { allowHiddenApiReflectionFrom.invoke(null, StaticMockMethodAdvice.class)97 allowHiddenApiReflectionFrom.invoke(null, StaticMockMethodAdvice.class); 98 } catch (InvocationTargetException e) { 99 throw e.getCause(); 100 } 101 } catch (Throwable throwable) { 102 agent = null; 103 initializationError = throwable; 104 } 105 106 AGENT = agent; 107 INITIALIZATION_ERROR = initializationError; 108 } 109 110 /** 111 * All currently active mock markers. We modify the class's byte code. Some objects of the class 112 * are modified, some are not. This list helps the {@link MockMethodAdvice} help figure out if a 113 * object's method calls should be intercepted. 114 */ 115 private final HashMap<Object, InvocationHandlerAdapter> markerToHandler = new HashMap<>(); 116 private final Map<Class, Object> classToMarker = new HashMap<>(); 117 118 /** 119 * Class doing the actual byte code transformation. 120 */ 121 private final StaticClassTransformer classTransformer; 122 123 /** 124 * Create a new mock maker. 125 */ InlineStaticMockMaker()126 public InlineStaticMockMaker() { 127 if (INITIALIZATION_ERROR != null) { 128 throw new RuntimeException("Could not initialize inline mock maker.\n" + "\n" + 129 "Release: Android " + Build.VERSION.RELEASE_OR_CODENAME + " " 130 + Build.VERSION.INCREMENTAL 131 + "Device: " + Build.BRAND + " " + Build.MODEL, INITIALIZATION_ERROR); 132 } 133 134 classTransformer = new StaticClassTransformer(AGENT, InlineDexmakerMockMaker 135 .DISPATCHER_CLASS, markerToHandler, classToMarker); 136 } 137 138 @Override createMock(MockCreationSettings<T> settings, MockHandler handler)139 public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) { 140 Class<T> typeToMock = settings.getTypeToMock(); 141 if (!typeToMock.equals(mockingInProgressClass.get()) || Modifier.isAbstract(typeToMock 142 .getModifiers())) { 143 return null; 144 } 145 146 Set<Class<?>> interfacesSet = settings.getExtraInterfaces(); 147 InvocationHandlerAdapter handlerAdapter = new InvocationHandlerAdapter(handler); 148 149 classTransformer.mockClass(MockFeatures.withMockFeatures(typeToMock, interfacesSet)); 150 151 Instantiator instantiator = Mockito.framework().getPlugins().getDefaultPlugin 152 (InstantiatorProvider2.class).getInstantiator(settings); 153 154 T mock; 155 try { 156 mock = instantiator.newInstance(typeToMock); 157 } catch (org.mockito.creation.instance.InstantiationException e) { 158 throw new MockitoException("Unable to create mock instance of type '" + typeToMock 159 .getSimpleName() + "'", e); 160 } 161 162 if (classToMarker.containsKey(typeToMock)) { 163 throw new MockitoException(typeToMock + " is already mocked"); 164 } 165 classToMarker.put(typeToMock, mock); 166 167 markerToHandler.put(mock, handlerAdapter); 168 return mock; 169 } 170 171 @Override resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings)172 public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) { 173 InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock); 174 if (adapter != null) { 175 if (mockingInProgressClass.get() == mock.getClass()) { 176 markerToHandler.remove(mock); 177 classToMarker.remove(mock.getClass()); 178 } else { 179 adapter.setHandler(newHandler); 180 } 181 } 182 } 183 184 @Override isTypeMockable(final Class<?> type)185 public TypeMockability isTypeMockable(final Class<?> type) { 186 if (mockingInProgressClass.get() == type) { 187 return new TypeMockability() { 188 @Override 189 public boolean mockable() { 190 return !Modifier.isAbstract(type.getModifiers()) && !type.isPrimitive() && type 191 != String.class; 192 } 193 194 @Override 195 public String nonMockableReason() { 196 if (Modifier.isAbstract(type.getModifiers())) { 197 return "abstract type"; 198 } 199 200 if (type.isPrimitive()) { 201 return "primitive type"; 202 } 203 204 if (type == String.class) { 205 return "string"; 206 } 207 208 return "not handled type"; 209 } 210 }; 211 } else { 212 return null; 213 } 214 } 215 216 @Override 217 public MockHandler getHandler(Object mock) { 218 InvocationHandlerAdapter adapter = getInvocationHandlerAdapter(mock); 219 return adapter != null ? adapter.getHandler() : null; 220 } 221 222 /** 223 * Get the {@link InvocationHandlerAdapter} registered for a marker. 224 * 225 * @param marker marker of the class that might have mocking set up 226 * @return adapter for this class, or {@code null} if not mocked 227 */ 228 private InvocationHandlerAdapter getInvocationHandlerAdapter(Object marker) { 229 if (marker == null) { 230 return null; 231 } 232 233 return markerToHandler.get(marker); 234 } 235 } 236