From cefc5fb1dfe2ac80c308c10a04add6d514ea6a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=91=E6=88=9F?= Date: Wed, 10 Feb 2021 13:09:56 +0800 Subject: [PATCH] inject singleton field and method to mock class --- .../testable/agent/constant/ConstPool.java | 5 ++- .../agent/handler/BaseClassHandler.java | 3 +- .../agent/handler/MockClassHandler.java | 39 ++++++++++++++++++- .../agent/handler/SourceClassHandler.java | 3 +- .../agent/handler/TestClassHandler.java | 9 ++++- .../transformer/TestableClassTransformer.java | 37 ++++++++++-------- .../testable/agent/util/ClassUtil.java | 9 +++++ .../agent/handler/SourceClassHandlerTest.java | 2 +- 8 files changed, 83 insertions(+), 24 deletions(-) diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/constant/ConstPool.java b/testable-agent/src/main/java/com/alibaba/testable/agent/constant/ConstPool.java index 0a9a44b..b30007e 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/constant/ConstPool.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/constant/ConstPool.java @@ -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$"; diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/BaseClassHandler.java b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/BaseClassHandler.java index 693cf07..7da4dde 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/BaseClassHandler.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/BaseClassHandler.java @@ -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 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; } 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 0389936..6ebdb57 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 @@ -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); } } diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java index e5a9e29..89e6c72 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/SourceClassHandler.java @@ -29,8 +29,9 @@ public class SourceClassHandler extends BaseClassHandler { add(Opcodes.INVOKEINTERFACE); }}; - public SourceClassHandler(List injectMethods) { + public SourceClassHandler(List injectMethods, String mockClassName) { this.injectMethods = injectMethods; + this.mockClassName = mockClassName; } /** diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java index 868e7dc..7b5a740 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/handler/TestClassHandler.java @@ -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, "", "()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); 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 055e18b..786ec2e 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 @@ -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 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 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; diff --git a/testable-agent/src/main/java/com/alibaba/testable/agent/util/ClassUtil.java b/testable-agent/src/main/java/com/alibaba/testable/agent/util/ClassUtil.java index 10fdb21..bf2594c 100644 --- a/testable-agent/src/main/java/com/alibaba/testable/agent/util/ClassUtil.java +++ b/testable-agent/src/main/java/com/alibaba/testable/agent/util/ClassUtil.java @@ -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 diff --git a/testable-agent/src/test/java/com/alibaba/testable/agent/handler/SourceClassHandlerTest.java b/testable-agent/src/test/java/com/alibaba/testable/agent/handler/SourceClassHandlerTest.java index fce756b..e92ffb4 100644 --- a/testable-agent/src/test/java/com/alibaba/testable/agent/handler/SourceClassHandlerTest.java +++ b/testable-agent/src/test/java/com/alibaba/testable/agent/handler/SourceClassHandlerTest.java @@ -15,7 +15,7 @@ import static org.objectweb.asm.Opcodes.*; class SourceClassHandlerTest { - private final SourceClassHandler handler = new SourceClassHandler(new ArrayList()); + private final SourceClassHandler handler = new SourceClassHandler(new ArrayList(), ""); @Test void should_get_member_method_start() {