Skip to content

Commit

Permalink
Fixes mockito#1614: Add API to clean up mocks.
Browse files Browse the repository at this point in the history
Due to the introduction of map from weak reference from mock instance to
its invocation handler, Mockito became vunerable to memory leaks as
there are multiple situations where Mockito could unintentionally hold
strong references to mock instances in the map record. The strong
references could be through spiedInstance for spies, and arguments used
to faciliate method stubbing.

Mockito could never know if the arguments passed in for method stubbing
are also strongly referenced somewhere else or not, so Mockito needs to
save a strong reference to these arguments to avoid premature GC.
Therefore to solve cyclic strong references through arguments Mockito
needs to explicitly know when mocks are out of their life, and clean
up all internal strong references associated with them.

This commit also fixes mockito#1532 and fixes mockito#1533.
  • Loading branch information
ttanxu committed Feb 28, 2019
1 parent f11705b commit c1ad3a0
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 52 deletions.
10 changes: 10 additions & 0 deletions src/main/java/org/mockito/MockitoFramework.java
Expand Up @@ -92,4 +92,14 @@ public interface MockitoFramework {
*/
@Incubating
InvocationFactory getInvocationFactory();

/**
* Clears up all existing mocks. This is useful when testing with {@link org.mockito.plugins.InlineMockMaker} which
* may hold references to mocks and leak memory. No interaction to any mock created previously is not allowed after
* calling this method.
*
* @since 2.24.8
*/
@Incubating
void clearAllMocks();
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2019 Mockito contributors
* This program is made available under the terms of the MIT License.
*/

package org.mockito.exceptions.misusing;

import org.mockito.MockitoSession;
import org.mockito.exceptions.base.MockitoException;

/**
* Reports the misuse where user tries to open more than one {@link MockitoSession} that tracks and cleans up mocks.
* User needs to finish previous session that tracks and cleans up mocks before trying to open a new one. Note this
* doesn't prevent user from opening sessions that doesn't track or clean up mocks.
*
* @since 2.24.4
*/
public class MultipleTrackingMockSessionException extends MockitoException {
public MultipleTrackingMockSessionException(String message) {
super(message);
}
}
Expand Up @@ -14,6 +14,7 @@
import org.mockito.internal.util.concurrent.WeakConcurrentMap;
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;
import org.mockito.plugins.InlineMockMaker;

import java.io.File;
import java.io.FileOutputStream;
Expand Down Expand Up @@ -89,7 +90,7 @@
* support this feature.
*/
@Incubating
public class InlineByteBuddyMockMaker implements ClassCreatingMockMaker {
public class InlineByteBuddyMockMaker implements ClassCreatingMockMaker, InlineMockMaker {

private static final Instrumentation INSTRUMENTATION;

Expand Down Expand Up @@ -271,6 +272,15 @@ public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings
}
}

@Override
public void cleanUpMock(Object mock) {
if (mock == null) {
mocks.clear();
} else {
mocks.remove(mock);
}
}

@Override
public TypeMockability isTypeMockable(final Class<?> type) {
return new TypeMockability() {
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/org/mockito/internal/exceptions/Reporter.java
Expand Up @@ -913,4 +913,11 @@ public static void unfinishedMockingSession() {
"Previous MockitoSession was not concluded with 'finishMocking()'.",
"For examples of correct usage see javadoc for MockitoSession class."));
}

public static void multipleTrackingMockSession() {
throw new MultipleTrackingMockSessionException(join(
"Multiple open sessions that track and clean up mocks detected.",
"There can only be at most one open session that tracks and cleans up mocks,",
"please finish previous session that tracks and clean up mocks before opening a new one."));
}
}
Expand Up @@ -10,6 +10,8 @@
import org.mockito.internal.util.Checks;
import org.mockito.invocation.InvocationFactory;
import org.mockito.listeners.MockitoListener;
import org.mockito.plugins.InlineMockMaker;
import org.mockito.plugins.MockMaker;
import org.mockito.plugins.MockitoPlugins;

