inject singleton field and method to mock class

This commit is contained in:
金戟 2021-02-10 13:09:56 +08:00
parent 948878687c
commit cefc5fb1df
8 changed files with 83 additions and 24 deletions

View File

@ -7,9 +7,12 @@ public class ConstPool {
public static final String DOT = ".";
public static final String SLASH = "/";
public static final String DOLLAR = "$";
public static final String UNDERLINE = "_";
public static final String TEST_POSTFIX = "Test";
public static final String MOCK_POSTFIX = "Mock";
public static final String INNER_MOCK_CLASS = "$Mock";
public static final String FIELD_TARGET_METHOD = "targetMethod";
public static final String FIELD_TARGET_CLASS = "targetClass";
@ -18,8 +21,6 @@ public class ConstPool {
public static final String MOCK_METHOD = "com.alibaba.testable.core.annotation.MockMethod";
public static final String MOCK_CONSTRUCTOR = "com.alibaba.testable.core.annotation.MockConstructor";
public static final String CGLIB_CLASS_INFIX = "$$EnhancerBy";
public static final String KOTLIN_POSTFIX_COMPANION = "$Companion";
public static final String KOTLIN_PREFIX_ACCESS = "access$";

View File

@ -15,6 +15,8 @@ import java.util.Iterator;
*/
abstract public class BaseClassHandler implements Opcodes {
protected String mockClassName;
protected boolean wasTransformed(ClassNode cn, String refName, String refDescriptor) {
Iterator<FieldNode> iterator = cn.fields.iterator();
if (iterator.hasNext()) {
@ -24,7 +26,6 @@ abstract public class BaseClassHandler implements Opcodes {
return true;
}
}
// TODO: `ACC_STATIC` should be removed in v0.5 to shorten the life cycle of this variable
cn.fields.add(new FieldNode(ACC_PRIVATE | ACC_STATIC, refName, refDescriptor, null, null));
return false;
}

View File

@ -1,15 +1,52 @@
package com.alibaba.testable.agent.handler;
import org.objectweb.asm.tree.ClassNode;
import com.alibaba.testable.agent.util.ClassUtil;
import org.objectweb.asm.tree.*;
import static com.alibaba.testable.agent.constant.ConstPool.CONSTRUCTOR;
/**
* @author flin
*/
public class MockClassHandler extends BaseClassHandler {
private static final String REF_INSTANCE = "_instance";
private static final String REF_GET_INSTANCE = "getInstance";
private static final String VOID_ARGS = "()";
private static final String VOID_RES = "V";
public MockClassHandler(String className) {
this.mockClassName = className;
}
@Override
protected void transform(ClassNode cn) {
if (wasTransformed(cn, REF_INSTANCE, ClassUtil.toByteCodeClassName(mockClassName))) {
return;
}
addGetInstanceMethod(cn);
}
private void addGetInstanceMethod(ClassNode cn) {
MethodNode getInstanceMethod = new MethodNode(ACC_PUBLIC | ACC_STATIC, REF_GET_INSTANCE,
VOID_ARGS + ClassUtil.toByteCodeClassName(mockClassName), null, null);
InsnList il = new InsnList();
il.add(new FieldInsnNode(GETSTATIC, mockClassName, REF_INSTANCE,
ClassUtil.toByteCodeClassName(mockClassName)));
LabelNode label = new LabelNode();
il.add(new JumpInsnNode(IFNONNULL, label));
il.add(new TypeInsnNode(NEW, mockClassName));
il.add(new InsnNode(DUP));
il.add(new MethodInsnNode(INVOKESPECIAL, mockClassName, CONSTRUCTOR, VOID_ARGS + VOID_RES, false));
il.add(new FieldInsnNode(PUTSTATIC, mockClassName, REF_INSTANCE, ClassUtil.toByteCodeClassName(mockClassName)));
il.add(label);
il.add(new FrameNode(F_SAME, 0, null, 0, null));
il.add(new FieldInsnNode(GETSTATIC, mockClassName, REF_INSTANCE, ClassUtil.toByteCodeClassName(mockClassName)));
il.add(new InsnNode(ARETURN));
getInstanceMethod.instructions = il;
getInstanceMethod.maxStack = 2;
getInstanceMethod.maxLocals = 0;
cn.methods.add(getInstanceMethod);
}
}

View File

@ -29,8 +29,9 @@ public class SourceClassHandler extends BaseClassHandler {
add(Opcodes.INVOKEINTERFACE);
}};
public SourceClassHandler(List<MethodInfo> injectMethods) {
public SourceClassHandler(List<MethodInfo> injectMethods, String mockClassName) {
this.injectMethods = injectMethods;
this.mockClassName = mockClassName;
}
/**

View File

@ -11,6 +11,7 @@ import org.objectweb.asm.tree.*;
import javax.lang.model.type.NullType;
import java.util.List;
import static com.alibaba.testable.agent.constant.ConstPool.CONSTRUCTOR;
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
/**
@ -35,13 +36,17 @@ public class TestClassHandler extends BaseClassHandler {
private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;ZZ)V";
private static final String SIGNATURE_PARAMETERS = "Ljava/util/Map;";
public TestClassHandler(String mockClassName) {
this.mockClassName = mockClassName;
}
/**
* Handle bytecode of test class
* @param cn original class node
*/
@Override
protected void transform(ClassNode cn) {
if (wasTransformed(cn, REF_TESTABLE_CONTEXT, ClassUtil.toByteCodeClassName(CLASS_MOCK_CONTEXT))) {
if (wasTransformed(cn, REF_TESTABLE_CONTEXT, ClassUtil.toByteCodeClassName(mockClassName))) {
return;
}
for (MethodNode mn : cn.methods) {
@ -58,7 +63,7 @@ public class TestClassHandler extends BaseClassHandler {
InsnList il = new InsnList();
il.add(new TypeInsnNode(NEW, CLASS_MOCK_CONTEXT));
il.add(new InsnNode(DUP));
il.add(new MethodInsnNode(INVOKESPECIAL, CLASS_MOCK_CONTEXT, "<init>", "()V", false));
il.add(new MethodInsnNode(INVOKESPECIAL, CLASS_MOCK_CONTEXT, CONSTRUCTOR, "()V", false));
il.add(new FieldInsnNode(PUTSTATIC, cn.name, REF_TESTABLE_CONTEXT,
ClassUtil.toByteCodeClassName(CLASS_MOCK_CONTEXT)));
mn.instructions.insertBefore(mn.instructions.get(0), il);

View File

@ -28,8 +28,7 @@ import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import static com.alibaba.testable.agent.constant.ConstPool.DOT;
import static com.alibaba.testable.agent.constant.ConstPool.SLASH;
import static com.alibaba.testable.agent.constant.ConstPool.*;
import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassName;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
@ -68,25 +67,27 @@ public class TestableClassTransformer implements ClassFileTransformer {
LogUtil.verbose("Handle class: " + className);
byte[] bytes = null;
try {
String mockClass = foundMockForSourceClass(className);
if (mockClass != null) {
// it's a source class with testable enabled
List<MethodInfo> injectMethods = getTestableMockMethods(mockClass);
LogUtil.diagnose("Handling source class %s", className);
bytes = new SourceClassHandler(injectMethods).getBytes(classFileBuffer);
if (isMockClass(className)) {
// it's a mock class
LogUtil.diagnose("Handling mock class %s", className);
bytes = new MockClassHandler(className).getBytes(classFileBuffer);
dumpByte(className, bytes);
} else {
mockClass = foundMockForTestClass(className);
String mockClass = foundMockForTestClass(className);
if (mockClass != null) {
// it's a test class with testable enabled
LogUtil.diagnose("Handling test class %s", className);
bytes = new TestClassHandler().getBytes(classFileBuffer);
dumpByte(className, bytes);
} else if (isMockClass(className)) {
// it's a mock class
LogUtil.diagnose("Handling mock class %s", className);
bytes = new MockClassHandler().getBytes(classFileBuffer);
bytes = new TestClassHandler(mockClass).getBytes(classFileBuffer);
dumpByte(className, bytes);
} else {
mockClass = foundMockForSourceClass(className);
if (mockClass != null) {
// it's a source class with testable enabled
List<MethodInfo> injectMethods = getTestableMockMethods(mockClass);
LogUtil.diagnose("Handling source class %s", className);
bytes = new SourceClassHandler(injectMethods, mockClass).getBytes(classFileBuffer);
dumpByte(className, bytes);
}
}
}
} catch (Throwable t) {
@ -104,7 +105,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
return;
}
try {
String dumpFile = StringUtil.joinPath(dumpDir, className.replaceAll(SLASH, DOT) + ".class");
String dumpFile = StringUtil.joinPath(dumpDir, className.replace(SLASH, DOT).replace(DOLLAR, UNDERLINE) + ".class");
LogUtil.verbose("Dump class: " + dumpFile);
FileOutputStream stream = new FileOutputStream(dumpFile);
stream.write(bytes);
@ -127,6 +128,10 @@ public class TestableClassTransformer implements ClassFileTransformer {
if (mockClass != null) {
return mockClass;
}
mockClass = ClassUtil.getInnerMockClassName(className);
if (isMockClass(mockClass)) {
return mockClass;
}
mockClass = ClassUtil.getMockClassName(ClassUtil.getSourceClassName(className));
if (isMockClass(mockClass)) {
return mockClass;

View File

@ -84,6 +84,15 @@ public class ClassUtil {
name.substring(ConstPool.KOTLIN_PREFIX_ACCESS.length()) : name;
}
/**
* get inner mock class name from test class name
* @param testClassName test class name
* @return mock class name
*/
public static String getInnerMockClassName(String testClassName) {
return testClassName + ConstPool.INNER_MOCK_CLASS;
}
/**
* get mock class name from source class name
* @param sourceClassName source class name

View File

@ -15,7 +15,7 @@ import static org.objectweb.asm.Opcodes.*;
class SourceClassHandlerTest {
private final SourceClassHandler handler = new SourceClassHandler(new ArrayList<MethodInfo>());
private final SourceClassHandler handler = new SourceClassHandler(new ArrayList<MethodInfo>(), "");
@Test
void should_get_member_method_start() {