mockit.internal.mockups.MockupsModifier Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jmockit Show documentation
Show all versions of jmockit Show documentation
JMockit is a Java toolkit for automated developer testing.
It contains mocking/faking APIs and a code coverage tool, supporting both JUnit and TestNG.
The mocking APIs allow all kinds of Java code, without testability restrictions, to be tested
in isolation from selected dependencies.
/*
* Copyright (c) 2006-2015 Rogério Liesenfeld
* This file is subject to the terms of the MIT license (see LICENSE.txt).
*/
package mockit.internal.mockups;
import javax.annotation.*;
import static java.lang.reflect.Modifier.*;
import mockit.*;
import mockit.external.asm.*;
import mockit.internal.*;
import mockit.internal.mockups.MockMethods.*;
import mockit.internal.state.*;
import static mockit.external.asm.Opcodes.*;
import static mockit.internal.util.GeneratedClasses.*;
/**
* Responsible for generating all necessary bytecode in the redefined (real) class.
* Such code will redirect calls made on "real" methods to equivalent calls on the corresponding "mock" methods.
* The original code won't be executed by the running JVM until the class redefinition is undone.
*
* Methods in the real class with no corresponding mock methods are unaffected.
*
* Any fields (static or not) in the real class remain untouched.
*/
final class MockupsModifier extends BaseClassModifier
{
private static final int ABSTRACT_OR_SYNTHETIC = ACC_ABSTRACT + ACC_SYNTHETIC;
@Nonnull private final MockMethods mockMethods;
private final boolean useMockingBridgeForUpdatingMockState;
@Nonnull private final Class> mockedClass;
private MockMethod mockMethod;
private boolean isConstructor;
/**
* Initializes the modifier for a given real/mock class pair.
*
* The mock instance provided will receive calls for any instance methods defined in the mock class.
* Therefore, it needs to be later recovered by the modified bytecode inside the real method.
* To enable this, the mock instance is added to a global data structure made available through the
* {@link TestRun#getMock(String, Object)} method.
*
* @param cr the class file reader for the real class
* @param realClass the class to be mocked-up, or a base type of an implementation class to be mocked-up
* @param mockUp an instance of the mockup class
* @param mockMethods contains the set of mock methods collected from the mock class; each mock method is identified
* by a pair composed of "name" and "desc", where "name" is the method name, and "desc" is the JVM internal
* description of the parameters; once the real class modification is complete this set will be empty, unless no
* corresponding real method was found for any of its method identifiers
*/
MockupsModifier(
@Nonnull ClassReader cr, @Nonnull Class> realClass, @Nonnull MockUp> mockUp, @Nonnull MockMethods mockMethods)
{
super(cr);
mockedClass = realClass;
this.mockMethods = mockMethods;
ClassLoader classLoaderOfRealClass = realClass.getClassLoader();
useMockingBridgeForUpdatingMockState = classLoaderOfRealClass == null;
inferUseOfMockingBridge(classLoaderOfRealClass, mockUp);
}
private void inferUseOfMockingBridge(@Nullable ClassLoader classLoaderOfRealClass, @Nonnull Object mock)
{
setUseMockingBridge(classLoaderOfRealClass);
if (!useMockingBridge && !isPublic(mock.getClass().getModifiers())) {
useMockingBridge = true;
}
}
@Override
public void visit(
int version, int access, @Nonnull String name, @Nullable String signature, @Nullable String superName,
@Nullable String[] interfaces)
{
super.visit(version, access, name, signature, superName, interfaces);
if (isGeneratedImplementationClass(name)) {
classDesc = name.replace(IMPLCLASS_PREFIX, "");
}
}
/**
* If the specified method has a mock definition, then generates bytecode to redirect calls made to it to the mock
* method. If it has no mock, does nothing.
*
* @param access not relevant
* @param name together with desc, used to identity the method in given set of mock methods
* @param signature not relevant
* @param exceptions not relevant
*
* @return {@code null} if the method was redefined, otherwise a {@code MethodWriter} that writes out the visited
* method code without changes
*/
@Override
public MethodVisitor visitMethod(
int access, @Nonnull String name, @Nonnull String desc, @Nullable String signature, @Nullable String[] exceptions)
{
if ((access & ABSTRACT_OR_SYNTHETIC) != 0) {
if (isAbstract(access)) {
// Marks a matching mock method (if any) as having the corresponding mocked method.
mockMethods.findMethod(access, name, desc, signature);
}
return cw.visitMethod(access, name, desc, signature, exceptions);
}
isConstructor = "".equals(name);
if (isConstructor && isMockedSuperclass() || !hasMock(access, name, desc, signature)) {
return cw.visitMethod(access, name, desc, signature, exceptions);
}
startModifiedMethodVersion(access, name, desc, signature, exceptions);
if (isConstructor) {
generateCallToSuperConstructor();
}
else if (isNative(methodAccess)) {
generateCallToUpdateMockState();
generateCallToMockMethod();
generateMethodReturn();
mw.visitMaxs(1, 0); // dummy values, real ones are calculated by ASM
return methodAnnotationsVisitor;
}
generateDynamicCallToMock();
return copyOriginalImplementationCode(isConstructor);
}
private boolean hasMock(int access, @Nonnull String name, @Nonnull String desc, @Nullable String signature)
{
String mockName = getCorrespondingMockName(name);
mockMethod = mockMethods.findMethod(access, mockName, desc, signature);
return mockMethod != null;
}
@Nonnull
private static String getCorrespondingMockName(@Nonnull String name)
{
if ("".equals(name)) {
return "$init";
}
if ("".equals(name)) {
return "$clinit";
}
return name;
}
private boolean isMockedSuperclass() { return mockedClass != mockMethods.getRealClass(); }
private void generateDynamicCallToMock()
{
Label startOfRealImplementation = null;
if (!isStatic(methodAccess) && !isConstructor && isMockedSuperclass()) {
startOfRealImplementation = new Label();
mw.visitVarInsn(ALOAD, 0);
mw.visitTypeInsn(INSTANCEOF, Type.getInternalName(mockMethods.getRealClass()));
mw.visitJumpInsn(IFEQ, startOfRealImplementation);
}
generateCallToUpdateMockState();
if (isConstructor) {
generateConditionalCallForMockedConstructor();
}
else {
generateConditionalCallForMockedMethod(startOfRealImplementation);
}
}
private void generateCallToUpdateMockState()
{
if (useMockingBridgeForUpdatingMockState) {
generateCallToControlMethodThroughMockingBridge();
mw.visitTypeInsn(CHECKCAST, "java/lang/Boolean");
mw.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false);
}
else {
mw.visitLdcInsn(mockMethods.getMockClassInternalName());
generateCodeToPassThisOrNullIfStaticMethod();
mw.visitIntInsn(SIPUSH, mockMethod.getIndexForMockState());
mw.visitMethodInsn(
INVOKESTATIC, "mockit/internal/state/TestRun", "updateMockState",
"(Ljava/lang/String;Ljava/lang/Object;I)Z", false);
}
}
private void generateCallToControlMethodThroughMockingBridge()
{
generateCodeToObtainInstanceOfMockingBridge(MockupBridge.MB);
// First and second "invoke" arguments:
generateCodeToPassThisOrNullIfStaticMethod();
mw.visitInsn(ACONST_NULL);
// Create array for call arguments (third "invoke" argument):
generateCodeToCreateArrayOfObject(mw, 2);
int i = 0;
generateCodeToFillArrayElement(i++, mockMethods.getMockClassInternalName());
generateCodeToFillArrayElement(i, mockMethod.getIndexForMockState());
generateCallToInvocationHandler();
}
private void generateConditionalCallForMockedMethod(@Nullable Label startOfRealImplementation)
{
if (startOfRealImplementation == null) {
//noinspection AssignmentToMethodParameter
startOfRealImplementation = new Label();
}
mw.visitJumpInsn(IFEQ, startOfRealImplementation);
generateCallToMockMethod();
generateMethodReturn();
mw.visitLabel(startOfRealImplementation);
}
private void generateConditionalCallForMockedConstructor()
{
generateCallToMockMethod();
int jumpInsnOpcode;
if (shouldUseMockingBridge()) {
mw.visitLdcInsn(VOID_TYPE);
jumpInsnOpcode = IF_ACMPEQ;
}
else {
jumpInsnOpcode = mockMethod.hasInvocationParameter ? IFNE : IFEQ;
}
Label startOfRealImplementation = new Label();
mw.visitJumpInsn(jumpInsnOpcode, startOfRealImplementation);
mw.visitInsn(RETURN);
mw.visitLabel(startOfRealImplementation);
}
private void generateCallToMockMethod()
{
if (shouldUseMockingBridge()) {
generateCallToMockMethodThroughMockingBridge();
}
else {
generateDirectCallToMockMethod();
}
}
private boolean shouldUseMockingBridge() { return useMockingBridge || !mockMethod.isPublic(); }
private void generateCallToMockMethodThroughMockingBridge()
{
generateCodeToObtainInstanceOfMockingBridge(MockMethodBridge.MB);
// First and second "invoke" arguments:
boolean isStatic = generateCodeToPassThisOrNullIfStaticMethod();
mw.visitInsn(ACONST_NULL);
// Create array for call arguments (third "invoke" argument):
Type[] argTypes = Type.getArgumentTypes(methodDesc);
generateCodeToCreateArrayOfObject(mw, 6 + argTypes.length);
int i = 0;
generateCodeToFillArrayElement(i++, mockMethods.getMockClassInternalName());
generateCodeToFillArrayElement(i++, classDesc);
generateCodeToFillArrayElement(i++, methodAccess);
if (mockMethod.isAdvice) {
generateCodeToFillArrayElement(i++, methodName);
generateCodeToFillArrayElement(i++, methodDesc);
}
else {
generateCodeToFillArrayElement(i++, mockMethod.name);
generateCodeToFillArrayElement(i++, mockMethod.desc);
}
generateCodeToFillArrayElement(i++, mockMethod.getIndexForMockState());
generateCodeToFillArrayWithParameterValues(mw, argTypes, i, isStatic ? 0 : 1);
generateCallToInvocationHandler();
}
private void generateDirectCallToMockMethod()
{
String mockClassDesc = mockMethods.getMockClassInternalName();
int invokeOpcode;
if (mockMethod.isStatic()) {
invokeOpcode = INVOKESTATIC;
}
else {
generateCodeToObtainMockUpInstance(mockClassDesc);
invokeOpcode = INVOKEVIRTUAL;
}
boolean canProceedIntoConstructor = generateArgumentsForMockMethodInvocation();
mw.visitMethodInsn(invokeOpcode, mockClassDesc, mockMethod.name, mockMethod.desc, false);
if (canProceedIntoConstructor) {
mw.visitMethodInsn(
INVOKEVIRTUAL, "mockit/internal/mockups/MockInvocation", "shouldProceedIntoConstructor", "()Z", false);
}
}
private void generateCodeToObtainMockUpInstance(@Nonnull String mockClassDesc)
{
mw.visitLdcInsn(mockClassDesc);
generateCodeToPassThisOrNullIfStaticMethod();
mw.visitMethodInsn(
INVOKESTATIC, "mockit/internal/state/TestRun", "getMock",
"(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;", false);
mw.visitTypeInsn(CHECKCAST, mockClassDesc);
}
private boolean generateArgumentsForMockMethodInvocation()
{
Type[] argTypes = Type.getArgumentTypes(mockMethod.mockDescWithoutInvocationParameter);
int varIndex = isStatic(methodAccess) ? 0 : 1;
boolean canProceedIntoConstructor = false;
if (mockMethod.hasInvocationParameter) {
generateCallToCreateNewMockInvocation(argTypes, varIndex);
// When invoking a constructor, the invocation object will need to be consulted for proceeding:
if (isConstructor) {
mw.visitInsn(mockMethod.isStatic() ? DUP : DUP_X1);
canProceedIntoConstructor = true;
}
}
boolean forGenericMethod = mockMethod.isForGenericMethod();
for (Type argType : argTypes) {
int opcode = argType.getOpcode(ILOAD);
mw.visitVarInsn(opcode, varIndex);
if (forGenericMethod && argType.getSort() >= Type.ARRAY) {
mw.visitTypeInsn(CHECKCAST, argType.getInternalName());
}
varIndex += argType.getSize();
}
return canProceedIntoConstructor;
}
private void generateCallToCreateNewMockInvocation(@Nonnull Type[] argTypes, int initialParameterIndex)
{
generateCodeToPassThisOrNullIfStaticMethod();
int argCount = argTypes.length;
if (argCount == 0) {
mw.visitInsn(ACONST_NULL);
}
else {
generateCodeToCreateArrayOfObject(mw, argCount);
generateCodeToFillArrayWithParameterValues(mw, argTypes, 0, initialParameterIndex);
}
mw.visitLdcInsn(mockMethods.getMockClassInternalName());
mw.visitIntInsn(SIPUSH, mockMethod.getIndexForMockState());
mw.visitLdcInsn(classDesc);
mw.visitLdcInsn(methodName);
mw.visitLdcInsn(methodDesc);
mw.visitMethodInsn(
INVOKESTATIC, "mockit/internal/mockups/MockInvocation", "create",
"(Ljava/lang/Object;[Ljava/lang/Object;Ljava/lang/String;I" +
"Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lmockit/internal/mockups/MockInvocation;", false);
}
private void generateMethodReturn()
{
if (shouldUseMockingBridge()) {
generateReturnWithObjectAtTopOfTheStack(methodDesc);
}
else {
Type returnType = Type.getReturnType(methodDesc);
mw.visitInsn(returnType.getOpcode(IRETURN));
}
}
}