import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress;
Expand Down Expand Up @@ -37,4 +39,16 @@ public MockitoPlugins getPlugins() {
public InvocationFactory getInvocationFactory() {
return new DefaultInvocationFactory();
}

@Override
public void clearAllMocks() {
MockMaker mockMaker = Plugins.getMockMaker();

if (!(mockMaker instanceof InlineMockMaker)) {
return;
}

InlineMockMaker inlineMockMaker = (InlineMockMaker) mockMaker;
inlineMockMaker.cleanUpMock(null);
}
}
9 changes: 9 additions & 0 deletions src/main/java/org/mockito/internal/util/MockUtil.java
Expand Up @@ -13,6 +13,7 @@
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;
import org.mockito.mock.MockName;
import org.mockito.plugins.InlineMockMaker;
import org.mockito.plugins.MockMaker;
import org.mockito.plugins.MockMaker.TypeMockability;

Expand Down Expand Up @@ -62,6 +63,14 @@ public static <T> MockHandler<T> getMockHandler(T mock) {
}
}

public static <T> void clearMock(T mock) {
if (!(mockMaker instanceof InlineMockMaker)) {
return;
}

((InlineMockMaker) mockMaker).cleanUpMock(mock);
}

public static InvocationContainerImpl getInvocationContainer(Object mock) {
return (InvocationContainerImpl) getMockHandler(mock).getInvocationContainer();
}
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/org/mockito/plugins/InlineMockMaker.java
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2019 Mockito contributors
* This program is made available under the terms of the MIT License.
*/

package org.mockito.plugins;

import org.mockito.Incubating;

/**
* Extension to {@link MockMaker} for mock makers that changes inline method implementations.
* @since 2.24.8
*/
@Incubating
public interface InlineMockMaker extends MockMaker {
/**
* Cleans up internal state for specified {@code mock}. You may assume there won't be any interaction to this {@code mock}
* after this is called.
*
* @param mock the mock instance whose internal state is to be cleaned, or {@code null} to clean all existing mocks.
* @since 2.24.8
*/
@Incubating
void cleanUpMock(Object mock);
}
Expand Up @@ -287,6 +287,15 @@ public void test_parameters_retention() throws Exception {
.getOnly().getParameters().getOnly().getName()).isEqualTo("bar");
}

@Test
public void test_cleanup_mock_clears_handler() {
MockCreationSettings<GenericSubClass> settings = settingsFor(GenericSubClass.class);
GenericSubClass proxy = mockMaker.createMock(settings, new MockHandlerImpl<GenericSubClass>(settings));
assertThat(mockMaker.getHandler(proxy)).isNotNull();
mockMaker.cleanUpMock(proxy);
assertThat(mockMaker.getHandler(proxy)).isNull();
}

private static <T> MockCreationSettings<T> settingsFor(Class<T> type, Class<?>... extraInterfaces) {
MockSettingsImpl<T> mockSettings = new MockSettingsImpl<T>();
mockSettings.setTypeToMock(type);
Expand Down
Expand Up @@ -10,14 +10,18 @@
import org.mockito.MockSettings;
import org.mockito.StateMaster;
import org.mockito.exceptions.misusing.RedundantListenerException;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.listeners.MockCreationListener;
import org.mockito.listeners.MockitoListener;
import org.mockito.mock.MockCreationSettings;
import org.mockito.plugins.InlineMockMaker;
import org.mockitoutil.TestBase;

import java.util.List;
import java.util.Set;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;
import static org.mockitoutil.ThrowableAssert.assertThat;

Expand Down Expand Up @@ -112,5 +116,17 @@ public void run() {
"For more information, see the javadoc for RedundantListenerException class.");
}

@Test
public void clears_all_mocks() {
List list = mock(List.class);
assertTrue(mockingDetails(list).isMock());

framework.clearAllMocks();

if (Plugins.getMockMaker() instanceof InlineMockMaker) {
assertFalse(mockingDetails(list).isMock());
}
}

private static class MyListener implements MockitoListener {}
}
Expand Up @@ -5,26 +5,16 @@

package org.mockito.internal.progress;

