diff --git a/src/main/java/org/mockito/internal/configuration/plugins/PluginFinder.java b/src/main/java/org/mockito/internal/configuration/plugins/PluginFinder.java index 7385afb184..3ca2ac5cd4 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/PluginFinder.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginFinder.java @@ -6,6 +6,8 @@ import java.io.InputStream; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import org.mockito.exceptions.base.MockitoException; import org.mockito.internal.util.io.IOUtil; @@ -43,4 +45,30 @@ String findPluginClass(Iterable resources) { } return null; } + + List findPluginClasses(Iterable resources) { + List pluginClassNames = new ArrayList<>(); + for (URL resource : resources) { + InputStream s = null; + try { + s = resource.openStream(); + String pluginClassName = new PluginFileReader().readPluginClass(s); + if (pluginClassName == null) { + // For backwards compatibility + // If the resource does not have plugin class name we're ignoring it + continue; + } + if (!pluginSwitch.isEnabled(pluginClassName)) { + continue; + } + pluginClassNames.add(pluginClassName); + } catch (Exception e) { + throw new MockitoException( + "Problems reading plugin implementation from: " + resource, e); + } finally { + IOUtil.closeQuietly(s); + } + } + return pluginClassNames; + } } diff --git a/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java b/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java index 8f8f76edcb..296397edc3 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java @@ -6,7 +6,9 @@ import java.io.IOException; import java.net.URL; +import java.util.ArrayList; import java.util.Enumeration; +import java.util.List; import org.mockito.internal.util.collections.Iterables; import org.mockito.plugins.PluginSwitch; @@ -56,4 +58,36 @@ public T loadImpl(Class service) { "Failed to load " + service + " implementation declared in " + resources, e); } } + + public List loadImpls(Class service) { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + if (loader == null) { + loader = ClassLoader.getSystemClassLoader(); + } + Enumeration resources; + try { + resources = loader.getResources("mockito-extensions/" + service.getName()); + } catch (IOException e) { + throw new IllegalStateException("Failed to load " + service, e); + } + + try { + List classesOrAliases = + new PluginFinder(pluginSwitch) + .findPluginClasses(Iterables.toIterable(resources)); + List impls = new ArrayList<>(); + for (String classOrAlias : classesOrAliases) { + if (classOrAlias.equals(alias)) { + classOrAlias = plugins.getDefaultPluginClass(alias); + } + Class pluginClass = loader.loadClass(classOrAlias); + Object plugin = pluginClass.getDeclaredConstructor().newInstance(); + impls.add(service.cast(plugin)); + } + return impls; + } catch (Exception e) { + throw new IllegalStateException( + "Failed to load " + service + " implementation declared in " + resources, e); + } + } } diff --git a/src/main/java/org/mockito/internal/configuration/plugins/PluginLoader.java b/src/main/java/org/mockito/internal/configuration/plugins/PluginLoader.java index 3d724aa49a..e76bb9729f 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/PluginLoader.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginLoader.java @@ -7,6 +7,8 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.List; import org.mockito.plugins.PluginSwitch; @@ -90,4 +92,32 @@ public Object invoke(Object proxy, Method method, Object[] args) }); } } + + /** + * Scans the classpath for given {@code pluginType} and returns a list of its instances. + * + * @return An list of {@code pluginType} or an empty list if none was found. + */ + @SuppressWarnings("unchecked") + List loadPlugins(final Class pluginType) { + try { + return initializer.loadImpls(pluginType); + } catch (final Throwable t) { + return Collections.singletonList( + (T) + Proxy.newProxyInstance( + pluginType.getClassLoader(), + new Class[] {pluginType}, + new InvocationHandler() { + @Override + public Object invoke( + Object proxy, Method method, Object[] args) + throws Throwable { + throw new IllegalStateException( + "Could not initialize plugin: " + pluginType, + t); + } + })); + } + } } diff --git a/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java b/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java index ba7aae1728..9a12d1755a 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java @@ -7,6 +7,8 @@ import org.mockito.internal.creation.instance.InstantiatorProviderAdapter; import org.mockito.plugins.*; +import java.util.List; + class PluginRegistry { private final PluginSwitch pluginSwitch = @@ -31,6 +33,9 @@ class PluginRegistry { private final MockitoLogger mockitoLogger = new PluginLoader(pluginSwitch).loadPlugin(MockitoLogger.class); + private final List mockResolvers = + new PluginLoader(pluginSwitch).loadPlugins(MockResolver.class); + PluginRegistry() { Object impl = new PluginLoader(pluginSwitch) @@ -100,4 +105,13 @@ AnnotationEngine getAnnotationEngine() { MockitoLogger getMockitoLogger() { return mockitoLogger; } + + /** + * Returns a list of available mock resolvers if any. + * + * @return A list of available mock resolvers or an empty list if none are registered. + */ + List getMockResolvers() { + return mockResolvers; + } } diff --git a/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java b/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java index 603a03008a..da225a2886 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java @@ -6,6 +6,8 @@ import org.mockito.plugins.*; +import java.util.List; + /** * Access to Mockito behavior that can be reconfigured by plugins */ @@ -71,6 +73,15 @@ public static MockitoLogger getMockitoLogger() { return registry.getMockitoLogger(); } + /** + * Returns a list of available mock resolvers if any. + * + * @return A list of available mock resolvers or an empty list if none are registered. + */ + public static List getMockResolvers() { + return registry.getMockResolvers(); + } + /** * @return instance of mockito plugins type */ diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java b/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java index 69a7ed21a6..9323368a14 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java @@ -397,13 +397,17 @@ public MethodVisitor wrap( .getDeclaredMethods() .filter(isConstructor().and(not(isPrivate()))); int arguments = Integer.MAX_VALUE; - boolean visible = false; + boolean packagePrivate = true; MethodDescription.InDefinedShape current = null; for (MethodDescription.InDefinedShape constructor : constructors) { + // We are choosing the shortest constructor with regards to arguments. + // Yet, we prefer a non-package-private constructor since they require + // the super class to be on the same class loader. if (constructor.getParameters().size() < arguments - && (!visible || constructor.isPackagePrivate())) { + && (packagePrivate || !constructor.isPackagePrivate())) { + arguments = constructor.getParameters().size(); + packagePrivate = constructor.isPackagePrivate(); current = constructor; - visible = constructor.isPackagePrivate(); } } if (current != null) { diff --git a/src/main/java/org/mockito/internal/util/DefaultMockingDetails.java b/src/main/java/org/mockito/internal/util/DefaultMockingDetails.java index e9887a57b5..1ad5a757f8 100644 --- a/src/main/java/org/mockito/internal/util/DefaultMockingDetails.java +++ b/src/main/java/org/mockito/internal/util/DefaultMockingDetails.java @@ -73,7 +73,7 @@ public Object getMock() { return toInspect; } - private MockHandler mockHandler() { + private MockHandler mockHandler() { assertGoodMock(); return MockUtil.getMockHandler(toInspect); } diff --git a/src/main/java/org/mockito/internal/util/MockUtil.java b/src/main/java/org/mockito/internal/util/MockUtil.java index 46e782cba0..25a29d2c0d 100644 --- a/src/main/java/org/mockito/internal/util/MockUtil.java +++ b/src/main/java/org/mockito/internal/util/MockUtil.java @@ -16,6 +16,7 @@ import org.mockito.mock.MockName; import org.mockito.plugins.MockMaker; import org.mockito.plugins.MockMaker.TypeMockability; +import org.mockito.plugins.MockResolver; import java.util.function.Function; @@ -55,21 +56,25 @@ public static T createMock(MockCreationSettings settings) { return mock; } - public static void resetMock(T mock) { + public static void resetMock(Object mock) { MockHandler oldHandler = getMockHandler(mock); MockCreationSettings settings = oldHandler.getMockSettings(); MockHandler newHandler = createMockHandler(settings); + mock = resolve(mock); mockMaker.resetMock(mock, newHandler, settings); } - public static MockHandler getMockHandler(T mock) { + public static MockHandler getMockHandler(Object mock) { if (mock == null) { throw new NotAMockException("Argument should be a mock, but is null!"); } - if (isMock(mock)) { - return mockMaker.getHandler(mock); + mock = resolve(mock); + + MockHandler handler = mockMaker.getHandler(mock); + if (handler != null) { + return handler; } else { throw new NotAMockException("Argument should be a mock, but is: " + mock.getClass()); } @@ -96,7 +101,23 @@ public static boolean isMock(Object mock) { // Potentially we could also move other methods to MockitoMock, some other candidates: // getInvocationContainer, isSpy, etc. // This also allows us to reuse our public API MockingDetails - return mock != null && mockMaker.getHandler(mock) != null; + if (mock == null) { + return false; + } + + mock = resolve(mock); + + return mockMaker.getHandler(mock) != null; + } + + private static Object resolve(Object mock) { + if (mock instanceof Class) { // static mocks are resolved by definition + return mock; + } + for (MockResolver mockResolver : Plugins.getMockResolvers()) { + mock = mockResolver.resolve(mock); + } + return mock; } public static MockName getMockName(Object mock) { diff --git a/src/main/java/org/mockito/plugins/MockMaker.java b/src/main/java/org/mockito/plugins/MockMaker.java index fbdaf1d0e6..fcebbb3ae7 100644 --- a/src/main/java/org/mockito/plugins/MockMaker.java +++ b/src/main/java/org/mockito/plugins/MockMaker.java @@ -85,7 +85,7 @@ public interface MockMaker { * {@link #getHandler(Object)} will return this instance. * @param instance The object to spy upon. * @param Type of the mock to return, actually the settings.getTypeToMock. - * @return + * @return The spy instance, if this mock maker supports direct spy creation. * @since 3.5.0 */ default Optional createSpy( diff --git a/src/main/java/org/mockito/plugins/MockResolver.java b/src/main/java/org/mockito/plugins/MockResolver.java new file mode 100644 index 0000000000..6eaa756f7e --- /dev/null +++ b/src/main/java/org/mockito/plugins/MockResolver.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.plugins; + +/** + * A mock resolver offers an opportunity to resolve a mock from any instance that is + * provided to the {@link org.mockito.Mockito}-DSL. This mechanism can be used by + * frameworks that provide mocks that are implemented by Mockito but which are wrapped + * by other instances to enhance the proxy further. + */ +public interface MockResolver { + + /** + * Returns the provided instance or the unwrapped mock that the provided + * instance represents. This method must not return {@code null}. + * @param instance The instance passed to the {@link org.mockito.Mockito}-DSL. + * @return The provided instance or the unwrapped mock. + */ + Object resolve(Object instance); +} diff --git a/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MockResolverTest.java b/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MockResolverTest.java new file mode 100644 index 0000000000..bd51fe6dc2 --- /dev/null +++ b/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MockResolverTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockitousage.plugins.resolver; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@MockitoSettings(strictness = Strictness.WARN) +@ExtendWith(MockitoExtension.class) +class MockResolverTest { + + @Test + void mock_resolver_can_unwrap_mocked_instance() { + Foo mock = mock(Foo.class), wrapper = new MockWrapper(mock); + when(wrapper.doIt()).thenReturn(123); + assertThat(mock.doIt()).isEqualTo(123); + assertThat(wrapper.doIt()).isEqualTo(123); + verify(wrapper, times(2)).doIt(); + } + + interface Foo { + int doIt(); + } + + static class MockWrapper implements Foo { + + private final Foo mock; + + MockWrapper(Foo mock) { + this.mock = mock; + } + + Object getMock() { + return mock; + } + + @Override + public int doIt() { + return mock.doIt(); + } + } + +} diff --git a/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MyMockResolver.java b/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MyMockResolver.java new file mode 100644 index 0000000000..e14804f6c5 --- /dev/null +++ b/subprojects/extTest/src/test/java/org/mockitousage/plugins/resolver/MyMockResolver.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockitousage.plugins.resolver; + +import org.mockito.plugins.MockResolver; + +public class MyMockResolver implements MockResolver { + + @Override + public Object resolve(Object instance) { + if (instance instanceof MockResolverTest.MockWrapper) { + return ((MockResolverTest.MockWrapper) instance).getMock(); + } + return instance; + } +} diff --git a/subprojects/extTest/src/test/java/org/mockitousage/plugins/switcher/PluginSwitchTest.java b/subprojects/extTest/src/test/java/org/mockitousage/plugins/switcher/PluginSwitchTest.java index d61f150bae..000c2e8ca9 100644 --- a/subprojects/extTest/src/test/java/org/mockitousage/plugins/switcher/PluginSwitchTest.java +++ b/subprojects/extTest/src/test/java/org/mockitousage/plugins/switcher/PluginSwitchTest.java @@ -7,6 +7,7 @@ import org.junit.Test; import org.mockitousage.plugins.instantiator.MyInstantiatorProvider2; import org.mockitousage.plugins.logger.MyMockitoLogger; +import org.mockitousage.plugins.resolver.MyMockResolver; import org.mockitousage.plugins.stacktrace.MyStackTraceCleanerProvider; import java.util.List; @@ -25,6 +26,7 @@ public void plugin_switcher_is_used() { assertEquals(MyPluginSwitch.invokedFor, asList(MyMockMaker.class.getName(), MyStackTraceCleanerProvider.class.getName(), MyMockitoLogger.class.getName(), + MyMockResolver.class.getName(), MyInstantiatorProvider2.class.getName())); } diff --git a/subprojects/extTest/src/test/resources/mockito-extensions/org.mockito.plugins.MockResolver b/subprojects/extTest/src/test/resources/mockito-extensions/org.mockito.plugins.MockResolver new file mode 100644 index 0000000000..7f2835f93a --- /dev/null +++ b/subprojects/extTest/src/test/resources/mockito-extensions/org.mockito.plugins.MockResolver @@ -0,0 +1 @@ +org.mockitousage.plugins.resolver.MyMockResolver