| /* |
| * Copyright (c) 2016 Mockito contributors |
| * This program is made available under the terms of the MIT License. |
| */ |
| package org.mockito.internal.creation.bytebuddy; |
| |
| import net.bytebuddy.agent.ByteBuddyAgent; |
| import org.mockito.Incubating; |
| import org.mockito.exceptions.base.MockitoException; |
| import org.mockito.exceptions.base.MockitoInitializationException; |
| import org.mockito.internal.configuration.plugins.Plugins; |
| import org.mockito.internal.creation.instance.Instantiator; |
| import org.mockito.internal.util.Platform; |
| import org.mockito.internal.util.concurrent.WeakConcurrentMap; |
| import org.mockito.invocation.MockHandler; |
| import org.mockito.mock.MockCreationSettings; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.instrument.Instrumentation; |
| import java.lang.reflect.Modifier; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.jar.JarOutputStream; |
| |
| import static org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.EXCLUDES; |
| import static org.mockito.internal.util.StringUtil.join; |
| |
| /** |
| * Agent and subclass based mock maker. |
| * <p> |
| * This mock maker which uses a combination of the Java instrumentation API and sub-classing rather than creating |
| * a new sub-class to create a mock. This way, it becomes possible to mock final types and methods. This mock |
| * maker <strong>must to be activated explicitly</strong> for supporting mocking final types and methods: |
| * <p> |
| * <p> |
| * This mock maker can be activated by creating the file <code>/mockito-extensions/org.mockito.plugins.MockMaker</code> |
| * containing the text <code>mock-maker-inline</code> or <code>org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker</code>. |
| * <p> |
| * <p> |
| * This mock maker will make a best effort to avoid subclass creation when creating a mock. Otherwise it will use the |
| * <code>org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker</code> to create the mock class. That means |
| * that the following condition is true |
| * <p> |
| * <pre class="code"><code class="java"> |
| * class Foo { } |
| * assert mock(Foo.class).getClass() == Foo.class; |
| * </pre></code> |
| * <p> |
| * unless any of the following conditions is met, in such case the mock maker <em>fall backs</em> to the |
| * the creation of a subclass. |
| * <p> |
| * <ul> |
| * <li>the type to mock is an abstract class.</li> |
| * <li>the mock is set to require additional interfaces.</li> |
| * <li>the mock is <a href="#20">explicitly set to support serialization</a>.</li> |
| * </ul> |
| * <p> |
| * <p> |
| * Some type of the JDK cannot be mocked, this includes <code>Class</code>, <code>String</code>, and wrapper types. |
| * <p> |
| * <p> |
| * Nevertheless, final methods of such types are mocked when using the inlining mock maker. Mocking final types and enums |
| * does however remain impossible when explicitly requiring serialization support or when adding ancillary interfaces. |
| * <p> |
| * <p> |
| * Important behavioral changes when using inline-mocks: |
| * <ul> |
| * <li>Mockito is capable of mocking package-private methods even if they are defined in different packages than |
| * the mocked type. Mockito voluntarily never mocks package-visible methods within <code>java.*</code> packages.</li> |
| * <li>Additionally to final types, Mockito can now mock types that are not visible for extension; such types |
| * include private types in a protected package.</li> |
| * <li>Mockito can no longer mock <code>native</code> methods. Inline mocks require byte code manipulation of a |
| * method where native methods do not offer any byte code to manipulate.</li> |
| * <li>Mockito cannot longer strip <code>synchronized</code> modifiers from mocked instances.</li> |
| * </ul> |
| * <p> |
| * <p> |
| * Note that inline mocks require a Java agent to be attached. Mockito will attempt an attachment of a Java agent upon |
| * loading the mock maker for creating inline mocks. Such runtime attachment is only possible when using a JVM that |
| * is part of a JDK or when using a Java 9 VM. When running on a non-JDK VM prior to Java 9, it is however possible to |
| * manually add the <a href="http://bytebuddy.net">Byte Buddy Java agent jar</a> using the <code>-javaagent</code> |
| * parameter upon starting the JVM. Furthermore, the inlining mock maker requires the VM to support class retransformation |
| * (also known as HotSwap). All major VM distributions such as HotSpot (OpenJDK), J9 (IBM/Websphere) or Zing (Azul) |
| * support this feature. |
| */ |
| @Incubating |
| public class InlineByteBuddyMockMaker implements ClassCreatingMockMaker { |
| |
| private static final Instrumentation INSTRUMENTATION; |
| |
| private static final Throwable INITIALIZATION_ERROR; |
| |
| static { |
| Instrumentation instrumentation; |
| Throwable initializationError = null; |
| try { |
| try { |
| instrumentation = ByteBuddyAgent.install(); |
| if (!instrumentation.isRetransformClassesSupported()) { |
| throw new IllegalStateException(join( |
| "Byte Buddy requires retransformation for creating inline mocks. This feature is unavailable on the current VM.", |
| "", |
| "You cannot use this mock maker on this VM")); |
| } |
| File boot = File.createTempFile("mockitoboot", ".jar"); |
| boot.deleteOnExit(); |
| JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(boot)); |
| try { |
| String source = "org/mockito/internal/creation/bytebuddy/MockMethodDispatcher"; |
| InputStream inputStream = InlineByteBuddyMockMaker.class.getClassLoader().getResourceAsStream(source + ".raw"); |
| if (inputStream == null) { |
| throw new IllegalStateException(join( |
| "The MockMethodDispatcher class file is not locatable: " + source + ".raw", |
| "", |
| "The class loader responsible for looking up the resource: " + InlineByteBuddyMockMaker.class.getClassLoader() |
| )); |
| } |
| outputStream.putNextEntry(new JarEntry(source + ".class")); |
| try { |
| int length; |
| byte[] buffer = new byte[1024]; |
| while ((length = inputStream.read(buffer)) != -1) { |
| outputStream.write(buffer, 0, length); |
| } |
| } finally { |
| inputStream.close(); |
| } |
| outputStream.closeEntry(); |
| } finally { |
| outputStream.close(); |
| } |
| instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(boot)); |
| try { |
| Class<?> dispatcher = Class.forName("org.mockito.internal.creation.bytebuddy.MockMethodDispatcher"); |
| if (dispatcher.getClassLoader() != null) { |
| throw new IllegalStateException(join( |
| "The MockMethodDispatcher must not be loaded manually but must be injected into the bootstrap class loader.", |
| "", |
| "The dispatcher class was already loaded by: " + dispatcher.getClassLoader())); |
| } |
| } catch (ClassNotFoundException cnfe) { |
| throw new IllegalStateException(join( |
| "Mockito failed to inject the MockMethodDispatcher class into the bootstrap class loader", |
| "", |
| "It seems like your current VM does not support the instrumentation API correctly."), cnfe); |
| } |
| } catch (IOException ioe) { |
| throw new IllegalStateException(join( |
| "Mockito could not self-attach a Java agent to the current VM. This feature is required for inline mocking.", |
| "This error occured due to an I/O error during the creation of this agent: " + ioe, |
| "", |
| "Potentially, the current VM does not support the instrumentation API correctly"), ioe); |
| } |
| } catch (Throwable throwable) { |
| instrumentation = null; |
| initializationError = throwable; |
| } |
| INSTRUMENTATION = instrumentation; |
| INITIALIZATION_ERROR = initializationError; |
| } |
| |
| private final BytecodeGenerator bytecodeGenerator; |
| |
| private final WeakConcurrentMap<Object, MockMethodInterceptor> mocks = new WeakConcurrentMap.WithInlinedExpunction<Object, MockMethodInterceptor>(); |
| |
| public InlineByteBuddyMockMaker() { |
| if (INITIALIZATION_ERROR != null) { |
| throw new MockitoInitializationException(join( |
| "Could not initialize inline Byte Buddy mock maker. (This mock maker is not supported on Android.)", |
| "", |
| Platform.describe()), INITIALIZATION_ERROR); |
| } |
| bytecodeGenerator = new TypeCachingBytecodeGenerator(new InlineBytecodeGenerator(INSTRUMENTATION, mocks), true); |
| } |
| |
| @Override |
| public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) { |
| Class<? extends T> type = createMockType(settings); |
| |
| Instantiator instantiator = Plugins.getInstantiatorProvider().getInstantiator(settings); |
| try { |
| T instance = instantiator.newInstance(type); |
| MockMethodInterceptor mockMethodInterceptor = new MockMethodInterceptor(handler, settings); |
| mocks.put(instance, mockMethodInterceptor); |
| if (instance instanceof MockAccess) { |
| ((MockAccess) instance).setMockitoInterceptor(mockMethodInterceptor); |
| } |
| return instance; |
| } catch (org.mockito.internal.creation.instance.InstantiationException e) { |
| throw new MockitoException("Unable to create mock instance of type '" + type.getSimpleName() + "'", e); |
| } |
| } |
| |
| @Override |
| public <T> Class<? extends T> createMockType(MockCreationSettings<T> settings) { |
| try { |
| return bytecodeGenerator.mockClass(MockFeatures.withMockFeatures( |
| settings.getTypeToMock(), |
| settings.getExtraInterfaces(), |
| settings.getSerializableMode(), |
| settings.isStripAnnotations() |
| )); |
| } catch (Exception bytecodeGenerationFailed) { |
| throw prettifyFailure(settings, bytecodeGenerationFailed); |
| } |
| } |
| |
| private <T> RuntimeException prettifyFailure(MockCreationSettings<T> mockFeatures, Exception generationFailed) { |
| if (mockFeatures.getTypeToMock().isArray()) { |
| throw new MockitoException(join( |
| "Arrays cannot be mocked: " + mockFeatures.getTypeToMock() + ".", |
| "" |
| ), generationFailed); |
| } |
| if (Modifier.isFinal(mockFeatures.getTypeToMock().getModifiers())) { |
| throw new MockitoException(join( |
| "Mockito cannot mock this class: " + mockFeatures.getTypeToMock() + ".", |
| "Can not mock final classes with the following settings :", |
| " - explicit serialization (e.g. withSettings().serializable())", |
| " - extra interfaces (e.g. withSettings().extraInterfaces(...))", |
| "", |
| "You are seeing this disclaimer because Mockito is configured to create inlined mocks.", |
| "You can learn about inline mocks and their limitations under item #39 of the Mockito class javadoc.", |
| "", |
| "Underlying exception : " + generationFailed |
| ), generationFailed); |
| } |
| if (Modifier.isPrivate(mockFeatures.getTypeToMock().getModifiers())) { |
| throw new MockitoException(join( |
| "Mockito cannot mock this class: " + mockFeatures.getTypeToMock() + ".", |
| "Most likely it is a private class that is not visible by Mockito", |
| "", |
| "You are seeing this disclaimer because Mockito is configured to create inlined mocks.", |
| "You can learn about inline mocks and their limitations under item #39 of the Mockito class javadoc.", |
| "" |
| ), generationFailed); |
| } |
| throw new MockitoException(join( |
| "Mockito cannot mock this class: " + mockFeatures.getTypeToMock() + ".", |
| "", |
| "If you're not sure why you're getting this error, please report to the mailing list.", |
| "", |
| Platform.warnForVM( |
| "IBM J9 VM", "Early IBM virtual machine are known to have issues with Mockito, please upgrade to an up-to-date version.\n", |
| "Hotspot", Platform.isJava8BelowUpdate45() ? "Java 8 early builds have bugs that were addressed in Java 1.8.0_45, please update your JDK!\n" : "" |
| ), |
| Platform.describe(), |
| "", |
| "You are seeing this disclaimer because Mockito is configured to create inlined mocks.", |
| "You can learn about inline mocks and their limitations under item #39 of the Mockito class javadoc.", |
| "", |
| "Underlying exception : " + generationFailed |
| ), generationFailed); |
| } |
| |
| @Override |
| public MockHandler getHandler(Object mock) { |
| MockMethodInterceptor interceptor = mocks.get(mock); |
| if (interceptor == null) { |
| return null; |
| } else { |
| return interceptor.handler; |
| } |
| } |
| |
| @Override |
| public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) { |
| MockMethodInterceptor mockMethodInterceptor = new MockMethodInterceptor(newHandler, settings); |
| mocks.put(mock, mockMethodInterceptor); |
| if (mock instanceof MockAccess) { |
| ((MockAccess) mock).setMockitoInterceptor(mockMethodInterceptor); |
| } |
| } |
| |
| @Override |
| public TypeMockability isTypeMockable(final Class<?> type) { |
| return new TypeMockability() { |
| @Override |
| public boolean mockable() { |
| return INSTRUMENTATION.isModifiableClass(type) && !EXCLUDES.contains(type); |
| } |
| |
| @Override |
| public String nonMockableReason() { |
| if (mockable()) { |
| return ""; |
| } |
| if (type.isPrimitive()) { |
| return "primitive type"; |
| } |
| if (EXCLUDES.contains(type)) { |
| return "Cannot mock wrapper types, String.class or Class.class"; |
| } |
| return "VM does not not support modification of given type"; |
| } |
| }; |
| } |
| |
| } |