mirror of
https://github.com/alibaba/testable-mock.git
synced 2026-08-22 19:23:29 +08:00
support specify target class and method for injection
This commit is contained in:
@@ -4,6 +4,7 @@ import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.agent.util.CollectionUtil;
|
||||
import com.alibaba.testable.agent.util.StringUtil;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
@@ -31,7 +32,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
List<MethodInfo> methods = new ArrayList<MethodInfo>();
|
||||
for (MethodNode m : cn.methods) {
|
||||
if (!ConstPool.CONSTRUCTOR.equals(m.name)) {
|
||||
methods.add(new MethodInfo(m.name, m.desc));
|
||||
methods.add(new MethodInfo(cn.name, m.name, m.desc));
|
||||
}
|
||||
}
|
||||
Set<MethodInfo> memberInjectMethods = CollectionUtil.getCrossSet(methods, injectMethods);
|
||||
@@ -48,13 +49,15 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
do {
|
||||
if (instructions[i].getOpcode() == Opcodes.INVOKESPECIAL) {
|
||||
MethodInsnNode node = (MethodInsnNode)instructions[i];
|
||||
if (cn.name.equals(node.owner) && memberInjectMethods.contains(new MethodInfo(node.name, node.desc))) {
|
||||
if (cn.name.equals(node.owner) && memberInjectMethods.contains(new MethodInfo(cn.name, node.name, node.desc))) {
|
||||
// it's a member method and an inject method for it exist
|
||||
int rangeStart = getMemberMethodStart(instructions, i);
|
||||
if (rangeStart >= 0) {
|
||||
instructions = replaceMemberCallOps(cn, mn, instructions, rangeStart, i);
|
||||
i = rangeStart;
|
||||
}
|
||||
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
|
||||
// it's a new operation
|
||||
String newOperatorInjectMethodName = getNewOperatorInjectMethodName(newOperatorInjectMethods, node);
|
||||
if (newOperatorInjectMethodName.length() > 0) {
|
||||
int rangeStart = getConstructorStart(instructions, node.owner, i);
|
||||
@@ -105,7 +108,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
AbstractInsnNode[] instructions, int start, int end) {
|
||||
String classType = ((TypeInsnNode)instructions[start]).desc;
|
||||
String constructorDesc = ((MethodInsnNode)instructions[end]).desc;
|
||||
String testClassName = cn.name + ConstPool.TEST_POSTFIX;
|
||||
String testClassName = StringUtil.getTestClassName(cn.name);
|
||||
mn.instructions.insertBefore(instructions[start], new FieldInsnNode(GETSTATIC, testClassName,
|
||||
ConstPool.TESTABLE_INJECT_REF, ClassUtil.toByteCodeClassName(testClassName)));
|
||||
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
|
||||
@@ -124,7 +127,7 @@ public class SourceClassHandler extends BaseClassHandler {
|
||||
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
|
||||
int start, int end) {
|
||||
MethodInsnNode method = (MethodInsnNode)instructions[end];
|
||||
String testClassName = cn.name + ConstPool.TEST_POSTFIX;
|
||||
String testClassName = StringUtil.getTestClassName(cn.name);
|
||||
mn.instructions.insertBefore(instructions[start], new FieldInsnNode(GETSTATIC, testClassName,
|
||||
ConstPool.TESTABLE_INJECT_REF, ClassUtil.toByteCodeClassName(testClassName)));
|
||||
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
|
||||
|
||||
@@ -5,14 +5,20 @@ package com.alibaba.testable.agent.model;
|
||||
*/
|
||||
public class MethodInfo {
|
||||
|
||||
private final String clazz;
|
||||
private final String name;
|
||||
private final String desc;
|
||||
|
||||
public MethodInfo(String name, String desc) {
|
||||
public MethodInfo(String clazz, String name, String desc) {
|
||||
this.clazz = clazz;
|
||||
this.name = name;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public String getClazz() {
|
||||
return clazz;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
@@ -23,19 +29,21 @@ public class MethodInfo {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
if (this == o) { return true; }
|
||||
if (o == null || getClass() != o.getClass()) { return false; }
|
||||
|
||||
MethodInfo that = (MethodInfo)o;
|
||||
return name.equals(that.name) && desc.equals(that.desc);
|
||||
|
||||
if (!clazz.equals(that.clazz)) { return false; }
|
||||
if (!name.equals(that.name)) { return false; }
|
||||
return desc.equals(that.desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * name.hashCode() + desc.hashCode();
|
||||
int result = clazz.hashCode();
|
||||
result = 31 * result + name.hashCode();
|
||||
result = 31 * result + desc.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.alibaba.testable.agent.handler.SourceClassHandler;
|
||||
import com.alibaba.testable.agent.handler.TestClassHandler;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import com.alibaba.testable.agent.util.StringUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
@@ -24,18 +25,20 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
|
||||
ProtectionDomain protectionDomain, byte[] classFileBuffer) {
|
||||
if (isSystemClass(loader, className) || loadedClassNames.contains(className)) {
|
||||
// Ignore system class and duplicate class
|
||||
// Ignore system class and reloaded class
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> annotations = ClassUtil.getAnnotations(className);
|
||||
List<String> testAnnotations = ClassUtil.getAnnotations(className + ConstPool.TEST_POSTFIX);
|
||||
List<String> testAnnotations = ClassUtil.getAnnotations(StringUtil.getTestClassName(className));
|
||||
try {
|
||||
if (testAnnotations.contains(ConstPool.ENABLE_TESTABLE)) {
|
||||
// it's a source class with testable enabled
|
||||
loadedClassNames.add(className);
|
||||
List<MethodInfo> injectMethods = ClassUtil.getTestableInjectMethods(className + ConstPool.TEST_POSTFIX);
|
||||
List<MethodInfo> injectMethods = ClassUtil.getTestableInjectMethods(StringUtil.getTestClassName(className));
|
||||
return new SourceClassHandler(injectMethods).getBytes(className);
|
||||
} else if (annotations.contains(ConstPool.ENABLE_TESTABLE)) {
|
||||
// it's a test class with testable enabled
|
||||
loadedClassNames.add(className);
|
||||
return new TestClassHandler().getBytes(className);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ public class ClassUtil {
|
||||
private static final char TYPE_ARRAY = '[';
|
||||
|
||||
private static final Map<Character, String> TYPE_MAPPING = new HashMap<Character, String>();
|
||||
private static final String TARGET_CLASS = "targetClass";
|
||||
private static final String TARGET_METHOD = "targetMethod";
|
||||
|
||||
static {
|
||||
TYPE_MAPPING.put(TYPE_BYTE, "java/lang/Byte");
|
||||
TYPE_MAPPING.put(TYPE_CHAR, "java/lang/Character");
|
||||
@@ -60,13 +63,17 @@ public class ClassUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get testable inject method from test class
|
||||
* @param className test class name
|
||||
*/
|
||||
public static List<MethodInfo> getTestableInjectMethods(String className) {
|
||||
try {
|
||||
List<MethodInfo> methodInfos = new ArrayList<MethodInfo>();
|
||||
ClassNode cn = new ClassNode();
|
||||
new ClassReader(className).accept(cn, 0);
|
||||
for (MethodNode mn : cn.methods) {
|
||||
checkMethodAnnotation(methodInfos, mn);
|
||||
checkMethodAnnotation(cn, methodInfos, mn);
|
||||
}
|
||||
return methodInfos;
|
||||
} catch (Exception e) {
|
||||
@@ -74,18 +81,30 @@ public class ClassUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkMethodAnnotation(List<MethodInfo> methodInfos, MethodNode mn) {
|
||||
private static void checkMethodAnnotation(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
|
||||
if (mn.visibleAnnotations == null) {
|
||||
return;
|
||||
}
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_INJECT)) {
|
||||
methodInfos.add(new MethodInfo(mn.name, mn.desc));
|
||||
String targetClass = getAnnotationParameter(an, TARGET_CLASS, StringUtil.getSourceClassName(cn.name));
|
||||
String targetMethod = getAnnotationParameter(an, TARGET_METHOD, mn.name);
|
||||
methodInfos.add(new MethodInfo(toSlashSeparateName(targetClass), targetMethod, mn.desc));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String getAnnotationParameter(AnnotationNode an, String key, String defaultValue) {
|
||||
if (an.values != null) {
|
||||
int i = an.values.indexOf(key);
|
||||
if (i % 2 == 0) {
|
||||
return (String)an.values.get(i+1);
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static List<Byte> getParameterTypes(String desc) {
|
||||
List<Byte> parameterTypes = new ArrayList<Byte>();
|
||||
boolean travelingClass = false;
|
||||
@@ -122,8 +141,12 @@ public class ClassUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static String toSlashSeparateName(String name) {
|
||||
return name.replace(ConstPool.DOT, ConstPool.SLASH);
|
||||
}
|
||||
|
||||
public static String toByteCodeClassName(String className) {
|
||||
return TYPE_CLASS + className.replace(ConstPool.DOT, ConstPool.SLASH) + CLASS_END;
|
||||
return TYPE_CLASS + toSlashSeparateName(className) + CLASS_END;
|
||||
}
|
||||
|
||||
public static String toDotSeparateFullClassName(String className) {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class StringUtil {
|
||||
|
||||
/**
|
||||
* repeat a text many times
|
||||
* @param text content to repeat
|
||||
* @param times count of repeating
|
||||
*/
|
||||
public static String repeat(String text, int times) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < times; i++) {
|
||||
@@ -13,4 +20,20 @@ public class StringUtil {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* get test class name from source class name
|
||||
* @param sourceClassName source class name
|
||||
*/
|
||||
public static String getTestClassName(String sourceClassName) {
|
||||
return sourceClassName + ConstPool.TEST_POSTFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* get source class name from test class name
|
||||
* @param testClassName test class name
|
||||
*/
|
||||
public static String getSourceClassName(String testClassName) {
|
||||
return testClassName.substring(0, testClassName.length() - ConstPool.TEST_POSTFIX.length());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,10 +11,4 @@ import java.lang.annotation.*;
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
public @interface EnableTestable {
|
||||
|
||||
/**
|
||||
* Whether use compile time code modification
|
||||
*/
|
||||
boolean withoutAgent() default false;
|
||||
|
||||
}
|
||||
|
||||
@@ -11,10 +11,4 @@ import java.lang.annotation.*;
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
public @interface EnableTestableInject {
|
||||
|
||||
/**
|
||||
* Test class names
|
||||
*/
|
||||
String[] value();
|
||||
|
||||
}
|
||||
|
||||
@@ -11,4 +11,25 @@ import java.lang.annotation.*;
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
public @interface TestableInject {
|
||||
|
||||
/**
|
||||
* only enable for invocation from specified methods
|
||||
*/
|
||||
String[] sourceMethods() default {};
|
||||
|
||||
/**
|
||||
* only enable for invocation from specified test cases
|
||||
*/
|
||||
String[] testMethods() default {};
|
||||
|
||||
/**
|
||||
* mock method of specified class instead of the class under test
|
||||
*/
|
||||
String targetClass() default "";
|
||||
|
||||
/**
|
||||
* mock specified method instead of method with same name
|
||||
*/
|
||||
String targetMethod() default "";
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user