Skip to content

Commit

Permalink
Adds support for creating spies from within a mock instance's constru…
Browse files Browse the repository at this point in the history
…ctor, thus avoiding reflection on final fields.
  • Loading branch information
raphw committed Aug 13, 2020
1 parent 19a7d84 commit 47ef058
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;

import java.util.Optional;
import java.util.function.Function;

/**
Expand All @@ -28,6 +29,12 @@ public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
return defaultByteBuddyMockMaker.createMock(settings, handler);
}

@Override
public <T> Optional<T> createSpy(
MockCreationSettings<T> settings, MockHandler handler, T object) {
return defaultByteBuddyMockMaker.createSpy(settings, handler, object);
}

@Override
public <T> Class<? extends T> createMockType(MockCreationSettings<T> creationSettings) {
return defaultByteBuddyMockMaker.createMockType(creationSettings);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@

public interface ConstructionCallback {

void accept(Class<?> type, Object object, Object[] arguments, String[] parameterTypeNames);
Object apply(Class<?> type, Object object, Object[] arguments, String[] parameterTypeNames);
}
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ public class InlineByteBuddyMockMaker

private final ThreadLocal<Boolean> mockitoConstruction = ThreadLocal.withInitial(() -> false);

private final ThreadLocal<Object> currentSpied = new ThreadLocal<>();

public InlineByteBuddyMockMaker() {
if (INITIALIZATION_ERROR != null) {
String detail;
Expand Down Expand Up @@ -252,8 +254,10 @@ public InlineByteBuddyMockMaker() {
};
ConstructionCallback onConstruction =
(type, object, arguments, parameterTypeNames) -> {
if (mockitoConstruction.get() || currentConstruction.get() != type) {
return;
if (mockitoConstruction.get()) {
return currentSpied.get();
} else if (currentConstruction.get() != type) {
return null;
}
currentConstruction.remove();
isSuspended.set(true);
Expand All @@ -273,6 +277,7 @@ public InlineByteBuddyMockMaker() {
} finally {
isSuspended.set(false);
}
return null;
};

bytecodeGenerator =
Expand All @@ -288,6 +293,27 @@ public InlineByteBuddyMockMaker() {

@Override
public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
return doCreateMock(settings, handler, false);
}

@Override
public <T> Optional<T> createSpy(
MockCreationSettings<T> 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> T doCreateMock(
MockCreationSettings<T> settings,
MockHandler handler,
boolean nullOnNonInlineConstruction) {
Class<? extends T> type = createMockType(settings);

try {
Expand All @@ -296,6 +322,9 @@ public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
// 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);
Expand Down Expand Up @@ -753,7 +782,8 @@ private InlineConstructionMockContext(
@Override
public int getCount() {
if (count == 0) {
throw new MockitoConfigurationException("mocked construction context is not initialized");
throw new MockitoConfigurationException(
"mocked construction context is not initialized");
}
return count;
}
Expand All @@ -767,7 +797,8 @@ public Constructor<?> constructor() {
parameterTypes[index++] = PRIMITIVES.get(parameterTypeName);
} else {
try {
parameterTypes[index++] = Class.forName(parameterTypeName, false, type.getClassLoader());
parameterTypes[index++] =
Class.forName(parameterTypeName, false, type.getClassLoader());
} catch (ClassNotFoundException e) {
throw new MockitoException(
"Could not find parameter of type " + parameterTypeName, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
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;
Expand All @@ -28,6 +31,7 @@
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;
Expand Down Expand Up @@ -166,9 +170,9 @@ public Callable<?> handleStatic(Class<?> type, Method origin, Object[] arguments
}

@Override
public void handleConstruction(
public Object handleConstruction(
Class<?> type, Object object, Object[] arguments, String[] parameterTypeNames) {
onConstruction.accept(type, object, arguments, parameterTypeNames);
return onConstruction.apply(type, object, arguments, parameterTypeNames);
}

@Override
Expand Down Expand Up @@ -423,10 +427,13 @@ public void visitCode() {
*
* if (MockMethodDispatcher.isConstructorMock(<identifier>, Current.class) {
* super(<default arguments>);
* MockMethodDispatcher.handleConstruction(Current.class,
* 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;
* }
*
Expand Down Expand Up @@ -549,19 +556,72 @@ public void visitCode() {
Type.getInternalName(MockMethodDispatcher.class),
"handleConstruction",
Type.getMethodDescriptor(
Type.VOID_TYPE,
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<FieldDescription.InDefinedShape> 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)) {
super.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
Object[] locals =
toFrames(
Opcodes.UNINITIALIZED_THIS,
instrumentedMethod
.getParameters()
.asTypeList()
.asErasures());
super.visitFrame(
Opcodes.F_FULL, locals.length, locals, 0, new Object[0]);
}
}

Expand All @@ -583,6 +643,32 @@ public void visitMaxs(int maxStack, int maxLocals) {
}
return methodVisitor;
}

private static Object[] toFrames(Object self, List<TypeDescription> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ public static boolean isConstructorMock(String identifier, Class<?> type) {
return DISPATCHERS.get(identifier).isConstructorMock(type);
}

public static void handleConstruction(
@SuppressWarnings("unused")
public static Object handleConstruction(
String identifier,
Class<?> type,
Object object,
Object[] arguments,
String[] parameterTypeNames) {
DISPATCHERS.get(identifier).handleConstruction(type, object, arguments, parameterTypeNames);
return DISPATCHERS
.get(identifier)
.handleConstruction(type, object, arguments, parameterTypeNames);
}

public abstract Callable<?> handle(Object instance, Method origin, Object[] arguments)
Expand All @@ -56,7 +59,7 @@ public abstract Callable<?> handle(Object instance, Method origin, Object[] argu
public abstract Callable<?> handleStatic(Class<?> type, Method origin, Object[] arguments)
throws Throwable;

public abstract void handleConstruction(
public abstract Object handleConstruction(
Class<?> type, Object object, Object[] arguments, String[] parameterTypeNames);

public abstract boolean isMock(Object instance);
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/org/mockito/internal/util/MockUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,21 @@ public static TypeMockability typeMockabilityOf(Class<?> type) {
public static <T> T createMock(MockCreationSettings<T> 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;
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/org/mockito/plugins/MockMaker.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.mockito.mock.MockCreationSettings;

import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import static org.mockito.internal.util.StringUtil.join;
Expand Down Expand Up @@ -73,6 +74,25 @@ public interface MockMaker {
*/
<T> T createMock(MockCreationSettings<T> 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}.
* <b>Do not</b> 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 <T> Type of the mock to return, actually the <code>settings.getTypeToMock</code>.
* @return
* @since 3.5.0
*/
default <T> Optional<T> createSpy(
MockCreationSettings<T> settings, MockHandler handler, T instance) {
return Optional.empty();
}

/**
* Returns the handler for the {@code mock}. <b>Do not</b> provide your own implementations at this time
* because the work on the {@link MockHandler} api is not completed.
Expand Down
Loading

0 comments on commit 47ef058

Please sign in to comment.