support set dump path for single class

This commit is contained in:
金戟 2021-03-27 00:23:19 +08:00
parent 55ccc385d5
commit 26d3178840
6 changed files with 61 additions and 19 deletions

View File

@ -47,7 +47,6 @@
| --- | --- | --- | ---- | --- | | --- | --- | --- | ---- | --- |
| N/A | Class | 否 | NullType.class | 指定使用的Mock容器类 | | N/A | Class | 否 | NullType.class | 指定使用的Mock容器类 |
| treatAs | ClassType | 否 | ClassType.GuessByName | 指定当前类是测试类或被测类 | | treatAs | ClassType | 否 | ClassType.GuessByName | 指定当前类是测试类或被测类 |
| diagnose | LogLevel | 否 | N/A | (**deprecated**)指定Mock诊断日志级别 |
#### @MockDiagnose #### @MockDiagnose

View File

@ -10,6 +10,7 @@ public class ConstPool {
public static final String FIELD_SCOPE = "scope"; public static final String FIELD_SCOPE = "scope";
public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith"; public static final String MOCK_WITH = "com.alibaba.testable.core.annotation.MockWith";
public static final String DUMP_TO = "com.alibaba.testable.core.annotation.DumpTo";
public static final String MOCK_DIAGNOSE = "com.alibaba.testable.core.annotation.MockDiagnose"; public static final String MOCK_DIAGNOSE = "com.alibaba.testable.core.annotation.MockDiagnose";
public static final String MOCK_METHOD = "com.alibaba.testable.core.annotation.MockMethod"; 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 MOCK_CONSTRUCTOR = "com.alibaba.testable.core.annotation.MockConstructor";

View File

@ -45,11 +45,10 @@ public class MockClassParser {
/** /**
* Check whether any method in specified class has mock-related annotation * Check whether any method in specified class has mock-related annotation
* *
* @param className class that need to explore * @param cn class that need to explore
* @return found annotation or not * @return found annotation or not
*/ */
public boolean isMockClass(String className) { public boolean isMockClass(ClassNode cn) {
ClassNode cn = ClassUtil.getClassNode(className);
if (cn == null) { if (cn == null) {
return false; return false;
} }

View File

@ -16,7 +16,6 @@ import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InnerClassNode; import org.objectweb.asm.tree.InnerClassNode;
import javax.lang.model.type.NullType; import javax.lang.model.type.NullType;
import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.ClassFileTransformer;
@ -36,6 +35,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
private static final String FIELD_VALUE = "value"; private static final String FIELD_VALUE = "value";
private static final String FIELD_TREAT_AS = "treatAs"; private static final String FIELD_TREAT_AS = "treatAs";
private static final String FIELD_PATH = "path";
private static final String COMMA = ","; private static final String COMMA = ",";
private static final String CLASS_NAME_MOCK = "Mock"; private static final String CLASS_NAME_MOCK = "Mock";
@ -55,19 +55,28 @@ public class TestableClassTransformer implements ClassFileTransformer {
} }
LogUtil.verbose("Handle class: " + className); LogUtil.verbose("Handle class: " + className);
byte[] bytes = new OmniClassHandler().getBytes(classFileBuffer); byte[] bytes = new OmniClassHandler().getBytes(classFileBuffer);
ClassNode cn = ClassUtil.getClassNode(className);
if (cn != null) {
return transformMock(bytes, cn);
}
return bytes;
}
private byte[] transformMock(byte[] bytes, ClassNode cn) {
String className = cn.name;
try { try {
if (mockClassParser.isMockClass(className)) { if (mockClassParser.isMockClass(cn)) {
// it's a mock class // it's a mock class
LogUtil.diagnose("Handling mock class %s", className); LogUtil.diagnose("Handling mock class %s", className);
bytes = new MockClassHandler(className).getBytes(bytes); bytes = new MockClassHandler(className).getBytes(bytes);
dumpByte(className, bytes); dumpByte(className, GlobalConfig.getDumpPath(), bytes);
} else { } else {
String mockClass = foundMockForTestClass(className); String mockClass = foundMockForTestClass(className);
if (mockClass != null) { if (mockClass != null) {
// it's a test class with testable enabled // it's a test class with testable enabled
LogUtil.diagnose("Handling test class %s", className); LogUtil.diagnose("Handling test class %s", className);
bytes = new TestClassHandler().getBytes(bytes); bytes = new TestClassHandler().getBytes(bytes);
dumpByte(className, bytes); dumpByte(className, GlobalConfig.getDumpPath(), bytes);
} else { } else {
mockClass = foundMockForSourceClass(className); mockClass = foundMockForSourceClass(className);
if (mockClass != null) { if (mockClass != null) {
@ -75,7 +84,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
List<MethodInfo> injectMethods = mockClassParser.getTestableMockMethods(mockClass); List<MethodInfo> injectMethods = mockClassParser.getTestableMockMethods(mockClass);
LogUtil.diagnose("Handling source class %s", className); LogUtil.diagnose("Handling source class %s", className);
bytes = new SourceClassHandler(injectMethods, mockClass).getBytes(bytes); bytes = new SourceClassHandler(injectMethods, mockClass).getBytes(bytes);
dumpByte(className, bytes); dumpByte(className, GlobalConfig.getDumpPath(), bytes);
} }
} }
} }
@ -86,16 +95,16 @@ public class TestableClassTransformer implements ClassFileTransformer {
} finally { } finally {
LogUtil.resetLogLevel(); LogUtil.resetLogLevel();
} }
dumpByte(className, getDumpPathByAnnotation(cn), bytes);
return bytes; return bytes;
} }
private void dumpByte(String className, byte[] bytes) { private void dumpByte(String className, String dumpPath, byte[] bytes) {
String dumpDir = GlobalConfig.getDumpPath(); if (dumpPath == null) {
if (dumpDir == null || dumpDir.isEmpty() || !new File(dumpDir).isDirectory()) {
return; return;
} }
try { try {
String dumpFile = StringUtil.joinPath(dumpDir, String dumpFile = StringUtil.joinPath(dumpPath,
className.replace(SLASH, DOT).replace(DOLLAR, UNDERLINE) + ".class"); className.replace(SLASH, DOT).replace(DOLLAR, UNDERLINE) + ".class");
LogUtil.verbose("Dump class: " + dumpFile); LogUtil.verbose("Dump class: " + dumpFile);
FileOutputStream stream = new FileOutputStream(dumpFile); FileOutputStream stream = new FileOutputStream(dumpFile);
@ -139,10 +148,9 @@ public class TestableClassTransformer implements ClassFileTransformer {
} }
private String lookForOuterMockClass(String className) { private String lookForOuterMockClass(String className) {
String mockClass; String mockClassName = ClassUtil.getMockClassName(ClassUtil.getSourceClassName(className));
mockClass = ClassUtil.getMockClassName(ClassUtil.getSourceClassName(className)); if (mockClassParser.isMockClass(ClassUtil.getClassNode(mockClassName))) {
if (mockClassParser.isMockClass(mockClass)) { return mockClassName;
return mockClass;
} }
return null; return null;
} }
@ -193,7 +201,8 @@ public class TestableClassTransformer implements ClassFileTransformer {
*/ */
private String lookForInnerMockClass(ClassNode cn) { private String lookForInnerMockClass(ClassNode cn) {
for (InnerClassNode ic : cn.innerClasses) { for (InnerClassNode ic : cn.innerClasses) {
if (ic.name.equals(getInnerMockClassName(cn.name)) && mockClassParser.isMockClass(ic.name)) { ClassNode innerClassNode = ClassUtil.getClassNode(ic.name);
if (ic.name.equals(getInnerMockClassName(cn.name)) && mockClassParser.isMockClass(innerClassNode)) {
if ((ic.access & ACC_STATIC) == 0) { if ((ic.access & ACC_STATIC) == 0) {
LogUtil.warn(String.format("Mock class in \"%s\" is not declared as static", cn.name)); LogUtil.warn(String.format("Mock class in \"%s\" is not declared as static", cn.name));
} else { } else {
@ -244,6 +253,17 @@ public class TestableClassTransformer implements ClassFileTransformer {
return null; return null;
} }
private String getDumpPathByAnnotation(ClassNode cn) {
if (cn.visibleAnnotations != null) {
for (AnnotationNode an : cn.visibleAnnotations) {
if (toJavaStyleClassName(an.desc).equals(ConstPool.DUMP_TO)) {
return AnnotationUtil.getAnnotationParameter(an, FIELD_PATH, null, String.class);
}
}
}
return null;
}
private boolean isExpectedType(String className, ClassType type, ClassType expectedType) { private boolean isExpectedType(String className, ClassType type, ClassType expectedType) {
if (type.equals(ClassType.GuessByName)) { if (type.equals(ClassType.GuessByName)) {
return expectedType.equals(ClassType.TestClass) == className.endsWith(TEST_POSTFIX); return expectedType.equals(ClassType.TestClass) == className.endsWith(TEST_POSTFIX);

View File

@ -3,6 +3,8 @@ package com.alibaba.testable.agent.util;
import com.alibaba.testable.core.model.MockScope; import com.alibaba.testable.core.model.MockScope;
import com.alibaba.testable.core.util.LogUtil; import com.alibaba.testable.core.util.LogUtil;
import java.io.File;
/** /**
* @author flin * @author flin
*/ */
@ -31,7 +33,7 @@ public class GlobalConfig {
} }
public static String getDumpPath() { public static String getDumpPath() {
return dumpPath; return (dumpPath == null || dumpPath.isEmpty() || !new File(dumpPath).isDirectory()) ? null : dumpPath;
} }
public static void setDumpPath(String path) { public static void setDumpPath(String path) {

View File

@ -0,0 +1,21 @@
package com.alibaba.testable.core.annotation;
import java.lang.annotation.*;
/**
* Dump byte code for single class
*
* @author flin
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface DumpTo {
/**
* dump class byte code to specified folder
* @return an exist folder
*/
String path() default "";
}