blob: abcf4f5c081c1497d2d9e9a8aeecb5b32fda8c31 [file] [log] [blame]
Philip P. Moltmann171f0972017-11-20 09:38:54 -08001/*
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
Philip P. Moltmannd4a20562018-03-13 13:11:07 -070019import android.os.AsyncTask;
20import android.os.Build;
21import android.util.ArraySet;
22
Philip P. Moltmann171f0972017-11-20 09:38:54 -080023import com.android.dx.stock.ProxyBuilder;
24import com.android.dx.stock.ProxyBuilder.MethodSetEntry;
25
Philip P. Moltmannd4a20562018-03-13 13:11:07 -070026import org.mockito.Mockito;
Philip P. Moltmann171f0972017-11-20 09:38:54 -080027import org.mockito.exceptions.base.MockitoException;
Philip P. Moltmann171f0972017-11-20 09:38:54 -080028import org.mockito.internal.creation.instance.Instantiator;
Philip P. Moltmann314cb2e2018-05-21 19:15:56 +000029import org.mockito.internal.util.reflection.LenientCopyTool;
Philip P. Moltmann171f0972017-11-20 09:38:54 -080030import org.mockito.invocation.MockHandler;
31import org.mockito.mock.MockCreationSettings;
Philip P. Moltmannd4a20562018-03-13 13:11:07 -070032import org.mockito.plugins.InstantiatorProvider;
Philip P. Moltmann171f0972017-11-20 09:38:54 -080033import org.mockito.plugins.MockMaker;
34
35import java.io.IOException;
36import java.io.InputStream;
Philip P. Moltmannd4a20562018-03-13 13:11:07 -070037import java.lang.ref.Reference;
38import java.lang.ref.ReferenceQueue;
39import java.lang.ref.WeakReference;
Philip P. Moltmann314cb2e2018-05-21 19:15:56 +000040import java.lang.reflect.InvocationTargetException;
Philip P. Moltmann171f0972017-11-20 09:38:54 -080041import java.lang.reflect.Method;
42import java.lang.reflect.Modifier;
43import java.lang.reflect.Proxy;
Philip P. Moltmannd4a20562018-03-13 13:11:07 -070044import java.util.AbstractMap;
45import java.util.Collection;
46import java.util.HashMap;
Philip P. Moltmann171f0972017-11-20 09:38:54 -080047import java.util.HashSet;
Philip P. Moltmannd4a20562018-03-13 13:11:07 -070048import java.util.Map;
Philip P. Moltmann171f0972017-11-20 09:38:54 -080049import 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 }
Philip P. Moltmann314cb2e2018-05-21 19:15:56 +0000111
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 }
Philip P. Moltmann171f0972017-11-20 09:38:54 -0800145 } 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 */
Philip P. Moltmannd4a20562018-03-13 13:11:07 -0700160 private final Map<Object, InvocationHandlerAdapter> mocks;
Philip P. Moltmann171f0972017-11-20 09:38:54 -0800161
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"
Philip P. Moltmannd4a20562018-03-13 13:11:07 -0700175 + "Release: Android " + Build.VERSION.RELEASE + " " + Build.VERSION.INCREMENTAL
176 + "Device: " + Build.BRAND + " " + Build.MODEL, INITIALIZATION_ERROR);
Philip P. Moltmann171f0972017-11-20 09:38:54 -0800177 }
178
Philip P. Moltmannd4a20562018-03-13 13:11:07 -0700179 mocks = new MockMap();
Philip P. Moltmann171f0972017-11-20 09:38:54 -0800180 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
Philip P. Moltmannd4a20562018-03-13 13:11:07 -0700262 Instantiator instantiator = Mockito.framework().getPlugins()
263 .getDefaultPlugin(InstantiatorProvider.class).getInstantiator(settings);
Philip P. Moltmann171f0972017-11-20 09:38:54 -0800264
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 }
Philip P. Moltmannd4a20562018-03-13 13:11:07 -0700350
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 }
Philip P. Moltmann171f0972017-11-20 09:38:54 -0800682}