diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/MockClassHandler.java b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/MockClassHandler.java index 6d06a5c..5d37bf0 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/MockClassHandler.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/MockClassHandler.java @@ -77,13 +77,10 @@ public class MockClassHandler extends BaseClassWithContextHandler { */ private void injectInheritedMockMethods(ClassNode cn) { List inheritedTypes = new ArrayList(); - if (cn.visibleAnnotations != null) { - for (AnnotationNode an : cn.visibleAnnotations) { - if ((ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONTAINER)).equals(an.desc)) { - inheritedTypes.addAll(AnnotationUtil.getAnnotationParameter(an, FIELD_INHERITS, - Collections.emptyList(), List.class)); - } - } + AnnotationNode an = AnnotationUtil.getClassAnnotation(cn, MOCK_CONTAINER); + if (an != null) { + inheritedTypes.addAll(AnnotationUtil.getAnnotationParameter(an, FIELD_INHERITS, + Collections.emptyList(), List.class)); } for (Type inheritedType : inheritedTypes) { String className = inheritedType.getClassName(); diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/OmniClassHandler.java b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/OmniClassHandler.java index 4de66d0..5e53973 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/OmniClassHandler.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/OmniClassHandler.java @@ -2,6 +2,7 @@ package com.alibaba.testable.agent.handler; import com.alibaba.testable.agent.handler.test.JUnit4Framework; import com.alibaba.testable.agent.handler.test.JUnit5Framework; +import com.alibaba.testable.agent.util.AnnotationUtil; import com.alibaba.testable.agent.util.ClassUtil; import com.alibaba.testable.core.util.CollectionUtil; import org.objectweb.asm.Label; @@ -24,7 +25,7 @@ public class OmniClassHandler extends BaseClassHandler { private static final String METHOD_START = "("; private static final String VOID_METHOD_END = ")V"; private static final String VOID_METHOD = "()V"; - private static final String ENABLE_CONFIGURATION = "Lorg/springframework/context/annotation/Configuration;"; + private static final String ENABLE_CONFIGURATION = "org.springframework.context.annotation.Configuration"; private static final String CLASS_ABSTRACT_COLLECTION = "java/util/AbstractCollection"; private static final String CLASS_NUMBER = "java/lang/Number"; private static final String CLASS_HASH_SET = "java/util/HashSet"; @@ -38,7 +39,8 @@ public class OmniClassHandler extends BaseClassHandler { @Override protected void transform(ClassNode cn) { - if (isInterfaceOrAtom(cn) || isUniqueConstructorClass(cn) || isUninstantiableClass(cn) || hasSpecialAnnotation(cn)) { + if (isInterfaceOrAtom(cn) || isUniqueConstructorClass(cn) || isUninstantiableClass(cn) || + AnnotationUtil.getClassAnnotation(cn, ENABLE_CONFIGURATION) != null) { return; } addConstructorWithVoidTypeParameter(cn); @@ -62,18 +64,6 @@ public class OmniClassHandler extends BaseClassHandler { cn.methods.add(constructor); } - private boolean hasSpecialAnnotation(ClassNode cn) { - if (cn.visibleAnnotations == null) { - return false; - } - for (AnnotationNode an : cn.visibleAnnotations) { - if (an.desc.equals(ENABLE_CONFIGURATION)) { - return true; - } - } - return false; - } - private boolean isUninstantiableClass(ClassNode cn) { // if the class has no even default constructor, skip it for (MethodNode mn : cn.methods) { diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/MockClassParser.java b/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/MockClassParser.java index 3a784b1..0a93b91 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/MockClassParser.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/MockClassParser.java @@ -44,9 +44,9 @@ public class MockClassParser { } /** - * Check whether any method in specified class has mock-related annotation + * Check whether specified class is declared as mock container or has any method with mock-related annotation * - * @param cn class that need to explore + * @param cn class to explore * @return found annotation or not */ public boolean isMockClass(ClassNode cn) { @@ -54,6 +54,9 @@ public class MockClassParser { return false; } DiagnoseUtil.setupByClass(cn); + if (AnnotationUtil.getClassAnnotation(cn, MOCK_CONTAINER) != null) { + return true; + } for (MethodNode mn : cn.name.endsWith(MOCK_POSTFIX) ? getAllMethods(cn) : cn.methods) { if (mn.visibleAnnotations != null) { for (AnnotationNode an : mn.visibleAnnotations) { @@ -97,21 +100,18 @@ public class MockClassParser { * Take care of @MockContainer annotation */ private void handleMockContainerInherits(List methodInfos, ClassNode cn) { - if (cn.visibleAnnotations != null) { - for (AnnotationNode an : cn.visibleAnnotations) { - if ((ClassUtil.toByteCodeClassName(ConstPool.MOCK_CONTAINER)).equals(an.desc)) { - for (Object st : AnnotationUtil.getAnnotationParameter(an, FIELD_INHERITS, - Collections.emptyList(), List.class)) { - String superClassName = ((Type)st).getClassName(); - ClassNode superCn = ClassUtil.getClassNode(superClassName); - if (superCn == null) { - LogUtil.warn("failed to load class '%s' inherited by '%s'", superClassName, cn.name); - continue; - } - for (MethodNode mn : getAllMethods(superCn)) { - addMethodWithAnnotationCheck(methodInfos, cn, mn); - } - } + AnnotationNode an = AnnotationUtil.getClassAnnotation(cn, MOCK_CONTAINER); + if (an != null) { + for (Object st : AnnotationUtil.getAnnotationParameter(an, FIELD_INHERITS, + Collections.emptyList(), List.class)) { + String superClassName = ((Type)st).getClassName(); + ClassNode superCn = ClassUtil.getClassNode(superClassName); + if (superCn == null) { + LogUtil.warn("failed to load class '%s' inherited by '%s'", superClassName, cn.name); + continue; + } + for (MethodNode mn : getAllMethods(superCn)) { + addMethodWithAnnotationCheck(methodInfos, cn, mn); } } } diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java b/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java index d66b93d..4862f3e 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/transformer/TestableClassTransformer.java @@ -19,8 +19,7 @@ import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; import java.util.List; -import static com.alibaba.testable.agent.constant.ConstPool.CGLIB_CLASS_PATTERN; -import static com.alibaba.testable.agent.constant.ConstPool.KOTLIN_POSTFIX_COMPANION; +import static com.alibaba.testable.agent.constant.ConstPool.*; import static com.alibaba.testable.core.constant.ConstPool.DOLLAR; import static com.alibaba.testable.core.constant.ConstPool.TEST_POSTFIX; import static org.objectweb.asm.Opcodes.ACC_STATIC; @@ -31,7 +30,7 @@ import static org.objectweb.asm.Opcodes.ACC_STATIC; public class TestableClassTransformer implements ClassFileTransformer { private static final String FIELD_TREAT_AS = "treatAs"; - private static final String CLASS_JUNIT_5_NESTED = "Lorg/junit/jupiter/api/Nested;"; + private static final String CLASS_JUNIT_5_NESTED = "org.junit.jupiter.api.Nested"; /** * Just avoid spend time to scan those surely non-user classes, should keep these lists as tiny as possible @@ -103,14 +102,17 @@ public class TestableClassTransformer implements ClassFileTransformer { private String foundMockForSourceClass(String name) { String className = (GlobalConfig.getMockPackageMapping() == null) ? name : mapPackage(name); + // handle @MockWith annotation on source class String mockClass = lookForMockWithAnnotationAsSourceClass(className); if (mockClass != null) { return mockClass; } - mockClass = foundMockForTestClass(ClassUtil.getTestClassName(className)); + // look for [ThisClass]Test.Mock and [ThisClass]Mock + mockClass = foundMockForStandardClass(className); if (mockClass != null) { return mockClass; } + // inner class should also look for mock class in the test class of its outer class return foundMockForInnerSourceClass(className); } @@ -127,32 +129,30 @@ public class TestableClassTransformer implements ClassFileTransformer { private String foundMockForInnerSourceClass(String className) { return (className.contains(DOLLAR) && !className.endsWith(KOTLIN_POSTFIX_COMPANION)) ? - foundMockForTestClass(ClassUtil.getTestClassName(className.substring(0, className.indexOf(DOLLAR)))) : null; + foundMockForStandardClass(className.substring(0, className.indexOf(DOLLAR))) : null; } - private String foundMockForTestClass(String className) { - ClassNode cn = adaptInnerClass(ClassUtil.getClassNode(className)); + private String foundMockForStandardClass(String className) { + ClassNode cn = adaptInnerClass(ClassUtil.getClassNode(ClassUtil.getTestClassName(className))); if (cn != null) { + // handle @MockWith annotation on test class String mockClass = lookForMockWithAnnotationAsTestClass(cn); if (mockClass != null) { return mockClass; } + // look for [ThisClass]Test.Mock mockClass = lookForInnerMockClass(cn); if (mockClass != null) { return mockClass; } } + // look for [ThisClass]Mock return lookForOuterMockClass(className); } private ClassNode adaptInnerClass(ClassNode cn) { - if (cn == null || cn.visibleAnnotations == null) { - return cn; - } - for (AnnotationNode an : cn.visibleAnnotations) { - if (an.desc.equals(CLASS_JUNIT_5_NESTED)) { - return ClassUtil.getClassNode(ClassUtil.toOuterClassName(cn.name)); - } + if (AnnotationUtil.getClassAnnotation(cn, CLASS_JUNIT_5_NESTED) != null) { + return ClassUtil.getClassNode(ClassUtil.toOuterClassName(cn.name)); } return cn; } @@ -214,7 +214,9 @@ public class TestableClassTransformer implements ClassFileTransformer { private String lookForInnerMockClass(ClassNode cn) { for (InnerClassNode ic : cn.innerClasses) { ClassNode innerClassNode = ClassUtil.getClassNode(ic.name); - if (ic.name.equals(getInnerMockClassName(cn.name)) && mockClassParser.isMockClass(innerClassNode)) { + boolean isNameMatched = ic.name.equals(getInnerMockClassName(cn.name)) || + AnnotationUtil.getClassAnnotation(innerClassNode, MOCK_CONTAINER) != null; + if (isNameMatched && mockClassParser.isMockClass(innerClassNode)) { if ((ic.access & ACC_STATIC) == 0) { LogUtil.warn("Mock class in \"%s\" is not declared as static", cn.name); } else { @@ -248,18 +250,15 @@ public class TestableClassTransformer implements ClassFileTransformer { * @return mock class name */ private String parseMockWithAnnotation(ClassNode cn, ClassType expectedType) { - if (cn.visibleAnnotations != null) { - for (AnnotationNode an : cn.visibleAnnotations) { - if ((ClassUtil.toByteCodeClassName(ConstPool.MOCK_WITH)).equals(an.desc)) { - ClassType type = AnnotationUtil.getAnnotationParameter(an, FIELD_TREAT_AS, ClassType.GuessByName, - ClassType.class); - if (isExpectedType(cn.name, type, expectedType)) { - Type clazz = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_VALUE, - Type.getType(NullType.class), Type.class); - DiagnoseUtil.setupByClass(ClassUtil.getClassNode(clazz.getClassName())); - return clazz.getClassName(); - } - } + AnnotationNode an = AnnotationUtil.getClassAnnotation(cn, MOCK_WITH); + if (an != null) { + ClassType type = AnnotationUtil.getAnnotationParameter(an, FIELD_TREAT_AS, ClassType.GuessByName, + ClassType.class); + if (isExpectedType(cn.name, type, expectedType)) { + Type clazz = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_VALUE, + Type.getType(NullType.class), Type.class); + DiagnoseUtil.setupByClass(ClassUtil.getClassNode(clazz.getClassName())); + return clazz.getClassName(); } } return null; diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/util/AnnotationUtil.java b/testable-agent/src/main/java/com/alibaba/testable/agent/util/AnnotationUtil.java index a09f01d..789f6ec 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/util/AnnotationUtil.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/util/AnnotationUtil.java @@ -1,6 +1,8 @@ package com.alibaba.testable.agent.util; +import com.alibaba.testable.agent.constant.ConstPool; import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; /** * @author flin @@ -60,4 +62,20 @@ public class AnnotationUtil { return false; } + /** + * Get specified annotation node from specified class, or null if the annotation not exist + * @param cn class to explore + * @param annotation name of annotation to look for + * @return the annotation instance or null + */ + public static AnnotationNode getClassAnnotation(ClassNode cn, String annotation) { + if (cn != null && cn.visibleAnnotations != null) { + for (AnnotationNode an : cn.visibleAnnotations) { + if (ClassUtil.toByteCodeClassName(annotation).equals(an.desc)) { + return an; + } + } + } + return null; + } } diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/util/BytecodeUtil.java b/testable-agent/src/main/java/com/alibaba/testable/agent/util/BytecodeUtil.java index ecd3636..e7a814b 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/util/BytecodeUtil.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/util/BytecodeUtil.java @@ -1,7 +1,6 @@ package com.alibaba.testable.agent.util; import com.alibaba.testable.agent.constant.ByteCodeConst; -import com.alibaba.testable.agent.constant.ConstPool; import com.alibaba.testable.agent.tool.ImmutablePair; import com.alibaba.testable.core.util.LogUtil; import org.objectweb.asm.tree.*; @@ -11,8 +10,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -import static com.alibaba.testable.agent.constant.ConstPool.FIELD_VALUE; -import static com.alibaba.testable.agent.constant.ConstPool.PROPERTY_USER_DIR; +import static com.alibaba.testable.agent.constant.ConstPool.*; import static com.alibaba.testable.core.constant.ConstPool.*; import static com.alibaba.testable.core.util.PathUtil.createFolder; import static org.objectweb.asm.Opcodes.*; @@ -283,15 +281,12 @@ public class BytecodeUtil { } private static String getDumpPathByAnnotation(ClassNode cn) { - if (cn.visibleAnnotations != null) { - for (AnnotationNode an : cn.visibleAnnotations) { - if ((ClassUtil.toByteCodeClassName(ConstPool.DUMP_TO)).equals(an.desc)) { - String path = AnnotationUtil.getAnnotationParameter(an, FIELD_VALUE, null, String.class); - String fullPath = PathUtil.join(System.getProperty(PROPERTY_USER_DIR), path); - if (createFolder(fullPath)) { - return fullPath; - } - } + AnnotationNode an = AnnotationUtil.getClassAnnotation(cn, DUMP_TO); + if (an != null) { + String path = AnnotationUtil.getAnnotationParameter(an, FIELD_VALUE, null, String.class); + String fullPath = PathUtil.join(System.getProperty(PROPERTY_USER_DIR), path); + if (createFolder(fullPath)) { + return fullPath; } } return null; diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/util/DiagnoseUtil.java b/testable-agent/src/main/java/com/alibaba/testable/agent/util/DiagnoseUtil.java index 50b29b2..2ff4eca 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/util/DiagnoseUtil.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/util/DiagnoseUtil.java @@ -6,16 +6,14 @@ import com.alibaba.testable.core.util.LogUtil; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; +import static com.alibaba.testable.agent.constant.ConstPool.MOCK_DIAGNOSE; + public class DiagnoseUtil { public static void setupByClass(ClassNode cn) { - if (cn == null || cn.visibleAnnotations == null) { - return; - } - for (AnnotationNode an : cn.visibleAnnotations) { - if (ClassUtil.toByteCodeClassName(ConstPool.MOCK_DIAGNOSE).equals(an.desc)) { - setupDiagnose(an, ConstPool.FIELD_VALUE); - } + AnnotationNode an = AnnotationUtil.getClassAnnotation(cn, MOCK_DIAGNOSE); + if (an != null) { + setupDiagnose(an, ConstPool.FIELD_VALUE); } }