import org.assertj.core.api.Assertions;
import org.assertj.core.api.ThrowableAssert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.exceptions.misusing.RedundantListenerException;
import org.mockito.internal.listeners.AutoCleanableListener;
import org.mockito.internal.verification.VerificationModeFactory;
import org.mockito.listeners.MockitoListener;
import org.mockito.verification.VerificationMode;
import org.mockitoutil.TestBase;

import java.util.LinkedHashSet;
import java.util.Set;

import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class MockingProgressImplTest extends TestBase {

Expand Down Expand Up @@ -66,45 +56,4 @@ public void shouldNotifyListenerSafely() throws Exception {
mockingProgress.mockingStarted(null, null);
}

@Test
public void should_not_allow_redundant_listeners() {
MockitoListener listener1 = mock(MockitoListener.class);
final MockitoListener listener2 = mock(MockitoListener.class);

final Set<MockitoListener> listeners = new LinkedHashSet<MockitoListener>();

//when
MockingProgressImpl.addListener(listener1, listeners);

//then
Assertions.assertThatThrownBy(new ThrowableAssert.ThrowingCallable() {
public void call() {
MockingProgressImpl.addListener(listener2, listeners);
}
}).isInstanceOf(RedundantListenerException.class);
}

@Test
public void should_clean_up_listeners_automatically() {
MockitoListener someListener = mock(MockitoListener.class);
MyListener cleanListener = mock(MyListener.class);
MyListener dirtyListener = when(mock(MyListener.class).isListenerDirty()).thenReturn(true).getMock();

Set<MockitoListener> listeners = new LinkedHashSet<MockitoListener>();

//when
MockingProgressImpl.addListener(someListener, listeners);
MockingProgressImpl.addListener(dirtyListener, listeners);

//then
Assertions.assertThat(listeners).containsExactlyInAnyOrder(someListener, dirtyListener);

//when
MockingProgressImpl.addListener(cleanListener, listeners);

//then dirty listener was removed automatically
Assertions.assertThat(listeners).containsExactlyInAnyOrder(someListener, cleanListener);
}

interface MyListener extends MockitoListener, AutoCleanableListener {}
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2019 Mockito contributors
* This program is made available under the terms of the MIT License.
*/

package org.mockitoinline.bugs;

import org.junit.Test;
import org.mockito.MockitoSession;

import static org.mockito.Mockito.framework;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockitoSession;

public class CyclicMockMethodArgumentMemoryLeakTest {
private static final int ARRAY_LENGTH = 1 << 20; // 4 MB

@Test
public void no_memory_leak_when_cyclically_calling_method_with_mocks() {
for (int i = 0; i < 100; ++i) {
final A a = mock(A.class);
a.largeArray = new int[ARRAY_LENGTH];
final B b = mock(B.class);

a.accept(b);
b.accept(a);

framework().clearAllMocks();
}
}

private static class A {
private int[] largeArray;

void accept(B b) {}
}

private static class B {
void accept(A a) {}
}
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2019 Mockito contributors
* This program is made available under the terms of the MIT License.
*/

package org.mockitoinline.bugs;

import org.junit.Test;
import org.mockito.MockitoSession;

import static org.mockito.Mockito.framework;
import static org.mockito.Mockito.mockitoSession;
import static org.mockito.Mockito.spy;

public class SelfSpyReferenceMemoryLeakTest {
private static final int ARRAY_LENGTH = 1 << 20; // 4 MB

@Test
public void no_memory_leak_when_spy_holds_reference_to_self() {
for (int i = 0; i < 100; ++i) {
final DeepRefSelfClass instance = spy(new DeepRefSelfClass());
instance.refInstance(instance);

framework().clearAllMocks();
}
}

private static class DeepRefSelfClass {
private final DeepRefSelfClass[] array = new DeepRefSelfClass[1];

private final int[] largeArray = new int[ARRAY_LENGTH];

private void refInstance(DeepRefSelfClass instance) {
array[0] = instance;
}
}
}

0 comments on commit c1ad3a0

Please sign in to comment.