diff --git a/src/main/java/org/mockito/MockedConstruction.java b/src/main/java/org/mockito/MockedConstruction.java new file mode 100644 index 0000000000..c00fb021a2 --- /dev/null +++ b/src/main/java/org/mockito/MockedConstruction.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2007 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito; + +import java.lang.reflect.Constructor; +import java.util.List; + +/** + * Represents a mock of any object construction of the represented type. Within the scope of the + * mocked construction, the invocation of any interceptor will generate a mock which will be + * prepared as specified when generating this scope. The mock can also be received via this + * instance. + *

+ * If the {@link Mock} annotation is used on fields or method parameters of this type, a mocked + * construction is created instead of a regular mock. The mocked construction is activated and + * released upon completing any relevant test. + * + * @param The type for which the construction is being mocked. + */ +@Incubating +public interface MockedConstruction extends ScopedMock { + + List constructed(); + + interface Context { + + int getCount(); + + Constructor constructor(); + + List arguments(); + } + + interface MockInitializer { + + void prepare(T mock, Context context) throws Throwable; + } +} diff --git a/src/main/java/org/mockito/MockedStatic.java b/src/main/java/org/mockito/MockedStatic.java index fc23c90776..ac291ba33c 100644 --- a/src/main/java/org/mockito/MockedStatic.java +++ b/src/main/java/org/mockito/MockedStatic.java @@ -24,7 +24,7 @@ * @param The type being mocked. */ @Incubating -public interface MockedStatic extends AutoCloseable { +public interface MockedStatic extends ScopedMock { /** * See {@link Mockito#when(Object)}. @@ -63,24 +63,6 @@ default void verify(Verification verification) { */ void verifyNoInteractions(); - /** - * Checks if this mock is closed. - * - * @return {@code true} if this mock is closed. - */ - boolean isClosed(); - - /** - * Releases this static mock and throws a {@link org.mockito.exceptions.base.MockitoException} if closed already. - */ - @Override - void close(); - - /** - * Releases this static mock and is non-operational if already released. - */ - void closeOnDemand(); - interface Verification { void apply() throws Throwable; diff --git a/src/main/java/org/mockito/Mockito.java b/src/main/java/org/mockito/Mockito.java index 6f6560d2c2..64df8b8db7 100644 --- a/src/main/java/org/mockito/Mockito.java +++ b/src/main/java/org/mockito/Mockito.java @@ -28,18 +28,10 @@ import org.mockito.quality.Strictness; import org.mockito.session.MockitoSessionBuilder; import org.mockito.session.MockitoSessionLogger; -import org.mockito.stubbing.Answer; -import org.mockito.stubbing.Answer1; -import org.mockito.stubbing.LenientStubber; -import org.mockito.stubbing.OngoingStubbing; -import org.mockito.stubbing.Stubber; -import org.mockito.stubbing.Stubbing; -import org.mockito.stubbing.VoidAnswer1; -import org.mockito.verification.After; -import org.mockito.verification.Timeout; -import org.mockito.verification.VerificationAfterDelay; -import org.mockito.verification.VerificationMode; -import org.mockito.verification.VerificationWithTimeout; +import org.mockito.stubbing.*; +import org.mockito.verification.*; + +import java.util.function.Function; /** *

Mockito logo

@@ -106,6 +98,7 @@ * 46. New Mockito.lenient() and MockSettings.lenient() methods (Since 2.20.0)
* 47. New API for clearing mock state in inline mocking (Since 2.25.0)
* 48. New API for mocking static methods (Since 3.4.0)
+ * 49. New API for mocking object construction (Since 3.5.0)
* * *

0. Migrating to Mockito 2

@@ -1565,9 +1558,32 @@ * assertEquals("foo", Foo.method()); * * - * Due to the defined scope of the static mock, it returns to its original behvior once the scope is released. To define mock + * Due to the defined scope of the static mock, it returns to its original behavior once the scope is released. To define mock * behavior and to verify static method invocations, use the MockedStatic that is returned. *

+ * + *

49. Mocking object construction (since 3.5.0)

+ * + * When using the inline mock maker, it is possible to generate mocks on constructor invocations within the current + * thread and a user-defined scope. This way, Mockito assures that concurrently and sequentially running tests do not interfere. + * + * To make sure a constructor mocks remain temporary, it is recommended to define the scope within a try-with-resources construct. + * In the following example, the Foo type's construction would generate a mock: + * + *

+ * assertEquals("foo", Foo.method());
+ * try (MockedConstruction mocked = mockConstruction(Foo.class)) {
+ * Foo foo = new Foo();
+ * when(foo.method()).thenReturn("bar");
+ * assertEquals("bar", foo.method());
+ * verify(foo).method();
+ * }
+ * assertEquals("foo", foo.method());
+ * 
+ * + * Due to the defined scope of the mocked construction, object construction returns to its original behavior once the scope is + * released. To define mock behavior and to verify static method invocations, use the MockedConstruction that is returned. + *

*/ @SuppressWarnings("unchecked") public class Mockito extends ArgumentMatchers { @@ -2144,6 +2160,152 @@ public static MockedStatic mockStatic(Class classToMock, MockSettings return MOCKITO_CORE.mockStatic(classToMock, mockSettings); } + /** + * Creates a thread-local mock controller for all constructions of the given class. + * The returned object's {@link MockedConstruction#close()} method must be called upon completing the + * test or the mock will remain active on the current thread. + *

+ * See examples in javadoc for {@link Mockito} class + * + * @param classToMock non-abstract class of which constructions should be mocked. + * @param defaultAnswer the default answer for the first created mock. + * @param additionalAnswers the default answer for all additional mocks. For any access mocks, the + * last answer is used. If this array is empty, the {@code defaultAnswer} is used. + * @return mock controller + */ + @Incubating + @CheckReturnValue + public static MockedConstruction mockConstructionWithAnswer( + Class classToMock, Answer defaultAnswer, Answer... additionalAnswers) { + return mockConstruction( + classToMock, + context -> { + if (context.getCount() == 1 || additionalAnswers.length == 0) { + return withSettings().defaultAnswer(defaultAnswer); + } else if (context.getCount() >= additionalAnswers.length) { + return withSettings() + .defaultAnswer(additionalAnswers[additionalAnswers.length - 1]); + } else { + return withSettings() + .defaultAnswer(additionalAnswers[context.getCount() - 2]); + } + }, + (mock, context) -> {}); + } + + /** + * Creates a thread-local mock controller for all constructions of the given class. + * The returned object's {@link MockedConstruction#close()} method must be called upon completing the + * test or the mock will remain active on the current thread. + *

+ * See examples in javadoc for {@link Mockito} class + * + * @param classToMock non-abstract class of which constructions should be mocked. + * @return mock controller + */ + @Incubating + @CheckReturnValue + public static MockedConstruction mockConstruction(Class classToMock) { + return mockConstruction(classToMock, index -> withSettings(), (mock, context) -> {}); + } + + /** + * Creates a thread-local mock controller for all constructions of the given class. + * The returned object's {@link MockedConstruction#close()} method must be called upon completing the + * test or the mock will remain active on the current thread. + *

+ * See examples in javadoc for {@link Mockito} class + * + * @param classToMock non-abstract class of which constructions should be mocked. + * @param mockInitializer a callback to prepare a mock's methods after its instantiation. + * @return mock controller + */ + @Incubating + @CheckReturnValue + public static MockedConstruction mockConstruction( + Class classToMock, MockedConstruction.MockInitializer mockInitializer) { + return mockConstruction(classToMock, withSettings(), mockInitializer); + } + + /** + * Creates a thread-local mock controller for all constructions of the given class. + * The returned object's {@link MockedConstruction#close()} method must be called upon completing the + * test or the mock will remain active on the current thread. + *

+ * See examples in javadoc for {@link Mockito} class + * + * @param classToMock non-abstract class of which constructions should be mocked. + * @param mockSettings the mock settings to use. + * @return mock controller + */ + @Incubating + @CheckReturnValue + public static MockedConstruction mockConstruction( + Class classToMock, MockSettings mockSettings) { + return mockConstruction(classToMock, context -> mockSettings); + } + + /** + * Creates a thread-local mock controller for all constructions of the given class. + * The returned object's {@link MockedConstruction#close()} method must be called upon completing the + * test or the mock will remain active on the current thread. + *

+ * See examples in javadoc for {@link Mockito} class + * + * @param classToMock non-abstract class of which constructions should be mocked. + * @param mockSettingsFactory the mock settings to use. + * @return mock controller + */ + @Incubating + @CheckReturnValue + public static MockedConstruction mockConstruction( + Class classToMock, + Function mockSettingsFactory) { + return mockConstruction(classToMock, mockSettingsFactory, (mock, context) -> {}); + } + + /** + * Creates a thread-local mock controller for all constructions of the given class. + * The returned object's {@link MockedConstruction#close()} method must be called upon completing the + * test or the mock will remain active on the current thread. + *

+ * See examples in javadoc for {@link Mockito} class + * + * @param classToMock non-abstract class of which constructions should be mocked. + * @param mockSettings the settings to use. + * @param mockInitializer a callback to prepare a mock's methods after its instantiation. + * @return mock controller + */ + @Incubating + @CheckReturnValue + public static MockedConstruction mockConstruction( + Class classToMock, + MockSettings mockSettings, + MockedConstruction.MockInitializer mockInitializer) { + return mockConstruction(classToMock, index -> mockSettings, mockInitializer); + } + + /** + * Creates a thread-local mock controller for all constructions of the given class. + * The returned object's {@link MockedConstruction#close()} method must be called upon completing the + * test or the mock will remain active on the current thread. + *

+ * See examples in javadoc for {@link Mockito} class + * + * @param classToMock non-abstract class of which constructions should be mocked. + * @param mockSettingsFactory a function to create settings to use. + * @param mockInitializer a callback to prepare a mock's methods after its instantiation. + * @return mock controller + */ + @Incubating + @CheckReturnValue + public static MockedConstruction mockConstruction( + Class classToMock, + Function mockSettingsFactory, + MockedConstruction.MockInitializer mockInitializer) { + return MOCKITO_CORE.mockConstruction(classToMock, mockSettingsFactory, mockInitializer); + } + /** * Enables stubbing methods. Use it when you want the mock to return particular value when particular method is called. *

diff --git a/src/main/java/org/mockito/ScopedMock.java b/src/main/java/org/mockito/ScopedMock.java new file mode 100644 index 0000000000..3ef0d9b3d3 --- /dev/null +++ b/src/main/java/org/mockito/ScopedMock.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2007 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito; + +/** + * Represents a mock with a thread-local explicit scope. Scoped mocks must be closed by the entity + * that activates the scoped mock. + */ +@Incubating +public interface ScopedMock extends AutoCloseable { + + /** + * Checks if this mock is closed. + * + * @return {@code true} if this mock is closed. + */ + boolean isClosed(); + + /** + * Closes this scoped mock and throws an exception if already closed. + */ + @Override + void close(); + + /** + * Releases this scoped mock and is non-operational if already released. + */ + void closeOnDemand(); +} diff --git a/src/main/java/org/mockito/creation/instance/InstantiationException.java b/src/main/java/org/mockito/creation/instance/InstantiationException.java index 1cfbaba23f..34548d5eb8 100644 --- a/src/main/java/org/mockito/creation/instance/InstantiationException.java +++ b/src/main/java/org/mockito/creation/instance/InstantiationException.java @@ -13,6 +13,13 @@ */ public class InstantiationException extends MockitoException { + /** + * @since 3.5.0 + */ + public InstantiationException(String message) { + super(message); + } + /** * @since 2.15.4 */ diff --git a/src/main/java/org/mockito/internal/MockedConstructionImpl.java b/src/main/java/org/mockito/internal/MockedConstructionImpl.java new file mode 100644 index 0000000000..ee09f3ccde --- /dev/null +++ b/src/main/java/org/mockito/internal/MockedConstructionImpl.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2007 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal; + +import java.util.Collections; +import java.util.List; + +import org.mockito.MockedConstruction; +import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.debugging.LocationImpl; +import org.mockito.invocation.Location; +import org.mockito.plugins.MockMaker; + +import static org.mockito.internal.util.StringUtil.*; + +public final class MockedConstructionImpl implements MockedConstruction { + + private final MockMaker.ConstructionMockControl control; + + private boolean closed; + + private final Location location = new LocationImpl(); + + protected MockedConstructionImpl(MockMaker.ConstructionMockControl control) { + this.control = control; + } + + @Override + public List constructed() { + return Collections.unmodifiableList(control.getMocks()); + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void close() { + assertNotClosed(); + + closed = true; + control.disable(); + } + + @Override + public void closeOnDemand() { + if (!closed) { + close(); + } + } + + private void assertNotClosed() { + if (closed) { + throw new MockitoException( + join( + "The static mock created at", + location.toString(), + "is already resolved and cannot longer be used")); + } + } +} diff --git a/src/main/java/org/mockito/internal/MockedStaticImpl.java b/src/main/java/org/mockito/internal/MockedStaticImpl.java index 25434769e2..cb7794cc03 100644 --- a/src/main/java/org/mockito/internal/MockedStaticImpl.java +++ b/src/main/java/org/mockito/internal/MockedStaticImpl.java @@ -4,8 +4,6 @@ */ package org.mockito.internal; -import org.mockito.Incubating; -import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.MockingDetails; import org.mockito.Mockito; @@ -29,21 +27,6 @@ import static org.mockito.internal.util.StringUtil.*; import static org.mockito.internal.verification.VerificationModeFactory.*; -/** - * Represents an active mock of a type's static methods. The mocking only affects the thread - * on which this static mock was created and it is not safe to use this object from another - * thread. The static mock is released when this object's {@link MockedStaticImpl#close()} method - * is invoked. If this object is never closed, the static mock will remain active on the - * initiating thread. It is therefore recommended to create this object within a try-with-resources - * statement unless when managed explicitly, for example by using a JUnit rule or extension. - *

- * If the {@link Mock} annotation is used on fields or method parameters of this type, a static mock - * is created instead of a regular mock. The static mock is activated and released upon completing any - * relevant test. - * - * @param The type being mocked. - */ -@Incubating public final class MockedStaticImpl implements MockedStatic { private final MockMaker.StaticMockControl control; diff --git a/src/main/java/org/mockito/internal/MockitoCore.java b/src/main/java/org/mockito/internal/MockitoCore.java index bccb045fe1..6901baef85 100644 --- a/src/main/java/org/mockito/internal/MockitoCore.java +++ b/src/main/java/org/mockito/internal/MockitoCore.java @@ -4,25 +4,7 @@ */ package org.mockito.internal; -import static org.mockito.internal.exceptions.Reporter.*; -import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress; -import static org.mockito.internal.util.MockUtil.createMock; -import static org.mockito.internal.util.MockUtil.createStaticMock; -import static org.mockito.internal.util.MockUtil.getInvocationContainer; -import static org.mockito.internal.util.MockUtil.getMockHandler; -import static org.mockito.internal.util.MockUtil.isMock; -import static org.mockito.internal.util.MockUtil.resetMock; -import static org.mockito.internal.util.MockUtil.typeMockabilityOf; -import static org.mockito.internal.verification.VerificationModeFactory.noInteractions; -import static org.mockito.internal.verification.VerificationModeFactory.noMoreInteractions; - -import java.util.Arrays; -import java.util.List; - -import org.mockito.InOrder; -import org.mockito.MockSettings; -import org.mockito.MockedStatic; -import org.mockito.MockingDetails; +import org.mockito.*; import org.mockito.exceptions.misusing.NotAMockException; import org.mockito.internal.creation.MockSettingsImpl; import org.mockito.internal.invocation.finder.VerifiableInvocationsFinder; @@ -49,6 +31,16 @@ import org.mockito.stubbing.Stubber; import org.mockito.verification.VerificationMode; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import static org.mockito.internal.exceptions.Reporter.*; +import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress; +import static org.mockito.internal.util.MockUtil.*; +import static org.mockito.internal.verification.VerificationModeFactory.noInteractions; +import static org.mockito.internal.verification.VerificationModeFactory.noMoreInteractions; + @SuppressWarnings("unchecked") public class MockitoCore { @@ -87,6 +79,29 @@ public MockedStatic mockStatic(Class classToMock, MockSettings setting return new MockedStaticImpl<>(control); } + public MockedConstruction mockConstruction( + Class typeToMock, + Function settingsFactory, + MockedConstruction.MockInitializer mockInitializer) { + Function> creationSettings = + context -> { + MockSettings value = settingsFactory.apply(context); + if (!MockSettingsImpl.class.isInstance(value)) { + throw new IllegalArgumentException( + "Unexpected implementation of '" + + value.getClass().getCanonicalName() + + "'\n" + + "At the moment, you cannot provide your own implementations of that class."); + } + MockSettingsImpl impl = MockSettingsImpl.class.cast(value); + return impl.build(typeToMock); + }; + MockMaker.ConstructionMockControl control = + createConstructionMock(typeToMock, creationSettings, mockInitializer); + control.enable(); + return new MockedConstructionImpl<>(control); + } + public OngoingStubbing when(T methodCall) { MockingProgress mockingProgress = mockingProgress(); mockingProgress.stubbingStarted(); diff --git a/src/main/java/org/mockito/internal/configuration/ClassPathLoader.java b/src/main/java/org/mockito/internal/configuration/ClassPathLoader.java index 8fc47b425f..0f031ee58a 100644 --- a/src/main/java/org/mockito/internal/configuration/ClassPathLoader.java +++ b/src/main/java/org/mockito/internal/configuration/ClassPathLoader.java @@ -64,7 +64,7 @@ public IMockitoConfiguration loadConfiguration() { } try { - return (IMockitoConfiguration) configClass.newInstance(); + return (IMockitoConfiguration) configClass.getDeclaredConstructor().newInstance(); } catch (ClassCastException e) { throw new MockitoConfigurationException( "MockitoConfiguration class must implement " diff --git a/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java b/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java index 24cb7a1cb2..43a35eee6e 100644 --- a/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java +++ b/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java @@ -5,7 +5,6 @@ package org.mockito.internal.configuration; import static org.mockito.internal.exceptions.Reporter.moreThanOneAnnotationNotAllowed; -import static org.mockito.internal.util.reflection.FieldSetter.setField; import java.lang.annotation.Annotation; import java.lang.reflect.Field; @@ -16,10 +15,12 @@ import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; +import org.mockito.ScopedMock; import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.plugins.AnnotationEngine; +import org.mockito.plugins.MemberAccessor; /** * Initializes fields annotated with @{@link org.mockito.Mock} or @{@link org.mockito.Captor}. @@ -64,23 +65,24 @@ private void registerAnnotationProcessor( @Override public AutoCloseable process(Class clazz, Object testInstance) { - List> mockedStatics = new ArrayList<>(); + List scopedMocks = new ArrayList<>(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { boolean alreadyAssigned = false; for (Annotation annotation : field.getAnnotations()) { Object mock = createMockFor(annotation, field); - if (mock instanceof MockedStatic) { - mockedStatics.add((MockedStatic) mock); + if (mock instanceof ScopedMock) { + scopedMocks.add((ScopedMock) mock); } if (mock != null) { throwIfAlreadyAssigned(field, alreadyAssigned); alreadyAssigned = true; + final MemberAccessor accessor = Plugins.getMemberAccessor(); try { - setField(testInstance, field, mock); + accessor.set(field, testInstance, mock); } catch (Exception e) { - for (MockedStatic mockedStatic : mockedStatics) { - mockedStatic.close(); + for (ScopedMock scopedMock : scopedMocks) { + scopedMock.close(); } throw new MockitoException( "Problems setting field " @@ -93,8 +95,8 @@ public AutoCloseable process(Class clazz, Object testInstance) { } } return () -> { - for (MockedStatic mockedStatic : mockedStatics) { - mockedStatic.closeOnDemand(); + for (ScopedMock scopedMock : scopedMocks) { + scopedMock.closeOnDemand(); } }; } diff --git a/src/main/java/org/mockito/internal/configuration/InjectingAnnotationEngine.java b/src/main/java/org/mockito/internal/configuration/InjectingAnnotationEngine.java index 59239a4674..176d3e5006 100644 --- a/src/main/java/org/mockito/internal/configuration/InjectingAnnotationEngine.java +++ b/src/main/java/org/mockito/internal/configuration/InjectingAnnotationEngine.java @@ -12,8 +12,8 @@ import java.util.List; import java.util.Set; -import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; +import org.mockito.ScopedMock; import org.mockito.internal.configuration.injection.scanner.InjectMocksScanner; import org.mockito.internal.configuration.injection.scanner.MockScanner; import org.mockito.plugins.AnnotationEngine; @@ -119,8 +119,8 @@ private AutoCloseable injectCloseableMocks(final Object testClassInstance) { return () -> { for (Object mock : mocks) { - if (mock instanceof MockedStatic) { - ((MockedStatic) mock).closeOnDemand(); + if (mock instanceof ScopedMock) { + ((ScopedMock) mock).closeOnDemand(); } } }; diff --git a/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java b/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java index 48f0515a4b..e48291e5d5 100644 --- a/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java +++ b/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java @@ -12,6 +12,7 @@ import org.mockito.Mock; import org.mockito.MockSettings; +import org.mockito.MockedConstruction; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.exceptions.base.MockitoException; @@ -52,13 +53,21 @@ public static Object processAnnotationForMock( mockSettings.defaultAnswer(annotation.answer()); if (type == MockedStatic.class) { - return Mockito.mockStatic(inferStaticMock(genericType.get(), name), mockSettings); + return Mockito.mockStatic( + inferParameterizedType( + genericType.get(), name, MockedStatic.class.getSimpleName()), + mockSettings); + } else if (type == MockedConstruction.class) { + return Mockito.mockConstruction( + inferParameterizedType( + genericType.get(), name, MockedConstruction.class.getSimpleName()), + mockSettings); } else { return Mockito.mock(type, mockSettings); } } - static Class inferStaticMock(Type type, String name) { + static Class inferParameterizedType(Type type, String name, String sort) { if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; Type[] arguments = parameterizedType.getActualTypeArguments(); @@ -72,11 +81,11 @@ static Class inferStaticMock(Type type, String name) { join( "Mockito cannot infer a static mock from a raw type for " + name, "", - "Instead of @Mock MockedStatic you need to specify a parameterized type", - "For example, if you would like to mock static methods of Sample.class, specify", + "Instead of @Mock " + sort + " you need to specify a parameterized type", + "For example, if you would like to mock Sample.class, specify", "", - "@Mock MockedStatic", + "@Mock " + sort + "", "", - "as the type parameter. If the type is parameterized, it should be specified as raw type.")); + "as the type parameter. If the type is itself parameterized, it should be specified as raw type.")); } } diff --git a/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java b/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java index b4686b30d8..b7a4a2cef9 100644 --- a/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java +++ b/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java @@ -22,8 +22,10 @@ import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.MockUtil; import org.mockito.plugins.AnnotationEngine; +import org.mockito.plugins.MemberAccessor; /** * Process fields annotated with @Spy. @@ -50,23 +52,23 @@ public class SpyAnnotationEngine @Override public AutoCloseable process(Class context, Object testInstance) { Field[] fields = context.getDeclaredFields(); + MemberAccessor accessor = Plugins.getMemberAccessor(); for (Field field : fields) { if (field.isAnnotationPresent(Spy.class) && !field.isAnnotationPresent(InjectMocks.class)) { assertNoIncompatibleAnnotations(Spy.class, field, Mock.class, Captor.class); - field.setAccessible(true); Object instance; try { - instance = field.get(testInstance); + instance = accessor.get(field, testInstance); if (MockUtil.isMock(instance)) { // instance has been spied earlier // for example happens when MockitoAnnotations.openMocks is called two // times. Mockito.reset(instance); } else if (instance != null) { - field.set(testInstance, spyInstance(field, instance)); + accessor.set(field, testInstance, spyInstance(field, instance)); } else { - field.set(testInstance, spyNewInstance(testInstance, field)); + accessor.set(field, testInstance, spyNewInstance(testInstance, field)); } } catch (Exception e) { throw new MockitoException( @@ -123,8 +125,8 @@ private static Object spyNewInstance(Object testInstance, Field field) Constructor constructor = noArgConstructorOf(type); if (Modifier.isPrivate(constructor.getModifiers())) { - constructor.setAccessible(true); - return Mockito.mock(type, settings.spiedInstance(constructor.newInstance())); + MemberAccessor accessor = Plugins.getMemberAccessor(); + return Mockito.mock(type, settings.spiedInstance(accessor.newInstance(constructor))); } else { return Mockito.mock(type, settings.useConstructor()); } diff --git a/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java b/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java index cd8fcc5e3a..6230e1b86f 100644 --- a/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java +++ b/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java @@ -5,7 +5,6 @@ package org.mockito.internal.configuration.injection; import static org.mockito.Mockito.withSettings; -import static org.mockito.internal.util.reflection.FieldSetter.setField; import java.lang.reflect.Field; import java.util.Set; @@ -13,8 +12,10 @@ import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.MockUtil; import org.mockito.internal.util.reflection.FieldReader; +import org.mockito.plugins.MemberAccessor; /** * Handler for field annotated with @InjectMocks and @Spy. @@ -26,6 +27,8 @@ */ public class SpyOnInjectedFieldsHandler extends MockInjectionStrategy { + private final MemberAccessor accessor = Plugins.getMemberAccessor(); + @Override protected boolean processInjection(Field field, Object fieldOwner, Set mockCandidates) { FieldReader fieldReader = new FieldReader(fieldOwner, field); @@ -46,7 +49,7 @@ protected boolean processInjection(Field field, Object fieldOwner, Set m .spiedInstance(instance) .defaultAnswer(Mockito.CALLS_REAL_METHODS) .name(field.getName())); - setField(fieldOwner, field, mock); + accessor.set(field, fieldOwner, mock); } } catch (Exception e) { throw new MockitoException("Problems initiating spied field " + field.getName(), e); diff --git a/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java b/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java index a682d92320..6c3c329b67 100644 --- a/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java +++ b/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java @@ -5,13 +5,14 @@ package org.mockito.internal.configuration.injection.filter; import static org.mockito.internal.exceptions.Reporter.cannotInjectDependency; -import static org.mockito.internal.util.reflection.FieldSetter.setField; import java.lang.reflect.Field; import java.util.Collection; import java.util.List; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.reflection.BeanPropertySetter; +import org.mockito.plugins.MemberAccessor; /** * This node returns an actual injecter which will be either : @@ -30,18 +31,17 @@ public OngoingInjector filterCandidate( if (mocks.size() == 1) { final Object matchingMock = mocks.iterator().next(); - return new OngoingInjector() { - public Object thenInject() { - try { - if (!new BeanPropertySetter(injectee, candidateFieldToBeInjected) - .set(matchingMock)) { - setField(injectee, candidateFieldToBeInjected, matchingMock); - } - } catch (RuntimeException e) { - throw cannotInjectDependency(candidateFieldToBeInjected, matchingMock, e); + MemberAccessor accessor = Plugins.getMemberAccessor(); + return () -> { + try { + if (!new BeanPropertySetter(injectee, candidateFieldToBeInjected) + .set(matchingMock)) { + accessor.set(candidateFieldToBeInjected, injectee, matchingMock); } - return matchingMock; + } catch (RuntimeException | IllegalAccessException e) { + throw cannotInjectDependency(candidateFieldToBeInjected, matchingMock, e); } + return matchingMock; }; } diff --git a/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java b/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java index 46bf3a7c0e..38b882a322 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java @@ -8,19 +8,13 @@ import java.util.Map; import org.mockito.internal.creation.instance.InstantiatorProvider2Adapter; -import org.mockito.plugins.AnnotationEngine; -import org.mockito.plugins.InstantiatorProvider; -import org.mockito.plugins.InstantiatorProvider2; -import org.mockito.plugins.MockMaker; -import org.mockito.plugins.MockitoLogger; -import org.mockito.plugins.MockitoPlugins; -import org.mockito.plugins.PluginSwitch; -import org.mockito.plugins.StackTraceCleanerProvider; +import org.mockito.plugins.*; class DefaultMockitoPlugins implements MockitoPlugins { private static final Map DEFAULT_PLUGINS = new HashMap(); static final String INLINE_ALIAS = "mock-maker-inline"; + static final String MODULE_ALIAS = "member-accessor-module"; static { // Keep the mapping: plugin interface name -> plugin implementation class name @@ -41,6 +35,11 @@ class DefaultMockitoPlugins implements MockitoPlugins { INLINE_ALIAS, "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker"); DEFAULT_PLUGINS.put( MockitoLogger.class.getName(), "org.mockito.internal.util.ConsoleMockitoLogger"); + DEFAULT_PLUGINS.put( + MemberAccessor.class.getName(), + "org.mockito.internal.util.reflection.ReflectionMemberAccessor"); + DEFAULT_PLUGINS.put( + MODULE_ALIAS, "org.mockito.internal.util.reflection.ModuleMemberAccessor"); } @Override @@ -80,7 +79,7 @@ private T create(Class pluginType, String className) { // Default implementation. Use our own ClassLoader instead of the context // ClassLoader, as the default implementation is assumed to be part of // Mockito and may not be available via the context ClassLoader. - return pluginType.cast(Class.forName(className).newInstance()); + return pluginType.cast(Class.forName(className).getDeclaredConstructor().newInstance()); } catch (Exception e) { throw new IllegalStateException( "Internal problem occurred, please report it. " 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 d58ca2cdf3..8f8f76edcb 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginInitializer.java @@ -47,7 +47,7 @@ public T loadImpl(Class service) { classOrAlias = plugins.getDefaultPluginClass(alias); } Class pluginClass = loader.loadClass(classOrAlias); - Object plugin = pluginClass.newInstance(); + Object plugin = pluginClass.getDeclaredConstructor().newInstance(); return service.cast(plugin); } return null; 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 0419001285..ba7aae1728 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java @@ -5,13 +5,7 @@ package org.mockito.internal.configuration.plugins; import org.mockito.internal.creation.instance.InstantiatorProviderAdapter; -import org.mockito.plugins.AnnotationEngine; -import org.mockito.plugins.InstantiatorProvider; -import org.mockito.plugins.InstantiatorProvider2; -import org.mockito.plugins.MockMaker; -import org.mockito.plugins.MockitoLogger; -import org.mockito.plugins.PluginSwitch; -import org.mockito.plugins.StackTraceCleanerProvider; +import org.mockito.plugins.*; class PluginRegistry { @@ -22,6 +16,10 @@ class PluginRegistry { new PluginLoader(pluginSwitch, DefaultMockitoPlugins.INLINE_ALIAS) .loadPlugin(MockMaker.class); + private final MemberAccessor memberAccessor = + new PluginLoader(pluginSwitch, DefaultMockitoPlugins.MODULE_ALIAS) + .loadPlugin(MemberAccessor.class); + private final StackTraceCleanerProvider stackTraceCleanerProvider = new PluginLoader(pluginSwitch).loadPlugin(StackTraceCleanerProvider.class); @@ -62,6 +60,16 @@ MockMaker getMockMaker() { return mockMaker; } + /** + * Returns the implementation of the member accessor available for the current runtime. + * + *

Returns {@link org.mockito.internal.util.reflection.ReflectionMemberAccessor} if no + * {@link org.mockito.plugins.MockMaker} extension exists or is visible in the current classpath.

+ */ + MemberAccessor getMemberAccessor() { + return memberAccessor; + } + /** * Returns the instantiator provider available for the current runtime. * 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 8469981202..603a03008a 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java @@ -4,12 +4,7 @@ */ package org.mockito.internal.configuration.plugins; -import org.mockito.plugins.AnnotationEngine; -import org.mockito.plugins.InstantiatorProvider2; -import org.mockito.plugins.MockMaker; -import org.mockito.plugins.MockitoLogger; -import org.mockito.plugins.MockitoPlugins; -import org.mockito.plugins.StackTraceCleanerProvider; +import org.mockito.plugins.*; /** * Access to Mockito behavior that can be reconfigured by plugins @@ -35,6 +30,16 @@ public static MockMaker getMockMaker() { return registry.getMockMaker(); } + /** + * Returns the implementation of the member accessor available for the current runtime. + * + *

Returns default member accessor if no + * {@link org.mockito.plugins.MemberAccessor} extension exists or is visible in the current classpath.

+ */ + public static MemberAccessor getMemberAccessor() { + return registry.getMemberAccessor(); + } + /** * Returns the instantiator provider available for the current runtime. * diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyCrossClassLoaderSerializationSupport.java b/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyCrossClassLoaderSerializationSupport.java index 4bb4405e73..90ccee882c 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyCrossClassLoaderSerializationSupport.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyCrossClassLoaderSerializationSupport.java @@ -6,7 +6,6 @@ import static org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.ForWriteReplace; import static org.mockito.internal.util.StringUtil.join; -import static org.mockito.internal.util.reflection.FieldSetter.setField; import java.io.*; import java.lang.reflect.Field; @@ -22,6 +21,7 @@ import org.mockito.mock.MockCreationSettings; import org.mockito.mock.MockName; import org.mockito.mock.SerializableMode; +import org.mockito.plugins.MemberAccessor; /** * This is responsible for serializing a mock, it is enabled if the mock is implementing {@link Serializable}. @@ -318,8 +318,14 @@ protected Class resolveClass(ObjectStreamClass desc) private void hackClassNameToMatchNewlyCreatedClass( ObjectStreamClass descInstance, Class proxyClass) throws ObjectStreamException { try { + MemberAccessor accessor = Plugins.getMemberAccessor(); Field classNameField = descInstance.getClass().getDeclaredField("name"); - setField(descInstance, classNameField, proxyClass.getCanonicalName()); + try { + accessor.set(classNameField, descInstance, proxyClass.getCanonicalName()); + } catch (IllegalAccessException e) { + throw new MockitoSerializationIssue( + "Access to " + classNameField + " was denied", e); + } } catch (NoSuchFieldException nsfe) { throw new MockitoSerializationIssue( join( diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyMockMaker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyMockMaker.java index 8ea513e2c6..5263ea1262 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyMockMaker.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyMockMaker.java @@ -5,9 +5,13 @@ package org.mockito.internal.creation.bytebuddy; import org.mockito.Incubating; +import org.mockito.MockedConstruction; import org.mockito.invocation.MockHandler; import org.mockito.mock.MockCreationSettings; +import java.util.Optional; +import java.util.function.Function; + /** * ByteBuddy MockMaker. * @@ -25,6 +29,12 @@ public T createMock(MockCreationSettings settings, MockHandler handler) { return defaultByteBuddyMockMaker.createMock(settings, handler); } + @Override + public Optional createSpy( + MockCreationSettings settings, MockHandler handler, T object) { + return defaultByteBuddyMockMaker.createSpy(settings, handler, object); + } + @Override public Class createMockType(MockCreationSettings creationSettings) { return defaultByteBuddyMockMaker.createMockType(creationSettings); @@ -51,4 +61,14 @@ public StaticMockControl createStaticMock( Class type, MockCreationSettings settings, MockHandler handler) { return defaultByteBuddyMockMaker.createStaticMock(type, settings, handler); } + + @Override + public ConstructionMockControl createConstructionMock( + Class type, + Function> settingsFactory, + Function> handlerFactory, + MockedConstruction.MockInitializer mockInitializer) { + return defaultByteBuddyMockMaker.createConstructionMock( + type, settingsFactory, handlerFactory, mockInitializer); + } } diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/BytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/BytecodeGenerator.java index b87d7cb6b7..b27b9727a1 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/BytecodeGenerator.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/BytecodeGenerator.java @@ -8,5 +8,7 @@ public interface BytecodeGenerator { Class mockClass(MockFeatures features); + void mockClassConstruction(Class type); + void mockClassStatic(Class type); } diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/ConstructionCallback.java b/src/main/java/org/mockito/internal/creation/bytebuddy/ConstructionCallback.java new file mode 100644 index 0000000000..ac8ecc4040 --- /dev/null +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/ConstructionCallback.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.creation.bytebuddy; + +public interface ConstructionCallback { + + Object apply(Class type, Object object, Object[] arguments, String[] parameterTypeNames); +} diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMaker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMaker.java index 097b8c8aae..0a20882b2c 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMaker.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMaker.java @@ -9,20 +9,25 @@ import java.io.IOException; import java.io.InputStream; import java.lang.instrument.Instrumentation; +import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; -import java.util.Arrays; -import java.util.Map; -import java.util.WeakHashMap; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import net.bytebuddy.agent.ByteBuddyAgent; import org.mockito.Incubating; +import org.mockito.MockedConstruction; +import org.mockito.creation.instance.InstantiationException; import org.mockito.creation.instance.Instantiator; import org.mockito.exceptions.base.MockitoException; import org.mockito.exceptions.base.MockitoInitializationException; +import org.mockito.exceptions.misusing.MockitoConfigurationException; import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.Platform; import org.mockito.internal.util.concurrent.DetachedThreadLocal; @@ -30,6 +35,7 @@ import org.mockito.invocation.MockHandler; import org.mockito.mock.MockCreationSettings; import org.mockito.plugins.InlineMockMaker; +import org.mockito.plugins.MemberAccessor; import static org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.*; import static org.mockito.internal.util.StringUtil.*; @@ -93,7 +99,8 @@ * support this feature. */ @Incubating -public class InlineByteBuddyMockMaker implements ClassCreatingMockMaker, InlineMockMaker { +public class InlineByteBuddyMockMaker + implements ClassCreatingMockMaker, InlineMockMaker, Instantiator { private static final Instrumentation INSTRUMENTATION; @@ -188,6 +195,13 @@ public class InlineByteBuddyMockMaker implements ClassCreatingMockMaker, InlineM private final DetachedThreadLocal, MockMethodInterceptor>> mockedStatics = new DetachedThreadLocal<>(DetachedThreadLocal.Cleaner.INLINE); + private final DetachedThreadLocal, BiConsumer>> + mockedConstruction = new DetachedThreadLocal<>(DetachedThreadLocal.Cleaner.INLINE); + + private final ThreadLocal mockitoConstruction = ThreadLocal.withInitial(() -> false); + + private final ThreadLocal currentSpied = new ThreadLocal<>(); + public InlineByteBuddyMockMaker() { if (INITIALIZATION_ERROR != null) { String detail; @@ -221,18 +235,101 @@ public InlineByteBuddyMockMaker() { Platform.describe()), INITIALIZATION_ERROR); } + + ThreadLocal> currentConstruction = new ThreadLocal<>(); + ThreadLocal isSuspended = ThreadLocal.withInitial(() -> false); + Predicate> isMockConstruction = + type -> { + if (isSuspended.get()) { + return false; + } else if (mockitoConstruction.get() || currentConstruction.get() != null) { + return true; + } + Map, ?> interceptors = mockedConstruction.get(); + if (interceptors != null && interceptors.containsKey(type)) { + currentConstruction.set(type); + return true; + } else { + return false; + } + }; + ConstructionCallback onConstruction = + (type, object, arguments, parameterTypeNames) -> { + if (mockitoConstruction.get()) { + return currentSpied.get(); + } else if (currentConstruction.get() != type) { + return null; + } + currentConstruction.remove(); + isSuspended.set(true); + try { + Map, BiConsumer> interceptors = + mockedConstruction.get(); + if (interceptors != null) { + BiConsumer interceptor = + interceptors.get(type); + if (interceptor != null) { + interceptor.accept( + object, + new InlineConstructionMockContext( + arguments, object.getClass(), parameterTypeNames)); + } + } + } finally { + isSuspended.set(false); + } + return null; + }; + bytecodeGenerator = new TypeCachingBytecodeGenerator( - new InlineBytecodeGenerator(INSTRUMENTATION, mocks, mockedStatics), true); + new InlineBytecodeGenerator( + INSTRUMENTATION, + mocks, + mockedStatics, + isMockConstruction, + onConstruction), + true); } @Override public T createMock(MockCreationSettings settings, MockHandler handler) { + return doCreateMock(settings, handler, false); + } + + @Override + public Optional createSpy( + MockCreationSettings settings, MockHandler handler, T object) { + if (object == null) { + throw new MockitoConfigurationException("Spy instance must not be null"); + } + currentSpied.set(object); + try { + return Optional.ofNullable(doCreateMock(settings, handler, true)); + } finally { + currentSpied.remove(); + } + } + + private T doCreateMock( + MockCreationSettings settings, + MockHandler handler, + boolean nullOnNonInlineConstruction) { Class type = createMockType(settings); - Instantiator instantiator = Plugins.getInstantiatorProvider().getInstantiator(settings); try { - T instance = instantiator.newInstance(type); + T instance; + try { + // We attempt to use the "native" mock maker first that avoids Objenesis and Unsafe + instance = newInstance(type); + } catch (InstantiationException ignored) { + if (nullOnNonInlineConstruction) { + return null; + } + Instantiator instantiator = + Plugins.getInstantiatorProvider().getInstantiator(settings); + instance = instantiator.newInstance(type); + } MockMethodInterceptor mockMethodInterceptor = new MockMethodInterceptor(handler, settings); mocks.put(instance, mockMethodInterceptor); @@ -424,6 +521,90 @@ public StaticMockControl createStaticMock( return new InlineStaticMockControl<>(type, interceptors, settings, handler); } + @Override + public ConstructionMockControl createConstructionMock( + Class type, + Function> settingsFactory, + Function> handlerFactory, + MockedConstruction.MockInitializer mockInitializer) { + if (type == Object.class) { + throw new MockitoException( + "It is not possible to mock construction of the Object class " + + "to avoid inference with default object constructor chains"); + } else if (type.isPrimitive() || Modifier.isAbstract(type.getModifiers())) { + throw new MockitoException( + "It is not possible to construct primitive types or abstract types: " + + type.getTypeName()); + } + + bytecodeGenerator.mockClassConstruction(type); + + Map, BiConsumer> interceptors = + mockedConstruction.get(); + if (interceptors == null) { + interceptors = new WeakHashMap<>(); + mockedConstruction.set(interceptors); + } + + return new InlineConstructionMockControl<>( + type, settingsFactory, handlerFactory, mockInitializer, interceptors); + } + + @Override + @SuppressWarnings("unchecked") + public T newInstance(Class cls) throws InstantiationException { + Constructor[] constructors = cls.getDeclaredConstructors(); + if (constructors.length == 0) { + throw new InstantiationException(cls.getTypeName() + " does not define a constructor"); + } + Constructor selected = constructors[0]; + for (Constructor constructor : constructors) { + if (Modifier.isPublic(constructor.getModifiers())) { + selected = constructor; + break; + } + } + Class[] types = selected.getParameterTypes(); + Object[] arguments = new Object[types.length]; + int index = 0; + for (Class type : types) { + arguments[index++] = makeStandardArgument(type); + } + MemberAccessor accessor = Plugins.getMemberAccessor(); + try { + mockitoConstruction.set(true); + try { + return (T) accessor.newInstance(selected, arguments); + } finally { + mockitoConstruction.set(false); + } + } catch (Exception e) { + throw new InstantiationException("Could not instantiate " + cls.getTypeName(), e); + } + } + + private Object makeStandardArgument(Class type) { + if (type == boolean.class) { + return false; + } else if (type == byte.class) { + return (byte) 0; + } else if (type == short.class) { + return (short) 0; + } else if (type == char.class) { + return (char) 0; + } else if (type == int.class) { + return 0; + } else if (type == long.class) { + return 0L; + } else if (type == float.class) { + return 0f; + } else if (type == double.class) { + return 0d; + } else { + return null; + } + } + private static class InlineStaticMockControl implements StaticMockControl { private final Class type; @@ -431,6 +612,7 @@ private static class InlineStaticMockControl implements StaticMockControl private final Map, MockMethodInterceptor> interceptors; private final MockCreationSettings settings; + private final MockHandler handler; private InlineStaticMockControl( @@ -478,4 +660,167 @@ public void disable() { } } } + + private class InlineConstructionMockControl implements ConstructionMockControl { + + private final Class type; + + private final Function> settingsFactory; + private final Function> handlerFactory; + + private final MockedConstruction.MockInitializer mockInitializer; + + private final Map, BiConsumer> interceptors; + + private final List all = new ArrayList<>(); + private int count; + + private InlineConstructionMockControl( + Class type, + Function> settingsFactory, + Function> handlerFactory, + MockedConstruction.MockInitializer mockInitializer, + Map, BiConsumer> interceptors) { + this.type = type; + this.settingsFactory = settingsFactory; + this.handlerFactory = handlerFactory; + this.mockInitializer = mockInitializer; + this.interceptors = interceptors; + } + + @Override + public Class getType() { + return type; + } + + @Override + public void enable() { + if (interceptors.putIfAbsent( + type, + (object, context) -> { + ((InlineConstructionMockContext) context).count = ++count; + MockMethodInterceptor interceptor = + new MockMethodInterceptor( + handlerFactory.apply(context), + settingsFactory.apply(context)); + mocks.put(object, interceptor); + try { + @SuppressWarnings("unchecked") + T cast = (T) object; + mockInitializer.prepare(cast, context); + } catch (Throwable t) { + mocks.remove(object); // TODO: filter stack trace? + throw new MockitoException( + "Could not initialize mocked construction", t); + } + all.add(object); + }) + != null) { + throw new MockitoException( + join( + "For " + + type.getName() + + ", static mocking is already registered in the current thread", + "", + "To create a new mock, the existing static mock registration must be deregistered")); + } + } + + @Override + public void disable() { + if (interceptors.remove(type) == null) { + throw new MockitoException( + join( + "Could not deregister " + + type.getName() + + " as a static mock since it is not currently registered", + "", + "To register a static mock, use Mockito.mockStatic(" + + type.getSimpleName() + + ".class)")); + } + all.clear(); + } + + @Override + @SuppressWarnings("unchecked") + public List getMocks() { + return (List) all; + } + } + + private static class InlineConstructionMockContext implements MockedConstruction.Context { + + private static final Map> PRIMITIVES = new HashMap<>(); + + static { + PRIMITIVES.put(boolean.class.getTypeName(), boolean.class); + PRIMITIVES.put(byte.class.getTypeName(), byte.class); + PRIMITIVES.put(short.class.getTypeName(), short.class); + PRIMITIVES.put(char.class.getTypeName(), char.class); + PRIMITIVES.put(int.class.getTypeName(), int.class); + PRIMITIVES.put(long.class.getTypeName(), long.class); + PRIMITIVES.put(float.class.getTypeName(), float.class); + PRIMITIVES.put(double.class.getTypeName(), double.class); + } + + private int count; + + private final Object[] arguments; + private final Class type; + private final String[] parameterTypeNames; + + private InlineConstructionMockContext( + Object[] arguments, Class type, String[] parameterTypeNames) { + this.arguments = arguments; + this.type = type; + this.parameterTypeNames = parameterTypeNames; + } + + @Override + public int getCount() { + if (count == 0) { + throw new MockitoConfigurationException( + "mocked construction context is not initialized"); + } + return count; + } + + @Override + public Constructor constructor() { + Class[] parameterTypes = new Class[parameterTypeNames.length]; + int index = 0; + for (String parameterTypeName : parameterTypeNames) { + if (PRIMITIVES.containsKey(parameterTypeName)) { + parameterTypes[index++] = PRIMITIVES.get(parameterTypeName); + } else { + try { + parameterTypes[index++] = + Class.forName(parameterTypeName, false, type.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new MockitoException( + "Could not find parameter of type " + parameterTypeName, e); + } + } + } + try { + return type.getDeclaredConstructor(parameterTypes); + } catch (NoSuchMethodException e) { + throw new MockitoException( + join( + "Could not resolve constructor of type", + "", + type.getTypeName(), + "", + "with arguments of types", + Arrays.toString(parameterTypes)), + e); + } + } + + @Override + public List arguments() { + return Collections.unmodifiableList(Arrays.asList(arguments)); + } + } } diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java index 187f1ea945..fc3c69df13 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java @@ -4,17 +4,17 @@ */ package org.mockito.internal.creation.bytebuddy; -import static net.bytebuddy.implementation.MethodDelegation.withDefaultConfiguration; -import static net.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder.ParameterBinder.ForFixedValue.OfConstant.of; -import static net.bytebuddy.matcher.ElementMatchers.*; -import static org.mockito.internal.util.StringUtil.join; - import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.security.ProtectionDomain; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; import net.bytebuddy.ByteBuddy; import net.bytebuddy.ClassFileVersion; @@ -43,6 +43,11 @@ import org.mockito.internal.util.concurrent.WeakConcurrentSet; import org.mockito.mock.SerializableMode; +import static net.bytebuddy.implementation.MethodDelegation.*; +import static net.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder.ParameterBinder.ForFixedValue.OfConstant.*; +import static net.bytebuddy.matcher.ElementMatchers.*; +import static org.mockito.internal.util.StringUtil.*; + public class InlineBytecodeGenerator implements BytecodeGenerator, ClassFileTransformer { private static final String PRELOAD = "org.mockito.inline.preload"; @@ -75,7 +80,9 @@ public class InlineBytecodeGenerator implements BytecodeGenerator, ClassFileTran public InlineBytecodeGenerator( Instrumentation instrumentation, WeakConcurrentMap mocks, - DetachedThreadLocal, MockMethodInterceptor>> mockedStatics) { + DetachedThreadLocal, MockMethodInterceptor>> mockedStatics, + Predicate> isMockConstruction, + ConstructionCallback onConstruction) { preload(); this.instrumentation = instrumentation; byteBuddy = @@ -118,6 +125,7 @@ public InlineBytecodeGenerator( Advice.withCustomMapping() .bind(MockMethodAdvice.Identifier.class, identifier) .to(MockMethodAdvice.ForStatic.class)) + .constructor(any(), new MockMethodAdvice.ConstructorShortcut(identifier)) .method( isHashCode(), Advice.withCustomMapping() @@ -150,7 +158,9 @@ public InlineBytecodeGenerator( this.canRead = canRead; this.redefineModule = redefineModule; MockMethodDispatcher.set( - identifier, new MockMethodAdvice(mocks, mockedStatics, identifier)); + identifier, + new MockMethodAdvice( + mocks, mockedStatics, identifier, isMockConstruction, onConstruction)); instrumentation.addTransformer(this, true); } @@ -202,10 +212,15 @@ public Class mockClass(MockFeatures features) { } @Override - public void mockClassStatic(Class type) { + public synchronized void mockClassStatic(Class type) { triggerRetransformation(Collections.singleton(type), true); } + @Override + public synchronized void mockClassConstruction(Class type) { + triggerRetransformation(Collections.singleton(type), false); + } + private void triggerRetransformation(Set> types, boolean flat) { Set> targets = new HashSet>(); @@ -335,8 +350,12 @@ public byte[] transform( return byteBuddy .redefine( classBeingRedefined, + // new ClassFileLocator.Compound( ClassFileLocator.Simple.of( - classBeingRedefined.getName(), classfileBuffer)) + classBeingRedefined.getName(), classfileBuffer) + // ,ClassFileLocator.ForClassLoader.ofSystemLoader() + // ) + ) // Note: The VM erases parameter meta data from the provided class file // (bug). We just add this information manually. .visit(new ParameterWritingVisitorWrapper(classBeingRedefined)) 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 072a958577..69a7ed21a6 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java @@ -12,18 +12,34 @@ import java.lang.ref.SoftReference; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; +import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.function.Predicate; +import net.bytebuddy.ClassFileVersion; import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.AsmVisitorWrapper; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldList; import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.method.ParameterDescription; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.scaffold.MethodGraph; +import net.bytebuddy.implementation.Implementation; import net.bytebuddy.implementation.bind.annotation.Argument; import net.bytebuddy.implementation.bind.annotation.This; +import net.bytebuddy.implementation.bytecode.StackSize; import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.jar.asm.Label; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.jar.asm.Type; +import net.bytebuddy.pool.TypePool; +import net.bytebuddy.utility.OpenedClassReader; import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.creation.bytebuddy.inject.MockMethodDispatcher; import org.mockito.internal.debugging.LocationImpl; import org.mockito.internal.exceptions.stacktrace.ConditionalStackTraceFilter; @@ -33,6 +49,9 @@ import org.mockito.internal.invocation.mockref.MockWeakReference; import org.mockito.internal.util.concurrent.DetachedThreadLocal; import org.mockito.internal.util.concurrent.WeakConcurrentMap; +import org.mockito.plugins.MemberAccessor; + +import static net.bytebuddy.matcher.ElementMatchers.*; public class MockMethodAdvice extends MockMethodDispatcher { @@ -46,13 +65,20 @@ public class MockMethodAdvice extends MockMethodDispatcher { private final WeakConcurrentMap, SoftReference> graphs = new WeakConcurrentMap.WithInlinedExpunction, SoftReference>(); + private final Predicate> isMockConstruction; + private final ConstructionCallback onConstruction; + public MockMethodAdvice( WeakConcurrentMap interceptors, DetachedThreadLocal, MockMethodInterceptor>> mockedStatics, - String identifier) { + String identifier, + Predicate> isMockConstruction, + ConstructionCallback onConstruction) { this.interceptors = interceptors; this.mockedStatics = mockedStatics; + this.onConstruction = onConstruction; this.identifier = identifier; + this.isMockConstruction = isMockConstruction; } @SuppressWarnings("unused") @@ -144,6 +170,12 @@ public Callable handleStatic(Class type, Method origin, Object[] arguments new LocationImpl(new Throwable(), true))); } + @Override + public Object handleConstruction( + Class type, Object object, Object[] arguments, String[] parameterTypeNames) { + return onConstruction.apply(type, object, arguments, parameterTypeNames); + } + @Override public boolean isMock(Object instance) { // We need to exclude 'interceptors.target' explicitly to avoid a recursive check on whether @@ -183,6 +215,11 @@ public boolean isOverridden(Object instance, Method origin) { .represents(origin.getDeclaringClass()); } + @Override + public boolean isConstructorMock(Class type) { + return isMockConstruction.test(type); + } + private static class RealMethodCall implements RealMethod { private final SelfCallInfo selfCallInfo; @@ -208,10 +245,6 @@ public boolean isInvokable() { @Override public Object invoke() throws Throwable { - if (!Modifier.isPublic( - origin.getDeclaringClass().getModifiers() & origin.getModifiers())) { - origin.setAccessible(true); - } selfCallInfo.set(instanceRef.get()); return tryInvoke(origin, instanceRef.get(), arguments); } @@ -243,10 +276,6 @@ public boolean isInvokable() { @Override public Object invoke() throws Throwable { Method method = origin.getJavaMethod(); - if (!Modifier.isPublic( - method.getDeclaringClass().getModifiers() & method.getModifiers())) { - method.setAccessible(true); - } MockMethodDispatcher mockMethodDispatcher = MockMethodDispatcher.get(identifier, instanceRef.get()); if (!(mockMethodDispatcher instanceof MockMethodAdvice)) { @@ -288,9 +317,6 @@ public boolean isInvokable() { @Override public Object invoke() throws Throwable { - if (!Modifier.isPublic(type.getModifiers() & origin.getModifiers())) { - origin.setAccessible(true); - } selfCallInfo.set(type); return tryInvoke(origin, null, arguments); } @@ -298,8 +324,9 @@ public Object invoke() throws Throwable { private static Object tryInvoke(Method origin, Object instance, Object[] arguments) throws Throwable { + MemberAccessor accessor = Plugins.getMemberAccessor(); try { - return origin.invoke(instance, arguments); + return accessor.invoke(origin, instance, arguments); } catch (InvocationTargetException exception) { Throwable cause = exception.getCause(); new ConditionalStackTraceFilter() @@ -344,6 +371,297 @@ boolean checkSelfCall(Object value) { } } + static class ConstructorShortcut + implements AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper { + + private final String identifier; + + ConstructorShortcut(String identifier) { + this.identifier = identifier; + } + + @Override + public MethodVisitor wrap( + TypeDescription instrumentedType, + MethodDescription instrumentedMethod, + MethodVisitor methodVisitor, + Implementation.Context implementationContext, + TypePool typePool, + int writerFlags, + int readerFlags) { + if (instrumentedMethod.isConstructor() && !instrumentedType.represents(Object.class)) { + MethodList constructors = + instrumentedType + .getSuperClass() + .asErasure() + .getDeclaredMethods() + .filter(isConstructor().and(not(isPrivate()))); + int arguments = Integer.MAX_VALUE; + boolean visible = false; + MethodDescription.InDefinedShape current = null; + for (MethodDescription.InDefinedShape constructor : constructors) { + if (constructor.getParameters().size() < arguments + && (!visible || constructor.isPackagePrivate())) { + current = constructor; + visible = constructor.isPackagePrivate(); + } + } + if (current != null) { + final MethodDescription.InDefinedShape selected = current; + return new MethodVisitor(OpenedClassReader.ASM_API, methodVisitor) { + @Override + public void visitCode() { + super.visitCode(); + /* + * The byte code that is added to the start of the method is roughly equivalent to + * the following byte code for a hypothetical constructor of class Current: + * + * if (MockMethodDispatcher.isConstructorMock(, Current.class) { + * super(); + * Current o = (Current) MockMethodDispatcher.handleConstruction(Current.class, + * this, + * new Object[] {argument1, argument2, ...}, + * new String[] {argumentType1, argumentType2, ...}); + * if (o != null) { + * this.field = o.field; // for each declared field + * } + * return; + * } + * + * This avoids the invocation of the original constructor chain but fullfils the + * verifier requirement to invoke a super constructor. + */ + Label label = new Label(); + super.visitLdcInsn(identifier); + if (implementationContext + .getClassFileVersion() + .isAtLeast(ClassFileVersion.JAVA_V5)) { + super.visitLdcInsn(Type.getType(instrumentedType.getDescriptor())); + } else { + super.visitLdcInsn(instrumentedType.getName()); + super.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(Class.class), + "forName", + Type.getMethodDescriptor( + Type.getType(Class.class), + Type.getType(String.class)), + false); + } + super.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(MockMethodDispatcher.class), + "isConstructorMock", + Type.getMethodDescriptor( + Type.BOOLEAN_TYPE, + Type.getType(String.class), + Type.getType(Class.class)), + false); + super.visitInsn(Opcodes.ICONST_0); + super.visitJumpInsn(Opcodes.IF_ICMPEQ, label); + super.visitVarInsn(Opcodes.ALOAD, 0); + for (TypeDescription type : + selected.getParameters().asTypeList().asErasures()) { + if (type.represents(boolean.class) + || type.represents(byte.class) + || type.represents(short.class) + || type.represents(char.class) + || type.represents(int.class)) { + super.visitInsn(Opcodes.ICONST_0); + } else if (type.represents(long.class)) { + super.visitInsn(Opcodes.LCONST_0); + } else if (type.represents(float.class)) { + super.visitInsn(Opcodes.FCONST_0); + } else if (type.represents(double.class)) { + super.visitInsn(Opcodes.DCONST_0); + } else { + super.visitInsn(Opcodes.ACONST_NULL); + } + } + super.visitMethodInsn( + Opcodes.INVOKESPECIAL, + selected.getDeclaringType().getInternalName(), + selected.getInternalName(), + selected.getDescriptor(), + false); + super.visitLdcInsn(identifier); + if (implementationContext + .getClassFileVersion() + .isAtLeast(ClassFileVersion.JAVA_V5)) { + super.visitLdcInsn(Type.getType(instrumentedType.getDescriptor())); + } else { + super.visitLdcInsn(instrumentedType.getName()); + super.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(Class.class), + "forName", + Type.getMethodDescriptor( + Type.getType(Class.class), + Type.getType(String.class)), + false); + } + super.visitVarInsn(Opcodes.ALOAD, 0); + super.visitLdcInsn(instrumentedMethod.getParameters().size()); + super.visitTypeInsn( + Opcodes.ANEWARRAY, Type.getInternalName(Object.class)); + int index = 0; + for (ParameterDescription parameter : + instrumentedMethod.getParameters()) { + super.visitInsn(Opcodes.DUP); + super.visitLdcInsn(index++); + Type type = + Type.getType( + parameter.getType().asErasure().getDescriptor()); + super.visitVarInsn( + type.getOpcode(Opcodes.ILOAD), parameter.getOffset()); + if (parameter.getType().isPrimitive()) { + Type wrapper = + Type.getType( + parameter + .getType() + .asErasure() + .asBoxed() + .getDescriptor()); + super.visitMethodInsn( + Opcodes.INVOKESTATIC, + wrapper.getInternalName(), + "valueOf", + Type.getMethodDescriptor(wrapper, type), + false); + } + super.visitInsn(Opcodes.AASTORE); + } + index = 0; + super.visitLdcInsn(instrumentedMethod.getParameters().size()); + super.visitTypeInsn( + Opcodes.ANEWARRAY, Type.getInternalName(String.class)); + for (TypeDescription typeDescription : + instrumentedMethod.getParameters().asTypeList().asErasures()) { + super.visitInsn(Opcodes.DUP); + super.visitLdcInsn(index++); + super.visitLdcInsn(typeDescription.getName()); + super.visitInsn(Opcodes.AASTORE); + } + super.visitMethodInsn( + Opcodes.INVOKESTATIC, + Type.getInternalName(MockMethodDispatcher.class), + "handleConstruction", + Type.getMethodDescriptor( + Type.getType(Object.class), + Type.getType(String.class), + Type.getType(Class.class), + Type.getType(Object.class), + Type.getType(Object[].class), + Type.getType(String[].class)), + false); + FieldList fields = + instrumentedType.getDeclaredFields().filter(not(isStatic())); + super.visitTypeInsn( + Opcodes.CHECKCAST, instrumentedType.getInternalName()); + super.visitInsn(Opcodes.DUP); + Label noSpy = new Label(); + super.visitJumpInsn(Opcodes.IFNULL, noSpy); + for (FieldDescription field : fields) { + super.visitInsn(Opcodes.DUP); + super.visitFieldInsn( + Opcodes.GETFIELD, + instrumentedType.getInternalName(), + field.getInternalName(), + field.getDescriptor()); + super.visitVarInsn(Opcodes.ALOAD, 0); + super.visitInsn( + field.getType().getStackSize() == StackSize.DOUBLE + ? Opcodes.DUP_X2 + : Opcodes.DUP_X1); + super.visitInsn(Opcodes.POP); + super.visitFieldInsn( + Opcodes.PUTFIELD, + instrumentedType.getInternalName(), + field.getInternalName(), + field.getDescriptor()); + } + super.visitLabel(noSpy); + if (implementationContext + .getClassFileVersion() + .isAtLeast(ClassFileVersion.JAVA_V6)) { + Object[] locals = + toFrames( + instrumentedType.getInternalName(), + instrumentedMethod + .getParameters() + .asTypeList() + .asErasures()); + super.visitFrame( + Opcodes.F_FULL, + locals.length, + locals, + 1, + new Object[] {instrumentedType.getInternalName()}); + } + super.visitInsn(Opcodes.POP); + super.visitInsn(Opcodes.RETURN); + super.visitLabel(label); + if (implementationContext + .getClassFileVersion() + .isAtLeast(ClassFileVersion.JAVA_V6)) { + Object[] locals = + toFrames( + Opcodes.UNINITIALIZED_THIS, + instrumentedMethod + .getParameters() + .asTypeList() + .asErasures()); + super.visitFrame( + Opcodes.F_FULL, locals.length, locals, 0, new Object[0]); + } + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + int prequel = Math.max(5, selected.getStackSize()); + for (ParameterDescription parameter : + instrumentedMethod.getParameters()) { + prequel = + Math.max( + prequel, + 6 + parameter.getType().getStackSize().getSize()); + prequel = Math.max(prequel, 8); + } + super.visitMaxs(Math.max(maxStack, prequel), maxLocals); + } + }; + } + } + return methodVisitor; + } + + private static Object[] toFrames(Object self, List types) { + Object[] frames = new Object[1 + types.size()]; + frames[0] = self; + int index = 0; + for (TypeDescription type : types) { + Object frame; + if (type.represents(boolean.class) + || type.represents(byte.class) + || type.represents(short.class) + || type.represents(char.class) + || type.represents(int.class)) { + frame = Opcodes.INTEGER; + } else if (type.represents(long.class)) { + frame = Opcodes.LONG; + } else if (type.represents(float.class)) { + frame = Opcodes.FLOAT; + } else if (type.represents(double.class)) { + frame = Opcodes.DOUBLE; + } else { + frame = type.getInternalName(); + } + frames[++index] = frame; + } + return frames; + } + } + @Retention(RetentionPolicy.RUNTIME) @interface Identifier {} diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java index 4a65643bc5..cd2b177640 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java @@ -208,6 +208,12 @@ public void mockClassStatic(Class type) { throw new MockitoException("The subclass byte code generator cannot create static mocks"); } + @Override + public void mockClassConstruction(Class type) { + throw new MockitoException( + "The subclass byte code generator cannot create construction mocks"); + } + private Collection> getAllTypes(Class type) { Collection> supertypes = new LinkedList>(); supertypes.add(type); diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/TypeCachingBytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/TypeCachingBytecodeGenerator.java index 76bf44dca8..b3e70c7da7 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/TypeCachingBytecodeGenerator.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/TypeCachingBytecodeGenerator.java @@ -62,6 +62,11 @@ public void mockClassStatic(Class type) { bytecodeGenerator.mockClassStatic(type); } + @Override + public void mockClassConstruction(Class type) { + bytecodeGenerator.mockClassConstruction(type); + } + private static class MockitoMockKey extends TypeCache.SimpleKey { private final SerializableMode serializableMode; diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.java b/src/main/java/org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.java index f1b49051e0..3be8501574 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.java @@ -36,12 +36,32 @@ public static void set(String identifier, MockMethodDispatcher dispatcher) { DISPATCHERS.putIfAbsent(identifier, dispatcher); } + @SuppressWarnings("unused") + public static boolean isConstructorMock(String identifier, Class type) { + return DISPATCHERS.get(identifier).isConstructorMock(type); + } + + @SuppressWarnings("unused") + public static Object handleConstruction( + String identifier, + Class type, + Object object, + Object[] arguments, + String[] parameterTypeNames) { + return DISPATCHERS + .get(identifier) + .handleConstruction(type, object, arguments, parameterTypeNames); + } + public abstract Callable handle(Object instance, Method origin, Object[] arguments) throws Throwable; public abstract Callable handleStatic(Class type, Method origin, Object[] arguments) throws Throwable; + public abstract Object handleConstruction( + Class type, Object object, Object[] arguments, String[] parameterTypeNames); + public abstract boolean isMock(Object instance); public abstract boolean isMocked(Object instance); @@ -49,4 +69,6 @@ public abstract Callable handleStatic(Class type, Method origin, Object[] public abstract boolean isMockedStatic(Class type); public abstract boolean isOverridden(Object instance, Method origin); + + public abstract boolean isConstructorMock(Class type); } diff --git a/src/main/java/org/mockito/internal/creation/instance/ConstructorInstantiator.java b/src/main/java/org/mockito/internal/creation/instance/ConstructorInstantiator.java index 94e12fa86a..13f11d7419 100644 --- a/src/main/java/org/mockito/internal/creation/instance/ConstructorInstantiator.java +++ b/src/main/java/org/mockito/internal/creation/instance/ConstructorInstantiator.java @@ -14,8 +14,9 @@ import org.mockito.creation.instance.InstantiationException; import org.mockito.creation.instance.Instantiator; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.Primitives; -import org.mockito.internal.util.reflection.AccessibilityChanger; +import org.mockito.plugins.MemberAccessor; public class ConstructorInstantiator implements Instantiator { @@ -64,9 +65,8 @@ private T withParams(Class cls, Object... params) { private static T invokeConstructor(Constructor constructor, Object... params) throws java.lang.InstantiationException, IllegalAccessException, InvocationTargetException { - AccessibilityChanger accessibility = new AccessibilityChanger(); - accessibility.enableAccess(constructor); - return (T) constructor.newInstance(params); + MemberAccessor accessor = Plugins.getMemberAccessor(); + return (T) accessor.newInstance(constructor, params); } private InstantiationException paramsException(Class cls, Exception e) { diff --git a/src/main/java/org/mockito/internal/junit/util/JUnitFailureHacker.java b/src/main/java/org/mockito/internal/junit/util/JUnitFailureHacker.java index 89bd4f1f4b..d6e784c647 100644 --- a/src/main/java/org/mockito/internal/junit/util/JUnitFailureHacker.java +++ b/src/main/java/org/mockito/internal/junit/util/JUnitFailureHacker.java @@ -7,7 +7,9 @@ import java.lang.reflect.Field; import org.junit.runner.notification.Failure; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.exceptions.ExceptionIncludingMockitoWarnings; +import org.mockito.plugins.MemberAccessor; @Deprecated public class JUnitFailureHacker { @@ -36,11 +38,11 @@ private boolean isEmpty(String warnings) { } private static Object getInternalState(Object target, String field) { + MemberAccessor accessor = Plugins.getMemberAccessor(); Class c = target.getClass(); try { Field f = getFieldFromHierarchy(c, field); - f.setAccessible(true); - return f.get(target); + return accessor.get(f, target); } catch (Exception e) { throw new RuntimeException( "Unable to get internal state on a private field. Please report to mockito mailing list.", @@ -49,11 +51,11 @@ private static Object getInternalState(Object target, String field) { } private static void setInternalState(Object target, String field, Object value) { + MemberAccessor accessor = Plugins.getMemberAccessor(); Class c = target.getClass(); try { Field f = getFieldFromHierarchy(c, field); - f.setAccessible(true); - f.set(target, value); + accessor.set(f, target, value); } catch (Exception e) { throw new RuntimeException( "Unable to set internal state on a private field. Please report to mockito mailing list.", diff --git a/src/main/java/org/mockito/internal/matchers/apachecommons/EqualsBuilder.java b/src/main/java/org/mockito/internal/matchers/apachecommons/EqualsBuilder.java index 9a43040e34..1789a5d7a3 100644 --- a/src/main/java/org/mockito/internal/matchers/apachecommons/EqualsBuilder.java +++ b/src/main/java/org/mockito/internal/matchers/apachecommons/EqualsBuilder.java @@ -4,7 +4,9 @@ */ package org.mockito.internal.matchers.apachecommons; -import java.lang.reflect.AccessibleObject; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.plugins.MemberAccessor; + import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Arrays; @@ -253,20 +255,16 @@ public static boolean reflectionEquals( return false; } EqualsBuilder equalsBuilder = new EqualsBuilder(); - try { - reflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields); - while (testClass.getSuperclass() != null && testClass != reflectUpToClass) { - testClass = testClass.getSuperclass(); - reflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields); - } - } catch (IllegalArgumentException e) { - // In this case, we tried to test a subclass vs. a superclass and - // the subclass has ivars or the ivars are transient and - // we are testing transients. - // If a subclass has ivars that we are trying to test them, we get an - // exception and we know that the objects are not equal. + if (reflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields)) { return false; } + while (testClass.getSuperclass() != null && testClass != reflectUpToClass) { + testClass = testClass.getSuperclass(); + if (reflectionAppend( + lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields)) { + return false; + } + } return equalsBuilder.isEquals(); } @@ -281,7 +279,7 @@ public static boolean reflectionEquals( * @param useTransients whether to test transient fields * @param excludeFields array of field names to exclude from testing */ - private static void reflectionAppend( + private static boolean reflectionAppend( Object lhs, Object rhs, Class clazz, @@ -293,7 +291,7 @@ private static void reflectionAppend( excludeFields != null ? Arrays.asList(excludeFields) : Collections.emptyList(); - AccessibleObject.setAccessible(fields, true); + MemberAccessor accessor = Plugins.getMemberAccessor(); for (int i = 0; i < fields.length && builder.isEquals; i++) { Field f = fields[i]; if (!excludedFieldList.contains(f.getName()) @@ -301,14 +299,18 @@ private static void reflectionAppend( && (useTransients || !Modifier.isTransient(f.getModifiers())) && (!Modifier.isStatic(f.getModifiers()))) { try { - builder.append(f.get(lhs), f.get(rhs)); - } catch (IllegalAccessException e) { - // this can't happen. Would get a Security exception instead - // throw a runtime exception in case the impossible happens. - throw new InternalError("Unexpected IllegalAccessException"); + builder.append(accessor.get(f, lhs), accessor.get(f, rhs)); + } catch (RuntimeException | IllegalAccessException ignored) { + // In this case, we tried to test a subclass vs. a superclass and + // the subclass has ivars or the ivars are transient and we are + // testing transients. If a subclass has ivars that we are trying + // to test them, we get an exception and we know that the objects + // are not equal. + return true; } } } + return false; } // ------------------------------------------------------------------------- diff --git a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ForwardsInvocations.java b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ForwardsInvocations.java index 0df0f6c3ef..0f2aaa3975 100644 --- a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ForwardsInvocations.java +++ b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ForwardsInvocations.java @@ -11,8 +11,10 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.invocation.Invocation; import org.mockito.invocation.InvocationOnMock; +import org.mockito.plugins.MemberAccessor; import org.mockito.stubbing.Answer; /** @@ -41,13 +43,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { mockMethod, delegateMethod, invocation.getMock(), delegatedObject); } + MemberAccessor accessor = Plugins.getMemberAccessor(); Object[] rawArguments = ((Invocation) invocation).getRawArguments(); - try { - delegateMethod.setAccessible(true); - } catch (SecurityException ignore) { - // try to invoke anyway - } - return delegateMethod.invoke(delegatedObject, rawArguments); + return accessor.invoke(delegateMethod, delegatedObject, rawArguments); } catch (NoSuchMethodException e) { throw delegatedMethodDoesNotExistOnDelegate( mockMethod, invocation.getMock(), delegatedObject); diff --git a/src/main/java/org/mockito/internal/util/JavaEightUtil.java b/src/main/java/org/mockito/internal/util/JavaEightUtil.java index bc39d44b68..20c6e80c3b 100644 --- a/src/main/java/org/mockito/internal/util/JavaEightUtil.java +++ b/src/main/java/org/mockito/internal/util/JavaEightUtil.java @@ -181,7 +181,7 @@ private static Object invokeNullaryFactoryMethod(final String fqcn, final String private static Object getStaticFieldValue(final String fqcn, final String fieldName) { try { final Class type = getClass(fqcn); - final Field field = type.getDeclaredField(fieldName); + final Field field = type.getField(fieldName); return field.get(null); // any exception is really unexpected since the type name has // already been verified diff --git a/src/main/java/org/mockito/internal/util/MockUtil.java b/src/main/java/org/mockito/internal/util/MockUtil.java index 4767ef9445..46e782cba0 100644 --- a/src/main/java/org/mockito/internal/util/MockUtil.java +++ b/src/main/java/org/mockito/internal/util/MockUtil.java @@ -4,8 +4,7 @@ */ package org.mockito.internal.util; -import static org.mockito.internal.handler.MockHandlerFactory.createMockHandler; - +import org.mockito.MockedConstruction; import org.mockito.Mockito; import org.mockito.exceptions.misusing.NotAMockException; import org.mockito.internal.configuration.plugins.Plugins; @@ -18,6 +17,10 @@ import org.mockito.plugins.MockMaker; import org.mockito.plugins.MockMaker.TypeMockability; +import java.util.function.Function; + +import static org.mockito.internal.handler.MockHandlerFactory.createMockHandler; + @SuppressWarnings("unchecked") public class MockUtil { @@ -32,11 +35,21 @@ public static TypeMockability typeMockabilityOf(Class type) { public static T createMock(MockCreationSettings settings) { MockHandler mockHandler = createMockHandler(settings); - T mock = mockMaker.createMock(settings, mockHandler); - Object spiedInstance = settings.getSpiedInstance(); + + T mock; if (spiedInstance != null) { - new LenientCopyTool().copyToMock(spiedInstance, mock); + mock = + mockMaker + .createSpy(settings, mockHandler, (T) spiedInstance) + .orElseGet( + () -> { + T instance = mockMaker.createMock(settings, mockHandler); + new LenientCopyTool().copyToMock(spiedInstance, instance); + return instance; + }); + } else { + mock = mockMaker.createMock(settings, mockHandler); } return mock; @@ -108,4 +121,14 @@ public static MockMaker.StaticMockControl createStaticMock( MockHandler handler = createMockHandler(settings); return mockMaker.createStaticMock(type, settings, handler); } + + public static MockMaker.ConstructionMockControl createConstructionMock( + Class type, + Function> settingsFactory, + MockedConstruction.MockInitializer mockInitializer) { + Function> handlerFactory = + context -> createMockHandler(settingsFactory.apply(context)); + return mockMaker.createConstructionMock( + type, settingsFactory, handlerFactory, mockInitializer); + } } diff --git a/src/main/java/org/mockito/internal/util/reflection/AccessibilityChanger.java b/src/main/java/org/mockito/internal/util/reflection/AccessibilityChanger.java deleted file mode 100644 index 33725e0137..0000000000 --- a/src/main/java/org/mockito/internal/util/reflection/AccessibilityChanger.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2007 Mockito contributors - * This program is made available under the terms of the MIT License. - */ -package org.mockito.internal.util.reflection; - -import java.lang.reflect.AccessibleObject; - -public class AccessibilityChanger { - - private Boolean wasAccessible = null; - - /** - * safely disables access - */ - public void safelyDisableAccess(AccessibleObject accessibleObject) { - assert wasAccessible != null : "accessibility info shall not be null"; - try { - accessibleObject.setAccessible(wasAccessible); - } catch (Throwable t) { - // ignore - } - } - - /** - * changes the accessibleObject accessibility and returns true if accessibility was changed - */ - public void enableAccess(AccessibleObject accessibleObject) { - wasAccessible = accessibleObject.isAccessible(); - accessibleObject.setAccessible(true); - } -} diff --git a/src/main/java/org/mockito/internal/util/reflection/BeanPropertySetter.java b/src/main/java/org/mockito/internal/util/reflection/BeanPropertySetter.java index f91a35c202..7a33dcec7b 100644 --- a/src/main/java/org/mockito/internal/util/reflection/BeanPropertySetter.java +++ b/src/main/java/org/mockito/internal/util/reflection/BeanPropertySetter.java @@ -4,6 +4,9 @@ */ package org.mockito.internal.util.reflection; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.plugins.MemberAccessor; + import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -51,13 +54,11 @@ public BeanPropertySetter(final Object target, final Field propertyField) { */ public boolean set(final Object value) { - AccessibilityChanger changer = new AccessibilityChanger(); + MemberAccessor accessor = Plugins.getMemberAccessor(); Method writeMethod = null; try { writeMethod = target.getClass().getMethod(setterName(field.getName()), field.getType()); - - changer.enableAccess(writeMethod); - writeMethod.invoke(target, value); + accessor.invoke(writeMethod, target, value); return true; } catch (InvocationTargetException e) { throw new RuntimeException( @@ -83,10 +84,6 @@ public boolean set(final Object value) { e); } catch (NoSuchMethodException e) { reportNoSetterFound(); - } finally { - if (writeMethod != null) { - changer.safelyDisableAccess(writeMethod); - } } reportNoSetterFound(); diff --git a/src/main/java/org/mockito/internal/util/reflection/FieldCopier.java b/src/main/java/org/mockito/internal/util/reflection/FieldCopier.java deleted file mode 100644 index 67f7203944..0000000000 --- a/src/main/java/org/mockito/internal/util/reflection/FieldCopier.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2007 Mockito contributors - * This program is made available under the terms of the MIT License. - */ -package org.mockito.internal.util.reflection; - -import java.lang.reflect.Field; - -public class FieldCopier { - - public void copyValue(T from, T to, Field field) throws IllegalAccessException { - Object value = field.get(from); - field.set(to, value); - } -} diff --git a/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java b/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java index a5c5d708b5..53bc8a1f6a 100644 --- a/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java +++ b/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java @@ -4,9 +4,10 @@ */ package org.mockito.internal.util.reflection; -import static java.lang.reflect.Modifier.isStatic; - -import static org.mockito.internal.util.reflection.FieldSetter.setField; +import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.internal.util.MockUtil; +import org.mockito.plugins.MemberAccessor; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -17,8 +18,7 @@ import java.util.Comparator; import java.util.List; -import org.mockito.exceptions.base.MockitoException; -import org.mockito.internal.util.MockUtil; +import static java.lang.reflect.Modifier.isStatic; /** * Initialize a field with type instance if a default constructor can be found. @@ -87,9 +87,6 @@ private FieldInitializer(Object fieldOwner, Field field, ConstructorInstantiator * @return Actual field instance. */ public FieldInitializationReport initialize() { - final AccessibilityChanger changer = new AccessibilityChanger(); - changer.enableAccess(field); - try { return acquireFieldInstance(); } catch (IllegalAccessException e) { @@ -100,8 +97,6 @@ public FieldInitializationReport initialize() { + field.getType().getSimpleName() + "'", e); - } finally { - changer.safelyDisableAccess(field); } } @@ -142,7 +137,8 @@ private void checkNotEnum(Field field) { } private FieldInitializationReport acquireFieldInstance() throws IllegalAccessException { - Object fieldInstance = field.get(fieldOwner); + final MemberAccessor accessor = Plugins.getMemberAccessor(); + Object fieldInstance = accessor.get(field, fieldOwner); if (fieldInstance != null) { return new FieldInitializationReport(fieldInstance, false, false); } @@ -198,17 +194,15 @@ static class NoArgConstructorInstantiator implements ConstructorInstantiator { } public FieldInitializationReport instantiate() { - final AccessibilityChanger changer = new AccessibilityChanger(); - Constructor constructor = null; + final MemberAccessor invoker = Plugins.getMemberAccessor(); try { - constructor = field.getType().getDeclaredConstructor(); - changer.enableAccess(constructor); + Constructor constructor = field.getType().getDeclaredConstructor(); final Object[] noArg = new Object[0]; - Object newFieldInstance = constructor.newInstance(noArg); - setField(testClass, field, newFieldInstance); + Object newFieldInstance = invoker.newInstance(constructor, noArg); + invoker.set(field, testClass, newFieldInstance); - return new FieldInitializationReport(field.get(testClass), true, false); + return new FieldInitializationReport(invoker.get(field, testClass), true, false); } catch (NoSuchMethodException e) { throw new MockitoException( "the type '" @@ -230,10 +224,6 @@ public FieldInitializationReport instantiate() { throw new MockitoException( "IllegalAccessException (see the stack trace for cause): " + e.toString(), e); - } finally { - if (constructor != null) { - changer.safelyDisableAccess(constructor); - } } } } @@ -289,18 +279,14 @@ private int countMockableParams(Constructor constructor) { } public FieldInitializationReport instantiate() { - final AccessibilityChanger changer = new AccessibilityChanger(); - Constructor constructor = null; + final MemberAccessor accessor = Plugins.getMemberAccessor(); + Constructor constructor = biggestConstructor(field.getType()); + final Object[] args = argResolver.resolveTypeInstances(constructor.getParameterTypes()); try { - constructor = biggestConstructor(field.getType()); - changer.enableAccess(constructor); - - final Object[] args = - argResolver.resolveTypeInstances(constructor.getParameterTypes()); - Object newFieldInstance = constructor.newInstance(args); - setField(testClass, field, newFieldInstance); + Object newFieldInstance = accessor.newInstance(constructor, args); + accessor.set(field, testClass, newFieldInstance); - return new FieldInitializationReport(field.get(testClass), false, true); + return new FieldInitializationReport(accessor.get(field, testClass), false, true); } catch (IllegalArgumentException e) { throw new MockitoException( "internal error : argResolver provided incorrect types for constructor " @@ -323,10 +309,6 @@ public FieldInitializationReport instantiate() { throw new MockitoException( "IllegalAccessException (see the stack trace for cause): " + e.toString(), e); - } finally { - if (constructor != null) { - changer.safelyDisableAccess(constructor); - } } } diff --git a/src/main/java/org/mockito/internal/util/reflection/FieldReader.java b/src/main/java/org/mockito/internal/util/reflection/FieldReader.java index 707eee6515..e202c714a0 100644 --- a/src/main/java/org/mockito/internal/util/reflection/FieldReader.java +++ b/src/main/java/org/mockito/internal/util/reflection/FieldReader.java @@ -7,17 +7,18 @@ import java.lang.reflect.Field; import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.plugins.MemberAccessor; public class FieldReader { final Object target; final Field field; - final AccessibilityChanger changer = new AccessibilityChanger(); + final MemberAccessor accessor = Plugins.getMemberAccessor(); public FieldReader(Object target, Field field) { this.target = target; this.field = field; - changer.enableAccess(field); } public boolean isNull() { @@ -26,7 +27,7 @@ public boolean isNull() { public Object read() { try { - return field.get(target); + return accessor.get(field, target); } catch (Exception e) { throw new MockitoException( "Cannot read state from field: " + field + ", on instance: " + target); diff --git a/src/main/java/org/mockito/internal/util/reflection/FieldSetter.java b/src/main/java/org/mockito/internal/util/reflection/FieldSetter.java deleted file mode 100644 index 46f906d9bf..0000000000 --- a/src/main/java/org/mockito/internal/util/reflection/FieldSetter.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2007 Mockito contributors - * This program is made available under the terms of the MIT License. - */ -package org.mockito.internal.util.reflection; - -import java.lang.reflect.Field; - -public class FieldSetter { - - private FieldSetter() {} - - public static void setField(Object target, Field field, Object value) { - AccessibilityChanger changer = new AccessibilityChanger(); - changer.enableAccess(field); - try { - field.set(target, value); - } catch (IllegalAccessException e) { - throw new RuntimeException( - "Access not authorized on field '" - + field - + "' of object '" - + target - + "' with value: '" - + value - + "'", - e); - } catch (IllegalArgumentException e) { - throw new RuntimeException( - "Wrong argument on field '" - + field - + "' of object '" - + target - + "' with value: '" - + value - + "', \n" - + "reason : " - + e.getMessage(), - e); - } - changer.safelyDisableAccess(field); - } -} diff --git a/src/main/java/org/mockito/internal/util/reflection/InstanceField.java b/src/main/java/org/mockito/internal/util/reflection/InstanceField.java index 5585874f46..03cd02c051 100644 --- a/src/main/java/org/mockito/internal/util/reflection/InstanceField.java +++ b/src/main/java/org/mockito/internal/util/reflection/InstanceField.java @@ -4,12 +4,13 @@ */ package org.mockito.internal.util.reflection; -import static org.mockito.internal.util.reflection.FieldSetter.setField; - import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.Checks; +import org.mockito.plugins.MemberAccessor; /** * Represents an accessible instance field. @@ -47,10 +48,14 @@ public Object read() { * Set the given value to the field of this instance. * * @param value The value that should be written to the field. - * @see FieldSetter */ public void set(Object value) { - setField(instance, field, value); + MemberAccessor accessor = Plugins.getMemberAccessor(); + try { + accessor.set(field, instance, value); + } catch (IllegalAccessException e) { + throw new MockitoException("Access to " + field + " was denied", e); + } } /** diff --git a/src/main/java/org/mockito/internal/util/reflection/InstrumentationMemberAccessor.java b/src/main/java/org/mockito/internal/util/reflection/InstrumentationMemberAccessor.java new file mode 100644 index 0000000000..c74c79efa6 --- /dev/null +++ b/src/main/java/org/mockito/internal/util/reflection/InstrumentationMemberAccessor.java @@ -0,0 +1,363 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.util.reflection; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.agent.ByteBuddyAgent; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodCall; +import org.mockito.exceptions.base.MockitoInitializationException; +import org.mockito.plugins.MemberAccessor; + +import java.lang.instrument.Instrumentation; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.*; +import java.util.*; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.mockito.internal.util.StringUtil.join; + +class InstrumentationMemberAccessor implements MemberAccessor { + + private static final Map, Class> WRAPPERS = new HashMap<>(); + + private static final Instrumentation INSTRUMENTATION; + private static final Dispatcher DISPATCHER; + + private static final Throwable INITIALIZATION_ERROR; + + static { + WRAPPERS.put(boolean.class, Boolean.class); + WRAPPERS.put(byte.class, Byte.class); + WRAPPERS.put(short.class, Short.class); + WRAPPERS.put(char.class, Character.class); + WRAPPERS.put(int.class, Integer.class); + WRAPPERS.put(long.class, Long.class); + WRAPPERS.put(float.class, Float.class); + WRAPPERS.put(double.class, Double.class); + Instrumentation instrumentation; + Dispatcher dispatcher; + Throwable throwable; + try { + instrumentation = ByteBuddyAgent.install(); + // We need to generate a dispatcher instance that is located in a distinguished class + // loader to create a unique (unnamed) module to which we can open other packages to. + // This way, we assure that classes within Mockito's module (which might be a shared, + // unnamed module) do not face escalated privileges where tests might pass that would + // otherwise fail without Mockito's opening. + dispatcher = + new ByteBuddy() + .subclass(Dispatcher.class) + .method(named("getLookup")) + .intercept(MethodCall.invoke(MethodHandles.class.getMethod("lookup"))) + .method(named("getModule")) + .intercept( + MethodCall.invoke(Class.class.getMethod("getModule")) + .onMethodCall( + MethodCall.invoke( + Object.class.getMethod("getClass")))) + .method(named("setAccessible")) + .intercept( + MethodCall.invoke( + AccessibleObject.class.getMethod( + "setAccessible", boolean.class)) + .onArgument(0) + .withArgument(1)) + .make() + .load( + InstrumentationMemberAccessor.class.getClassLoader(), + ClassLoadingStrategy.Default.WRAPPER) + .getLoaded() + .getConstructor() + .newInstance(); + throwable = null; + } catch (Throwable t) { + instrumentation = null; + dispatcher = null; + throwable = t; + } + INSTRUMENTATION = instrumentation; + DISPATCHER = dispatcher; + INITIALIZATION_ERROR = throwable; + } + + private final MethodHandle getModule, isOpen, redefineModule, privateLookupIn; + + InstrumentationMemberAccessor() { + if (INITIALIZATION_ERROR != null) { + throw new MockitoInitializationException( + join( + "Could not initialize the Mockito instrumentation member accessor", + "", + "This is unexpected on JVMs from Java 9 or later - possibly, the instrumentation API could not be resolved"), + INITIALIZATION_ERROR); + } + try { + Class module = Class.forName("java.lang.Module"); + getModule = + MethodHandles.publicLookup() + .findVirtual(Class.class, "getModule", MethodType.methodType(module)); + isOpen = + MethodHandles.publicLookup() + .findVirtual( + module, + "isOpen", + MethodType.methodType(boolean.class, String.class, module)); + redefineModule = + MethodHandles.publicLookup() + .findVirtual( + Instrumentation.class, + "redefineModule", + MethodType.methodType( + void.class, + module, + Set.class, + Map.class, + Map.class, + Set.class, + Map.class)); + privateLookupIn = + MethodHandles.publicLookup() + .findStatic( + MethodHandles.class, + "privateLookupIn", + MethodType.methodType( + MethodHandles.Lookup.class, + Class.class, + MethodHandles.Lookup.class)); + } catch (Throwable t) { + throw new MockitoInitializationException( + "Could not resolve instrumentation invoker", t); + } + } + + @Override + public Object newInstance(Constructor constructor, Object... arguments) + throws InstantiationException, InvocationTargetException { + if (Modifier.isAbstract(constructor.getDeclaringClass().getModifiers())) { + throw new InstantiationException( + "Cannot instantiate abstract " + constructor.getDeclaringClass().getTypeName()); + } + assureArguments(constructor, null, null, arguments, constructor.getParameterTypes()); + try { + Object module = getModule.bindTo(constructor.getDeclaringClass()).invokeWithArguments(); + String packageName = constructor.getDeclaringClass().getPackage().getName(); + assureOpen(module, packageName); + MethodHandle handle = + ((MethodHandles.Lookup) + privateLookupIn.invokeExact( + constructor.getDeclaringClass(), + DISPATCHER.getLookup())) + .unreflectConstructor(constructor); + try { + return handle.invokeWithArguments(arguments); + } catch (Throwable t) { + throw new InvocationTargetException(t); + } + } catch (InvocationTargetException e) { + throw e; + } catch (Throwable t) { + throw new IllegalStateException( + "Could not construct " + + constructor + + " with arguments " + + Arrays.toString(arguments), + t); + } + } + + @Override + public Object invoke(Method method, Object target, Object... arguments) + throws InvocationTargetException { + assureArguments( + method, + Modifier.isStatic(method.getModifiers()) ? null : target, + method.getDeclaringClass(), + arguments, + method.getParameterTypes()); + try { + Object module = getModule.bindTo(method.getDeclaringClass()).invokeWithArguments(); + String packageName = method.getDeclaringClass().getPackage().getName(); + assureOpen(module, packageName); + MethodHandle handle = + ((MethodHandles.Lookup) + privateLookupIn.invokeExact( + method.getDeclaringClass(), DISPATCHER.getLookup())) + .unreflect(method); + if (!Modifier.isStatic(method.getModifiers())) { + handle = handle.bindTo(target); + } + try { + return handle.invokeWithArguments(arguments); + } catch (Throwable t) { + throw new InvocationTargetException(t); + } + } catch (InvocationTargetException e) { + throw e; + } catch (Throwable t) { + throw new IllegalStateException( + "Could not invoke " + + method + + " on " + + target + + " with arguments " + + Arrays.toString(arguments), + t); + } + } + + @Override + public Object get(Field field, Object target) { + assureArguments( + field, + Modifier.isStatic(field.getModifiers()) ? null : target, + field.getDeclaringClass(), + new Object[0], + new Class[0]); + try { + Object module = getModule.bindTo(field.getDeclaringClass()).invokeWithArguments(); + String packageName = field.getDeclaringClass().getPackage().getName(); + assureOpen(module, packageName); + MethodHandle handle = + ((MethodHandles.Lookup) + privateLookupIn.invokeExact( + field.getDeclaringClass(), DISPATCHER.getLookup())) + .unreflectGetter(field); + if (!Modifier.isStatic(field.getModifiers())) { + handle = handle.bindTo(target); + } + return handle.invokeWithArguments(); + } catch (Throwable t) { + throw new IllegalStateException("Could not read " + field + " on " + target, t); + } + } + + @Override + public void set(Field field, Object target, Object value) throws IllegalAccessException { + assureArguments( + field, + Modifier.isStatic(field.getModifiers()) ? null : target, + field.getDeclaringClass(), + new Object[] {value}, + new Class[] {field.getType()}); + boolean illegalAccess = false; + try { + Object module = getModule.bindTo(field.getDeclaringClass()).invokeWithArguments(); + String packageName = field.getDeclaringClass().getPackage().getName(); + assureOpen(module, packageName); + // Method handles do not allow setting final fields where setAccessible(true) + // is required before unreflecting. + boolean isFinal; + if (Modifier.isFinal(field.getModifiers())) { + isFinal = true; + try { + DISPATCHER.setAccessible(field, true); + } catch (Throwable ignored) { + illegalAccess = + true; // To distinguish from propagated illegal access exception. + throw new IllegalAccessException( + "Could not make final field " + field + " accessible"); + } + } else { + isFinal = false; + } + try { + MethodHandle handle = + ((MethodHandles.Lookup) + privateLookupIn.invokeExact( + field.getDeclaringClass(), DISPATCHER.getLookup())) + .unreflectSetter(field); + if (!Modifier.isStatic(field.getModifiers())) { + handle = handle.bindTo(target); + } + handle.invokeWithArguments(value); + } finally { + if (isFinal) { + DISPATCHER.setAccessible(field, false); + } + } + } catch (Throwable t) { + if (illegalAccess) { + throw (IllegalAccessException) t; + } else { + throw new IllegalStateException("Could not read " + field + " on " + target, t); + } + } + } + + private void assureOpen(Object module, String packageName) throws Throwable { + if (!(Boolean) isOpen.invokeWithArguments(module, packageName, DISPATCHER.getModule())) { + redefineModule + .bindTo(INSTRUMENTATION) + .invokeWithArguments( + module, + Collections.emptySet(), + Collections.emptyMap(), + Collections.singletonMap( + packageName, Collections.singleton(DISPATCHER.getModule())), + Collections.emptySet(), + Collections.emptyMap()); + } + } + + private static void assureArguments( + AccessibleObject target, + Object owner, + Class type, + Object[] values, + Class[] types) { + if (owner != null) { + if (!type.isAssignableFrom(owner.getClass())) { + throw new IllegalArgumentException("Cannot access " + target + " on " + owner); + } + } + if (types.length != values.length) { + throw new IllegalArgumentException( + "Incorrect number of arguments for " + + target + + ": expected " + + types.length + + " but recevied " + + values.length); + } + for (int index = 0; index < values.length; index++) { + if (values[index] == null) { + if (types[index].isPrimitive()) { + throw new IllegalArgumentException( + "Cannot assign null to primitive type " + + types[index].getTypeName() + + " for " + + index + + " parameter of " + + target); + } + } else { + Class resolved = WRAPPERS.getOrDefault(types[index], types[index]); + if (!resolved.isAssignableFrom(values[index].getClass())) { + throw new IllegalArgumentException( + "Cannot assign value of type " + + values[index].getClass() + + " to " + + resolved + + " for " + + index + + " parameter of " + + target); + } + } + } + } + + public interface Dispatcher { + + MethodHandles.Lookup getLookup(); + + Object getModule(); + + void setAccessible(AccessibleObject target, boolean value); + } +} diff --git a/src/main/java/org/mockito/internal/util/reflection/LenientCopyTool.java b/src/main/java/org/mockito/internal/util/reflection/LenientCopyTool.java index ccdd923835..95b30198db 100644 --- a/src/main/java/org/mockito/internal/util/reflection/LenientCopyTool.java +++ b/src/main/java/org/mockito/internal/util/reflection/LenientCopyTool.java @@ -4,13 +4,15 @@ */ package org.mockito.internal.util.reflection; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.plugins.MemberAccessor; + import java.lang.reflect.Field; import java.lang.reflect.Modifier; -@SuppressWarnings("unchecked") public class LenientCopyTool { - FieldCopier fieldCopier = new FieldCopier(); + MemberAccessor accessor = Plugins.getMemberAccessor(); public void copyToMock(T from, T mock) { copy(from, mock, from.getClass()); @@ -35,14 +37,11 @@ private void copyValues(T from, T mock, Class classFrom) { if (Modifier.isStatic(field.getModifiers())) { continue; } - AccessibilityChanger accessibilityChanger = new AccessibilityChanger(); try { - accessibilityChanger.enableAccess(field); - fieldCopier.copyValue(from, mock, field); + Object value = accessor.get(field, from); + accessor.set(field, mock, value); } catch (Throwable t) { // Ignore - be lenient - if some field cannot be copied then let's be it - } finally { - accessibilityChanger.safelyDisableAccess(field); } } } diff --git a/src/main/java/org/mockito/internal/util/reflection/ModuleMemberAccessor.java b/src/main/java/org/mockito/internal/util/reflection/ModuleMemberAccessor.java new file mode 100644 index 0000000000..8be77860ad --- /dev/null +++ b/src/main/java/org/mockito/internal/util/reflection/ModuleMemberAccessor.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.util.reflection; + +import net.bytebuddy.ClassFileVersion; +import org.mockito.plugins.MemberAccessor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ModuleMemberAccessor implements MemberAccessor { + + private final MemberAccessor delegate; + + public ModuleMemberAccessor() { + if (ClassFileVersion.ofThisVm().isAtLeast(ClassFileVersion.JAVA_V9)) { + delegate = new InstrumentationMemberAccessor(); + } else { + delegate = new ReflectionMemberAccessor(); + } + } + + @Override + public Object newInstance(Constructor constructor, Object... arguments) + throws InstantiationException, InvocationTargetException, IllegalAccessException { + return delegate.newInstance(constructor, arguments); + } + + @Override + public Object invoke(Method method, Object target, Object... arguments) + throws InvocationTargetException, IllegalAccessException { + return delegate.invoke(method, target, arguments); + } + + @Override + public Object get(Field field, Object target) throws IllegalAccessException { + return delegate.get(field, target); + } + + @Override + public void set(Field field, Object target, Object value) throws IllegalAccessException { + delegate.set(field, target, value); + } +} diff --git a/src/main/java/org/mockito/internal/util/reflection/ReflectionMemberAccessor.java b/src/main/java/org/mockito/internal/util/reflection/ReflectionMemberAccessor.java new file mode 100644 index 0000000000..e61a5bcc02 --- /dev/null +++ b/src/main/java/org/mockito/internal/util/reflection/ReflectionMemberAccessor.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2007 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.util.reflection; + +import org.mockito.plugins.MemberAccessor; + +import java.lang.reflect.*; +import java.util.Arrays; + +public class ReflectionMemberAccessor implements MemberAccessor { + + @Override + public Object newInstance(Constructor constructor, Object... arguments) + throws InstantiationException, InvocationTargetException, IllegalAccessException { + silentSetAccessible(constructor, true); + try { + return constructor.newInstance(arguments); + } catch (InvocationTargetException + | IllegalAccessException + | InstantiationException + | IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException( + "Failed to invoke " + constructor + " with " + Arrays.toString(arguments), e); + } finally { + silentSetAccessible(constructor, false); + } + } + + @Override + public Object invoke(Method method, Object target, Object... arguments) + throws InvocationTargetException, IllegalAccessException { + silentSetAccessible(method, true); + try { + return method.invoke(target, arguments); + } catch (InvocationTargetException | IllegalAccessException | IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Could not invoke " + method + " on " + target, e); + } finally { + silentSetAccessible(method, false); + } + } + + @Override + public Object get(Field field, Object target) throws IllegalAccessException { + silentSetAccessible(field, true); + try { + return field.get(target); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Could not read " + field + " from " + target); + } finally { + silentSetAccessible(field, false); + } + } + + @Override + public void set(Field field, Object target, Object value) throws IllegalAccessException { + silentSetAccessible(field, true); + try { + field.set(target, value); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Could not write " + field + " to " + target, e); + } finally { + silentSetAccessible(field, false); + } + } + + private static void silentSetAccessible(AccessibleObject object, boolean value) { + try { + object.setAccessible(value); + } catch (Exception ignored) { + } + } +} diff --git a/src/main/java/org/mockito/plugins/MemberAccessor.java b/src/main/java/org/mockito/plugins/MemberAccessor.java new file mode 100644 index 0000000000..081814e1db --- /dev/null +++ b/src/main/java/org/mockito/plugins/MemberAccessor.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.plugins; + +import org.mockito.Incubating; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * A member accessor is responsible for invoking methods, constructors and for setting + * and reading field values. + */ +@Incubating +public interface MemberAccessor { + + Object newInstance(Constructor constructor, Object... arguments) + throws InstantiationException, InvocationTargetException, IllegalAccessException; + + Object invoke(Method method, Object target, Object... arguments) + throws InvocationTargetException, IllegalAccessException; + + Object get(Field field, Object target) throws IllegalAccessException; + + void set(Field field, Object target, Object value) throws IllegalAccessException; +} diff --git a/src/main/java/org/mockito/plugins/MockMaker.java b/src/main/java/org/mockito/plugins/MockMaker.java index df3ff4210d..fbdaf1d0e6 100644 --- a/src/main/java/org/mockito/plugins/MockMaker.java +++ b/src/main/java/org/mockito/plugins/MockMaker.java @@ -5,11 +5,16 @@ package org.mockito.plugins; import org.mockito.Incubating; +import org.mockito.MockedConstruction; import org.mockito.exceptions.base.MockitoException; import org.mockito.invocation.MockHandler; import org.mockito.mock.MockCreationSettings; -import static org.mockito.internal.util.StringUtil.*; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import static org.mockito.internal.util.StringUtil.join; /** * The facility to create mocks. @@ -69,6 +74,25 @@ public interface MockMaker { */ T createMock(MockCreationSettings settings, MockHandler handler); + /** + * By implementing this method, a mock maker can optionally support the creation of spies where all fields + * are set within a constructor. This avoids problems when creating spies of classes that declare + * effectively final instance fields where setting field values from outside the constructor is prohibited. + * + * @param settings Mock creation settings like type to mock, extra interfaces and so on. + * @param handler See {@link org.mockito.invocation.MockHandler}. + * Do not provide your own implementation at this time. Make sure your implementation of + * {@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 + * @since 3.5.0 + */ + default Optional createSpy( + MockCreationSettings settings, MockHandler handler, T instance) { + return Optional.empty(); + } + /** * Returns the handler for the {@code mock}. Do not provide your own implementations at this time * because the work on the {@link MockHandler} api is not completed. @@ -141,6 +165,39 @@ default StaticMockControl createStaticMock( "Note that Mockito's inline mock maker is not supported on Android.")); } + /** + * If you want to provide your own implementation of {@code MockMaker} this method should: + *
    + *
  • Intercept all constructions of the specified type in the current thread
  • + *
  • Only intercept the construction after being enabled.
  • + *
  • Stops the interception when disabled.
  • + *
+ * + * @param settingsFactory Factory for mock creation settings like type to mock, extra interfaces and so on. + * @param handlerFactory Factory for settings. See {@link org.mockito.invocation.MockHandler}. + * Do not provide your own implementation at this time. Make sure your implementation of + * {@link #getHandler(Object)} will return this instance. + * @param Type of the mock to return, actually the settings.getTypeToMock. + * @return A control for the mocked construction. + * @since 3.5.0 + */ + @Incubating + default ConstructionMockControl createConstructionMock( + Class type, + Function> settingsFactory, + Function> handlerFactory, + MockedConstruction.MockInitializer mockInitializer) { + throw new MockitoException( + join( + "The used MockMaker " + + getClass().getSimpleName() + + " does not support the creation of construction mocks", + "", + "Mockito's inline mock maker supports construction mocks based on the Instrumentation API.", + "You can simply enable this mock mode, by placing the 'mockito-inline' artifact where you are currently using 'mockito-core'.", + "Note that Mockito's inline mock maker is not supported on Android.")); + } + /** * Carries the mockability information * @@ -168,4 +225,16 @@ interface StaticMockControl { void disable(); } + + @Incubating + interface ConstructionMockControl { + + Class getType(); + + void enable(); + + void disable(); + + List getMocks(); + } } diff --git a/src/test/java/org/mockito/MockitoTest.java b/src/test/java/org/mockito/MockitoTest.java index 23df60e08e..b41dd2091b 100644 --- a/src/test/java/org/mockito/MockitoTest.java +++ b/src/test/java/org/mockito/MockitoTest.java @@ -71,6 +71,12 @@ public void shouldGiveExplantionOnStaticMockingWithoutInlineMockMaker() { Mockito.mockStatic(Object.class); } + @SuppressWarnings({"CheckReturnValue", "MockitoUsage"}) + @Test(expected = MockitoException.class) + public void shouldGiveExplantionOnConstructionMockingWithoutInlineMockMaker() { + Mockito.mockConstruction(Object.class); + } + @Test public void shouldStartingMockSettingsContainDefaultBehavior() { // when diff --git a/src/test/java/org/mockito/internal/configuration/MockAnnotationProcessorTest.java b/src/test/java/org/mockito/internal/configuration/MockAnnotationProcessorTest.java index 906d134d61..9c4ef88d88 100644 --- a/src/test/java/org/mockito/internal/configuration/MockAnnotationProcessorTest.java +++ b/src/test/java/org/mockito/internal/configuration/MockAnnotationProcessorTest.java @@ -26,24 +26,28 @@ public class MockAnnotationProcessorTest { @Test public void testNonGeneric() throws Exception { Class type = - MockAnnotationProcessor.inferStaticMock( + MockAnnotationProcessor.inferParameterizedType( MockAnnotationProcessorTest.class .getDeclaredField("nonGeneric") .getGenericType(), - "nonGeneric"); + "nonGeneric", + "Sample"); assertThat(type).isEqualTo(Void.class); } @Test(expected = MockitoException.class) public void testGeneric() throws Exception { - MockAnnotationProcessor.inferStaticMock( + MockAnnotationProcessor.inferParameterizedType( MockAnnotationProcessorTest.class.getDeclaredField("generic").getGenericType(), - "generic"); + "generic", + "Sample"); } @Test(expected = MockitoException.class) public void testRaw() throws Exception { - MockAnnotationProcessor.inferStaticMock( - MockAnnotationProcessorTest.class.getDeclaredField("raw").getGenericType(), "raw"); + MockAnnotationProcessor.inferParameterizedType( + MockAnnotationProcessorTest.class.getDeclaredField("raw").getGenericType(), + "raw", + "Sample"); } } diff --git a/src/test/java/org/mockito/internal/creation/bytebuddy/AbstractByteBuddyMockMakerTest.java b/src/test/java/org/mockito/internal/creation/bytebuddy/AbstractByteBuddyMockMakerTest.java index f5b807a1ce..3ddc1817ba 100644 --- a/src/test/java/org/mockito/internal/creation/bytebuddy/AbstractByteBuddyMockMakerTest.java +++ b/src/test/java/org/mockito/internal/creation/bytebuddy/AbstractByteBuddyMockMakerTest.java @@ -165,7 +165,7 @@ public void instantiate_fine_when_objenesis_on_the_classpath() throws Exception classpath_with_objenesis); // when - mock_maker_class_loaded_fine_until.newInstance(); + mock_maker_class_loaded_fine_until.getConstructor().newInstance(); // then everything went fine } diff --git a/src/test/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMakerTest.java b/src/test/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMakerTest.java index 8ec5318748..bfc16b4d15 100644 --- a/src/test/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMakerTest.java +++ b/src/test/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMakerTest.java @@ -11,11 +11,7 @@ import static org.assertj.core.api.Assertions.fail; import static org.junit.Assume.assumeTrue; -import java.util.HashMap; -import java.util.List; -import java.util.Observable; -import java.util.Observer; -import java.util.Set; +import java.util.*; import java.util.concurrent.Callable; import java.util.regex.Pattern; @@ -57,6 +53,39 @@ public void should_create_mock_from_final_class() throws Exception { assertThat(proxy.foo()).isEqualTo("bar"); } + @Test + public void should_create_mock_from_final_spy() throws Exception { + MockCreationSettings settings = settingsFor(FinalSpy.class); + Optional proxy = + mockMaker.createSpy( + settings, + new MockHandlerImpl<>(settings), + new FinalSpy("value", true, (byte) 1, (short) 1, (char) 1, 1, 1L, 1f, 1d)); + assertThat(proxy) + .hasValueSatisfying( + spy -> { + assertThat(spy.aString).isEqualTo("value"); + assertThat(spy.aBoolean).isTrue(); + assertThat(spy.aByte).isEqualTo((byte) 1); + assertThat(spy.aShort).isEqualTo((short) 1); + assertThat(spy.aChar).isEqualTo((char) 1); + assertThat(spy.anInt).isEqualTo(1); + assertThat(spy.aLong).isEqualTo(1L); + assertThat(spy.aFloat).isEqualTo(1f); + assertThat(spy.aDouble).isEqualTo(1d); + }); + } + + @Test + public void should_create_mock_from_non_constructable_class() throws Exception { + MockCreationSettings settings = + settingsFor(NonConstructableClass.class); + NonConstructableClass proxy = + mockMaker.createMock( + settings, new MockHandlerImpl(settings)); + assertThat(proxy.foo()).isEqualTo("bar"); + } + @Test public void should_create_mock_from_final_class_in_the_JDK() throws Exception { MockCreationSettings settings = settingsFor(Pattern.class); @@ -406,6 +435,51 @@ public String foo() { } } + private static final class FinalSpy { + + private final String aString; + private final boolean aBoolean; + private final byte aByte; + private final short aShort; + private final char aChar; + private final int anInt; + private final long aLong; + private final float aFloat; + private final double aDouble; + + private FinalSpy( + String aString, + boolean aBoolean, + byte aByte, + short aShort, + char aChar, + int anInt, + long aLong, + float aFloat, + double aDouble) { + this.aString = aString; + this.aBoolean = aBoolean; + this.aByte = aByte; + this.aShort = aShort; + this.aChar = aChar; + this.anInt = anInt; + this.aLong = aLong; + this.aFloat = aFloat; + this.aDouble = aDouble; + } + } + + private static class NonConstructableClass { + + private NonConstructableClass() { + throw new AssertionError(); + } + + public String foo() { + return "foo"; + } + } + private enum EnumClass { INSTANCE; diff --git a/src/test/java/org/mockito/internal/util/reflection/AccessibilityChangerTest.java b/src/test/java/org/mockito/internal/util/reflection/AccessibilityChangerTest.java deleted file mode 100644 index 032cb53770..0000000000 --- a/src/test/java/org/mockito/internal/util/reflection/AccessibilityChangerTest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2007 Mockito contributors - * This program is made available under the terms of the MIT License. - */ -package org.mockito.internal.util.reflection; - -import static org.mockitoutil.VmArgAssumptions.assumeVmArgPresent; - -import java.lang.reflect.Field; -import java.util.Observable; - -import org.junit.Test; - -public class AccessibilityChangerTest { - - @SuppressWarnings("unused") - private Observable whatever; - - @Test - public void should_enable_and_safely_disable() throws Exception { - AccessibilityChanger changer = new AccessibilityChanger(); - changer.enableAccess(field("whatever")); - changer.safelyDisableAccess(field("whatever")); - } - - @Test(expected = java.lang.AssertionError.class) - public void safelyDisableAccess_should_fail_when_enableAccess_not_called() throws Exception { - assumeVmArgPresent("-ea"); - new AccessibilityChanger().safelyDisableAccess(field("whatever")); - } - - private Field field(String fieldName) throws NoSuchFieldException { - return this.getClass().getDeclaredField(fieldName); - } -} diff --git a/src/test/java/org/mockito/internal/util/reflection/LenientCopyToolTest.java b/src/test/java/org/mockito/internal/util/reflection/LenientCopyToolTest.java index 76ebd337b9..e70d8aa3c4 100644 --- a/src/test/java/org/mockito/internal/util/reflection/LenientCopyToolTest.java +++ b/src/test/java/org/mockito/internal/util/reflection/LenientCopyToolTest.java @@ -6,13 +6,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.mockito.Mockito.*; import java.lang.reflect.Field; import java.util.LinkedList; import org.junit.Test; +import org.mockito.plugins.MemberAccessor; import org.mockitoutil.TestBase; @SuppressWarnings("unchecked") @@ -127,36 +127,23 @@ public void shouldCopyValuesOfInheritedFields() throws Exception { assertEquals(((InheritMe) from).privateInherited, ((InheritMe) to).privateInherited); } - @Test - public void shouldEnableAndThenDisableAccessibility() throws Exception { - // given - Field privateField = SomeObject.class.getDeclaredField("privateField"); - assertFalse(privateField.isAccessible()); - - // when - tool.copyToMock(from, to); - - // then - privateField = SomeObject.class.getDeclaredField("privateField"); - assertFalse(privateField.isAccessible()); - } - @Test public void shouldContinueEvenIfThereAreProblemsCopyingSingleFieldValue() throws Exception { // given - tool.fieldCopier = mock(FieldCopier.class); + tool.accessor = mock(MemberAccessor.class); doNothing() - .doThrow(new IllegalAccessException()) + .doThrow(new IllegalStateException()) .doNothing() - .when(tool.fieldCopier) - .copyValue(anyObject(), anyObject(), any(Field.class)); + .when(tool.accessor) + .set(any(Field.class), anyObject(), anyObject()); // when tool.copyToMock(from, to); // then - verify(tool.fieldCopier, atLeast(3)).copyValue(any(), any(), any(Field.class)); + verify(tool.accessor, atLeast(3)).get(any(Field.class), any()); + verify(tool.accessor, atLeast(3)).set(any(Field.class), any(), any()); } @Test diff --git a/src/test/java/org/mockito/internal/util/reflection/MemberAccessorTest.java b/src/test/java/org/mockito/internal/util/reflection/MemberAccessorTest.java new file mode 100644 index 0000000000..ee390312ed --- /dev/null +++ b/src/test/java/org/mockito/internal/util/reflection/MemberAccessorTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.util.reflection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.plugins.MemberAccessor; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@RunWith(Parameterized.class) +public class MemberAccessorTest { + + @Parameterized.Parameters + public static Collection data() { + List data = new ArrayList<>(); + data.add(new Object[] {new ReflectionMemberAccessor()}); + data.add(new Object[] {new ModuleMemberAccessor()}); + return data; + } + + private final MemberAccessor accessor; + + public MemberAccessorTest(MemberAccessor accessor) { + this.accessor = accessor; + } + + @Test + public void test_read_field() throws Exception { + assertThat(accessor.get(Sample.class.getDeclaredField("field"), new Sample("foo"))) + .isEqualTo("foo"); + } + + @Test + public void test_read_static_field() throws Exception { + Sample.staticField = "foo"; + assertThat(accessor.get(Sample.class.getDeclaredField("staticField"), null)) + .isEqualTo("foo"); + } + + @Test + public void test_write_field() throws Exception { + Sample sample = new Sample("foo"); + accessor.set(Sample.class.getDeclaredField("field"), sample, "bar"); + assertThat(sample.field).isEqualTo("bar"); + } + + @Test + public void test_write_static_field() throws Exception { + Sample.staticField = "foo"; + accessor.set(Sample.class.getDeclaredField("staticField"), null, "bar"); + assertThat(Sample.staticField).isEqualTo("bar"); + } + + @Test + public void test_invoke() throws Exception { + assertThat( + accessor.invoke( + Sample.class.getDeclaredMethod("test", String.class), + new Sample(null), + "foo")) + .isEqualTo("foo"); + } + + @Test + public void test_invoke_invocation_exception() { + assertThatThrownBy( + () -> + accessor.invoke( + Sample.class.getDeclaredMethod("test", String.class), + new Sample(null), + "exception")) + .isInstanceOf(InvocationTargetException.class); + } + + @Test + public void test_invoke_illegal_arguments() { + assertThatThrownBy( + () -> + accessor.invoke( + Sample.class.getDeclaredMethod("test", String.class), + new Sample(null), + 42)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void test_new_instance() throws Exception { + assertThat(accessor.newInstance(Sample.class.getDeclaredConstructor(String.class), "foo")) + .isInstanceOf(Sample.class); + } + + @Test + public void test_new_instance_illegal_arguments() { + assertThatThrownBy( + () -> + accessor.newInstance( + Sample.class.getDeclaredConstructor(String.class), 42)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void test_new_instance_invocation_exception() { + assertThatThrownBy( + () -> + accessor.newInstance( + Sample.class.getDeclaredConstructor(String.class), + "exception")) + .isInstanceOf(InvocationTargetException.class); + } + + @Test + public void test_new_instance_instantiation_exception() { + assertThatThrownBy( + () -> accessor.newInstance(AbstractSample.class.getDeclaredConstructor())) + .isInstanceOf(InstantiationException.class); + } + + @Test + public void test_set_final_field() throws Exception { + Sample sample = new Sample("foo"); + accessor.set(Sample.class.getDeclaredField("finalField"), sample, "foo"); + assertThat(sample.finalField).isEqualTo("foo"); + } + + private static class Sample { + + private String field; + + private final String finalField = null; + + private static String staticField = "foo"; + + public Sample(String field) { + if ("exception".equals(field)) { + throw new RuntimeException(); + } + this.field = field; + } + + private String test(String value) { + if ("exception".equals(value)) { + throw new RuntimeException(); + } + return value; + } + } + + private abstract static class AbstractSample {} +} diff --git a/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java b/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java index 342007122c..2363fe6f35 100644 --- a/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java +++ b/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java @@ -41,6 +41,10 @@ public void pure_mockito_should_not_depend_bytecode_libraries() throws Exception pureMockitoAPIClasses.remove("org.mockito.internal.exceptions.stacktrace.StackTraceFilter"); pureMockitoAPIClasses.remove("org.mockito.internal.util.MockUtil"); + // Remove instrumentation-based member accessor which is optional. + pureMockitoAPIClasses.remove( + "org.mockito.internal.util.reflection.InstrumentationMemberAccessor"); + for (String pureMockitoAPIClass : pureMockitoAPIClasses) { checkDependency(classLoader_without_bytecode_libraries, pureMockitoAPIClass); } diff --git a/src/test/java/org/mockitousage/annotation/MockInjectionUsingSetterOrPropertyTest.java b/src/test/java/org/mockitousage/annotation/MockInjectionUsingSetterOrPropertyTest.java index 765c4b8d0a..0d2af98d67 100644 --- a/src/test/java/org/mockitousage/annotation/MockInjectionUsingSetterOrPropertyTest.java +++ b/src/test/java/org/mockitousage/annotation/MockInjectionUsingSetterOrPropertyTest.java @@ -12,6 +12,7 @@ import java.util.TreeSet; import org.assertj.core.api.Assertions; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; @@ -50,10 +51,19 @@ public class MockInjectionUsingSetterOrPropertyTest extends TestBase { @Spy private TreeSet searchTree = new TreeSet(); + private AutoCloseable session; + @Before public void enforces_new_instances() { - // initMocks called in TestBase Before method, so instances are not the same - MockitoAnnotations.openMocks(this); + // openMocks called in TestBase Before method, so instances are not the same + session = MockitoAnnotations.openMocks(this); + } + + @After + public void close_new_instances() throws Exception { + if (session != null) { + session.close(); + } } @Test diff --git a/src/test/java/org/mockitousage/spies/SpyingOnInterfacesTest.java b/src/test/java/org/mockitousage/spies/SpyingOnInterfacesTest.java index ec8d5933d2..b6fe514cde 100644 --- a/src/test/java/org/mockitousage/spies/SpyingOnInterfacesTest.java +++ b/src/test/java/org/mockitousage/spies/SpyingOnInterfacesTest.java @@ -106,7 +106,7 @@ public void shouldAllowSpyingOnDefaultMethod() throws Exception { .load(iFace.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); - Object object = spy(impl.newInstance()); + Object object = spy(impl.getConstructor().newInstance()); // when Assertions.assertThat(impl.getMethod("foo").invoke(object)).isEqualTo((Object) "bar"); diff --git a/src/test/java/org/mockitoutil/ClassLoaders.java b/src/test/java/org/mockitoutil/ClassLoaders.java index 2b3f51051d..308b309ec8 100644 --- a/src/test/java/org/mockitoutil/ClassLoaders.java +++ b/src/test/java/org/mockitoutil/ClassLoaders.java @@ -36,6 +36,8 @@ import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.plugins.MemberAccessor; import org.objenesis.Objenesis; import org.objenesis.ObjenesisStd; import org.objenesis.instantiator.ObjectInstantiator; @@ -167,9 +169,8 @@ Runnable reloadTaskInClassLoader(Runnable task) { continue; } if (declaredField.getType() == field.getType()) { // don't copy this - field.setAccessible(true); - declaredField.setAccessible(true); - declaredField.set(reloaded, field.get(task)); + MemberAccessor accessor = Plugins.getMemberAccessor(); + accessor.set(declaredField, reloaded, accessor.get(field, task)); } } diff --git a/subprojects/inline/src/main/resources/mockito-extensions/org.mockito.plugins.MemberAccessor b/subprojects/inline/src/main/resources/mockito-extensions/org.mockito.plugins.MemberAccessor new file mode 100644 index 0000000000..1422f9900b --- /dev/null +++ b/subprojects/inline/src/main/resources/mockito-extensions/org.mockito.plugins.MemberAccessor @@ -0,0 +1 @@ +member-accessor-module diff --git a/subprojects/inline/src/test/java/org/mockitoinline/ConstructionMockRuleTest.java b/subprojects/inline/src/test/java/org/mockitoinline/ConstructionMockRuleTest.java new file mode 100644 index 0000000000..e53202eb33 --- /dev/null +++ b/subprojects/inline/src/test/java/org/mockitoinline/ConstructionMockRuleTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockitoinline; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import static junit.framework.TestCase.*; + +public final class ConstructionMockRuleTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private MockedConstruction dummy; + + @Test + public void testConstructionMockSimple() { + assertNull(new Dummy().foo()); + } + + @Test + public void testConstructionMockCollection() { + assertEquals(0, dummy.constructed().size()); + Dummy mock = new Dummy(); + assertEquals(1, dummy.constructed().size()); + assertTrue(dummy.constructed().contains(mock)); + } + + static class Dummy { + + String foo() { + return "foo"; + } + } +} diff --git a/subprojects/inline/src/test/java/org/mockitoinline/ConstructionMockTest.java b/subprojects/inline/src/test/java/org/mockitoinline/ConstructionMockTest.java new file mode 100644 index 0000000000..dd5e46d494 --- /dev/null +++ b/subprojects/inline/src/test/java/org/mockitoinline/ConstructionMockTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockitoinline; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.exceptions.base.MockitoException; + +import static junit.framework.TestCase.*; +import static org.mockito.Mockito.*; + +public final class ConstructionMockTest { + + @Test + public void testConstructionMockSimple() { + assertEquals("foo", new Dummy().foo()); + try (MockedConstruction ignored = Mockito.mockConstruction(Dummy.class)) { + assertNull(new Dummy().foo()); + } + assertEquals("foo", new Dummy().foo()); + } + + @Test + public void testConstructionMockCollection() { + try (MockedConstruction dummy = Mockito.mockConstruction(Dummy.class)) { + assertEquals(0, dummy.constructed().size()); + Dummy mock = new Dummy(); + assertEquals(1, dummy.constructed().size()); + assertTrue(dummy.constructed().contains(mock)); + } + } + + @Test + public void testConstructionMockDefaultAnswer() { + try (MockedConstruction ignored = Mockito.mockConstructionWithAnswer(Dummy.class, invocation -> "bar")) { + assertEquals("bar", new Dummy().foo()); + } + } + + @Test + public void testConstructionMockDefaultAnswerMultiple() { + try (MockedConstruction ignored = Mockito.mockConstructionWithAnswer(Dummy.class, invocation -> "bar", invocation -> "qux")) { + assertEquals("bar", new Dummy().foo()); + assertEquals("qux", new Dummy().foo()); + assertEquals("qux", new Dummy().foo()); + } + } + + @Test + public void testConstructionMockPrepared() { + try (MockedConstruction ignored = Mockito.mockConstruction(Dummy.class, (mock, context) -> when(mock.foo()).thenReturn("bar"))) { + assertEquals("bar", new Dummy().foo()); + } + } + + + @Test + public void testConstructionMockContext() { + try (MockedConstruction ignored = Mockito.mockConstruction(Dummy.class, (mock, context) -> { + assertEquals(1, context.getCount()); + assertEquals(Collections.singletonList("foobar"), context.arguments()); + assertEquals(mock.getClass().getDeclaredConstructor(String.class), context.constructor()); + when(mock.foo()).thenReturn("bar"); + })) { + assertEquals("bar", new Dummy("foobar").foo()); + } + } + + @Test + public void testConstructionMockDoesNotAffectDifferentThread() throws InterruptedException { + try (MockedConstruction ignored = Mockito.mockConstruction(Dummy.class)) { + Dummy dummy = new Dummy(); + when(dummy.foo()).thenReturn("bar"); + assertEquals("bar", dummy.foo()); + verify(dummy).foo(); + AtomicReference reference = new AtomicReference<>(); + Thread thread = new Thread(() -> reference.set(new Dummy().foo())); + thread.start(); + thread.join(); + assertEquals("foo", reference.get()); + when(dummy.foo()).thenReturn("bar"); + assertEquals("bar", dummy.foo()); + verify(dummy, times(2)).foo(); + } + } + + @Test + public void testConstructionMockCanCoexistWithMockInDifferentThread() throws InterruptedException { + try (MockedConstruction ignored = Mockito.mockConstruction(Dummy.class)) { + Dummy dummy = new Dummy(); + when(dummy.foo()).thenReturn("bar"); + assertEquals("bar", dummy.foo()); + verify(dummy).foo(); + AtomicReference reference = new AtomicReference<>(); + Thread thread = new Thread(() -> { + try (MockedConstruction ignored2 = Mockito.mockConstruction(Dummy.class)) { + Dummy other = new Dummy(); + when(other.foo()).thenReturn("qux"); + reference.set(other.foo()); + } + }); + thread.start(); + thread.join(); + assertEquals("qux", reference.get()); + assertEquals("bar", dummy.foo()); + verify(dummy, times(2)).foo(); + } + } + + @Test(expected = MockitoException.class) + public void testConstructionMockMustBeExclusiveInScopeWithinThread() { + try ( + MockedConstruction dummy = Mockito.mockConstruction(Dummy.class); + MockedConstruction duplicate = Mockito.mockConstruction(Dummy.class) + ) { + fail("Not supposed to allow duplicates"); + } + } + + @Test(expected = MockitoException.class) + public void testConstructionMockMustNotTargetAbstractClass() { + Mockito.mockConstruction(Runnable.class).close(); + } + + static class Dummy { + + + public Dummy() { + } + + public Dummy(String value) { + } + + String foo() { + return "foo"; + } + } +} diff --git a/subprojects/inline/src/test/java/org/mockitoinline/StaticMockRuleTest.java b/subprojects/inline/src/test/java/org/mockitoinline/StaticMockRuleTest.java new file mode 100644 index 0000000000..c54d6a6cf1 --- /dev/null +++ b/subprojects/inline/src/test/java/org/mockitoinline/StaticMockRuleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockitoinline; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import static junit.framework.TestCase.*; + +public final class StaticMockRuleTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private MockedStatic dummy; + + @Test + public void testStaticMockSimple() { + assertNull(Dummy.foo()); + } + + @Test + public void testStaticMockWithVerification() { + dummy.when(Dummy::foo).thenReturn("bar"); + assertEquals("bar", Dummy.foo()); + dummy.verify(Dummy::foo); + } + + static class Dummy { + + static String foo() { + return "foo"; + } + } +} diff --git a/subprojects/junitJupiterInlineMockMakerExtensionTest/src/test/java/org/mockitousage/NoExtendsTest.java b/subprojects/junitJupiterInlineMockMakerExtensionTest/src/test/java/org/mockitousage/NoExtendsTest.java index 57115fa979..06c14d2bb3 100644 --- a/subprojects/junitJupiterInlineMockMakerExtensionTest/src/test/java/org/mockitousage/NoExtendsTest.java +++ b/subprojects/junitJupiterInlineMockMakerExtensionTest/src/test/java/org/mockitousage/NoExtendsTest.java @@ -4,10 +4,9 @@ */ package org.mockitousage; -import java.util.UUID; - import org.junit.jupiter.api.Test; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.MockedStatic; import static org.assertj.core.api.Assertions.*; @@ -15,11 +14,29 @@ class NoExtendsTest { @Mock - private MockedStatic mock; + private MockedStatic staticMethod; + + @Mock + private MockedConstruction construction; + + @Test + void runsStaticMethods() { + assertThat(Dummy.foo()).isNull(); + } @Test - void runs() { - mock.when(UUID::randomUUID).thenReturn(new UUID(123, 456)); - assertThat(UUID.randomUUID()).isEqualTo(new UUID(123, 456)); + void runsConstruction() { + assertThat(new Dummy().bar()).isNull(); + } + + static class Dummy { + + static String foo() { + return "foo"; + } + + String bar() { + return "foo"; + } } } diff --git a/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleAccessTest.java b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleAccessTest.java new file mode 100644 index 0000000000..6382a253b8 --- /dev/null +++ b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleAccessTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.moduletest; + +import org.junit.Test; +import org.mockito.internal.util.reflection.ModuleMemberAccessor; +import org.mockito.internal.util.reflection.ReflectionMemberAccessor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; +import java.util.concurrent.Callable; + +import static junit.framework.TestCase.fail; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.moduletest.ModuleUtil.layer; +import static org.mockito.moduletest.ModuleUtil.modularJar; + +public class ModuleAccessTest { + + @Test + public void can_access_non_opened_module_with_module_member_accessor() throws Exception { + Path jar = modularJar(false, false, false); + ModuleLayer layer = layer(jar, false, true); + + ClassLoader loader = layer.findLoader("mockito.test"); + Class type = loader.loadClass("sample.MyCallable"); + + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader); + try { + Class moduleMemberAccessor = loader.loadClass(ModuleMemberAccessor.class.getName()); + Object instance = moduleMemberAccessor.getConstructor().newInstance(); + @SuppressWarnings("unchecked") + Callable mock = (Callable) moduleMemberAccessor + .getMethod("newInstance", Constructor.class, Object[].class) + .invoke(instance, type.getConstructor(), new Object[0]); + assertThat(mock.call()).isEqualTo("foo"); + } finally { + Thread.currentThread().setContextClassLoader(contextLoader); + } + } + + @Test + public void cannot_access_non_opened_module_with_reflection_member_accessor() throws Exception { + Path jar = modularJar(false, false, false); + ModuleLayer layer = layer(jar, false, true); + + ClassLoader loader = layer.findLoader("mockito.test"); + Class type = loader.loadClass("sample.MyCallable"); + + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader); + try { + Class moduleMemberAccessor = loader.loadClass(ReflectionMemberAccessor.class.getName()); + try { + Object instance = moduleMemberAccessor.getConstructor().newInstance(); + moduleMemberAccessor + .getMethod("newInstance", Constructor.class, Object[].class) + .invoke(instance, type.getConstructor(), new Object[0]); + fail(); + } catch (InvocationTargetException e) { + assertThat(e.getCause()).isInstanceOf(IllegalAccessException.class); + } + } finally { + Thread.currentThread().setContextClassLoader(contextLoader); + } + } +} diff --git a/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleHandlingTest.java b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleHandlingTest.java index ed732c8089..fec5e9a841 100644 --- a/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleHandlingTest.java +++ b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleHandlingTest.java @@ -4,15 +4,7 @@ */ package org.mockito.moduletest; -import java.util.concurrent.locks.Lock; -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.dynamic.loading.ClassInjector; -import net.bytebuddy.implementation.FixedValue; -import net.bytebuddy.jar.asm.ClassWriter; -import net.bytebuddy.jar.asm.ModuleVisitor; -import net.bytebuddy.jar.asm.Opcodes; -import net.bytebuddy.utility.OpenedClassReader; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -22,33 +14,19 @@ import org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker; import org.mockito.stubbing.OngoingStubbing; -import java.io.IOException; -import java.lang.module.Configuration; -import java.lang.module.ModuleDescriptor; -import java.lang.module.ModuleFinder; -import java.lang.module.ModuleReader; -import java.lang.module.ModuleReference; import java.lang.reflect.InvocationTargetException; -import java.net.MalformedURLException; -import java.net.URI; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; import java.util.concurrent.Callable; -import java.util.jar.JarEntry; -import java.util.jar.JarOutputStream; -import java.util.stream.Stream; +import java.util.concurrent.locks.Lock; -import static net.bytebuddy.matcher.ElementMatchers.named; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.hamcrest.core.Is.is; import static org.junit.Assume.assumeThat; +import static org.mockito.moduletest.ModuleUtil.layer; +import static org.mockito.moduletest.ModuleUtil.modularJar; @RunWith(Parameterized.class) public class ModuleHandlingTest { @@ -71,7 +49,7 @@ public void can_define_class_in_open_reading_module() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(true, true, true); - ModuleLayer layer = layer(jar, true); + ModuleLayer layer = layer(jar, true, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -97,7 +75,7 @@ public void can_define_class_in_open_java_util_module() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(true, true, true); - ModuleLayer layer = layer(jar, true); + ModuleLayer layer = layer(jar, true, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("java.util.concurrent.locks.Lock"); @@ -125,7 +103,7 @@ public void inline_mock_maker_can_mock_closed_modules() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(true)); Path jar = modularJar(false, false, false); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -145,7 +123,7 @@ public void can_define_class_in_open_reading_private_module() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(false, true, true); - ModuleLayer layer = layer(jar, true); + ModuleLayer layer = layer(jar, true, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -171,7 +149,7 @@ public void can_define_class_in_open_non_reading_module() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(true, true, true); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -197,7 +175,7 @@ public void can_define_class_in_open_non_reading_non_exporting_module() throws E assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(true, false, true); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -223,7 +201,7 @@ public void can_define_class_in_closed_module() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(true, true, false); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -252,7 +230,7 @@ public void cannot_define_class_in_non_opened_non_exported_module_if_lookup_inje assumeThat(!Boolean.getBoolean("org.mockito.internal.noUnsafeInjection") && ClassInjector.UsingReflection.isAvailable(), is(true)); Path jar = modularJar(false, false, false); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -279,7 +257,7 @@ public void can_define_class_in_non_opened_non_exported_module_if_unsafe_injecti assumeThat(!Boolean.getBoolean("org.mockito.internal.noUnsafeInjection") && ClassInjector.UsingReflection.isAvailable(), is(false)); Path jar = modularJar(false, false, false); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -298,120 +276,4 @@ public void can_define_class_in_non_opened_non_exported_module_if_unsafe_injecti Thread.currentThread().setContextClassLoader(contextLoader); } } - - private static Path modularJar(boolean isPublic, boolean isExported, boolean isOpened) throws IOException { - Path jar = Files.createTempFile("sample-module", ".jar"); - try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(jar))) { - out.putNextEntry(new JarEntry("module-info.class")); - out.write(moduleInfo(isExported, isOpened)); - out.closeEntry(); - out.putNextEntry(new JarEntry("sample/MyCallable.class")); - out.write(type(isPublic)); - out.closeEntry(); - } - return jar; - } - - private static byte[] type(boolean isPublic) { - return new ByteBuddy() - .subclass(Callable.class) - .name("sample.MyCallable") - .merge(isPublic ? Visibility.PUBLIC : Visibility.PACKAGE_PRIVATE) - .method(named("call")) - .intercept(FixedValue.value("foo")) - .make() - .getBytes(); - } - - private static byte[] moduleInfo(boolean isExported, boolean isOpened) { - ClassWriter classWriter = new ClassWriter(OpenedClassReader.ASM_API); - classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null); - ModuleVisitor mv = classWriter.visitModule("mockito.test", 0, null); - mv.visitRequire("java.base", Opcodes.ACC_MANDATED, null); - mv.visitPackage("sample"); - if (isExported) { - mv.visitExport("sample", 0); - } - if (isOpened) { - mv.visitOpen("sample", 0); - } - mv.visitEnd(); - classWriter.visitEnd(); - return classWriter.toByteArray(); - } - - private ModuleLayer layer(Path jar, boolean canRead) throws MalformedURLException { - Set modules = new HashSet<>(); - modules.add("mockito.test"); - ModuleFinder moduleFinder = ModuleFinder.of(jar); - if (namedModules) { - modules.add("org.mockito"); - modules.add("net.bytebuddy"); - modules.add("net.bytebuddy.agent"); - // We do not list all packages but only roots and packages that interact with the mock where - // we attempt to validate an interaction of two modules. This is of course a bit hacky as those - // libraries would normally be entirely encapsulated in an automatic module with all their classes - // but it is sufficient for the test and saves us a significant amount of code. - moduleFinder = ModuleFinder.compose(moduleFinder, - automaticModule("org.mockito", "org.mockito", "org.mockito.internal.creation.bytebuddy"), - automaticModule("net.bytebuddy", "net.bytebuddy"), - automaticModule("net.bytebuddy.agent", "net.bytebuddy.agent")); - } - Configuration configuration = Configuration.resolve( - moduleFinder, - Collections.singletonList(ModuleLayer.boot().configuration()), - ModuleFinder.of(), - modules - ); - ClassLoader classLoader = new ReplicatingClassLoader(jar); - ModuleLayer.Controller controller = ModuleLayer.defineModules( - configuration, - Collections.singletonList(ModuleLayer.boot()), - module -> classLoader - ); - if (canRead) { - controller.addReads( - controller.layer().findModule("mockito.test").orElseThrow(IllegalStateException::new), - Mockito.class.getModule() - ); - } - return controller.layer(); - } - - private static ModuleFinder automaticModule(String moduleName, String... packages) { - ModuleDescriptor descriptor = ModuleDescriptor.newAutomaticModule(moduleName) - .packages(new HashSet<>(Arrays.asList(packages))) - .build(); - ModuleReference reference = new ModuleReference(descriptor, null) { - @Override - public ModuleReader open() { - return new ModuleReader() { - @Override - public Optional find(String name) { - return Optional.empty(); - } - - @Override - public Stream list() { - return Stream.empty(); - } - - @Override - public void close() { - } - }; - } - }; - return new ModuleFinder() { - @Override - public Optional find(String name) { - return name.equals(moduleName) ? Optional.of(reference) : Optional.empty(); - } - - @Override - public Set findAll() { - return Collections.singleton(reference); - } - }; - } } diff --git a/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleUtil.java b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleUtil.java new file mode 100644 index 0000000000..49632fd71d --- /dev/null +++ b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleUtil.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.moduletest; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.implementation.FixedValue; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.ModuleVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.utility.OpenedClassReader; +import org.mockito.Mockito; + +import java.io.IOException; +import java.lang.module.*; +import java.net.MalformedURLException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.stream.Stream; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +public class ModuleUtil { + + public static Path modularJar(boolean isPublic, boolean isExported, boolean isOpened) throws IOException { + Path jar = Files.createTempFile("sample-module", ".jar"); + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(jar))) { + out.putNextEntry(new JarEntry("module-info.class")); + out.write(moduleInfo(isExported, isOpened)); + out.closeEntry(); + out.putNextEntry(new JarEntry("sample/MyCallable.class")); + out.write(type(isPublic)); + out.closeEntry(); + } + return jar; + } + + private static byte[] type(boolean isPublic) { + return new ByteBuddy() + .subclass(Callable.class) + .name("sample.MyCallable") + .merge(isPublic ? Visibility.PUBLIC : Visibility.PACKAGE_PRIVATE) + .method(named("call")) + .intercept(FixedValue.value("foo")) + .make() + .getBytes(); + } + + private static byte[] moduleInfo(boolean isExported, boolean isOpened) { + ClassWriter classWriter = new ClassWriter(OpenedClassReader.ASM_API); + classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null); + ModuleVisitor mv = classWriter.visitModule("mockito.test", 0, null); + mv.visitRequire("java.base", Opcodes.ACC_MANDATED, null); + mv.visitPackage("sample"); + if (isExported) { + mv.visitExport("sample", 0); + } + if (isOpened) { + mv.visitOpen("sample", 0); + } + mv.visitEnd(); + classWriter.visitEnd(); + return classWriter.toByteArray(); + } + + public static ModuleLayer layer(Path jar, boolean canRead, boolean namedModules) throws MalformedURLException { + Set modules = new HashSet<>(); + modules.add("mockito.test"); + ModuleFinder moduleFinder = ModuleFinder.of(jar); + if (namedModules) { + modules.add("org.mockito"); + modules.add("net.bytebuddy"); + modules.add("net.bytebuddy.agent"); + // We do not list all packages but only roots and packages that interact with the mock where + // we attempt to validate an interaction of two modules. This is of course a bit hacky as those + // libraries would normally be entirely encapsulated in an automatic module with all their classes + // but it is sufficient for the test and saves us a significant amount of code. + moduleFinder = ModuleFinder.compose(moduleFinder, + automaticModule("org.mockito", "org.mockito", "org.mockito.internal.creation.bytebuddy"), + automaticModule("net.bytebuddy", "net.bytebuddy"), + automaticModule("net.bytebuddy.agent", "net.bytebuddy.agent")); + } + Configuration configuration = Configuration.resolve( + moduleFinder, + Collections.singletonList(ModuleLayer.boot().configuration()), + ModuleFinder.of(), + modules + ); + ClassLoader classLoader = new ReplicatingClassLoader(jar); + ModuleLayer.Controller controller = ModuleLayer.defineModules( + configuration, + Collections.singletonList(ModuleLayer.boot()), + module -> classLoader + ); + if (canRead) { + controller.addReads( + controller.layer().findModule("mockito.test").orElseThrow(IllegalStateException::new), + Mockito.class.getModule() + ); + } + return controller.layer(); + } + + private static ModuleFinder automaticModule(String moduleName, String... packages) { + ModuleDescriptor descriptor = ModuleDescriptor.newAutomaticModule(moduleName) + .packages(new HashSet<>(Arrays.asList(packages))) + .build(); + ModuleReference reference = new ModuleReference(descriptor, null) { + @Override + public ModuleReader open() { + return new ModuleReader() { + @Override + public Optional find(String name) { + return Optional.empty(); + } + + @Override + public Stream list() { + return Stream.empty(); + } + + @Override + public void close() { + } + }; + } + }; + return new ModuleFinder() { + @Override + public Optional find(String name) { + return name.equals(moduleName) ? Optional.of(reference) : Optional.empty(); + } + + @Override + public Set findAll() { + return Collections.singleton(reference); + } + }; + } +} diff --git a/subprojects/module-test/src/test/java/org/mockito/moduletest/ReplicatingClassLoader.java b/subprojects/module-test/src/test/java/org/mockito/moduletest/ReplicatingClassLoader.java index 9ee006c880..c9cf0a32d2 100644 --- a/subprojects/module-test/src/test/java/org/mockito/moduletest/ReplicatingClassLoader.java +++ b/subprojects/module-test/src/test/java/org/mockito/moduletest/ReplicatingClassLoader.java @@ -48,4 +48,9 @@ public Class loadClass(String name) throws ClassNotFoundException { } } } + + @Override + protected URL findResource(String moduleName, String name) { + return Mockito.class.getResource("/" + name); + } } diff --git a/version.properties b/version.properties index 3cb4008653..061b231748 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ #Currently building Mockito version -version=3.4.9 +version=3.5.0 #Previous version used to generate release notes delta previousVersion=3.4.8