Skip to content

Commit 3ee1d52

Browse files
committed
fix Mockito memory leak
1 parent 5ca4c11 commit 3ee1d52

5 files changed

Lines changed: 83 additions & 14 deletions

File tree

src/main/java/frc/robot/lib/DummySparkMaxAnswer.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.revrobotics.CANSparkMaxLowLevel.MotorType;
1010
import com.revrobotics.SparkMaxPIDController.AccelStrategy;
1111

12-
import org.mockito.Mockito;
1312
import org.mockito.invocation.InvocationOnMock;
1413

1514
public class DummySparkMaxAnswer extends REVLibErrorAnswer {
@@ -18,12 +17,12 @@ public class DummySparkMaxAnswer extends REVLibErrorAnswer {
1817

1918
public static final DummySparkMaxAnswer ANSWER = new DummySparkMaxAnswer();
2019

21-
public static final CANSparkMax DUMMY_SPARK_MAX = Mockito.mock(CANSparkMax.class, ANSWER);
20+
public static final CANSparkMax DUMMY_SPARK_MAX = Mocks.mock(CANSparkMax.class, ANSWER);
2221

23-
public static final RelativeEncoder DUMMY_ENCODER = Mockito.mock(RelativeEncoder.class, REVLibErrorAnswer.ANSWER);
24-
public static final SparkMaxAnalogSensor DUMMY_ANALOG_SENSOR = Mockito.mock(SparkMaxAnalogSensor.class, REVLibErrorAnswer.ANSWER);
25-
public static final SparkMaxLimitSwitch DUMMY_LIMIT_SWITCH = Mockito.mock(SparkMaxLimitSwitch.class, REVLibErrorAnswer.ANSWER);
26-
public static final SparkMaxPIDController DUMMY_PID_CONTROLLER = Mockito.mock(SparkMaxPIDController.class, ANSWER);
22+
public static final RelativeEncoder DUMMY_ENCODER = Mocks.mock(RelativeEncoder.class, REVLibErrorAnswer.ANSWER);
23+
public static final SparkMaxAnalogSensor DUMMY_ANALOG_SENSOR = Mocks.mock(SparkMaxAnalogSensor.class, REVLibErrorAnswer.ANSWER);
24+
public static final SparkMaxLimitSwitch DUMMY_LIMIT_SWITCH = Mocks.mock(SparkMaxLimitSwitch.class, REVLibErrorAnswer.ANSWER);
25+
public static final SparkMaxPIDController DUMMY_PID_CONTROLLER = Mocks.mock(SparkMaxPIDController.class, ANSWER);
2726

2827
@Override
2928
public Object answer(InvocationOnMock invocation) throws Throwable {

src/main/java/frc/robot/lib/Mocks.java

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
11
package frc.robot.lib;
22

3+
import java.lang.ref.WeakReference;
34
import java.lang.reflect.InvocationTargetException;
45
import java.lang.reflect.Method;
56
import java.lang.reflect.Modifier;
67
import java.util.ArrayList;
78
import java.util.Arrays;
9+
import java.util.Collections;
810
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.function.Consumer;
13+
import java.util.function.Predicate;
914
import java.util.stream.Collectors;
10-
import org.mockito.MockSettings;
1115

16+
import org.mockito.MockSettings;
1217
import org.mockito.Mockito;
1318
import org.mockito.internal.stubbing.defaultanswers.ReturnsSmartNulls;
1419
import org.mockito.invocation.InvocationOnMock;
1520
import org.mockito.stubbing.Answer;
1621

1722
public final class Mocks {
23+
24+
private static final List<WeakReference<Object>> MOCKS = Collections.synchronizedList(new ArrayList<>());
25+
private static final Predicate<WeakReference<?>> IS_REFERENCE_CLEARED = reference -> reference.get() == null;
26+
private static final Consumer<WeakReference<Object>> CLEAR_INVOCATIONS_ON_REFERENCED_MOCK = reference -> Mockito.clearInvocations(reference.get());
27+
private static final Predicate<WeakReference<Object>> CLEAR_INVOCATIONS_ON_REFERENCED_MOCK_IF_REFERNCE_NOT_CLEARED = reference -> {
28+
if(IS_REFERENCE_CLEARED.test(reference)) return true;
29+
CLEAR_INVOCATIONS_ON_REFERENCED_MOCK.accept(reference);
30+
return false;
31+
};
32+
33+
static {
34+
// Use a single predicate so that clearing references and invocations is an atomic operation
35+
// Otherwise, we could (rarely) run into:
36+
// 1) Mock is added
37+
// 2) Garbage collected references are removed
38+
// 3) Mock is garbage collected
39+
// 4) Mock invocations are cleared -> throws NullPointerException
40+
Lib199Subsystem.registerPeriodic(() -> MOCKS.removeIf(CLEAR_INVOCATIONS_ON_REFERENCED_MOCK_IF_REFERNCE_NOT_CLEARED));
41+
}
1842

1943
/**
2044
* Attempts to create an instance of a class in which some or all of the classes methods are replaced with a mocked implementation
@@ -77,7 +101,7 @@ public static <T, U> T createMock(Class<T> classToMock, U implClass, Answer<Obje
77101
settings = Mockito.withSettings().extraInterfaces(interfaces);
78102
}
79103
settings = settings.defaultAnswer(new MockAnswer<>(methods, implClass, defaultAnswer));
80-
T mock = Mockito.mock(classToMock, settings);
104+
T mock = mock(classToMock, settings);
81105
return mock;
82106
}
83107

@@ -88,6 +112,55 @@ public static Method[] listMethods(Class<?> base, Class<?>... interfaces) {
88112
return out.toArray(Method[]::new);
89113
}
90114

115+
/**
116+
* A wrapper for the underlying Mockito method which automatically calls {@link Mockito#clearInvocations(Object...)} to prevent memory leaks
117+
*
118+
* @see Mockito#mock(Class)
119+
*/
120+
public static <T> T mock(Class<T> classToMock) {
121+
return reportMock(Mockito.mock(classToMock));
122+
}
123+
124+
/**
125+
* A wrapper for the underlying Mockito method which automatically calls {@link Mockito#clearInvocations(Object...)} to prevent memory leaks
126+
*
127+
* @see Mockito#mock(Class, String)
128+
*/
129+
public static <T> T mock(Class<T> classToMock, String name) {
130+
return reportMock(Mockito.mock(classToMock, name));
131+
}
132+
133+
/**
134+
* A wrapper for the underlying Mockito method which automatically calls {@link Mockito#clearInvocations(Object...)} to prevent memory leaks
135+
*
136+
* @see Mockito#mock(Class, Answer)
137+
*/
138+
public static <T> T mock(Class<T> classToMock, Answer<?> defaultAnswer) {
139+
return reportMock(Mockito.mock(classToMock, defaultAnswer));
140+
}
141+
142+
/**
143+
* A wrapper for the underlying Mockito method which automatically calls {@link Mockito#clearInvocations(Object...)} to prevent memory leaks
144+
*
145+
* @see Mockito#mock(Class, MockSettings)
146+
*/
147+
public static <T> T mock(Class<T> classToMock, MockSettings mockSettings) {
148+
return reportMock(Mockito.mock(classToMock, mockSettings));
149+
}
150+
151+
/**
152+
* Registers a Mockito mock and periodically calls {@link Mockito#clearInvocations(Object...)} on it to prevent memory leaks
153+
*
154+
* @param <T> The type of the mock
155+
* @param t The mock
156+
* @return The mock
157+
*/
158+
public static <T> T reportMock(T t) {
159+
// Wrap in a WeakReference to prevent memory leaks on objects with no more references
160+
if(Mockito.mockingDetails(t).isMock()) MOCKS.add(new WeakReference<Object>(t));
161+
return t;
162+
}
163+
91164
private Mocks() {}
92165

93166
private static final class MockAnswer<U> implements Answer<Object> {

src/test/java/frc/robot/lib/DummySparkMaxAnswerTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@
1212
import com.revrobotics.SparkMaxPIDController.AccelStrategy;
1313

1414
import org.junit.Test;
15-
import org.mockito.Mockito;
1615

1716
public class DummySparkMaxAnswerTest {
1817

1918
public CANSparkMax createMockedSparkMax() {
20-
return Mockito.mock(CANSparkMax.class, new DummySparkMaxAnswer());
19+
return Mocks.mock(CANSparkMax.class, new DummySparkMaxAnswer());
2120
}
2221

2322
public static void assertTestResponses(CANSparkMax spark) {

src/test/java/frc/robot/lib/ErrorCodeAnswerTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
import com.ctre.phoenix.motorcontrol.can.WPI_TalonSRX;
77

88
import org.junit.Test;
9-
import org.mockito.Mockito;
109

1110
public class ErrorCodeAnswerTest {
1211

1312
@Test
1413
public void testResponses() throws Exception {
15-
WPI_TalonSRX talon = Mockito.mock(WPI_TalonSRX.class, new ErrorCodeAnswer());
14+
WPI_TalonSRX talon = Mocks.mock(WPI_TalonSRX.class, new ErrorCodeAnswer());
1615

1716
// Check that primative types return "null"
1817
assertEquals(0, talon.get(), 0.01);

src/test/java/frc/robot/lib/REVLibErrorAnswerTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
import com.revrobotics.RelativeEncoder;
77

88
import org.junit.Test;
9-
import org.mockito.Mockito;
109

1110
public class REVLibErrorAnswerTest {
1211

1312
@Test
1413
public void testResponses() throws Exception {
15-
RelativeEncoder enc = Mockito.mock(RelativeEncoder.class, new REVLibErrorAnswer());
14+
RelativeEncoder enc = Mocks.mock(RelativeEncoder.class, new REVLibErrorAnswer());
1615

1716
// Check that primative types return "null"
1817
assertEquals(0, enc.getPosition(), 0.01);

0 commit comments

Comments
 (0)