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