diff --git a/agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java b/agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java index e9889bc..0514a6e 100644 --- a/agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java +++ b/agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java @@ -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 methods = new ArrayList(); 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 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, diff --git a/agent/src/main/java/com/alibaba/testable/agent/model/MethodInfo.java b/agent/src/main/java/com/alibaba/testable/agent/model/MethodInfo.java index 477914f..8906e94 100644 --- a/agent/src/main/java/com/alibaba/testable/agent/model/MethodInfo.java +++ b/agent/src/main/java/com/alibaba/testable/agent/model/MethodInfo.java @@ -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; } - } diff --git a/agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java b/agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java index 5875b3b..3cd6c82 100644 --- a/agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java +++ b/agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java @@ -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 annotations = ClassUtil.getAnnotations(className); - List testAnnotations = ClassUtil.getAnnotations(className + ConstPool.TEST_POSTFIX); + List 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 injectMethods = ClassUtil.getTestableInjectMethods(className + ConstPool.TEST_POSTFIX); + List 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); } diff --git a/agent/src/main/java/com/alibaba/testable/agent/util/ClassUtil.java b/agent/src/main/java/com/alibaba/testable/agent/util/ClassUtil.java index 4745165..fe22b9e 100644 --- a/agent/src/main/java/com/alibaba/testable/agent/util/ClassUtil.java +++ b/agent/src/main/java/com/alibaba/testable/agent/util/ClassUtil.java @@ -31,6 +31,9 @@ public class ClassUtil { private static final char TYPE_ARRAY = '['; private static final Map TYPE_MAPPING = new HashMap(); + 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 getTestableInjectMethods(String className) { try { List methodInfos = new ArrayList(); 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 methodInfos, MethodNode mn) { + private static void checkMethodAnnotation(ClassNode cn, List 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 getParameterTypes(String desc) { List parameterTypes = new ArrayList(); 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) { diff --git a/agent/src/main/java/com/alibaba/testable/agent/util/StringUtil.java b/agent/src/main/java/com/alibaba/testable/agent/util/StringUtil.java index 0eaec24..1e95315 100644 --- a/agent/src/main/java/com/alibaba/testable/agent/util/StringUtil.java +++ b/agent/src/main/java/com/alibaba/testable/agent/util/StringUtil.java @@ -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()); + } + } diff --git a/core/src/main/java/com/alibaba/testable/core/annotation/EnableTestable.java b/core/src/main/java/com/alibaba/testable/core/annotation/EnableTestable.java index 886658e..52e3ea1 100644 --- a/core/src/main/java/com/alibaba/testable/core/annotation/EnableTestable.java +++ b/core/src/main/java/com/alibaba/testable/core/annotation/EnableTestable.java @@ -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; - } diff --git a/core/src/main/java/com/alibaba/testable/core/annotation/EnableTestableInject.java b/core/src/main/java/com/alibaba/testable/core/annotation/EnableTestableInject.java index 16b1b1d..9a19d3d 100644 --- a/core/src/main/java/com/alibaba/testable/core/annotation/EnableTestableInject.java +++ b/core/src/main/java/com/alibaba/testable/core/annotation/EnableTestableInject.java @@ -11,10 +11,4 @@ import java.lang.annotation.*; @Target(ElementType.TYPE) @Documented public @interface EnableTestableInject { - - /** - * Test class names - */ - String[] value(); - } diff --git a/core/src/main/java/com/alibaba/testable/core/annotation/TestableInject.java b/core/src/main/java/com/alibaba/testable/core/annotation/TestableInject.java index e047dec..06d17d9 100644 --- a/core/src/main/java/com/alibaba/testable/core/annotation/TestableInject.java +++ b/core/src/main/java/com/alibaba/testable/core/annotation/TestableInject.java @@ -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 ""; + }